diff options
Diffstat (limited to 'internal/node/primary')
| -rw-r--r-- | internal/node/primary/endpoint_external.go | 147 | ||||
| -rw-r--r-- | internal/node/primary/endpoint_external_test.go | 626 | ||||
| -rw-r--r-- | internal/node/primary/endpoint_internal.go | 30 | ||||
| -rw-r--r-- | internal/node/primary/endpoint_internal_test.go | 82 | ||||
| -rw-r--r-- | internal/node/primary/primary.go | 74 | ||||
| -rw-r--r-- | internal/node/primary/primary_test.go | 75 | 
6 files changed, 1034 insertions, 0 deletions
| diff --git a/internal/node/primary/endpoint_external.go b/internal/node/primary/endpoint_external.go new file mode 100644 index 0000000..42e8fcb --- /dev/null +++ b/internal/node/primary/endpoint_external.go @@ -0,0 +1,147 @@ +package primary + +// This file implements external HTTP handler callbacks for primary nodes. + +import ( +	"context" +	"fmt" +	"net/http" + +	"git.sigsum.org/log-go/internal/node/handler" +	"git.sigsum.org/log-go/internal/requests" +	"git.sigsum.org/sigsum-go/pkg/log" +) + +func addLeaf(ctx context.Context, c handler.Config, w http.ResponseWriter, r *http.Request) (int, error) { +	p := c.(Primary) +	log.Debug("handling add-leaf request") +	req, err := requests.LeafRequestFromHTTP(r, p.Config.ShardStart, ctx, p.DNS) +	if err != nil { +		return http.StatusBadRequest, err +	} + +	sth := p.Stateman.ToCosignTreeHead() +	sequenced, err := p.TrillianClient.AddLeaf(ctx, req, sth.TreeSize) +	if err != nil { +		return http.StatusInternalServerError, err +	} +	if sequenced { +		return http.StatusOK, nil +	} else { +		return http.StatusAccepted, nil +	} +} + +func addCosignature(ctx context.Context, c handler.Config, w http.ResponseWriter, r *http.Request) (int, error) { +	p := c.(Primary) +	log.Debug("handling add-cosignature request") +	req, err := requests.CosignatureRequestFromHTTP(r, p.Witnesses) +	if err != nil { +		return http.StatusBadRequest, err +	} +	vk := p.Witnesses[req.KeyHash] +	if err := p.Stateman.AddCosignature(ctx, &vk, &req.Cosignature); err != nil { +		return http.StatusBadRequest, err +	} +	return http.StatusOK, nil +} + +func getTreeHeadToCosign(ctx context.Context, c handler.Config, w http.ResponseWriter, _ *http.Request) (int, error) { +	p := c.(Primary) +	log.Debug("handling get-tree-head-to-cosign request") +	sth := p.Stateman.ToCosignTreeHead() +	if err := sth.ToASCII(w); err != nil { +		return http.StatusInternalServerError, err +	} +	return http.StatusOK, nil +} + +func getTreeHeadCosigned(ctx context.Context, c handler.Config, w http.ResponseWriter, _ *http.Request) (int, error) { +	p := c.(Primary) +	log.Debug("handling get-tree-head-cosigned request") +	cth, err := p.Stateman.CosignedTreeHead(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, c handler.Config, w http.ResponseWriter, r *http.Request) (int, error) { +	p := c.(Primary) +	log.Debug("handling get-consistency-proof request") +	req, err := requests.ConsistencyProofRequestFromHTTP(r) +	if err != nil { +		return http.StatusBadRequest, err +	} + +	curTree := p.Stateman.ToCosignTreeHead() +	if req.NewSize > curTree.TreeHead.TreeSize { +		return http.StatusBadRequest, fmt.Errorf("new_size outside of current tree") +	} + +	proof, err := p.TrillianClient.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, c handler.Config, w http.ResponseWriter, r *http.Request) (int, error) { +	p := c.(Primary) +	log.Debug("handling get-inclusion-proof request") +	req, err := requests.InclusionProofRequestFromHTTP(r) +	if err != nil { +		return http.StatusBadRequest, err +	} + +	curTree := p.Stateman.ToCosignTreeHead() +	if req.TreeSize > curTree.TreeHead.TreeSize { +		return http.StatusBadRequest, fmt.Errorf("tree_size outside of current tree") +	} + +	proof, err := p.TrillianClient.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 getLeavesGeneral(ctx context.Context, c handler.Config, w http.ResponseWriter, r *http.Request, doLimitToCurrentTree bool) (int, error) { +	p := c.(Primary) +	log.Debug("handling get-leaves request") +	req, err := requests.LeavesRequestFromHTTP(r, uint64(p.MaxRange)) +	if err != nil { +		return http.StatusBadRequest, err +	} + +	if doLimitToCurrentTree { +		curTree := p.Stateman.ToCosignTreeHead() +		if req.EndSize >= curTree.TreeHead.TreeSize { +			return http.StatusBadRequest, fmt.Errorf("end_size outside of current tree") +		} +	} + +	leaves, err := p.TrillianClient.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 +} + +func getLeavesExternal(ctx context.Context, c handler.Config, w http.ResponseWriter, r *http.Request) (int, error) { +	return getLeavesGeneral(ctx, c, w, r, true) +} diff --git a/internal/node/primary/endpoint_external_test.go b/internal/node/primary/endpoint_external_test.go new file mode 100644 index 0000000..7ee161b --- /dev/null +++ b/internal/node/primary/endpoint_external_test.go @@ -0,0 +1,626 @@ +package primary + +import ( +	"bytes" +	"crypto/ed25519" +	"crypto/rand" +	"fmt" +	"io" +	"net/http" +	"net/http/httptest" +	"reflect" +	"testing" +	"time" + +	mocksDB "git.sigsum.org/log-go/internal/mocks/db" +	mocksDNS "git.sigsum.org/log-go/internal/mocks/dns" +	mocksState "git.sigsum.org/log-go/internal/mocks/state" +	"git.sigsum.org/log-go/internal/node/handler" +	"git.sigsum.org/sigsum-go/pkg/merkle" +	"git.sigsum.org/sigsum-go/pkg/types" +	"github.com/golang/mock/gomock" +) + +var ( +	testSTH = &types.SignedTreeHead{ +		TreeHead:  *testTH, +		Signature: types.Signature{}, +	} +	testCTH = &types.CosignedTreeHead{ +		SignedTreeHead: *testSTH, +		Cosignature:    []types.Signature{types.Signature{}}, +		KeyHash:        []merkle.Hash{merkle.Hash{}}, +	} +	sth1 = types.SignedTreeHead{TreeHead: types.TreeHead{TreeSize: 1}} +	sth2 = types.SignedTreeHead{TreeHead: types.TreeHead{TreeSize: 2}} // 2 < testConfig.MaxRange +	sth5 = types.SignedTreeHead{TreeHead: types.TreeHead{TreeSize: 5}} // 5 >= testConfig.MaxRange+1 +) + +// TODO: remove tests that are now located in internal/requests instead + +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 +		expectStateman bool +		sequenced      bool // return value from db.AddLeaf() +		sthStateman    *types.SignedTreeHead +	}{ +		{ +			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, merkle.Hash{}, false), +			wantCode:    http.StatusBadRequest, +		}, +		{ +			description: "invalid: bad request (shard hint is before shard start)", +			ascii:       mustLeafBuffer(t, 9, merkle.Hash{}, true), +			wantCode:    http.StatusBadRequest, +		}, +		{ +			description: "invalid: bad request (shard hint is after shard end)", +			ascii:       mustLeafBuffer(t, uint64(time.Now().Unix())+1024, merkle.Hash{}, true), +			wantCode:    http.StatusBadRequest, +		}, +		{ +			description: "invalid: failed verifying domain hint", +			ascii:       mustLeafBuffer(t, 10, merkle.Hash{}, true), +			expectDNS:   true, +			errDNS:      fmt.Errorf("something went wrong"), +			wantCode:    http.StatusBadRequest, +		}, +		{ +			description:    "invalid: backend failure", +			ascii:          mustLeafBuffer(t, 10, merkle.Hash{}, true), +			expectDNS:      true, +			expectStateman: true, +			sthStateman:    testSTH, +			expectTrillian: true, +			errTrillian:    fmt.Errorf("something went wrong"), +			wantCode:       http.StatusInternalServerError, +		}, +		{ +			description:    "valid: 202", +			ascii:          mustLeafBuffer(t, 10, merkle.Hash{}, true), +			expectDNS:      true, +			expectStateman: true, +			sthStateman:    testSTH, +			expectTrillian: true, +			wantCode:       http.StatusAccepted, +		}, +		{ +			description:    "valid: 200", +			ascii:          mustLeafBuffer(t, 10, merkle.Hash{}, true), +			expectDNS:      true, +			expectStateman: true, +			sthStateman:    testSTH, +			expectTrillian: true, +			sequenced:      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(), gomock.Any()).Return(table.sequenced, table.errTrillian) +			} +			stateman := mocksState.NewMockStateManager(ctrl) +			if table.expectStateman { +				stateman.EXPECT().ToCosignTreeHead().Return(table.sthStateman) +			} +			node := Primary{ +				Config:         testConfig, +				TrillianClient: client, +				Stateman:       stateman, +				DNS:            dns, +			} + +			// Create HTTP request +			url := types.EndpointAddLeaf.Path("http://example.com", node.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() +			mustHandlePublic(t, node, 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", *merkle.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", *merkle.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) +			} +			node := Primary{ +				Config:   testConfig, +				Stateman: stateman, +			} + +			// Create HTTP request +			url := types.EndpointAddCosignature.Path("http://example.com", node.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() +			mustHandlePublic(t, node, 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 TestGetTreeToCosign(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: "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().ToCosignTreeHead().Return(table.rsp) +			} +			node := Primary{ +				Config:   testConfig, +				Stateman: stateman, +			} + +			// Create HTTP request +			url := types.EndpointGetTreeHeadToCosign.Path("http://example.com", node.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() +			mustHandlePublic(t, node, types.EndpointGetTreeHeadToCosign).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: no cosigned STH", +			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().CosignedTreeHead(gomock.Any()).Return(table.rsp, table.err) +			} +			node := Primary{ +				Config:   testConfig, +				Stateman: stateman, +			} + +			// Create HTTP request +			url := types.EndpointGetTreeHeadCosigned.Path("http://example.com", node.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() +			mustHandlePublic(t, node, 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) { +	for _, table := range []struct { +		description string +		params      string // params is the query's url params +		sth         *types.SignedTreeHead +		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)", +			params:      "a/1", +			wantCode:    http.StatusBadRequest, +		}, +		{ +			description: "invalid: bad request (OldSize is zero)", +			params:      "0/1", +			wantCode:    http.StatusBadRequest, +		}, +		{ +			description: "invalid: bad request (OldSize > NewSize)", +			params:      "2/1", +			wantCode:    http.StatusBadRequest, +		}, +		{ +			description: "invalid: bad request (NewSize > tree size)", +			params:      "1/2", +			sth:         &sth1, +			wantCode:    http.StatusBadRequest, +		}, +		{ +			description: "invalid: backend failure", +			params:      "1/2", +			sth:         &sth2, +			expect:      true, +			err:         fmt.Errorf("something went wrong"), +			wantCode:    http.StatusInternalServerError, +		}, +		{ +			description: "valid", +			params:      "1/2", +			sth:         &sth2, +			expect:      true, +			rsp: &types.ConsistencyProof{ +				OldSize: 1, +				NewSize: 2, +				Path: []merkle.Hash{ +					*merkle.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) +			} +			stateman := mocksState.NewMockStateManager(ctrl) +			if table.sth != nil { +				stateman.EXPECT().ToCosignTreeHead().Return(table.sth) +			} +			node := Primary{ +				Config:         testConfig, +				TrillianClient: client, +				Stateman:       stateman, +			} + +			// Create HTTP request +			url := types.EndpointGetConsistencyProof.Path("http://example.com", node.Prefix()) +			req, err := http.NewRequest(http.MethodGet, url+table.params, nil) +			if err != nil { +				t.Fatalf("must create http request: %v", err) +			} + +			// Run HTTP request +			w := httptest.NewRecorder() +			mustHandlePublic(t, node, 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) { +	for _, table := range []struct { +		description string +		params      string // params is the query's url params +		sth         *types.SignedTreeHead +		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)", +			params:      "a/0000000000000000000000000000000000000000000000000000000000000000", +			wantCode:    http.StatusBadRequest, +		}, +		{ +			description: "invalid: bad request (no proof available for tree size 1)", +			params:      "1/0000000000000000000000000000000000000000000000000000000000000000", +			wantCode:    http.StatusBadRequest, +		}, +		{ +			description: "invalid: bad request (request outside current tree size)", +			params:      "2/0000000000000000000000000000000000000000000000000000000000000000", +			sth:         &sth1, +			wantCode:    http.StatusBadRequest, +		}, +		{ +			description: "invalid: backend failure", +			params:      "2/0000000000000000000000000000000000000000000000000000000000000000", +			sth:         &sth2, +			expect:      true, +			err:         fmt.Errorf("something went wrong"), +			wantCode:    http.StatusInternalServerError, +		}, +		{ +			description: "valid", +			params:      "2/0000000000000000000000000000000000000000000000000000000000000000", +			sth:         &sth2, +			expect:      true, +			rsp: &types.InclusionProof{ +				TreeSize:  2, +				LeafIndex: 0, +				Path: []merkle.Hash{ +					*merkle.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) +			} +			stateman := mocksState.NewMockStateManager(ctrl) +			if table.sth != nil { +				stateman.EXPECT().ToCosignTreeHead().Return(table.sth) +			} +			node := Primary{ +				Config:         testConfig, +				TrillianClient: client, +				Stateman:       stateman, +			} + +			// Create HTTP request +			url := types.EndpointGetInclusionProof.Path("http://example.com", node.Prefix()) +			req, err := http.NewRequest(http.MethodGet, url+table.params, nil) +			if err != nil { +				t.Fatalf("must create http request: %v", err) +			} + +			// Run HTTP request +			w := httptest.NewRecorder() +			mustHandlePublic(t, node, 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) { +	for _, table := range []struct { +		description string +		params      string // params is the query's url params +		sth         *types.SignedTreeHead +		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)", +			params:      "a/1", +			wantCode:    http.StatusBadRequest, +		}, +		{ +			description: "invalid: bad request (StartSize > EndSize)", +			params:      "1/0", +			wantCode:    http.StatusBadRequest, +		}, +		{ +			description: "invalid: bad request (EndSize >= current tree size)", +			params:      "0/2", +			sth:         &sth2, +			wantCode:    http.StatusBadRequest, +		}, +		{ +			description: "invalid: backend failure", +			params:      "0/0", +			sth:         &sth2, +			expect:      true, +			err:         fmt.Errorf("something went wrong"), +			wantCode:    http.StatusInternalServerError, +		}, +		{ +			description: "valid: one more entry than the configured MaxRange", +			params:      fmt.Sprintf("%d/%d", 0, testConfig.MaxRange), // query will be pruned +			sth:         &sth5, +			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:  merkle.Hash{}, +						}, +						Signature: types.Signature{}, +						KeyHash:   merkle.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) +			} +			stateman := mocksState.NewMockStateManager(ctrl) +			if table.sth != nil { +				stateman.EXPECT().ToCosignTreeHead().Return(table.sth) +			} +			node := Primary{ +				Config:         testConfig, +				TrillianClient: client, +				Stateman:       stateman, +			} + +			// Create HTTP request +			url := types.EndpointGetLeaves.Path("http://example.com", node.Prefix()) +			req, err := http.NewRequest(http.MethodGet, url+table.params, nil) +			if err != nil { +				t.Fatalf("must create http request: %v", err) +			} + +			// Run HTTP request +			w := httptest.NewRecorder() +			mustHandlePublic(t, node, 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 mustHandlePublic(t *testing.T, p Primary, e types.Endpoint) handler.Handler { +	for _, handler := range p.PublicHTTPHandlers() { +		if handler.Endpoint == e { +			return handler +		} +	} +	t.Fatalf("must handle endpoint: %v", e) +	return handler.Handler{} +} + +func mustLeafBuffer(t *testing.T, shardHint uint64, message merkle.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:  *merkle.HashFn(message[:]), +	} +	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, +		"message", message[:], +		"signature", sig, +		"public_key", vk, +		"domain_hint", "example.com", +	)) +} diff --git a/internal/node/primary/endpoint_internal.go b/internal/node/primary/endpoint_internal.go new file mode 100644 index 0000000..f7a684f --- /dev/null +++ b/internal/node/primary/endpoint_internal.go @@ -0,0 +1,30 @@ +package primary + +// This file implements internal HTTP handler callbacks for primary nodes. + +import ( +	"context" +	"fmt" +	"net/http" + +	"git.sigsum.org/log-go/internal/node/handler" +	"git.sigsum.org/sigsum-go/pkg/log" +	"git.sigsum.org/sigsum-go/pkg/types" +) + +func getTreeHeadUnsigned(ctx context.Context, c handler.Config, w http.ResponseWriter, _ *http.Request) (int, error) { +	log.Debug("handling %s request", types.EndpointGetTreeHeadUnsigned) +	p := c.(Primary) +	th, err := p.TrillianClient.GetTreeHead(ctx) +	if err != nil { +		return http.StatusInternalServerError, fmt.Errorf("failed getting tree head: %v", err) +	} +	if err := th.ToASCII(w); err != nil { +		return http.StatusInternalServerError, err +	} +	return http.StatusOK, nil +} + +func getLeavesInternal(ctx context.Context, c handler.Config, w http.ResponseWriter, r *http.Request) (int, error) { +	return getLeavesGeneral(ctx, c, w, r, false) +} diff --git a/internal/node/primary/endpoint_internal_test.go b/internal/node/primary/endpoint_internal_test.go new file mode 100644 index 0000000..6c76c33 --- /dev/null +++ b/internal/node/primary/endpoint_internal_test.go @@ -0,0 +1,82 @@ +package primary + +import ( +	"fmt" +	"net/http" +	"net/http/httptest" +	"testing" + +	mocksDB "git.sigsum.org/log-go/internal/mocks/db" +	"git.sigsum.org/log-go/internal/node/handler" +	"git.sigsum.org/sigsum-go/pkg/merkle" +	"git.sigsum.org/sigsum-go/pkg/types" +	"github.com/golang/mock/gomock" +) + +var ( +	testTH = &types.TreeHead{ +		Timestamp: 0, +		TreeSize:  0, +		RootHash:  *merkle.HashFn([]byte("root hash")), +	} +) + +func TestGetTreeHeadUnsigned(t *testing.T) { +	for _, table := range []struct { +		description string +		expect      bool            // set if a mock answer is expected +		rsp         *types.TreeHead // 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:         testTH, +			wantCode:    http.StatusOK, +		}, +	} { +		// Run deferred functions at the end of each iteration +		func() { +			ctrl := gomock.NewController(t) +			defer ctrl.Finish() +			trillianClient := mocksDB.NewMockClient(ctrl) +			trillianClient.EXPECT().GetTreeHead(gomock.Any()).Return(table.rsp, table.err) + +			node := Primary{ +				Config:         testConfig, +				TrillianClient: trillianClient, +			} + +			// Create HTTP request +			url := types.EndpointGetTreeHeadUnsigned.Path("http://example.com", node.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() +			mustHandleInternal(t, node, types.EndpointGetTreeHeadUnsigned).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 mustHandleInternal(t *testing.T, p Primary, e types.Endpoint) handler.Handler { +	for _, handler := range p.InternalHTTPHandlers() { +		if handler.Endpoint == e { +			return handler +		} +	} +	t.Fatalf("must handle endpoint: %v", e) +	return handler.Handler{} +} diff --git a/internal/node/primary/primary.go b/internal/node/primary/primary.go new file mode 100644 index 0000000..6128f49 --- /dev/null +++ b/internal/node/primary/primary.go @@ -0,0 +1,74 @@ +package primary + +import ( +	"crypto" +	"net/http" +	"time" + +	"git.sigsum.org/log-go/internal/db" +	"git.sigsum.org/log-go/internal/node/handler" +	"git.sigsum.org/log-go/internal/state" +	"git.sigsum.org/sigsum-go/pkg/client" +	"git.sigsum.org/sigsum-go/pkg/dns" +	"git.sigsum.org/sigsum-go/pkg/merkle" +	"git.sigsum.org/sigsum-go/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 +	ShardStart uint64        // Shard interval start (num seconds since UNIX epoch) + +	// Witnesses map trusted witness identifiers to public keys +	Witnesses map[merkle.Hash]types.PublicKey +} + +// Primary is an instance of the log's primary node +type Primary struct { +	Config +	PublicHTTPMux   *http.ServeMux +	InternalHTTPMux *http.ServeMux +	TrillianClient  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 +	Secondary       client.Client +} + +// Implementing handler.Config +func (p Primary) Prefix() string { +	return p.Config.Prefix +} +func (p Primary) LogID() string { +	return p.Config.LogID +} +func (p Primary) Deadline() time.Duration { +	return p.Config.Deadline +} + +// PublicHTTPHandlers returns all external handlers +func (p Primary) PublicHTTPHandlers() []handler.Handler { +	return []handler.Handler{ +		handler.Handler{p, addLeaf, types.EndpointAddLeaf, http.MethodPost}, +		handler.Handler{p, addCosignature, types.EndpointAddCosignature, http.MethodPost}, +		handler.Handler{p, getTreeHeadToCosign, types.EndpointGetTreeHeadToCosign, http.MethodGet}, +		handler.Handler{p, getTreeHeadCosigned, types.EndpointGetTreeHeadCosigned, http.MethodGet}, +		handler.Handler{p, getConsistencyProof, types.EndpointGetConsistencyProof, http.MethodGet}, +		handler.Handler{p, getInclusionProof, types.EndpointGetInclusionProof, http.MethodGet}, +		handler.Handler{p, getLeavesExternal, types.EndpointGetLeaves, http.MethodGet}, +	} +} + +// InternalHTTPHandlers() returns all internal handlers +func (p Primary) InternalHTTPHandlers() []handler.Handler { +	return []handler.Handler{ +		handler.Handler{p, getTreeHeadUnsigned, types.EndpointGetTreeHeadUnsigned, http.MethodGet}, +		handler.Handler{p, getConsistencyProof, types.EndpointGetConsistencyProof, http.MethodGet}, +		handler.Handler{p, getLeavesInternal, types.EndpointGetLeaves, http.MethodGet}, +	} +} diff --git a/internal/node/primary/primary_test.go b/internal/node/primary/primary_test.go new file mode 100644 index 0000000..5062955 --- /dev/null +++ b/internal/node/primary/primary_test.go @@ -0,0 +1,75 @@ +package primary + +import ( +	"fmt" +	"testing" + +	"git.sigsum.org/sigsum-go/pkg/merkle" +	"git.sigsum.org/sigsum-go/pkg/types" +) + +var ( +	testWitVK  = types.PublicKey{} +	testConfig = Config{ +		LogID:      fmt.Sprintf("%x", merkle.HashFn([]byte("logid"))[:]), +		TreeID:     0, +		Prefix:     "testonly", +		MaxRange:   3, +		Deadline:   10, +		Interval:   10, +		ShardStart: 10, +		Witnesses: map[merkle.Hash]types.PublicKey{ +			*merkle.HashFn(testWitVK[:]): testWitVK, +		}, +	} +) + +// TestPublicHandlers checks that the expected external handlers are configured +func TestPublicHandlers(t *testing.T) { +	endpoints := map[types.Endpoint]bool{ +		types.EndpointAddLeaf:             false, +		types.EndpointAddCosignature:      false, +		types.EndpointGetTreeHeadToCosign: false, +		types.EndpointGetTreeHeadCosigned: false, +		types.EndpointGetConsistencyProof: false, +		types.EndpointGetInclusionProof:   false, +		types.EndpointGetLeaves:           false, +	} +	node := Primary{ +		Config: testConfig, +	} +	for _, handler := range node.PublicHTTPHandlers() { +		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) +		} +	} +} + +// TestIntHandlers checks that the expected internal handlers are configured +func TestIntHandlers(t *testing.T) { +	endpoints := map[types.Endpoint]bool{ +		types.EndpointGetTreeHeadUnsigned: false, +		types.EndpointGetConsistencyProof: false, +		types.EndpointGetLeaves:           false, +	} +	node := Primary{ +		Config: testConfig, +	} +	for _, handler := range node.InternalHTTPHandlers() { +		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) +		} +	} +} | 
