diff options
author | Rasmus Dahlberg <rasmus@mullvad.net> | 2021-12-20 19:53:54 +0100 |
---|---|---|
committer | Rasmus Dahlberg <rasmus@mullvad.net> | 2021-12-20 19:53:54 +0100 |
commit | dda238b9fc105219f220f0ec3b341b0c81b71301 (patch) | |
tree | edbbb787ccd1c1816edfa44caf749c8be68b7bf9 /pkg/state | |
parent | 5ba4a77233549819440cc41a02503f3a85213e24 (diff) |
types: Start using sigsum-lib-go
This commit does not change the way in which the log behaves externally.
In other words, all changes are internal and involves renaming and code
restructuring. Most notably picking up the refactored sigsum-lib-go.
Diffstat (limited to 'pkg/state')
-rw-r--r-- | pkg/state/mocks/signer.go | 23 | ||||
-rw-r--r-- | pkg/state/mocks/state_manager.go | 107 | ||||
-rw-r--r-- | pkg/state/single.go | 156 | ||||
-rw-r--r-- | pkg/state/single_test.go (renamed from pkg/state/state_manager_test.go) | 233 | ||||
-rw-r--r-- | pkg/state/state_manager.go | 148 |
5 files changed, 412 insertions, 255 deletions
diff --git a/pkg/state/mocks/signer.go b/pkg/state/mocks/signer.go new file mode 100644 index 0000000..7c699dd --- /dev/null +++ b/pkg/state/mocks/signer.go @@ -0,0 +1,23 @@ +package mocks + +import ( + "crypto" + "crypto/ed25519" + "io" +) + +// TestSign implements the signer interface. It can be used to mock an Ed25519 +// signer that always return the same public key, signature, and error. +type TestSigner struct { + PublicKey [ed25519.PublicKeySize]byte + Signature [ed25519.SignatureSize]byte + Error error +} + +func (ts *TestSigner) Public() crypto.PublicKey { + return ed25519.PublicKey(ts.PublicKey[:]) +} + +func (ts *TestSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + return ts.Signature[:], ts.Error +} diff --git a/pkg/state/mocks/state_manager.go b/pkg/state/mocks/state_manager.go new file mode 100644 index 0000000..009e20f --- /dev/null +++ b/pkg/state/mocks/state_manager.go @@ -0,0 +1,107 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: git.sigsum.org/sigsum-log-go/pkg/state (interfaces: StateManager) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + types "git.sigsum.org/sigsum-lib-go/pkg/types" + gomock "github.com/golang/mock/gomock" +) + +// MockStateManager is a mock of StateManager interface. +type MockStateManager struct { + ctrl *gomock.Controller + recorder *MockStateManagerMockRecorder +} + +// MockStateManagerMockRecorder is the mock recorder for MockStateManager. +type MockStateManagerMockRecorder struct { + mock *MockStateManager +} + +// NewMockStateManager creates a new mock instance. +func NewMockStateManager(ctrl *gomock.Controller) *MockStateManager { + mock := &MockStateManager{ctrl: ctrl} + mock.recorder = &MockStateManagerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockStateManager) EXPECT() *MockStateManagerMockRecorder { + return m.recorder +} + +// AddCosignature mocks base method. +func (m *MockStateManager) AddCosignature(arg0 context.Context, arg1 *types.PublicKey, arg2 *types.Signature) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddCosignature", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddCosignature indicates an expected call of AddCosignature. +func (mr *MockStateManagerMockRecorder) AddCosignature(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddCosignature", reflect.TypeOf((*MockStateManager)(nil).AddCosignature), arg0, arg1, arg2) +} + +// Cosigned mocks base method. +func (m *MockStateManager) Cosigned(arg0 context.Context) (*types.CosignedTreeHead, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Cosigned", arg0) + ret0, _ := ret[0].(*types.CosignedTreeHead) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Cosigned indicates an expected call of Cosigned. +func (mr *MockStateManagerMockRecorder) Cosigned(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Cosigned", reflect.TypeOf((*MockStateManager)(nil).Cosigned), arg0) +} + +// Latest mocks base method. +func (m *MockStateManager) Latest(arg0 context.Context) (*types.SignedTreeHead, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Latest", arg0) + ret0, _ := ret[0].(*types.SignedTreeHead) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Latest indicates an expected call of Latest. +func (mr *MockStateManagerMockRecorder) Latest(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Latest", reflect.TypeOf((*MockStateManager)(nil).Latest), arg0) +} + +// Run mocks base method. +func (m *MockStateManager) Run(arg0 context.Context) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Run", arg0) +} + +// Run indicates an expected call of Run. +func (mr *MockStateManagerMockRecorder) Run(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MockStateManager)(nil).Run), arg0) +} + +// ToSign mocks base method. +func (m *MockStateManager) ToSign(arg0 context.Context) (*types.SignedTreeHead, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ToSign", arg0) + ret0, _ := ret[0].(*types.SignedTreeHead) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ToSign indicates an expected call of ToSign. +func (mr *MockStateManagerMockRecorder) ToSign(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ToSign", reflect.TypeOf((*MockStateManager)(nil).ToSign), arg0) +} diff --git a/pkg/state/single.go b/pkg/state/single.go new file mode 100644 index 0000000..d02b86f --- /dev/null +++ b/pkg/state/single.go @@ -0,0 +1,156 @@ +package state + +import ( + "context" + "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. +type StateManagerSingle struct { + client db.Client + signer crypto.Signer + interval time.Duration + deadline time.Duration + sync.RWMutex + + // 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 + 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, + } + 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 +} + +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) + if err != nil { + glog.Warningf("rotate failed: Latest: %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) + } + + namespace := types.HashFn(sm.signer.Public().(ed25519.PublicKey)) + return th.Sign(sm.signer, namespace) +} + +func (sm *StateManagerSingle) ToSign(_ context.Context) (*types.SignedTreeHead, error) { + sm.RLock() + defer sm.RUnlock() + return &sm.toSign, nil +} + +func (sm *StateManagerSingle) Cosigned(_ context.Context) (*types.CosignedTreeHead, error) { + sm.RLock() + defer sm.RUnlock() + if len(sm.cosigned.Cosignature) == 0 { + return nil, fmt.Errorf("no witness cosignatures available") + } + return &sm.cosigned, nil +} + +func (sm *StateManagerSingle) AddCosignature(_ context.Context, vk *types.PublicKey, sig *types.Signature) error { + 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") + } + + witness := types.HashFn(vk[:]) + if _, ok := sm.cosignatures[*witness]; ok { + return fmt.Errorf("signature-signer pair is a duplicate") // TODO: maybe not an error + } + sm.cosignatures[*witness] = sig + + glog.V(3).Infof("accepted new cosignature from witness: %x", *witness) + return nil +} + +// 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") + } + var cosignatures []types.Signature + var keyHashes []types.Hash + + for keyHash, cosignature := range sm.cosignatures { + cosignatures = append(cosignatures, *cosignature) + keyHashes = append(keyHashes, keyHash) + } + + // Update cosigned tree head + sm.cosigned.SignedTreeHead = sm.toSign + sm.cosigned.Cosignature = cosignatures + sm.cosigned.KeyHash = keyHashes + + // 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") +} diff --git a/pkg/state/state_manager_test.go b/pkg/state/single_test.go index f11ba28..b315b3e 100644 --- a/pkg/state/state_manager_test.go +++ b/pkg/state/single_test.go @@ -11,36 +11,34 @@ import ( "testing" "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" "github.com/golang/mock/gomock" - "git.sigsum.org/sigsum-log-go/pkg/mocks" - "git.sigsum.org/sigsum-log-go/pkg/types" ) var ( - testSig = &[types.SignatureSize]byte{} - testPub = &[types.VerificationKeySize]byte{} - testTH = &types.TreeHead{ + testTH = types.TreeHead{ Timestamp: 0, TreeSize: 0, - RootHash: types.Hash(nil), - KeyHash: types.Hash(testPub[:]), + RootHash: types.Hash{}, } - testSigIdent = &types.SigIdent{ - Signature: testSig, - KeyHash: types.Hash(testPub[:]), + testSTH = types.SignedTreeHead{ + TreeHead: testTH, + Signature: types.Signature{}, } - testSTH = &types.SignedTreeHead{ - TreeHead: *testTH, - Signature: testSig, - } - testCTH = &types.CosignedTreeHead{ - SignedTreeHead: *testSTH, - SigIdent: []*types.SigIdent{ - testSigIdent, + testCTH = types.CosignedTreeHead{ + SignedTreeHead: testSTH, + Cosignature: []types.Signature{ + types.Signature{}, + }, + KeyHash: []types.Hash{ + types.Hash{}, }, } - testSignerOK = &mocks.TestSigner{testPub, testSig, nil} - testSignerErr = &mocks.TestSigner{testPub, testSig, fmt.Errorf("something went wrong")} + + 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) { @@ -61,15 +59,15 @@ func TestNewStateManagerSingle(t *testing.T) { { description: "valid", signer: testSignerOK, - rsp: testTH, - wantSth: testSTH, + rsp: &testTH, + wantSth: &testSTH, }, } { // Run deferred functions at the end of each iteration func() { ctrl := gomock.NewController(t) defer ctrl.Finish() - client := mocks.NewMockClient(ctrl) + 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)) @@ -82,11 +80,11 @@ func TestNewStateManagerSingle(t *testing.T) { 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) + 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.cosignature), 0; got != want { + if got, want := len(sm.cosignatures), 0; got != want { t.Errorf("got %d cosignatures but wanted %d in test %q", got, want, table.description) } }() @@ -110,22 +108,22 @@ func TestLatest(t *testing.T) { }, { description: "invalid: signature failure", - rsp: testTH, + rsp: &testTH, signer: testSignerErr, wantErr: true, }, { description: "valid", signer: testSignerOK, - rsp: testTH, - wantSth: testSTH, + rsp: &testTH, + wantSth: &testSTH, }, } { // Run deferred functions at the end of each iteration func() { ctrl := gomock.NewController(t) defer ctrl.Finish() - client := mocks.NewMockClient(ctrl) + client := mocksTrillian.NewMockClient(ctrl) client.EXPECT().GetTreeHead(gomock.Any()).Return(table.rsp, table.err) sm := StateManagerSingle{ client: client, @@ -149,14 +147,14 @@ func TestLatest(t *testing.T) { func TestToSign(t *testing.T) { description := "valid" sm := StateManagerSingle{ - tosign: *testSTH, + toSign: testSTH, } sth, err := sm.ToSign(context.Background()) if err != nil { t.Errorf("ToSign should not fail with error: %v", err) return } - if got, want := sth, testSTH; !reflect.DeepEqual(got, want) { + 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) } } @@ -164,18 +162,19 @@ func TestToSign(t *testing.T) { func TestCosigned(t *testing.T) { description := "valid" sm := StateManagerSingle{ - cosigned: *testCTH, + cosigned: testCTH, } cth, err := sm.Cosigned(context.Background()) if err != nil { t.Errorf("Cosigned should not fail with error: %v", err) return } - if got, want := cth, testCTH; !reflect.DeepEqual(got, want) { + 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) } - sm.cosigned.SigIdent = make([]*types.SigIdent, 0) + sm.cosigned.Cosignature = make([]types.Signature, 0) + sm.cosigned.KeyHash = make([]types.Hash, 0) cth, err = sm.Cosigned(context.Background()) if err == nil { t.Errorf("Cosigned should fail without witness cosignatures") @@ -188,57 +187,55 @@ func TestAddCosignature(t *testing.T) { if err != nil { t.Fatalf("GenerateKey: %v", err) } - if bytes.Equal(vk[:], testPub[:]) { + if bytes.Equal(vk[:], new(types.PublicKey)[:]) { t.Fatalf("Sampled same key as testPub, aborting...") } - var vkArray [types.VerificationKeySize]byte + var vkArray types.PublicKey copy(vkArray[:], vk[:]) for _, table := range []struct { description string signer crypto.Signer - vk *[types.VerificationKeySize]byte - th *types.TreeHead + vk types.PublicKey + th types.TreeHead wantErr bool }{ { description: "invalid: signature error", signer: sk, - vk: testPub, // wrong key for message + vk: types.PublicKey{}, // wrong key for message th: testTH, wantErr: true, }, { description: "valid", signer: sk, - vk: &vkArray, + vk: vkArray, th: testTH, }, } { - sth, _ := table.th.Sign(testSignerOK) + kh := types.HashFn(testSignerOK.Public().(ed25519.PublicKey)) + sth := mustSign(t, testSignerOK, &table.th, kh) cth := &types.CosignedTreeHead{ SignedTreeHead: *sth, - SigIdent: []*types.SigIdent{}, + Cosignature: make([]types.Signature, 0), + KeyHash: make([]types.Hash, 0), } sm := &StateManagerSingle{ - signer: testSignerOK, - cosigned: *cth, - tosign: *sth, - cosignature: map[[types.HashSize]byte]*types.SigIdent{}, + signer: testSignerOK, + cosigned: *cth, + toSign: *sth, + cosignatures: make(map[types.Hash]*types.Signature, 0), } // Prepare witness signature - sth, err := table.th.Sign(table.signer) - if err != nil { - t.Fatalf("Sign: %v", err) - } - si := &types.SigIdent{ - KeyHash: types.Hash(table.signer.Public().(ed25519.PublicKey)[:]), - Signature: sth.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(), table.vk, si.Signature) + err = sm.AddCosignature(context.Background(), &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) } @@ -247,48 +244,47 @@ func TestAddCosignature(t *testing.T) { } // We should have one witness signature - if got, want := len(sm.cosignature), 1; got != want { + 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 - sigident, ok := sm.cosignature[*si.KeyHash] + sig, ok := sm.cosignatures[*kh] if !ok { t.Errorf("witness signature is missing") continue } - if got, want := si, sigident; !reflect.DeepEqual(got, want) { + 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(), table.vk, si.Signature); err == nil { + if err := sm.AddCosignature(context.Background(), &vk, &sth.Signature); err == nil { t.Errorf("duplicate witness signature accepted as valid") } } } func TestRotate(t *testing.T) { - log := testSigIdent - wit1 := &types.SigIdent{ - Signature: testSig, - KeyHash: types.Hash([]byte("wit1 key")), - } - wit2 := &types.SigIdent{ - Signature: testSig, - KeyHash: types.Hash([]byte("wit2 key")), - } - th0 := testTH + logSig := types.Signature{} + wit1Sig := types.Signature{} + wit2Sig := types.Signature{} + + //logKH := &types.Hash{} + wit1KH := types.HashFn([]byte("wit1 key")) + wit2KH := types.HashFn([]byte("wit2 key")) + + th0 := &testTH th1 := &types.TreeHead{ Timestamp: 1, TreeSize: 1, - RootHash: types.Hash([]byte("1")), + RootHash: *types.HashFn([]byte("1")), } th2 := &types.TreeHead{ Timestamp: 2, TreeSize: 2, - RootHash: types.Hash([]byte("2")), + RootHash: *types.HashFn([]byte("2")), } for _, table := range []struct { @@ -297,77 +293,81 @@ func TestRotate(t *testing.T) { next *types.SignedTreeHead }{ { - description: "tosign tree head repated, but got one new witnes signature", + description: "toSign tree head repated, but got one new witnes signature", before: &StateManagerSingle{ cosigned: types.CosignedTreeHead{ SignedTreeHead: types.SignedTreeHead{ TreeHead: *th0, - Signature: log.Signature, + Signature: logSig, }, - SigIdent: []*types.SigIdent{wit1}, + Cosignature: []types.Signature{wit1Sig}, + KeyHash: []types.Hash{*wit1KH}, }, - tosign: types.SignedTreeHead{ + toSign: types.SignedTreeHead{ TreeHead: *th0, - Signature: log.Signature, + Signature: logSig, }, - cosignature: map[[types.HashSize]byte]*types.SigIdent{ - *wit2.KeyHash: wit2, // the new witness signature + cosignatures: map[types.Hash]*types.Signature{ + *wit2KH: &wit2Sig, // the new witness signature }, }, next: &types.SignedTreeHead{ TreeHead: *th1, - Signature: log.Signature, + Signature: logSig, }, after: &StateManagerSingle{ cosigned: types.CosignedTreeHead{ SignedTreeHead: types.SignedTreeHead{ TreeHead: *th0, - Signature: log.Signature, + Signature: logSig, }, - SigIdent: []*types.SigIdent{wit1, wit2}, + Cosignature: []types.Signature{wit1Sig, wit2Sig}, + KeyHash: []types.Hash{*wit1KH, *wit2KH}, }, - tosign: types.SignedTreeHead{ + toSign: types.SignedTreeHead{ TreeHead: *th1, - Signature: log.Signature, + Signature: logSig, }, - cosignature: map[[types.HashSize]byte]*types.SigIdent{}, + cosignatures: map[types.Hash]*types.Signature{}, }, }, { - description: "tosign tree head did not repeat, it got one witness signature", + description: "toSign tree head did not repeat, it got one witness signature", before: &StateManagerSingle{ cosigned: types.CosignedTreeHead{ SignedTreeHead: types.SignedTreeHead{ TreeHead: *th0, - Signature: log.Signature, + Signature: logSig, }, - SigIdent: []*types.SigIdent{wit1}, + Cosignature: []types.Signature{wit1Sig}, + KeyHash: []types.Hash{*wit1KH}, }, - tosign: types.SignedTreeHead{ + toSign: types.SignedTreeHead{ TreeHead: *th1, - Signature: log.Signature, + Signature: logSig, }, - cosignature: map[[types.HashSize]byte]*types.SigIdent{ - *log.KeyHash: wit2, + cosignatures: map[types.Hash]*types.Signature{ + *wit2KH: &wit2Sig, }, }, next: &types.SignedTreeHead{ TreeHead: *th2, - Signature: log.Signature, + Signature: logSig, }, after: &StateManagerSingle{ cosigned: types.CosignedTreeHead{ SignedTreeHead: types.SignedTreeHead{ TreeHead: *th1, - Signature: log.Signature, + Signature: logSig, }, - SigIdent: []*types.SigIdent{wit2}, + Cosignature: []types.Signature{wit2Sig}, + KeyHash: []types.Hash{*wit2KH}, }, - tosign: types.SignedTreeHead{ + toSign: types.SignedTreeHead{ TreeHead: *th2, - Signature: log.Signature, + Signature: logSig, }, - cosignature: map[[types.HashSize]byte]*types.SigIdent{}, + cosignatures: map[types.Hash]*types.Signature{}, }, }, } { @@ -375,31 +375,44 @@ func TestRotate(t *testing.T) { 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.SigIdent, table.after.cosigned.SigIdent) - 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) + 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.cosignature, table.after.cosignature; !reflect.DeepEqual(got, want) { - t.Errorf("got cosignature map\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) } } } -func checkWitnessList(t *testing.T, description string, got, want []*types.SigIdent) { +func checkWitnessList(t *testing.T, description string, got, want types.CosignedTreeHead) { t.Helper() - for _, si := range got { + 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 _, sj := range want { - if reflect.DeepEqual(si, sj) { + 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, si.KeyHash[:]) + t.Errorf("got unexpected signature-signer pair with key hash in test %q: %x", description, got.KeyHash[i][:]) } } - if len(got) != len(want) { - t.Errorf("got %d signature-signer pairs but wanted %d in test %q", len(got), len(want), description) +} + +func mustSign(t *testing.T, s crypto.Signer, th *types.TreeHead, kh *types.Hash) *types.SignedTreeHead { + sth, err := th.Sign(s, kh) + if err != nil { + t.Fatal(err) } + return sth } diff --git a/pkg/state/state_manager.go b/pkg/state/state_manager.go index 84431be..5aa7609 100644 --- a/pkg/state/state_manager.go +++ b/pkg/state/state_manager.go @@ -2,157 +2,15 @@ package state import ( "context" - "crypto" - "crypto/ed25519" - "fmt" - "reflect" - "sync" - "time" - "github.com/golang/glog" - "github.com/google/certificate-transparency-go/schedule" - "git.sigsum.org/sigsum-log-go/pkg/trillian" - "git.sigsum.org/sigsum-log-go/pkg/types" + "git.sigsum.org/sigsum-lib-go/pkg/types" ) -// StateManager coordinates access to the 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) - AddCosignature(context.Context, *[types.VerificationKeySize]byte, *[types.SignatureSize]byte) error + AddCosignature(context.Context, *types.PublicKey, *types.Signature) error Run(context.Context) } - -// StateManagerSingle implements the StateManager interface. It is assumed that -// the log server is running on a single-instance machine. So, no coordination. -type StateManagerSingle struct { - client trillian.Client - signer crypto.Signer - interval time.Duration - deadline time.Duration - sync.RWMutex - - // 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 - - // cosignature keeps track of all cosignatures for the tosign tree head - cosignature map[[types.HashSize]byte]*types.SigIdent -} - -func NewStateManagerSingle(client trillian.Client, signer crypto.Signer, interval, deadline time.Duration) (*StateManagerSingle, error) { - sm := &StateManagerSingle{ - client: client, - signer: signer, - interval: interval, - deadline: deadline, - } - - ctx, _ := context.WithTimeout(context.Background(), sm.deadline) - sth, err := sm.Latest(ctx) - if err != nil { - return nil, fmt.Errorf("Latest: %v", err) - } - - sm.cosigned = types.CosignedTreeHead{ - SignedTreeHead: *sth, - SigIdent: []*types.SigIdent{}, - } - sm.tosign = *sth - sm.cosignature = map[[types.HashSize]byte]*types.SigIdent{} - return sm, nil -} - -func (sm *StateManagerSingle) Run(ctx context.Context) { - schedule.Every(ctx, sm.interval, func(ctx context.Context) { - ictx, _ := context.WithTimeout(ctx, sm.deadline) - nextSTH, err := sm.Latest(ictx) - if err != nil { - glog.Warningf("rotate failed: Latest: %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) - } - th.KeyHash = types.Hash(sm.signer.Public().(ed25519.PublicKey)[:]) - sth, err := th.Sign(sm.signer) - if err != nil { - return nil, fmt.Errorf("sign: %v", err) - } - return sth, nil -} - -func (sm *StateManagerSingle) ToSign(_ context.Context) (*types.SignedTreeHead, error) { - sm.RLock() - defer sm.RUnlock() - return &sm.tosign, nil -} - -func (sm *StateManagerSingle) Cosigned(_ context.Context) (*types.CosignedTreeHead, error) { - sm.RLock() - defer sm.RUnlock() - if len(sm.cosigned.SigIdent) == 0 { - return nil, fmt.Errorf("no witness cosignatures available") - } - return &sm.cosigned, nil -} - -func (sm *StateManagerSingle) AddCosignature(_ context.Context, vk *[types.VerificationKeySize]byte, sig *[types.SignatureSize]byte) error { - sm.Lock() - defer sm.Unlock() - - if err := sm.tosign.TreeHead.Verify(vk, sig); err != nil { - return fmt.Errorf("Verify: %v", err) - } - witness := types.Hash(vk[:]) - if _, ok := sm.cosignature[*witness]; ok { - return fmt.Errorf("signature-signer pair is a duplicate") - } - sm.cosignature[*witness] = &types.SigIdent{ - Signature: sig, - KeyHash: witness, - } - - glog.V(3).Infof("accepted new cosignature from witness: %x", *witness) - return nil -} - -// 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 _, sigident := range sm.cosigned.SigIdent { - if _, ok := sm.cosignature[*sigident.KeyHash]; !ok { - sm.cosignature[*sigident.KeyHash] = sigident - } - } - glog.V(3).Infof("cosigned tree head repeated, merged signatures") - } - var cosignatures []*types.SigIdent - for _, sigident := range sm.cosignature { - cosignatures = append(cosignatures, sigident) - } - - // Update cosigned tree head - sm.cosigned.SignedTreeHead = sm.tosign - sm.cosigned.SigIdent = cosignatures - - // Update to-sign tree head - sm.tosign = *next - sm.cosignature = map[[types.HashSize]byte]*types.SigIdent{} // TODO: on repeat we might want to not zero this - glog.V(3).Infof("rotated tree heads") -} |