aboutsummaryrefslogtreecommitdiff
path: root/state
diff options
context:
space:
mode:
authorRasmus Dahlberg <rasmus.dahlberg@kau.se>2021-06-06 11:06:17 +0200
committerRasmus Dahlberg <rasmus.dahlberg@kau.se>2021-06-06 11:06:17 +0200
commit0285454c34b0b3003bc8ede3e304b843ad949be8 (patch)
tree630b89c85defa3aaf8f4cc2037f31dab0f3d2cbd /state
parentebd9efbf32065baa261b2af1625991100ed23bf7 (diff)
added state manager with tests
Diffstat (limited to 'state')
-rw-r--r--state/state_manager.go50
-rw-r--r--state/state_manager_test.go369
2 files changed, 386 insertions, 33 deletions
diff --git a/state/state_manager.go b/state/state_manager.go
index 21c3eda..3199e61 100644
--- a/state/state_manager.go
+++ b/state/state_manager.go
@@ -3,7 +3,6 @@ package stfe
import (
"context"
"crypto"
- "crypto/ed25519"
"fmt"
"reflect"
"sync"
@@ -20,14 +19,14 @@ type StateManager interface {
Latest(context.Context) (*types.SignedTreeHead, error)
ToSign(context.Context) (*types.SignedTreeHead, error)
Cosigned(context.Context) (*types.SignedTreeHead, error)
- AddCosignature(context.Context, ed25519.PublicKey, *[types.SignatureSize]byte) error
+ AddCosignature(context.Context, *[types.VerificationKeySize]byte, *[types.SignatureSize]byte) 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
+ client trillian.Client
signer crypto.Signer
interval time.Duration
deadline time.Duration
@@ -43,7 +42,7 @@ type StateManagerSingle struct {
cosignature map[[types.HashSize]byte]*types.SigIdent
}
-func NewStateManagerSingle(client *trillian.Client, signer crypto.Signer, interval, deadline time.Duration) (*StateManagerSingle, error) {
+func NewStateManagerSingle(client trillian.Client, signer crypto.Signer, interval, deadline time.Duration) (*StateManagerSingle, error) {
sm := &StateManagerSingle{
client: client,
signer: signer,
@@ -59,7 +58,9 @@ func NewStateManagerSingle(client *trillian.Client, signer crypto.Signer, interv
sm.cosigned = *sth
sm.tosign = *sth
- sm.cosignature = make(map[[types.HashSize]byte]*types.SigIdent)
+ sm.cosignature = map[[types.HashSize]byte]*types.SigIdent{
+ *sth.SigIdent[0].KeyHash: sth.SigIdent[0], // log signature
+ }
return sm, nil
}
@@ -83,7 +84,7 @@ func (sm *StateManagerSingle) Latest(ctx context.Context) (*types.SignedTreeHead
if err != nil {
return nil, fmt.Errorf("LatestTreeHead: %v", err)
}
- sth, err := sign(sm.signer, th)
+ sth, err := th.Sign(sm.signer)
if err != nil {
return nil, fmt.Errorf("sign: %v", err)
}
@@ -102,12 +103,12 @@ func (sm *StateManagerSingle) Cosigned(_ context.Context) (*types.SignedTreeHead
return &sm.cosigned, nil
}
-func (sm *StateManagerSingle) AddCosignature(_ context.Context, vk ed25519.PublicKey, sig *[types.SignatureSize]byte) error {
+func (sm *StateManagerSingle) AddCosignature(_ context.Context, vk *[types.VerificationKeySize]byte, sig *[types.SignatureSize]byte) error {
sm.Lock()
defer sm.Unlock()
- if msg := sm.tosign.TreeHead.Marshal(); !ed25519.Verify(vk, msg, sig[:]) {
- return fmt.Errorf("invalid signature for tree head with timestamp: %d", sm.tosign.Timestamp)
+ 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 {
@@ -126,13 +127,15 @@ func (sm *StateManagerSingle) AddCosignature(_ context.Context, vk ed25519.Publi
// 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.TreeHead, sm.tosign.TreeHead) {
- for _, sigident := range sm.cosigned.SigIdent[1:] { // skip log sigident
+ // 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")
}
- // cosignatures will contain all cosignatures (even if repeated tree head)
var cosignatures []*types.SigIdent
for _, sigident := range sm.cosignature {
cosignatures = append(cosignatures, sigident)
@@ -140,29 +143,12 @@ func (sm *StateManagerSingle) rotate(next *types.SignedTreeHead) {
// Update cosigned tree head
sm.cosigned.TreeHead = sm.tosign.TreeHead
- sm.cosigned.SigIdent = append(sm.tosign.SigIdent, cosignatures...)
+ sm.cosigned.SigIdent = cosignatures
// Update to-sign tree head
sm.tosign = *next
- sm.cosignature = make(map[[types.HashSize]byte]*types.SigIdent)
- glog.V(3).Infof("rotated sth")
-}
-
-func sign(signer crypto.Signer, th *types.TreeHead) (*types.SignedTreeHead, error) {
- sig, err := signer.Sign(nil, th.Marshal(), crypto.Hash(0))
- if err != nil {
- return nil, fmt.Errorf("Sign: %v", err)
- }
-
- sigident := types.SigIdent{
- KeyHash: types.Hash(signer.Public().(ed25519.PublicKey)[:]),
- Signature: &[types.SignatureSize]byte{},
+ sm.cosignature = map[[types.HashSize]byte]*types.SigIdent{
+ *next.SigIdent[0].KeyHash: next.SigIdent[0], // log signature
}
- copy(sigident.Signature[:], sig)
- return &types.SignedTreeHead{
- TreeHead: *th,
- SigIdent: []*types.SigIdent{
- &sigident,
- },
- }, nil
+ glog.V(3).Infof("rotated tree heads")
}
diff --git a/state/state_manager_test.go b/state/state_manager_test.go
index 6db8592..348074c 100644
--- a/state/state_manager_test.go
+++ b/state/state_manager_test.go
@@ -1,26 +1,393 @@
package stfe
import (
+ "bytes"
+ "context"
+ "crypto"
+ "crypto/ed25519"
+ "crypto/rand"
+ "fmt"
+ "reflect"
"testing"
+ "time"
+
+ "github.com/golang/mock/gomock"
+ "github.com/system-transparency/stfe/mocks"
+ "github.com/system-transparency/stfe/types"
+)
+
+var (
+ testSig = &[types.SignatureSize]byte{}
+ testPub = &[types.VerificationKeySize]byte{}
+ testTH = &types.TreeHead{
+ Timestamp: 0,
+ TreeSize: 0,
+ RootHash: types.Hash(nil),
+ }
+ testSigIdent = &types.SigIdent{
+ Signature: testSig,
+ KeyHash: types.Hash(testPub[:]),
+ }
+ testSTH = &types.SignedTreeHead{
+ TreeHead: *testTH,
+ SigIdent: []*types.SigIdent{testSigIdent},
+ }
+ testSignerOK = &mocks.TestSigner{testPub, testSig, nil}
+ testSignerErr = &mocks.TestSigner{testPub, testSig, fmt.Errorf("something went wrong")}
)
func TestNewStateManagerSingle(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: "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 := mocks.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, 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.cosignature), 1; 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,
+ 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 := mocks.NewMockClient(ctrl)
+ client.EXPECT().GetTreeHead(gomock.Any()).Return(table.rsp, table.err)
+ sm := StateManagerSingle{
+ client: client,
+ signer: table.signer,
+ }
+
+ sth, err := sm.Latest(context.Background())
+ 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)
+ }
+ }()
+ }
}
func TestToSign(t *testing.T) {
+ description := "valid"
+ sm := StateManagerSingle{
+ 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) {
+ t.Errorf("got signed tree head\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, description)
+ }
}
func TestCosigned(t *testing.T) {
+ description := "valid"
+ sm := StateManagerSingle{
+ cosigned: *testSTH,
+ }
+ sth, err := sm.Cosigned(context.Background())
+ if err != nil {
+ t.Errorf("Cosigned 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)
+ }
}
func TestAddCosignature(t *testing.T) {
+ vk, sk, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ t.Fatalf("GenerateKey: %v", err)
+ }
+ if bytes.Equal(vk[:], testPub[:]) {
+ t.Fatalf("Sampled same key as testPub, aborting...")
+ }
+ var vkArray [types.VerificationKeySize]byte
+ copy(vkArray[:], vk[:])
+
+ for _, table := range []struct {
+ description string
+ signer crypto.Signer
+ vk *[types.VerificationKeySize]byte
+ th *types.TreeHead
+ wantErr bool
+ }{
+ {
+ description: "invalid: signature error",
+ signer: sk,
+ vk: testPub, // wrong key for message
+ th: testTH,
+ wantErr: true,
+ },
+ {
+ description: "valid",
+ signer: sk,
+ vk: &vkArray,
+ th: testTH,
+ },
+ } {
+ sth, _ := table.th.Sign(testSignerOK)
+ logKeyHash := sth.SigIdent[0].KeyHash
+ logSigIdent := sth.SigIdent[0]
+ sm := &StateManagerSingle{
+ signer: testSignerOK,
+ cosigned: *sth,
+ tosign: *sth,
+ cosignature: map[[types.HashSize]byte]*types.SigIdent{
+ *logKeyHash: logSigIdent,
+ },
+ }
+
+ // Prepare witness signature
+ sth, err := table.th.Sign(table.signer)
+ if err != nil {
+ t.Fatalf("Sign: %v", err)
+ }
+ witnessKeyHash := sth.SigIdent[0].KeyHash
+ witnessSigIdent := sth.SigIdent[0]
+
+ // Add witness signature
+ err = sm.AddCosignature(context.Background(), table.vk, witnessSigIdent.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)
+ }
+ if err != nil {
+ continue
+ }
+
+ // We should have two signatures (log + witness)
+ if got, want := len(sm.cosignature), 2; got != want {
+ t.Errorf("got %d cosignatures but wanted %v in test %q", got, want, table.description)
+ continue
+ }
+ // check that log signature is there
+ sigident, ok := sm.cosignature[*logKeyHash]
+ if !ok {
+ t.Errorf("log signature is missing")
+ continue
+ }
+ if got, want := sigident, logSigIdent; !reflect.DeepEqual(got, want) {
+ t.Errorf("got log sigident\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description)
+ }
+ // check that witness signature is there
+ sigident, ok = sm.cosignature[*witnessKeyHash]
+ if !ok {
+ t.Errorf("witness signature is missing")
+ continue
+ }
+ if got, want := sigident, witnessSigIdent; !reflect.DeepEqual(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, witnessSigIdent.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
+ th1 := &types.TreeHead{
+ Timestamp: 1,
+ TreeSize: 1,
+ RootHash: types.Hash([]byte("1")),
+ }
+ th2 := &types.TreeHead{
+ Timestamp: 2,
+ TreeSize: 2,
+ RootHash: types.Hash([]byte("2")),
+ }
+
+ 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.SignedTreeHead{
+ TreeHead: *th0,
+ SigIdent: []*types.SigIdent{log, wit1},
+ },
+ tosign: types.SignedTreeHead{
+ TreeHead: *th0,
+ SigIdent: []*types.SigIdent{log},
+ },
+ cosignature: map[[types.HashSize]byte]*types.SigIdent{
+ *log.KeyHash: log,
+ *wit2.KeyHash: wit2, // the new witness signature
+ },
+ },
+ next: &types.SignedTreeHead{
+ TreeHead: *th1,
+ SigIdent: []*types.SigIdent{log},
+ },
+ after: &StateManagerSingle{
+ cosigned: types.SignedTreeHead{
+ TreeHead: *th0,
+ SigIdent: []*types.SigIdent{log, wit1, wit2},
+ },
+ tosign: types.SignedTreeHead{
+ TreeHead: *th1,
+ SigIdent: []*types.SigIdent{log},
+ },
+ cosignature: map[[types.HashSize]byte]*types.SigIdent{
+ *log.KeyHash: log, // after rotate we always have log sig
+ },
+ },
+ },
+ {
+ description: "tosign tree head did not repeat, it got one witness signature",
+ before: &StateManagerSingle{
+ cosigned: types.SignedTreeHead{
+ TreeHead: *th0,
+ SigIdent: []*types.SigIdent{log, wit1},
+ },
+ tosign: types.SignedTreeHead{
+ TreeHead: *th1,
+ SigIdent: []*types.SigIdent{log},
+ },
+ cosignature: map[[types.HashSize]byte]*types.SigIdent{
+ *log.KeyHash: log,
+ *wit2.KeyHash: wit2, // the only witness that signed tosign
+ },
+ },
+ next: &types.SignedTreeHead{
+ TreeHead: *th2,
+ SigIdent: []*types.SigIdent{log},
+ },
+ after: &StateManagerSingle{
+ cosigned: types.SignedTreeHead{
+ TreeHead: *th1,
+ SigIdent: []*types.SigIdent{log, wit2},
+ },
+ tosign: types.SignedTreeHead{
+ TreeHead: *th2,
+ SigIdent: []*types.SigIdent{log},
+ },
+ cosignature: map[[types.HashSize]byte]*types.SigIdent{
+ *log.KeyHash: log, // after rotate we always have log sig
+ },
+ },
+ },
+ } {
+ table.before.rotate(table.next)
+ if got, want := table.before.cosigned.TreeHead, table.after.cosigned.TreeHead; !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.TreeHead, table.after.tosign.TreeHead; !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.tosign.SigIdent, table.after.tosign.SigIdent)
+ 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)
+ }
+ }
}
-func TestSign(t *testing.T) {
+func checkWitnessList(t *testing.T, description string, got, want []*types.SigIdent) {
+ t.Helper()
+ for _, si := range got {
+ found := false
+ for _, sj := range want {
+ if reflect.DeepEqual(si, sj) {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Errorf("got unexpected signature-signer pair with key hash in test %q: %x", description, si.KeyHash[:])
+ }
+ }
+ if len(got) != len(want) {
+ t.Errorf("got %d signature-signer pairs but wanted %d in test %q", len(got), len(want), description)
+ }
}