diff options
Diffstat (limited to 'pkg')
| -rw-r--r-- | pkg/instance/endpoint.go | 122 | ||||
| -rw-r--r-- | pkg/instance/endpoint_test.go | 432 | ||||
| -rw-r--r-- | pkg/instance/instance.go | 159 | ||||
| -rw-r--r-- | pkg/instance/instance_test.go | 9 | ||||
| -rw-r--r-- | pkg/instance/metric.go | 19 | ||||
| -rw-r--r-- | pkg/mocks/crypto.go | 23 | ||||
| -rw-r--r-- | pkg/mocks/state.go | 107 | ||||
| -rw-r--r-- | pkg/mocks/stfe.go | 110 | ||||
| -rw-r--r-- | pkg/mocks/trillian.go | 317 | ||||
| -rw-r--r-- | pkg/state/state_manager.go | 154 | ||||
| -rw-r--r-- | pkg/state/state_manager_test.go | 393 | ||||
| -rw-r--r-- | pkg/trillian/client.go | 178 | ||||
| -rw-r--r-- | pkg/trillian/client_test.go | 533 | ||||
| -rw-r--r-- | pkg/trillian/util.go | 33 | ||||
| -rw-r--r-- | pkg/types/ascii.go | 421 | ||||
| -rw-r--r-- | pkg/types/ascii_test.go | 465 | ||||
| -rw-r--r-- | pkg/types/trunnel.go | 60 | ||||
| -rw-r--r-- | pkg/types/trunnel_test.go | 114 | ||||
| -rw-r--r-- | pkg/types/types.go | 155 | ||||
| -rw-r--r-- | pkg/types/types_test.go | 58 | ||||
| -rw-r--r-- | pkg/types/util.go | 21 | 
21 files changed, 3883 insertions, 0 deletions
| diff --git a/pkg/instance/endpoint.go b/pkg/instance/endpoint.go new file mode 100644 index 0000000..5085c49 --- /dev/null +++ b/pkg/instance/endpoint.go @@ -0,0 +1,122 @@ +package stfe + +import ( +	"context" +	"net/http" + +	"github.com/golang/glog" +) + +func addLeaf(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) { +	glog.V(3).Info("handling add-entry request") +	req, err := i.leafRequestFromHTTP(r) +	if err != nil { +		return http.StatusBadRequest, err +	} +	if err := i.Client.AddLeaf(ctx, req); err != nil { +		return http.StatusInternalServerError, err +	} +	return http.StatusOK, nil +} + +func addCosignature(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) { +	glog.V(3).Info("handling add-cosignature request") +	req, err := i.cosignatureRequestFromHTTP(r) +	if err != nil { +		return http.StatusBadRequest, err +	} +	vk := i.Witnesses[*req.KeyHash] +	if err := i.Stateman.AddCosignature(ctx, &vk, req.Signature); err != nil { +		return http.StatusBadRequest, err +	} +	return http.StatusOK, nil +} + +func getTreeHeadLatest(ctx context.Context, i *Instance, w http.ResponseWriter, _ *http.Request) (int, error) { +	glog.V(3).Info("handling get-tree-head-latest request") +	sth, err := i.Stateman.Latest(ctx) +	if err != nil { +		return http.StatusInternalServerError, err +	} +	if err := sth.MarshalASCII(w); err != nil { +		return http.StatusInternalServerError, err +	} +	return http.StatusOK, nil +} + +func getTreeHeadToSign(ctx context.Context, i *Instance, w http.ResponseWriter, _ *http.Request) (int, error) { +	glog.V(3).Info("handling get-tree-head-to-sign request") +	sth, err := i.Stateman.ToSign(ctx) +	if err != nil { +		return http.StatusInternalServerError, err +	} +	if err := sth.MarshalASCII(w); err != nil { +		return http.StatusInternalServerError, err +	} +	return http.StatusOK, nil +} + +func getTreeHeadCosigned(ctx context.Context, i *Instance, w http.ResponseWriter, _ *http.Request) (int, error) { +	glog.V(3).Info("handling get-tree-head-cosigned request") +	sth, err := i.Stateman.Cosigned(ctx) +	if err != nil { +		return http.StatusInternalServerError, err +	} +	if err := sth.MarshalASCII(w); err != nil { +		return http.StatusInternalServerError, err +	} +	return http.StatusOK, nil +} + +func getConsistencyProof(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) { +	glog.V(3).Info("handling get-consistency-proof request") +	req, err := i.consistencyProofRequestFromHTTP(r) +	if err != nil { +		return http.StatusBadRequest, err +	} + +	proof, err := i.Client.GetConsistencyProof(ctx, req) +	if err != nil { +		return http.StatusInternalServerError, err +	} +	if err := proof.MarshalASCII(w); err != nil { +		return http.StatusInternalServerError, err +	} +	return http.StatusOK, nil +} + +func getInclusionProof(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) { +	glog.V(3).Info("handling get-proof-by-hash request") +	req, err := i.inclusionProofRequestFromHTTP(r) +	if err != nil { +		return http.StatusBadRequest, err +	} + +	proof, err := i.Client.GetInclusionProof(ctx, req) +	if err != nil { +		return http.StatusInternalServerError, err +	} +	if err := proof.MarshalASCII(w); err != nil { +		return http.StatusInternalServerError, err +	} +	return http.StatusOK, nil +} + +func getLeaves(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) { +	glog.V(3).Info("handling get-leaves request") +	req, err := i.leavesRequestFromHTTP(r) +	if err != nil { +		return http.StatusBadRequest, err +	} + +	leaves, err := i.Client.GetLeaves(ctx, req) +	if err != nil { +		return http.StatusInternalServerError, err +	} +	for _, leaf := range *leaves { +		if err := leaf.MarshalASCII(w); err != nil { +			return http.StatusInternalServerError, err +		} +	} +	return http.StatusOK, nil +} diff --git a/pkg/instance/endpoint_test.go b/pkg/instance/endpoint_test.go new file mode 100644 index 0000000..efcd4c0 --- /dev/null +++ b/pkg/instance/endpoint_test.go @@ -0,0 +1,432 @@ +package stfe + +import ( +	"bytes" +	"encoding/hex" +	"fmt" +	"io" +	"net/http" +	"net/http/httptest" +	"testing" + +	"github.com/golang/mock/gomock" +	"github.com/system-transparency/stfe/pkg/mocks" +	"github.com/system-transparency/stfe/pkg/types" +) + +var ( +	testWitVK  = [types.VerificationKeySize]byte{} +	testConfig = Config{ +		LogID:    hex.EncodeToString(types.Hash([]byte("logid"))[:]), +		TreeID:   0, +		Prefix:   "testonly", +		MaxRange: 3, +		Deadline: 10, +		Interval: 10, +		Witnesses: map[[types.HashSize]byte][types.VerificationKeySize]byte{ +			*types.Hash(testWitVK[:]): testWitVK, +		}, +	} +	testSTH = &types.SignedTreeHead{ +		TreeHead: types.TreeHead{ +			Timestamp: 0, +			TreeSize:  0, +			RootHash:  types.Hash(nil), +		}, +		SigIdent: []*types.SigIdent{ +			&types.SigIdent{ +				Signature: &[types.SignatureSize]byte{}, +				KeyHash:   &[types.HashSize]byte{}, +			}, +		}, +	} +) + +func mustHandle(t *testing.T, i Instance, e types.Endpoint) Handler { +	for _, handler := range i.Handlers() { +		if handler.Endpoint == e { +			return handler +		} +	} +	t.Fatalf("must handle endpoint: %v", e) +	return Handler{} +} + +func TestAddLeaf(t *testing.T) { +	buf := func() io.Reader { +		// A valid leaf request that was created manually +		return bytes.NewBufferString(fmt.Sprintf( +			"%s%s%s%s"+"%s%s%s%s"+"%s%s%s%s"+"%s%s%s%s"+"%s%s%s%s", +			types.ShardHint, types.Delim, "0", types.EOL, +			types.Checksum, types.Delim, "0000000000000000000000000000000000000000000000000000000000000000", types.EOL, +			types.SignatureOverMessage, types.Delim, "4cb410a4d48f52f761a7c01abcc28fd71811b84ded5403caed5e21b374f6aac9637cecd36828f17529fd503413d30ab66d7bb37a31dbf09a90d23b9241c45009", types.EOL, +			types.VerificationKey, types.Delim, "f2b7a00b625469d32502e06e8b7fad1ef258d4ad0c6cd87b846142ab681957d5", types.EOL, +			types.DomainHint, types.Delim, "example.com", types.EOL, +		)) +	} +	for _, table := range []struct { +		description string +		ascii       io.Reader // buffer used to populate HTTP request +		expect      bool      // set if a mock answer is expected +		err         error     // error from Trillian client +		wantCode    int       // HTTP status ok +	}{ +		{ +			description: "invalid: bad request (parser error)", +			ascii:       bytes.NewBufferString("key=value\n"), +			wantCode:    http.StatusBadRequest, +		}, +		{ +			description: "invalid: bad request (signature error)", +			ascii: bytes.NewBufferString(fmt.Sprintf( +				"%s%s%s%s"+"%s%s%s%s"+"%s%s%s%s"+"%s%s%s%s"+"%s%s%s%s", +				types.ShardHint, types.Delim, "1", types.EOL, +				types.Checksum, types.Delim, "1111111111111111111111111111111111111111111111111111111111111111", types.EOL, +				types.SignatureOverMessage, types.Delim, "4cb410a4d48f52f761a7c01abcc28fd71811b84ded5403caed5e21b374f6aac9637cecd36828f17529fd503413d30ab66d7bb37a31dbf09a90d23b9241c45009", types.EOL, +				types.VerificationKey, types.Delim, "f2b7a00b625469d32502e06e8b7fad1ef258d4ad0c6cd87b846142ab681957d5", types.EOL, +				types.DomainHint, types.Delim, "example.com", types.EOL, +			)), +			wantCode: http.StatusBadRequest, +		}, +		{ +			description: "invalid: backend failure", +			ascii:       buf(), +			expect:      true, +			err:         fmt.Errorf("something went wrong"), +			wantCode:    http.StatusInternalServerError, +		}, +		{ +			description: "valid", +			ascii:       buf(), +			expect:      true, +			wantCode:    http.StatusOK, +		}, +	} { +		// Run deferred functions at the end of each iteration +		func() { +			ctrl := gomock.NewController(t) +			defer ctrl.Finish() +			client := mocks.NewMockClient(ctrl) +			if table.expect { +				client.EXPECT().AddLeaf(gomock.Any(), gomock.Any()).Return(table.err) +			} +			i := Instance{ +				Config: testConfig, +				Client: client, +			} + +			// Create HTTP request +			url := types.EndpointAddLeaf.Path("http://example.com", i.Prefix) +			req, err := http.NewRequest("POST", url, table.ascii) +			if err != nil { +				t.Fatalf("must create http request: %v", err) +			} + +			// Run HTTP request +			w := httptest.NewRecorder() +			mustHandle(t, i, types.EndpointAddLeaf).ServeHTTP(w, req) +			if got, want := w.Code, table.wantCode; got != want { +				t.Errorf("got HTTP status code %v but wanted %v in test %q", got, want, table.description) +			} +		}() +	} +} + +func TestAddCosignature(t *testing.T) { +	buf := func() io.Reader { +		return bytes.NewBufferString(fmt.Sprintf( +			"%s%s%x%s"+"%s%s%x%s", +			types.Signature, types.Delim, make([]byte, types.SignatureSize), types.EOL, +			types.KeyHash, types.Delim, *types.Hash(testWitVK[:]), types.EOL, +		)) +	} +	for _, table := range []struct { +		description string +		ascii       io.Reader // buffer used to populate HTTP request +		expect      bool      // set if a mock answer is expected +		err         error     // error from Trillian client +		wantCode    int       // HTTP status ok +	}{ +		{ +			description: "invalid: bad request (parser error)", +			ascii:       bytes.NewBufferString("key=value\n"), +			wantCode:    http.StatusBadRequest, +		}, +		{ +			description: "invalid: bad request (unknown witness)", +			ascii: bytes.NewBufferString(fmt.Sprintf( +				"%s%s%x%s"+"%s%s%x%s", +				types.Signature, types.Delim, make([]byte, types.SignatureSize), types.EOL, +				types.KeyHash, types.Delim, *types.Hash(testWitVK[1:]), types.EOL, +			)), +			wantCode: http.StatusBadRequest, +		}, +		{ +			description: "invalid: backend failure", +			ascii:       buf(), +			expect:      true, +			err:         fmt.Errorf("something went wrong"), +			wantCode:    http.StatusBadRequest, +		}, +		{ +			description: "valid", +			ascii:       buf(), +			expect:      true, +			wantCode:    http.StatusOK, +		}, +	} { +		// Run deferred functions at the end of each iteration +		func() { +			ctrl := gomock.NewController(t) +			defer ctrl.Finish() +			stateman := mocks.NewMockStateManager(ctrl) +			if table.expect { +				stateman.EXPECT().AddCosignature(gomock.Any(), gomock.Any(), gomock.Any()).Return(table.err) +			} +			i := Instance{ +				Config:   testConfig, +				Stateman: stateman, +			} + +			// Create HTTP request +			url := types.EndpointAddCosignature.Path("http://example.com", i.Prefix) +			req, err := http.NewRequest("POST", url, table.ascii) +			if err != nil { +				t.Fatalf("must create http request: %v", err) +			} + +			// Run HTTP request +			w := httptest.NewRecorder() +			mustHandle(t, i, types.EndpointAddCosignature).ServeHTTP(w, req) +			if got, want := w.Code, table.wantCode; got != want { +				t.Errorf("got HTTP status code %v but wanted %v in test %q", got, want, table.description) +			} +		}() +	} +} + +func TestGetTreeHeadLatest(t *testing.T) { +	for _, table := range []struct { +		description string +		expect      bool                  // set if a mock answer is expected +		rsp         *types.SignedTreeHead // signed tree head from Trillian client +		err         error                 // error from Trillian client +		wantCode    int                   // HTTP status ok +	}{ +		{ +			description: "invalid: backend failure", +			expect:      true, +			err:         fmt.Errorf("something went wrong"), +			wantCode:    http.StatusInternalServerError, +		}, +		{ +			description: "valid", +			expect:      true, +			rsp:         testSTH, +			wantCode:    http.StatusOK, +		}, +	} { +		// Run deferred functions at the end of each iteration +		func() { +			ctrl := gomock.NewController(t) +			defer ctrl.Finish() +			stateman := mocks.NewMockStateManager(ctrl) +			if table.expect { +				stateman.EXPECT().Latest(gomock.Any()).Return(table.rsp, table.err) +			} +			i := Instance{ +				Config:   testConfig, +				Stateman: stateman, +			} + +			// Create HTTP request +			url := types.EndpointGetTreeHeadLatest.Path("http://example.com", i.Prefix) +			req, err := http.NewRequest("GET", url, nil) +			if err != nil { +				t.Fatalf("must create http request: %v", err) +			} + +			// Run HTTP request +			w := httptest.NewRecorder() +			mustHandle(t, i, types.EndpointGetTreeHeadLatest).ServeHTTP(w, req) +			if got, want := w.Code, table.wantCode; got != want { +				t.Errorf("got HTTP status code %v but wanted %v in test %q", got, want, table.description) +			} +		}() +	} +} + +func TestGetTreeToSign(t *testing.T) { +	for _, table := range []struct { +		description string +		expect      bool                  // set if a mock answer is expected +		rsp         *types.SignedTreeHead // signed tree head from Trillian client +		err         error                 // error from Trillian client +		wantCode    int                   // HTTP status ok +	}{ +		{ +			description: "invalid: backend failure", +			expect:      true, +			err:         fmt.Errorf("something went wrong"), +			wantCode:    http.StatusInternalServerError, +		}, +		{ +			description: "valid", +			expect:      true, +			rsp:         testSTH, +			wantCode:    http.StatusOK, +		}, +	} { +		// Run deferred functions at the end of each iteration +		func() { +			ctrl := gomock.NewController(t) +			defer ctrl.Finish() +			stateman := mocks.NewMockStateManager(ctrl) +			if table.expect { +				stateman.EXPECT().ToSign(gomock.Any()).Return(table.rsp, table.err) +			} +			i := Instance{ +				Config:   testConfig, +				Stateman: stateman, +			} + +			// Create HTTP request +			url := types.EndpointGetTreeHeadToSign.Path("http://example.com", i.Prefix) +			req, err := http.NewRequest("GET", url, nil) +			if err != nil { +				t.Fatalf("must create http request: %v", err) +			} + +			// Run HTTP request +			w := httptest.NewRecorder() +			mustHandle(t, i, types.EndpointGetTreeHeadToSign).ServeHTTP(w, req) +			if got, want := w.Code, table.wantCode; got != want { +				t.Errorf("got HTTP status code %v but wanted %v in test %q", got, want, table.description) +			} +		}() +	} +} + +func TestGetTreeCosigned(t *testing.T) { +	for _, table := range []struct { +		description string +		expect      bool                  // set if a mock answer is expected +		rsp         *types.SignedTreeHead // signed tree head from Trillian client +		err         error                 // error from Trillian client +		wantCode    int                   // HTTP status ok +	}{ +		{ +			description: "invalid: backend failure", +			expect:      true, +			err:         fmt.Errorf("something went wrong"), +			wantCode:    http.StatusInternalServerError, +		}, +		{ +			description: "valid", +			expect:      true, +			rsp:         testSTH, +			wantCode:    http.StatusOK, +		}, +	} { +		// Run deferred functions at the end of each iteration +		func() { +			ctrl := gomock.NewController(t) +			defer ctrl.Finish() +			stateman := mocks.NewMockStateManager(ctrl) +			if table.expect { +				stateman.EXPECT().Cosigned(gomock.Any()).Return(table.rsp, table.err) +			} +			i := Instance{ +				Config:   testConfig, +				Stateman: stateman, +			} + +			// Create HTTP request +			url := types.EndpointGetTreeHeadCosigned.Path("http://example.com", i.Prefix) +			req, err := http.NewRequest("GET", url, nil) +			if err != nil { +				t.Fatalf("must create http request: %v", err) +			} + +			// Run HTTP request +			w := httptest.NewRecorder() +			mustHandle(t, i, types.EndpointGetTreeHeadCosigned).ServeHTTP(w, req) +			if got, want := w.Code, table.wantCode; got != want { +				t.Errorf("got HTTP status code %v but wanted %v in test %q", got, want, table.description) +			} +		}() +	} +} + +func TestGetConsistencyProof(t *testing.T) { +	buf := func(oldSize, newSize int) io.Reader { +		return bytes.NewBufferString(fmt.Sprintf( +			"%s%s%d%s"+"%s%s%d%s", +			types.OldSize, types.Delim, oldSize, types.EOL, +			types.NewSize, types.Delim, newSize, types.EOL, +		)) +	} +	// values in testProof are not relevant for the test, just need a path +	testProof := &types.ConsistencyProof{ +		OldSize: 1, +		NewSize: 2, +		Path: []*[types.HashSize]byte{ +			types.Hash(nil), +		}, +	} +	for _, table := range []struct { +		description string +		ascii       io.Reader               // buffer used to populate HTTP request +		expect      bool                    // set if a mock answer is expected +		rsp         *types.ConsistencyProof // consistency proof from Trillian client +		err         error                   // error from Trillian client +		wantCode    int                     // HTTP status ok +	}{ +		{ +			description: "invalid: bad request (parser error)", +			ascii:       bytes.NewBufferString("key=value\n"), +			wantCode:    http.StatusBadRequest, +		}, +		{ +			description: "valid", +			ascii:       buf(1, 2), +			expect:      true, +			rsp:         testProof, +			wantCode:    http.StatusOK, +		}, +	} { +		// Run deferred functions at the end of each iteration +		func() { +			ctrl := gomock.NewController(t) +			defer ctrl.Finish() +			client := mocks.NewMockClient(ctrl) +			if table.expect { +				client.EXPECT().GetConsistencyProof(gomock.Any(), gomock.Any()).Return(table.rsp, table.err) +			} +			i := Instance{ +				Config: testConfig, +				Client: client, +			} + +			// Create HTTP request +			url := types.EndpointGetConsistencyProof.Path("http://example.com", i.Prefix) +			req, err := http.NewRequest("POST", url, table.ascii) +			if err != nil { +				t.Fatalf("must create http request: %v", err) +			} + +			// Run HTTP request +			w := httptest.NewRecorder() +			mustHandle(t, i, types.EndpointGetConsistencyProof).ServeHTTP(w, req) +			if got, want := w.Code, table.wantCode; got != want { +				t.Errorf("got HTTP status code %v but wanted %v in test %q", got, want, table.description) +			} +		}() +	} +} + +func TestGetInclusionProof(t *testing.T) { +} + +func TestGetLeaves(t *testing.T) { +} diff --git a/pkg/instance/instance.go b/pkg/instance/instance.go new file mode 100644 index 0000000..c2fe8fa --- /dev/null +++ b/pkg/instance/instance.go @@ -0,0 +1,159 @@ +package stfe + +import ( +	"context" +	"crypto" +	"crypto/ed25519" +	"fmt" +	"net/http" +	"time" + +	"github.com/golang/glog" +	"github.com/system-transparency/stfe/pkg/state" +	"github.com/system-transparency/stfe/pkg/trillian" +	"github.com/system-transparency/stfe/pkg/types" +) + +// Config is a collection of log parameters +type Config struct { +	LogID    string        // H(public key), then hex-encoded +	TreeID   int64         // Merkle tree identifier used by Trillian +	Prefix   string        // The portion between base URL and st/v0 (may be "") +	MaxRange int64         // Maximum number of leaves per get-leaves request +	Deadline time.Duration // Deadline used for gRPC requests +	Interval time.Duration // Cosigning frequency + +	// Witnesses map trusted witness identifiers to public verification keys +	Witnesses map[[types.HashSize]byte][types.VerificationKeySize]byte +} + +// Instance is an instance of the log's front-end +type Instance struct { +	Config                      // configuration parameters +	Client   trillian.Client    // provides access to the Trillian backend +	Signer   crypto.Signer      // provides access to Ed25519 private key +	Stateman state.StateManager // coordinates access to (co)signed tree heads +} + +// Handler implements the http.Handler interface, and contains a reference +// to an STFE server instance as well as a function that uses it. +type Handler struct { +	Instance *Instance +	Endpoint types.Endpoint +	Method   string +	Handler  func(context.Context, *Instance, http.ResponseWriter, *http.Request) (int, error) +} + +// Handlers returns a list of STFE handlers +func (i *Instance) Handlers() []Handler { +	return []Handler{ +		Handler{Instance: i, Handler: addLeaf, Endpoint: types.EndpointAddLeaf, Method: http.MethodPost}, +		Handler{Instance: i, Handler: addCosignature, Endpoint: types.EndpointAddCosignature, Method: http.MethodPost}, +		Handler{Instance: i, Handler: getTreeHeadLatest, Endpoint: types.EndpointGetTreeHeadLatest, Method: http.MethodGet}, +		Handler{Instance: i, Handler: getTreeHeadToSign, Endpoint: types.EndpointGetTreeHeadToSign, Method: http.MethodGet}, +		Handler{Instance: i, Handler: getTreeHeadCosigned, Endpoint: types.EndpointGetTreeHeadCosigned, Method: http.MethodGet}, +		Handler{Instance: i, Handler: getConsistencyProof, Endpoint: types.EndpointGetConsistencyProof, Method: http.MethodPost}, +		Handler{Instance: i, Handler: getInclusionProof, Endpoint: types.EndpointGetProofByHash, Method: http.MethodPost}, +		Handler{Instance: i, Handler: getLeaves, Endpoint: types.EndpointGetLeaves, Method: http.MethodPost}, +	} +} + +// Path returns a path that should be configured for this handler +func (h Handler) Path() string { +	return h.Endpoint.Path(h.Instance.Prefix, "st", "v0") +} + +// ServeHTTP is part of the http.Handler interface +func (a Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { +	// export prometheus metrics +	var now time.Time = time.Now() +	var statusCode int +	defer func() { +		rspcnt.Inc(a.Instance.LogID, string(a.Endpoint), fmt.Sprintf("%d", statusCode)) +		latency.Observe(time.Now().Sub(now).Seconds(), a.Instance.LogID, string(a.Endpoint), fmt.Sprintf("%d", statusCode)) +	}() +	reqcnt.Inc(a.Instance.LogID, string(a.Endpoint)) + +	ctx, cancel := context.WithDeadline(r.Context(), now.Add(a.Instance.Deadline)) +	defer cancel() + +	if r.Method != a.Method { +		glog.Warningf("%s/%s: got HTTP %s, wanted HTTP %s", a.Instance.Prefix, string(a.Endpoint), r.Method, a.Method) +		http.Error(w, "", http.StatusMethodNotAllowed) +		return +	} + +	statusCode, err := a.Handler(ctx, a.Instance, w, r) +	if err != nil { +		glog.Warningf("handler error %s/%s: %v", a.Instance.Prefix, a.Endpoint, err) +		http.Error(w, fmt.Sprintf("%s%s%s%s", "Error", types.Delim, err.Error(), types.EOL), statusCode) +	} +} + +func (i *Instance) leafRequestFromHTTP(r *http.Request) (*types.LeafRequest, error) { +	var req types.LeafRequest +	if err := req.UnmarshalASCII(r.Body); err != nil { +		return nil, fmt.Errorf("UnmarshalASCII: %v", err) +	} + +	vk := ed25519.PublicKey(req.VerificationKey[:]) +	msg := req.Message.Marshal() +	sig := req.Signature[:] +	if !ed25519.Verify(vk, msg, sig) { +		return nil, fmt.Errorf("invalid signature") +	} +	// TODO: check shard hint +	// TODO: check domain hint +	return &req, nil +} + +func (i *Instance) cosignatureRequestFromHTTP(r *http.Request) (*types.CosignatureRequest, error) { +	var req types.CosignatureRequest +	if err := req.UnmarshalASCII(r.Body); err != nil { +		return nil, fmt.Errorf("unpackOctetPost: %v", err) +	} +	if _, ok := i.Witnesses[*req.KeyHash]; !ok { +		return nil, fmt.Errorf("Unknown witness: %x", req.KeyHash) +	} +	return &req, nil +} + +func (i *Instance) consistencyProofRequestFromHTTP(r *http.Request) (*types.ConsistencyProofRequest, error) { +	var req types.ConsistencyProofRequest +	if err := req.UnmarshalASCII(r.Body); err != nil { +		return nil, fmt.Errorf("UnmarshalASCII: %v", err) +	} +	if req.OldSize < 1 { +		return nil, fmt.Errorf("OldSize(%d) must be larger than zero", req.OldSize) +	} +	if req.NewSize <= req.OldSize { +		return nil, fmt.Errorf("NewSize(%d) must be larger than OldSize(%d)", req.NewSize, req.OldSize) +	} +	return &req, nil +} + +func (i *Instance) inclusionProofRequestFromHTTP(r *http.Request) (*types.InclusionProofRequest, error) { +	var req types.InclusionProofRequest +	if err := req.UnmarshalASCII(r.Body); err != nil { +		return nil, fmt.Errorf("UnmarshalASCII: %v", err) +	} +	if req.TreeSize < 1 { +		return nil, fmt.Errorf("TreeSize(%d) must be larger than zero", req.TreeSize) +	} +	return &req, nil +} + +func (i *Instance) leavesRequestFromHTTP(r *http.Request) (*types.LeavesRequest, error) { +	var req types.LeavesRequest +	if err := req.UnmarshalASCII(r.Body); err != nil { +		return nil, fmt.Errorf("UnmarshalASCII: %v", err) +	} + +	if req.StartSize > req.EndSize { +		return nil, fmt.Errorf("StartSize(%d) must be less than or equal to EndSize(%d)", req.StartSize, req.EndSize) +	} +	if req.EndSize-req.StartSize+1 > uint64(i.MaxRange) { +		req.EndSize = req.StartSize + uint64(i.MaxRange) - 1 +	} +	return &req, nil +} diff --git a/pkg/instance/instance_test.go b/pkg/instance/instance_test.go new file mode 100644 index 0000000..45a2837 --- /dev/null +++ b/pkg/instance/instance_test.go @@ -0,0 +1,9 @@ +package stfe + +import ( +	"testing" +) + +func TestHandlers(t *testing.T)  {} +func TestPath(t *testing.T)      {} +func TestServeHTTP(t *testing.T) {} diff --git a/pkg/instance/metric.go b/pkg/instance/metric.go new file mode 100644 index 0000000..db11bd2 --- /dev/null +++ b/pkg/instance/metric.go @@ -0,0 +1,19 @@ +package stfe + +import ( +	"github.com/google/trillian/monitoring" +	"github.com/google/trillian/monitoring/prometheus" +) + +var ( +	reqcnt  monitoring.Counter   // number of incoming http requests +	rspcnt  monitoring.Counter   // number of valid http responses +	latency monitoring.Histogram // request-response latency +) + +func init() { +	mf := prometheus.MetricFactory{} +	reqcnt = mf.NewCounter("http_req", "number of http requests", "logid", "endpoint") +	rspcnt = mf.NewCounter("http_rsp", "number of http requests", "logid", "endpoint", "status") +	latency = mf.NewHistogram("http_latency", "http request-response latency", "logid", "endpoint", "status") +} diff --git a/pkg/mocks/crypto.go b/pkg/mocks/crypto.go new file mode 100644 index 0000000..87c883a --- /dev/null +++ b/pkg/mocks/crypto.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/mocks/state.go b/pkg/mocks/state.go new file mode 100644 index 0000000..41d8d08 --- /dev/null +++ b/pkg/mocks/state.go @@ -0,0 +1,107 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/system-transparency/stfe/pkg/state (interfaces: StateManager) + +// Package mocks is a generated GoMock package. +package mocks + +import ( +	context "context" +	reflect "reflect" + +	gomock "github.com/golang/mock/gomock" +	types "github.com/system-transparency/stfe/pkg/types" +) + +// 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 *[32]byte, arg2 *[64]byte) 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.SignedTreeHead, error) { +	m.ctrl.T.Helper() +	ret := m.ctrl.Call(m, "Cosigned", arg0) +	ret0, _ := ret[0].(*types.SignedTreeHead) +	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/mocks/stfe.go b/pkg/mocks/stfe.go new file mode 100644 index 0000000..def5bc6 --- /dev/null +++ b/pkg/mocks/stfe.go @@ -0,0 +1,110 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/system-transparency/stfe/trillian (interfaces: Client) + +// Package mocks is a generated GoMock package. +package mocks + +import ( +	context "context" +	reflect "reflect" + +	gomock "github.com/golang/mock/gomock" +	types "github.com/system-transparency/stfe/pkg/types" +) + +// MockClient is a mock of Client interface. +type MockClient struct { +	ctrl     *gomock.Controller +	recorder *MockClientMockRecorder +} + +// MockClientMockRecorder is the mock recorder for MockClient. +type MockClientMockRecorder struct { +	mock *MockClient +} + +// NewMockClient creates a new mock instance. +func NewMockClient(ctrl *gomock.Controller) *MockClient { +	mock := &MockClient{ctrl: ctrl} +	mock.recorder = &MockClientMockRecorder{mock} +	return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockClient) EXPECT() *MockClientMockRecorder { +	return m.recorder +} + +// AddLeaf mocks base method. +func (m *MockClient) AddLeaf(arg0 context.Context, arg1 *types.LeafRequest) error { +	m.ctrl.T.Helper() +	ret := m.ctrl.Call(m, "AddLeaf", arg0, arg1) +	ret0, _ := ret[0].(error) +	return ret0 +} + +// AddLeaf indicates an expected call of AddLeaf. +func (mr *MockClientMockRecorder) AddLeaf(arg0, arg1 interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddLeaf", reflect.TypeOf((*MockClient)(nil).AddLeaf), arg0, arg1) +} + +// GetConsistencyProof mocks base method. +func (m *MockClient) GetConsistencyProof(arg0 context.Context, arg1 *types.ConsistencyProofRequest) (*types.ConsistencyProof, error) { +	m.ctrl.T.Helper() +	ret := m.ctrl.Call(m, "GetConsistencyProof", arg0, arg1) +	ret0, _ := ret[0].(*types.ConsistencyProof) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// GetConsistencyProof indicates an expected call of GetConsistencyProof. +func (mr *MockClientMockRecorder) GetConsistencyProof(arg0, arg1 interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConsistencyProof", reflect.TypeOf((*MockClient)(nil).GetConsistencyProof), arg0, arg1) +} + +// GetInclusionProof mocks base method. +func (m *MockClient) GetInclusionProof(arg0 context.Context, arg1 *types.InclusionProofRequest) (*types.InclusionProof, error) { +	m.ctrl.T.Helper() +	ret := m.ctrl.Call(m, "GetInclusionProof", arg0, arg1) +	ret0, _ := ret[0].(*types.InclusionProof) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// GetInclusionProof indicates an expected call of GetInclusionProof. +func (mr *MockClientMockRecorder) GetInclusionProof(arg0, arg1 interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInclusionProof", reflect.TypeOf((*MockClient)(nil).GetInclusionProof), arg0, arg1) +} + +// GetLeaves mocks base method. +func (m *MockClient) GetLeaves(arg0 context.Context, arg1 *types.LeavesRequest) (*types.LeafList, error) { +	m.ctrl.T.Helper() +	ret := m.ctrl.Call(m, "GetLeaves", arg0, arg1) +	ret0, _ := ret[0].(*types.LeafList) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// GetLeaves indicates an expected call of GetLeaves. +func (mr *MockClientMockRecorder) GetLeaves(arg0, arg1 interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLeaves", reflect.TypeOf((*MockClient)(nil).GetLeaves), arg0, arg1) +} + +// GetTreeHead mocks base method. +func (m *MockClient) GetTreeHead(arg0 context.Context) (*types.TreeHead, error) { +	m.ctrl.T.Helper() +	ret := m.ctrl.Call(m, "GetTreeHead", arg0) +	ret0, _ := ret[0].(*types.TreeHead) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// GetTreeHead indicates an expected call of GetTreeHead. +func (mr *MockClientMockRecorder) GetTreeHead(arg0 interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTreeHead", reflect.TypeOf((*MockClient)(nil).GetTreeHead), arg0) +} diff --git a/pkg/mocks/trillian.go b/pkg/mocks/trillian.go new file mode 100644 index 0000000..8aa3a58 --- /dev/null +++ b/pkg/mocks/trillian.go @@ -0,0 +1,317 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/google/trillian (interfaces: TrillianLogClient) + +// Package mocks is a generated GoMock package. +package mocks + +import ( +	context "context" +	reflect "reflect" + +	gomock "github.com/golang/mock/gomock" +	trillian "github.com/google/trillian" +	grpc "google.golang.org/grpc" +) + +// MockTrillianLogClient is a mock of TrillianLogClient interface. +type MockTrillianLogClient struct { +	ctrl     *gomock.Controller +	recorder *MockTrillianLogClientMockRecorder +} + +// MockTrillianLogClientMockRecorder is the mock recorder for MockTrillianLogClient. +type MockTrillianLogClientMockRecorder struct { +	mock *MockTrillianLogClient +} + +// NewMockTrillianLogClient creates a new mock instance. +func NewMockTrillianLogClient(ctrl *gomock.Controller) *MockTrillianLogClient { +	mock := &MockTrillianLogClient{ctrl: ctrl} +	mock.recorder = &MockTrillianLogClientMockRecorder{mock} +	return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockTrillianLogClient) EXPECT() *MockTrillianLogClientMockRecorder { +	return m.recorder +} + +// AddSequencedLeaf mocks base method. +func (m *MockTrillianLogClient) AddSequencedLeaf(arg0 context.Context, arg1 *trillian.AddSequencedLeafRequest, arg2 ...grpc.CallOption) (*trillian.AddSequencedLeafResponse, error) { +	m.ctrl.T.Helper() +	varargs := []interface{}{arg0, arg1} +	for _, a := range arg2 { +		varargs = append(varargs, a) +	} +	ret := m.ctrl.Call(m, "AddSequencedLeaf", varargs...) +	ret0, _ := ret[0].(*trillian.AddSequencedLeafResponse) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// AddSequencedLeaf indicates an expected call of AddSequencedLeaf. +func (mr *MockTrillianLogClientMockRecorder) AddSequencedLeaf(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	varargs := append([]interface{}{arg0, arg1}, arg2...) +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddSequencedLeaf", reflect.TypeOf((*MockTrillianLogClient)(nil).AddSequencedLeaf), varargs...) +} + +// AddSequencedLeaves mocks base method. +func (m *MockTrillianLogClient) AddSequencedLeaves(arg0 context.Context, arg1 *trillian.AddSequencedLeavesRequest, arg2 ...grpc.CallOption) (*trillian.AddSequencedLeavesResponse, error) { +	m.ctrl.T.Helper() +	varargs := []interface{}{arg0, arg1} +	for _, a := range arg2 { +		varargs = append(varargs, a) +	} +	ret := m.ctrl.Call(m, "AddSequencedLeaves", varargs...) +	ret0, _ := ret[0].(*trillian.AddSequencedLeavesResponse) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// AddSequencedLeaves indicates an expected call of AddSequencedLeaves. +func (mr *MockTrillianLogClientMockRecorder) AddSequencedLeaves(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	varargs := append([]interface{}{arg0, arg1}, arg2...) +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddSequencedLeaves", reflect.TypeOf((*MockTrillianLogClient)(nil).AddSequencedLeaves), varargs...) +} + +// GetConsistencyProof mocks base method. +func (m *MockTrillianLogClient) GetConsistencyProof(arg0 context.Context, arg1 *trillian.GetConsistencyProofRequest, arg2 ...grpc.CallOption) (*trillian.GetConsistencyProofResponse, error) { +	m.ctrl.T.Helper() +	varargs := []interface{}{arg0, arg1} +	for _, a := range arg2 { +		varargs = append(varargs, a) +	} +	ret := m.ctrl.Call(m, "GetConsistencyProof", varargs...) +	ret0, _ := ret[0].(*trillian.GetConsistencyProofResponse) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// GetConsistencyProof indicates an expected call of GetConsistencyProof. +func (mr *MockTrillianLogClientMockRecorder) GetConsistencyProof(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	varargs := append([]interface{}{arg0, arg1}, arg2...) +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConsistencyProof", reflect.TypeOf((*MockTrillianLogClient)(nil).GetConsistencyProof), varargs...) +} + +// GetEntryAndProof mocks base method. +func (m *MockTrillianLogClient) GetEntryAndProof(arg0 context.Context, arg1 *trillian.GetEntryAndProofRequest, arg2 ...grpc.CallOption) (*trillian.GetEntryAndProofResponse, error) { +	m.ctrl.T.Helper() +	varargs := []interface{}{arg0, arg1} +	for _, a := range arg2 { +		varargs = append(varargs, a) +	} +	ret := m.ctrl.Call(m, "GetEntryAndProof", varargs...) +	ret0, _ := ret[0].(*trillian.GetEntryAndProofResponse) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// GetEntryAndProof indicates an expected call of GetEntryAndProof. +func (mr *MockTrillianLogClientMockRecorder) GetEntryAndProof(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	varargs := append([]interface{}{arg0, arg1}, arg2...) +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEntryAndProof", reflect.TypeOf((*MockTrillianLogClient)(nil).GetEntryAndProof), varargs...) +} + +// GetInclusionProof mocks base method. +func (m *MockTrillianLogClient) GetInclusionProof(arg0 context.Context, arg1 *trillian.GetInclusionProofRequest, arg2 ...grpc.CallOption) (*trillian.GetInclusionProofResponse, error) { +	m.ctrl.T.Helper() +	varargs := []interface{}{arg0, arg1} +	for _, a := range arg2 { +		varargs = append(varargs, a) +	} +	ret := m.ctrl.Call(m, "GetInclusionProof", varargs...) +	ret0, _ := ret[0].(*trillian.GetInclusionProofResponse) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// GetInclusionProof indicates an expected call of GetInclusionProof. +func (mr *MockTrillianLogClientMockRecorder) GetInclusionProof(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	varargs := append([]interface{}{arg0, arg1}, arg2...) +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInclusionProof", reflect.TypeOf((*MockTrillianLogClient)(nil).GetInclusionProof), varargs...) +} + +// GetInclusionProofByHash mocks base method. +func (m *MockTrillianLogClient) GetInclusionProofByHash(arg0 context.Context, arg1 *trillian.GetInclusionProofByHashRequest, arg2 ...grpc.CallOption) (*trillian.GetInclusionProofByHashResponse, error) { +	m.ctrl.T.Helper() +	varargs := []interface{}{arg0, arg1} +	for _, a := range arg2 { +		varargs = append(varargs, a) +	} +	ret := m.ctrl.Call(m, "GetInclusionProofByHash", varargs...) +	ret0, _ := ret[0].(*trillian.GetInclusionProofByHashResponse) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// GetInclusionProofByHash indicates an expected call of GetInclusionProofByHash. +func (mr *MockTrillianLogClientMockRecorder) GetInclusionProofByHash(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	varargs := append([]interface{}{arg0, arg1}, arg2...) +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInclusionProofByHash", reflect.TypeOf((*MockTrillianLogClient)(nil).GetInclusionProofByHash), varargs...) +} + +// GetLatestSignedLogRoot mocks base method. +func (m *MockTrillianLogClient) GetLatestSignedLogRoot(arg0 context.Context, arg1 *trillian.GetLatestSignedLogRootRequest, arg2 ...grpc.CallOption) (*trillian.GetLatestSignedLogRootResponse, error) { +	m.ctrl.T.Helper() +	varargs := []interface{}{arg0, arg1} +	for _, a := range arg2 { +		varargs = append(varargs, a) +	} +	ret := m.ctrl.Call(m, "GetLatestSignedLogRoot", varargs...) +	ret0, _ := ret[0].(*trillian.GetLatestSignedLogRootResponse) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// GetLatestSignedLogRoot indicates an expected call of GetLatestSignedLogRoot. +func (mr *MockTrillianLogClientMockRecorder) GetLatestSignedLogRoot(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	varargs := append([]interface{}{arg0, arg1}, arg2...) +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLatestSignedLogRoot", reflect.TypeOf((*MockTrillianLogClient)(nil).GetLatestSignedLogRoot), varargs...) +} + +// GetLeavesByHash mocks base method. +func (m *MockTrillianLogClient) GetLeavesByHash(arg0 context.Context, arg1 *trillian.GetLeavesByHashRequest, arg2 ...grpc.CallOption) (*trillian.GetLeavesByHashResponse, error) { +	m.ctrl.T.Helper() +	varargs := []interface{}{arg0, arg1} +	for _, a := range arg2 { +		varargs = append(varargs, a) +	} +	ret := m.ctrl.Call(m, "GetLeavesByHash", varargs...) +	ret0, _ := ret[0].(*trillian.GetLeavesByHashResponse) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// GetLeavesByHash indicates an expected call of GetLeavesByHash. +func (mr *MockTrillianLogClientMockRecorder) GetLeavesByHash(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	varargs := append([]interface{}{arg0, arg1}, arg2...) +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLeavesByHash", reflect.TypeOf((*MockTrillianLogClient)(nil).GetLeavesByHash), varargs...) +} + +// GetLeavesByIndex mocks base method. +func (m *MockTrillianLogClient) GetLeavesByIndex(arg0 context.Context, arg1 *trillian.GetLeavesByIndexRequest, arg2 ...grpc.CallOption) (*trillian.GetLeavesByIndexResponse, error) { +	m.ctrl.T.Helper() +	varargs := []interface{}{arg0, arg1} +	for _, a := range arg2 { +		varargs = append(varargs, a) +	} +	ret := m.ctrl.Call(m, "GetLeavesByIndex", varargs...) +	ret0, _ := ret[0].(*trillian.GetLeavesByIndexResponse) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// GetLeavesByIndex indicates an expected call of GetLeavesByIndex. +func (mr *MockTrillianLogClientMockRecorder) GetLeavesByIndex(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	varargs := append([]interface{}{arg0, arg1}, arg2...) +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLeavesByIndex", reflect.TypeOf((*MockTrillianLogClient)(nil).GetLeavesByIndex), varargs...) +} + +// GetLeavesByRange mocks base method. +func (m *MockTrillianLogClient) GetLeavesByRange(arg0 context.Context, arg1 *trillian.GetLeavesByRangeRequest, arg2 ...grpc.CallOption) (*trillian.GetLeavesByRangeResponse, error) { +	m.ctrl.T.Helper() +	varargs := []interface{}{arg0, arg1} +	for _, a := range arg2 { +		varargs = append(varargs, a) +	} +	ret := m.ctrl.Call(m, "GetLeavesByRange", varargs...) +	ret0, _ := ret[0].(*trillian.GetLeavesByRangeResponse) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// GetLeavesByRange indicates an expected call of GetLeavesByRange. +func (mr *MockTrillianLogClientMockRecorder) GetLeavesByRange(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	varargs := append([]interface{}{arg0, arg1}, arg2...) +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLeavesByRange", reflect.TypeOf((*MockTrillianLogClient)(nil).GetLeavesByRange), varargs...) +} + +// GetSequencedLeafCount mocks base method. +func (m *MockTrillianLogClient) GetSequencedLeafCount(arg0 context.Context, arg1 *trillian.GetSequencedLeafCountRequest, arg2 ...grpc.CallOption) (*trillian.GetSequencedLeafCountResponse, error) { +	m.ctrl.T.Helper() +	varargs := []interface{}{arg0, arg1} +	for _, a := range arg2 { +		varargs = append(varargs, a) +	} +	ret := m.ctrl.Call(m, "GetSequencedLeafCount", varargs...) +	ret0, _ := ret[0].(*trillian.GetSequencedLeafCountResponse) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// GetSequencedLeafCount indicates an expected call of GetSequencedLeafCount. +func (mr *MockTrillianLogClientMockRecorder) GetSequencedLeafCount(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	varargs := append([]interface{}{arg0, arg1}, arg2...) +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSequencedLeafCount", reflect.TypeOf((*MockTrillianLogClient)(nil).GetSequencedLeafCount), varargs...) +} + +// InitLog mocks base method. +func (m *MockTrillianLogClient) InitLog(arg0 context.Context, arg1 *trillian.InitLogRequest, arg2 ...grpc.CallOption) (*trillian.InitLogResponse, error) { +	m.ctrl.T.Helper() +	varargs := []interface{}{arg0, arg1} +	for _, a := range arg2 { +		varargs = append(varargs, a) +	} +	ret := m.ctrl.Call(m, "InitLog", varargs...) +	ret0, _ := ret[0].(*trillian.InitLogResponse) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// InitLog indicates an expected call of InitLog. +func (mr *MockTrillianLogClientMockRecorder) InitLog(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	varargs := append([]interface{}{arg0, arg1}, arg2...) +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InitLog", reflect.TypeOf((*MockTrillianLogClient)(nil).InitLog), varargs...) +} + +// QueueLeaf mocks base method. +func (m *MockTrillianLogClient) QueueLeaf(arg0 context.Context, arg1 *trillian.QueueLeafRequest, arg2 ...grpc.CallOption) (*trillian.QueueLeafResponse, error) { +	m.ctrl.T.Helper() +	varargs := []interface{}{arg0, arg1} +	for _, a := range arg2 { +		varargs = append(varargs, a) +	} +	ret := m.ctrl.Call(m, "QueueLeaf", varargs...) +	ret0, _ := ret[0].(*trillian.QueueLeafResponse) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// QueueLeaf indicates an expected call of QueueLeaf. +func (mr *MockTrillianLogClientMockRecorder) QueueLeaf(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	varargs := append([]interface{}{arg0, arg1}, arg2...) +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueueLeaf", reflect.TypeOf((*MockTrillianLogClient)(nil).QueueLeaf), varargs...) +} + +// QueueLeaves mocks base method. +func (m *MockTrillianLogClient) QueueLeaves(arg0 context.Context, arg1 *trillian.QueueLeavesRequest, arg2 ...grpc.CallOption) (*trillian.QueueLeavesResponse, error) { +	m.ctrl.T.Helper() +	varargs := []interface{}{arg0, arg1} +	for _, a := range arg2 { +		varargs = append(varargs, a) +	} +	ret := m.ctrl.Call(m, "QueueLeaves", varargs...) +	ret0, _ := ret[0].(*trillian.QueueLeavesResponse) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// QueueLeaves indicates an expected call of QueueLeaves. +func (mr *MockTrillianLogClientMockRecorder) QueueLeaves(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	varargs := append([]interface{}{arg0, arg1}, arg2...) +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueueLeaves", reflect.TypeOf((*MockTrillianLogClient)(nil).QueueLeaves), varargs...) +} diff --git a/pkg/state/state_manager.go b/pkg/state/state_manager.go new file mode 100644 index 0000000..dfa73f4 --- /dev/null +++ b/pkg/state/state_manager.go @@ -0,0 +1,154 @@ +package state + +import ( +	"context" +	"crypto" +	"fmt" +	"reflect" +	"sync" +	"time" + +	"github.com/golang/glog" +	"github.com/google/certificate-transparency-go/schedule" +	"github.com/system-transparency/stfe/pkg/trillian" +	"github.com/system-transparency/stfe/pkg/types" +) + +// StateManager coordinates access to the 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.SignedTreeHead, 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 +	signer   crypto.Signer +	interval time.Duration +	deadline time.Duration +	sync.RWMutex + +	// cosigned is the current cosigned tree head that is being served +	cosigned types.SignedTreeHead + +	// 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 = *sth +	sm.tosign = *sth +	sm.cosignature = map[[types.HashSize]byte]*types.SigIdent{ +		*sth.SigIdent[0].KeyHash: sth.SigIdent[0], // log signature +	} +	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) +		nextTreeHead, err := sm.Latest(ictx) +		if err != nil { +			glog.Warningf("rotate failed: Latest: %v", err) +			return +		} + +		sm.Lock() +		defer sm.Unlock() +		sm.rotate(nextTreeHead) +	}) +} + +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) +	} +	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.SignedTreeHead, error) { +	sm.RLock() +	defer sm.RUnlock() +	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.TreeHead, sm.tosign.TreeHead) { +		// 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.TreeHead = sm.tosign.TreeHead +	sm.cosigned.SigIdent = cosignatures + +	// Update to-sign tree head +	sm.tosign = *next +	sm.cosignature = map[[types.HashSize]byte]*types.SigIdent{ +		*next.SigIdent[0].KeyHash: next.SigIdent[0], // log signature +	} +	glog.V(3).Infof("rotated tree heads") +} diff --git a/pkg/state/state_manager_test.go b/pkg/state/state_manager_test.go new file mode 100644 index 0000000..08990cc --- /dev/null +++ b/pkg/state/state_manager_test.go @@ -0,0 +1,393 @@ +package state + +import ( +	"bytes" +	"context" +	"crypto" +	"crypto/ed25519" +	"crypto/rand" +	"fmt" +	"reflect" +	"testing" +	"time" + +	"github.com/golang/mock/gomock" +	"github.com/system-transparency/stfe/pkg/mocks" +	"github.com/system-transparency/stfe/pkg/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 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) +	} +} diff --git a/pkg/trillian/client.go b/pkg/trillian/client.go new file mode 100644 index 0000000..9523e56 --- /dev/null +++ b/pkg/trillian/client.go @@ -0,0 +1,178 @@ +package trillian + +import ( +	"context" +	"fmt" + +	"github.com/golang/glog" +	"github.com/google/trillian" +	ttypes "github.com/google/trillian/types" +	"github.com/system-transparency/stfe/pkg/types" +	"google.golang.org/grpc/codes" +) + +type Client interface { +	AddLeaf(context.Context, *types.LeafRequest) error +	GetConsistencyProof(context.Context, *types.ConsistencyProofRequest) (*types.ConsistencyProof, error) +	GetTreeHead(context.Context) (*types.TreeHead, error) +	GetInclusionProof(context.Context, *types.InclusionProofRequest) (*types.InclusionProof, error) +	GetLeaves(context.Context, *types.LeavesRequest) (*types.LeafList, error) +} + +// TrillianClient is a wrapper around the Trillian gRPC client. +type TrillianClient struct { +	// TreeID is a Merkle tree identifier that Trillian uses +	TreeID int64 + +	// GRPC is a Trillian gRPC client +	GRPC trillian.TrillianLogClient +} + +func (c *TrillianClient) 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 *TrillianClient) 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)) +	} +	return treeHeadFromLogRoot(&r), nil +} + +func (c *TrillianClient) GetConsistencyProof(ctx context.Context, req *types.ConsistencyProofRequest) (*types.ConsistencyProof, error) { +	rsp, err := c.GRPC.GetConsistencyProof(ctx, &trillian.GetConsistencyProofRequest{ +		LogId:          c.TreeID, +		FirstTreeSize:  int64(req.OldSize), +		SecondTreeSize: int64(req.NewSize), +	}) +	if err != nil { +		return nil, fmt.Errorf("backend failure: %v", err) +	} +	if rsp == nil { +		return nil, fmt.Errorf("no response") +	} +	if rsp.Proof == nil { +		return nil, fmt.Errorf("no consistency proof") +	} +	if len(rsp.Proof.Hashes) == 0 { +		return nil, fmt.Errorf("not a consistency proof: empty") +	} +	path, err := nodePathFromHashes(rsp.Proof.Hashes) +	if err != nil { +		return nil, fmt.Errorf("not a consistency proof: %v", err) +	} +	return &types.ConsistencyProof{ +		OldSize: req.OldSize, +		NewSize: req.NewSize, +		Path:    path, +	}, nil +} + +func (c *TrillianClient) GetInclusionProof(ctx context.Context, req *types.InclusionProofRequest) (*types.InclusionProof, error) { +	rsp, err := c.GRPC.GetInclusionProofByHash(ctx, &trillian.GetInclusionProofByHashRequest{ +		LogId:           c.TreeID, +		LeafHash:        req.LeafHash[:], +		TreeSize:        int64(req.TreeSize), +		OrderBySequence: true, +	}) +	if err != nil { +		return nil, fmt.Errorf("backend failure: %v", err) +	} +	if rsp == nil { +		return nil, fmt.Errorf("no response") +	} +	if len(rsp.Proof) != 1 { +		return nil, fmt.Errorf("bad proof count: %d", len(rsp.Proof)) +	} +	proof := rsp.Proof[0] +	if len(proof.Hashes) == 0 { +		return nil, fmt.Errorf("not an inclusion proof: empty") +	} +	path, err := nodePathFromHashes(proof.Hashes) +	if err != nil { +		return nil, fmt.Errorf("not an inclusion proof: %v", err) +	} +	return &types.InclusionProof{ +		TreeSize:  req.TreeSize, +		LeafIndex: uint64(proof.LeafIndex), +		Path:      path, +	}, nil +} + +func (c *TrillianClient) GetLeaves(ctx context.Context, req *types.LeavesRequest) (*types.LeafList, error) { +	rsp, err := c.GRPC.GetLeavesByRange(ctx, &trillian.GetLeavesByRangeRequest{ +		LogId:      c.TreeID, +		StartIndex: int64(req.StartSize), +		Count:      int64(req.EndSize-req.StartSize) + 1, +	}) +	if err != nil { +		return nil, fmt.Errorf("backend failure: %v", err) +	} +	if rsp == nil { +		return nil, fmt.Errorf("no response") +	} +	if got, want := len(rsp.Leaves), int(req.EndSize-req.StartSize+1); got != want { +		return nil, fmt.Errorf("unexpected number of leaves: %d", got) +	} +	var list types.LeafList +	for i, leaf := range rsp.Leaves { +		leafIndex := int64(req.StartSize + uint64(i)) +		if leafIndex != leaf.LeafIndex { +			return nil, fmt.Errorf("unexpected leaf(%d): got index %d", leafIndex, leaf.LeafIndex) +		} + +		var l types.Leaf +		if err := l.Unmarshal(leaf.LeafValue); err != nil { +			return nil, fmt.Errorf("unexpected leaf(%d): %v", leafIndex, err) +		} +		list = append(list[:], &l) +	} +	return &list, nil +} diff --git a/pkg/trillian/client_test.go b/pkg/trillian/client_test.go new file mode 100644 index 0000000..6b3d881 --- /dev/null +++ b/pkg/trillian/client_test.go @@ -0,0 +1,533 @@ +package trillian + +import ( +	"context" +	"fmt" +	"reflect" +	"testing" + +	"github.com/golang/mock/gomock" +	"github.com/google/trillian" +	ttypes "github.com/google/trillian/types" +	"github.com/system-transparency/stfe/pkg/mocks" +	"github.com/system-transparency/stfe/pkg/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 := mocks.NewMockTrillianLogClient(ctrl) +			grpc.EXPECT().QueueLeaf(gomock.Any(), gomock.Any()).Return(table.rsp, table.err) +			client := TrillianClient{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 := mocks.NewMockTrillianLogClient(ctrl) +			grpc.EXPECT().GetLatestSignedLogRoot(gomock.Any(), gomock.Any()).Return(table.rsp, table.err) +			client := TrillianClient{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) { +	req := &types.ConsistencyProofRequest{ +		OldSize: 1, +		NewSize: 3, +	} +	for _, table := range []struct { +		description string +		req         *types.ConsistencyProofRequest +		rsp         *trillian.GetConsistencyProofResponse +		err         error +		wantErr     bool +		wantProof   *types.ConsistencyProof +	}{ +		{ +			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 consistency proof", +			req:         req, +			rsp:         &trillian.GetConsistencyProofResponse{}, +			wantErr:     true, +		}, +		{ +			description: "invalid: not a consistency proof (1/2)", +			req:         req, +			rsp: &trillian.GetConsistencyProofResponse{ +				Proof: &trillian.Proof{ +					Hashes: [][]byte{}, +				}, +			}, +			wantErr: true, +		}, +		{ +			description: "invalid: not a consistency proof (2/2)", +			req:         req, +			rsp: &trillian.GetConsistencyProofResponse{ +				Proof: &trillian.Proof{ +					Hashes: [][]byte{ +						make([]byte, types.HashSize), +						make([]byte, types.HashSize+1), +					}, +				}, +			}, +			wantErr: true, +		}, +		{ +			description: "valid", +			req:         req, +			rsp: &trillian.GetConsistencyProofResponse{ +				Proof: &trillian.Proof{ +					Hashes: [][]byte{ +						make([]byte, types.HashSize), +						make([]byte, types.HashSize), +					}, +				}, +			}, +			wantProof: &types.ConsistencyProof{ +				OldSize: 1, +				NewSize: 3, +				Path: []*[types.HashSize]byte{ +					&[types.HashSize]byte{}, +					&[types.HashSize]byte{}, +				}, +			}, +		}, +	} { +		// Run deferred functions at the end of each iteration +		func() { +			ctrl := gomock.NewController(t) +			defer ctrl.Finish() +			grpc := mocks.NewMockTrillianLogClient(ctrl) +			grpc.EXPECT().GetConsistencyProof(gomock.Any(), gomock.Any()).Return(table.rsp, table.err) +			client := TrillianClient{GRPC: grpc} + +			proof, err := client.GetConsistencyProof(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) +			} +			if err != nil { +				return +			} +			if got, want := proof, table.wantProof; !reflect.DeepEqual(got, want) { +				t.Errorf("got proof\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) +			} +		}() +	} +} + +func TestGetInclusionProof(t *testing.T) { +	req := &types.InclusionProofRequest{ +		TreeSize: 4, +		LeafHash: &[types.HashSize]byte{}, +	} +	for _, table := range []struct { +		description string +		req         *types.InclusionProofRequest +		rsp         *trillian.GetInclusionProofByHashResponse +		err         error +		wantErr     bool +		wantProof   *types.InclusionProof +	}{ +		{ +			description: "invalid: backend failure", +			req:         req, +			err:         fmt.Errorf("something went wrong"), +			wantErr:     true, +		}, +		{ +			description: "invalid: no response", +			req:         req, +			wantErr:     true, +		}, +		{ +			description: "invalid: bad proof count", +			req:         req, +			rsp: &trillian.GetInclusionProofByHashResponse{ +				Proof: []*trillian.Proof{ +					&trillian.Proof{}, +					&trillian.Proof{}, +				}, +			}, +			wantErr: true, +		}, +		{ +			description: "invalid: not an inclusion proof (1/2)", +			req:         req, +			rsp: &trillian.GetInclusionProofByHashResponse{ +				Proof: []*trillian.Proof{ +					&trillian.Proof{ +						LeafIndex: 1, +						Hashes:    [][]byte{}, +					}, +				}, +			}, +			wantErr: true, +		}, +		{ +			description: "invalid: not an inclusion proof (2/2)", +			req:         req, +			rsp: &trillian.GetInclusionProofByHashResponse{ +				Proof: []*trillian.Proof{ +					&trillian.Proof{ +						LeafIndex: 1, +						Hashes: [][]byte{ +							make([]byte, types.HashSize), +							make([]byte, types.HashSize+1), +						}, +					}, +				}, +			}, +			wantErr: true, +		}, +		{ +			description: "valid", +			req:         req, +			rsp: &trillian.GetInclusionProofByHashResponse{ +				Proof: []*trillian.Proof{ +					&trillian.Proof{ +						LeafIndex: 1, +						Hashes: [][]byte{ +							make([]byte, types.HashSize), +							make([]byte, types.HashSize), +						}, +					}, +				}, +			}, +			wantProof: &types.InclusionProof{ +				TreeSize:  4, +				LeafIndex: 1, +				Path: []*[types.HashSize]byte{ +					&[types.HashSize]byte{}, +					&[types.HashSize]byte{}, +				}, +			}, +		}, +	} { +		// Run deferred functions at the end of each iteration +		func() { +			ctrl := gomock.NewController(t) +			defer ctrl.Finish() +			grpc := mocks.NewMockTrillianLogClient(ctrl) +			grpc.EXPECT().GetInclusionProofByHash(gomock.Any(), gomock.Any()).Return(table.rsp, table.err) +			client := TrillianClient{GRPC: grpc} + +			proof, err := client.GetInclusionProof(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) +			} +			if err != nil { +				return +			} +			if got, want := proof, table.wantProof; !reflect.DeepEqual(got, want) { +				t.Errorf("got proof\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) +			} +		}() +	} +} + +func TestGetLeaves(t *testing.T) { +	req := &types.LeavesRequest{ +		StartSize: 1, +		EndSize:   2, +	} +	firstLeaf := &types.Leaf{ +		Message: types.Message{ +			ShardHint: 0, +			Checksum:  &[types.HashSize]byte{}, +		}, +		SigIdent: types.SigIdent{ +			Signature: &[types.SignatureSize]byte{}, +			KeyHash:   &[types.HashSize]byte{}, +		}, +	} +	secondLeaf := &types.Leaf{ +		Message: types.Message{ +			ShardHint: 0, +			Checksum:  &[types.HashSize]byte{}, +		}, +		SigIdent: types.SigIdent{ +			Signature: &[types.SignatureSize]byte{}, +			KeyHash:   &[types.HashSize]byte{}, +		}, +	} + +	for _, table := range []struct { +		description string +		req         *types.LeavesRequest +		rsp         *trillian.GetLeavesByRangeResponse +		err         error +		wantErr     bool +		wantLeaves  *types.LeafList +	}{ +		{ +			description: "invalid: backend failure", +			req:         req, +			err:         fmt.Errorf("something went wrong"), +			wantErr:     true, +		}, +		{ +			description: "invalid: no response", +			req:         req, +			wantErr:     true, +		}, +		{ +			description: "invalid: unexpected number of leaves", +			req:         req, +			rsp: &trillian.GetLeavesByRangeResponse{ +				Leaves: []*trillian.LogLeaf{ +					&trillian.LogLeaf{ +						LeafValue: firstLeaf.Marshal(), +						LeafIndex: 1, +					}, +				}, +			}, +			wantErr: true, +		}, +		{ +			description: "invalid: unexpected leaf (1/2)", +			req:         req, +			rsp: &trillian.GetLeavesByRangeResponse{ +				Leaves: []*trillian.LogLeaf{ +					&trillian.LogLeaf{ +						LeafValue: firstLeaf.Marshal(), +						LeafIndex: 1, +					}, +					&trillian.LogLeaf{ +						LeafValue: secondLeaf.Marshal(), +						LeafIndex: 3, +					}, +				}, +			}, +			wantErr: true, +		}, +		{ +			description: "invalid: unexpected leaf (2/2)", +			req:         req, +			rsp: &trillian.GetLeavesByRangeResponse{ +				Leaves: []*trillian.LogLeaf{ +					&trillian.LogLeaf{ +						LeafValue: firstLeaf.Marshal(), +						LeafIndex: 1, +					}, +					&trillian.LogLeaf{ +						LeafValue: secondLeaf.Marshal()[1:], +						LeafIndex: 2, +					}, +				}, +			}, +			wantErr: true, +		}, +		{ +			description: "valid", +			req:         req, +			rsp: &trillian.GetLeavesByRangeResponse{ +				Leaves: []*trillian.LogLeaf{ +					&trillian.LogLeaf{ +						LeafValue: firstLeaf.Marshal(), +						LeafIndex: 1, +					}, +					&trillian.LogLeaf{ +						LeafValue: secondLeaf.Marshal(), +						LeafIndex: 2, +					}, +				}, +			}, +			wantLeaves: &types.LeafList{ +				firstLeaf, +				secondLeaf, +			}, +		}, +	} { +		// Run deferred functions at the end of each iteration +		func() { +			ctrl := gomock.NewController(t) +			defer ctrl.Finish() +			grpc := mocks.NewMockTrillianLogClient(ctrl) +			grpc.EXPECT().GetLeavesByRange(gomock.Any(), gomock.Any()).Return(table.rsp, table.err) +			client := TrillianClient{GRPC: grpc} + +			leaves, err := client.GetLeaves(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) +			} +			if err != nil { +				return +			} +			if got, want := leaves, table.wantLeaves; !reflect.DeepEqual(got, want) { +				t.Errorf("got leaves\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) +			} +		}() +	} +} diff --git a/pkg/trillian/util.go b/pkg/trillian/util.go new file mode 100644 index 0000000..4cf31fb --- /dev/null +++ b/pkg/trillian/util.go @@ -0,0 +1,33 @@ +package trillian + +import ( +	"fmt" + +	trillian "github.com/google/trillian/types" +	siglog "github.com/system-transparency/stfe/pkg/types" +) + +func treeHeadFromLogRoot(lr *trillian.LogRootV1) *siglog.TreeHead { +	var hash [siglog.HashSize]byte +	th := siglog.TreeHead{ +		Timestamp: uint64(lr.TimestampNanos / 1000 / 1000 / 1000), +		TreeSize:  uint64(lr.TreeSize), +		RootHash:  &hash, +	} +	copy(th.RootHash[:], lr.RootHash) +	return &th +} + +func nodePathFromHashes(hashes [][]byte) ([]*[siglog.HashSize]byte, error) { +	var path []*[siglog.HashSize]byte +	for _, hash := range hashes { +		if len(hash) != siglog.HashSize { +			return nil, fmt.Errorf("unexpected hash length: %v", len(hash)) +		} + +		var h [siglog.HashSize]byte +		copy(h[:], hash) +		path = append(path, &h) +	} +	return path, nil +} diff --git a/pkg/types/ascii.go b/pkg/types/ascii.go new file mode 100644 index 0000000..d27d79b --- /dev/null +++ b/pkg/types/ascii.go @@ -0,0 +1,421 @@ +package types + +import ( +	"bytes" +	"encoding/hex" +	"fmt" +	"io" +	"io/ioutil" +	"strconv" +) + +const ( +	// Delim is a key-value separator +	Delim = "=" + +	// EOL is a line sepator +	EOL = "\n" + +	// NumField* is the number of unique keys in an incoming ASCII message +	NumFieldLeaf                    = 4 +	NumFieldSignedTreeHead          = 5 +	NumFieldConsistencyProof        = 3 +	NumFieldInclusionProof          = 3 +	NumFieldLeavesRequest           = 2 +	NumFieldInclusionProofRequest   = 2 +	NumFieldConsistencyProofRequest = 2 +	NumFieldLeafRequest             = 5 +	NumFieldCosignatureRequest      = 2 + +	// New leaf keys +	ShardHint            = "shard_hint" +	Checksum             = "checksum" +	SignatureOverMessage = "signature_over_message" +	VerificationKey      = "verification_key" +	DomainHint           = "domain_hint" + +	// Inclusion proof keys +	LeafHash      = "leaf_hash" +	LeafIndex     = "leaf_index" +	InclusionPath = "inclusion_path" + +	// Consistency proof keys +	NewSize         = "new_size" +	OldSize         = "old_size" +	ConsistencyPath = "consistency_path" + +	// Range of leaves keys +	StartSize = "start_size" +	EndSize   = "end_size" + +	// Tree head keys +	Timestamp = "timestamp" +	TreeSize  = "tree_size" +	RootHash  = "root_hash" + +	// Signature and signer-identity keys +	Signature = "signature" +	KeyHash   = "key_hash" +) + +// MessageASCI is a wrapper that manages ASCII key-value pairs +type MessageASCII struct { +	m map[string][]string +} + +// NewMessageASCII unpacks an incoming ASCII message +func NewMessageASCII(r io.Reader, numFieldExpected int) (*MessageASCII, error) { +	buf, err := ioutil.ReadAll(r) +	if err != nil { +		return nil, fmt.Errorf("ReadAll: %v", err) +	} +	lines := bytes.Split(buf, []byte(EOL)) +	if len(lines) <= 1 { +		return nil, fmt.Errorf("Not enough lines: empty") +	} +	lines = lines[:len(lines)-1] // valid message => split gives empty last line + +	msg := MessageASCII{make(map[string][]string)} +	for _, line := range lines { +		split := bytes.Index(line, []byte(Delim)) +		if split == -1 { +			return nil, fmt.Errorf("invalid line: %v", string(line)) +		} + +		key := string(line[:split]) +		value := string(line[split+len(Delim):]) +		values, ok := msg.m[key] +		if !ok { +			values = nil +			msg.m[key] = values +		} +		msg.m[key] = append(values, value) +	} + +	if msg.NumField() != numFieldExpected { +		return nil, fmt.Errorf("Unexpected number of keys: %v", msg.NumField()) +	} +	return &msg, nil +} + +// NumField returns the number of unique keys +func (msg *MessageASCII) NumField() int { +	return len(msg.m) +} + +// GetStrings returns a list of strings +func (msg *MessageASCII) GetStrings(key string) []string { +	strs, ok := msg.m[key] +	if !ok { +		return nil +	} +	return strs +} + +// GetString unpacks a string +func (msg *MessageASCII) GetString(key string) (string, error) { +	strs := msg.GetStrings(key) +	if len(strs) != 1 { +		return "", fmt.Errorf("expected one string: %v", strs) +	} +	return strs[0], nil +} + +// GetUint64 unpacks an uint64 +func (msg *MessageASCII) GetUint64(key string) (uint64, error) { +	str, err := msg.GetString(key) +	if err != nil { +		return 0, fmt.Errorf("GetString: %v", err) +	} +	num, err := strconv.ParseUint(str, 10, 64) +	if err != nil { +		return 0, fmt.Errorf("ParseUint: %v", err) +	} +	return num, nil +} + +// GetHash unpacks a hash +func (msg *MessageASCII) GetHash(key string) (*[HashSize]byte, error) { +	str, err := msg.GetString(key) +	if err != nil { +		return nil, fmt.Errorf("GetString: %v", err) +	} + +	var hash [HashSize]byte +	if err := decodeHex(str, hash[:]); err != nil { +		return nil, fmt.Errorf("decodeHex: %v", err) +	} +	return &hash, nil +} + +// GetSignature unpacks a signature +func (msg *MessageASCII) GetSignature(key string) (*[SignatureSize]byte, error) { +	str, err := msg.GetString(key) +	if err != nil { +		return nil, fmt.Errorf("GetString: %v", err) +	} + +	var signature [SignatureSize]byte +	if err := decodeHex(str, signature[:]); err != nil { +		return nil, fmt.Errorf("decodeHex: %v", err) +	} +	return &signature, nil +} + +// GetVerificationKey unpacks a verification key +func (msg *MessageASCII) GetVerificationKey(key string) (*[VerificationKeySize]byte, error) { +	str, err := msg.GetString(key) +	if err != nil { +		return nil, fmt.Errorf("GetString: %v", err) +	} + +	var vk [VerificationKeySize]byte +	if err := decodeHex(str, vk[:]); err != nil { +		return nil, fmt.Errorf("decodeHex: %v", err) +	} +	return &vk, nil +} + +// decodeHex decodes a hex-encoded string into an already-sized byte slice +func decodeHex(str string, out []byte) error { +	buf, err := hex.DecodeString(str) +	if err != nil { +		return fmt.Errorf("DecodeString: %v", err) +	} +	if len(buf) != len(out) { +		return fmt.Errorf("invalid length: %v", len(buf)) +	} +	copy(out, buf) +	return nil +} + +/* + * + * MarshalASCII wrappers for types that the log server outputs + * + */ +func (l *Leaf) MarshalASCII(w io.Writer) error { +	if err := writeASCII(w, ShardHint, strconv.FormatUint(l.ShardHint, 10)); err != nil { +		return fmt.Errorf("writeASCII: %v", err) +	} +	if err := writeASCII(w, Checksum, hex.EncodeToString(l.Checksum[:])); err != nil { +		return fmt.Errorf("writeASCII: %v", err) +	} +	if err := writeASCII(w, SignatureOverMessage, hex.EncodeToString(l.Signature[:])); err != nil { +		return fmt.Errorf("writeASCII: %v", err) +	} +	if err := writeASCII(w, KeyHash, hex.EncodeToString(l.KeyHash[:])); err != nil { +		return fmt.Errorf("writeASCII: %v", err) +	} +	return nil +} + +func (sth *SignedTreeHead) MarshalASCII(w io.Writer) error { +	if err := writeASCII(w, Timestamp, strconv.FormatUint(sth.Timestamp, 10)); err != nil { +		return fmt.Errorf("writeASCII: %v", err) +	} +	if err := writeASCII(w, TreeSize, strconv.FormatUint(sth.TreeSize, 10)); err != nil { +		return fmt.Errorf("writeASCII: %v", err) +	} +	if err := writeASCII(w, RootHash, hex.EncodeToString(sth.RootHash[:])); err != nil { +		return fmt.Errorf("writeASCII: %v", err) +	} +	for _, sigident := range sth.SigIdent { +		if err := sigident.MarshalASCII(w); err != nil { +			return fmt.Errorf("MarshalASCII: %v", err) +		} +	} +	return nil +} + +func (si *SigIdent) MarshalASCII(w io.Writer) error { +	if err := writeASCII(w, Signature, hex.EncodeToString(si.Signature[:])); err != nil { +		return fmt.Errorf("writeASCII: %v", err) +	} +	if err := writeASCII(w, KeyHash, hex.EncodeToString(si.KeyHash[:])); err != nil { +		return fmt.Errorf("writeASCII: %v", err) +	} +	return nil +} + +func (p *ConsistencyProof) MarshalASCII(w io.Writer) error { +	if err := writeASCII(w, NewSize, strconv.FormatUint(p.NewSize, 10)); err != nil { +		return fmt.Errorf("writeASCII: %v", err) +	} +	if err := writeASCII(w, OldSize, strconv.FormatUint(p.OldSize, 10)); err != nil { +		return fmt.Errorf("writeASCII: %v", err) +	} +	for _, hash := range p.Path { +		if err := writeASCII(w, ConsistencyPath, hex.EncodeToString(hash[:])); err != nil { +			return fmt.Errorf("writeASCII: %v", err) +		} +	} +	return nil +} + +func (p *InclusionProof) MarshalASCII(w io.Writer) error { +	if err := writeASCII(w, TreeSize, strconv.FormatUint(p.TreeSize, 10)); err != nil { +		return fmt.Errorf("writeASCII: %v", err) +	} +	if err := writeASCII(w, LeafIndex, strconv.FormatUint(p.LeafIndex, 10)); err != nil { +		return fmt.Errorf("writeASCII: %v", err) +	} +	for _, hash := range p.Path { +		if err := writeASCII(w, InclusionPath, hex.EncodeToString(hash[:])); err != nil { +			return fmt.Errorf("writeASCII: %v", err) +		} +	} +	return nil +} + +func writeASCII(w io.Writer, key, value string) error { +	if _, err := fmt.Fprintf(w, "%s%s%s%s", key, Delim, value, EOL); err != nil { +		return fmt.Errorf("Fprintf: %v", err) +	} +	return nil +} + +/* + * + * Unmarshal ASCII wrappers that the log server and/or log clients receive. + * + */ +func (ll *LeafList) UnmarshalASCII(r io.Reader) error { +	return nil +} + +func (sth *SignedTreeHead) UnmarshalASCII(r io.Reader) error { +	msg, err := NewMessageASCII(r, NumFieldSignedTreeHead) +	if err != nil { +		return fmt.Errorf("NewMessageASCII: %v", err) +	} + +	// TreeHead +	if sth.Timestamp, err = msg.GetUint64(Timestamp); err != nil { +		return fmt.Errorf("GetUint64(Timestamp): %v", err) +	} +	if sth.TreeSize, err = msg.GetUint64(TreeSize); err != nil { +		return fmt.Errorf("GetUint64(TreeSize): %v", err) +	} +	if sth.RootHash, err = msg.GetHash(RootHash); err != nil { +		return fmt.Errorf("GetHash(RootHash): %v", err) +	} + +	// SigIdent +	signatures := msg.GetStrings(Signature) +	if len(signatures) == 0 { +		return fmt.Errorf("no signer") +	} +	keyHashes := msg.GetStrings(KeyHash) +	if len(signatures) != len(keyHashes) { +		return fmt.Errorf("mismatched signature-signer count") +	} +	sth.SigIdent = make([]*SigIdent, 0, len(signatures)) +	for i, n := 0, len(signatures); i < n; i++ { +		var signature [SignatureSize]byte +		if err := decodeHex(signatures[i], signature[:]); err != nil { +			return fmt.Errorf("decodeHex: %v", err) +		} +		var hash [HashSize]byte +		if err := decodeHex(keyHashes[i], hash[:]); err != nil { +			return fmt.Errorf("decodeHex: %v", err) +		} +		sth.SigIdent = append(sth.SigIdent, &SigIdent{ +			Signature: &signature, +			KeyHash:   &hash, +		}) +	} +	return nil +} + +func (p *InclusionProof) UnmarshalASCII(r io.Reader) error { +	return nil +} + +func (p *ConsistencyProof) UnmarshalASCII(r io.Reader) error { +	return nil +} + +func (req *InclusionProofRequest) UnmarshalASCII(r io.Reader) error { +	msg, err := NewMessageASCII(r, NumFieldInclusionProofRequest) +	if err != nil { +		return fmt.Errorf("NewMessageASCII: %v", err) +	} + +	if req.LeafHash, err = msg.GetHash(LeafHash); err != nil { +		return fmt.Errorf("GetHash(LeafHash): %v", err) +	} +	if req.TreeSize, err = msg.GetUint64(TreeSize); err != nil { +		return fmt.Errorf("GetUint64(TreeSize): %v", err) +	} +	return nil +} + +func (req *ConsistencyProofRequest) UnmarshalASCII(r io.Reader) error { +	msg, err := NewMessageASCII(r, NumFieldConsistencyProofRequest) +	if err != nil { +		return fmt.Errorf("NewMessageASCII: %v", err) +	} + +	if req.NewSize, err = msg.GetUint64(NewSize); err != nil { +		return fmt.Errorf("GetUint64(NewSize): %v", err) +	} +	if req.OldSize, err = msg.GetUint64(OldSize); err != nil { +		return fmt.Errorf("GetUint64(OldSize): %v", err) +	} +	return nil +} + +func (req *LeavesRequest) UnmarshalASCII(r io.Reader) error { +	msg, err := NewMessageASCII(r, NumFieldLeavesRequest) +	if err != nil { +		return fmt.Errorf("NewMessageASCII: %v", err) +	} + +	if req.StartSize, err = msg.GetUint64(StartSize); err != nil { +		return fmt.Errorf("GetUint64(StartSize): %v", err) +	} +	if req.EndSize, err = msg.GetUint64(EndSize); err != nil { +		return fmt.Errorf("GetUint64(EndSize): %v", err) +	} +	return nil +} + +func (req *LeafRequest) UnmarshalASCII(r io.Reader) error { +	msg, err := NewMessageASCII(r, NumFieldLeafRequest) +	if err != nil { +		return fmt.Errorf("NewMessageASCII: %v", err) +	} + +	if req.ShardHint, err = msg.GetUint64(ShardHint); err != nil { +		return fmt.Errorf("GetUint64(ShardHint): %v", err) +	} +	if req.Checksum, err = msg.GetHash(Checksum); err != nil { +		return fmt.Errorf("GetHash(Checksum): %v", err) +	} +	if req.Signature, err = msg.GetSignature(SignatureOverMessage); err != nil { +		return fmt.Errorf("GetSignature: %v", err) +	} +	if req.VerificationKey, err = msg.GetVerificationKey(VerificationKey); err != nil { +		return fmt.Errorf("GetVerificationKey: %v", err) +	} +	if req.DomainHint, err = msg.GetString(DomainHint); err != nil { +		return fmt.Errorf("GetString(DomainHint): %v", err) +	} +	return nil +} + +func (req *CosignatureRequest) UnmarshalASCII(r io.Reader) error { +	msg, err := NewMessageASCII(r, NumFieldCosignatureRequest) +	if err != nil { +		return fmt.Errorf("NewMessageASCII: %v", err) +	} + +	if req.Signature, err = msg.GetSignature(Signature); err != nil { +		return fmt.Errorf("GetSignature: %v", err) +	} +	if req.KeyHash, err = msg.GetHash(KeyHash); err != nil { +		return fmt.Errorf("GetHash(KeyHash): %v", err) +	} +	return nil +} diff --git a/pkg/types/ascii_test.go b/pkg/types/ascii_test.go new file mode 100644 index 0000000..92732f9 --- /dev/null +++ b/pkg/types/ascii_test.go @@ -0,0 +1,465 @@ +package types + +import ( +	"bytes" +	"fmt" +	"io" +	"reflect" +	"testing" +) + +/* + * + * MessageASCII methods and helpers + * + */ +func TestNewMessageASCII(t *testing.T) { +	for _, table := range []struct { +		description string +		input       io.Reader +		wantErr     bool +		wantMap     map[string][]string +	}{ +		{ +			description: "invalid: not enough lines", +			input:       bytes.NewBufferString(""), +			wantErr:     true, +		}, +		{ +			description: "invalid: lines must end with new line", +			input:       bytes.NewBufferString("k1=v1\nk2=v2"), +			wantErr:     true, +		}, +		{ +			description: "invalid: lines must not be empty", +			input:       bytes.NewBufferString("k1=v1\n\nk2=v2\n"), +			wantErr:     true, +		}, +		{ +			description: "invalid: wrong number of fields", +			input:       bytes.NewBufferString("k1=v1\n"), +			wantErr:     true, +		}, +		{ +			description: "valid", +			input:       bytes.NewBufferString("k1=v1\nk2=v2\nk2=v3=4\n"), +			wantMap: map[string][]string{ +				"k1": []string{"v1"}, +				"k2": []string{"v2", "v3=4"}, +			}, +		}, +	} { +		msg, err := NewMessageASCII(table.input, len(table.wantMap)) +		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 +		} +		if got, want := msg.m, table.wantMap; !reflect.DeepEqual(got, want) { +			t.Errorf("got\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) +		} +	} +} + +func TestNumField(t *testing.T)           {} +func TestGetStrings(t *testing.T)         {} +func TestGetString(t *testing.T)          {} +func TestGetUint64(t *testing.T)          {} +func TestGetHash(t *testing.T)            {} +func TestGetSignature(t *testing.T)       {} +func TestGetVerificationKey(t *testing.T) {} +func TestDecodeHex(t *testing.T)          {} + +/* + * + * MarshalASCII methods and helpers + * + */ +func TestLeafMarshalASCII(t *testing.T) { +	description := "valid: two leaves" +	leafList := []*Leaf{ +		&Leaf{ +			Message: Message{ +				ShardHint: 123, +				Checksum:  testBuffer32, +			}, +			SigIdent: SigIdent{ +				Signature: testBuffer64, +				KeyHash:   testBuffer32, +			}, +		}, +		&Leaf{ +			Message: Message{ +				ShardHint: 456, +				Checksum:  testBuffer32, +			}, +			SigIdent: SigIdent{ +				Signature: testBuffer64, +				KeyHash:   testBuffer32, +			}, +		}, +	} +	wantBuf := bytes.NewBufferString(fmt.Sprintf( +		"%s%s%d%s"+"%s%s%x%s"+"%s%s%x%s"+"%s%s%x%s"+ +			"%s%s%d%s"+"%s%s%x%s"+"%s%s%x%s"+"%s%s%x%s", +		// Leaf 1 +		ShardHint, Delim, 123, EOL, +		Checksum, Delim, testBuffer32[:], EOL, +		SignatureOverMessage, Delim, testBuffer64[:], EOL, +		KeyHash, Delim, testBuffer32[:], EOL, +		// Leaf 2 +		ShardHint, Delim, 456, EOL, +		Checksum, Delim, testBuffer32[:], EOL, +		SignatureOverMessage, Delim, testBuffer64[:], EOL, +		KeyHash, Delim, testBuffer32[:], EOL, +	)) +	buf := bytes.NewBuffer(nil) +	for _, leaf := range leafList { +		if err := leaf.MarshalASCII(buf); err != nil { +			t.Errorf("expected error %v but got %v in test %q: %v", false, true, description, err) +			return +		} +	} +	if got, want := buf.Bytes(), wantBuf.Bytes(); !bytes.Equal(got, want) { +		t.Errorf("got\n\t%v\nbut wanted\n\t%v\nin test %q", string(got), string(want), description) +	} +} + +func TestSignedTreeHeadMarshalASCII(t *testing.T) { +	description := "valid" +	sth := &SignedTreeHead{ +		TreeHead: TreeHead{ +			Timestamp: 123, +			TreeSize:  456, +			RootHash:  testBuffer32, +		}, +		SigIdent: []*SigIdent{ +			&SigIdent{ +				Signature: testBuffer64, +				KeyHash:   testBuffer32, +			}, +			&SigIdent{ +				Signature: testBuffer64, +				KeyHash:   testBuffer32, +			}, +		}, +	} +	wantBuf := bytes.NewBufferString(fmt.Sprintf( +		"%s%s%d%s"+"%s%s%d%s"+"%s%s%x%s"+"%s%s%x%s"+"%s%s%x%s"+"%s%s%x%s"+"%s%s%x%s", +		Timestamp, Delim, 123, EOL, +		TreeSize, Delim, 456, EOL, +		RootHash, Delim, testBuffer32[:], EOL, +		Signature, Delim, testBuffer64[:], EOL, +		KeyHash, Delim, testBuffer32[:], EOL, +		Signature, Delim, testBuffer64[:], EOL, +		KeyHash, Delim, testBuffer32[:], EOL, +	)) +	buf := bytes.NewBuffer(nil) +	if err := sth.MarshalASCII(buf); err != nil { +		t.Errorf("expected error %v but got %v in test %q", false, true, description) +		return +	} +	if got, want := buf.Bytes(), wantBuf.Bytes(); !bytes.Equal(got, want) { +		t.Errorf("got\n\t%v\nbut wanted\n\t%v\nin test %q", string(got), string(want), description) +	} +} + +func TestInclusionProofMarshalASCII(t *testing.T) { +	description := "valid" +	proof := InclusionProof{ +		TreeSize:  321, +		LeafIndex: 123, +		Path: []*[HashSize]byte{ +			testBuffer32, +			testBuffer32, +		}, +	} +	wantBuf := bytes.NewBufferString(fmt.Sprintf( +		"%s%s%d%s"+"%s%s%d%s"+"%s%s%x%s"+"%s%s%x%s", +		TreeSize, Delim, 321, EOL, +		LeafIndex, Delim, 123, EOL, +		InclusionPath, Delim, testBuffer32[:], EOL, +		InclusionPath, Delim, testBuffer32[:], EOL, +	)) +	buf := bytes.NewBuffer(nil) +	if err := proof.MarshalASCII(buf); err != nil { +		t.Errorf("expected error %v but got %v in test %q", false, true, description) +		return +	} +	if got, want := buf.Bytes(), wantBuf.Bytes(); !bytes.Equal(got, want) { +		t.Errorf("got\n\t%v\nbut wanted\n\t%v\nin test %q", string(got), string(want), description) +	} +} + +func TestConsistencyProofMarshalASCII(t *testing.T) { +	description := "valid" +	proof := ConsistencyProof{ +		NewSize: 321, +		OldSize: 123, +		Path: []*[HashSize]byte{ +			testBuffer32, +			testBuffer32, +		}, +	} +	wantBuf := bytes.NewBufferString(fmt.Sprintf( +		"%s%s%d%s"+"%s%s%d%s"+"%s%s%x%s"+"%s%s%x%s", +		NewSize, Delim, 321, EOL, +		OldSize, Delim, 123, EOL, +		ConsistencyPath, Delim, testBuffer32[:], EOL, +		ConsistencyPath, Delim, testBuffer32[:], EOL, +	)) +	buf := bytes.NewBuffer(nil) +	if err := proof.MarshalASCII(buf); err != nil { +		t.Errorf("expected error %v but got %v in test %q", false, true, description) +		return +	} +	if got, want := buf.Bytes(), wantBuf.Bytes(); !bytes.Equal(got, want) { +		t.Errorf("got\n\t%v\nbut wanted\n\t%v\nin test %q", string(got), string(want), description) +	} +} + +func TestWriteASCII(t *testing.T) { +} + +/* + * + * UnmarshalASCII methods and helpers + * + */ +func TestLeafListUnmarshalASCII(t *testing.T) {} + +func TestSignedTreeHeadUnmarshalASCII(t *testing.T) { +	for _, table := range []struct { +		description string +		buf         io.Reader +		wantErr     bool +		wantSth     *SignedTreeHead +	}{ +		{ +			description: "valid", +			buf: bytes.NewBufferString(fmt.Sprintf( +				"%s%s%d%s"+"%s%s%d%s"+"%s%s%x%s"+"%s%s%x%s"+"%s%s%x%s"+"%s%s%x%s"+"%s%s%x%s", +				Timestamp, Delim, 123, EOL, +				TreeSize, Delim, 456, EOL, +				RootHash, Delim, testBuffer32[:], EOL, +				Signature, Delim, testBuffer64[:], EOL, +				KeyHash, Delim, testBuffer32[:], EOL, +				Signature, Delim, testBuffer64[:], EOL, +				KeyHash, Delim, testBuffer32[:], EOL, +			)), +			wantSth: &SignedTreeHead{ +				TreeHead: TreeHead{ +					Timestamp: 123, +					TreeSize:  456, +					RootHash:  testBuffer32, +				}, +				SigIdent: []*SigIdent{ +					&SigIdent{ +						Signature: testBuffer64, +						KeyHash:   testBuffer32, +					}, +					&SigIdent{ +						Signature: testBuffer64, +						KeyHash:   testBuffer32, +					}, +				}, +			}, +		}, +	} { +		var sth SignedTreeHead +		err := sth.UnmarshalASCII(table.buf) +		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 +		} +		if got, want := &sth, table.wantSth; !reflect.DeepEqual(got, want) { +			t.Errorf("got\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) +		} +	} +} + +func TestInclusionProofUnmarshalASCII(t *testing.T)   {} +func TestConsistencyProofUnmarshalASCII(t *testing.T) {} + +func TestInclusionProofRequestUnmarshalASCII(t *testing.T) { +	for _, table := range []struct { +		description string +		buf         io.Reader +		wantErr     bool +		wantReq     *InclusionProofRequest +	}{ +		{ +			description: "valid", +			buf: bytes.NewBufferString(fmt.Sprintf( +				"%s%s%x%s"+"%s%s%d%s", +				LeafHash, Delim, testBuffer32[:], EOL, +				TreeSize, Delim, 123, EOL, +			)), +			wantReq: &InclusionProofRequest{ +				LeafHash: testBuffer32, +				TreeSize: 123, +			}, +		}, +	} { +		var req InclusionProofRequest +		err := req.UnmarshalASCII(table.buf) +		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 +		} +		if got, want := &req, table.wantReq; !reflect.DeepEqual(got, want) { +			t.Errorf("got\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) +		} +	} +} + +func TestConsistencyProofRequestUnmarshalASCII(t *testing.T) { +	for _, table := range []struct { +		description string +		buf         io.Reader +		wantErr     bool +		wantReq     *ConsistencyProofRequest +	}{ +		{ +			description: "valid", +			buf: bytes.NewBufferString(fmt.Sprintf( +				"%s%s%d%s"+"%s%s%d%s", +				NewSize, Delim, 321, EOL, +				OldSize, Delim, 123, EOL, +			)), +			wantReq: &ConsistencyProofRequest{ +				NewSize: 321, +				OldSize: 123, +			}, +		}, +	} { +		var req ConsistencyProofRequest +		err := req.UnmarshalASCII(table.buf) +		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 +		} +		if got, want := &req, table.wantReq; !reflect.DeepEqual(got, want) { +			t.Errorf("got\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) +		} +	} +} + +func TestLeavesRequestUnmarshalASCII(t *testing.T) { +	for _, table := range []struct { +		description string +		buf         io.Reader +		wantErr     bool +		wantReq     *LeavesRequest +	}{ +		{ +			description: "valid", +			buf: bytes.NewBufferString(fmt.Sprintf( +				"%s%s%d%s"+"%s%s%d%s", +				StartSize, Delim, 123, EOL, +				EndSize, Delim, 456, EOL, +			)), +			wantReq: &LeavesRequest{ +				StartSize: 123, +				EndSize:   456, +			}, +		}, +	} { +		var req LeavesRequest +		err := req.UnmarshalASCII(table.buf) +		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 +		} +		if got, want := &req, table.wantReq; !reflect.DeepEqual(got, want) { +			t.Errorf("got\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) +		} +	} +} + +func TestLeafRequestUnmarshalASCII(t *testing.T) { +	for _, table := range []struct { +		description string +		buf         io.Reader +		wantErr     bool +		wantReq     *LeafRequest +	}{ +		{ +			description: "valid", +			buf: bytes.NewBufferString(fmt.Sprintf( +				"%s%s%d%s"+"%s%s%x%s"+"%s%s%x%s"+"%s%s%x%s"+"%s%s%s%s", +				ShardHint, Delim, 123, EOL, +				Checksum, Delim, testBuffer32[:], EOL, +				SignatureOverMessage, Delim, testBuffer64[:], EOL, +				VerificationKey, Delim, testBuffer32[:], EOL, +				DomainHint, Delim, "example.com", EOL, +			)), +			wantReq: &LeafRequest{ +				Message: Message{ +					ShardHint: 123, +					Checksum:  testBuffer32, +				}, +				Signature:       testBuffer64, +				VerificationKey: testBuffer32, +				DomainHint:      "example.com", +			}, +		}, +	} { +		var req LeafRequest +		err := req.UnmarshalASCII(table.buf) +		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 +		} +		if got, want := &req, table.wantReq; !reflect.DeepEqual(got, want) { +			t.Errorf("got\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) +		} +	} +} + +func TestCosignatureRequestUnmarshalASCII(t *testing.T) { +	for _, table := range []struct { +		description string +		buf         io.Reader +		wantErr     bool +		wantReq     *CosignatureRequest +	}{ +		{ +			description: "valid", +			buf: bytes.NewBufferString(fmt.Sprintf( +				"%s%s%x%s"+"%s%s%x%s", +				Signature, Delim, testBuffer64[:], EOL, +				KeyHash, Delim, testBuffer32[:], EOL, +			)), +			wantReq: &CosignatureRequest{ +				SigIdent: SigIdent{ +					Signature: testBuffer64, +					KeyHash:   testBuffer32, +				}, +			}, +		}, +	} { +		var req CosignatureRequest +		err := req.UnmarshalASCII(table.buf) +		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 +		} +		if got, want := &req, table.wantReq; !reflect.DeepEqual(got, want) { +			t.Errorf("got\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) +		} +	} +} diff --git a/pkg/types/trunnel.go b/pkg/types/trunnel.go new file mode 100644 index 0000000..268f6f7 --- /dev/null +++ b/pkg/types/trunnel.go @@ -0,0 +1,60 @@ +package types + +import ( +	"encoding/binary" +	"fmt" +) + +const ( +	// MessageSize is the number of bytes in a Trunnel-encoded leaf message +	MessageSize = 8 + HashSize +	// LeafSize is the number of bytes in a Trunnel-encoded leaf +	LeafSize = MessageSize + SignatureSize + HashSize +) + +// Marshal returns a Trunnel-encoded message +func (m *Message) Marshal() []byte { +	buf := make([]byte, MessageSize) +	binary.BigEndian.PutUint64(buf, m.ShardHint) +	copy(buf[8:], m.Checksum[:]) +	return buf +} + +// Marshal returns a Trunnel-encoded leaf +func (l *Leaf) Marshal() []byte { +	buf := l.Message.Marshal() +	buf = append(buf, l.SigIdent.Signature[:]...) +	buf = append(buf, l.SigIdent.KeyHash[:]...) +	return buf +} + +// Marshal returns a Trunnel-encoded tree head +func (th *TreeHead) Marshal() []byte { +	buf := make([]byte, 8+8+HashSize) +	binary.BigEndian.PutUint64(buf[0:8], th.Timestamp) +	binary.BigEndian.PutUint64(buf[8:16], th.TreeSize) +	copy(buf[16:], th.RootHash[:]) +	return buf +} + +// Unmarshal parses the Trunnel-encoded buffer as a leaf +func (l *Leaf) Unmarshal(buf []byte) error { +	if len(buf) != LeafSize { +		return fmt.Errorf("invalid leaf size: %v", len(buf)) +	} +	// Shard hint +	l.ShardHint = binary.BigEndian.Uint64(buf) +	offset := 8 +	// Checksum +	l.Checksum = &[HashSize]byte{} +	copy(l.Checksum[:], buf[offset:offset+HashSize]) +	offset += HashSize +	// Signature +	l.Signature = &[SignatureSize]byte{} +	copy(l.Signature[:], buf[offset:offset+SignatureSize]) +	offset += SignatureSize +	// KeyHash +	l.KeyHash = &[HashSize]byte{} +	copy(l.KeyHash[:], buf[offset:]) +	return nil +} diff --git a/pkg/types/trunnel_test.go b/pkg/types/trunnel_test.go new file mode 100644 index 0000000..297578c --- /dev/null +++ b/pkg/types/trunnel_test.go @@ -0,0 +1,114 @@ +package types + +import ( +	"bytes" +	"reflect" +	"testing" +) + +var ( +	testBuffer32 = &[32]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31} +	testBuffer64 = &[64]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63} +) + +func TestMarshalMessage(t *testing.T) { +	description := "valid: shard hint 72623859790382856, checksum 0x00,0x01,..." +	message := &Message{ +		ShardHint: 72623859790382856, +		Checksum:  testBuffer32, +	} +	want := bytes.Join([][]byte{ +		[]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, +		testBuffer32[:], +	}, nil) +	if got := message.Marshal(); !bytes.Equal(got, want) { +		t.Errorf("got message\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, description) +	} +} + +func TestMarshalLeaf(t *testing.T) { +	description := "valid: shard hint 72623859790382856, buffers 0x00,0x01,..." +	leaf := &Leaf{ +		Message: Message{ +			ShardHint: 72623859790382856, +			Checksum:  testBuffer32, +		}, +		SigIdent: SigIdent{ +			Signature: testBuffer64, +			KeyHash:   testBuffer32, +		}, +	} +	want := bytes.Join([][]byte{ +		[]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, +		testBuffer32[:], testBuffer64[:], testBuffer32[:], +	}, nil) +	if got := leaf.Marshal(); !bytes.Equal(got, want) { +		t.Errorf("got leaf\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, description) +	} +} + +func TestMarshalTreeHead(t *testing.T) { +	description := "valid: timestamp 16909060, tree size 72623859790382856, root hash 0x00,0x01,..." +	th := &TreeHead{ +		Timestamp: 16909060, +		TreeSize:  72623859790382856, +		RootHash:  testBuffer32, +	} +	want := bytes.Join([][]byte{ +		[]byte{0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04}, +		[]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, +		testBuffer32[:], +	}, nil) +	if got := th.Marshal(); !bytes.Equal(got, want) { +		t.Errorf("got tree head\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, description) +	} +} + +func TestUnmarshalLeaf(t *testing.T) { +	for _, table := range []struct { +		description string +		serialized  []byte +		wantErr     bool +		want        *Leaf +	}{ +		{ +			description: "invalid: not enough bytes", +			serialized:  make([]byte, LeafSize-1), +			wantErr:     true, +		}, +		{ +			description: "invalid: too many bytes", +			serialized:  make([]byte, LeafSize+1), +			wantErr:     true, +		}, +		{ +			description: "valid: shard hint 72623859790382856, buffers 0x00,0x01,...", +			serialized: bytes.Join([][]byte{ +				[]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, +				testBuffer32[:], testBuffer64[:], testBuffer32[:], +			}, nil), +			want: &Leaf{ +				Message: Message{ +					ShardHint: 72623859790382856, +					Checksum:  testBuffer32, +				}, +				SigIdent: SigIdent{ +					Signature: testBuffer64, +					KeyHash:   testBuffer32, +				}, +			}, +		}, +	} { +		var leaf Leaf +		err := leaf.Unmarshal(table.serialized) +		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 +		} +		if got, want := &leaf, table.want; !reflect.DeepEqual(got, want) { +			t.Errorf("got leaf\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, table.description) +		} +	} +} diff --git a/pkg/types/types.go b/pkg/types/types.go new file mode 100644 index 0000000..9ca7db8 --- /dev/null +++ b/pkg/types/types.go @@ -0,0 +1,155 @@ +package types + +import ( +	"crypto" +	"crypto/ed25519" +	"crypto/sha256" +	"fmt" +	"strings" +) + +const ( +	HashSize            = sha256.Size +	SignatureSize       = ed25519.SignatureSize +	VerificationKeySize = ed25519.PublicKeySize + +	EndpointAddLeaf             = Endpoint("add-leaf") +	EndpointAddCosignature      = Endpoint("add-cosignature") +	EndpointGetTreeHeadLatest   = Endpoint("get-tree-head-latest") +	EndpointGetTreeHeadToSign   = Endpoint("get-tree-head-to-sign") +	EndpointGetTreeHeadCosigned = Endpoint("get-tree-head-cosigned") +	EndpointGetProofByHash      = Endpoint("get-proof-by-hash") +	EndpointGetConsistencyProof = Endpoint("get-consistency-proof") +	EndpointGetLeaves           = Endpoint("get-leaves") +) + +// Endpoint is a named HTTP API endpoint +type Endpoint string + +// Path joins a number of components to form a full endpoint path.  For example, +// EndpointAddLeaf.Path("example.com", "st/v0") -> example.com/st/v0/add-leaf. +func (e Endpoint) Path(components ...string) string { +	return strings.Join(append(components, string(e)), "/") +} + +// Leaf is the log's Merkle tree leaf. +type Leaf struct { +	Message +	SigIdent +} + +// Message is composed of a shard hint and a checksum.  The submitter selects +// these values to fit the log's shard interval and the opaque data in question. +type Message struct { +	ShardHint uint64 +	Checksum  *[HashSize]byte +} + +// SigIdent is composed of a signature-signer pair.  The signature is computed +// over the Trunnel-serialized leaf message.  KeyHash identifies the signer. +type SigIdent struct { +	Signature *[SignatureSize]byte +	KeyHash   *[HashSize]byte +} + +// SignedTreeHead is composed of a tree head and a list of signature-signer +// pairs.  Each signature is computed over the Trunnel-serialized tree head. +type SignedTreeHead struct { +	TreeHead +	SigIdent []*SigIdent +} + +// TreeHead is the log's tree head. +type TreeHead struct { +	Timestamp uint64 +	TreeSize  uint64 +	RootHash  *[HashSize]byte +} + +// ConsistencyProof is a consistency proof that proves the log's append-only +// property. +type ConsistencyProof struct { +	NewSize uint64 +	OldSize uint64 +	Path    []*[HashSize]byte +} + +// InclusionProof is an inclusion proof that proves a leaf is included in the +// log. +type InclusionProof struct { +	TreeSize  uint64 +	LeafIndex uint64 +	Path      []*[HashSize]byte +} + +// LeafList is a list of leaves +type LeafList []*Leaf + +// ConsistencyProofRequest is a get-consistency-proof request +type ConsistencyProofRequest struct { +	NewSize uint64 +	OldSize uint64 +} + +// InclusionProofRequest is a get-proof-by-hash request +type InclusionProofRequest struct { +	LeafHash *[HashSize]byte +	TreeSize uint64 +} + +// LeavesRequest is a get-leaves request +type LeavesRequest struct { +	StartSize uint64 +	EndSize   uint64 +} + +// LeafRequest is an add-leaf request +type LeafRequest struct { +	Message +	Signature       *[SignatureSize]byte +	VerificationKey *[VerificationKeySize]byte +	DomainHint      string +} + +// CosignatureRequest is an add-cosignature request +type CosignatureRequest struct { +	SigIdent +} + +// Sign signs the tree head using the log's signature scheme +func (th *TreeHead) Sign(signer crypto.Signer) (*SignedTreeHead, error) { +	sig, err := signer.Sign(nil, th.Marshal(), crypto.Hash(0)) +	if err != nil { +		return nil, fmt.Errorf("Sign: %v", err) +	} + +	sigident := SigIdent{ +		KeyHash:   Hash(signer.Public().(ed25519.PublicKey)[:]), +		Signature: &[SignatureSize]byte{}, +	} +	copy(sigident.Signature[:], sig) +	return &SignedTreeHead{ +		TreeHead: *th, +		SigIdent: []*SigIdent{ +			&sigident, +		}, +	}, nil +} + +// Verify verifies the tree head signature using the log's signature scheme +func (th *TreeHead) Verify(vk *[VerificationKeySize]byte, sig *[SignatureSize]byte) error { +	if !ed25519.Verify(ed25519.PublicKey(vk[:]), th.Marshal(), sig[:]) { +		return fmt.Errorf("invalid tree head signature") +	} +	return nil +} + +// Verify checks if a leaf is included in the log +func (p *InclusionProof) Verify(leaf *Leaf, th *TreeHead) error { // TODO +	return nil +} + +// Verify checks if two tree heads are consistent +func (p *ConsistencyProof) Verify(oldTH, newTH *TreeHead) error { // TODO +	return nil +} diff --git a/pkg/types/types_test.go b/pkg/types/types_test.go new file mode 100644 index 0000000..da89c59 --- /dev/null +++ b/pkg/types/types_test.go @@ -0,0 +1,58 @@ +package types + +import ( +	"testing" +) + +func TestEndpointPath(t *testing.T) { +	base, prefix, proto := "example.com", "log", "st/v0" +	for _, table := range []struct { +		endpoint Endpoint +		want     string +	}{ +		{ +			endpoint: EndpointAddLeaf, +			want:     "example.com/log/st/v0/add-leaf", +		}, +		{ +			endpoint: EndpointAddCosignature, +			want:     "example.com/log/st/v0/add-cosignature", +		}, +		{ +			endpoint: EndpointGetTreeHeadLatest, +			want:     "example.com/log/st/v0/get-tree-head-latest", +		}, +		{ +			endpoint: EndpointGetTreeHeadToSign, +			want:     "example.com/log/st/v0/get-tree-head-to-sign", +		}, +		{ +			endpoint: EndpointGetTreeHeadCosigned, +			want:     "example.com/log/st/v0/get-tree-head-cosigned", +		}, +		{ +			endpoint: EndpointGetConsistencyProof, +			want:     "example.com/log/st/v0/get-consistency-proof", +		}, +		{ +			endpoint: EndpointGetProofByHash, +			want:     "example.com/log/st/v0/get-proof-by-hash", +		}, +		{ +			endpoint: EndpointGetLeaves, +			want:     "example.com/log/st/v0/get-leaves", +		}, +	} { +		if got, want := table.endpoint.Path(base+"/"+prefix+"/"+proto), table.want; got != want { +			t.Errorf("got endpoint\n%s\n\tbut wanted\n%s\n\twith one component", got, want) +		} +		if got, want := table.endpoint.Path(base, prefix, proto), table.want; got != want { +			t.Errorf("got endpoint\n%s\n\tbut wanted\n%s\n\tmultiple components", got, want) +		} +	} +} + +func TestTreeHeadSign(t *testing.T)           {} +func TestTreeHeadVerify(t *testing.T)         {} +func TestInclusionProofVerify(t *testing.T)   {} +func TestConsistencyProofVerify(t *testing.T) {} diff --git a/pkg/types/util.go b/pkg/types/util.go new file mode 100644 index 0000000..3cd7dfa --- /dev/null +++ b/pkg/types/util.go @@ -0,0 +1,21 @@ +package types + +import ( +	"crypto/sha256" +) + +const ( +	LeafHashPrefix = 0x00 +) + +func Hash(buf []byte) *[HashSize]byte { +	var ret [HashSize]byte +	hash := sha256.New() +	hash.Write(buf) +	copy(ret[:], hash.Sum(nil)) +	return &ret +} + +func HashLeaf(buf []byte) *[HashSize]byte { +	return Hash(append([]byte{LeafHashPrefix}, buf...)) +} | 
