Merge pull request #792 from safing/feature/signed-updates

Add support for signed updates
This commit is contained in:
Daniel Hovie
2022-09-29 14:50:14 +02:00
committed by GitHub
18 changed files with 652 additions and 362 deletions

View File

@@ -3,7 +3,6 @@ package main
import (
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"os/user"
@@ -32,7 +31,7 @@ func checkAndCreateInstanceLock(path, name string, perUser bool) (pid int32, err
}
// read current pid file
data, err := ioutil.ReadFile(lockFilePath)
data, err := os.ReadFile(lockFilePath)
if err != nil {
if os.IsNotExist(err) {
// create new lock
@@ -93,7 +92,7 @@ func createInstanceLock(lockFilePath string) error {
// create lock file
// TODO: Investigate required permissions.
err = ioutil.WriteFile(lockFilePath, []byte(fmt.Sprintf("%d", os.Getpid())), 0o0666) //nolint:gosec
err = os.WriteFile(lockFilePath, []byte(fmt.Sprintf("%d", os.Getpid())), 0o0666) //nolint:gosec
if err != nil {
return err
}

View File

@@ -34,8 +34,9 @@ var (
UpdateURLs: []string{
"https://updates.safing.io",
},
DevMode: false,
Online: true, // is disabled later based on command
Verification: helper.VerificationConfig,
DevMode: false,
Online: true, // is disabled later based on command
}
rootCmd = &cobra.Command{

View File

@@ -65,7 +65,7 @@ func downloadUpdates() error {
// logging is configured as a persistent pre-run method inherited from
// the root command but since we don't use run.Run() we need to start
// logging ourself.
log.SetLogLevel(log.TraceLevel)
log.SetLogLevel(log.InfoLevel)
err := log.Start()
if err != nil {
fmt.Printf("failed to start logging: %s\n", err)

View File

@@ -0,0 +1,179 @@
package main
import (
"context"
"errors"
"fmt"
"io/fs"
"log"
"os"
"strings"
"github.com/spf13/cobra"
"github.com/safing/jess"
"github.com/safing/jess/filesig"
portlog "github.com/safing/portbase/log"
"github.com/safing/portbase/updater"
"github.com/safing/portmaster/updates/helper"
)
var (
verifyVerbose bool
verifyFix bool
verifyCmd = &cobra.Command{
Use: "verify",
Short: "Check integrity of updates / components",
RunE: func(cmd *cobra.Command, args []string) error {
return verifyUpdates(cmd.Context())
},
}
)
func init() {
rootCmd.AddCommand(verifyCmd)
flags := verifyCmd.Flags()
flags.BoolVarP(&verifyVerbose, "verbose", "v", false, "Enable verbose output")
flags.BoolVar(&verifyFix, "fix", false, "Delete and re-download broken components")
}
func verifyUpdates(ctx context.Context) error {
// Force registry to require signatures for all enabled scopes.
for _, opts := range registry.Verification {
if opts != nil {
opts.DownloadPolicy = updater.SignaturePolicyRequire
opts.DiskLoadPolicy = updater.SignaturePolicyRequire
}
}
// Load indexes again to ensure they are correctly signed.
err := registry.LoadIndexes(ctx)
if err != nil {
if verifyFix {
log.Println("[WARN] loading indexes failed, re-downloading...")
err = registry.UpdateIndexes(ctx)
if err != nil {
return fmt.Errorf("failed to download indexes: %w", err)
}
log.Println("[ OK ] indexes re-downloaded and verified")
} else {
return fmt.Errorf("failed to verify indexes: %w", err)
}
} else {
log.Println("[ OK ] indexes verified")
}
// Verify all resources.
export := registry.Export()
var verified, fails, skipped int
for _, rv := range export {
for _, version := range rv.Versions {
// Don't verify files we don't have.
if !version.Available {
continue
}
// Verify file signature.
file := version.GetFile()
fileData, err := file.Verify()
switch {
case err == nil:
verified++
if verifyVerbose {
verifOpts := registry.GetVerificationOptions(file.Identifier())
if verifOpts != nil {
log.Printf(
"[ OK ] valid signature for %s: signed by %s",
file.Path(), getSignedByMany(fileData, verifOpts.TrustStore),
)
} else {
log.Printf("[ OK ] valid signature for %s", file.Path())
}
}
case errors.Is(err, updater.ErrVerificationNotConfigured):
skipped++
if verifyVerbose {
log.Printf("[SKIP] no verification configured for %s", file.Path())
}
default:
log.Printf("[FAIL] failed to verify %s: %s", file.Path(), err)
fails++
if verifyFix {
// Delete file.
err = os.Remove(file.Path())
if err != nil && !errors.Is(err, fs.ErrNotExist) {
log.Printf("[FAIL] failed to delete %s to prepare re-download: %s", file.Path(), err)
} else {
// We should not be changing the version, but we are in a cmd-like
// scenario here without goroutines.
version.Available = false
}
// Delete file sig.
err = os.Remove(file.Path() + filesig.Extension)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
log.Printf("[FAIL] failed to delete %s to prepare re-download: %s", file.Path()+filesig.Extension, err)
} else {
// We should not be changing the version, but we are in a cmd-like
// scenario here without goroutines.
version.SigAvailable = false
}
}
}
}
}
if verified > 0 {
log.Printf("[STAT] verified %d files", verified)
}
if skipped > 0 && verifyVerbose {
log.Printf("[STAT] skipped %d files (no verification configured)", skipped)
}
if fails > 0 {
if verifyFix {
log.Printf("[WARN] verification failed on %d files, re-downloading...", fails)
} else {
return fmt.Errorf("failed to verify %d files", fails)
}
} else {
// Everything was verified!
return nil
}
// Start logging system for update process.
portlog.SetLogLevel(portlog.InfoLevel)
err = portlog.Start()
if err != nil {
log.Printf("[WARN] failed to start logging for monitoring update process: %s\n", err)
}
defer portlog.Shutdown()
// Re-download broken files.
registry.MandatoryUpdates = helper.MandatoryUpdates()
registry.AutoUnpack = helper.AutoUnpackUpdates()
err = registry.DownloadUpdates(ctx)
if err != nil {
return fmt.Errorf("failed to re-download files: %w", err)
}
return 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 ")
}

View File

@@ -4,9 +4,9 @@ import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"time"
"github.com/spf13/cobra"
@@ -75,18 +75,26 @@ func release(cmd *cobra.Command, args []string) error {
}
func writeIndex(channel string, versions map[string]string) error {
// Create new index file.
indexFile := &updater.IndexFile{
Channel: channel,
Published: time.Now().UTC().Round(time.Second),
Releases: versions,
}
// Export versions and format them.
versionData, err := json.MarshalIndent(versions, "", " ")
confirmData, err := json.MarshalIndent(indexFile, "", " ")
if err != nil {
return err
}
// Build destination path.
indexFilePath := filepath.Join(registry.StorageDir().Path, channel+".json")
// Build index paths.
oldIndexPath := filepath.Join(registry.StorageDir().Path, channel+".json")
newIndexPath := filepath.Join(registry.StorageDir().Path, channel+".v2.json")
// Print preview.
fmt.Printf("%s (%s):\n", channel, indexFilePath)
fmt.Println(string(versionData))
fmt.Printf("%s\n%s\n%s\n\n", channel, oldIndexPath, newIndexPath)
fmt.Println(string(confirmData))
// Ask for confirmation.
if !confirm("\nDo you want to write this index?") {
@@ -94,13 +102,33 @@ func writeIndex(channel string, versions map[string]string) error {
return nil
}
// Write new index to disk.
err = ioutil.WriteFile(indexFilePath, versionData, 0o0644) //nolint:gosec // 0644 is intended
// Write indexes.
err = writeAsJSON(oldIndexPath, versions)
if err != nil {
return fmt.Errorf("failed to write %s: %w", oldIndexPath, err)
}
err = writeAsJSON(newIndexPath, indexFile)
if err != nil {
return fmt.Errorf("failed to write %s: %w", newIndexPath, err)
}
return nil
}
func writeAsJSON(path string, data any) error {
// Marshal to JSON.
jsonData, err := json.MarshalIndent(data, "", " ")
if err != nil {
return err
}
fmt.Printf("written %s\n", indexFilePath)
// Write to disk.
err = os.WriteFile(path, jsonData, 0o0644) //nolint:gosec
if err != nil {
return err
}
fmt.Printf("written %s\n", path)
return nil
}

302
cmds/updatemgr/sign.go Normal file
View File

@@ -0,0 +1,302 @@
package main
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"github.com/safing/jess"
"github.com/safing/jess/filesig"
"github.com/safing/jess/truststores"
)
func init() {
rootCmd.AddCommand(signCmd)
// Required argument: envelope
signCmd.PersistentFlags().StringVarP(&envelopeName, "envelope", "", "",
"specify envelope name used for signing",
)
_ = signCmd.MarkFlagRequired("envelope")
// Optional arguments: verbose, tsdir, tskeyring
signCmd.PersistentFlags().BoolVarP(&signVerbose, "verbose", "v", false,
"enable verbose output",
)
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",
)
// Subcommand for signing indexes.
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
signVerbose bool
)
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 verified, signed, 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 {
if signVerbose {
fmt.Printf("[ OK ] valid signature for %s: signed by %s\n", file.Path(), getSignedByMany(fileData, trustStore))
}
verified++
}
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))
signed++
}
default:
// File access error.
fmt.Printf("[FAIL] failed to access %s: %s\n", file.Path(), err)
fails++
}
}
}
if verified > 0 {
fmt.Printf("[STAT] verified %d files", verified)
}
if signed > 0 {
fmt.Printf("[STAT] signed %d files", signed)
}
if fails > 0 {
return fmt.Errorf("signing or verification failed on %d files", fails)
}
return nil
}
func signIndex(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
}
// Resolve globs.
files := make([]string, 0, len(args))
for _, arg := range args {
matches, err := filepath.Glob(arg)
if err != nil {
return err
}
files = append(files, matches...)
}
// Go through all files.
var verified, signed, fails int
for _, file := range files {
sigFile := file + filesig.Extension
// Ignore matches for the signatures.
if strings.HasSuffix(file, filesig.Extension) {
continue
}
// Check if there is an existing signature.
_, err := os.Stat(sigFile)
switch {
case err == nil || os.IsExist(err):
// If the file exists, just verify.
fileData, err := filesig.VerifyFile(
file,
sigFile,
nil,
trustStore,
)
if err == nil {
if signVerbose {
fmt.Printf("[ OK ] valid signature for %s: signed by %s\n", file, getSignedByMany(fileData, trustStore))
}
verified++
// Indexes are expected to change, so just sign the index again if verification fails.
continue
}
fallthrough
case os.IsNotExist(err):
// Attempt to sign file.
fileData, err := filesig.SignFile(
file,
sigFile,
nil,
signingEnvelope,
trustStore,
)
if err != nil {
fmt.Printf("[FAIL] failed to sign %s: %s\n", file, err)
fails++
} else {
fmt.Printf("[SIGN] signed %s with %s\n", file, getSignedBySingle(fileData, trustStore))
signed++
}
default:
// File access error.
fmt.Printf("[FAIL] failed to access %s: %s\n", sigFile, err)
fails++
}
}
if verified > 0 {
fmt.Printf("[STAT] verified %d files", verified)
}
if signed > 0 {
fmt.Printf("[STAT] signed %d files", signed)
}
if fails > 0 {
return fmt.Errorf("signing failed on %d files", fails)
}
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 ""
}