diff --git a/cmds/updatemgr/sign.go b/cmds/updatemgr/sign.go new file mode 100644 index 00000000..5f6d8b31 --- /dev/null +++ b/cmds/updatemgr/sign.go @@ -0,0 +1,180 @@ +package main + +import ( + "errors" + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" + + "github.com/safing/jess" + "github.com/safing/jess/filesig" + "github.com/safing/jess/truststores" + "github.com/safing/portmaster/service/updates" +) + +func init() { + rootCmd.AddCommand(signCmd) + + // Required argument: envelope + signCmd.Flags().StringVarP(&envelopeName, "envelope", "", "", + "specify envelope name used for signing", + ) + _ = signCmd.MarkFlagRequired("envelope") + + // Optional arguments: verbose, tsdir, tskeyring + signCmd.Flags().BoolVarP(&signVerbose, "verbose", "v", false, + "enable verbose output", + ) + signCmd.Flags().StringVarP(&trustStoreDir, "tsdir", "", "", + "specify a truststore directory (default loaded from JESS_TS_DIR env variable)", + ) + signCmd.Flags().StringVarP(&trustStoreKeyring, "tskeyring", "", "", + "specify a truststore keyring namespace (default loaded from JESS_TS_KEYRING env variable) - lower priority than tsdir", + ) +} + +var ( + signCmd = &cobra.Command{ + Use: "sign [index.json file]", + Short: "Sign an index", + RunE: sign, + Args: cobra.ExactArgs(1), + } + + envelopeName string + signVerbose bool +) + +func sign(cmd *cobra.Command, args []string) error { + indexFilename := args[0] + + // Setup trust store. + trustStore, err := setupTrustStore() + if err != nil { + return err + } + + // Get envelope. + signingEnvelope, err := trustStore.GetEnvelope(envelopeName) + if err != nil { + return err + } + + // Read index file from disk. + unsignedIndexData, err := os.ReadFile(indexFilename) + if err != nil { + return fmt.Errorf("read index file: %w", err) + } + + // Parse index and check if it is valid. + index, err := updates.ParseIndex(unsignedIndexData, nil) + if err != nil { + return fmt.Errorf("invalid index: %w", err) + } + err = index.CanDoUpgrades() + if err != nil { + return fmt.Errorf("invalid index: %w", err) + } + + // Sign index. + signedIndexData, err := filesig.AddJSONSignature(unsignedIndexData, signingEnvelope, trustStore) + if err != nil { + return fmt.Errorf("sign: %w", err) + } + + // Check by parsing again. + index, err = updates.ParseIndex(signedIndexData, nil) + if err != nil { + return fmt.Errorf("invalid index after signing: %w", err) + } + err = index.CanDoUpgrades() + if err != nil { + return fmt.Errorf("invalid index after signing: %w", err) + } + + // Write back to file. + err = os.WriteFile(indexFilename, signedIndexData, 0o0644) + if err != nil { + return fmt.Errorf("write signed index file: %w", err) + } + + return nil +} + +var ( + trustStoreDir string + trustStoreKeyring string +) + +func setupTrustStore() (trustStore truststores.ExtendedTrustStore, err error) { + // Get trust store directory. + if trustStoreDir == "" { + trustStoreDir, _ = os.LookupEnv("JESS_TS_DIR") + if trustStoreDir == "" { + trustStoreDir, _ = os.LookupEnv("JESS_TSDIR") + } + } + if trustStoreDir != "" { + trustStore, err = truststores.NewDirTrustStore(trustStoreDir) + if err != nil { + return nil, err + } + } + + // Get trust store keyring. + if trustStore == nil { + if trustStoreKeyring == "" { + trustStoreKeyring, _ = os.LookupEnv("JESS_TS_KEYRING") + if trustStoreKeyring == "" { + trustStoreKeyring, _ = os.LookupEnv("JESS_TSKEYRING") + } + } + if trustStoreKeyring != "" { + trustStore, err = truststores.NewKeyringTrustStore(trustStoreKeyring) + if err != nil { + return nil, err + } + } + } + + // Truststore is mandatory. + if trustStore == nil { + return nil, errors.New("no truststore configured, please pass arguments or use env variables") + } + + return trustStore, nil +} + +func getSignedByMany(fds []*filesig.FileData, trustStore jess.TrustStore) string { + signedBy := make([]string, 0, len(fds)) + for _, fd := range fds { + if sig := fd.Signature(); sig != nil { + for _, seal := range sig.Signatures { + if signet, err := trustStore.GetSignet(seal.ID, true); err == nil { + signedBy = append(signedBy, fmt.Sprintf("%s (%s)", signet.Info.Name, seal.ID)) + } else { + signedBy = append(signedBy, seal.ID) + } + } + } + } + return strings.Join(signedBy, " and ") +} + +func getSignedBySingle(fd *filesig.FileData, trustStore jess.TrustStore) string { + if sig := fd.Signature(); sig != nil { + signedBy := make([]string, 0, len(sig.Signatures)) + for _, seal := range sig.Signatures { + if signet, err := trustStore.GetSignet(seal.ID, true); err == nil { + signedBy = append(signedBy, fmt.Sprintf("%s (%s)", signet.Info.Name, seal.ID)) + } else { + signedBy = append(signedBy, seal.ID) + } + } + return strings.Join(signedBy, " and ") + } + + return "" +} diff --git a/go.mod b/go.mod index 9590ae56..a97838af 100644 --- a/go.mod +++ b/go.mod @@ -44,7 +44,7 @@ require ( github.com/oschwald/maxminddb-golang v1.13.1 github.com/r3labs/diff/v3 v3.0.1 github.com/rot256/pblind v0.0.0-20240730113005-f3275049ead5 - github.com/safing/jess v0.3.4 + github.com/safing/jess v0.3.5 github.com/safing/structures v1.1.0 github.com/seehuhn/fortuna v1.0.1 github.com/shirou/gopsutil v3.21.11+incompatible diff --git a/go.sum b/go.sum index fbd91dd0..8f65cf10 100644 --- a/go.sum +++ b/go.sum @@ -250,6 +250,8 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/safing/jess v0.3.4 h1:/p6ensqEUn2jI/z1EB9JUdwH4MJQirh/C9jEwNBzxw8= github.com/safing/jess v0.3.4/go.mod h1:+B6UJnXVxi406Wk08SDnoC5NNBL7t3N0vZGokEbkVQI= +github.com/safing/jess v0.3.5 h1:KS5elTKfWcDUow8SUoCj5QdyyGJNoExJNySerNkbxUU= +github.com/safing/jess v0.3.5/go.mod h1:+B6UJnXVxi406Wk08SDnoC5NNBL7t3N0vZGokEbkVQI= github.com/safing/structures v1.1.0 h1:QzHBQBjaZSLzw2f6PM4ibSmPcfBHAOB5CKJ+k4FYkhQ= github.com/safing/structures v1.1.0/go.mod h1:QUrB74FcU41ahQ5oy3YNFCoSq+twE/n3+vNZc2K35II= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=