| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585 |
- // Copyright (c) Tailscale Inc & AUTHORS
- // SPDX-License-Identifier: BSD-3-Clause
- package distsign
- import (
- "bytes"
- "context"
- "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", ed25519.Sign(srv.roots[0].k, []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(context.Background(), 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 TestValidateLocalBinary(t *testing.T) {
- srv := newTestServer(t)
- c := srv.client(t)
- tests := []struct {
- desc string
- before func(*testing.T)
- src string
- 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",
- },
- {
- desc: "contents changed",
- before: func(*testing.T) {
- srv.addSigned("hello", []byte("new world"))
- },
- src: "hello",
- wantErr: true,
- },
- {
- 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", ed25519.Sign(srv.roots[0].k, []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()
- // First just do a successful Download.
- want := []byte("world")
- srv.addSigned("hello", want)
- dst := filepath.Join(t.TempDir(), tt.src)
- err := c.Download(context.Background(), tt.src, dst)
- if err != nil {
- t.Fatalf("unexpected error from Download(%q): %v", tt.src, err)
- }
- got, err := os.ReadFile(dst)
- if err != nil {
- t.Fatal(err)
- }
- if !bytes.Equal(want, got) {
- t.Errorf("Download(%q): got %q, want %q", tt.src, got, want)
- }
- // Now we reset srv with the test case and validate against the local dst.
- srv.reset()
- tt.before(t)
- err = c.ValidateLocalBinary(tt.src, dst)
- if err != nil {
- if tt.wantErr {
- return
- }
- t.Fatalf("unexpected error from ValidateLocalBinary(%q): %v", tt.src, err)
- }
- if tt.wantErr {
- t.Fatalf("ValidateLocalBinary(%q) succeeded, expected an error", tt.src)
- }
- })
- }
- }
- func TestRotateRoot(t *testing.T) {
- srv := newTestServer(t)
- c1 := srv.client(t)
- ctx := context.Background()
- srv.addSigned("hello", []byte("world"))
- if err := c1.Download(ctx, "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(ctx, "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(ctx, "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(ctx, "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(ctx, "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)
- ctx := context.Background()
- srv.addSigned("hello", []byte("world"))
- if err := c.Download(ctx, "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(ctx, "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(ctx, "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(ctx, "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(ctx, "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(ctx, "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(ctx, "hello", filepath.Join(t.TempDir(), "hello")); err != nil {
- t.Fatalf("Download failed after publishing new signing key: %v", err)
- }
- }
- func TestParseRootKey(t *testing.T) {
- tests := []struct {
- desc string
- generate func() ([]byte, []byte, error)
- wantErr bool
- }{
- {
- desc: "valid",
- generate: GenerateRootKey,
- },
- {
- desc: "signing",
- generate: GenerateSigningKey,
- wantErr: true,
- },
- {
- desc: "nil",
- generate: func() ([]byte, []byte, error) { return nil, nil, nil },
- wantErr: true,
- },
- {
- desc: "invalid PEM tag",
- generate: func() ([]byte, []byte, error) {
- priv, pub, err := GenerateRootKey()
- priv = bytes.Replace(priv, []byte("ROOT "), nil, -1)
- return priv, pub, err
- },
- wantErr: true,
- },
- {
- desc: "not PEM",
- generate: func() ([]byte, []byte, error) { return []byte("s3cr3t"), nil, nil },
- wantErr: true,
- },
- }
- for _, tt := range tests {
- t.Run(tt.desc, func(t *testing.T) {
- priv, _, err := tt.generate()
- if err != nil {
- t.Fatal(err)
- }
- r, err := ParseRootKey(priv)
- if err != nil {
- if tt.wantErr {
- return
- }
- t.Fatalf("unexpected error: %v", err)
- }
- if tt.wantErr {
- t.Fatal("expected non-nil error")
- }
- if r == nil {
- t.Errorf("got nil error and nil RootKey")
- }
- })
- }
- }
- func TestParseSigningKey(t *testing.T) {
- tests := []struct {
- desc string
- generate func() ([]byte, []byte, error)
- wantErr bool
- }{
- {
- desc: "valid",
- generate: GenerateSigningKey,
- },
- {
- desc: "root",
- generate: GenerateRootKey,
- wantErr: true,
- },
- {
- desc: "nil",
- generate: func() ([]byte, []byte, error) { return nil, nil, nil },
- wantErr: true,
- },
- {
- desc: "invalid PEM tag",
- generate: func() ([]byte, []byte, error) {
- priv, pub, err := GenerateSigningKey()
- priv = bytes.Replace(priv, []byte("SIGNING "), nil, -1)
- return priv, pub, err
- },
- wantErr: true,
- },
- {
- desc: "not PEM",
- generate: func() ([]byte, []byte, error) { return []byte("s3cr3t"), nil, nil },
- wantErr: true,
- },
- }
- for _, tt := range tests {
- t.Run(tt.desc, func(t *testing.T) {
- priv, _, err := tt.generate()
- if err != nil {
- t.Fatal(err)
- }
- r, err := ParseSigningKey(priv)
- if err != nil {
- if tt.wantErr {
- return
- }
- t.Fatalf("unexpected error: %v", err)
- }
- if tt.wantErr {
- t.Fatal("expected non-nil error")
- }
- if r == nil {
- t.Errorf("got nil error and nil SigningKey")
- }
- })
- }
- }
- type testServer struct {
- roots []rootKeyPair
- sign []signingKeyPair
- files map[string][]byte
- srv *httptest.Server
- }
- func newTestServer(t *testing.T) *testServer {
- var roots []rootKeyPair
- for range 3 {
- 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, pemTypeRootPublic)
- 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{
- logf: t.Logf,
- 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 {
- privRaw, pubRaw, err := GenerateRootKey()
- if err != nil {
- t.Fatalf("GenerateRootKey: %v", err)
- }
- kp := keyPair{
- privRaw: privRaw,
- pubRaw: pubRaw,
- }
- priv, err := parsePrivateKey(kp.privRaw, pemTypeRootPrivate)
- if err != nil {
- t.Fatalf("parsePrivateKey: %v", err)
- }
- return rootKeyPair{
- RootKey: &RootKey{k: 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 {
- privRaw, pubRaw, err := GenerateSigningKey()
- if err != nil {
- t.Fatalf("GenerateSigningKey: %v", err)
- }
- kp := keyPair{
- privRaw: privRaw,
- pubRaw: pubRaw,
- }
- priv, err := parsePrivateKey(kp.privRaw, pemTypeSigningPrivate)
- if err != nil {
- t.Fatalf("parsePrivateKey: %v", err)
- }
- return signingKeyPair{
- SigningKey: &SigningKey{k: 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
- }
|