diff options
Diffstat (limited to 'trillian')
| -rw-r--r-- | trillian/client.go | 99 | ||||
| -rw-r--r-- | trillian/client_test.go | 196 | 
2 files changed, 295 insertions, 0 deletions
| diff --git a/trillian/client.go b/trillian/client.go new file mode 100644 index 0000000..c22e9cc --- /dev/null +++ b/trillian/client.go @@ -0,0 +1,99 @@ +package trillian + +import ( +	"context" +	"fmt" + +	"github.com/golang/glog" +	"github.com/google/trillian" +	ttypes "github.com/google/trillian/types" +	"github.com/system-transparency/stfe/types" +	"google.golang.org/grpc/codes" +) + +// Client is a wrapper around the Trillian gRPC client +type Client struct { +	// TreeID is a Merkle tree identifier that Trillian uses +	TreeID int64 + +	// GRPC is a Trillian gRPC client +	GRPC trillian.TrillianLogClient +} + +func (c *Client) AddLeaf(ctx context.Context, req *types.LeafRequest) error { +	leaf := types.Leaf{ +		Message: req.Message, +		SigIdent: types.SigIdent{ +			Signature: req.Signature, +			KeyHash:   types.Hash(req.VerificationKey[:]), +		}, +	} +	serialized := leaf.Marshal() + +	glog.V(3).Infof("queueing leaf request: %x", types.HashLeaf(serialized)) +	rsp, err := c.GRPC.QueueLeaf(ctx, &trillian.QueueLeafRequest{ +		LogId: c.TreeID, +		Leaf: &trillian.LogLeaf{ +			LeafValue: serialized, +		}, +	}) +	if err != nil { +		return fmt.Errorf("backend failure: %v", err) +	} +	if rsp == nil { +		return fmt.Errorf("no response") +	} +	if rsp.QueuedLeaf == nil { +		return fmt.Errorf("no queued leaf") +	} +	if codes.Code(rsp.QueuedLeaf.GetStatus().GetCode()) == codes.AlreadyExists { +		return fmt.Errorf("leaf is already queued or included") +	} +	return nil +} + +func (c *Client) GetTreeHead(ctx context.Context) (*types.TreeHead, error) { +	rsp, err := c.GRPC.GetLatestSignedLogRoot(ctx, &trillian.GetLatestSignedLogRootRequest{ +		LogId: c.TreeID, +	}) +	if err != nil { +		return nil, fmt.Errorf("backend failure: %v", err) +	} +	if rsp == nil { +		return nil, fmt.Errorf("no response") +	} +	if rsp.SignedLogRoot == nil { +		return nil, fmt.Errorf("no signed log root") +	} +	if rsp.SignedLogRoot.LogRoot == nil { +		return nil, fmt.Errorf("no log root") +	} +	var r ttypes.LogRootV1 +	if err := r.UnmarshalBinary(rsp.SignedLogRoot.LogRoot); err != nil { +		return nil, fmt.Errorf("no log root: unmarshal failed: %v", err) +	} +	if len(r.RootHash) != types.HashSize { +		return nil, fmt.Errorf("unexpected hash length: %d", len(r.RootHash)) +	} + +	var hash [types.HashSize]byte +	th := types.TreeHead{ +		Timestamp: uint64(r.TimestampNanos / 1000 / 1000 / 1000), +		TreeSize:  uint64(r.TreeSize), +		RootHash:  &hash, +	} +	copy(th.RootHash[:], r.RootHash) +	return &th, nil +} + +func (c *Client) GetConsistencyProof(ctx context.Context, req *types.ConsistencyProofRequest) (*types.ConsistencyProof, error) { +	return nil, fmt.Errorf("TODO") +} + +func (c *Client) GetInclusionProof(ctx context.Context, req *types.InclusionProofRequest) (*types.InclusionProof, error) { +	return nil, fmt.Errorf("TODO") +} + +func (c *Client) GetLeaves(ctx context.Context, req *types.LeavesRequest) (*types.LeafList, error) { +	return nil, fmt.Errorf("TODO") +} diff --git a/trillian/client_test.go b/trillian/client_test.go new file mode 100644 index 0000000..1807615 --- /dev/null +++ b/trillian/client_test.go @@ -0,0 +1,196 @@ +package trillian + +import ( +	"context" +	"fmt" +	"reflect" +	"testing" + +	"github.com/golang/mock/gomock" +	"github.com/google/certificate-transparency-go/trillian/mockclient" +	"github.com/google/trillian" +	ttypes "github.com/google/trillian/types" +	"github.com/system-transparency/stfe/types" +	"google.golang.org/grpc/codes" +	"google.golang.org/grpc/status" +) + +func TestAddLeaf(t *testing.T) { +	req := &types.LeafRequest{ +		Message: types.Message{ +			ShardHint: 0, +			Checksum:  &[types.HashSize]byte{}, +		}, +		Signature:       &[types.SignatureSize]byte{}, +		VerificationKey: &[types.VerificationKeySize]byte{}, +		DomainHint:      "example.com", +	} +	for _, table := range []struct { +		description string +		req         *types.LeafRequest +		rsp         *trillian.QueueLeafResponse +		err         error +		wantErr     bool +	}{ +		{ +			description: "invalid: backend failure", +			req:         req, +			err:         fmt.Errorf("something went wrong"), +			wantErr:     true, +		}, +		{ +			description: "invalid: no response", +			req:         req, +			wantErr:     true, +		}, +		{ +			description: "invalid: no queued leaf", +			req:         req, +			rsp:         &trillian.QueueLeafResponse{}, +			wantErr:     true, +		}, +		{ +			description: "invalid: leaf is already queued or included", +			req:         req, +			rsp: &trillian.QueueLeafResponse{ +				QueuedLeaf: &trillian.QueuedLogLeaf{ +					Leaf: &trillian.LogLeaf{ +						LeafValue: req.Message.Marshal(), +					}, +					Status: status.New(codes.AlreadyExists, "duplicate").Proto(), +				}, +			}, +			wantErr: true, +		}, +		{ +			description: "valid", +			req:         req, +			rsp: &trillian.QueueLeafResponse{ +				QueuedLeaf: &trillian.QueuedLogLeaf{ +					Leaf: &trillian.LogLeaf{ +						LeafValue: req.Message.Marshal(), +					}, +					Status: status.New(codes.OK, "ok").Proto(), +				}, +			}, +		}, +	} { +		// Run deferred functions at the end of each iteration +		func() { +			ctrl := gomock.NewController(t) +			defer ctrl.Finish() +			grpc := mockclient.NewMockTrillianLogClient(ctrl) +			grpc.EXPECT().QueueLeaf(gomock.Any(), gomock.Any()).Return(table.rsp, table.err) +			client := Client{GRPC: grpc} + +			err := client.AddLeaf(context.Background(), table.req) +			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) +			} +		}() +	} +} + +func TestGetTreeHead(t *testing.T) { +	// valid root +	root := &ttypes.LogRootV1{ +		TreeSize:       0, +		RootHash:       make([]byte, types.HashSize), +		TimestampNanos: 1622585623133599429, +	} +	buf, err := root.MarshalBinary() +	if err != nil { +		t.Fatalf("must marshal log root: %v", err) +	} +	// invalid root +	root.RootHash = make([]byte, types.HashSize+1) +	bufBadHash, err := root.MarshalBinary() +	if err != nil { +		t.Fatalf("must marshal log root: %v", err) +	} + +	for _, table := range []struct { +		description string +		rsp         *trillian.GetLatestSignedLogRootResponse +		err         error +		wantErr     bool +		wantTh      *types.TreeHead +	}{ +		{ +			description: "invalid: backend failure", +			err:         fmt.Errorf("something went wrong"), +			wantErr:     true, +		}, +		{ +			description: "invalid: no response", +			wantErr:     true, +		}, +		{ +			description: "invalid: no signed log root", +			rsp:         &trillian.GetLatestSignedLogRootResponse{}, +			wantErr:     true, +		}, +		{ +			description: "invalid: no log root", +			rsp: &trillian.GetLatestSignedLogRootResponse{ +				SignedLogRoot: &trillian.SignedLogRoot{}, +			}, +			wantErr: true, +		}, +		{ +			description: "invalid: no log root: unmarshal failed", +			rsp: &trillian.GetLatestSignedLogRootResponse{ +				SignedLogRoot: &trillian.SignedLogRoot{ +					LogRoot: buf[1:], +				}, +			}, +			wantErr: true, +		}, +		{ +			description: "invalid: unexpected hash length", +			rsp: &trillian.GetLatestSignedLogRootResponse{ +				SignedLogRoot: &trillian.SignedLogRoot{ +					LogRoot: bufBadHash, +				}, +			}, +			wantErr: true, +		}, +		{ +			description: "valid", +			rsp: &trillian.GetLatestSignedLogRootResponse{ +				SignedLogRoot: &trillian.SignedLogRoot{ +					LogRoot: buf, +				}, +			}, +			wantTh: &types.TreeHead{ +				Timestamp: 1622585623, +				TreeSize:  0, +				RootHash:  &[types.HashSize]byte{}, +			}, +		}, +	} { +		// Run deferred functions at the end of each iteration +		func() { +			ctrl := gomock.NewController(t) +			defer ctrl.Finish() +			grpc := mockclient.NewMockTrillianLogClient(ctrl) +			grpc.EXPECT().GetLatestSignedLogRoot(gomock.Any(), gomock.Any()).Return(table.rsp, table.err) +			client := Client{GRPC: grpc} + +			th, err := client.GetTreeHead(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 := th, table.wantTh; !reflect.DeepEqual(got, want) { +				t.Errorf("got tree head\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) +			} +		}() +	} +} + +func TestGetConsistencyProof(t *testing.T) {} +func TestGetInclusionProof(t *testing.T)   {} +func TestGetLeaves(t *testing.T)           {} | 
