clientupdate/distsign: add new library for package signing/verification (#8943)
This library is intended for use during release to sign packages which are then served from pkgs.tailscale.com. The library is also then used by clients downloading packages for `tailscale update` where OS package managers / app stores aren't used. Updates https://github.com/tailscale/tailscale/issues/8760 Updates https://github.com/tailscale/tailscale/issues/6995 Signed-off-by: Andrew Lytvynov <awly@tailscale.com>main
parent
4b13e6e087
commit
7364c6beec
@ -0,0 +1,338 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package distsign implements signature and validation of arbitrary
|
||||
// distributable files.
|
||||
//
|
||||
// There are 3 parties in this exchange:
|
||||
// - builder, which creates files, signs them with signing keys and publishes
|
||||
// to server
|
||||
// - server, which distributes public signing keys, files and signatures
|
||||
// - client, which downloads files and signatures from server, and validates
|
||||
// the signatures
|
||||
//
|
||||
// There are 2 types of keys:
|
||||
// - signing keys, that sign individual distributable files on the builder
|
||||
// - root keys, that sign signing keys and are kept offline
|
||||
//
|
||||
// root keys -(sign)-> signing keys -(sign)-> files
|
||||
//
|
||||
// All keys are asymmetric Ed25519 key pairs.
|
||||
//
|
||||
// The server serves static files under some known prefix. The kinds of files are:
|
||||
// - distsign.pub - bundle of PEM-encoded public signing keys
|
||||
// - distsign.pub.sig - signature of distsign.pub using one of the root keys
|
||||
// - $file - any distributable file
|
||||
// - $file.sig - signature of $file using any of the signing keys
|
||||
//
|
||||
// The root public keys are baked into the client software at compile time.
|
||||
// These keys are long-lived and prove the validity of current signing keys
|
||||
// from distsign.pub. To rotate root keys, a new client release must be
|
||||
// published, they are not rotated dynamically. There are multiple root keys in
|
||||
// different locations specifically to allow this rotation without using the
|
||||
// discarded root key for any new signatures.
|
||||
//
|
||||
// The signing public keys are fetched by the client dynamically before every
|
||||
// download and can be rotated more readily, assuming that most deployed
|
||||
// clients trust the root keys used to issue fresh signing keys.
|
||||
package distsign |
||||
|
||||
import ( |
||||
"crypto" |
||||
"crypto/ed25519" |
||||
"crypto/rand" |
||||
"encoding/binary" |
||||
"encoding/pem" |
||||
"errors" |
||||
"fmt" |
||||
"hash" |
||||
"io" |
||||
"net/http" |
||||
"net/url" |
||||
"os" |
||||
|
||||
"github.com/hdevalence/ed25519consensus" |
||||
"golang.org/x/crypto/blake2s" |
||||
) |
||||
|
||||
const ( |
||||
pemTypePrivate = "PRIVATE KEY" |
||||
pemTypePublic = "PUBLIC KEY" |
||||
|
||||
downloadSizeLimit = 1 << 29 // 512MB
|
||||
signingKeysSizeLimit = 1 << 20 // 1MB
|
||||
signatureSizeLimit = ed25519.SignatureSize |
||||
) |
||||
|
||||
// GenerateKey generates a new key pair and encodes it as PEM.
|
||||
func GenerateKey() (priv, pub []byte, err error) { |
||||
pub, priv, err = ed25519.GenerateKey(rand.Reader) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
return pem.EncodeToMemory(&pem.Block{ |
||||
Type: pemTypePrivate, |
||||
Bytes: []byte(priv), |
||||
}), pem.EncodeToMemory(&pem.Block{ |
||||
Type: pemTypePublic, |
||||
Bytes: []byte(pub), |
||||
}), nil |
||||
} |
||||
|
||||
// RootKey is a root key Signer used to sign signing keys.
|
||||
type RootKey Signer |
||||
|
||||
// SignSigningKeys signs the bundle of public signing keys. The bundle must be
|
||||
// a sequence of PEM blocks joined with newlines.
|
||||
func (s *RootKey) SignSigningKeys(pubBundle []byte) ([]byte, error) { |
||||
return s.Sign(nil, pubBundle, crypto.Hash(0)) |
||||
} |
||||
|
||||
// SigningKey is a signing key Signer used to sign packages.
|
||||
type SigningKey Signer |
||||
|
||||
// SignPackageHash signs the hash and the length of a package. Use PackageHash
|
||||
// to compute the inputs.
|
||||
func (s SigningKey) SignPackageHash(hash []byte, len int64) ([]byte, error) { |
||||
if len <= 0 { |
||||
return nil, fmt.Errorf("package length must be positive, got %d", len) |
||||
} |
||||
msg := binary.LittleEndian.AppendUint64(hash, uint64(len)) |
||||
return s.Sign(nil, msg, crypto.Hash(0)) |
||||
} |
||||
|
||||
// PackageHash is a hash.Hash that counts the number of bytes written. Use it
|
||||
// to get the hash and length inputs to SigningKey.SignPackageHash.
|
||||
type PackageHash struct { |
||||
hash.Hash |
||||
len int64 |
||||
} |
||||
|
||||
// NewPackageHash returns an initialized PackageHash using BLAKE2s.
|
||||
func NewPackageHash() *PackageHash { |
||||
h, err := blake2s.New256(nil) |
||||
if err != nil { |
||||
// Should never happen with a nil key passed to blake2s.
|
||||
panic(err) |
||||
} |
||||
return &PackageHash{Hash: h} |
||||
} |
||||
|
||||
func (ph *PackageHash) Write(b []byte) (int, error) { |
||||
ph.len += int64(len(b)) |
||||
return ph.Hash.Write(b) |
||||
} |
||||
|
||||
// Reset the PackageHash to its initial state.
|
||||
func (ph *PackageHash) Reset() { |
||||
ph.len = 0 |
||||
ph.Hash.Reset() |
||||
} |
||||
|
||||
// Len returns the total number of bytes written.
|
||||
func (ph *PackageHash) Len() int64 { return ph.len } |
||||
|
||||
// Signer is crypto.Signer using a single key (root or signing).
|
||||
type Signer struct { |
||||
crypto.Signer |
||||
} |
||||
|
||||
// NewSigner parses the PEM-encoded private key stored in the file named
|
||||
// privKeyPath and creates a Signer for it. The key is expected to be in the
|
||||
// same format as returned by GenerateKey.
|
||||
func NewSigner(privKeyPath string) (Signer, error) { |
||||
raw, err := os.ReadFile(privKeyPath) |
||||
if err != nil { |
||||
return Signer{}, err |
||||
} |
||||
k, err := parsePrivateKey(raw) |
||||
if err != nil { |
||||
return Signer{}, fmt.Errorf("failed to parse %q: %w", privKeyPath, err) |
||||
} |
||||
return Signer{Signer: k}, nil |
||||
} |
||||
|
||||
// Client downloads and validates files from a distribution server.
|
||||
type Client struct { |
||||
roots []ed25519.PublicKey |
||||
pkgsAddr *url.URL |
||||
} |
||||
|
||||
// NewClient returns a new client for distribution server located at pkgsAddr,
|
||||
// and uses embedded root keys from the roots/ subdirectory of this package.
|
||||
func NewClient(pkgsAddr string) (*Client, error) { |
||||
u, err := url.Parse(pkgsAddr) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("invalid pkgsAddr %q: %w", pkgsAddr, err) |
||||
} |
||||
return &Client{roots: roots(), pkgsAddr: u}, nil |
||||
} |
||||
|
||||
func (c *Client) url(path string) string { |
||||
return c.pkgsAddr.JoinPath(path).String() |
||||
} |
||||
|
||||
// Download fetches a file at path srcPath from pkgsAddr passed in NewClient.
|
||||
// The file is downloaded to dstPath and its signature is validated using the
|
||||
// embedded root keys. Download returns an error if anything goes wrong with
|
||||
// the actual file download or with signature validation.
|
||||
func (c *Client) Download(srcPath, dstPath string) error { |
||||
// Always fetch a fresh signing key.
|
||||
sigPub, err := c.signingKeys() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
srcURL := c.url(srcPath) |
||||
sigURL := srcURL + ".sig" |
||||
|
||||
dstPathUnverified := dstPath + ".unverified" |
||||
hash, len, err := download(srcURL, dstPathUnverified, downloadSizeLimit) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
sig, err := fetch(sigURL, signatureSizeLimit) |
||||
if err != nil { |
||||
// Best-effort clean up of downloaded package.
|
||||
os.Remove(dstPathUnverified) |
||||
return err |
||||
} |
||||
msg := binary.LittleEndian.AppendUint64(hash, uint64(len)) |
||||
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) |
||||
} |
||||
|
||||
if err := os.Rename(dstPathUnverified, dstPath); err != nil { |
||||
return fmt.Errorf("failed to move %q to %q after signature validation", dstPathUnverified, dstPath) |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// signingKeys fetches current signing keys from the server and validates them
|
||||
// against the roots. Should be called before validation of any downloaded file
|
||||
// to get the fresh keys.
|
||||
func (c *Client) signingKeys() ([]ed25519.PublicKey, error) { |
||||
keyURL := c.url("distsign.pub") |
||||
sigURL := keyURL + ".sig" |
||||
raw, err := fetch(keyURL, signingKeysSizeLimit) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
sig, err := fetch(sigURL, signatureSizeLimit) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
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) |
||||
} |
||||
|
||||
// Parse the bundle of public signing keys.
|
||||
var keys []ed25519.PublicKey |
||||
for len(raw) > 0 { |
||||
pub, rest, err := parsePublicKey(raw) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
keys = append(keys, pub) |
||||
raw = rest |
||||
} |
||||
if len(keys) == 0 { |
||||
return nil, fmt.Errorf("no signing keys found at %q", keyURL) |
||||
} |
||||
return keys, nil |
||||
} |
||||
|
||||
// fetch reads the response body from url into memory, up to limit bytes.
|
||||
func fetch(url string, limit int64) ([]byte, error) { |
||||
resp, err := http.Get(url) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
return io.ReadAll(io.LimitReader(resp.Body, limit)) |
||||
} |
||||
|
||||
// download writes the response body of url into a local file at dst, up to
|
||||
// limit bytes. On success, the returned value is a BLAKE2s hash of the file.
|
||||
func download(url, dst string, limit int64) ([]byte, int64, error) { |
||||
resp, err := http.Get(url) |
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
h := NewPackageHash() |
||||
r := io.TeeReader(io.LimitReader(resp.Body, limit), h) |
||||
|
||||
f, err := os.Create(dst) |
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
defer f.Close() |
||||
|
||||
if _, err := io.Copy(f, r); err != nil { |
||||
return nil, 0, err |
||||
} |
||||
if err := f.Close(); err != nil { |
||||
return nil, 0, err |
||||
} |
||||
|
||||
return h.Sum(nil), h.Len(), nil |
||||
} |
||||
|
||||
func parsePrivateKey(data []byte) (ed25519.PrivateKey, error) { |
||||
b, rest := pem.Decode(data) |
||||
if b == nil { |
||||
return nil, errors.New("failed to decode PEM data") |
||||
} |
||||
if len(rest) > 0 { |
||||
return nil, errors.New("trailing PEM data") |
||||
} |
||||
if b.Type != pemTypePrivate { |
||||
return nil, fmt.Errorf("PEM type is %q, want %q", b.Type, pemTypePrivate) |
||||
} |
||||
if len(b.Bytes) != ed25519.PrivateKeySize { |
||||
return nil, errors.New("private key has incorrect length for an Ed25519 private key") |
||||
} |
||||
return ed25519.PrivateKey(b.Bytes), nil |
||||
} |
||||
|
||||
func parseSinglePublicKey(data []byte) (ed25519.PublicKey, error) { |
||||
pub, rest, err := parsePublicKey(data) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if len(rest) > 0 { |
||||
return nil, errors.New("trailing PEM data") |
||||
} |
||||
return pub, err |
||||
} |
||||
|
||||
func parsePublicKey(data []byte) (pub ed25519.PublicKey, rest []byte, retErr error) { |
||||
b, rest := pem.Decode(data) |
||||
if b == nil { |
||||
return nil, nil, errors.New("failed to decode PEM data") |
||||
} |
||||
if b.Type != pemTypePublic { |
||||
return nil, nil, fmt.Errorf("PEM type is %q, want %q", b.Type, pemTypePublic) |
||||
} |
||||
if len(b.Bytes) != ed25519.PublicKeySize { |
||||
return nil, nil, errors.New("public key has incorrect length for an Ed25519 public key") |
||||
} |
||||
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 { |
||||
for _, k := range keys { |
||||
if ed25519consensus.Verify(k, msg, sig) { |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
@ -0,0 +1,347 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package distsign |
||||
|
||||
import ( |
||||
"bytes" |
||||
"crypto/ed25519" |
||||
"net/http" |
||||
"net/http/httptest" |
||||
"net/url" |
||||
"os" |
||||
"path/filepath" |
||||
"strings" |
||||
"testing" |
||||
|
||||
"golang.org/x/crypto/blake2s" |
||||
) |
||||
|
||||
func TestDownload(t *testing.T) { |
||||
srv := newTestServer(t) |
||||
c := srv.client(t) |
||||
|
||||
tests := []struct { |
||||
desc string |
||||
before func(*testing.T) |
||||
src string |
||||
want []byte |
||||
wantErr bool |
||||
}{ |
||||
{ |
||||
desc: "missing file", |
||||
before: func(*testing.T) {}, |
||||
src: "hello", |
||||
wantErr: true, |
||||
}, |
||||
{ |
||||
desc: "success", |
||||
before: func(*testing.T) { |
||||
srv.addSigned("hello", []byte("world")) |
||||
}, |
||||
src: "hello", |
||||
want: []byte("world"), |
||||
}, |
||||
{ |
||||
desc: "no signature", |
||||
before: func(*testing.T) { |
||||
srv.add("hello", []byte("world")) |
||||
}, |
||||
src: "hello", |
||||
wantErr: true, |
||||
}, |
||||
{ |
||||
desc: "bad signature", |
||||
before: func(*testing.T) { |
||||
srv.add("hello", []byte("world")) |
||||
srv.add("hello.sig", []byte("potato")) |
||||
}, |
||||
src: "hello", |
||||
wantErr: true, |
||||
}, |
||||
{ |
||||
desc: "signed with untrusted key", |
||||
before: func(t *testing.T) { |
||||
srv.add("hello", []byte("world")) |
||||
srv.add("hello.sig", newSigningKeyPair(t).sign([]byte("world"))) |
||||
}, |
||||
src: "hello", |
||||
wantErr: true, |
||||
}, |
||||
{ |
||||
desc: "signed with root key", |
||||
before: func(t *testing.T) { |
||||
srv.add("hello", []byte("world")) |
||||
srv.add("hello.sig", srv.roots[0].sign([]byte("world"))) |
||||
}, |
||||
src: "hello", |
||||
wantErr: true, |
||||
}, |
||||
{ |
||||
desc: "bad signing key signature", |
||||
before: func(t *testing.T) { |
||||
srv.add("distsign.pub.sig", []byte("potato")) |
||||
srv.addSigned("hello", []byte("world")) |
||||
}, |
||||
src: "hello", |
||||
wantErr: true, |
||||
}, |
||||
} |
||||
|
||||
for _, tt := range tests { |
||||
t.Run(tt.desc, func(t *testing.T) { |
||||
srv.reset() |
||||
tt.before(t) |
||||
|
||||
dst := filepath.Join(t.TempDir(), tt.src) |
||||
t.Cleanup(func() { |
||||
os.Remove(dst) |
||||
}) |
||||
err := c.Download(tt.src, dst) |
||||
if err != nil { |
||||
if tt.wantErr { |
||||
return |
||||
} |
||||
t.Fatalf("unexpected error from Download(%q): %v", tt.src, err) |
||||
} |
||||
if tt.wantErr { |
||||
t.Fatalf("Download(%q) succeeded, expected an error", tt.src) |
||||
} |
||||
got, err := os.ReadFile(dst) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if !bytes.Equal(tt.want, got) { |
||||
t.Errorf("Download(%q): got %q, want %q", tt.src, got, tt.want) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestRotateRoot(t *testing.T) { |
||||
srv := newTestServer(t) |
||||
c1 := srv.client(t) |
||||
|
||||
srv.addSigned("hello", []byte("world")) |
||||
if err := c1.Download("hello", filepath.Join(t.TempDir(), "hello")); err != nil { |
||||
t.Fatalf("Download failed on a fresh server: %v", err) |
||||
} |
||||
|
||||
// Remove first root and replace it with a new key.
|
||||
srv.roots = append(srv.roots[1:], newRootKeyPair(t)) |
||||
|
||||
// Old client can still download files because it still trusts the old
|
||||
// root key.
|
||||
if err := c1.Download("hello", filepath.Join(t.TempDir(), "hello")); err != nil { |
||||
t.Fatalf("Download failed after root rotation on old client: %v", err) |
||||
} |
||||
// New client should fail download because current signing key is signed by
|
||||
// the revoked root that new client doesn't trust.
|
||||
c2 := srv.client(t) |
||||
if err := c2.Download("hello", filepath.Join(t.TempDir(), "hello")); err == nil { |
||||
t.Fatalf("Download succeeded on new client, but signing key is signed with revoked root key") |
||||
} |
||||
// Re-sign signing key with another valid root that client still trusts.
|
||||
srv.resignSigningKeys() |
||||
// Both old and new clients should now be able to download.
|
||||
//
|
||||
// Note: we don't need to re-sign the "hello" file because signing key
|
||||
// didn't change (only signing key's signature).
|
||||
if err := c1.Download("hello", filepath.Join(t.TempDir(), "hello")); err != nil { |
||||
t.Fatalf("Download failed after root rotation on old client with re-signed signing key: %v", err) |
||||
} |
||||
if err := c2.Download("hello", filepath.Join(t.TempDir(), "hello")); err != nil { |
||||
t.Fatalf("Download failed after root rotation on new client with re-signed signing key: %v", err) |
||||
} |
||||
} |
||||
|
||||
func TestRotateSigning(t *testing.T) { |
||||
srv := newTestServer(t) |
||||
c := srv.client(t) |
||||
|
||||
srv.addSigned("hello", []byte("world")) |
||||
if err := c.Download("hello", filepath.Join(t.TempDir(), "hello")); err != nil { |
||||
t.Fatalf("Download failed on a fresh server: %v", err) |
||||
} |
||||
|
||||
// Replace signing key but don't publish it yet.
|
||||
srv.sign = append(srv.sign, newSigningKeyPair(t)) |
||||
if err := c.Download("hello", filepath.Join(t.TempDir(), "hello")); err != nil { |
||||
t.Fatalf("Download failed after new signing key added but before publishing it: %v", err) |
||||
} |
||||
|
||||
// Publish new signing key bundle with both keys.
|
||||
srv.resignSigningKeys() |
||||
if err := c.Download("hello", filepath.Join(t.TempDir(), "hello")); err != nil { |
||||
t.Fatalf("Download failed after new signing key was published: %v", err) |
||||
} |
||||
|
||||
// Re-sign the "hello" file with new signing key.
|
||||
srv.add("hello.sig", srv.sign[1].sign([]byte("world"))) |
||||
if err := c.Download("hello", filepath.Join(t.TempDir(), "hello")); err != nil { |
||||
t.Fatalf("Download failed after re-signing with new signing key: %v", err) |
||||
} |
||||
|
||||
// Drop the old signing key.
|
||||
srv.sign = srv.sign[1:] |
||||
srv.resignSigningKeys() |
||||
if err := c.Download("hello", filepath.Join(t.TempDir(), "hello")); err != nil { |
||||
t.Fatalf("Download failed after removing old signing key: %v", err) |
||||
} |
||||
|
||||
// Add another key and re-sign the file with it *before* publishing.
|
||||
srv.sign = append(srv.sign, newSigningKeyPair(t)) |
||||
srv.add("hello.sig", srv.sign[1].sign([]byte("world"))) |
||||
if err := c.Download("hello", filepath.Join(t.TempDir(), "hello")); err == nil { |
||||
t.Fatalf("Download succeeded when signed with a not-yet-published signing key") |
||||
} |
||||
// Fix this by publishing the new key.
|
||||
srv.resignSigningKeys() |
||||
if err := c.Download("hello", filepath.Join(t.TempDir(), "hello")); err != nil { |
||||
t.Fatalf("Download failed after publishing new signing key: %v", err) |
||||
} |
||||
} |
||||
|
||||
type testServer struct { |
||||
roots []rootKeyPair |
||||
sign []signingKeyPair |
||||
files map[string][]byte |
||||
srv *httptest.Server |
||||
} |
||||
|
||||
func newTestServer(t *testing.T) *testServer { |
||||
var roots []rootKeyPair |
||||
for i := 0; i < 3; i++ { |
||||
roots = append(roots, newRootKeyPair(t)) |
||||
} |
||||
|
||||
ts := &testServer{ |
||||
roots: roots, |
||||
sign: []signingKeyPair{newSigningKeyPair(t)}, |
||||
} |
||||
ts.reset() |
||||
ts.srv = httptest.NewServer(ts) |
||||
t.Cleanup(ts.srv.Close) |
||||
return ts |
||||
} |
||||
|
||||
func (s *testServer) client(t *testing.T) *Client { |
||||
roots := make([]ed25519.PublicKey, 0, len(s.roots)) |
||||
for _, r := range s.roots { |
||||
pub, err := parseSinglePublicKey(r.pubRaw) |
||||
if err != nil { |
||||
t.Fatalf("parsePublicKey: %v", err) |
||||
} |
||||
roots = append(roots, pub) |
||||
} |
||||
u, err := url.Parse(s.srv.URL) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
return &Client{ |
||||
roots: roots, |
||||
pkgsAddr: u, |
||||
} |
||||
} |
||||
|
||||
func (s *testServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
||||
path := strings.TrimPrefix(r.URL.Path, "/") |
||||
data, ok := s.files[path] |
||||
if !ok { |
||||
http.NotFound(w, r) |
||||
return |
||||
} |
||||
w.Write(data) |
||||
} |
||||
|
||||
func (s *testServer) addSigned(name string, data []byte) { |
||||
s.files[name] = data |
||||
s.files[name+".sig"] = s.sign[0].sign(data) |
||||
} |
||||
|
||||
func (s *testServer) add(name string, data []byte) { |
||||
s.files[name] = data |
||||
} |
||||
|
||||
func (s *testServer) reset() { |
||||
s.files = make(map[string][]byte) |
||||
s.resignSigningKeys() |
||||
} |
||||
|
||||
func (s *testServer) resignSigningKeys() { |
||||
var pubs [][]byte |
||||
for _, k := range s.sign { |
||||
pubs = append(pubs, k.pubRaw) |
||||
} |
||||
bundle := bytes.Join(pubs, []byte("\n")) |
||||
sig := s.roots[0].sign(bundle) |
||||
s.files["distsign.pub"] = bundle |
||||
s.files["distsign.pub.sig"] = sig |
||||
} |
||||
|
||||
type rootKeyPair struct { |
||||
*RootKey |
||||
keyPair |
||||
} |
||||
|
||||
func newRootKeyPair(t *testing.T) rootKeyPair { |
||||
kp := newKeyPair(t) |
||||
priv, err := parsePrivateKey(kp.privRaw) |
||||
if err != nil { |
||||
t.Fatalf("parsePrivateKey: %v", err) |
||||
} |
||||
return rootKeyPair{ |
||||
RootKey: &RootKey{Signer: priv}, |
||||
keyPair: kp, |
||||
} |
||||
} |
||||
|
||||
func (s rootKeyPair) sign(bundle []byte) []byte { |
||||
sig, err := s.SignSigningKeys(bundle) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
return sig |
||||
} |
||||
|
||||
type signingKeyPair struct { |
||||
*SigningKey |
||||
keyPair |
||||
} |
||||
|
||||
func newSigningKeyPair(t *testing.T) signingKeyPair { |
||||
kp := newKeyPair(t) |
||||
priv, err := parsePrivateKey(kp.privRaw) |
||||
if err != nil { |
||||
t.Fatalf("parsePrivateKey: %v", err) |
||||
} |
||||
return signingKeyPair{ |
||||
SigningKey: &SigningKey{Signer: priv}, |
||||
keyPair: kp, |
||||
} |
||||
} |
||||
|
||||
func (s signingKeyPair) sign(blob []byte) []byte { |
||||
hash := blake2s.Sum256(blob) |
||||
sig, err := s.SignPackageHash(hash[:], int64(len(blob))) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
return sig |
||||
} |
||||
|
||||
type keyPair struct { |
||||
privRaw []byte |
||||
pubRaw []byte |
||||
} |
||||
|
||||
func newKeyPair(t *testing.T) keyPair { |
||||
privRaw, pubRaw, err := GenerateKey() |
||||
if err != nil { |
||||
t.Fatalf("GenerateKey: %v", err) |
||||
} |
||||
return keyPair{ |
||||
privRaw: privRaw, |
||||
pubRaw: pubRaw, |
||||
} |
||||
} |
||||
@ -0,0 +1,54 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package distsign |
||||
|
||||
import ( |
||||
"crypto/ed25519" |
||||
"embed" |
||||
"errors" |
||||
"fmt" |
||||
"path" |
||||
"path/filepath" |
||||
"sync" |
||||
) |
||||
|
||||
//go:embed roots
|
||||
var rootsFS embed.FS |
||||
|
||||
var roots = sync.OnceValue(func() []ed25519.PublicKey { |
||||
roots, err := parseRoots() |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
return roots |
||||
}) |
||||
|
||||
func parseRoots() ([]ed25519.PublicKey, error) { |
||||
files, err := rootsFS.ReadDir("roots") |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
var keys []ed25519.PublicKey |
||||
for _, f := range files { |
||||
if !f.Type().IsRegular() { |
||||
continue |
||||
} |
||||
if filepath.Ext(f.Name()) != ".pub" { |
||||
continue |
||||
} |
||||
raw, err := rootsFS.ReadFile(path.Join("roots", f.Name())) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
key, err := parseSinglePublicKey(raw) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("parsing root key %q: %w", f.Name(), err) |
||||
} |
||||
keys = append(keys, key) |
||||
} |
||||
if len(keys) == 0 { |
||||
return nil, errors.New("no embedded root keys, please check clientupdate/distsign/roots/") |
||||
} |
||||
return keys, nil |
||||
} |
||||
@ -0,0 +1,3 @@ |
||||
-----BEGIN PUBLIC KEY----- |
||||
JNBgo4EFQ+DpRcESM2xU19xQWGffvLcmxtBMT4I+Qo0= |
||||
-----END PUBLIC KEY----- |
||||
@ -0,0 +1,16 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package distsign |
||||
|
||||
import "testing" |
||||
|
||||
func TestParseRoots(t *testing.T) { |
||||
roots, err := parseRoots() |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
if len(roots) == 0 { |
||||
t.Error("parseRoots returned no root keys") |
||||
} |
||||
} |
||||
Loading…
Reference in new issue