package state import ( "context" "crypto" "crypto/ed25519" "crypto/rand" "fmt" "reflect" "testing" "time" db "git.sigsum.org/log-go/pkg/db/mocks" "git.sigsum.org/log-go/pkg/state/mocks" "git.sigsum.org/sigsum-go/pkg/types" "github.com/golang/mock/gomock" ) 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 err error wantErr bool wantSth types.SignedTreeHead }{ { description: "invalid: backend failure", signer: signerOk, err: fmt.Errorf("something went wrong"), wantErr: true, }, { description: "invalid: signer failure", signer: signerErr, rsp: types.TreeHead{}, wantErr: true, }, { description: "valid", signer: signerOk, rsp: types.TreeHead{}, wantSth: types.SignedTreeHead{}, }, } { func() { ctrl := gomock.NewController(t) defer ctrl.Finish() client := db.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.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 TestToCosignTreeHead(t *testing.T) { want := &types.SignedTreeHead{} sm := StateManagerSingle{ signedTreeHead: want, } sth, err := sm.ToCosignTreeHead(context.Background()) if err != nil { t.Errorf("should not fail with error: %v", err) return } if got := sth; !reflect.DeepEqual(got, want) { t.Errorf("got signed tree head\n\t%v\nbut wanted\n\t%v", got, want) } } func TestCosignedTreeHead(t *testing.T) { want := &types.CosignedTreeHead{ Cosignature: make([]types.Signature, 1), KeyHash: make([]types.Hash, 1), } sm := StateManagerSingle{ cosignedTreeHead: want, } cth, err := sm.CosignedTreeHead(context.Background()) if err != nil { t.Errorf("should not fail with error: %v", err) return } if got := cth; !reflect.DeepEqual(got, want) { t.Errorf("got cosigned tree head\n\t%v\nbut wanted\n\t%v", got, want) } sm.cosignedTreeHead = nil cth, err = sm.CosignedTreeHead(context.Background()) if err == nil { t.Errorf("should fail without a cosigned tree head") return } } func TestAddCosignature(t *testing.T) { secret, public := mustKeyPair(t) for _, table := range []struct { desc string signer crypto.Signer vk types.PublicKey wantErr bool }{ { desc: "invalid: wrong public key", signer: secret, vk: types.PublicKey{}, wantErr: true, }, { desc: "valid", signer: secret, vk: public, }, } { sm := &StateManagerSingle{ namespace: *types.HashFn(nil), signedTreeHead: &types.SignedTreeHead{}, events: make(chan *event, 1), } defer close(sm.events) 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.desc, err) } if err != nil { 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 := 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) { 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) 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) } 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) } 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 mustKeyPair(t *testing.T) (crypto.Signer, types.PublicKey) { t.Helper() 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) } return sth }