diff --git a/cmds/portmaster-start/main.go b/cmds/portmaster-start/main.go index e0430ef7..96801174 100644 --- a/cmds/portmaster-start/main.go +++ b/cmds/portmaster-start/main.go @@ -34,6 +34,10 @@ var ( UpdateURLs: []string{ "https://updates.safing.io", }, + Verification: helper.VerificationConfig, + // FIXME: Add: + // integrity check: pretty much the same as "jess verify" + // integrity fix: same as check, but delete broken stuff and download again DevMode: false, Online: true, // is disabled later based on command } diff --git a/cmds/updatemgr/sign.go b/cmds/updatemgr/sign.go new file mode 100644 index 00000000..e66b8cfd --- /dev/null +++ b/cmds/updatemgr/sign.go @@ -0,0 +1,268 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" + + "github.com/spf13/cobra" + + "github.com/safing/jess" + "github.com/safing/jess/filesig" + "github.com/safing/jess/truststores" + "github.com/safing/portbase/formats/dsd" + "github.com/safing/portbase/updater" +) + +const letterFileExtension = ".letter" + +func init() { + rootCmd.AddCommand(signCmd) + signCmd.PersistentFlags().StringVarP(&envelopeName, "envelope", "", "", + "specify envelope name used for signing", + ) + _ = signCmd.MarkFlagRequired("envelope") + signCmd.PersistentFlags().StringVarP(&trustStoreDir, "tsdir", "", "", + "specify a truststore directory (default loaded from JESS_TS_DIR env variable)", + ) + signCmd.PersistentFlags().StringVarP(&trustStoreKeyring, "tskeyring", "", "", + "specify a truststore keyring namespace (default loaded from JESS_TS_KEYRING env variable) - lower priority than tsdir", + ) + signCmd.AddCommand(signIndexCmd) +} + +var ( + signCmd = &cobra.Command{ + Use: "sign", + Short: "Sign resources", + RunE: sign, + Args: cobra.NoArgs, + } + signIndexCmd = &cobra.Command{ + Use: "index", + Short: "Sign indexes", + RunE: signIndex, + Args: cobra.ExactArgs(1), + } + + envelopeName string +) + +func sign(cmd *cobra.Command, args []string) error { + // Setup trust store. + trustStore, err := setupTrustStore() + if err != nil { + return err + } + + // Get envelope. + signingEnvelope, err := trustStore.GetEnvelope(envelopeName) + if err != nil { + return err + } + + // Get all resources and iterate over all versions. + export := registry.Export() + var fails int + for _, rv := range export { + for _, version := range rv.Versions { + file := version.GetFile() + + // Check if there is an existing signature. + _, err := os.Stat(file.Path() + filesig.Extension) + switch { + case err == nil || os.IsExist(err): + // If the file exists, just verify. + fileData, err := filesig.VerifyFile( + file.Path(), + file.Path()+filesig.Extension, + file.SigningMetadata(), + trustStore, + ) + if err != nil { + fmt.Printf("[FAIL] signature error for %s: %s\n", file.Path(), err) + fails++ + } else { + fmt.Printf("[ OK ] valid signature for %s: signed by %s\n", file.Path(), getSignedByMany(fileData, trustStore)) + } + + case os.IsNotExist(err): + // Attempt to sign file. + fileData, err := filesig.SignFile( + file.Path(), + file.Path()+filesig.Extension, + file.SigningMetadata(), + signingEnvelope, + trustStore, + ) + if err != nil { + fmt.Printf("[FAIL] failed to sign %s: %s\n", file.Path(), err) + fails++ + } else { + fmt.Printf("[SIGN] signed %s with %s\n", file.Path(), getSignedBySingle(fileData, trustStore)) + } + + default: + // File access error. + fmt.Printf("[FAIL] failed to access %s: %s\n", file.Path(), err) + fails++ + } + } + } + + if fails > 0 { + return fmt.Errorf("signing or checking failed on %d files", fails) + } + return nil +} + +func signIndex(cmd *cobra.Command, args []string) error { + // FIXME: + // Do not sign embedded, but also as a separate file. + // Slightly more complex, but it makes all the other handling easier. + + indexFilePath := 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. + indexData, err := ioutil.ReadFile(indexFilePath) + if err != nil { + return fmt.Errorf("failed to read index file %s: %w", indexFilePath, err) + } + + // Load index. + resourceVersions := make(map[string]string) + err = json.Unmarshal(indexData, &resourceVersions) + if err != nil { + return fmt.Errorf("failed to parse index file: %w", err) + } + + // Create signed index file structure. + index := updater.IndexFile{ + Channel: strings.TrimSuffix(filepath.Base(indexFilePath), filepath.Ext(indexFilePath)), + Published: time.Now(), + Expires: time.Now().Add(3 * 31 * 24 * time.Hour), // Expires in 3 Months. + Versions: resourceVersions, + } + + // Serialize index. + indexData, err = dsd.Dump(index, dsd.CBOR) + if err != nil { + return fmt.Errorf("failed to serialize index structure: %w", err) + } + + // Sign index. + session, err := signingEnvelope.Correspondence(trustStore) + if err != nil { + return fmt.Errorf("failed to prepare signing: %w", err) + } + signedIndex, err := session.Close(indexData) + if err != nil { + return fmt.Errorf("failed to sign: %w", err) + } + + // Write new file. + signedIndexData, err := signedIndex.ToDSD(dsd.CBOR) + if err != nil { + return fmt.Errorf("failed to serialize signed index: %w", err) + } + signedIndexFilePath := strings.TrimSuffix(indexFilePath, filepath.Ext(indexFilePath)) + letterFileExtension + err = ioutil.WriteFile(signedIndexFilePath, signedIndexData, 0o644) //nolint:gosec // Permission is ok. + if err != nil { + return fmt.Errorf("failed to write signed index to %s: %w", signedIndexFilePath, 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/updates/helper/indexes.go b/updates/helper/indexes.go index e925be1a..d0722f34 100644 --- a/updates/helper/indexes.go +++ b/updates/helper/indexes.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" + "github.com/safing/jess/filesig" "github.com/safing/portbase/updater" ) @@ -103,9 +104,17 @@ func indexExists(registry *updater.ResourceRegistry, indexPath string) bool { } func deleteIndex(registry *updater.ResourceRegistry, indexPath string) error { + // Remove index itself. err := os.Remove(filepath.Join(registry.StorageDir().Path, indexPath)) if err != nil && !os.IsNotExist(err) { return err } + + // Remove any accompanying signature. + err = os.Remove(filepath.Join(registry.StorageDir().Path, indexPath+filesig.Extension)) + if err != nil && !os.IsNotExist(err) { + return err + } + return nil } diff --git a/updates/helper/signing.go b/updates/helper/signing.go new file mode 100644 index 00000000..78ccab47 --- /dev/null +++ b/updates/helper/signing.go @@ -0,0 +1,42 @@ +package helper + +import ( + "github.com/safing/jess" + "github.com/safing/portbase/updater" +) + +var ( + // VerificationConfig holds the complete verification configuration for the registry. + VerificationConfig = map[string]*updater.VerificationOptions{ + "": { // Default. + TrustStore: BinarySigningTrustStore, + DownloadPolicy: updater.SignaturePolicyRequire, + DiskLoadPolicy: updater.SignaturePolicyWarn, + }, + "all/intel/": nil, // Disable until IntelHub supports signing. + } + + // BinarySigningKeys holds the signing keys in text format. + BinarySigningKeys = []string{ + // Safing Code Signing Key #1 + "recipient:public-ed25519-key:safing-code-signing-key-1:92bgBLneQUWrhYLPpBDjqHbpFPuNVCPAaivQ951A4aq72HcTiw7R1QmPJwFM1mdePAvEVDjkeb8S4fp2pmRCsRa8HrCvWQEjd88rfZ6TznJMfY4g7P8ioGFjfpyx2ZJ8WCZJG5Qt4Z9nkabhxo2Nbi3iywBTYDLSbP5CXqi7jryW7BufWWuaRVufFFzhwUC2ryWFWMdkUmsAZcvXwde4KLN9FrkWAy61fGaJ8GCwGnGCSitANnU2cQrsGBXZzxmzxwrYD", + // Safing Code Signing Key #2 + "recipient:public-ed25519-key:safing-code-signing-key-2:92bgBLneQUWrhYLPpBDjqHbPC2d1o5JMyZFdavWBNVtdvbPfzDewLW95ScXfYPHd3QvWHSWCtB4xpthaYWxSkK1kYiGp68DPa2HaU8yQ5dZhaAUuV4Kzv42pJcWkCeVnBYqgGBXobuz52rFqhDJy3rz7soXEmYhJEJWwLwMeioK3VzN3QmGSYXXjosHMMNC76rjufSoLNtUQUWZDSnHmqbuxbKMCCsjFXUGGhtZVyb7bnu7QLTLk6SKHBJDMB6zdL9sw3", + } + + // BinarySigningTrustStore is an in-memory trust store with the signing keys. + BinarySigningTrustStore = jess.NewMemTrustStore() +) + +func init() { + for _, signingKey := range BinarySigningKeys { + rcpt, err := jess.RecipientFromTextFormat(signingKey) + if err != nil { + panic(err) + } + err = BinarySigningTrustStore.StoreSignet(rcpt) + if err != nil { + panic(err) + } + } +} diff --git a/updates/main.go b/updates/main.go index 434256cf..87b63b93 100644 --- a/updates/main.go +++ b/updates/main.go @@ -213,6 +213,15 @@ func DisableUpdateSchedule() error { var updateFailedCnt = new(atomic.Int32) func checkForUpdates(ctx context.Context) (err error) { + // Set correct error if context was canceled. + defer func() { + select { + case <-ctx.Done(): + err = context.Canceled + default: + } + }() + if !forceUpdate.SetToIf(true, false) && !enableUpdates() { log.Warningf("updates: automatic updates are disabled") return nil