From d45af7c66f637eafe35eadf7b4563b0cc0473d07 Mon Sep 17 00:00:00 2001 From: Andrew Lytvynov Date: Thu, 24 Aug 2023 11:54:42 -0600 Subject: [PATCH] release/dist/cli: add sign-key and verify-key-signature commands (#9041) Now we have all the commands to generate the key hierarchy and verify that signing keys were signed correctly: ``` $ ./tool/go run ./cmd/dist gen-key --priv-path root-priv.pem --pub-path root-pub.pem --root wrote private key to root-priv.pem wrote public key to root-pub.pem $ ./tool/go run ./cmd/dist gen-key --priv-path signing-priv.pem --pub-path signing-pub.pem --signing wrote private key to signing-priv.pem wrote public key to signing-pub.pem $ ./tool/go run ./cmd/dist sign-key --root-priv-path root-priv.pem --sign-pub-path signing-pub.pem wrote signature to signature.bin $ ./tool/go run ./cmd/dist verify-key-signature --root-pub-path root-pub.pem --sign-pub-path signing-pub.pem --sig-path signature.bin signature ok ``` Updates #8760 Signed-off-by: Andrew Lytvynov --- clientupdate/distsign/distsign.go | 28 +++++++--- release/dist/cli/cli.go | 92 +++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 9 deletions(-) diff --git a/clientupdate/distsign/distsign.go b/clientupdate/distsign/distsign.go index 1efe1e5f6..28076c5b6 100644 --- a/clientupdate/distsign/distsign.go +++ b/clientupdate/distsign/distsign.go @@ -98,7 +98,7 @@ func ParseRootKey(privKey []byte) (*RootKey, error) { // SignSigningKeys signs the bundle of public signing keys. The bundle must be // a sequence of PEM blocks joined with newlines. func (r *RootKey) SignSigningKeys(pubBundle []byte) ([]byte, error) { - if _, err := parseSigningKeyBundle(pubBundle); err != nil { + if _, err := ParseSigningKeyBundle(pubBundle); err != nil { return nil, err } return ed25519.Sign(r.k, pubBundle), nil @@ -221,7 +221,7 @@ func (c *Client) Download(srcPath, dstPath string) error { return err } msg := binary.LittleEndian.AppendUint64(hash, uint64(len)) - if !verifyAny(sigPub, msg, sig) { + 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) @@ -248,11 +248,11 @@ func (c *Client) signingKeys() ([]ed25519.PublicKey, error) { if err != nil { return nil, err } - if !verifyAny(c.roots, raw, sig) { + if !VerifyAny(c.roots, raw, sig) { return nil, fmt.Errorf("signature %q for key %q does not validate with any known root key; either you are under attack, or running a very old version of Tailscale with outdated root keys", sigURL, keyURL) } - keys, err := parseSigningKeyBundle(raw) + keys, err := ParseSigningKeyBundle(raw) if err != nil { return nil, fmt.Errorf("cannot parse signing key bundle from %q: %w", keyURL, err) } @@ -315,10 +315,20 @@ func parsePrivateKey(data []byte, typeTag string) (ed25519.PrivateKey, error) { return ed25519.PrivateKey(b.Bytes), nil } -func parseSigningKeyBundle(bundle []byte) ([]ed25519.PublicKey, error) { +// ParseSigningKeyBundle parses the bundle of PEM-encoded public signing keys. +func ParseSigningKeyBundle(bundle []byte) ([]ed25519.PublicKey, error) { + return parsePublicKeyBundle(bundle, pemTypeSigningPublic) +} + +// ParseRootKeyBundle parses the bundle of PEM-encoded public root keys. +func ParseRootKeyBundle(bundle []byte) ([]ed25519.PublicKey, error) { + return parsePublicKeyBundle(bundle, pemTypeRootPublic) +} + +func parsePublicKeyBundle(bundle []byte, typeTag string) ([]ed25519.PublicKey, error) { var keys []ed25519.PublicKey for len(bundle) > 0 { - pub, rest, err := parsePublicKey(bundle, pemTypeSigningPublic) + pub, rest, err := parsePublicKey(bundle, typeTag) if err != nil { return nil, err } @@ -356,9 +366,9 @@ func parsePublicKey(data []byte, typeTag string) (pub ed25519.PublicKey, rest [] return ed25519.PublicKey(b.Bytes), rest, nil } -// verifyAny verifies whether sig is valid for msg using any of the keys. -// verifyAny will panic of any of the keys have the wrong size for Ed25519. -func verifyAny(keys []ed25519.PublicKey, msg, sig []byte) bool { +// VerifyAny verifies whether sig is valid for msg using any of the keys. +// VerifyAny will panic if any of the keys have the wrong size for Ed25519. +func VerifyAny(keys []ed25519.PublicKey, msg, sig []byte) bool { for _, k := range keys { if ed25519consensus.Verify(k, msg, sig) { return true diff --git a/release/dist/cli/cli.go b/release/dist/cli/cli.go index 9329dcd14..42b1b6314 100644 --- a/release/dist/cli/cli.go +++ b/release/dist/cli/cli.go @@ -95,6 +95,36 @@ func CLI(getTargets func(unixpkgs.Signers) ([]dist.Target, error)) *ffcli.Comman return fs })(), }, + { + Name: "sign-key", + Exec: func(ctx context.Context, args []string) error { + return runSignKey(ctx) + }, + ShortUsage: "dist sign-key", + ShortHelp: "Sign signing keys with a root key", + FlagSet: (func() *flag.FlagSet { + fs := flag.NewFlagSet("sign-key", flag.ExitOnError) + fs.StringVar(&signKeyArgs.rootPrivPath, "root-priv-path", "root-private-key.pem", "path to the root private key to sign with") + fs.StringVar(&signKeyArgs.signPubPath, "sign-pub-path", "signing-public-keys.pem", "path to the signing public key bundle to sign; the bundle should include all active signing keys") + fs.StringVar(&signKeyArgs.sigPath, "sig-path", "signature.bin", "oputput path for the signature") + return fs + })(), + }, + { + Name: "verify-key-signature", + Exec: func(ctx context.Context, args []string) error { + return runVerifyKeySignature(ctx) + }, + ShortUsage: "dist verify-key-signature", + ShortHelp: "Verify a root signture of the signing keys' bundle", + FlagSet: (func() *flag.FlagSet { + fs := flag.NewFlagSet("verify-key-signature", flag.ExitOnError) + fs.StringVar(&verifyKeySignatureArgs.rootPubPath, "root-pub-path", "root-public-key.pem", "path to the root public key; this can be a bundle of multiple keys") + fs.StringVar(&verifyKeySignatureArgs.signPubPath, "sign-pub-path", "", "path to the signing public key bundle that was signed") + fs.StringVar(&verifyKeySignatureArgs.sigPath, "sig-path", "signature.bin", "path to the signature file") + return fs + })(), + }, }, Exec: func(context.Context, []string) error { return flag.ErrHelp }, } @@ -224,3 +254,65 @@ func runGenKey(ctx context.Context) error { fmt.Println("wrote public key to", genKeyArgs.pubPath) return nil } + +var signKeyArgs struct { + rootPrivPath string + signPubPath string + sigPath string +} + +func runSignKey(ctx context.Context) error { + rkRaw, err := os.ReadFile(signKeyArgs.rootPrivPath) + if err != nil { + return err + } + rk, err := distsign.ParseRootKey(rkRaw) + if err != nil { + return err + } + + bundle, err := os.ReadFile(signKeyArgs.signPubPath) + if err != nil { + return err + } + sig, err := rk.SignSigningKeys(bundle) + if err != nil { + return err + } + + if err := os.WriteFile(signKeyArgs.sigPath, sig, 0400); err != nil { + return fmt.Errorf("failed writing signature file: %w", err) + } + fmt.Println("wrote signature to", signKeyArgs.sigPath) + return nil +} + +var verifyKeySignatureArgs struct { + rootPubPath string + signPubPath string + sigPath string +} + +func runVerifyKeySignature(ctx context.Context) error { + rootPubBundle, err := os.ReadFile(verifyKeySignatureArgs.rootPubPath) + if err != nil { + return err + } + rootPubs, err := distsign.ParseRootKeyBundle(rootPubBundle) + if err != nil { + return fmt.Errorf("parsing %q: %w", verifyKeySignatureArgs.rootPubPath, err) + } + signPubBundle, err := os.ReadFile(verifyKeySignatureArgs.signPubPath) + if err != nil { + return err + } + sig, err := os.ReadFile(verifyKeySignatureArgs.sigPath) + if err != nil { + return err + } + if !distsign.VerifyAny(rootPubs, signPubBundle, sig) { + return errors.New("signature not valid") + } + fmt.Println("signature ok") + return nil +}