diff options
Diffstat (limited to 'pkg/instance')
| -rw-r--r-- | pkg/instance/endpoint.go | 122 | ||||
| -rw-r--r-- | pkg/instance/endpoint_test.go | 480 | ||||
| -rw-r--r-- | pkg/instance/instance.go | 90 | ||||
| -rw-r--r-- | pkg/instance/instance_test.go | 158 | ||||
| -rw-r--r-- | pkg/instance/metric.go | 23 | ||||
| -rw-r--r-- | pkg/instance/request.go | 77 | ||||
| -rw-r--r-- | pkg/instance/request_test.go | 318 | 
7 files changed, 1268 insertions, 0 deletions
| diff --git a/pkg/instance/endpoint.go b/pkg/instance/endpoint.go new file mode 100644 index 0000000..5085c49 --- /dev/null +++ b/pkg/instance/endpoint.go @@ -0,0 +1,122 @@ +package stfe + +import ( +	"context" +	"net/http" + +	"github.com/golang/glog" +) + +func addLeaf(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) { +	glog.V(3).Info("handling add-entry request") +	req, err := i.leafRequestFromHTTP(r) +	if err != nil { +		return http.StatusBadRequest, err +	} +	if err := i.Client.AddLeaf(ctx, req); err != nil { +		return http.StatusInternalServerError, err +	} +	return http.StatusOK, nil +} + +func addCosignature(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) { +	glog.V(3).Info("handling add-cosignature request") +	req, err := i.cosignatureRequestFromHTTP(r) +	if err != nil { +		return http.StatusBadRequest, err +	} +	vk := i.Witnesses[*req.KeyHash] +	if err := i.Stateman.AddCosignature(ctx, &vk, req.Signature); err != nil { +		return http.StatusBadRequest, err +	} +	return http.StatusOK, nil +} + +func getTreeHeadLatest(ctx context.Context, i *Instance, w http.ResponseWriter, _ *http.Request) (int, error) { +	glog.V(3).Info("handling get-tree-head-latest request") +	sth, err := i.Stateman.Latest(ctx) +	if err != nil { +		return http.StatusInternalServerError, err +	} +	if err := sth.MarshalASCII(w); err != nil { +		return http.StatusInternalServerError, err +	} +	return http.StatusOK, nil +} + +func getTreeHeadToSign(ctx context.Context, i *Instance, w http.ResponseWriter, _ *http.Request) (int, error) { +	glog.V(3).Info("handling get-tree-head-to-sign request") +	sth, err := i.Stateman.ToSign(ctx) +	if err != nil { +		return http.StatusInternalServerError, err +	} +	if err := sth.MarshalASCII(w); err != nil { +		return http.StatusInternalServerError, err +	} +	return http.StatusOK, nil +} + +func getTreeHeadCosigned(ctx context.Context, i *Instance, w http.ResponseWriter, _ *http.Request) (int, error) { +	glog.V(3).Info("handling get-tree-head-cosigned request") +	sth, err := i.Stateman.Cosigned(ctx) +	if err != nil { +		return http.StatusInternalServerError, err +	} +	if err := sth.MarshalASCII(w); err != nil { +		return http.StatusInternalServerError, err +	} +	return http.StatusOK, nil +} + +func getConsistencyProof(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) { +	glog.V(3).Info("handling get-consistency-proof request") +	req, err := i.consistencyProofRequestFromHTTP(r) +	if err != nil { +		return http.StatusBadRequest, err +	} + +	proof, err := i.Client.GetConsistencyProof(ctx, req) +	if err != nil { +		return http.StatusInternalServerError, err +	} +	if err := proof.MarshalASCII(w); err != nil { +		return http.StatusInternalServerError, err +	} +	return http.StatusOK, nil +} + +func getInclusionProof(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) { +	glog.V(3).Info("handling get-proof-by-hash request") +	req, err := i.inclusionProofRequestFromHTTP(r) +	if err != nil { +		return http.StatusBadRequest, err +	} + +	proof, err := i.Client.GetInclusionProof(ctx, req) +	if err != nil { +		return http.StatusInternalServerError, err +	} +	if err := proof.MarshalASCII(w); err != nil { +		return http.StatusInternalServerError, err +	} +	return http.StatusOK, nil +} + +func getLeaves(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) { +	glog.V(3).Info("handling get-leaves request") +	req, err := i.leavesRequestFromHTTP(r) +	if err != nil { +		return http.StatusBadRequest, err +	} + +	leaves, err := i.Client.GetLeaves(ctx, req) +	if err != nil { +		return http.StatusInternalServerError, err +	} +	for _, leaf := range *leaves { +		if err := leaf.MarshalASCII(w); err != nil { +			return http.StatusInternalServerError, err +		} +	} +	return http.StatusOK, nil +} diff --git a/pkg/instance/endpoint_test.go b/pkg/instance/endpoint_test.go new file mode 100644 index 0000000..8511b8d --- /dev/null +++ b/pkg/instance/endpoint_test.go @@ -0,0 +1,480 @@ +package stfe + +import ( +	"bytes" +	"context" +	"fmt" +	"net/http" +	"net/http/httptest" +	"reflect" +	"testing" + +	"github.com/golang/mock/gomock" +	cttestdata "github.com/google/certificate-transparency-go/trillian/testdata" +	"github.com/google/trillian" +	"github.com/system-transparency/stfe/pkg/testdata" +	"github.com/system-transparency/stfe/pkg/types" +) + +func TestEndpointAddEntry(t *testing.T) { +	for _, table := range []struct { +		description string +		breq        *bytes.Buffer +		trsp        *trillian.QueueLeafResponse +		terr        error +		wantCode    int +	}{ +		{ +			description: "invalid: bad request: empty", +			breq:        bytes.NewBuffer(nil), +			wantCode:    http.StatusBadRequest, +		}, +		{ +			description: "invalid: bad Trillian response: error", +			breq:        testdata.AddSignedChecksumBuffer(t, testdata.Ed25519SkSubmitter, testdata.Ed25519VkSubmitter), +			terr:        fmt.Errorf("backend failure"), +			wantCode:    http.StatusInternalServerError, +		}, +		{ +			description: "valid", +			breq:        testdata.AddSignedChecksumBuffer(t, testdata.Ed25519SkSubmitter, testdata.Ed25519VkSubmitter), +			trsp:        testdata.DefaultTQlr(t, false), +			wantCode:    http.StatusOK, +		}, +	} { +		func() { // run deferred functions at the end of each iteration +			ti := newTestInstance(t, nil) +			defer ti.ctrl.Finish() + +			url := EndpointAddEntry.Path("http://example.com", ti.instance.LogParameters.Prefix) +			req, err := http.NewRequest("POST", url, table.breq) +			if err != nil { +				t.Fatalf("must create http request: %v", err) +			} +			req.Header.Set("Content-Type", "application/octet-stream") +			if table.trsp != nil || table.terr != nil { +				ti.client.EXPECT().QueueLeaf(newDeadlineMatcher(), gomock.Any()).Return(table.trsp, table.terr) +			} + +			w := httptest.NewRecorder() +			ti.postHandler(t, EndpointAddEntry).ServeHTTP(w, req) +			if got, want := w.Code, table.wantCode; got != want { +				t.Errorf("got error code %d but wanted %d in test %q", got, want, table.description) +			} +		}() +	} +} + +func TestEndpointAddCosignature(t *testing.T) { +	for _, table := range []struct { +		description string +		breq        *bytes.Buffer +		wantCode    int +	}{ +		{ +			description: "invalid: bad request: empty", +			breq:        bytes.NewBuffer(nil), +			wantCode:    http.StatusBadRequest, +		}, +		{ +			description: "invalid: signed wrong sth", // newLogParameters() use testdata.Ed25519VkLog as default +			breq:        testdata.AddCosignatureBuffer(t, testdata.DefaultSth(t, testdata.Ed25519VkLog2), &testdata.Ed25519SkWitness, &testdata.Ed25519VkWitness), +			wantCode:    http.StatusBadRequest, +		}, +		{ +			description: "valid", +			breq:        testdata.AddCosignatureBuffer(t, testdata.DefaultSth(t, testdata.Ed25519VkLog), &testdata.Ed25519SkWitness, &testdata.Ed25519VkWitness), +			wantCode:    http.StatusOK, +		}, +	} { +		func() { // run deferred functions at the end of each iteration +			ti := newTestInstance(t, nil) +			defer ti.ctrl.Finish() + +			url := EndpointAddCosignature.Path("http://example.com", ti.instance.LogParameters.Prefix) +			req, err := http.NewRequest("POST", url, table.breq) +			if err != nil { +				t.Fatalf("must create http request: %v", err) +			} +			req.Header.Set("Content-Type", "application/octet-stream") + +			w := httptest.NewRecorder() +			ti.postHandler(t, EndpointAddCosignature).ServeHTTP(w, req) +			if got, want := w.Code, table.wantCode; got != want { +				t.Errorf("got error code %d but wanted %d in test %q", got, want, table.description) +			} +		}() +	} +} + +func TestEndpointGetLatestSth(t *testing.T) { +	for _, table := range []struct { +		description string +		trsp        *trillian.GetLatestSignedLogRootResponse +		terr        error +		wantCode    int +		wantItem    *types.StItem +	}{ +		{ +			description: "backend failure", +			terr:        fmt.Errorf("backend failure"), +			wantCode:    http.StatusInternalServerError, +		}, +		{ +			description: "valid", +			trsp:        testdata.DefaultTSlr(t), +			wantCode:    http.StatusOK, +			wantItem:    testdata.DefaultSth(t, testdata.Ed25519VkLog), +		}, +	} { +		func() { // run deferred functions at the end of each iteration +			ti := newTestInstance(t, cttestdata.NewSignerWithFixedSig(nil, testdata.Signature)) +			ti.ctrl.Finish() + +			// Setup and run client query +			url := EndpointGetLatestSth.Path("http://example.com", ti.instance.LogParameters.Prefix) +			req, err := http.NewRequest("GET", url, nil) +			if err != nil { +				t.Fatalf("must create http request: %v", err) +			} +			if table.trsp != nil || table.terr != nil { +				ti.client.EXPECT().GetLatestSignedLogRoot(newDeadlineMatcher(), gomock.Any()).Return(table.trsp, table.terr) +			} + +			w := httptest.NewRecorder() +			ti.getHandler(t, EndpointGetLatestSth).ServeHTTP(w, req) +			if got, want := w.Code, table.wantCode; got != want { +				t.Errorf("got error code %d but wanted %d in test %q", got, want, table.description) +			} +			if w.Code != http.StatusOK { +				return +			} + +			var item types.StItem +			if err := types.Unmarshal([]byte(w.Body.String()), &item); err != nil { +				t.Errorf("valid response cannot be unmarshalled in test %q: %v", table.description, err) +			} +			if got, want := item, *table.wantItem; !reflect.DeepEqual(got, want) { +				t.Errorf("got item\n%v\n\tbut wanted\n%v\n\tin test %q", got, want, table.description) +			} +		}() +	} +} + +func TestEndpointGetStableSth(t *testing.T) { +	for _, table := range []struct { +		description  string +		useBadSource bool +		wantCode     int +		wantItem     *types.StItem +	}{ +		{ +			description:  "invalid: sth source failure", +			useBadSource: true, +			wantCode:     http.StatusInternalServerError, +		}, +		{ +			description: "valid", +			wantCode:    http.StatusOK, +			wantItem:    testdata.DefaultSth(t, testdata.Ed25519VkLog), +		}, +	} { +		func() { // run deferred functions at the end of each iteration +			ti := newTestInstance(t, nil) +			ti.ctrl.Finish() +			if table.useBadSource { +				ti.instance.SthSource = &ActiveSthSource{} +			} + +			// Setup and run client query +			url := EndpointGetStableSth.Path("http://example.com", ti.instance.LogParameters.Prefix) +			req, err := http.NewRequest("GET", url, nil) +			if err != nil { +				t.Fatalf("must create http request: %v", err) +			} + +			w := httptest.NewRecorder() +			ti.getHandler(t, EndpointGetStableSth).ServeHTTP(w, req) +			if got, want := w.Code, table.wantCode; got != want { +				t.Errorf("got error code %d but wanted %d in test %q", got, want, table.description) +			} +			if w.Code != http.StatusOK { +				return +			} + +			var item types.StItem +			if err := types.Unmarshal([]byte(w.Body.String()), &item); err != nil { +				t.Errorf("valid response cannot be unmarshalled in test %q: %v", table.description, err) +			} +			if got, want := item, *table.wantItem; !reflect.DeepEqual(got, want) { +				t.Errorf("got item\n%v\n\tbut wanted\n%v\n\tin test %q", got, want, table.description) +			} +		}() +	} +} + +func TestEndpointGetCosignedSth(t *testing.T) { +	for _, table := range []struct { +		description  string +		useBadSource bool +		wantCode     int +		wantItem     *types.StItem +	}{ +		{ +			description:  "invalid: sth source failure", +			useBadSource: true, +			wantCode:     http.StatusInternalServerError, +		}, +		{ +			description: "valid", +			wantCode:    http.StatusOK, +			wantItem:    testdata.DefaultCosth(t, testdata.Ed25519VkLog, [][32]byte{testdata.Ed25519VkWitness}), +		}, +	} { +		func() { // run deferred functions at the end of each iteration +			ti := newTestInstance(t, nil) +			ti.ctrl.Finish() +			if table.useBadSource { +				ti.instance.SthSource = &ActiveSthSource{} +			} + +			// Setup and run client query +			url := EndpointGetCosignedSth.Path("http://example.com", ti.instance.LogParameters.Prefix) +			req, err := http.NewRequest("GET", url, nil) +			if err != nil { +				t.Fatalf("must create http request: %v", err) +			} + +			w := httptest.NewRecorder() +			ti.getHandler(t, EndpointGetCosignedSth).ServeHTTP(w, req) +			if got, want := w.Code, table.wantCode; got != want { +				t.Errorf("got error code %d but wanted %d in test %q", got, want, table.description) +			} +			if w.Code != http.StatusOK { +				return +			} + +			var item types.StItem +			if err := types.Unmarshal([]byte(w.Body.String()), &item); err != nil { +				t.Errorf("valid response cannot be unmarshalled in test %q: %v", table.description, err) +			} +			if got, want := item, *table.wantItem; !reflect.DeepEqual(got, want) { +				t.Errorf("got item\n%v\n\tbut wanted\n%v\n\tin test %q", got, want, table.description) +			} +		}() +	} +} + +func TestEndpointGetProofByHash(t *testing.T) { +	for _, table := range []struct { +		description string +		breq        *bytes.Buffer +		trsp        *trillian.GetInclusionProofByHashResponse +		terr        error +		wantCode    int +		wantItem    *types.StItem +	}{ +		{ +			description: "invalid: bad request: empty", +			breq:        bytes.NewBuffer(nil), +			wantCode:    http.StatusBadRequest, +		}, +		{ +			description: "invalid: bad Trillian response: error", +			breq:        bytes.NewBuffer(marshal(t, types.GetProofByHashV1{TreeSize: 1, Hash: testdata.LeafHash})), +			terr:        fmt.Errorf("backend failure"), +			wantCode:    http.StatusInternalServerError, +		}, +		{ +			description: "valid", +			breq:        bytes.NewBuffer(marshal(t, types.GetProofByHashV1{TreeSize: 1, Hash: testdata.LeafHash})), +			trsp:        testdata.DefaultTGipbhr(t), +			wantCode:    http.StatusOK, +			wantItem:    testdata.DefaultInclusionProof(t, 1), +		}, +	} { +		func() { // run deferred functions at the end of each iteration +			ti := newTestInstance(t, nil) +			defer ti.ctrl.Finish() + +			url := EndpointGetProofByHash.Path("http://example.com", ti.instance.LogParameters.Prefix) +			req, err := http.NewRequest("POST", url, table.breq) +			if err != nil { +				t.Fatalf("must create http request: %v", err) +			} +			req.Header.Set("Content-Type", "application/octet-stream") +			if table.trsp != nil || table.terr != nil { +				ti.client.EXPECT().GetInclusionProofByHash(newDeadlineMatcher(), gomock.Any()).Return(table.trsp, table.terr) +			} + +			w := httptest.NewRecorder() +			ti.postHandler(t, EndpointGetProofByHash).ServeHTTP(w, req) +			if got, want := w.Code, table.wantCode; got != want { +				t.Errorf("got error code %d but wanted %d in test %q", got, want, table.description) +			} +			if w.Code != http.StatusOK { +				return +			} + +			var item types.StItem +			if err := types.Unmarshal([]byte(w.Body.String()), &item); err != nil { +				t.Errorf("valid response cannot be unmarshalled in test %q: %v", table.description, err) +			} +			if got, want := item, *table.wantItem; !reflect.DeepEqual(got, want) { +				t.Errorf("got item\n%v\n\tbut wanted\n%v\n\tin test %q", got, want, table.description) +			} +		}() +	} +} + +func TestEndpointGetConsistencyProof(t *testing.T) { +	for _, table := range []struct { +		description string +		breq        *bytes.Buffer +		trsp        *trillian.GetConsistencyProofResponse +		terr        error +		wantCode    int +		wantItem    *types.StItem +	}{ +		{ +			description: "invalid: bad request: empty", +			breq:        bytes.NewBuffer(nil), +			wantCode:    http.StatusBadRequest, +		}, +		{ +			description: "invalid: bad Trillian response: error", +			breq:        bytes.NewBuffer(marshal(t, types.GetConsistencyProofV1{First: 1, Second: 2})), +			terr:        fmt.Errorf("backend failure"), +			wantCode:    http.StatusInternalServerError, +		}, +		{ +			description: "valid", +			breq:        bytes.NewBuffer(marshal(t, types.GetConsistencyProofV1{First: 1, Second: 2})), +			trsp:        testdata.DefaultTGcpr(t), +			wantCode:    http.StatusOK, +			wantItem:    testdata.DefaultConsistencyProof(t, 1, 2), +		}, +	} { +		func() { // run deferred functions at the end of each iteration +			ti := newTestInstance(t, nil) +			defer ti.ctrl.Finish() + +			url := EndpointGetConsistencyProof.Path("http://example.com", ti.instance.LogParameters.Prefix) +			req, err := http.NewRequest("POST", url, table.breq) +			if err != nil { +				t.Fatalf("must create http request: %v", err) +			} +			req.Header.Set("Content-Type", "application/octet-stream") +			if table.trsp != nil || table.terr != nil { +				ti.client.EXPECT().GetConsistencyProof(newDeadlineMatcher(), gomock.Any()).Return(table.trsp, table.terr) +			} + +			w := httptest.NewRecorder() +			ti.postHandler(t, EndpointGetConsistencyProof).ServeHTTP(w, req) +			if got, want := w.Code, table.wantCode; got != want { +				t.Errorf("got error code %d but wanted %d in test %q", got, want, table.description) +			} +			if w.Code != http.StatusOK { +				return +			} + +			var item types.StItem +			if err := types.Unmarshal([]byte(w.Body.String()), &item); err != nil { +				t.Errorf("valid response cannot be unmarshalled in test %q: %v", table.description, err) +			} +			if got, want := item, *table.wantItem; !reflect.DeepEqual(got, want) { +				t.Errorf("got item\n%v\n\tbut wanted\n%v\n\tin test %q", got, want, table.description) +			} +		}() +	} +} + +func TestEndpointGetEntriesV1(t *testing.T) { +	for _, table := range []struct { +		description string +		breq        *bytes.Buffer +		trsp        *trillian.GetLeavesByRangeResponse +		terr        error +		wantCode    int +		wantItem    *types.StItemList +	}{ +		{ +			description: "invalid: bad request: empty", +			breq:        bytes.NewBuffer(nil), +			wantCode:    http.StatusBadRequest, +		}, +		{ +			description: "invalid: bad Trillian response: error", +			breq:        bytes.NewBuffer(marshal(t, types.GetEntriesV1{Start: 0, End: 0})), +			terr:        fmt.Errorf("backend failure"), +			wantCode:    http.StatusInternalServerError, +		}, +		{ +			description: "valid", // remember that newLogParameters() have testdata.MaxRange configured +			breq:        bytes.NewBuffer(marshal(t, types.GetEntriesV1{Start: 0, End: uint64(testdata.MaxRange - 1)})), +			trsp:        testdata.DefaultTGlbrr(t, 0, testdata.MaxRange-1), +			wantCode:    http.StatusOK, +			wantItem:    testdata.DefaultStItemList(t, 0, uint64(testdata.MaxRange)-1), +		}, +	} { +		func() { // run deferred functions at the end of each iteration +			ti := newTestInstance(t, nil) +			defer ti.ctrl.Finish() + +			url := EndpointGetEntries.Path("http://example.com", ti.instance.LogParameters.Prefix) +			req, err := http.NewRequest("POST", url, table.breq) +			if err != nil { +				t.Fatalf("must create http request: %v", err) +			} +			req.Header.Set("Content-Type", "application/octet-stream") +			if table.trsp != nil || table.terr != nil { +				ti.client.EXPECT().GetLeavesByRange(newDeadlineMatcher(), gomock.Any()).Return(table.trsp, table.terr) +			} + +			w := httptest.NewRecorder() +			ti.postHandler(t, EndpointGetEntries).ServeHTTP(w, req) +			if got, want := w.Code, table.wantCode; got != want { +				t.Errorf("got error code %d but wanted %d in test %q", got, want, table.description) +			} +			if w.Code != http.StatusOK { +				return +			} + +			var item types.StItemList +			if err := types.Unmarshal([]byte(w.Body.String()), &item); err != nil { +				t.Errorf("valid response cannot be unmarshalled in test %q: %v", table.description, err) +			} +			if got, want := item, *table.wantItem; !reflect.DeepEqual(got, want) { +				t.Errorf("got item\n%v\n\tbut wanted\n%v\n\tin test %q", got, want, table.description) +			} +		}() +	} +} + +// TODO: TestWriteOctetResponse +func TestWriteOctetResponse(t *testing.T) { +} + +// deadlineMatcher implements gomock.Matcher, such that an error is raised if +// there is no context.Context deadline set +type deadlineMatcher struct{} + +// newDeadlineMatcher returns a new DeadlineMatcher +func newDeadlineMatcher() gomock.Matcher { +	return &deadlineMatcher{} +} + +// Matches returns true if the passed interface is a context with a deadline +func (dm *deadlineMatcher) Matches(i interface{}) bool { +	ctx, ok := i.(context.Context) +	if !ok { +		return false +	} +	_, ok = ctx.Deadline() +	return ok +} + +// String is needed to implement gomock.Matcher +func (dm *deadlineMatcher) String() string { +	return fmt.Sprintf("deadlineMatcher{}") +} diff --git a/pkg/instance/instance.go b/pkg/instance/instance.go new file mode 100644 index 0000000..3441a0a --- /dev/null +++ b/pkg/instance/instance.go @@ -0,0 +1,90 @@ +package stfe + +import ( +	"context" +	"crypto" +	"fmt" +	"net/http" +	"time" + +	"github.com/golang/glog" +	"github.com/system-transparency/stfe/pkg/state" +	"github.com/system-transparency/stfe/pkg/trillian" +	"github.com/system-transparency/stfe/pkg/types" +) + +// Config is a collection of log parameters +type Config struct { +	LogID    string        // H(public key), then hex-encoded +	TreeID   int64         // Merkle tree identifier used by Trillian +	Prefix   string        // The portion between base URL and st/v0 (may be "") +	MaxRange int64         // Maximum number of leaves per get-leaves request +	Deadline time.Duration // Deadline used for gRPC requests +	Interval time.Duration // Cosigning frequency + +	// Witnesses map trusted witness identifiers to public verification keys +	Witnesses map[[types.HashSize]byte][types.VerificationKeySize]byte +} + +// Instance is an instance of the log's front-end +type Instance struct { +	Config                      // configuration parameters +	Client   trillian.Client    // provides access to the Trillian backend +	Signer   crypto.Signer      // provides access to Ed25519 private key +	Stateman state.StateManager // coordinates access to (co)signed tree heads +} + +// Handler implements the http.Handler interface, and contains a reference +// to an STFE server instance as well as a function that uses it. +type Handler struct { +	Instance *Instance +	Endpoint types.Endpoint +	Method   string +	Handler  func(context.Context, *Instance, http.ResponseWriter, *http.Request) (int, error) +} + +// Handlers returns a list of STFE handlers +func (i *Instance) Handlers() []Handler { +	return []Handler{ +		Handler{Instance: i, Handler: addLeaf, Endpoint: types.EndpointAddLeaf, Method: http.MethodPost}, +		Handler{Instance: i, Handler: addCosignature, Endpoint: types.EndpointAddCosignature, Method: http.MethodPost}, +		Handler{Instance: i, Handler: getTreeHeadLatest, Endpoint: types.EndpointGetTreeHeadLatest, Method: http.MethodGet}, +		Handler{Instance: i, Handler: getTreeHeadToSign, Endpoint: types.EndpointGetTreeHeadToSign, Method: http.MethodGet}, +		Handler{Instance: i, Handler: getTreeHeadCosigned, Endpoint: types.EndpointGetTreeHeadCosigned, Method: http.MethodGet}, +		Handler{Instance: i, Handler: getConsistencyProof, Endpoint: types.EndpointGetConsistencyProof, Method: http.MethodPost}, +		Handler{Instance: i, Handler: getInclusionProof, Endpoint: types.EndpointGetProofByHash, Method: http.MethodPost}, +		Handler{Instance: i, Handler: getLeaves, Endpoint: types.EndpointGetLeaves, Method: http.MethodPost}, +	} +} + +// Path returns a path that should be configured for this handler +func (h Handler) Path() string { +	return h.Endpoint.Path(h.Instance.Prefix, "st", "v0") +} + +// ServeHTTP is part of the http.Handler interface +func (a Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { +	// export prometheus metrics +	var now time.Time = time.Now() +	var statusCode int +	defer func() { +		rspcnt.Inc(a.Instance.LogID, string(a.Endpoint), fmt.Sprintf("%d", statusCode)) +		latency.Observe(time.Now().Sub(now).Seconds(), a.Instance.LogID, string(a.Endpoint), fmt.Sprintf("%d", statusCode)) +	}() +	reqcnt.Inc(a.Instance.LogID, string(a.Endpoint)) + +	ctx, cancel := context.WithDeadline(r.Context(), now.Add(a.Instance.Deadline)) +	defer cancel() + +	if r.Method != a.Method { +		glog.Warningf("%s/%s: got HTTP %s, wanted HTTP %s", a.Instance.Prefix, string(a.Endpoint), r.Method, a.Method) +		http.Error(w, "", http.StatusMethodNotAllowed) +		return +	} + +	statusCode, err := a.Handler(ctx, a.Instance, w, r) +	if err != nil { +		glog.Warningf("handler error %s/%s: %v", a.Instance.Prefix, a.Endpoint, err) +		http.Error(w, fmt.Sprintf("%s%s%s%s", "Error", types.Delim, err.Error(), types.EOL), statusCode) +	} +} diff --git a/pkg/instance/instance_test.go b/pkg/instance/instance_test.go new file mode 100644 index 0000000..a7a3d8a --- /dev/null +++ b/pkg/instance/instance_test.go @@ -0,0 +1,158 @@ +package stfe + +import ( +	"crypto" +	"net/http" +	"net/http/httptest" +	"testing" + +	"github.com/golang/mock/gomock" +	"github.com/google/certificate-transparency-go/trillian/mockclient" +	"github.com/system-transparency/stfe/pkg/testdata" +	"github.com/system-transparency/stfe/pkg/types" +) + +type testInstance struct { +	ctrl     *gomock.Controller +	client   *mockclient.MockTrillianLogClient +	instance *Instance +} + +// newTestInstances sets up a test instance that uses default log parameters +// with an optional signer, see newLogParameters() for further details.  The +// SthSource is instantiated with an ActiveSthSource that has (i) the default +// STH as the currently cosigned STH based on testdata.Ed25519VkWitness, and +// (ii) the default STH without any cosignatures as the currently stable STH. +func newTestInstance(t *testing.T, signer crypto.Signer) *testInstance { +	t.Helper() +	ctrl := gomock.NewController(t) +	client := mockclient.NewMockTrillianLogClient(ctrl) +	return &testInstance{ +		ctrl:   ctrl, +		client: client, +		instance: &Instance{ +			Client:        client, +			LogParameters: newLogParameters(t, signer), +			SthSource: &ActiveSthSource{ +				client:          client, +				logParameters:   newLogParameters(t, signer), +				currCosth:       testdata.DefaultCosth(t, testdata.Ed25519VkLog, [][32]byte{testdata.Ed25519VkWitness}), +				nextCosth:       testdata.DefaultCosth(t, testdata.Ed25519VkLog, nil), +				cosignatureFrom: make(map[[types.NamespaceFingerprintSize]byte]bool), +			}, +		}, +	} +} + +// getHandlers returns all endpoints that use HTTP GET as a map to handlers +func (ti *testInstance) getHandlers(t *testing.T) map[Endpoint]Handler { +	t.Helper() +	return map[Endpoint]Handler{ +		EndpointGetLatestSth:   Handler{Instance: ti.instance, Handler: getLatestSth, Endpoint: EndpointGetLatestSth, Method: http.MethodGet}, +		EndpointGetStableSth:   Handler{Instance: ti.instance, Handler: getStableSth, Endpoint: EndpointGetStableSth, Method: http.MethodGet}, +		EndpointGetCosignedSth: Handler{Instance: ti.instance, Handler: getCosignedSth, Endpoint: EndpointGetCosignedSth, Method: http.MethodGet}, +	} +} + +// postHandlers returns all endpoints that use HTTP POST as a map to handlers +func (ti *testInstance) postHandlers(t *testing.T) map[Endpoint]Handler { +	t.Helper() +	return map[Endpoint]Handler{ +		EndpointAddEntry:            Handler{Instance: ti.instance, Handler: addEntry, Endpoint: EndpointAddEntry, Method: http.MethodPost}, +		EndpointAddCosignature:      Handler{Instance: ti.instance, Handler: addCosignature, Endpoint: EndpointAddCosignature, Method: http.MethodPost}, +		EndpointGetConsistencyProof: Handler{Instance: ti.instance, Handler: getConsistencyProof, Endpoint: EndpointGetConsistencyProof, Method: http.MethodPost}, +		EndpointGetProofByHash:      Handler{Instance: ti.instance, Handler: getProofByHash, Endpoint: EndpointGetProofByHash, Method: http.MethodPost}, +		EndpointGetEntries:          Handler{Instance: ti.instance, Handler: getEntries, Endpoint: EndpointGetEntries, Method: http.MethodPost}, +	} +} + +// getHandler must return a particular HTTP GET handler +func (ti *testInstance) getHandler(t *testing.T, endpoint Endpoint) Handler { +	t.Helper() +	handler, ok := ti.getHandlers(t)[endpoint] +	if !ok { +		t.Fatalf("must return HTTP GET handler for endpoint: %s", endpoint) +	} +	return handler +} + +// postHandler must return a particular HTTP POST handler +func (ti *testInstance) postHandler(t *testing.T, endpoint Endpoint) Handler { +	t.Helper() +	handler, ok := ti.postHandlers(t)[endpoint] +	if !ok { +		t.Fatalf("must return HTTP POST handler for endpoint: %s", endpoint) +	} +	return handler +} + +// TestHandlers checks that we configured all endpoints and that there are no +// unexpected ones. +func TestHandlers(t *testing.T) { +	endpoints := map[Endpoint]bool{ +		EndpointAddEntry:            false, +		EndpointAddCosignature:      false, +		EndpointGetLatestSth:        false, +		EndpointGetStableSth:        false, +		EndpointGetCosignedSth:      false, +		EndpointGetConsistencyProof: false, +		EndpointGetProofByHash:      false, +		EndpointGetEntries:          false, +	} +	i := &Instance{nil, newLogParameters(t, nil), nil} +	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) +		} +	} +} + +// TestGetHandlersRejectPost checks that all get handlers reject post requests +func TestGetHandlersRejectPost(t *testing.T) { +	ti := newTestInstance(t, nil) +	defer ti.ctrl.Finish() + +	for endpoint, handler := range ti.getHandlers(t) { +		t.Run(string(endpoint), func(t *testing.T) { +			s := httptest.NewServer(handler) +			defer s.Close() + +			url := endpoint.Path(s.URL, ti.instance.LogParameters.Prefix) +			if rsp, err := http.Post(url, "application/json", nil); err != nil { +				t.Fatalf("http.Post(%s)=(_,%q), want (_,nil)", url, err) +			} else if rsp.StatusCode != http.StatusMethodNotAllowed { +				t.Errorf("http.Post(%s)=(%d,nil), want (%d, nil)", url, rsp.StatusCode, http.StatusMethodNotAllowed) +			} +		}) +	} +} + +// TestPostHandlersRejectGet checks that all post handlers reject get requests +func TestPostHandlersRejectGet(t *testing.T) { +	ti := newTestInstance(t, nil) +	defer ti.ctrl.Finish() + +	for endpoint, handler := range ti.postHandlers(t) { +		t.Run(string(endpoint), func(t *testing.T) { +			s := httptest.NewServer(handler) +			defer s.Close() + +			url := endpoint.Path(s.URL, ti.instance.LogParameters.Prefix) +			if rsp, err := http.Get(url); err != nil { +				t.Fatalf("http.Get(%s)=(_,%q), want (_,nil)", url, err) +			} else if rsp.StatusCode != http.StatusMethodNotAllowed { +				t.Errorf("http.Get(%s)=(%d,nil), want (%d, nil)", url, rsp.StatusCode, http.StatusMethodNotAllowed) +			} +		}) +	} +} + +// TODO: TestHandlerPath +func TestHandlerPath(t *testing.T) { +} diff --git a/pkg/instance/metric.go b/pkg/instance/metric.go new file mode 100644 index 0000000..7e3e8b2 --- /dev/null +++ b/pkg/instance/metric.go @@ -0,0 +1,23 @@ +package stfe + +import ( +	"github.com/google/trillian/monitoring" +	"github.com/google/trillian/monitoring/prometheus" +) + +var ( +	reqcnt           monitoring.Counter   // number of incoming http requests +	rspcnt           monitoring.Counter   // number of valid http responses +	latency          monitoring.Histogram // request-response latency +	lastSthTimestamp monitoring.Gauge     // unix timestamp from the most recent sth +	lastSthSize      monitoring.Gauge     // tree size of most recent sth +) + +func init() { +	mf := prometheus.MetricFactory{} +	reqcnt = mf.NewCounter("http_req", "number of http requests", "logid", "endpoint") +	rspcnt = mf.NewCounter("http_rsp", "number of http requests", "logid", "endpoint", "status") +	latency = mf.NewHistogram("http_latency", "http request-response latency", "logid", "endpoint", "status") +	lastSthTimestamp = mf.NewGauge("last_sth_timestamp", "unix timestamp while handling the most recent sth", "logid") +	lastSthSize = mf.NewGauge("last_sth_size", "most recent sth tree size", "logid") +} diff --git a/pkg/instance/request.go b/pkg/instance/request.go new file mode 100644 index 0000000..7475b26 --- /dev/null +++ b/pkg/instance/request.go @@ -0,0 +1,77 @@ +package stfe + +import ( +	"crypto/ed25519" +	"fmt" +	"net/http" + +	"github.com/system-transparency/stfe/pkg/types" +) + +func (i *Instance) leafRequestFromHTTP(r *http.Request) (*types.LeafRequest, error) { +	var req types.LeafRequest +	if err := req.UnmarshalASCII(r.Body); err != nil { +		return nil, fmt.Errorf("UnmarshalASCII: %v", err) +	} + +	vk := ed25519.PublicKey(req.VerificationKey[:]) +	msg := req.Message.Marshal() +	sig := req.Signature[:] +	if !ed25519.Verify(vk, msg, sig) { +		return nil, fmt.Errorf("invalid signature") +	} +	// TODO: check shard hint +	// TODO: check domain hint +	return &req, nil +} + +func (i *Instance) cosignatureRequestFromHTTP(r *http.Request) (*types.CosignatureRequest, error) { +	var req types.CosignatureRequest +	if err := req.UnmarshalASCII(r.Body); err != nil { +		return nil, fmt.Errorf("unpackOctetPost: %v", err) +	} +	if _, ok := i.Witnesses[*req.KeyHash]; !ok { +		return nil, fmt.Errorf("Unknown witness: %x", req.KeyHash) +	} +	return &req, nil +} + +func (i *Instance) consistencyProofRequestFromHTTP(r *http.Request) (*types.ConsistencyProofRequest, error) { +	var req types.ConsistencyProofRequest +	if err := req.UnmarshalASCII(r.Body); err != nil { +		return nil, fmt.Errorf("UnmarshalASCII: %v", err) +	} +	if req.OldSize < 1 { +		return nil, fmt.Errorf("OldSize(%d) must be larger than zero", req.OldSize) +	} +	if req.NewSize <= req.OldSize { +		return nil, fmt.Errorf("NewSize(%d) must be larger than OldSize(%d)", req.NewSize, req.OldSize) +	} +	return &req, nil +} + +func (i *Instance) inclusionProofRequestFromHTTP(r *http.Request) (*types.InclusionProofRequest, error) { +	var req types.InclusionProofRequest +	if err := req.UnmarshalASCII(r.Body); err != nil { +		return nil, fmt.Errorf("UnmarshalASCII: %v", err) +	} +	if req.TreeSize < 1 { +		return nil, fmt.Errorf("TreeSize(%d) must be larger than zero", req.TreeSize) +	} +	return &req, nil +} + +func (i *Instance) leavesRequestFromHTTP(r *http.Request) (*types.LeavesRequest, error) { +	var req types.LeavesRequest +	if err := req.UnmarshalASCII(r.Body); err != nil { +		return nil, fmt.Errorf("UnmarshalASCII: %v", err) +	} + +	if req.StartSize > req.EndSize { +		return nil, fmt.Errorf("StartSize(%d) must be less than or equal to EndSize(%d)", req.StartSize, req.EndSize) +	} +	if req.EndSize-req.StartSize+1 > uint64(i.MaxRange) { +		req.EndSize = req.StartSize + uint64(i.MaxRange) - 1 +	} +	return &req, nil +} diff --git a/pkg/instance/request_test.go b/pkg/instance/request_test.go new file mode 100644 index 0000000..0a5a908 --- /dev/null +++ b/pkg/instance/request_test.go @@ -0,0 +1,318 @@ +package stfe + +import ( +	"bytes" +	//"fmt" +	"reflect" +	"testing" +	//"testing/iotest" + +	"net/http" + +	"github.com/system-transparency/stfe/pkg/testdata" +	"github.com/system-transparency/stfe/pkg/types" +) + +func TestParseAddEntryV1Request(t *testing.T) { +	lp := newLogParameters(t, nil) +	for _, table := range []struct { +		description string +		breq        *bytes.Buffer +		wantErr     bool +	}{ +		{ +			description: "invalid: nothing to unpack", +			breq:        bytes.NewBuffer(nil), +			wantErr:     true, +		}, +		{ +			description: "invalid: not a signed checksum entry", +			breq:        testdata.AddCosignatureBuffer(t, testdata.DefaultSth(t, testdata.Ed25519VkLog), &testdata.Ed25519SkWitness, &testdata.Ed25519VkWitness), +			wantErr:     true, +		}, +		{ +			description: "invalid: untrusted submitter", // only testdata.Ed25519VkSubmitter is registered by default in newLogParameters() + +			breq:    testdata.AddSignedChecksumBuffer(t, testdata.Ed25519SkSubmitter2, testdata.Ed25519VkSubmitter2), +			wantErr: true, +		}, +		{ +			description: "invalid: signature does not cover message", + +			breq:    testdata.AddSignedChecksumBuffer(t, testdata.Ed25519SkSubmitter2, testdata.Ed25519VkSubmitter), +			wantErr: true, +		}, +		{ +			description: "valid", +			breq:        testdata.AddSignedChecksumBuffer(t, testdata.Ed25519SkSubmitter, testdata.Ed25519VkSubmitter), +		}, // TODO: add test case that disables submitter policy (i.e., unregistered namespaces are accepted) +	} { +		url := EndpointAddEntry.Path("http://example.com", lp.Prefix) +		req, err := http.NewRequest("POST", url, table.breq) +		if err != nil { +			t.Fatalf("failed creating http request: %v", err) +		} +		req.Header.Set("Content-Type", "application/octet-stream") + +		_, err = lp.parseAddEntryV1Request(req) +		if got, want := err != nil, table.wantErr; got != want { +			t.Errorf("got errror %v but wanted %v in test %q: %v", got, want, table.description, err) +		} +	} +} + +func TestParseAddCosignatureV1Request(t *testing.T) { +	lp := newLogParameters(t, nil) +	for _, table := range []struct { +		description string +		breq        *bytes.Buffer +		wantErr     bool +	}{ +		{ +			description: "invalid: nothing to unpack", +			breq:        bytes.NewBuffer(nil), +			wantErr:     true, +		}, +		{ +			description: "invalid: not a cosigned sth", +			breq:        testdata.AddSignedChecksumBuffer(t, testdata.Ed25519SkSubmitter, testdata.Ed25519VkSubmitter), +			wantErr:     true, +		}, +		{ +			description: "invalid: no cosignature", +			breq:        testdata.AddCosignatureBuffer(t, testdata.DefaultSth(t, testdata.Ed25519VkLog), &testdata.Ed25519SkWitness, nil), +			wantErr:     true, +		}, +		{ +			description: "invalid: untrusted witness", // only testdata.Ed25519VkWitness is registered by default in newLogParameters() +			breq:        testdata.AddCosignatureBuffer(t, testdata.DefaultSth(t, testdata.Ed25519VkLog), &testdata.Ed25519SkWitness2, &testdata.Ed25519VkWitness2), +			wantErr:     true, +		}, +		{ +			description: "invalid: signature does not cover message", +			breq:        testdata.AddCosignatureBuffer(t, testdata.DefaultSth(t, testdata.Ed25519VkLog), &testdata.Ed25519SkWitness2, &testdata.Ed25519VkWitness), +			wantErr:     true, +		}, +		{ +			description: "valid", +			breq:        testdata.AddCosignatureBuffer(t, testdata.DefaultSth(t, testdata.Ed25519VkLog), &testdata.Ed25519SkWitness, &testdata.Ed25519VkWitness), +		}, // TODO: add test case that disables witness policy (i.e., unregistered namespaces are accepted) +	} { +		url := EndpointAddCosignature.Path("http://example.com", lp.Prefix) +		req, err := http.NewRequest("POST", url, table.breq) +		if err != nil { +			t.Fatalf("failed creating http request: %v", err) +		} +		req.Header.Set("Content-Type", "application/octet-stream") + +		_, err = lp.parseAddCosignatureV1Request(req) +		if got, want := err != nil, table.wantErr; got != want { +			t.Errorf("got errror %v but wanted %v in test %q: %v", got, want, table.description, err) +		} +	} +} + +func TestNewGetConsistencyProofRequest(t *testing.T) { +	lp := newLogParameters(t, nil) +	for _, table := range []struct { +		description string +		req         *types.GetConsistencyProofV1 +		wantErr     bool +	}{ +		{ +			description: "invalid: nothing to unpack", +			req:         nil, +			wantErr:     true, +		}, +		{ +			description: "invalid: first must be larger than zero", +			req:         &types.GetConsistencyProofV1{First: 0, Second: 0}, +			wantErr:     true, +		}, +		{ +			description: "invalid: second must be larger than first", +			req:         &types.GetConsistencyProofV1{First: 2, Second: 1}, +			wantErr:     true, +		}, +		{ +			description: "valid", +			req:         &types.GetConsistencyProofV1{First: 1, Second: 2}, +		}, +	} { +		var buf *bytes.Buffer +		if table.req == nil { +			buf = bytes.NewBuffer(nil) +		} else { +			buf = bytes.NewBuffer(marshal(t, *table.req)) +		} + +		url := EndpointGetConsistencyProof.Path("http://example.com", lp.Prefix) +		req, err := http.NewRequest("POST", url, buf) +		if err != nil { +			t.Fatalf("failed creating http request: %v", err) +		} +		req.Header.Set("Content-Type", "application/octet-stream") + +		_, err = lp.parseGetConsistencyProofV1Request(req) +		if got, want := err != nil, table.wantErr; got != want { +			t.Errorf("got errror %v but wanted %v in test %q: %v", got, want, table.description, err) +		} +	} +} + +func TestNewGetProofByHashRequest(t *testing.T) { +	lp := newLogParameters(t, nil) +	for _, table := range []struct { +		description string +		req         *types.GetProofByHashV1 +		wantErr     bool +	}{ +		{ +			description: "invalid: nothing to unpack", +			req:         nil, +			wantErr:     true, +		}, +		{ +			description: "invalid: no entry in an empty tree", +			req:         &types.GetProofByHashV1{TreeSize: 0, Hash: testdata.LeafHash}, +			wantErr:     true, +		}, +		{ +			description: "valid", +			req:         &types.GetProofByHashV1{TreeSize: 1, Hash: testdata.LeafHash}, +		}, +	} { +		var buf *bytes.Buffer +		if table.req == nil { +			buf = bytes.NewBuffer(nil) +		} else { +			buf = bytes.NewBuffer(marshal(t, *table.req)) +		} + +		url := EndpointGetProofByHash.Path("http://example.com", lp.Prefix) +		req, err := http.NewRequest("POST", url, buf) +		if err != nil { +			t.Fatalf("failed creating http request: %v", err) +		} +		req.Header.Set("Content-Type", "application/octet-stream") + +		_, err = lp.parseGetProofByHashV1Request(req) +		if got, want := err != nil, table.wantErr; got != want { +			t.Errorf("got errror %v but wanted %v in test %q: %v", got, want, table.description, err) +		} +	} +} + +func TestParseGetEntriesV1Request(t *testing.T) { +	lp := newLogParameters(t, nil) +	for _, table := range []struct { +		description string +		req         *types.GetEntriesV1 +		wantErr     bool +		wantReq     *types.GetEntriesV1 +	}{ +		{ +			description: "invalid: nothing to unpack", +			req:         nil, +			wantErr:     true, +		}, +		{ +			description: "invalid: start must be larger than end", +			req:         &types.GetEntriesV1{Start: 1, End: 0}, +			wantErr:     true, +		}, +		{ +			description: "valid: want truncated range", +			req:         &types.GetEntriesV1{Start: 0, End: uint64(testdata.MaxRange)}, +			wantReq:     &types.GetEntriesV1{Start: 0, End: uint64(testdata.MaxRange) - 1}, +		}, +		{ +			description: "valid", +			req:         &types.GetEntriesV1{Start: 0, End: 0}, +			wantReq:     &types.GetEntriesV1{Start: 0, End: 0}, +		}, +	} { +		var buf *bytes.Buffer +		if table.req == nil { +			buf = bytes.NewBuffer(nil) +		} else { +			buf = bytes.NewBuffer(marshal(t, *table.req)) +		} + +		url := EndpointGetEntries.Path("http://example.com", lp.Prefix) +		req, err := http.NewRequest("POST", url, buf) +		if err != nil { +			t.Fatalf("failed creating http request: %v", err) +		} +		req.Header.Set("Content-Type", "application/octet-stream") + +		output, err := lp.parseGetEntriesV1Request(req) +		if got, want := err != nil, table.wantErr; got != want { +			t.Errorf("got errror %v but wanted %v in test %q: %v", got, want, table.description, err) +		} +		if err != nil { +			continue +		} +		if got, want := output, table.wantReq; !reflect.DeepEqual(got, want) { +			t.Errorf("got request\n%v\n\tbut wanted\n%v\n\t in test %q", got, want, table.description) +		} +	} +} + +func TestUnpackOctetPost(t *testing.T) { +	for _, table := range []struct { +		description string +		req         *http.Request +		out         interface{} +		wantErr     bool +	}{ +		//{ +		//	description: "invalid: cannot read request body", +		//	req: func() *http.Request { +		//		req, err := http.NewRequest(http.MethodPost, "", iotest.ErrReader(fmt.Errorf("bad reader"))) +		//		if err != nil { +		//			t.Fatalf("must make new http request: %v", err) +		//		} +		//		return req +		//	}(), +		//	out:     &types.StItem{}, +		//	wantErr: true, +		//}, // testcase requires Go 1.16 +		{ +			description: "invalid: cannot unmarshal", +			req: func() *http.Request { +				req, err := http.NewRequest(http.MethodPost, "", bytes.NewBuffer(nil)) +				if err != nil { +					t.Fatalf("must make new http request: %v", err) +				} +				return req +			}(), +			out:     &types.StItem{}, +			wantErr: true, +		}, +		{ +			description: "valid", +			req: func() *http.Request { +				req, err := http.NewRequest(http.MethodPost, "", bytes.NewBuffer([]byte{0})) +				if err != nil { +					t.Fatalf("must make new http request: %v", err) +				} +				return req +			}(), +			out: &struct{ SomeUint8 uint8 }{}, +		}, +	} { +		err := unpackOctetPost(table.req, table.out) +		if got, want := err != nil, table.wantErr; got != want { +			t.Errorf("got error %v but wanted %v in test %q", got, want, table.description) +		} +	} +} + +func marshal(t *testing.T, out interface{}) []byte { +	b, err := types.Marshal(out) +	if err != nil { +		t.Fatalf("must marshal: %v", err) +	} +	return b +} | 
