From dda238b9fc105219f220f0ec3b341b0c81b71301 Mon Sep 17 00:00:00 2001 From: Rasmus Dahlberg Date: Mon, 20 Dec 2021 19:53:54 +0100 Subject: types: Start using sigsum-lib-go This commit does not change the way in which the log behaves externally. In other words, all changes are internal and involves renaming and code restructuring. Most notably picking up the refactored sigsum-lib-go. --- pkg/instance/endpoint.go | 122 ------ pkg/instance/endpoint_test.go | 661 ------------------------------ pkg/instance/experimental.go | 85 ++++ pkg/instance/experimental_endpoint.go | 85 ---- pkg/instance/handler.go | 169 ++++++++ pkg/instance/handler_test.go | 736 ++++++++++++++++++++++++++++++++++ pkg/instance/instance.go | 104 ++--- pkg/instance/instance_test.go | 98 ----- 8 files changed, 1018 insertions(+), 1042 deletions(-) delete mode 100644 pkg/instance/endpoint.go delete mode 100644 pkg/instance/endpoint_test.go create mode 100644 pkg/instance/experimental.go delete mode 100644 pkg/instance/experimental_endpoint.go create mode 100644 pkg/instance/handler.go create mode 100644 pkg/instance/handler_test.go delete mode 100644 pkg/instance/instance_test.go (limited to 'pkg/instance') diff --git a/pkg/instance/endpoint.go b/pkg/instance/endpoint.go deleted file mode 100644 index a6d424d..0000000 --- a/pkg/instance/endpoint.go +++ /dev/null @@ -1,122 +0,0 @@ -package instance - -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(ctx, 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") - cth, err := i.Stateman.Cosigned(ctx) - if err != nil { - return http.StatusInternalServerError, err - } - if err := cth.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 deleted file mode 100644 index 18a6c27..0000000 --- a/pkg/instance/endpoint_test.go +++ /dev/null @@ -1,661 +0,0 @@ -package instance - -import ( - "bytes" - "crypto/ed25519" - "crypto/rand" - "encoding/hex" - "fmt" - "io" - "net/http" - "net/http/httptest" - "testing" - "time" - - "git.sigsum.org/sigsum-log-go/pkg/mocks" - "git.sigsum.org/sigsum-log-go/pkg/types" - "github.com/golang/mock/gomock" -) - -var ( - testWitVK = [types.VerificationKeySize]byte{} - testConfig = Config{ - LogID: hex.EncodeToString(types.Hash([]byte("logid"))[:]), - TreeID: 0, - Prefix: "testonly", - MaxRange: 3, - Deadline: 10, - Interval: 10, - ShardStart: 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), - }, - Signature: &[types.SignatureSize]byte{}, - } - testCTH = &types.CosignedTreeHead{ - SignedTreeHead: *testSTH, - SigIdent: []*types.SigIdent{ - &types.SigIdent{ - KeyHash: &[types.HashSize]byte{}, - Signature: &[types.SignatureSize]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) { - for _, table := range []struct { - description string - ascii io.Reader // buffer used to populate HTTP request - expectTrillian bool // expect Trillian client code path - errTrillian error // error from Trillian client - expectDNS bool // expect DNS verifier code path - errDNS error // error from DNS verifier - 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: mustLeafBuffer(t, 10, &[types.HashSize]byte{}, false), - wantCode: http.StatusBadRequest, - }, - { - description: "invalid: bad request (shard hint is before shard start)", - ascii: mustLeafBuffer(t, 9, &[types.HashSize]byte{}, true), - wantCode: http.StatusBadRequest, - }, - { - description: "invalid: bad request (shard hint is after shard end)", - ascii: mustLeafBuffer(t, uint64(time.Now().Unix())+1024, &[types.HashSize]byte{}, true), - wantCode: http.StatusBadRequest, - }, - { - description: "invalid: failed verifying domain hint", - ascii: mustLeafBuffer(t, 10, &[types.HashSize]byte{}, true), - expectDNS: true, - errDNS: fmt.Errorf("something went wrong"), - wantCode: http.StatusBadRequest, - }, - { - description: "invalid: backend failure", - ascii: mustLeafBuffer(t, 10, &[types.HashSize]byte{}, true), - expectDNS: true, - expectTrillian: true, - errTrillian: fmt.Errorf("something went wrong"), - wantCode: http.StatusInternalServerError, - }, - { - description: "valid", - ascii: mustLeafBuffer(t, 10, &[types.HashSize]byte{}, true), - expectDNS: true, - expectTrillian: true, - wantCode: http.StatusOK, - }, - } { - // Run deferred functions at the end of each iteration - func() { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - dns := mocks.NewMockVerifier(ctrl) - if table.expectDNS { - dns.EXPECT().Verify(gomock.Any(), gomock.Any(), gomock.Any()).Return(table.errDNS) - } - client := mocks.NewMockClient(ctrl) - if table.expectTrillian { - client.EXPECT().AddLeaf(gomock.Any(), gomock.Any()).Return(table.errTrillian) - } - i := Instance{ - Config: testConfig, - Client: client, - DNS: dns, - } - - // 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.Cosignature, 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.CosignedTreeHead // cosigned 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: testCTH, - 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, - )) - } - 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: "invalid: bad request (OldSize is zero)", - ascii: buf(0, 1), - wantCode: http.StatusBadRequest, - }, - { - description: "invalid: bad request (OldSize > NewSize)", - ascii: buf(2, 1), - wantCode: http.StatusBadRequest, - }, - { - description: "invalid: backend failure", - ascii: buf(1, 2), - expect: true, - err: fmt.Errorf("something went wrong"), - wantCode: http.StatusInternalServerError, - }, - { - description: "valid", - ascii: buf(1, 2), - expect: true, - rsp: &types.ConsistencyProof{ - OldSize: 1, - NewSize: 2, - Path: []*[types.HashSize]byte{ - types.Hash(nil), - }, - }, - 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) { - buf := func(hash *[types.HashSize]byte, treeSize int) io.Reader { - return bytes.NewBufferString(fmt.Sprintf( - "%s%s%x%s"+"%s%s%d%s", - types.LeafHash, types.Delim, hash[:], types.EOL, - types.TreeSize, types.Delim, treeSize, 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 - rsp *types.InclusionProof // inclusion 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: "invalid: bad request (no proof for tree size)", - ascii: buf(types.Hash(nil), 1), - wantCode: http.StatusBadRequest, - }, - { - description: "invalid: backend failure", - ascii: buf(types.Hash(nil), 2), - expect: true, - err: fmt.Errorf("something went wrong"), - wantCode: http.StatusInternalServerError, - }, - { - description: "valid", - ascii: buf(types.Hash(nil), 2), - expect: true, - rsp: &types.InclusionProof{ - TreeSize: 2, - LeafIndex: 0, - Path: []*[types.HashSize]byte{ - types.Hash(nil), - }, - }, - 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().GetInclusionProof(gomock.Any(), gomock.Any()).Return(table.rsp, table.err) - } - i := Instance{ - Config: testConfig, - Client: client, - } - - // Create HTTP request - url := types.EndpointGetInclusionProof.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.EndpointGetInclusionProof).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 TestGetLeaves(t *testing.T) { - buf := func(startSize, endSize int64) io.Reader { - return bytes.NewBufferString(fmt.Sprintf( - "%s%s%d%s"+"%s%s%d%s", - types.StartSize, types.Delim, startSize, types.EOL, - types.EndSize, types.Delim, endSize, 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 - rsp *types.LeafList // list of leaves 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: "invalid: bad request (StartSize > EndSize)", - ascii: buf(1, 0), - wantCode: http.StatusBadRequest, - }, - { - description: "invalid: backend failure", - ascii: buf(0, 0), - expect: true, - err: fmt.Errorf("something went wrong"), - wantCode: http.StatusInternalServerError, - }, - { - description: "valid: one more entry than the configured MaxRange", - ascii: buf(0, testConfig.MaxRange), // query will be pruned - expect: true, - rsp: func() *types.LeafList { - var list types.LeafList - for i := int64(0); i < testConfig.MaxRange; i++ { - list = append(list[:], &types.Leaf{ - Message: types.Message{ - ShardHint: 0, - Checksum: types.Hash(nil), - }, - SigIdent: types.SigIdent{ - Signature: &[types.SignatureSize]byte{}, - KeyHash: types.Hash(nil), - }, - }) - } - return &list - }(), - 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().GetLeaves(gomock.Any(), gomock.Any()).Return(table.rsp, table.err) - } - i := Instance{ - Config: testConfig, - Client: client, - } - - // Create HTTP request - url := types.EndpointGetLeaves.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.EndpointGetLeaves).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) - } - if w.Code != http.StatusOK { - return - } - - // TODO: check that we got the right leaves back. It is especially - // important that we check that we got the right number of leaves. - // - // Pseuducode for when we have types.LeafList.UnmarshalASCII() - // - //list := &types.LeafList{} - //if err := list.UnmarshalASCII(w.Body); err != nil { - // t.Fatalf("must unmarshal leaf list: %v", err) - //} - //if got, want := list, table.rsp; !reflect.DeepEqual(got, want) { - // t.Errorf("got leaf list\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) - //} - }() - } -} - -func mustLeafBuffer(t *testing.T, shardHint uint64, checksum *[types.HashSize]byte, wantSig bool) io.Reader { - t.Helper() - - vk, sk, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - t.Fatalf("must generate ed25519 keys: %v", err) - } - msg := types.Message{ - ShardHint: shardHint, - Checksum: checksum, - } - sig := ed25519.Sign(sk, msg.Marshal()) - if !wantSig { - sig[0] += 1 - } - return bytes.NewBufferString(fmt.Sprintf( - "%s%s%d%s"+"%s%s%x%s"+"%s%s%x%s"+"%s%s%x%s"+"%s%s%s%s", - types.ShardHint, types.Delim, shardHint, types.EOL, - types.Checksum, types.Delim, checksum[:], types.EOL, - types.Signature, types.Delim, sig, types.EOL, - types.VerificationKey, types.Delim, vk, types.EOL, - types.DomainHint, types.Delim, "example.com", types.EOL, - )) -} diff --git a/pkg/instance/experimental.go b/pkg/instance/experimental.go new file mode 100644 index 0000000..ab81ada --- /dev/null +++ b/pkg/instance/experimental.go @@ -0,0 +1,85 @@ +package instance + +import ( + "bytes" + "context" + "crypto" + "crypto/ed25519" + "crypto/sha256" + "encoding/base64" + "encoding/binary" + "fmt" + "net/http" + + "git.sigsum.org/sigsum-lib-go/pkg/types" + "github.com/golang/glog" +) + +// algEd25519 identifies a checkpoint signature algorithm +const algEd25519 byte = 1 + +// getCheckpoint is an experimental endpoint that is not part of the official +// Sigsum API. Documentation can be found in the transparency-dev repo. +func getCheckpoint(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) { + glog.V(3).Info("handling get-checkpoint request") + sth, err := i.Stateman.ToSign(ctx) + if err != nil { + return http.StatusInternalServerError, err + } + if err := i.signWriteNote(w, sth); err != nil { + return http.StatusInternalServerError, err + } + return http.StatusOK, nil +} + +// signWriteNote signs and writes a checkpoint which uses "sigsum.org:" +// as origin string. Origin string is also used as ID in the note signature. +// This means that a sigsum log's prefix (say, "glass-frog"), must be unique. +func (i *Instance) signWriteNote(w http.ResponseWriter, sth *types.SignedTreeHead) error { + origin := fmt.Sprintf("sigsum.org:%s", i.Prefix) + msg := fmt.Sprintf("%s\n%d\n%s\n", + origin, + sth.TreeSize, + base64.StdEncoding.EncodeToString(sth.RootHash[:]), + ) + sig, err := noteSign(i.Signer, origin, msg) + if err != nil { + return err + } + + fmt.Fprintf(w, "%s\n\u2014 %s %s\n", msg, origin, sig) + return nil +} + +// noteSign returns a note signature for the provided origin and message +func noteSign(signer crypto.Signer, origin, msg string) (string, error) { + sig, err := signer.Sign(nil, []byte(msg), crypto.Hash(0)) + if err != nil { + return "", err + } + + var hbuf [4]byte + binary.BigEndian.PutUint32(hbuf[:], noteKeyHash(origin, notePubKeyEd25519(signer))) + sig = append(hbuf[:], sig...) + return base64.StdEncoding.EncodeToString(sig), nil +} + +// See: +// https://cs.opensource.google/go/x/mod/+/refs/tags/v0.5.1:sumdb/note/note.go;l=336 +func notePubKeyEd25519(signer crypto.Signer) []byte { + return bytes.Join([][]byte{ + []byte{algEd25519}, + signer.Public().(ed25519.PublicKey), + }, nil) +} + +// Source: +// https://cs.opensource.google/go/x/mod/+/refs/tags/v0.5.1:sumdb/note/note.go;l=222 +func noteKeyHash(name string, key []byte) uint32 { + h := sha256.New() + h.Write([]byte(name)) + h.Write([]byte("\n")) + h.Write(key) + sum := h.Sum(nil) + return binary.BigEndian.Uint32(sum) +} diff --git a/pkg/instance/experimental_endpoint.go b/pkg/instance/experimental_endpoint.go deleted file mode 100644 index 2986a27..0000000 --- a/pkg/instance/experimental_endpoint.go +++ /dev/null @@ -1,85 +0,0 @@ -package instance - -import ( - "bytes" - "context" - "crypto" - "crypto/ed25519" - "crypto/sha256" - "encoding/base64" - "encoding/binary" - "fmt" - "net/http" - - "git.sigsum.org/sigsum-log-go/pkg/types" - "github.com/golang/glog" -) - -// algEd25519 identifies a checkpoint signature algorithm -const algEd25519 byte = 1 - -// getCheckpoint is an experimental endpoint that is not part of the official -// Sigsum API. Documentation can be found in the transparency-dev repo. -func getCheckpoint(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) { - glog.V(3).Info("handling get-checkpoint request") - sth, err := i.Stateman.ToSign(ctx) - if err != nil { - return http.StatusInternalServerError, err - } - if err := i.signWriteNote(w, sth); err != nil { - return http.StatusInternalServerError, err - } - return http.StatusOK, nil -} - -// signWriteNote signs and writes a checkpoint which uses "sigsum.org:" -// as origin string. Origin string is also used as ID in the note signature. -// This means that a sigsum log's prefix (say, "glass-frog"), must be unique. -func (i *Instance) signWriteNote(w http.ResponseWriter, sth *types.SignedTreeHead) error { - origin := fmt.Sprintf("sigsum.org:%s", i.Prefix) - msg := fmt.Sprintf("%s\n%d\n%s\n", - origin, - sth.TreeSize, - base64.StdEncoding.EncodeToString(sth.RootHash[:]), - ) - sig, err := noteSign(i.Signer, origin, msg) - if err != nil { - return err - } - - fmt.Fprintf(w, "%s\n\u2014 %s %s\n", msg, origin, sig) - return nil -} - -// noteSign returns a note signature for the provided origin and message -func noteSign(signer crypto.Signer, origin, msg string) (string, error) { - sig, err := signer.Sign(nil, []byte(msg), crypto.Hash(0)) - if err != nil { - return "", err - } - - var hbuf [4]byte - binary.BigEndian.PutUint32(hbuf[:], noteKeyHash(origin, notePubKeyEd25519(signer))) - sig = append(hbuf[:], sig...) - return base64.StdEncoding.EncodeToString(sig), nil -} - -// See: -// https://cs.opensource.google/go/x/mod/+/refs/tags/v0.5.1:sumdb/note/note.go;l=336 -func notePubKeyEd25519(signer crypto.Signer) []byte { - return bytes.Join([][]byte{ - []byte{algEd25519}, - signer.Public().(ed25519.PublicKey), - }, nil) -} - -// Source: -// https://cs.opensource.google/go/x/mod/+/refs/tags/v0.5.1:sumdb/note/note.go;l=222 -func noteKeyHash(name string, key []byte) uint32 { - h := sha256.New() - h.Write([]byte(name)) - h.Write([]byte("\n")) - h.Write(key) - sum := h.Sum(nil) - return binary.BigEndian.Uint32(sum) -} diff --git a/pkg/instance/handler.go b/pkg/instance/handler.go new file mode 100644 index 0000000..66a20a5 --- /dev/null +++ b/pkg/instance/handler.go @@ -0,0 +1,169 @@ +package instance + +import ( + "context" + "fmt" + "net/http" + "time" + + "git.sigsum.org/sigsum-lib-go/pkg/types" + "github.com/golang/glog" +) + +// Handler implements the http.Handler interface, and contains a reference +// to a sigsum 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) +} + +// Path returns a path that should be configured for this handler +func (h Handler) Path() string { + if len(h.Instance.Prefix) == 0 { + return h.Endpoint.Path("", "sigsum", "v0") + } + return h.Endpoint.Path("", h.Instance.Prefix, "sigsum", "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("Error=%s\n", err.Error()), statusCode) + } +} + +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(ctx, 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.Cosignature); 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.ToASCII(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.ToASCII(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") + cth, err := i.Stateman.Cosigned(ctx) + if err != nil { + return http.StatusInternalServerError, err + } + if err := cth.ToASCII(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.ToASCII(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.ToASCII(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.ToASCII(w); err != nil { + return http.StatusInternalServerError, err + } + } + return http.StatusOK, nil +} diff --git a/pkg/instance/handler_test.go b/pkg/instance/handler_test.go new file mode 100644 index 0000000..ba5b60c --- /dev/null +++ b/pkg/instance/handler_test.go @@ -0,0 +1,736 @@ +package instance + +import ( + "bytes" + "crypto/ed25519" + "crypto/rand" + "fmt" + "io" + "net/http" + "net/http/httptest" + "reflect" + "testing" + "time" + + "git.sigsum.org/sigsum-lib-go/pkg/types" + mocksDB "git.sigsum.org/sigsum-log-go/pkg/db/mocks" + mocksDNS "git.sigsum.org/sigsum-log-go/pkg/dns/mocks" + mocksState "git.sigsum.org/sigsum-log-go/pkg/state/mocks" + "github.com/golang/mock/gomock" +) + +var ( + testWitVK = types.PublicKey{} + testConfig = Config{ + LogID: fmt.Sprintf("%x", types.HashFn([]byte("logid"))[:]), + TreeID: 0, + Prefix: "testonly", + MaxRange: 3, + Deadline: 10, + Interval: 10, + ShardStart: 10, + Witnesses: map[types.Hash]types.PublicKey{ + *types.HashFn(testWitVK[:]): testWitVK, + }, + } + testSTH = &types.SignedTreeHead{ + TreeHead: types.TreeHead{ + Timestamp: 0, + TreeSize: 0, + RootHash: *types.HashFn([]byte("root hash")), + }, + Signature: types.Signature{}, + } + testCTH = &types.CosignedTreeHead{ + SignedTreeHead: *testSTH, + Cosignature: []types.Signature{types.Signature{}}, + KeyHash: []types.Hash{types.Hash{}}, + } +) + +// TestHandlers check that the expected handlers are configured +func TestHandlers(t *testing.T) { + endpoints := map[types.Endpoint]bool{ + types.EndpointAddLeaf: false, + types.EndpointAddCosignature: false, + types.EndpointGetTreeHeadLatest: false, + types.EndpointGetTreeHeadToSign: false, + types.EndpointGetTreeHeadCosigned: false, + types.EndpointGetConsistencyProof: false, + types.EndpointGetInclusionProof: false, + types.EndpointGetLeaves: false, + types.Endpoint("get-checkpoint"): false, + } + i := &Instance{ + Config: testConfig, + } + for _, handler := range i.Handlers() { + if _, ok := endpoints[handler.Endpoint]; !ok { + t.Errorf("got unexpected endpoint: %s", handler.Endpoint) + } + endpoints[handler.Endpoint] = true + } + for endpoint, ok := range endpoints { + if !ok { + t.Errorf("endpoint %s is not configured", endpoint) + } + } +} + +// TestServeHTTP checks that invalid HTTP methods are rejected +func TestServeHTTP(t *testing.T) { + i := &Instance{ + Config: testConfig, + } + for _, handler := range i.Handlers() { + // Prepare invalid HTTP request + method := http.MethodPost + if method == handler.Method { + method = http.MethodGet + } + url := handler.Endpoint.Path("http://example.com", i.Prefix) + req, err := http.NewRequest(method, url, nil) + if err != nil { + t.Fatalf("must create HTTP request: %v", err) + } + w := httptest.NewRecorder() + + // Check that it is rejected + handler.ServeHTTP(w, req) + if got, want := w.Code, http.StatusMethodNotAllowed; got != want { + t.Errorf("got HTTP code %v but wanted %v for endpoint %q", got, want, handler.Endpoint) + } + } +} + +// TestPath checks that Path works for an endpoint (add-leaf) +func TestPath(t *testing.T) { + for _, table := range []struct { + description string + prefix string + want string + }{ + { + description: "no prefix", + want: "/sigsum/v0/add-leaf", + }, + { + description: "a prefix", + prefix: "test-prefix", + want: "/test-prefix/sigsum/v0/add-leaf", + }, + } { + instance := &Instance{ + Config: Config{ + Prefix: table.prefix, + }, + } + handler := Handler{ + Instance: instance, + Handler: addLeaf, + Endpoint: types.EndpointAddLeaf, + Method: http.MethodPost, + } + if got, want := handler.Path(), table.want; got != want { + t.Errorf("got path %v but wanted %v", got, want) + } + } +} + +func TestAddLeaf(t *testing.T) { + for _, table := range []struct { + description string + ascii io.Reader // buffer used to populate HTTP request + expectTrillian bool // expect Trillian client code path + errTrillian error // error from Trillian client + expectDNS bool // expect DNS verifier code path + errDNS error // error from DNS verifier + 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: mustLeafBuffer(t, 10, types.Hash{}, false), + wantCode: http.StatusBadRequest, + }, + { + description: "invalid: bad request (shard hint is before shard start)", + ascii: mustLeafBuffer(t, 9, types.Hash{}, true), + wantCode: http.StatusBadRequest, + }, + { + description: "invalid: bad request (shard hint is after shard end)", + ascii: mustLeafBuffer(t, uint64(time.Now().Unix())+1024, types.Hash{}, true), + wantCode: http.StatusBadRequest, + }, + { + description: "invalid: failed verifying domain hint", + ascii: mustLeafBuffer(t, 10, types.Hash{}, true), + expectDNS: true, + errDNS: fmt.Errorf("something went wrong"), + wantCode: http.StatusBadRequest, + }, + { + description: "invalid: backend failure", + ascii: mustLeafBuffer(t, 10, types.Hash{}, true), + expectDNS: true, + expectTrillian: true, + errTrillian: fmt.Errorf("something went wrong"), + wantCode: http.StatusInternalServerError, + }, + { + description: "valid", + ascii: mustLeafBuffer(t, 10, types.Hash{}, true), + expectDNS: true, + expectTrillian: true, + wantCode: http.StatusOK, + }, + } { + // Run deferred functions at the end of each iteration + func() { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + dns := mocksDNS.NewMockVerifier(ctrl) + if table.expectDNS { + dns.EXPECT().Verify(gomock.Any(), gomock.Any(), gomock.Any()).Return(table.errDNS) + } + client := mocksDB.NewMockClient(ctrl) + if table.expectTrillian { + client.EXPECT().AddLeaf(gomock.Any(), gomock.Any()).Return(table.errTrillian) + } + i := Instance{ + Config: testConfig, + Client: client, + DNS: dns, + } + + // 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=%x\n%s=%x\n", + "cosignature", types.Signature{}, + "key_hash", *types.HashFn(testWitVK[:]), + )) + } + 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=%x\n%s=%x\n", + "cosignature", types.Signature{}, + "key_hash", *types.HashFn(testWitVK[1:]), + )), + 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 := mocksState.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 := mocksState.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 := mocksState.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.CosignedTreeHead // cosigned 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: testCTH, + wantCode: http.StatusOK, + }, + } { + // Run deferred functions at the end of each iteration + func() { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + stateman := mocksState.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=%d\n%s=%d\n", + "old_size", oldSize, + "new_size", newSize, + )) + } + 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: "invalid: bad request (OldSize is zero)", + ascii: buf(0, 1), + wantCode: http.StatusBadRequest, + }, + { + description: "invalid: bad request (OldSize > NewSize)", + ascii: buf(2, 1), + wantCode: http.StatusBadRequest, + }, + { + description: "invalid: backend failure", + ascii: buf(1, 2), + expect: true, + err: fmt.Errorf("something went wrong"), + wantCode: http.StatusInternalServerError, + }, + { + description: "valid", + ascii: buf(1, 2), + expect: true, + rsp: &types.ConsistencyProof{ + OldSize: 1, + NewSize: 2, + Path: []types.Hash{ + *types.HashFn([]byte{}), + }, + }, + wantCode: http.StatusOK, + }, + } { + // Run deferred functions at the end of each iteration + func() { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + client := mocksDB.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) { + buf := func(hash *types.Hash, treeSize int) io.Reader { + return bytes.NewBufferString(fmt.Sprintf("%s=%x\n%s=%d\n", + "leaf_hash", hash[:], + "tree_size", treeSize, + )) + } + 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.InclusionProof // inclusion 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: "invalid: bad request (no proof for tree size)", + ascii: buf(types.HashFn([]byte{}), 1), + wantCode: http.StatusBadRequest, + }, + { + description: "invalid: backend failure", + ascii: buf(types.HashFn([]byte{}), 2), + expect: true, + err: fmt.Errorf("something went wrong"), + wantCode: http.StatusInternalServerError, + }, + { + description: "valid", + ascii: buf(types.HashFn([]byte{}), 2), + expect: true, + rsp: &types.InclusionProof{ + TreeSize: 2, + LeafIndex: 0, + Path: []types.Hash{ + *types.HashFn([]byte{}), + }, + }, + wantCode: http.StatusOK, + }, + } { + // Run deferred functions at the end of each iteration + func() { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + client := mocksDB.NewMockClient(ctrl) + if table.expect { + client.EXPECT().GetInclusionProof(gomock.Any(), gomock.Any()).Return(table.rsp, table.err) + } + i := Instance{ + Config: testConfig, + Client: client, + } + + // Create HTTP request + url := types.EndpointGetInclusionProof.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.EndpointGetInclusionProof).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 TestGetLeaves(t *testing.T) { + buf := func(startSize, endSize int64) io.Reader { + return bytes.NewBufferString(fmt.Sprintf("%s=%d\n%s=%d\n", + "start_size", startSize, + "end_size", endSize, + )) + } + 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.Leaves // list of leaves 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: "invalid: bad request (StartSize > EndSize)", + ascii: buf(1, 0), + wantCode: http.StatusBadRequest, + }, + { + description: "invalid: backend failure", + ascii: buf(0, 0), + expect: true, + err: fmt.Errorf("something went wrong"), + wantCode: http.StatusInternalServerError, + }, + { + description: "valid: one more entry than the configured MaxRange", + ascii: buf(0, testConfig.MaxRange), // query will be pruned + expect: true, + rsp: func() *types.Leaves { + var list types.Leaves + for i := int64(0); i < testConfig.MaxRange; i++ { + list = append(list[:], types.Leaf{ + Statement: types.Statement{ + ShardHint: 0, + Checksum: types.Hash{}, + }, + Signature: types.Signature{}, + KeyHash: types.Hash{}, + }) + } + return &list + }(), + wantCode: http.StatusOK, + }, + } { + // Run deferred functions at the end of each iteration + func() { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + client := mocksDB.NewMockClient(ctrl) + if table.expect { + client.EXPECT().GetLeaves(gomock.Any(), gomock.Any()).Return(table.rsp, table.err) + } + i := Instance{ + Config: testConfig, + Client: client, + } + + // Create HTTP request + url := types.EndpointGetLeaves.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.EndpointGetLeaves).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) + } + if w.Code != http.StatusOK { + return + } + + list := types.Leaves{} + if err := list.FromASCII(w.Body); err != nil { + t.Fatalf("must unmarshal leaf list: %v", err) + } + if got, want := &list, table.rsp; !reflect.DeepEqual(got, want) { + t.Errorf("got leaf list\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) + } + }() + } +} + +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 mustLeafBuffer(t *testing.T, shardHint uint64, checksum types.Hash, wantSig bool) io.Reader { + t.Helper() + + vk, sk, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("must generate ed25519 keys: %v", err) + } + msg := types.Statement{ + ShardHint: shardHint, + Checksum: checksum, + } + sig := ed25519.Sign(sk, msg.ToBinary()) + if !wantSig { + sig[0] += 1 + } + return bytes.NewBufferString(fmt.Sprintf( + "%s=%d\n"+"%s=%x\n"+"%s=%x\n"+"%s=%x\n"+"%s=%s\n", + "shard_hint", shardHint, + "checksum", checksum[:], + "signature", sig, + "verification_key", vk, + "domain_hint", "example.com", + )) +} diff --git a/pkg/instance/instance.go b/pkg/instance/instance.go index 4dff31a..9f71aa3 100644 --- a/pkg/instance/instance.go +++ b/pkg/instance/instance.go @@ -3,16 +3,15 @@ package instance import ( "context" "crypto" - "crypto/ed25519" "fmt" "net/http" "time" + "git.sigsum.org/sigsum-lib-go/pkg/requests" + "git.sigsum.org/sigsum-lib-go/pkg/types" + "git.sigsum.org/sigsum-log-go/pkg/db" "git.sigsum.org/sigsum-log-go/pkg/dns" "git.sigsum.org/sigsum-log-go/pkg/state" - "git.sigsum.org/sigsum-log-go/pkg/trillian" - "git.sigsum.org/sigsum-log-go/pkg/types" - "github.com/golang/glog" ) // Config is a collection of log parameters @@ -26,27 +25,18 @@ type Config struct { ShardStart uint64 // Shard interval start (num seconds since UNIX epoch) // Witnesses map trusted witness identifiers to public verification keys - Witnesses map[[types.HashSize]byte][types.VerificationKeySize]byte + Witnesses map[types.Hash]types.PublicKey } // 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 + Client db.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 DNS dns.Verifier // checks if domain name knows a public key } -// Handler implements the http.Handler interface, and contains a reference -// to a sigsum 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 sigsum handlers func (i *Instance) Handlers() []Handler { return []Handler{ @@ -62,51 +52,13 @@ func (i *Instance) Handlers() []Handler { } } -// Path returns a path that should be configured for this handler -func (h Handler) Path() string { - if len(h.Instance.Prefix) == 0 { - return h.Endpoint.Path("", "sigsum", "v0") - } - return h.Endpoint.Path("", h.Instance.Prefix, "sigsum", "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(ctx context.Context, 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) +func (i *Instance) leafRequestFromHTTP(ctx context.Context, r *http.Request) (*requests.Leaf, error) { + var req requests.Leaf + if err := req.FromASCII(r.Body); err != nil { + return nil, fmt.Errorf("FromASCII: %v", err) } - vk := ed25519.PublicKey(req.VerificationKey[:]) - msg := req.Message.Marshal() - sig := req.Signature[:] - if !ed25519.Verify(vk, msg, sig) { + if !req.Statement.Verify(&req.VerificationKey, &req.Signature) { return nil, fmt.Errorf("invalid signature") } shardEnd := uint64(time.Now().Unix()) @@ -116,27 +68,27 @@ func (i *Instance) leafRequestFromHTTP(ctx context.Context, r *http.Request) (*t if req.ShardHint > shardEnd { return nil, fmt.Errorf("invalid shard hint: %d not in [%d, %d]", req.ShardHint, i.ShardStart, shardEnd) } - if err := i.DNS.Verify(ctx, req.DomainHint, req.VerificationKey); err != nil { + if err := i.DNS.Verify(ctx, req.DomainHint, &req.VerificationKey); err != nil { return nil, fmt.Errorf("invalid domain hint: %v", err) } 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("UnmarshalASCII: %v", err) +func (i *Instance) cosignatureRequestFromHTTP(r *http.Request) (*requests.Cosignature, error) { + var req requests.Cosignature + if err := req.FromASCII(r.Body); err != nil { + return nil, fmt.Errorf("FromASCII: %v", err) } - if _, ok := i.Witnesses[*req.KeyHash]; !ok { + 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) +func (i *Instance) consistencyProofRequestFromHTTP(r *http.Request) (*requests.ConsistencyProof, error) { + var req requests.ConsistencyProof + if err := req.FromASCII(r.Body); err != nil { + return nil, fmt.Errorf("FromASCII: %v", err) } if req.OldSize < 1 { return nil, fmt.Errorf("OldSize(%d) must be larger than zero", req.OldSize) @@ -147,10 +99,10 @@ func (i *Instance) consistencyProofRequestFromHTTP(r *http.Request) (*types.Cons 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) +func (i *Instance) inclusionProofRequestFromHTTP(r *http.Request) (*requests.InclusionProof, error) { + var req requests.InclusionProof + if err := req.FromASCII(r.Body); err != nil { + return nil, fmt.Errorf("FromASCII: %v", err) } if req.TreeSize < 2 { // TreeSize:0 => not possible to prove inclusion of anything @@ -160,10 +112,10 @@ func (i *Instance) inclusionProofRequestFromHTTP(r *http.Request) (*types.Inclus 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) +func (i *Instance) leavesRequestFromHTTP(r *http.Request) (*requests.Leaves, error) { + var req requests.Leaves + if err := req.FromASCII(r.Body); err != nil { + return nil, fmt.Errorf("FromASCII: %v", err) } if req.StartSize > req.EndSize { diff --git a/pkg/instance/instance_test.go b/pkg/instance/instance_test.go deleted file mode 100644 index 6ca7baf..0000000 --- a/pkg/instance/instance_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package instance - -import ( - "net/http" - "net/http/httptest" - "testing" - - "git.sigsum.org/sigsum-log-go/pkg/types" -) - -// TestHandlers check that the expected handlers are configured -func TestHandlers(t *testing.T) { - endpoints := map[types.Endpoint]bool{ - types.EndpointAddLeaf: false, - types.EndpointAddCosignature: false, - types.EndpointGetTreeHeadLatest: false, - types.EndpointGetTreeHeadToSign: false, - types.EndpointGetTreeHeadCosigned: false, - types.EndpointGetConsistencyProof: false, - types.EndpointGetInclusionProof: false, - types.EndpointGetLeaves: false, - types.Endpoint("get-checkpoint"): false, - } - i := &Instance{ - Config: testConfig, - } - for _, handler := range i.Handlers() { - if _, ok := endpoints[handler.Endpoint]; !ok { - t.Errorf("got unexpected endpoint: %s", handler.Endpoint) - } - endpoints[handler.Endpoint] = true - } - for endpoint, ok := range endpoints { - if !ok { - t.Errorf("endpoint %s is not configured", endpoint) - } - } -} - -// TestServeHTTP checks that invalid HTTP methods are rejected -func TestServeHTTP(t *testing.T) { - i := &Instance{ - Config: testConfig, - } - for _, handler := range i.Handlers() { - // Prepare invalid HTTP request - method := http.MethodPost - if method == handler.Method { - method = http.MethodGet - } - url := handler.Endpoint.Path("http://example.com", i.Prefix) - req, err := http.NewRequest(method, url, nil) - if err != nil { - t.Fatalf("must create HTTP request: %v", err) - } - w := httptest.NewRecorder() - - // Check that it is rejected - handler.ServeHTTP(w, req) - if got, want := w.Code, http.StatusMethodNotAllowed; got != want { - t.Errorf("got HTTP code %v but wanted %v for endpoint %q", got, want, handler.Endpoint) - } - } -} - -// TestPath checks that Path works for an endpoint (add-leaf) -func TestPath(t *testing.T) { - for _, table := range []struct { - description string - prefix string - want string - }{ - { - description: "no prefix", - want: "/sigsum/v0/add-leaf", - }, - { - description: "a prefix", - prefix: "test-prefix", - want: "/test-prefix/sigsum/v0/add-leaf", - }, - } { - instance := &Instance{ - Config: Config{ - Prefix: table.prefix, - }, - } - handler := Handler{ - Instance: instance, - Handler: addLeaf, - Endpoint: types.EndpointAddLeaf, - Method: http.MethodPost, - } - if got, want := handler.Path(), table.want; got != want { - t.Errorf("got path %v but wanted %v", got, want) - } - } -} -- cgit v1.2.3