From 72c2f39216d8d8e9bce589271b38c76d16ee2a36 Mon Sep 17 00:00:00 2001 From: Rasmus Dahlberg Date: Tue, 25 Jan 2022 01:04:09 +0100 Subject: state: Refactor with new tree head endpoints For details see doc/proposals/2022-01-no-quick-tree-head-endpoint. --- issues/implement-decided-proposals.md | 3 +- issues/refactor-pkg-state-single.md | 20 -- pkg/state/single.go | 203 +++++++++-------- pkg/state/single_test.go | 413 +++++++++------------------------- pkg/state/state_manager.go | 21 +- 5 files changed, 230 insertions(+), 430 deletions(-) delete mode 100644 issues/refactor-pkg-state-single.md diff --git a/issues/implement-decided-proposals.md b/issues/implement-decided-proposals.md index e3155c9..eecc58b 100644 --- a/issues/implement-decided-proposals.md +++ b/issues/implement-decided-proposals.md @@ -25,5 +25,4 @@ Current status on implementing the above: * [ ] get-\* endpoints * [ ] domain hint * [ ] add-leaf -* [ ] tree-head endpoints - * rgdd is assigned, started on something similar in `rgdd/state` +* [x] tree-head endpoints diff --git a/issues/refactor-pkg-state-single.md b/issues/refactor-pkg-state-single.md deleted file mode 100644 index 78a4e6c..0000000 --- a/issues/refactor-pkg-state-single.md +++ /dev/null @@ -1,20 +0,0 @@ -**Title:** Refactor pkg/state/single.go
-**Date:** 2021-12-20
- -# Summary -Remove unwanted dependencies and resolve minor TODOs in `pkg/state/single.go`. - -# Description -Some refactoring is needed in `pkg/state/single.go`. In more detail, the -following dependencies are overkill and should ideally be removed: -- "github.com/google/certificate-transparency-go/schedule" -- "reflect" - -There are also two TODO prints in the code: -``` -$ git g TODO -single.go -115: return fmt.Errorf("signature-signer pair is a duplicate") // TODO: maybe not an error -154: sm.cosignatures = make(map[types.Hash]*types.Signature, 0) // TODO: on repeat we might want to not zero this -``` - diff --git a/pkg/state/single.go b/pkg/state/single.go index d02b86f..5c69232 100644 --- a/pkg/state/single.go +++ b/pkg/state/single.go @@ -5,152 +5,161 @@ import ( "crypto" "crypto/ed25519" "fmt" - "reflect" "sync" "time" "git.sigsum.org/sigsum-lib-go/pkg/types" "git.sigsum.org/sigsum-log-go/pkg/db" "github.com/golang/glog" - "github.com/google/certificate-transparency-go/schedule" ) -// StateManagerSingle implements a single-instance StateManager. In other -// words, there's no other state that needs to be synced on any remote machine. +// StateManagerSingle implements a single-instance StateManager type StateManagerSingle struct { - client db.Client - signer crypto.Signer - interval time.Duration - deadline time.Duration + client db.Client + signer crypto.Signer + namespace types.Hash + interval time.Duration + deadline time.Duration + + // Lock-protected access to pointers. A write lock is only obtained once + // per interval when doing pointer rotation. All endpoints are readers. sync.RWMutex + signedTreeHead *types.SignedTreeHead + cosignedTreeHead *types.CosignedTreeHead - // cosigned is the current cosigned tree head that is being served - cosigned types.CosignedTreeHead - - // toSign is the current tree head that is being cosigned by witnesses - toSign types.SignedTreeHead - - // cosignatures keep track of all cosignatures for the toSign tree head + // Syncronized and deduplicated witness cosignatures for signedTreeHead + events chan *event cosignatures map[types.Hash]*types.Signature } func NewStateManagerSingle(client db.Client, signer crypto.Signer, interval, deadline time.Duration) (*StateManagerSingle, error) { sm := &StateManagerSingle{ - client: client, - signer: signer, - interval: interval, - deadline: deadline, + client: client, + signer: signer, + namespace: *types.HashFn(signer.Public().(ed25519.PublicKey)), + interval: interval, + deadline: deadline, } - ctx, cancel := context.WithTimeout(context.Background(), sm.deadline) - defer cancel() - - sth, err := sm.Latest(ctx) - if err != nil { - return nil, fmt.Errorf("Latest: %v", err) - } - sm.toSign = *sth - sm.cosignatures = make(map[types.Hash]*types.Signature) - sm.cosigned = types.CosignedTreeHead{ - SignedTreeHead: *sth, - Cosignature: make([]types.Signature, 0), - KeyHash: make([]types.Hash, 0), - } - return sm, nil + sth, err := sm.latestSTH(context.Background()) + sm.setCosignedTreeHead() + sm.setToCosignTreeHead(sth) + return sm, err } func (sm *StateManagerSingle) Run(ctx context.Context) { - schedule.Every(ctx, sm.interval, func(ctx context.Context) { - ictx, cancel := context.WithTimeout(ctx, sm.deadline) - defer cancel() - - nextSTH, err := sm.Latest(ictx) + rotation := func() { + nextSTH, err := sm.latestSTH(ctx) if err != nil { - glog.Warningf("rotate failed: Latest: %v", err) + glog.Warningf("cannot rotate without tree head: %v", err) return } - - sm.Lock() - defer sm.Unlock() sm.rotate(nextSTH) - }) -} - -func (sm *StateManagerSingle) Latest(ctx context.Context) (*types.SignedTreeHead, error) { - th, err := sm.client.GetTreeHead(ctx) - if err != nil { - return nil, fmt.Errorf("LatestTreeHead: %v", err) } + sm.events = make(chan *event, 4096) + defer close(sm.events) + ticker := time.NewTicker(sm.interval) + defer ticker.Stop() + + rotation() + for { + select { + case <-ticker.C: + rotation() + case ev := <-sm.events: + sm.handleEvent(ev) + case <-ctx.Done(): + return + } + } +} - namespace := types.HashFn(sm.signer.Public().(ed25519.PublicKey)) - return th.Sign(sm.signer, namespace) +func (sm *StateManagerSingle) ToCosignTreeHead(_ context.Context) (*types.SignedTreeHead, error) { + sm.RLock() + defer sm.RUnlock() + return sm.signedTreeHead, nil } -func (sm *StateManagerSingle) ToSign(_ context.Context) (*types.SignedTreeHead, error) { +func (sm *StateManagerSingle) CosignedTreeHead(_ context.Context) (*types.CosignedTreeHead, error) { sm.RLock() defer sm.RUnlock() - return &sm.toSign, nil + if sm.cosignedTreeHead == nil { + return nil, fmt.Errorf("no cosignatures available") + } + return sm.cosignedTreeHead, nil } -func (sm *StateManagerSingle) Cosigned(_ context.Context) (*types.CosignedTreeHead, error) { +func (sm *StateManagerSingle) AddCosignature(ctx context.Context, pub *types.PublicKey, sig *types.Signature) error { sm.RLock() defer sm.RUnlock() - if len(sm.cosigned.Cosignature) == 0 { - return nil, fmt.Errorf("no witness cosignatures available") + + msg := sm.signedTreeHead.TreeHead.ToBinary(&sm.namespace) + if !ed25519.Verify(ed25519.PublicKey(pub[:]), msg, sig[:]) { + return fmt.Errorf("invalid cosignature") + } + select { + case sm.events <- &event{types.HashFn(pub[:]), sig}: + return nil + case <-ctx.Done(): + return fmt.Errorf("request timeout") } - return &sm.cosigned, nil } -func (sm *StateManagerSingle) AddCosignature(_ context.Context, vk *types.PublicKey, sig *types.Signature) error { +func (sm *StateManagerSingle) rotate(nextSTH *types.SignedTreeHead) { sm.Lock() defer sm.Unlock() - namespace := types.HashFn(sm.signer.Public().(ed25519.PublicKey)) - msg := sm.toSign.TreeHead.ToBinary(namespace) - if !ed25519.Verify(ed25519.PublicKey(vk[:]), msg, sig[:]) { - return fmt.Errorf("invalid tree head signature") - } + glog.V(3).Infof("rotating tree heads") + sm.handleEvents() + sm.setCosignedTreeHead() + sm.setToCosignTreeHead(nextSTH) +} - witness := types.HashFn(vk[:]) - if _, ok := sm.cosignatures[*witness]; ok { - return fmt.Errorf("signature-signer pair is a duplicate") // TODO: maybe not an error +func (sm *StateManagerSingle) handleEvents() { + glog.V(3).Infof("handling any outstanding events") + for i, n := 0, len(sm.events); i < n; i++ { + sm.handleEvent(<-sm.events) } - sm.cosignatures[*witness] = sig +} - glog.V(3).Infof("accepted new cosignature from witness: %x", *witness) - return nil +func (sm *StateManagerSingle) handleEvent(ev *event) { + glog.V(3).Infof("handling event from witness %x", ev.keyHash[:]) + sm.cosignatures[*ev.keyHash] = ev.cosignature } -// rotate rotates the log's cosigned and stable STH. The caller must aquire the -// source's read-write lock if there are concurrent reads and/or writes. -func (sm *StateManagerSingle) rotate(next *types.SignedTreeHead) { - if reflect.DeepEqual(sm.cosigned.SignedTreeHead, sm.toSign) { - // cosigned and toSign are the same. So, we need to merge all - // cosignatures that we already had with the new collected ones. - for i := 0; i < len(sm.cosigned.Cosignature); i++ { - kh := sm.cosigned.KeyHash[i] - sig := sm.cosigned.Cosignature[i] - - if _, ok := sm.cosignatures[kh]; !ok { - sm.cosignatures[kh] = &sig - } - } - glog.V(3).Infof("cosigned tree head repeated, merged signatures") +func (sm *StateManagerSingle) setCosignedTreeHead() { + n := len(sm.cosignatures) + if n == 0 { + sm.cosignedTreeHead = nil + return } - var cosignatures []types.Signature - var keyHashes []types.Hash + var cth types.CosignedTreeHead + cth.SignedTreeHead = *sm.signedTreeHead + cth.Cosignature = make([]types.Signature, 0, n) + cth.KeyHash = make([]types.Hash, 0, n) for keyHash, cosignature := range sm.cosignatures { - cosignatures = append(cosignatures, *cosignature) - keyHashes = append(keyHashes, keyHash) + cth.KeyHash = append(cth.KeyHash, keyHash) + cth.Cosignature = append(cth.Cosignature, *cosignature) } + sm.cosignedTreeHead = &cth +} - // Update cosigned tree head - sm.cosigned.SignedTreeHead = sm.toSign - sm.cosigned.Cosignature = cosignatures - sm.cosigned.KeyHash = keyHashes +func (sm *StateManagerSingle) setToCosignTreeHead(nextSTH *types.SignedTreeHead) { + sm.cosignatures = make(map[types.Hash]*types.Signature) + sm.signedTreeHead = nextSTH +} + +func (sm *StateManagerSingle) latestSTH(ctx context.Context) (*types.SignedTreeHead, error) { + ictx, cancel := context.WithTimeout(ctx, sm.deadline) + defer cancel() - // Update to-sign tree head - sm.toSign = *next - sm.cosignatures = make(map[types.Hash]*types.Signature, 0) // TODO: on repeat we might want to not zero this - glog.V(3).Infof("rotated tree heads") + th, err := sm.client.GetTreeHead(ictx) + if err != nil { + return nil, fmt.Errorf("failed fetching tree head: %v", err) + } + sth, err := th.Sign(sm.signer, &sm.namespace) + if err != nil { + return nil, fmt.Errorf("failed signing tree head: %v", err) + } + return sth, nil } diff --git a/pkg/state/single_test.go b/pkg/state/single_test.go index b315b3e..f619e2f 100644 --- a/pkg/state/single_test.go +++ b/pkg/state/single_test.go @@ -1,7 +1,6 @@ package state import ( - "bytes" "context" "crypto" "crypto/ed25519" @@ -12,404 +11,204 @@ import ( "time" "git.sigsum.org/sigsum-lib-go/pkg/types" - mocksTrillian "git.sigsum.org/sigsum-log-go/pkg/db/mocks" - mocksSigner "git.sigsum.org/sigsum-log-go/pkg/state/mocks" + db "git.sigsum.org/sigsum-log-go/pkg/db/mocks" + "git.sigsum.org/sigsum-log-go/pkg/state/mocks" "github.com/golang/mock/gomock" ) -var ( - testTH = types.TreeHead{ - Timestamp: 0, - TreeSize: 0, - RootHash: types.Hash{}, - } - testSTH = types.SignedTreeHead{ - TreeHead: testTH, - Signature: types.Signature{}, - } - testCTH = types.CosignedTreeHead{ - SignedTreeHead: testSTH, - Cosignature: []types.Signature{ - types.Signature{}, - }, - KeyHash: []types.Hash{ - types.Hash{}, - }, - } - - testSignerOK = &mocksSigner.TestSigner{types.PublicKey{}, types.Signature{}, nil} - testSignerErr = &mocksSigner.TestSigner{types.PublicKey{}, types.Signature{}, fmt.Errorf("something went wrong")} -) - func TestNewStateManagerSingle(t *testing.T) { + signerOk := &mocks.TestSigner{types.PublicKey{}, types.Signature{}, nil} + signerErr := &mocks.TestSigner{types.PublicKey{}, types.Signature{}, fmt.Errorf("err")} for _, table := range []struct { description string signer crypto.Signer - rsp *types.TreeHead + rsp types.TreeHead err error wantErr bool - wantSth *types.SignedTreeHead + wantSth types.SignedTreeHead }{ { description: "invalid: backend failure", - signer: testSignerOK, + signer: signerOk, err: fmt.Errorf("something went wrong"), wantErr: true, }, { - description: "valid", - signer: testSignerOK, - rsp: &testTH, - wantSth: &testSTH, - }, - } { - // Run deferred functions at the end of each iteration - func() { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - client := mocksTrillian.NewMockClient(ctrl) - client.EXPECT().GetTreeHead(gomock.Any()).Return(table.rsp, table.err) - - sm, err := NewStateManagerSingle(client, table.signer, time.Duration(0), time.Duration(0)) - if got, want := err != nil, table.wantErr; got != want { - t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) - } - if err != nil { - return - } - if got, want := &sm.cosigned.SignedTreeHead, table.wantSth; !reflect.DeepEqual(got, want) { - t.Errorf("got cosigned tree head\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) - } - if got, want := &sm.toSign, table.wantSth; !reflect.DeepEqual(got, want) { - t.Errorf("got toSign tree head\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) - } - // we only have log signature on startup - if got, want := len(sm.cosignatures), 0; got != want { - t.Errorf("got %d cosignatures but wanted %d in test %q", got, want, table.description) - } - }() - } -} - -func TestLatest(t *testing.T) { - for _, table := range []struct { - description string - signer crypto.Signer - rsp *types.TreeHead - err error - wantErr bool - wantSth *types.SignedTreeHead - }{ - { - description: "invalid: backend failure", - signer: testSignerOK, - err: fmt.Errorf("something went wrong"), - wantErr: true, - }, - { - description: "invalid: signature failure", - rsp: &testTH, - signer: testSignerErr, + description: "invalid: signer failure", + signer: signerErr, + rsp: types.TreeHead{}, wantErr: true, }, { description: "valid", - signer: testSignerOK, - rsp: &testTH, - wantSth: &testSTH, + signer: signerOk, + rsp: types.TreeHead{}, + wantSth: types.SignedTreeHead{}, }, } { - // Run deferred functions at the end of each iteration func() { ctrl := gomock.NewController(t) defer ctrl.Finish() - client := mocksTrillian.NewMockClient(ctrl) - client.EXPECT().GetTreeHead(gomock.Any()).Return(table.rsp, table.err) - sm := StateManagerSingle{ - client: client, - signer: table.signer, - } + client := db.NewMockClient(ctrl) + client.EXPECT().GetTreeHead(gomock.Any()).Return(&table.rsp, table.err) - sth, err := sm.Latest(context.Background()) + sm, err := NewStateManagerSingle(client, table.signer, time.Duration(0), time.Duration(0)) if got, want := err != nil, table.wantErr; got != want { t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) } if err != nil { return } - if got, want := sth, table.wantSth; !reflect.DeepEqual(got, want) { - t.Errorf("got signed tree head\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) + if got, want := sm.signedTreeHead, &table.wantSth; !reflect.DeepEqual(got, want) { + t.Errorf("got to-cosign tree head\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) + } + if got := sm.cosignedTreeHead; got != nil { + t.Errorf("got cosigned tree head but should have none in test %q", table.description) } }() } } -func TestToSign(t *testing.T) { - description := "valid" +func TestToCosignTreeHead(t *testing.T) { + want := &types.SignedTreeHead{} sm := StateManagerSingle{ - toSign: testSTH, + signedTreeHead: want, } - sth, err := sm.ToSign(context.Background()) + sth, err := sm.ToCosignTreeHead(context.Background()) if err != nil { - t.Errorf("ToSign should not fail with error: %v", err) + t.Errorf("should not fail with error: %v", err) return } - if got, want := sth, &testSTH; !reflect.DeepEqual(got, want) { - t.Errorf("got signed tree head\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, description) + if got := sth; !reflect.DeepEqual(got, want) { + t.Errorf("got signed tree head\n\t%v\nbut wanted\n\t%v", got, want) } } -func TestCosigned(t *testing.T) { - description := "valid" +func TestCosignedTreeHead(t *testing.T) { + want := &types.CosignedTreeHead{ + Cosignature: make([]types.Signature, 1), + KeyHash: make([]types.Hash, 1), + } sm := StateManagerSingle{ - cosigned: testCTH, + cosignedTreeHead: want, } - cth, err := sm.Cosigned(context.Background()) + cth, err := sm.CosignedTreeHead(context.Background()) if err != nil { - t.Errorf("Cosigned should not fail with error: %v", err) + t.Errorf("should not fail with error: %v", err) return } - if got, want := cth, &testCTH; !reflect.DeepEqual(got, want) { - t.Errorf("got signed tree head\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, description) + if got := cth; !reflect.DeepEqual(got, want) { + t.Errorf("got cosigned tree head\n\t%v\nbut wanted\n\t%v", got, want) } - sm.cosigned.Cosignature = make([]types.Signature, 0) - sm.cosigned.KeyHash = make([]types.Hash, 0) - cth, err = sm.Cosigned(context.Background()) + sm.cosignedTreeHead = nil + cth, err = sm.CosignedTreeHead(context.Background()) if err == nil { - t.Errorf("Cosigned should fail without witness cosignatures") + t.Errorf("should fail without a cosigned tree head") return } } func TestAddCosignature(t *testing.T) { - vk, sk, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - t.Fatalf("GenerateKey: %v", err) - } - if bytes.Equal(vk[:], new(types.PublicKey)[:]) { - t.Fatalf("Sampled same key as testPub, aborting...") - } - var vkArray types.PublicKey - copy(vkArray[:], vk[:]) - + secret, public := mustKeyPair(t) for _, table := range []struct { - description string - signer crypto.Signer - vk types.PublicKey - th types.TreeHead - wantErr bool + desc string + signer crypto.Signer + vk types.PublicKey + wantErr bool }{ { - description: "invalid: signature error", - signer: sk, - vk: types.PublicKey{}, // wrong key for message - th: testTH, - wantErr: true, + desc: "invalid: wrong public key", + signer: secret, + vk: types.PublicKey{}, + wantErr: true, }, { - description: "valid", - signer: sk, - vk: vkArray, - th: testTH, + desc: "valid", + signer: secret, + vk: public, }, } { - kh := types.HashFn(testSignerOK.Public().(ed25519.PublicKey)) - sth := mustSign(t, testSignerOK, &table.th, kh) - cth := &types.CosignedTreeHead{ - SignedTreeHead: *sth, - Cosignature: make([]types.Signature, 0), - KeyHash: make([]types.Hash, 0), - } sm := &StateManagerSingle{ - signer: testSignerOK, - cosigned: *cth, - toSign: *sth, - cosignatures: make(map[types.Hash]*types.Signature, 0), + namespace: *types.HashFn(nil), + signedTreeHead: &types.SignedTreeHead{}, + events: make(chan *event, 1), } + defer close(sm.events) - // Prepare witness signature - var vk types.PublicKey - copy(vk[:], table.vk[:]) //table.signer.Public().(ed25519.PublicKey)) - sth = mustSign(t, table.signer, &table.th, kh) - kh = types.HashFn(vk[:]) - - // Add witness signature - err = sm.AddCosignature(context.Background(), &vk, &sth.Signature) + sth := mustSign(t, table.signer, &sm.signedTreeHead.TreeHead, &sm.namespace) + ctx := context.Background() + err := sm.AddCosignature(ctx, &table.vk, &sth.Signature) if got, want := err != nil, table.wantErr; got != want { - t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) + t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.desc, err) } if err != nil { continue } - // We should have one witness signature - if got, want := len(sm.cosignatures), 1; got != want { - t.Errorf("got %d cosignatures but wanted %v in test %q", got, want, table.description) - continue - } - // check that witness signature is there - sig, ok := sm.cosignatures[*kh] - if !ok { - t.Errorf("witness signature is missing") - continue + ctx, cancel := context.WithTimeout(ctx, 50*time.Millisecond) + defer cancel() + if err := sm.AddCosignature(ctx, &table.vk, &sth.Signature); err == nil { + t.Errorf("expected full channel in test %q", table.desc) } - if got, want := sig[:], sth.Signature[:]; !bytes.Equal(got, want) { - t.Errorf("got witness sigident\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) - continue - } - - // Adding a duplicate signature should give an error - if err := sm.AddCosignature(context.Background(), &vk, &sth.Signature); err == nil { - t.Errorf("duplicate witness signature accepted as valid") + if got, want := len(sm.events), 1; got != want { + t.Errorf("wanted %d cosignatures but got %d in test %q", want, got, table.desc) } } } func TestRotate(t *testing.T) { - logSig := types.Signature{} - wit1Sig := types.Signature{} - wit2Sig := types.Signature{} - - //logKH := &types.Hash{} - wit1KH := types.HashFn([]byte("wit1 key")) - wit2KH := types.HashFn([]byte("wit2 key")) + sth := &types.SignedTreeHead{} + nextSTH := &types.SignedTreeHead{TreeHead: types.TreeHead{Timestamp: 1}} + ev := &event{ + keyHash: &types.Hash{}, + cosignature: &types.Signature{}, + } + wantCTH := &types.CosignedTreeHead{ + SignedTreeHead: *sth, + KeyHash: []types.Hash{*ev.keyHash}, + Cosignature: []types.Signature{*ev.cosignature}, + } + sm := &StateManagerSingle{ + signedTreeHead: sth, + cosignatures: make(map[types.Hash]*types.Signature), + events: make(chan *event, 1), + } + defer close(sm.events) - th0 := &testTH - th1 := &types.TreeHead{ - Timestamp: 1, - TreeSize: 1, - RootHash: *types.HashFn([]byte("1")), + sm.events <- ev + sm.rotate(nextSTH) + if got, want := sm.signedTreeHead, nextSTH; !reflect.DeepEqual(got, want) { + t.Errorf("got to-cosign tree head\n\t%v\nbut wanted\n\t%v", got, want) } - th2 := &types.TreeHead{ - Timestamp: 2, - TreeSize: 2, - RootHash: *types.HashFn([]byte("2")), + if got, want := sm.cosignedTreeHead, wantCTH; !reflect.DeepEqual(got, want) { + t.Errorf("got cosigned tree head\n\t%v\nbut wanted\n\t%v", got, want) } - for _, table := range []struct { - description string - before, after *StateManagerSingle - next *types.SignedTreeHead - }{ - { - description: "toSign tree head repated, but got one new witnes signature", - before: &StateManagerSingle{ - cosigned: types.CosignedTreeHead{ - SignedTreeHead: types.SignedTreeHead{ - TreeHead: *th0, - Signature: logSig, - }, - Cosignature: []types.Signature{wit1Sig}, - KeyHash: []types.Hash{*wit1KH}, - }, - toSign: types.SignedTreeHead{ - TreeHead: *th0, - Signature: logSig, - }, - cosignatures: map[types.Hash]*types.Signature{ - *wit2KH: &wit2Sig, // the new witness signature - }, - }, - next: &types.SignedTreeHead{ - TreeHead: *th1, - Signature: logSig, - }, - after: &StateManagerSingle{ - cosigned: types.CosignedTreeHead{ - SignedTreeHead: types.SignedTreeHead{ - TreeHead: *th0, - Signature: logSig, - }, - Cosignature: []types.Signature{wit1Sig, wit2Sig}, - KeyHash: []types.Hash{*wit1KH, *wit2KH}, - }, - toSign: types.SignedTreeHead{ - TreeHead: *th1, - Signature: logSig, - }, - cosignatures: map[types.Hash]*types.Signature{}, - }, - }, - { - description: "toSign tree head did not repeat, it got one witness signature", - before: &StateManagerSingle{ - cosigned: types.CosignedTreeHead{ - SignedTreeHead: types.SignedTreeHead{ - TreeHead: *th0, - Signature: logSig, - }, - Cosignature: []types.Signature{wit1Sig}, - KeyHash: []types.Hash{*wit1KH}, - }, - toSign: types.SignedTreeHead{ - TreeHead: *th1, - Signature: logSig, - }, - cosignatures: map[types.Hash]*types.Signature{ - *wit2KH: &wit2Sig, - }, - }, - next: &types.SignedTreeHead{ - TreeHead: *th2, - Signature: logSig, - }, - after: &StateManagerSingle{ - cosigned: types.CosignedTreeHead{ - SignedTreeHead: types.SignedTreeHead{ - TreeHead: *th1, - Signature: logSig, - }, - Cosignature: []types.Signature{wit2Sig}, - KeyHash: []types.Hash{*wit2KH}, - }, - toSign: types.SignedTreeHead{ - TreeHead: *th2, - Signature: logSig, - }, - cosignatures: map[types.Hash]*types.Signature{}, - }, - }, - } { - table.before.rotate(table.next) - if got, want := table.before.cosigned.SignedTreeHead, table.after.cosigned.SignedTreeHead; !reflect.DeepEqual(got, want) { - t.Errorf("got cosigned tree head\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) - } - checkWitnessList(t, table.description, table.before.cosigned, table.after.cosigned) - if got, want := table.before.toSign, table.after.toSign; !reflect.DeepEqual(got, want) { - t.Errorf("got toSign tree head\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) - } - if got, want := table.before.cosignatures, table.after.cosignatures; !reflect.DeepEqual(got, want) { - t.Errorf("got cosignatures map\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) - } + sth = nextSTH + nextSTH = &types.SignedTreeHead{TreeHead: types.TreeHead{Timestamp: 2}} + sm.rotate(nextSTH) + if got, want := sm.signedTreeHead, nextSTH; !reflect.DeepEqual(got, want) { + t.Errorf("got to-cosign tree head\n\t%v\nbut wanted\n\t%v", got, want) + } + if got := sm.cosignedTreeHead; got != nil { + t.Errorf("expected no cosignatures to be available") } } -func checkWitnessList(t *testing.T, description string, got, want types.CosignedTreeHead) { +func mustKeyPair(t *testing.T) (crypto.Signer, types.PublicKey) { t.Helper() - if got, want := len(got.Cosignature), len(want.Cosignature); got != want { - t.Errorf("got %d cosignatures but wanted %d in test %q", got, want, description) - return - } - if got, want := len(got.KeyHash), len(want.KeyHash); got != want { - t.Errorf("got %d key hashes but wanted %d in test %q", got, want, description) - return - } - for i := 0; i < len(got.Cosignature); i++ { - found := false - for j := 0; j < len(want.Cosignature); j++ { - if bytes.Equal(got.Cosignature[i][:], want.Cosignature[j][:]) && bytes.Equal(got.KeyHash[i][:], want.KeyHash[j][:]) { - found = true - break - } - } - if !found { - t.Errorf("got unexpected signature-signer pair with key hash in test %q: %x", description, got.KeyHash[i][:]) - } + vk, sk, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) } + var pub types.PublicKey + copy(pub[:], vk[:]) + return sk, pub } func mustSign(t *testing.T, s crypto.Signer, th *types.TreeHead, kh *types.Hash) *types.SignedTreeHead { + t.Helper() sth, err := th.Sign(s, kh) if err != nil { t.Fatal(err) diff --git a/pkg/state/state_manager.go b/pkg/state/state_manager.go index 5aa7609..ab0293c 100644 --- a/pkg/state/state_manager.go +++ b/pkg/state/state_manager.go @@ -6,11 +6,24 @@ import ( "git.sigsum.org/sigsum-lib-go/pkg/types" ) -// StateManager coordinates access to a log's tree heads and (co)signatures +// StateManager coordinates access to a log's tree heads and (co)signatures. type StateManager interface { - Latest(context.Context) (*types.SignedTreeHead, error) - ToSign(context.Context) (*types.SignedTreeHead, error) - Cosigned(context.Context) (*types.CosignedTreeHead, error) + // ToCosignTreeHead returns the log's to-cosign tree head + ToCosignTreeHead(context.Context) (*types.SignedTreeHead, error) + + // CosignedTreeHead returns the log's cosigned tree head + CosignedTreeHead(context.Context) (*types.CosignedTreeHead, error) + + // AddCosignature verifies that a cosignature is valid for the to-cosign + // tree head before adding it AddCosignature(context.Context, *types.PublicKey, *types.Signature) error + + // Run peridically rotates the log's to-cosign and cosigned tree heads Run(context.Context) } + +// event is a verified cosignature request +type event struct { + keyHash *types.Hash + cosignature *types.Signature +} -- cgit v1.2.3