From f47a82a708dcca6323d75c994cbb422a8e8a9a11 Mon Sep 17 00:00:00 2001 From: Al Cutter Date: Thu, 7 Dec 2023 17:20:03 +0100 Subject: [PATCH] Migrate to CoSigV1 timestamped signatures (#62) --- cmd/internal/distributor/distributor.go | 10 +- cmd/internal/distributor/distributor_test.go | 53 ++++++- cmd/main.go | 10 +- go.mod | 2 +- go.sum | 4 +- internal/note/note_verifier.go | 142 ----------------- internal/note/note_verifier_test.go | 154 ------------------- 7 files changed, 66 insertions(+), 309 deletions(-) delete mode 100644 internal/note/note_verifier.go delete mode 100644 internal/note/note_verifier_test.go diff --git a/cmd/internal/distributor/distributor.go b/cmd/internal/distributor/distributor.go index 9013461..f7fadf3 100644 --- a/cmd/internal/distributor/distributor.go +++ b/cmd/internal/distributor/distributor.go @@ -164,6 +164,14 @@ func (d *Distributor) Distribute(ctx context.Context, logID, witID string, nextR } } + // Remove any unexpected signatures submitted alongside the log+witness we recognised. + n.UnverifiedSigs = nil + nextRaw, err = note.Sign(n) + if err != nil { + return fmt.Errorf("failed to serialise note with filtered sigs: %v", err) + } + glog.V(1).Infof("Accepted: %s", string(nextRaw)) + // At this point we know that we have a valid checkpoint that is fresher than any previous version for // this witness. We should now store this, and then attempt to merge with other checkpoints for the same // log size to create the checkpoint.N files. @@ -283,5 +291,5 @@ func getLatestCheckpoint(ctx context.Context, tx *sql.Tx, logID, witID string) ( // approach is followed to ensure that the DB size stays limited, i.e. don't allow // the same/similar inconsistencies to be written indefinitely. func reportInconsistency(oldCP, newCP []byte) { - glog.Errorf("Found inconsistent checkpoints:\n%v\n\n%v", oldCP, newCP) + glog.Errorf("Found inconsistent checkpoints:\n%v\n\n%v", string(oldCP), string(newCP)) } diff --git a/cmd/internal/distributor/distributor_test.go b/cmd/internal/distributor/distributor_test.go index e5e894b..bc6b45d 100644 --- a/cmd/internal/distributor/distributor_test.go +++ b/cmd/internal/distributor/distributor_test.go @@ -499,6 +499,55 @@ func TestGetCheckpointWitness(t *testing.T) { } } +func TestFiltersUnknownSignatures(t *testing.T) { + ws := map[string]note.Verifier{ + "Aardvark": witAardvark.verifier, + } + ls := map[string]distributor.LogInfo{ + "FooLog": logFoo.LogInfo, + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + db, err := helper.create("TestFiltersUnknownSignatures") + if err != nil { + t.Fatalf("helper.create(): %v", err) + } + d, err := distributor.NewDistributor(ws, ls, db) + if err != nil { + t.Fatalf("NewDistributor(): %v", err) + } + writeCP := logFoo.checkpoint(16, "16", witAardvark.signer, witChameleon.signer) + + // Assert there we're starting with a surplus of signatures + wN, err := note.Open(writeCP, note.VerifierList([]note.Verifier{logFoo.Verifier, witAardvark.verifier, witChameleon.verifier}...)) + if err != nil { + t.Fatalf("Open(writeCP): %v", err) + } + if got, want := len(wN.Sigs), 3; got != want { + t.Errorf("Sanity failure, want 1 log + 2 witness sigs on submitted checkpoint, got %d", got) + } + + // Send checkpoint with "unknown" witness signature to distro + err = d.Distribute(ctx, "FooLog", "Aardvark", writeCP) + if err != nil { + t.Fatalf("Distribute(): %v", err) + } + + // Assert that we get back a checkpoint with only signatures from the log and exptected witness + readCP, err := d.GetCheckpointWitness(ctx, logFoo.Verifier.Name(), witAardvark.verifier.Name()) + if err != nil { + t.Errorf("GetCheckpointWitness: %v", err) + } + rN, err := note.Open(readCP, note.VerifierList([]note.Verifier{logFoo.Verifier, witAardvark.verifier, witChameleon.verifier}...)) + if err != nil { + t.Fatalf("Open(readCP): %v", err) + } + if gotSig, wantSig, gotUnverified, wantUnverified := len(rN.Sigs), 2, len(rN.UnverifiedSigs), 0; gotSig != wantSig || gotUnverified != wantUnverified { + t.Errorf("got %d sigs want %d, got %d unverified sigs want %d:\n%v", gotSig, wantSig, gotUnverified, wantUnverified, string(readCP)) + } +} + func TestGetCheckpointN(t *testing.T) { // The base case for this test is that 2 checkpoints have already been written: // - aardvark, at tree size 16 @@ -805,7 +854,7 @@ type fakeLog struct { signer note.Signer } -func (l fakeLog) checkpoint(size uint64, hashSeed string, wit note.Signer) []byte { +func (l fakeLog) checkpoint(size uint64, hashSeed string, wit ...note.Signer) []byte { hbs := sha256.Sum256([]byte(hashSeed)) rawCP := log.Checkpoint{ Origin: l.Origin, @@ -814,7 +863,7 @@ func (l fakeLog) checkpoint(size uint64, hashSeed string, wit note.Signer) []byt }.Marshal() n := note.Note{} n.Text = string(rawCP) - bs, err := note.Sign(&n, []note.Signer{l.signer, wit}...) + bs, err := note.Sign(&n, append([]note.Signer{l.signer}, wit...)...) if err != nil { panic(err) } diff --git a/cmd/main.go b/cmd/main.go index 93cd471..0ca6eda 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -30,8 +30,8 @@ import ( "github.com/gorilla/mux" "github.com/transparency-dev/distributor/cmd/internal/distributor" ihttp "github.com/transparency-dev/distributor/cmd/internal/http" - i_note "github.com/transparency-dev/distributor/internal/note" "github.com/transparency-dev/formats/log" + f_note "github.com/transparency-dev/formats/note" "golang.org/x/mod/sumdb/note" "golang.org/x/sync/errgroup" "gopkg.in/yaml.v3" @@ -158,7 +158,7 @@ func getLogsOrDie() map[string]distributor.LogInfo { } ls := make(map[string]distributor.LogInfo, len(logsCfg.Logs)) for _, l := range logsCfg.Logs { - lSigV, err := i_note.NewVerifier(l.PublicKeyType, l.PublicKey) + lSigV, err := f_note.NewVerifier(l.PublicKey) if err != nil { glog.Exitf("Invalid log public key: %v", err) } @@ -192,7 +192,7 @@ func getWitnessesOrDie() map[string]note.Verifier { } ws := make(map[string]note.Verifier, len(witCfg.Witnesses)) for _, w := range witCfg.Witnesses { - wSigV, err := note.NewVerifier(w) + wSigV, err := f_note.NewVerifierForCosignatureV1(w) if err != nil { glog.Exitf("Invalid witness public key: %v", err) } @@ -216,10 +216,6 @@ type logConfig struct { ID string `yaml:"ID"` // PublicKey used to verify checkpoints from this log. PublicKey string `yaml:"PublicKey"` - // PublicKeyType identifies the format of the key present in the PublicKey field. - // If unset, the key should be assumed to be in a format which `note.NewVerifier` - // understands. - PublicKeyType string `yaml:"PublicKeyType"` // Origin is the expected first line of checkpoints from the log. Origin string `yaml:"Origin"` // URL is the URL of the root of the log. diff --git a/go.mod b/go.mod index 28d6859..a99ebbf 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/gorilla/mux v1.8.1 github.com/mattn/go-sqlite3 v1.14.18 github.com/ory/dockertest/v3 v3.10.0 - github.com/transparency-dev/formats v0.0.0-20230619083159-fea486e0b437 + github.com/transparency-dev/formats v0.0.0-20231205184308-949529efd6b3 golang.org/x/mod v0.14.0 golang.org/x/sync v0.5.0 google.golang.org/grpc v1.59.0 diff --git a/go.sum b/go.sum index b308acc..354e28a 100644 --- a/go.sum +++ b/go.sum @@ -157,8 +157,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/transparency-dev/formats v0.0.0-20230619083159-fea486e0b437 h1:URBmePD31wqFRk6JyuH1f4zCxizn86EIe54I0y49Mf0= -github.com/transparency-dev/formats v0.0.0-20230619083159-fea486e0b437/go.mod h1:n4WaqmAvPXspZADcvhGOLGIp+S/vrikmArFONkfH2rs= +github.com/transparency-dev/formats v0.0.0-20231205184308-949529efd6b3 h1:Mpx9pqc7bKrx2QQxKL3SPbLIGH4gTBR1ZFrNuKq3CcY= +github.com/transparency-dev/formats v0.0.0-20231205184308-949529efd6b3/go.mod h1:tY9Z9oBaYdQt4NWIhsFAtv0altwLk+K9Gg/2tbS0eBQ= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= diff --git a/internal/note/note_verifier.go b/internal/note/note_verifier.go deleted file mode 100644 index 922ef1a..0000000 --- a/internal/note/note_verifier.go +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright 2021 Google LLC. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package note provides note-compatible signature verifiers. -package note - -import ( - "crypto/ecdsa" - "crypto/sha256" - "crypto/x509" - "encoding/base64" - "encoding/binary" - "fmt" - "strconv" - "strings" - - sdb_note "golang.org/x/mod/sumdb/note" -) - -const ( - // Note represents a key type that the Go SumDB note will - // know about. - Note = "" - - // ECDSA is an ECDSA signature over SHA256. - // This signature type has been agreed to be represented by algo ID 2 by the note authors. - ECDSA = "ecdsa" - - algECDSAWithSHA256 = 2 -) - -// NewVerifier returns a verifier for the given key type and key. -func NewVerifier(keyType, key string) (sdb_note.Verifier, error) { - switch keyType { - case ECDSA: - return NewECDSAVerifier(key) - case Note: - return sdb_note.NewVerifier(key) - default: - return nil, fmt.Errorf("unknown key type %q", keyType) - } -} - -// verifier is a note-compatible verifier. -type verifier struct { - name string - keyHash uint32 - v func(msg, sig []byte) bool -} - -// Name returns the name associated with the key this verifier is based on. -func (v *verifier) Name() string { - return v.name -} - -// KeyHash returns a truncated hash of the key this verifier is based on. -func (v *verifier) KeyHash() uint32 { - return v.keyHash -} - -// Verify checks that the provided sig is valid over msg for the key this verifier is based on. -func (v *verifier) Verify(msg, sig []byte) bool { - return v.v(msg, sig) -} - -// NewECDSAVerifier creates a new note verifier for checking ECDSA signatures over SHA256 digests. -// This implementation is compatible with the signature scheme used by the Sigstore Rékor Log. -// -// The key is expected to be provided as a string in the following form: -// -// ++ -// -// Where -// -// is a human readable identifier for the key, containing no whitespace or "+" symbols -// is base64 encoded blob starting with a 0x02 (algECDSAWithSHA256) byte and followed -// by the DER encoded public key in SPKI format. -// is a 32bit hash of the key DER -// -// e.g.: -// -// "rekor.sigstore.dev+12345678+AjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNhtmPtrWm3U1eQXBogSMdGvXwBcK5AW5i0hrZLOC96l+smGNM7nwZ4QvFK/4sueRoVj//QP22Ni4Qt9DPfkWLc= -func NewECDSAVerifier(key string) (sdb_note.Verifier, error) { - parts := strings.SplitN(key, "+", 3) - if got, want := len(parts), 3; got != want { - return nil, fmt.Errorf("key has %d parts, expected %d: %q", got, want, key) - } - keyBytes, err := base64.StdEncoding.DecodeString(parts[2]) - if err != nil { - return nil, fmt.Errorf("key has invalid base64 %q: %v", parts[2], err) - } - if len(keyBytes) < 2 { - return nil, fmt.Errorf("invalid key, key bytes too short") - } - if keyBytes[0] != algECDSAWithSHA256 { - return nil, fmt.Errorf("key has incorrect type %d", keyBytes[0]) - } - der := keyBytes[1:] - kh := keyHash(der) - - khProvided, err := strconv.ParseUint(parts[1], 16, 32) - if err != nil { - return nil, fmt.Errorf("invalid key, couldn't parse keyhash: %v", err) - } - if uint32(khProvided) != kh { - return nil, fmt.Errorf("invalid keyhash %x, expected %x", khProvided, kh) - } - - k, err := x509.ParsePKIXPublicKey(der) - if err != nil { - return nil, fmt.Errorf("couldn't parse public key: %v", err) - } - ecdsaKey, ok := k.(*ecdsa.PublicKey) - if !ok { - return nil, fmt.Errorf("key is a %T, expected an ECDSA key", k) - } - - return &verifier{ - name: parts[0], - v: func(msg, sig []byte) bool { - dgst := sha256.Sum256(msg) - return ecdsa.VerifyASN1(ecdsaKey, dgst[:], sig) - }, - keyHash: kh, - }, nil -} - -func keyHash(i []byte) uint32 { - h := sha256.Sum256(i) - return binary.BigEndian.Uint32(h[:]) -} diff --git a/internal/note/note_verifier_test.go b/internal/note/note_verifier_test.go deleted file mode 100644 index a6c9c8d..0000000 --- a/internal/note/note_verifier_test.go +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright 2021 Google LLC. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License.package note - -package note - -import ( - "testing" - - "golang.org/x/mod/sumdb/note" -) - -const ( - // These come from the the current SigStore Rekór key, which is an ECDSA key: - sigStoreKeyMaterial = "AjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNhtmPtrWm3U1eQXBogSMdGvXwBcK5AW5i0hrZLOC96l+smGNM7nwZ4QvFK/4sueRoVj//QP22Ni4Qt9DPfkWLc=" - sigStoreKeyHash = "c0d23d6a" - sigStoreKey = "rekor.sigstore.dev" + "+" + sigStoreKeyHash + "+" + sigStoreKeyMaterial - - // These come from the the current Pixel6 log key, which is an ECDSA key. - // KeyMaterial converted from PEM contents here: https://go.dev/play/p/xKGbOGW_JHZ - pixelKeyMaterial = "AjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABN+4x0Jk1yTwvLFI9A4NDdGZcX0aiWVdWM5XJVy0M4VWD3AvyW5Q6Hs9A0mcDkpUoYgn+KKPNzFC0H3nN3q6JQ8=" - pixelKeyHash = "91c16e30" - pixelKey = "pixel6_transparency_log" + "+" + pixelKeyHash + "+" + pixelKeyMaterial -) - -func TestNewVerifier(t *testing.T) { - for _, test := range []struct { - name string - keyType string - key string - wantErr bool - }{ - { - name: "note works", - key: "PeterNeumann+c74f20a3+ARpc2QcUPDhMQegwxbzhKqiBfsVkmqq/LDE4izWy10TW", - }, { - name: "note mismatch", - key: sigStoreKey, - wantErr: true, - }, { - name: "ECDSA works", - keyType: ECDSA, - key: sigStoreKey, - }, { - name: "ECDSA mismatch", - keyType: ECDSA, - key: "PeterNeumann+c74f20a3+ARpc2QcUPDhMQegwxbzhKqiBfsVkmqq/LDE4izWy10TW", - wantErr: true, - }, { - name: "unknown type fails", - keyType: "bananas", - wantErr: true, - }, - } { - t.Run(test.name, func(t *testing.T) { - _, err := NewVerifier(test.keyType, test.key) - if gotErr := err != nil; gotErr != test.wantErr { - t.Fatalf("NewVerifier: %v, wantErr %t", err, test.wantErr) - } - }) - } -} - -func TestNewECDSAVerifier(t *testing.T) { - for _, test := range []struct { - name string - pubK string - wantErr bool - }{ - { - name: "sigStore works", - pubK: sigStoreKey, - }, { - name: "pixel works", - pubK: pixelKey, - }, { - name: "wrong number of parts", - pubK: "bananas.sigstore.dev+12344556", - wantErr: true, - }, { - name: "invalid base64", - pubK: "rekor.sigstore.dev+12345678+THIS_IS_NOT_BASE64!", - wantErr: true, - }, { - name: "invalid algo", - pubK: "rekor.sigstore.dev+12345678+AwEB", - wantErr: true, - }, { - name: "invalid keyhash", - pubK: "rekor.sigstore.dev+NOT_A_NUMBER+" + sigStoreKeyMaterial, - wantErr: true, - }, { - name: "incorrect keyhash", - pubK: "rekor.sigstore.dev" + "+" + "00000000" + "+" + sigStoreKeyMaterial, - wantErr: true, - }, - } { - t.Run(test.name, func(t *testing.T) { - _, err := NewECDSAVerifier(test.pubK) - if gotErr := err != nil; gotErr != test.wantErr { - t.Fatalf("Failed to create new ECDSA verifier from %q: %v", test.pubK, err) - } - }) - } -} -func TestECDSAVerifier(t *testing.T) { - for _, test := range []struct { - name string - pubK string - note []byte - wantErr bool - }{ - { - name: "sigstore works", - pubK: sigStoreKey, - note: []byte("Rekor\n798034\nf+7CoKgXKE/tNys9TTXcr/ad6U/K3xvznmzew9y6SP0=\n\n— rekor.sigstore.dev wNI9ajBEAiARInWIWyCdyG27CO6LPnPekyw20qO0YJfoaPaowGp/XgIgc+qEHS3+GKVClgqq20uDLet7MCoTURUCRdxwWBHHufk=\n"), - }, { - name: "pixel works", - pubK: pixelKey, - note: []byte("DEFAULT\n10\nbsWRucJU5xJPHb5eBdOm6+DM+VelCZBuvtI3sHERJ9Y=\n\n— pixel6_transparency_log kcFuMDBFAiEAhqMAP8P6qf6QxtUJhzMhbN+MbZ9dwfUHzGQJmffJHtoCIGD0cNe47dHWBoPwYdgBCepB06/+g5O1FmYjXl06owL4\n"), - }, { - name: "invalid name", - pubK: "bananas.sigstore.dev" + "+" + sigStoreKeyHash + "+" + sigStoreKeyMaterial, - note: []byte("Rekor\n798034\nf+7CoKgXKE/tNys9TTXcr/ad6U/K3xvznmzew9y6SP0=\n\n— rekor.sigstore.dev wNI9ajBEAiARInWIWyCdyG27CO6LPnPekyw20qO0YJfoaPaowGp/XgIgc+qEHS3+GKVClgqq20uDLet7MCoTURUCRdxwWBHHufk=\n"), - wantErr: true, - }, { - name: "invalid signature", - pubK: sigStoreKey, - note: []byte("Rekor\n798034\nf+7CoKgXKE/tNys9TTXcr/ad6U/K3xvznmzew9y6SP0=\n\n— rekor.sigstore.dev THIS/IS/PROBABLY/NOT/A/VALID/SIGNATURE/ANy/MOREowGp/XgIgc+qEHS3+GKVClgqq20uDLet7MCoTURUCRdxwWBHHufk=\n"), - wantErr: true, - }, - } { - t.Run(test.name, func(t *testing.T) { - v, err := NewECDSAVerifier(test.pubK) - if err != nil { - t.Fatalf("Failed to create new ECDSA verifier from %q: %v", test.pubK, err) - } - _, err = note.Open(test.note, note.VerifierList(v)) - if gotErr := err != nil; gotErr != test.wantErr { - t.Fatalf("Got err %v, but want error %v", err, test.wantErr) - } - }) - } -}