diff --git a/clientupdate/distsign/distsign.go b/clientupdate/distsign/distsign.go index 28076c5b6..26ec292db 100644 --- a/clientupdate/distsign/distsign.go +++ b/clientupdate/distsign/distsign.go @@ -224,7 +224,7 @@ func (c *Client) Download(srcPath, dstPath string) error { if !VerifyAny(sigPub, msg, sig) { // Best-effort clean up of downloaded package. os.Remove(dstPathUnverified) - return fmt.Errorf("signature %q for key %q does not validate with the current release signing key; either you are under attack, or attempting to download an old version of Tailscale which was signed with an older signing key", sigURL, srcURL) + return fmt.Errorf("signature %q for file %q does not validate with the current release signing key; either you are under attack, or attempting to download an old version of Tailscale which was signed with an older signing key", sigURL, srcURL) } if err := os.Rename(dstPathUnverified, dstPath); err != nil { diff --git a/release/dist/cli/cli.go b/release/dist/cli/cli.go index 3ae613a34..d54c23147 100644 --- a/release/dist/cli/cli.go +++ b/release/dist/cli/cli.go @@ -8,10 +8,12 @@ import ( "context" "crypto" "crypto/x509" + "encoding/binary" "encoding/pem" "errors" "flag" "fmt" + "io" "os" "path/filepath" "strings" @@ -119,6 +121,21 @@ func CLI(getTargets func() ([]dist.Target, error)) *ffcli.Command { return fs })(), }, + { + Name: "verify-package-signature", + Exec: func(ctx context.Context, args []string) error { + return runVerifyPackageSignature(ctx) + }, + ShortUsage: "dist verify-package-signature", + ShortHelp: "Verify a package signture using a signing key", + FlagSet: (func() *flag.FlagSet { + fs := flag.NewFlagSet("verify-package-signature", flag.ExitOnError) + fs.StringVar(&verifyPackageSignatureArgs.signPubPath, "sign-pub-path", "signing-public-key.pem", "path to the signing public key; this can be a bundle of multiple keys") + fs.StringVar(&verifyPackageSignatureArgs.packagePath, "package-path", "", "path to the package that was signed") + fs.StringVar(&verifyPackageSignatureArgs.sigPath, "sig-path", "signature.bin", "path to the signature file") + return fs + })(), + }, }, Exec: func(context.Context, []string) error { return flag.ErrHelp }, } @@ -287,19 +304,20 @@ var verifyKeySignatureArgs struct { } func runVerifyKeySignature(ctx context.Context) error { - rootPubBundle, err := os.ReadFile(verifyKeySignatureArgs.rootPubPath) + args := verifyKeySignatureArgs + rootPubBundle, err := os.ReadFile(args.rootPubPath) if err != nil { return err } rootPubs, err := distsign.ParseRootKeyBundle(rootPubBundle) if err != nil { - return fmt.Errorf("parsing %q: %w", verifyKeySignatureArgs.rootPubPath, err) + return fmt.Errorf("parsing %q: %w", args.rootPubPath, err) } - signPubBundle, err := os.ReadFile(verifyKeySignatureArgs.signPubPath) + signPubBundle, err := os.ReadFile(args.signPubPath) if err != nil { return err } - sig, err := os.ReadFile(verifyKeySignatureArgs.sigPath) + sig, err := os.ReadFile(args.sigPath) if err != nil { return err } @@ -309,3 +327,40 @@ func runVerifyKeySignature(ctx context.Context) error { fmt.Println("signature ok") return nil } + +var verifyPackageSignatureArgs struct { + signPubPath string + packagePath string + sigPath string +} + +func runVerifyPackageSignature(ctx context.Context) error { + args := verifyPackageSignatureArgs + signPubBundle, err := os.ReadFile(args.signPubPath) + if err != nil { + return err + } + signPubs, err := distsign.ParseSigningKeyBundle(signPubBundle) + if err != nil { + return fmt.Errorf("parsing %q: %w", args.signPubPath, err) + } + pkg, err := os.Open(args.packagePath) + if err != nil { + return err + } + defer pkg.Close() + pkgHash := distsign.NewPackageHash() + if _, err := io.Copy(pkgHash, pkg); err != nil { + return fmt.Errorf("reading %q: %w", args.packagePath, err) + } + hash := binary.LittleEndian.AppendUint64(pkgHash.Sum(nil), uint64(pkgHash.Len())) + sig, err := os.ReadFile(args.sigPath) + if err != nil { + return err + } + if !distsign.VerifyAny(signPubs, hash, sig) { + return errors.New("signature not valid") + } + fmt.Println("signature ok") + return nil +}