diff options
69 files changed, 4886 insertions, 5753 deletions
| diff --git a/client/client.go b/client/client.go deleted file mode 100644 index ba81f4d..0000000 --- a/client/client.go +++ /dev/null @@ -1,242 +0,0 @@ -package client - -import ( -	"bytes" -	"context" -	"crypto" -	"fmt" - -	"io/ioutil" -	"net/http" - -	"github.com/golang/glog" -	"github.com/google/trillian/merkle/rfc6962" -	"github.com/system-transparency/stfe" -	"github.com/system-transparency/stfe/types" -	"golang.org/x/net/context/ctxhttp" -) - -// Descriptor is a log descriptor -type Descriptor struct { -	Namespace *types.Namespace // log identifier is a namespace -	Url       string           // log url, e.g., http://example.com/st/v1 -} - -// Client is a log client -type Client struct { -	HttpClient *http.Client -	Signer     crypto.Signer    // client's private identity -	Namespace  *types.Namespace // client's public identity -	Log        *Descriptor      // log's public identity -} - -// GetLatestSth fetches and verifies the signature of the most recent STH. -// Outputs the resulting STH. -func (c *Client) GetLatestSth(ctx context.Context) (*types.StItem, error) { -	url := stfe.EndpointGetLatestSth.Path(c.Log.Url) -	req, err := http.NewRequest("GET", url, nil) -	if err != nil { -		return nil, fmt.Errorf("failed creating http request: %v", err) -	} -	glog.V(3).Infof("created http request: %s %s", req.Method, req.URL) - -	item, err := c.doRequestWithStItemResponse(ctx, req) -	if err != nil { -		return nil, err -	} -	if got, want := item.Format, types.StFormatSignedTreeHeadV1; got != want { -		return nil, fmt.Errorf("unexpected StItem format: %v", got) -	} -	if err := VerifySignedTreeHeadV1(c.Log.Namespace, item); err != nil { -		return nil, fmt.Errorf("signature verification failed: %v", err) -	} -	glog.V(3).Infof("verified sth") -	return item, nil -} - -// GetProofByHash fetches and verifies an inclusion proof for a leaf hash -// against an STH.  Outputs the resulting proof. -func (c *Client) GetProofByHash(ctx context.Context, leafHash []byte, sth *types.StItem) (*types.StItem, error) { -	if err := VerifySignedTreeHeadV1(c.Log.Namespace, sth); err != nil { -		return nil, fmt.Errorf("invalid sth: %v", err) -	} -	glog.V(3).Infof("verified sth") -	params := types.GetProofByHashV1{ -		TreeSize: sth.SignedTreeHeadV1.TreeHead.TreeSize, -	} -	copy(params.Hash[:], leafHash) -	buf, err := types.Marshal(params) -	if err != nil { -		return nil, fmt.Errorf("req: Marshal: %v", err) -	} - -	url := stfe.EndpointGetProofByHash.Path(c.Log.Url) -	req, err := http.NewRequest("POST", url, bytes.NewBuffer(buf)) -	if err != nil { -		return nil, fmt.Errorf("failed creating http request: %v", err) -	} -	req.Header.Set("Content-Type", "application/octet-stream") -	glog.V(3).Infof("created http request: %s %s", req.Method, req.URL) - -	item, err := c.doRequestWithStItemResponse(ctx, req) -	if err != nil { -		return nil, fmt.Errorf("doRequestWithStItemResponse: %v", err) -	} -	if got, want := item.Format, types.StFormatInclusionProofV1; got != want { -		return nil, fmt.Errorf("unexpected StItem format: %v", item.Format) -	} -	if err := VerifyInclusionProofV1(item, sth, params.Hash[:]); err != nil { -		return nil, fmt.Errorf("invalid inclusion proof: %v", err) -	} -	glog.V(3).Infof("verified inclusion proof") -	return item, nil -} - -// GetConsistencyProof fetches and verifies a consistency proof betweeen two -// STHs.  Outputs the resulting proof. -func (c *Client) GetConsistencyProof(ctx context.Context, sth1, sth2 *types.StItem) (*types.StItem, error) { -	if err := VerifySignedTreeHeadV1(c.Log.Namespace, sth1); err != nil { -		return nil, fmt.Errorf("invalid first sth: %v", err) -	} -	if err := VerifySignedTreeHeadV1(c.Log.Namespace, sth2); err != nil { -		return nil, fmt.Errorf("invalid second sth: %v", err) -	} -	glog.V(3).Infof("verified sths") -	buf, err := types.Marshal(types.GetConsistencyProofV1{ -		First:  sth1.SignedTreeHeadV1.TreeHead.TreeSize, -		Second: sth2.SignedTreeHeadV1.TreeHead.TreeSize, -	}) -	if err != nil { -		return nil, fmt.Errorf("req: Marshal: %v", err) -	} - -	url := stfe.EndpointGetConsistencyProof.Path(c.Log.Url) -	req, err := http.NewRequest("POST", url, bytes.NewBuffer(buf)) -	if err != nil { -		return nil, fmt.Errorf("failed creating http request: %v", err) -	} -	req.Header.Set("Content-Type", "application/octet-stream") -	glog.V(3).Infof("created http request: %s %s", req.Method, req.URL) - -	item, err := c.doRequestWithStItemResponse(ctx, req) -	if err != nil { -		return nil, fmt.Errorf("doRequestWithStItemResponse: %v", err) -	} -	if got, want := item.Format, types.StFormatConsistencyProofV1; got != want { -		return nil, fmt.Errorf("unexpected StItem format: %v", item.Format) -	} -	if err := VerifyConsistencyProofV1(item, sth1, sth2); err != nil { -		return nil, fmt.Errorf("invalid inclusion proof: %v", err) -	} -	glog.V(3).Infof("verified inclusion proof") -	return item, nil -} - -// AddEntry signs and submits a checksum_v1 entry to the log.  Outputs the -// resulting leaf-hash on success. -func (c *Client) AddEntry(ctx context.Context, data *types.ChecksumV1) ([]byte, error) { -	msg, err := types.Marshal(*data) -	if err != nil { -		return nil, fmt.Errorf("failed marshaling ChecksumV1: %v", err) -	} -	sig, err := c.Signer.Sign(nil, msg, crypto.Hash(0)) -	if err != nil { -		return nil, fmt.Errorf("failed signing ChecksumV1: %v", err) -	} -	leaf, err := types.Marshal(*types.NewSignedChecksumV1(data, &types.SignatureV1{ -		Namespace: *c.Namespace, -		Signature: sig, -	})) -	if err != nil { -		return nil, fmt.Errorf("failed marshaling SignedChecksumV1: %v", err) -	} -	glog.V(3).Infof("signed checksum entry for identifier %q", string(data.Identifier)) - -	url := stfe.EndpointAddEntry.Path(c.Log.Url) -	req, err := http.NewRequest("POST", url, bytes.NewBuffer(leaf)) -	if err != nil { -		return nil, fmt.Errorf("failed creating http request: %v", err) -	} -	req.Header.Set("Content-Type", "application/octet-stream") -	glog.V(3).Infof("created http request: %s %s", req.Method, req.URL) - -	if rsp, err := c.doRequest(ctx, req); err != nil { -		return nil, fmt.Errorf("doRequest: %v", err) -	} else if len(rsp) != 0 { -		return nil, fmt.Errorf("extra data: %v", err) -	} -	glog.V(3).Infof("add-entry succeded") -	return rfc6962.DefaultHasher.HashLeaf(leaf), nil -} - -// GetEntries fetches a range of entries from the log, verifying that they are -// of type signed_checksum_v1 but nothing more than that.  Outputs the resulting -// range that may be truncated by the log if [start,end] is too large. -func (c *Client) GetEntries(ctx context.Context, start, end uint64) ([]*types.StItem, error) { -	buf, err := types.Marshal(types.GetEntriesV1{ -		Start: start, -		End:   end, -	}) -	if err != nil { -		return nil, fmt.Errorf("Marshal: %v", err) -	} -	url := stfe.EndpointGetEntries.Path(c.Log.Url) -	req, err := http.NewRequest("POST", url, bytes.NewBuffer(buf)) -	if err != nil { -		return nil, fmt.Errorf("failed creating http request: %v", err) -	} -	req.Header.Set("Content-Type", "application/octet-stream") -	glog.V(3).Infof("created http request: %s %s", req.Method, req.URL) -	glog.V(3).Infof("request data: start(%d), end(%d)", start, end) - -	body, err := c.doRequest(ctx, req) -	if err != nil { -		return nil, fmt.Errorf("doRequest: %v", err) -	} -	var list types.StItemList -	if err := types.Unmarshal(body, &list); err != nil { -		return nil, fmt.Errorf("Unmarshal: %v", err) -	} -	ret := make([]*types.StItem, 0, len(list.Items)) -	for i, _ := range list.Items { -		item := list.Items[i] -		if got, want := item.Format, types.StFormatSignedChecksumV1; got != want { -			return nil, fmt.Errorf("unexpected StItem format: %v", got) -		} -		ret = append(ret, &item) -	} -	return ret, nil -} - -// doRequest sends an HTTP request and outputs the raw body -func (c *Client) doRequest(ctx context.Context, req *http.Request) ([]byte, error) { -	rsp, err := ctxhttp.Do(ctx, c.HttpClient, req) -	if err != nil { -		return nil, fmt.Errorf("no response: %v", err) -	} -	defer rsp.Body.Close() -	if got, want := rsp.StatusCode, http.StatusOK; got != want { -		return nil, fmt.Errorf("bad http status: %v", got) -	} -	body, err := ioutil.ReadAll(rsp.Body) -	if err != nil { -		return nil, fmt.Errorf("cannot read body: %v", err) -	} -	return body, nil -} - -// -// doRequestWithStItemResponse sends an HTTP request and returns a decoded -// StItem that the resulting HTTP response contained json:ed and marshaled -func (c *Client) doRequestWithStItemResponse(ctx context.Context, req *http.Request) (*types.StItem, error) { -	body, err := c.doRequest(ctx, req) -	if err != nil { -		return nil, err -	} -	var item types.StItem -	if err := types.Unmarshal(body, &item); err != nil { -		return nil, fmt.Errorf("failed decoding StItem: %v", err) -	} -	glog.V(9).Infof("got StItem: %v", item) -	return &item, nil -} diff --git a/client/cmd/add-entry/main.go b/client/cmd/add-entry/main.go deleted file mode 100644 index a29d01f..0000000 --- a/client/cmd/add-entry/main.go +++ /dev/null @@ -1,52 +0,0 @@ -package main - -import ( -	"context" -	"flag" -	"fmt" - -	"encoding/base64" - -	"github.com/golang/glog" -	"github.com/system-transparency/stfe/client" -	"github.com/system-transparency/stfe/types" -) - -var ( -	identifier = flag.String("identifier", "", "checksum identifier") -	checksum   = flag.String("checksum", "", "base64-encoded checksum") -) - -func main() { -	flag.Parse() -	defer glog.Flush() - -	client, err := client.NewClientFromFlags() -	if err != nil { -		glog.Errorf("NewClientFromFlags: %v", err) -		return -	} -	data, err := NewChecksumV1FromFlags() -	if err != nil { -		glog.Errorf("NewChecksumV1FromFlags: %v", err) -		return -	} -	leafHash, err := client.AddEntry(context.Background(), data) -	if err != nil { -		glog.Errorf("AddEntry: %v", err) -		return -	} -	fmt.Println("leaf hash:", base64.StdEncoding.EncodeToString(leafHash)) -} - -func NewChecksumV1FromFlags() (*types.ChecksumV1, error) { -	var err error -	data := types.ChecksumV1{ -		Identifier: []byte(*identifier), -	} -	data.Checksum, err = base64.StdEncoding.DecodeString(*checksum) -	if err != nil { -		return nil, fmt.Errorf("entry_checksum: DecodeString: %v", err) -	} -	return &data, nil -} diff --git a/client/cmd/example.sh b/client/cmd/example.sh deleted file mode 100755 index d790712..0000000 --- a/client/cmd/example.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash -set -eu - -log_url=http://tlog-poc.system-transparency.org:4780/st/v1 -log_id=AAG+ZW+UesWdMFytUGkp28csBcziomSB3U2vvkAW55MVZQ== -tmpdir=$(mktemp -dt stfe.XXXXXXXX) -cp $0 $tmpdir/ -cd $tmpdir - -commonargs="--log_id $log_id --log_url $log_url" # --logtostderr -v 3 -pause="sleep 1" - -echo "arguments used:" -echo $commonargs -echo "" - -echo "fetching sth..." -get-sth $commonargs | tee sth1.output -echo "" && $pause - -echo "adding an entry..." -add-entry $commonargs \ -	--identifier "example.sh v0.0.1-$(cat /dev/urandom | base64 | head -c 10)" \ -	--checksum $(sha256sum "$0") | tee add-entry.output -echo "" && $pause - -echo "fetching another sth..." -get-sth $commonargs | tee sth2.output -echo "" && $pause - -echo "verifying inclusion..." -get-proof-by-hash $commonargs \ -	--leaf_hash $(cat add-entry.output | awk '{print $3}') \ -	--sth $(cat sth2.output | awk '{print $2}') -echo "" && $pause - -echo "verifying consistency..." -get-consistency-proof $commonargs \ -	--first $(cat sth1.output | awk '{print $2}') \ -	--second $(cat sth2.output | awk '{print $2}') -echo "" && $pause - -echo "fetching the log's first entry..." -get-entries $commonargs --start 0 --end 0 -echo "" - -rm *.output $0 -cd -rmdir $tmpdir diff --git a/client/cmd/get-consistency-proof/main.go b/client/cmd/get-consistency-proof/main.go deleted file mode 100644 index bb8a7a6..0000000 --- a/client/cmd/get-consistency-proof/main.go +++ /dev/null @@ -1,70 +0,0 @@ -package main - -import ( -	"context" -	"flag" -	"fmt" - -	"encoding/base64" - -	"github.com/golang/glog" -	"github.com/system-transparency/stfe/client" -	"github.com/system-transparency/stfe/types" -) - -var ( -	first  = flag.String("first", "", "base64-encoded sth") -	second = flag.String("second", "", "base64-encoded sth") -) - -func main() { -	flag.Parse() -	defer glog.Flush() - -	client, err := client.NewClientFromFlags() -	if err != nil { -		glog.Errorf("NewClientFromFlags: %v", err) -		return -	} -	sth1, sth2, err := newParamsFromFlags() -	if err != nil { -		glog.Errorf("NewRequestFromFlags: %v", err) -		return -	} - -	proof, err := client.GetConsistencyProof(context.Background(), sth1, sth2) -	if err != nil { -		glog.Errorf("GetConsistencyProof: %v", err) -		return -	} -	serialized, err := types.Marshal(*proof) -	if err != nil { -		glog.Errorf("Marshal: %v", err) -		return -	} -	fmt.Println("proof:", base64.StdEncoding.EncodeToString(serialized)) -} - -func newParamsFromFlags() (*types.StItem, *types.StItem, error) { -	sth1, err := decodeSthStr(*first) -	if err != nil { -		return nil, nil, fmt.Errorf("first: decodeSthStr: %v", err) -	} -	sth2, err := decodeSthStr(*second) -	if err != nil { -		return nil, nil, fmt.Errorf("second: decodeSthStr: %v", err) -	} -	return sth1, sth2, nil -} - -func decodeSthStr(sthStr string) (*types.StItem, error) { -	serialized, err := base64.StdEncoding.DecodeString(sthStr) -	if err != nil { -		return nil, fmt.Errorf("DecodeString: %v", err) -	} -	var item types.StItem -	if err = types.Unmarshal(serialized, &item); err != nil { -		return nil, fmt.Errorf("Unmarshal: %v", err) -	} -	return &item, nil -} diff --git a/client/cmd/get-entries/main.go b/client/cmd/get-entries/main.go deleted file mode 100644 index f32fdbf..0000000 --- a/client/cmd/get-entries/main.go +++ /dev/null @@ -1,83 +0,0 @@ -package main - -import ( -	"context" -	"flag" -	"fmt" - -	"encoding/base64" - -	"github.com/golang/glog" -	"github.com/google/trillian/merkle/rfc6962" -	"github.com/system-transparency/stfe/client" -	"github.com/system-transparency/stfe/types" -) - -var ( -	start = flag.Uint64("start", 0, "inclusive start index to download") -	end   = flag.Uint64("end", 0, "inclusive stop index to download") -) - -func main() { -	flag.Parse() -	defer glog.Flush() - -	client, err := client.NewClientFromFlags() -	if err != nil { -		glog.Errorf("NewClientFromFlags: %v", err) -		return -	} -	items, err := getRange(client, *start, *end) -	if err != nil { -		glog.Errorf("getRange: %v", err) -		return -	} -	if err := printRange(items); err != nil { -		glog.Errorf("printRange: %v", err) -		return -	} -} - -func getRange(client *client.Client, start, end uint64) ([]*types.StItem, error) { -	items := make([]*types.StItem, 0, end-start+1) -	for len(items) != cap(items) { -		rsp, err := client.GetEntries(context.Background(), start, end) -		if err != nil { -			return nil, fmt.Errorf("fetching entries failed: %v", err) -		} -		items = append(items, rsp...) -		start += uint64(len(rsp)) -	} -	return items, nil -} - -func printRange(items []*types.StItem) error { -	for i, item := range items { -		var status string -		msg, err := types.Marshal(item.SignedChecksumV1.Data) -		if err != nil { -			return fmt.Errorf("Marshal data failed: %v", err) -		} -		sig := item.SignedChecksumV1.Signature.Signature -		namespace := &item.SignedChecksumV1.Signature.Namespace -		if err := namespace.Verify(msg, sig); err != nil { -			status = "unverified signature" -		} else { -			status = "verified signature" -		} -		serializedNamespace, err := types.Marshal(*namespace) -		if err != nil { -			return fmt.Errorf("Marshal namespace failed: %v", err) -		} -		serializedLeaf, err := types.Marshal(*item) -		if err != nil { -			return fmt.Errorf("Marshal item on index %d: %v", *start+uint64(i), err) -		} -		fmt.Printf("Index(%d) - %s\n", *start+uint64(i), status) -		fmt.Printf("-> Namespace: %s\n", base64.StdEncoding.EncodeToString(serializedNamespace)) -		fmt.Printf("-> Identifier: %s\n", string(item.SignedChecksumV1.Data.Identifier)) -		fmt.Printf("-> Checksum: %s\n", base64.StdEncoding.EncodeToString(item.SignedChecksumV1.Data.Checksum)) -		fmt.Printf("-> Leaf hash: %s\n", base64.StdEncoding.EncodeToString(rfc6962.DefaultHasher.HashLeaf(serializedLeaf))) -	} -	return nil -} diff --git a/client/cmd/get-proof-by-hash/main.go b/client/cmd/get-proof-by-hash/main.go deleted file mode 100644 index 1f4f304..0000000 --- a/client/cmd/get-proof-by-hash/main.go +++ /dev/null @@ -1,66 +0,0 @@ -package main - -import ( -	"context" -	"flag" -	"fmt" - -	"encoding/base64" - -	"github.com/golang/glog" -	"github.com/system-transparency/stfe/client" -	"github.com/system-transparency/stfe/types" -) - -var ( -	sthStr      = flag.String("sth", "", "base64-encoded StItem of type StFormatSignedTreeHeadV1 (default: fetch new sth)") -	leafHashStr = flag.String("leaf_hash", "", "base64-encoded leaf hash") -) - -func main() { -	flag.Parse() -	defer glog.Flush() - -	client, err := client.NewClientFromFlags() -	if err != nil { -		glog.Errorf("NewClientFromFlags: %v", err) -		return -	} -	leafHash, sth, err := newParamsFromFlags(client) -	if err != nil { -		glog.Errorf("NewRequestFromFlags: %v", err) -		return -	} - -	proof, err := client.GetProofByHash(context.Background(), leafHash, sth) -	if err != nil { -		glog.Errorf("GetProofByHash: %v", err) -		return -	} -	serialized, err := types.Marshal(*proof) -	if err != nil { -		glog.Errorf("Marshal: %v", err) -	} -	fmt.Println("proof:", base64.StdEncoding.EncodeToString(serialized)) -} - -func newParamsFromFlags(client *client.Client) ([]byte, *types.StItem, error) { -	serialized, err := base64.StdEncoding.DecodeString(*sthStr) -	if err != nil { -		return nil, nil, fmt.Errorf("sth: DecodeString: %v", err) -	} -	var item types.StItem -	if err = types.Unmarshal(serialized, &item); err != nil { -		return nil, nil, fmt.Errorf("sth: Unmarshal: %v", err) -	} else if got, want := item.Format, types.StFormatSignedTreeHeadV1; got != want { -		return nil, nil, fmt.Errorf("unexpected StItem format: %v", got) -	} -	leafHash, err := base64.StdEncoding.DecodeString(*leafHashStr) -	if err != nil { -		return nil, nil, fmt.Errorf("leaf_hash: DecodeString: %v", err) -	} else if got, want := len(leafHash), 32; got != want { -		return nil, nil, fmt.Errorf("leaf_hash: unexpected size: %v", got) -	} -	glog.V(3).Infof("created request parameters TreeSize(%d) and LeafHash(%s)", item.SignedTreeHeadV1.TreeHead.TreeSize, *leafHashStr) -	return leafHash, &item, nil -} diff --git a/client/cmd/get-sth/main.go b/client/cmd/get-sth/main.go deleted file mode 100644 index 6b23b06..0000000 --- a/client/cmd/get-sth/main.go +++ /dev/null @@ -1,35 +0,0 @@ -package main - -import ( -	"context" -	"flag" -	"fmt" - -	"encoding/base64" - -	"github.com/golang/glog" -	"github.com/system-transparency/stfe/client" -	"github.com/system-transparency/stfe/types" -) - -func main() { -	flag.Parse() -	defer glog.Flush() - -	client, err := client.NewClientFromFlags() -	if err != nil { -		glog.Errorf("NewClientFromFlags: %v", err) -		return -	} -	sth, err := client.GetLatestSth(context.Background()) -	if err != nil { -		glog.Errorf("GetLatestSth: %v", err) -		return -	} -	serialized, err := types.Marshal(*sth) -	if err != nil { -		glog.Errorf("Marshal: %v", err) -		return -	} -	fmt.Println("sth:", base64.StdEncoding.EncodeToString(serialized)) -} diff --git a/client/flag.go b/client/flag.go deleted file mode 100644 index 8ba7a10..0000000 --- a/client/flag.go +++ /dev/null @@ -1,55 +0,0 @@ -package client - -import ( -	"flag" -	"fmt" - -	"crypto/ed25519" -	"encoding/base64" -	"net/http" - -	"github.com/system-transparency/stfe/types" -) - -var ( -	logId      = flag.String("log_id", "AAG+ZW+UesWdMFytUGkp28csBcziomSB3U2vvkAW55MVZQ==", "base64-encoded log identifier") -	logUrl     = flag.String("log_url", "http://tlog-poc.system-transparency.org:4780/st/v1", "log url") -	ed25519_sk = flag.String("ed25519_sk", "d8i6nud7PS1vdO0sIk9H+W0nyxbM63Y3/mSeUPRafWaFh8iH8QXvL7NaAYn2RZPrnEey+FdpmTYXE47OFO70eg==", "base64-encoded ed25519 signing key") -) - -func NewClientFromFlags() (*Client, error) { -	var err error -	c := Client{ -		HttpClient: &http.Client{}, -	} -	if len(*ed25519_sk) != 0 { -		sk, err := base64.StdEncoding.DecodeString(*ed25519_sk) -		if err != nil { -			return nil, fmt.Errorf("ed25519_sk: DecodeString: %v", err) -		} -		c.Signer = ed25519.PrivateKey(sk) -		c.Namespace, err = types.NewNamespaceEd25519V1([]byte(ed25519.PrivateKey(sk).Public().(ed25519.PublicKey))) -		if err != nil { -			return nil, fmt.Errorf("ed25519_vk: NewNamespaceEd25519V1: %v", err) -		} -	} -	if c.Log, err = NewDescriptorFromFlags(); err != nil { -		return nil, fmt.Errorf("NewDescriptorFromFlags: %v", err) -	} -	return &c, nil -} - -func NewDescriptorFromFlags() (*Descriptor, error) { -	b, err := base64.StdEncoding.DecodeString(*logId) -	if err != nil { -		return nil, fmt.Errorf("LogId: DecodeString: %v", err) -	} -	var namespace types.Namespace -	if err := types.Unmarshal(b, &namespace); err != nil { -		return nil, fmt.Errorf("LogId: Unmarshal: %v", err) -	} -	return &Descriptor{ -		Namespace: &namespace, -		Url:       *logUrl, -	}, nil -} diff --git a/client/verify.go b/client/verify.go deleted file mode 100644 index c95828c..0000000 --- a/client/verify.go +++ /dev/null @@ -1,52 +0,0 @@ -package client - -import ( -	"fmt" -	"reflect" - -	"github.com/google/trillian/merkle" -	"github.com/google/trillian/merkle/rfc6962" -	"github.com/system-transparency/stfe/types" -) - -func VerifySignedTreeHeadV1(namespace *types.Namespace, sth *types.StItem) error { -	if got, want := &sth.SignedTreeHeadV1.Signature.Namespace, namespace; !reflect.DeepEqual(got, want) { -		return fmt.Errorf("unexpected log id: %v", want) -	} -	th, err := types.Marshal(sth.SignedTreeHeadV1.TreeHead) -	if err != nil { -		return fmt.Errorf("Marshal: %v", err) -	} -	if err := namespace.Verify(th, sth.SignedTreeHeadV1.Signature.Signature); err != nil { -		return fmt.Errorf("Verify: %v", err) -	} -	return nil -} - -func VerifyConsistencyProofV1(proof, first, second *types.StItem) error { -	path := make([][]byte, 0, len(proof.ConsistencyProofV1.ConsistencyPath)) -	for _, nh := range proof.ConsistencyProofV1.ConsistencyPath { -		path = append(path, nh.Data) -	} -	return merkle.NewLogVerifier(rfc6962.DefaultHasher).VerifyConsistencyProof( -		int64(proof.ConsistencyProofV1.TreeSize1), -		int64(proof.ConsistencyProofV1.TreeSize2), -		first.SignedTreeHeadV1.TreeHead.RootHash.Data, -		second.SignedTreeHeadV1.TreeHead.RootHash.Data, -		path, -	) -} - -func VerifyInclusionProofV1(proof, sth *types.StItem, leafHash []byte) error { -	path := make([][]byte, 0, len(proof.InclusionProofV1.InclusionPath)) -	for _, nh := range proof.InclusionProofV1.InclusionPath { -		path = append(path, nh.Data) -	} -	return merkle.NewLogVerifier(rfc6962.DefaultHasher).VerifyInclusionProof( -		int64(proof.InclusionProofV1.LeafIndex), -		int64(proof.InclusionProofV1.TreeSize), -		path, -		sth.SignedTreeHeadV1.TreeHead.RootHash.Data, -		leafHash, -	) -} diff --git a/server/.gitignore b/cmd/siglog_server/.gitignore index 254defd..254defd 100644 --- a/server/.gitignore +++ b/cmd/siglog_server/.gitignore diff --git a/server/README.md b/cmd/siglog_server/README.md index 71bb3ac..71bb3ac 100644 --- a/server/README.md +++ b/cmd/siglog_server/README.md diff --git a/cmd/siglog_server/main.go b/cmd/siglog_server/main.go new file mode 100644 index 0000000..368b0a7 --- /dev/null +++ b/cmd/siglog_server/main.go @@ -0,0 +1,176 @@ +// Package main provides an STFE server binary +package main + +import ( +	"context" +	"crypto" +	"crypto/ed25519" +	"encoding/hex" +	"flag" +	"fmt" +	"net/http" +	"os" +	"os/signal" +	"strings" +	"sync" +	"syscall" +	"time" + +	"github.com/golang/glog" +	"github.com/google/trillian" +	"github.com/prometheus/client_golang/prometheus/promhttp" +	stfe "github.com/system-transparency/stfe/pkg/instance" +	"github.com/system-transparency/stfe/pkg/state" +	trillianWrapper "github.com/system-transparency/stfe/pkg/trillian" +	"github.com/system-transparency/stfe/pkg/types" +	"google.golang.org/grpc" +) + +var ( +	httpEndpoint = flag.String("http_endpoint", "localhost:6965", "host:port specification of where stfe serves clients") +	rpcBackend   = flag.String("log_rpc_server", "localhost:6962", "host:port specification of where Trillian serves clients") +	prefix       = flag.String("prefix", "", "a prefix that proceeds /st/v0/<endpoint>") +	trillianID   = flag.Int64("trillian_id", 0, "log identifier in the Trillian database") +	deadline     = flag.Duration("deadline", time.Second*10, "deadline for backend requests") +	key          = flag.String("key", "", "hex-encoded Ed25519 signing key") +	witnesses    = flag.String("witnesses", "", "comma-separated list of trusted witness verification keys in hex") +	maxRange     = flag.Int64("max_range", 10, "maximum number of entries that can be retrived in a single request") +	interval     = flag.Duration("interval", time.Second*30, "interval used to rotate the log's cosigned STH") +) + +func main() { +	flag.Parse() +	defer glog.Flush() + +	// wait for clean-up before exit +	var wg sync.WaitGroup +	defer wg.Wait() +	ctx, cancel := context.WithCancel(context.Background()) +	defer cancel() + +	glog.V(3).Infof("configuring stfe instance...") +	instance, err := setupInstanceFromFlags() +	if err != nil { +		glog.Errorf("setupInstance: %v", err) +		return +	} + +	glog.V(3).Infof("spawning state manager") +	go func() { +		wg.Add(1) +		defer wg.Done() +		instance.Stateman.Run(ctx) +		glog.Errorf("state manager shutdown") +		cancel() // must have state manager running +	}() + +	glog.V(3).Infof("spawning await") +	server := http.Server{Addr: *httpEndpoint} +	go await(ctx, func() { +		wg.Add(1) +		defer wg.Done() +		ctxInner, _ := context.WithTimeout(ctx, time.Second*60) +		glog.Infof("Shutting down HTTP server...") +		server.Shutdown(ctxInner) +		glog.V(3).Infof("HTTP server shutdown") +		glog.Infof("Shutting down spawned go routines...") +		cancel() +	}) + +	glog.Infof("Serving on %v/%v", *httpEndpoint, *prefix) +	if err = server.ListenAndServe(); err != http.ErrServerClosed { +		glog.Errorf("ListenAndServe: %v", err) +	} +} + +// SetupInstance sets up a new STFE instance from flags +func setupInstanceFromFlags() (*stfe.Instance, error) { +	var i stfe.Instance +	var err error + +	// Setup log configuration +	i.Signer, i.LogID, err = newLogIdentity(*key) +	if err != nil { +		return nil, fmt.Errorf("newLogIdentity: %v", err) +	} +	i.TreeID = *trillianID +	i.Prefix = *prefix +	i.MaxRange = *maxRange +	i.Deadline = *deadline +	i.Interval = *interval +	i.Witnesses, err = newWitnessMap(*witnesses) +	if err != nil { +		return nil, fmt.Errorf("newWitnessMap: %v", err) +	} + +	// Setup log client +	dialOpts := []grpc.DialOption{grpc.WithInsecure(), grpc.WithBlock(), grpc.WithTimeout(i.Deadline)} +	conn, err := grpc.Dial(*rpcBackend, dialOpts...) +	if err != nil { +		return nil, fmt.Errorf("Dial: %v", err) +	} +	i.Client = &trillianWrapper.TrillianClient{ +		TreeID: i.TreeID, +		GRPC:   trillian.NewTrillianLogClient(conn), +	} + +	// Setup state manager +	i.Stateman, err = state.NewStateManagerSingle(i.Client, i.Signer, i.Interval, i.Deadline) +	if err != nil { +		return nil, fmt.Errorf("NewStateManager: %v", err) +	} + +	// Register HTTP endpoints +	mux := http.NewServeMux() +	http.Handle("/", mux) +	for _, handler := range i.Handlers() { +		glog.V(3).Infof("adding handler: %s", handler.Path()) +		mux.Handle(handler.Path(), handler) +	} +	glog.V(3).Infof("Adding prometheus handler on path: /metrics") +	http.Handle("/metrics", promhttp.Handler()) + +	return &i, nil +} + +func newLogIdentity(key string) (crypto.Signer, string, error) { +	buf, err := hex.DecodeString(key) +	if err != nil { +		return nil, "", fmt.Errorf("DecodeString: %v", err) +	} +	sk := crypto.Signer(ed25519.PrivateKey(buf)) +	vk := sk.Public().(ed25519.PublicKey) +	return sk, hex.EncodeToString([]byte(vk[:])), nil +} + +// newWitnessMap creates a new map of trusted witnesses +func newWitnessMap(witnesses string) (map[[types.HashSize]byte][types.VerificationKeySize]byte, error) { +	w := make(map[[types.HashSize]byte][types.VerificationKeySize]byte) +	if len(witnesses) > 0 { +		for _, witness := range strings.Split(witnesses, ",") { +			b, err := hex.DecodeString(witness) +			if err != nil { +				return nil, fmt.Errorf("DecodeString: %v", err) +			} + +			var vk [types.VerificationKeySize]byte +			if n := copy(vk[:], b); n != types.VerificationKeySize { +				return nil, fmt.Errorf("Invalid verification key size: %v", n) +			} +			w[*types.Hash(vk[:])] = vk +		} +	} +	return w, nil +} + +// await waits for a shutdown signal and then runs a clean-up function +func await(ctx context.Context, done func()) { +	sigs := make(chan os.Signal, 1) +	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) +	select { +	case <-sigs: +	case <-ctx.Done(): +	} +	glog.V(3).Info("received shutdown signal") +	done() +} diff --git a/cmd/tmp/README.md b/cmd/tmp/README.md new file mode 100644 index 0000000..30d5317 --- /dev/null +++ b/cmd/tmp/README.md @@ -0,0 +1,2 @@ +# Warning +These basic commands will be moved or replaced by proper tooling. diff --git a/cmd/tmp/cosign/main.go b/cmd/tmp/cosign/main.go new file mode 100644 index 0000000..a51f17d --- /dev/null +++ b/cmd/tmp/cosign/main.go @@ -0,0 +1,56 @@ +package main + +import ( +	"bytes" +	"crypto/ed25519" +	"encoding/hex" +	"flag" +	"fmt" +	"log" +	"net/http" + +	"github.com/system-transparency/stfe/pkg/types" +) + +var ( +	url = flag.String("url", "http://localhost:6965/st/v0", "base url") +	sk  = flag.String("sk", "e1d7c494dacb0ddf809a17e4528b01f584af22e3766fa740ec52a1711c59500d711090dd2286040b50961b0fe09f58aa665ccee5cb7ee042d819f18f6ab5046b", "hex key") +) + +func main() { +	priv, err := hex.DecodeString(*sk) +	if err != nil { +		log.Fatalf("DecodeString: %v", err) +	} +	sk := ed25519.PrivateKey(priv) +	vk := sk.Public().(ed25519.PublicKey) +	fmt.Printf("sk: %x\nvk: %x\n", sk, vk) + +	rsp, err := http.Get(*url + "/get-tree-head-to-sign") +	if err != nil { +		log.Fatalf("Get: %v", err) +	} +	var sth types.SignedTreeHead +	if err := sth.UnmarshalASCII(rsp.Body); err != nil { +		log.Fatalf("UnmarshalASCII: %v", err) +	} +	fmt.Printf("%+v\n", sth) + +	msg := sth.TreeHead.Marshal() +	sig := ed25519.Sign(sk, msg) +	sigident := &types.SigIdent{ +		KeyHash:   types.Hash(vk[:]), +		Signature: &[types.SignatureSize]byte{}, +	} +	copy(sigident.Signature[:], sig) + +	buf := bytes.NewBuffer(nil) +	if err := sigident.MarshalASCII(buf); err != nil { +		log.Fatalf("MarshalASCII: %v", err) +	} +	rsp, err = http.Post(*url+"/add-cosignature", "type/stfe", buf) +	if err != nil { +		log.Fatalf("Post: %v", err) +	} +	fmt.Printf("Status: %v\n", rsp.StatusCode) +} diff --git a/cmd/tmp/keygen/main.go b/cmd/tmp/keygen/main.go new file mode 100644 index 0000000..c1c1b58 --- /dev/null +++ b/cmd/tmp/keygen/main.go @@ -0,0 +1,17 @@ +package main + +import ( +	"crypto/ed25519" +	"crypto/rand" +	"fmt" +	"log" +) + +func main() { +	vk, sk, err := ed25519.GenerateKey(rand.Reader) +	if err != nil { +		log.Fatalf("GenerateKey: %v", err) +	} +	fmt.Printf("sk: %x\n", sk[:]) +	fmt.Printf("vk: %x\n", vk[:]) +} diff --git a/cmd/tmp/submit/main.go b/cmd/tmp/submit/main.go new file mode 100644 index 0000000..3dcaa97 --- /dev/null +++ b/cmd/tmp/submit/main.go @@ -0,0 +1,29 @@ +package main + +// go run . | bash + +import ( +	"crypto/ed25519" +	"crypto/rand" +	"fmt" + +	"github.com/system-transparency/stfe/pkg/types" +) + +func main() { +	checksum := [32]byte{} +	msg := types.Message{ +		ShardHint: 0, +		Checksum:  &checksum, +	} + +	vk, sk, err := ed25519.GenerateKey(rand.Reader) +	if err != nil { +		fmt.Printf("ed25519.GenerateKey: %v\n", err) +		return +	} +	sig := ed25519.Sign(sk, msg.Marshal()) +	//fmt.Printf("sk: %x\nvk: %x\n", sk[:], vk[:]) + +	fmt.Printf("echo \"shard_hint=%d\nchecksum=%x\nsignature_over_message=%x\nverification_key=%x\ndomain_hint=%s\" | curl --data-binary @- localhost:6965/st/v0/add-leaf\n", msg.ShardHint, msg.Checksum[:], sig, vk[:], "example.com") +} @@ -1,3 +0,0 @@ -// Package stfe implements a System Transparency Front-End (STFE) personality -// for the Trillian log server gRPC API. -package stfe diff --git a/doc/api.md b/doc/api.md new file mode 100644 index 0000000..57ad119 --- /dev/null +++ b/doc/api.md @@ -0,0 +1,398 @@ +# System Transparency Logging: API v0 +This document describes details of the System Transparency logging +API, version 0.  The broader picture is not explained here.  We assume +that you have read the System Transparency Logging design document. +It can be found +[here](https://github.com/system-transparency/stfe/blob/design/doc/design.md). + +**Warning.** +This is a work-in-progress document that may be moved or modified. + +## Overview +Logs implement an HTTP(S) API for accepting requests and sending +responses. + +- Input data in requests and output data in responses are expressed as +  ASCII-encoded key/value pairs. +- Requests with input data use HTTP POST to send the data to a log. +- Binary data is hex-encoded before being transmitted. + +The motivation for using a text based key/value format for request and +response data is that it's simple to parse.  Note that this format is +not being used for the serialization of signed or logged data, where a +more well defined and storage efficient format is desirable.  A +submitter may distribute log responses to their end-users in any +format that suits them.  The (de)serialization required for +_end-users_ is a small subset of Trunnel.  Trunnel is an "idiot-proof" +wire-format in use by the Tor project. + +## Primitives +### Cryptography +Logs use the same Merkle tree hash strategy as +[RFC 6962,§2](https://tools.ietf.org/html/rfc6962#section-2). +The hash functions must be +[SHA256](https://csrc.nist.gov/csrc/media/publications/fips/180/4/final/documents/fips180-4-draft-aug2014.pdf). +Logs must sign tree heads using +[Ed25519](https://tools.ietf.org/html/rfc8032).  Log witnesses +must also sign tree heads using Ed25519. + +All other parts that are not Merkle tree related also use SHA256 as +the hash function.  Using more than one hash function would increases +the overall attack surface: two hash functions must be collision +resistant instead of one. + +### Serialization +Log requests and responses are transmitted as ASCII-encoded key/value +pairs, for a smaller dependency than an alternative parser like JSON. +Some input and output data is binary: cryptographic hashes and +signatures.  Binary data must be Base16-encoded, also known as hex +encoding.  Using hex as opposed to base64 is motivated by it being +simpler, favoring ease of decoding and encoding over efficiency on the +wire. + +We use the +[Trunnel](https://gitweb.torproject.org/trunnel.git) [description language](https://www.seul.org/~nickm/trunnel-manual.html) +to define (de)serialization of data structures that need to be signed or +inserted into the Merkle tree.  Trunnel is more expressive than the +[SSH wire format](https://tools.ietf.org/html/rfc4251#section-5). +It is about as expressive as the +[TLS presentation language](https://tools.ietf.org/html/rfc8446#section-3). +A notable difference is that Trunnel supports integer constraints. +The Trunnel language is also readable by humans _and_ machines. +"Obviously correct code" can be generated in C and Go. + +A fair summary of our Trunnel usage is as follows. + +All integers are 64-bit, unsigned, and in network byte order. +Fixed-size byte arrays are put into the serialization buffer in-order, +starting from the first byte.  Variable length byte arrays first +declare their length as an integer, which is then followed by that +number of bytes.  These basic types are concatenated to form a +collection.  You should not need a general-purpose Trunnel +(de)serialization parser to work with this format.  If you have one, +you may use it though.  The main point of using Trunnel is that it +makes a simple format explicit and unambiguous. + +#### Merkle tree head +Tree heads are signed both by a log and its witnesses.  It contains a +timestamp, a tree size, and a root hash.  The timestamp is included so +that monitors can ensure _liveliness_.  It is the time since the UNIX +epoch (January 1, 1970 00:00 UTC) in seconds.  The tree size +specifies the current number of leaves.  The root hash fixes the +structure and content of the Merkle tree. + +``` +struct tree_head { +	u64 timestamp; +	u64 tree_size; +	u8 root_hash[32]; +}; +``` + +The serialized tree head must be signed using Ed25519.  A witness must +not cosign a tree head if it is inconsistent with prior history or if +the timestamp is backdated or future-dated more than 12 hours. + +#### Merkle tree leaf +Logs support a single leaf type.  It contains a shard hint, a +checksum over whatever the submitter wants to log a checksum for, a +signature that the submitter computed over the shard hint and the +checksum, and a hash of the submitter's public verification key, that +can be used to verify the signature. + +``` +struct message { +    u64 shard_hint; +    u8 checksum[32]; +}; + +struct tree_leaf { +    struct message; +    u8 signature_over_message[64]; +    u8 key_hash[32]; +} +``` + +`message` is composed of the `shard_hint`, chosen by the submitter to +match the shard interval for the log it's submitting to, and the +submitter's `checksum` to be logged. + +`signature_over_message` is a signature over `message`, using the +submitter's verification key. It must be possible to verify the +signature using the submitter's public verification key, as indicated +by `key_hash`. + +`key_hash` is a hash of the submitter's verification key used for +signing `message`. It is included in `tree_leaf` so that the leaf can +be attributed to the submitter.  A hash, rather than the full public +key, is used to motivate verifiers to locate the appropriate key and +make an explicit trust decision. + +## Public endpoints +Every log has a base URL that identifies it uniquely.  The only +constraint is that it must be a valid HTTP(S) URL that can have the +`/st/v0/<endpoint>` suffix appended.  For example, a complete endpoint +URL could be +`https://log.example.com/2021/st/v0/get-tree-head-cosigned`. + +Input data (in requests) is POST:ed in the HTTP message body as ASCII +key/value pairs. + +Output data (in replies) is sent in the HTTP message body in the same +format as the input data, i.e. as ASCII key/value pairs on the format +`Key=Value` + +The HTTP status code is 200 OK to indicate success.  A different HTTP +status code is used to indicate failure, in which case a log should +respond with a human-readable string describing what went wrong using +the key `error`. Example: `error=Invalid signature.`. + +### get-tree-head-cosigned +Returns the latest cosigned tree head. Used together with +`get-proof-by-hash` and `get-consistency-proof` for verifying the tree. + +``` +GET <base url>/st/v0/get-tree-head-cosigned +``` + +Input: +- None + +Output on success: +- `timestamp`: `tree_head.timestamp` ASCII-encoded decimal number, +  seconds since the UNIX epoch. +- `tree_size`: `tree_head.tree_size` ASCII-encoded decimal number. +- `root_hash`: `tree_head.root_hash` hex-encoded. +- `signature`: hex-encoded Ed25519 signature over `timestamp`, +  `tree_size` and `root_hash` serialized into a `tree_head` as +  described in section `Merkle tree head`. +- `key_hash`: a hash of the public verification key (belonging to +  either the log or to one of its witnesses), which can be used to +  verify the most recent `signature`.  The key is encoded as defined +  in [RFC 8032, section 5.1.2](https://tools.ietf.org/html/rfc8032#section-5.1.2),  +  and then hashed using SHA256.  The hash value is hex-encoded. + +The `signature` and `key_hash` fields may repeat. The first signature +corresponds to the first key hash, the second signature corresponds to +the second key hash, etc.  The number of signatures and key hashes +must match. + +### get-tree-head-to-sign +Returns the latest tree head to be signed by log witnesses. Used by +witnesses. + +``` +GET <base url>/st/v0/get-tree-head-to-sign +``` + +Input: +- None + +Output on success: +- `timestamp`: `tree_head.timestamp` ASCII-encoded decimal number, +  seconds since the UNIX epoch. +- `tree_size`: `tree_head.tree_size` ASCII-encoded decimal number. +- `root_hash`: `tree_head.root_hash` hex-encoded. +- `signature`: hex-encoded Ed25519 signature over `timestamp`, +  `tree_size` and `root_hash` serialized into a `tree_head` as +  described in section `Merkle tree head`. +- `key_hash`: a hash of the log's public verification key, which can +  be used to verify `signature`.  The key is encoded as defined in +  [RFC 8032, section 5.1.2](https://tools.ietf.org/html/rfc8032#section-5.1.2), +  and then hashed using SHA256.  The hash value is hex-encoded. + +There is exactly one `signature` and one `key_hash` field. The +`key_hash` refers to the log's public verification key. + + +### get-tree-head-latest +Returns the latest tree head, signed only by the log. Used for +debugging purposes. + +``` +GET <base url>/st/v0/get-tree-head-latest +``` + +Input: +- None + +Output on success: +- `timestamp`: `tree_head.timestamp` ASCII-encoded decimal number, +  seconds since the UNIX epoch. +- `tree_size`: `tree_head.tree_size` ASCII-encoded decimal number. +- `root_hash`: `tree_head.root_hash` hex-encoded. +- `signature`: hex-encoded Ed25519 signature over `timestamp`, +  `tree_size` and `root_hash` serialized into a `tree_head` as +  described in section `Merkle tree head`. +- `key_hash`: a hash of the log's public verification key that can be +  used to verify `signature`.  The key is encoded as defined in +  [RFC 8032, section 5.1.2](https://tools.ietf.org/html/rfc8032#section-5.1.2), +  and then hashed using SHA256.  The hash value is hex-encoded. + +There is exactly one `signature` and one `key_hash` field. The +`key_hash` refers to the log's public verification key. + + +### get-proof-by-hash +``` +POST <base url>/st/v0/get-proof-by-hash +``` + +Input: +- `leaf_hash`: leaf identifying which `tree_leaf` the log should prove +  inclusion of, hex-encoded. +- `tree_size`: tree size of the tree head that the proof should be +  based on, as an ASCII-encoded decimal number. + +Output on success: +- `tree_size`: tree size that the proof is based on, as an +  ASCII-encoded decimal number. +- `leaf_index`: zero-based index of the leaf that the proof is based +  on, as an ASCII-encoded decimal number. +- `inclusion_path`: node hash, hex-encoded. + +The leaf hash is computed using the RFC 6962 hashing strategy.  In +other words, `SHA256(0x00 | tree_leaf)`. + +`inclusion_path` may be omitted or repeated to represent an inclusion +proof of zero or more node hashes.  The order of node hashes follow +from the hash strategy, see RFC 6962. + +Example: `echo "leaf_hash=241fd4538d0a35c2d0394e4710ea9e6916854d08f62602fb03b55221dcdac90f +tree_size=4711" | curl --data-binary @- localhost/st/v0/get-proof-by-hash` + +### get-consistency-proof +``` +POST <base url>/st/v0/get-consistency-proof +``` + +Input: +- `new_size`: tree size of a newer tree head, as an ASCII-encoded +  decimal number. +- `old_size`: tree size of an older tree head that the log should +  prove is consistent with the newer tree head, as an ASCII-encoded +  decimal number. + +Output on success: +- `new_size`: tree size of the newer tree head that the proof is based +  on, as an ASCII-encoded decimal number. +- `old_size`: tree size of the older tree head that the proof is based +  on, as an ASCII-encoded decimal number. +- `consistency_path`: node hash, hex-encoded. + +`consistency_path` may be omitted or repeated to represent a +consistency proof of zero or more node hashes.  The order of node +hashes follow from the hash strategy, see RFC 6962. + +Example: `echo "new_size=4711 +old_size=42" | curl --data-binary @- localhost/st/v0/get-consistency-proof` + +### get-leaves +``` +POST <base url>/st/v0/get-leaves +``` + +Input: +- `start_size`: index of the first leaf to retrieve, as an +  ASCII-encoded decimal number. +- `end_size`: index of the last leaf to retrieve, as an ASCII-encoded +  decimal number. + +Output on success: +- `shard_hint`: `tree_leaf.message.shard_hint` as an ASCII-encoded +  decimal number. +- `checksum`: `tree_leaf.message.checksum`, hex-encoded. +- `signature`: `tree_leaf.signature_over_message`, hex-encoded. +- `key_hash`: `tree_leaf.key_hash`, hex-encoded. + +All fields may be repeated to return more than one leaf.  The first +value in each list refers to the first leaf, the second value in each +list refers to the second leaf, etc.  The size of each list must +match. + +A log may return fewer leaves than requested.  At least one leaf +must be returned on HTTP status code 200 OK. + +Example: `echo "start_size=42 +end_size=4711" | curl --data-binary @- localhost/st/v0/get-leaves` + +### add-leaf +``` +POST <base url>/st/v0/add-leaf +``` + +Input: +- `shard_hint`: number within the log's shard interval as an +  ASCII-encoded decimal number. +- `checksum`: the cryptographic checksum that the submitter wants to +  log, hex-encoded. +- `signature_over_message`: the submitter's signature over +  `tree_leaf.message`, hex-encoded. +- `verification_key`: the submitter's public verification key.  The +  key is encoded as defined in +  [RFC 8032, section 5.1.2](https://tools.ietf.org/html/rfc8032#section-5.1.2) +  and then hex-encoded. +- `domain_hint`: domain name indicating where `tree_leaf.key_hash` +  can be found as a DNS TXT resource record. + +Output on success: +- None + +The submission will not be accepted if `signature_over_message` is +invalid or if the key hash retrieved using `domain_hint` does not +match a hash over `verification_key`. + +The submission may also not be accepted if the second-level domain +name exceeded its rate limit.  By coupling every add-leaf request to +a second-level domain, it becomes more difficult to spam logs.  You +would need an excessive number of domain names.  This becomes costly +if free domain names are rejected. + +Logs don't publish domain-name to key bindings because key +management is more complex than that. + +Public logging should not be assumed to have happened until an +inclusion proof is available.  An inclusion proof should not be relied +upon unless it leads up to a trustworthy signed tree head.  Witness +cosigning can make a tree head trustworthy. + +Example: `echo "shard_hint=1640995200 +checksum=cfa2d8e78bf273ab85d3cef7bde62716261d1e42626d776f9b4e6aae7b6ff953 +signature_over_message=c026687411dea494539516ee0c4e790c24450f1a4440c2eb74df311ca9a7adf2847b99273af78b0bda65dfe9c4f7d23a5d319b596a8881d3bc2964749ae9ece3 +verification_key=c9a674888e905db1761ba3f10f3ad09586dddfe8581964b55787b44f318cbcdf +domain_hint=example.com" | curl --data-binary @- localhost/st/v0/add-leaf` + +### add-cosignature +``` +POST <base url>/st/v0/add-cosignature +``` + +Input: +- `signature`: Ed25519 signature over `tree_head`, hex-encoded. +- `key_hash`: hash of the witness' public verification key that can be +  used to verify `signature`.  The key is encoded as defined in +  [RFC 8032, section 5.1.2](https://tools.ietf.org/html/rfc8032#section-5.1.2), +  and then hashed using SHA256. The hash value is hex-encoded. + +Output on success: +- None + +`key_hash` can be used to identify which witness signed the tree +head.  A key-hash, rather than the full verification key, is used to +motivate verifiers to locate the appropriate key and make an explicit +trust decision. + +Example: `echo "signature=d1b15061d0f287847d066630339beaa0915a6bbb77332c3e839a32f66f1831b69c678e8ca63afd24e436525554dbc6daa3b1201cc0c93721de24b778027d41af +key_hash=662ce093682280f8fbea9939abe02fdba1f0dc39594c832b411ddafcffb75b1d" | curl --data-binary @- localhost/st/v0/add-cosignature` + +## Summary of log parameters +- **Public key**: The Ed25519 verification key to be used for +  verifying tree head signatures. +- **Log identifier**: The public verification key `Public key` hashed +  using SHA256. +- **Shard interval start**: The earliest time at which logging +  requests are accepted as the number of seconds since the UNIX epoch. +- **Shard interval end**: The latest time at which logging +  requests are accepted as the number of seconds since the UNIX epoch. +- **Base URL**: Where the log can be reached over HTTP(S).  It is the +  prefix to be used to construct a version 0 specific endpoint. diff --git a/doc/claimant.md b/doc/claimant.md new file mode 100644 index 0000000..6728fef --- /dev/null +++ b/doc/claimant.md @@ -0,0 +1,71 @@ +# Claimant model +## **System<sup>CHECKSUM</sup>** +System<sup>CHECKSUM</sup> is about the claims made by a data publisher. +* **Claim<sup>CHECKSUM</sup>**: +	_I, data publisher, claim that the data_: +	1. has cryptographic hash X +	2. is produced by no-one but myself +* **Statement<sup>CHECKSUM</sup>**: signed checksum<br> +* **Claimant<sup>CHECKSUM</sup>**: data publisher<br> +	The data publisher is a party that wants to publish some data. +* **Believer<sup>CHECKSUM</sup>**: end-user<br> +	The end-user is a party that wants to use some published data. +* **Verifier<sup>CHECKSUM</sup>**: data publisher<br> +	Only the data publisher can verify the above claims. +* **Arbiter<sup>CHECKSUM</sup>**:<br> +    There's no official body.  Invalidated claims would affect reputation. + +System<sup>CHECKSUM\*</sup> can be defined to make more specific claims.  Below +is a reproducible builds example. + +### **System<sup>CHECKSUM-RB</sup>**: +System<sup>CHECKSUM-RB</sup> is about the claims made by a _software publisher_ +that makes reproducible builds available. +* **Claim<sup>CHECKSUM-RB</sup>**: +	_I, software publisher, claim that the data_: +	1. has cryptographic hash X +	2. is the output of a reproducible build for which the source can be located +	using X as an identifier +* **Statement<sup>CHECKSUM-RB</sup>**: Statement<sup>CHECKSUM</sup> +* **Claimant<sup>CHECKSUM-RB</sup>**: software publisher<br> +	The software publisher is a party that wants to publish the output of a +	reproducible build. +* **Believer<sup>CHECKSUM-RB</sup>**: end-user<br> +	The end-user is a party that wants to run an executable binary that built +	reproducibly. +* **Verifier<sup>CHECKSUM-RB</sup>**: any interested party<br> +	These parties try to verify the above claims.  For example: +	* the software publisher itself (_"has my identity been compromised?"_) +	* rebuilders that check for locatability and reproducibility +* **Arbiter<sup>CHECKSUM-RB</sup>**:<br> +    There's no official body.  Invalidated claims would affect reputation. + +## **System<sup>CHECKSUM-LOG</sup>**: +System<sup>CHECKSUM-LOG</sup> is about the claims made by a _log operator_. +It adds _discoverability_ into System<sup>CHECKSUM\*</sup>.  Discoverability +means that Verifier<sup>CHECKSUM\*</sup> can see all +Statement<sup>CHECKSUM</sup> that Believer<sup>CHECKSUM\*</sup> accept. + +* **Claim<sup>CHECKSUM-LOG</sup>**: +	_I, log operator, make available:_ +	1. a globally consistent append-only log of Statement<sup>CHECKSUM</sup> +* **Statement<sup>CHECKSUM-LOG</sup>**: signed tree head +* **Claimant<sup>CHECKSUM-LOG</sup>**: log operator<br> +   Possible operators might be: +	* a small subset of data publishers +	* members of relevant consortia +* **Believer<sup>CHECKSUM-LOG</sup>**: +	* Believer<sup>CHECKSUM\*</sup> +	* Verifier<sup>CHECKSUM\*</sup><br> +* **Verifier<sup>CHECKSUM-LOG</sup>**: third parties<br> +	These parties verify the above claims.  Examples include: +	* members of relevant consortia +	* non-profits and other reputable organizations +	* security enthusiasts and researchers +	* log operators (cross-ecosystem) +	* monitors (cross-ecosystem) +	* a small subset of data publishers (cross-ecosystem) +* **Arbiter<sup>CHECKSUM-LOG</sup>**:<br> +	There is no official body.  The ecosystem at large should stop using an +	instance of System<sup>CHECKSUM-LOG</sup> if cryptographic proofs of log +	misbehavior are preseneted by some Verifier<sup>CHECKSUM-LOG</sup>. diff --git a/doc/design.md b/doc/design.md new file mode 100644 index 0000000..2e01a34 --- /dev/null +++ b/doc/design.md @@ -0,0 +1,251 @@ +# System Transparency Logging: Design v0 +We propose System Transparency logging.  It is similar to Certificate +Transparency, except that cryptographically signed checksums are logged as +opposed to X.509 certificates.  Publicly logging signed checksums allow anyone +to discover which keys produced what signatures.  As such, malicious and +unintended key-usage can be _detected_.  We present our design and conclude by +providing two use-cases: binary transparency and reproducible builds. + +**Target audience.** +You are most likely interested in transparency logs or supply-chain security. + +**Preliminaries.** +You have basic understanding of cryptographic primitives like digital +signatures, hash functions, and Merkle trees.  You roughly know what problem +Certificate Transparency solves and how. + +**Warning.** +This is a work-in-progress document that may be moved or modified.  A future +revision of this document will bump the version number to v1.  Please let us +know if you have any feedback. + +## Introduction +Transparency logs make it possible to detect unwanted events.  For example, +	are there any (mis-)issued TLS certificates [\[CT\]](https://tools.ietf.org/html/rfc6962), +	did you get a different Go module than everyone else [\[ChecksumDB\]](https://go.googlesource.com/proposal/+/master/design/25530-sumdb.md), +	or is someone running unexpected commands on your server [\[AuditLog\]](https://transparency.dev/application/reliably-log-all-actions-performed-on-your-servers/). +A System Transparency log makes signed checksums transparent.  The overall goal +is to facilitate detection of unwanted key-usage. + +## Threat model and (non-)goals +We consider a powerful attacker that gained control of a target's signing and +release infrastructure.  This covers a weaker form of attacker that is able to +sign data and distribute it to a subset of isolated users.  For example, this is +essentially what the FBI requested from Apple in the San Bernardino case [\[FBI-Apple\]](https://www.eff.org/cases/apple-challenges-fbi-all-writs-act-order). +The fact that signing keys and related infrastructure components get +compromised should not be controversial these days [\[SolarWinds\]](https://www.zdnet.com/article/third-malware-strain-discovered-in-solarwinds-supply-chain-attack/). + +The attacker can also gain control of the transparency log's signing key and +infrastructure.  This covers a weaker form of attacker that is able to sign log +data and distribute it to a subset of isolated users.  For example, this could +have been the case when a remote code execution was found for a Certificate +Transparency Log [\[DigiCert\]](https://groups.google.com/a/chromium.org/g/ct-policy/c/aKNbZuJzwfM). + +Any attacker that is able to position itself to control these components will +likely be _risk-averse_.  This is at minimum due to two factors.  First, +detection would result in a significant loss of capability that is by no means +trivial to come by.  Second, detection means that some part of the attacker's +malicious behavior will be disclosed publicly. + +Our goal is to facilitate _detection_ of compromised signing keys.  We consider +a signing key compromised if an end-user accepts an unwanted signature as valid. +The solution that we propose is that signed checksums are transparency logged. +For security we need a collision resistant hash function and an unforgeable +signature scheme.  We also assume that at most a threshold of seemingly +independent parties are adversarial. + +It is a non-goal to disclose the data that a checksum represents.  For example, +the log cannot distinguish between a checksum that represents a tax declaration, +an ISO image, or a Debian package.  This means that the type of detection we +support is more _coarse-grained_ when compared to Certificate Transparency. + +## Design +We consider a data publisher that wants to digitally sign their data.  The data +is of opaque type.  We assume that end-users have a mechanism to locate the +relevant public verification keys.  Data and signatures can also be retrieved +(in)directly from the data publisher.  We make little assumptions about the +signature tooling.  The ecosystem at large can continue to use `gpg`, `openssl`, +`ssh-keygen -Y`, `signify`, or something else. + +We _have to assume_ that additional tooling can be installed by end-users that +wish to enforce transparency logging.  For example, none of the existing +signature tooling supports verification of Merkle tree proofs.  A side-effect of +our design is that this additional tooling makes no outbound connections.  The +above data flows are thus preserved. + +### A bird's view +A central part of any transparency log is the data stored by the log.  The data is stored by the +leaves of an append-only Merkle tree.  Our leaf structure contains four fields: +- **shard_hint**: a number that binds the leaf to a particular _shard interval_. +Sharding means that the log has a predefined time during which logging requests +are accepted.  Once elapsed, the log can be shut down. +- **checksum**: a cryptographic hash of some opaque data.  The log never +sees the opaque data; just the hash made by the data publisher. +- **signature**: a digital signature that is computed by the data publisher over +the leaf's shard hint and checksum. +- **key_hash**: a cryptographic hash of the data publisher's public verification key that can be +used to verify the signature. + +#### Step 1 - preparing a logging request +The data publisher selects a shard hint and a checksum that should be logged. +For example, the shard hint could be "logs that are active during 2021".  The +checksum might be the hash of a release file. + +The data publisher signs the selected shard hint and checksum using a secret +signing key.  Both the signed message and the signature is stored +in the leaf for anyone to verify.  Including a shard hint in the signed message +ensures that a good Samaritan cannot change it to log all leaves from an +earlier shard into a newer one. + +A hash of the public verification key is also stored in the leaf.  This makes it +possible to attribute the leaf to the data publisher.  For example, a data publisher +that monitors the log can look for leaves that match their own key hash(es). + +A hash, rather than the full public verification key, is used to motivate the +verifier to locate the key and make an explicit trust decision.  Not disclosing the public +verification key in the leaf makes it more unlikely that someone would use an untrusted key _by +mistake_. + +#### Step 2 - submitting a logging request +The log implements an HTTP(S) API.  Input and output is human-readable and uses +a simple key-value format.  A more complex parser like JSON is not needed +because the exchanged data structures are primitive enough. + +The data publisher submits their shard hint, checksum, signature, and public +verification key as key-value pairs.  The log will use the public verification +key to check that the signature is valid, then hash it to construct the `key_hash` part of the leaf. + +The data publisher also submits a _domain hint_.  The log will download a DNS +TXT resource record based on the provided domain name.  The downloaded result +must match the public verification key hash.  By verifying that the submitter +controls a domain that is aware of the public verification key, rate limits can +be applied per second-level domain.  As a result, you would need a large number +of domain names to spam the log in any significant way. + +Using DNS to combat spam is convenient because many data publishers already have +a domain name.  A single domain name is also relatively cheap.  Another +benefit is that the same anti-spam mechanism can be used across several +independent logs without coordination.  This is important because a healthy log +ecosystem needs more than one log in order to be reliable.  DNS also has built-in +caching which data publishers can influence by setting TTLs accordingly. + +The submitter's domain hint is not part of the leaf because key management is +more complex than that.  A separate project should focus on transparent key +management.  The scope of our work is transparent _key-usage_. + +The log will _try_ to incorporate a leaf into the Merkle tree if a logging +request is accepted.  There are no _promises of public logging_ as in +Certificate Transparency.  Therefore, the submitter needs to wait for an +inclusion proof to appear before concluding that the logging request succeeded.  Not having +inclusion promises makes the log less complex. + +#### Step 3 - distributing proofs of public logging +The data publisher is responsible for collecting all cryptographic proofs that +their end-users will need to enforce public logging.  The collection below +should be downloadable from the same place that published data is normally hosted. +1. **Opaque data**: the data publisher's opaque data. +2. **Shard hint**: the data publisher's selected shard hint. +3. **Signature**: the data publisher's leaf signature. +4. **Cosigned tree head**: the log's tree head and a _list of signatures_ that +state it is consistent with prior history. +5. **Inclusion proof**: a proof of inclusion based on the logged leaf and tree +head in question. + +The data publisher's public verification key is known.  Therefore, the first three fields are +sufficient to reconstruct the logged leaf.  The leaf's signature can be +verified.  The final two fields then prove that the leaf is in the log.  If the +leaf is included in the log, any monitor can detect that there is a new +signature made by a given data publisher, 's public verification key. + +The catch is that the proof of logging is only as convincing as the tree head +that the inclusion proof leads up to.  To bypass public logging, the attacker +needs to control a threshold of independent _witnesses_ that cosign the log.  A +benign witness will only sign the log's tree head if it is consistent with prior +history. + +#### Summary +The log is sharded and will shut down at a predefined time.  The log can shut +down _safely_ because end-user verification is not interactive.  The difficulty +of bypassing public logging is based on the difficulty of controlling a +threshold of independent witnesses.  Witnesses cosign tree heads to make them +trustworthy. + +Submitters, monitors, and witnesses interact with the log using an HTTP(S) API. +Submitters must prove that they own a domain name as an anti-spam mechanism. +End-users interact with the log _indirectly_ via a data publisher.  It is the +data publisher's job to log signed checksums, distribute necessary proofs of +logging, and monitor the log. + +### A peek into the details +Our bird's view introduction skipped many details that matter in practise.  Some +of these details are presented here using a question-answer format.  A +question-answer format is helpful because it is easily modified and extended. + +#### What cryptographic primitives are supported? +The only supported hash algorithm is SHA256.  The only supported signature +scheme is Ed25519.  Not having any cryptographic agility makes the protocol less +complex and more secure. + +We can be cryptographically opinionated because of a key insight.  Existing +signature tools like `gpg`, `ssh-keygen -Y`, and `signify` cannot verify proofs +of public logging.  Therefore, _additional tooling must already be installed by +end-users_.  That tooling should verify hashes using the log's hash function. +That tooling should also verify signatures using the log's signature scheme. +Both tree heads and tree leaves are being signed. + +#### Why not let the data publisher pick their own signature scheme and format? +Agility introduces complexity and difficult policy questions.  For example, +which algorithms and formats should (not) be supported and why?  Picking Ed25519 +is a current best practise that should be encouraged if possible. + +There is not much we can do if a data publisher _refuses_ to rely on the log's +hash function or signature scheme. + +#### What if the data publisher must use a specific signature scheme or format? +They may _cross-sign_ the data as follows. +1. Sign the data as they're used to. +2. Hash the data and use the result as the leaf's checksum to be logged. +3. Sign the leaf using the log's signature scheme. + +For verification, the end-user first verifies that the usual signature from step 1 is valid.  Then the +end-user uses the additional tooling (which is already required) to verify the rest. +Cross-signing should be a relatively comfortable upgrade path that is backwards +compatible.  The downside is that the data publisher may need to manage an +additional key-pair. + +#### What (de)serialization parsers are needed? +#### What policy should be used? +#### Why witness cosigning? +#### Why sharding? +Unlike X.509 certificates which already have validity ranges, a +checksum does not carry any such information.  Therefore, we require +that the submitter selects a _shard hint_.  The selected shard hint +must be in the log's _shard interval_.  A shard interval is defined by +a start time and an end time.  Both ends of the shard interval are +inclusive and expressed as the number of seconds since the UNIX epoch +(January 1, 1970 00:00 UTC). + +Sharding simplifies log operations because it becomes explicit when a +log can be shutdown.  A log must only accept logging requests that +have valid shard hints.  A log should only accept logging requests +during the predefined shard interval.  Note that _the submitter's +shard hint is not a verified timestamp_.  The submitter should set the +shard hint as large as possible.  If a roughly verified timestamp is +needed, a cosigned tree head can be used. + +Without a shard hint, the good Samaritan could log all leaves from an +earlier shard into a newer one.  Not only would that defeat the +purpose of sharding, but it would also become a potential +denial-of-service vector. + +#### TODO +Add more key questions and answers. +- Log spamming +- Log poisoning +- Why we removed identifier field from the leaf +- Explain `latest`, `stable` and `cosigned` tree head. +- Privacy aspects +- How does this whole thing work with more than one log? + +## Concluding remarks +Example of binary transparency and reproducible builds. diff --git a/doc/sketch.md b/doc/sketch.md deleted file mode 100644 index 31964e0..0000000 --- a/doc/sketch.md +++ /dev/null @@ -1,372 +0,0 @@ -# System Transparency Logging -This document provides a sketch of System Transparency (ST) logging.  The basic -idea is to insert hashes of system artifacts into a public, append-only, and -tamper-evident transparency log, such that any enforcing client can be sure that -they see the same system artifacts as everyone else.  A system artifact could -be a browser update, an operating system image, a Debian package, or more -generally something that is opaque. - -We take inspiration from the Certificate Transparency Front-End -([CTFE](https://github.com/google/certificate-transparency-go/tree/master/trillian/ctfe)) -that implements [RFC 6962](https://tools.ietf.org/html/rfc6962) for -[Trillian](https://transparency.dev). - -## Log parameters -An ST log is defined by the following parameters: -- `log_identifier`: a `Namespace` of type `ed25519_v1` that defines the log's -signing algorithm and public verification key. -- `supported_namespaces`: a list of namespace types that the log supports. -Entities must use a supported namespace type when posting signed data to the -log. -- `base_url`: prefix used by clients that contact the log, e.g., -example.com:1234/log. -- `final_cosigned_tree_head`: an `StItem` of type `cosigned_tree_head_v*`.  Not -set until the log is turned into read-only mode in preparation of a shutdown. - -ST logs use the same hash strategy as described in RFC 6962: SHA256 with `0x00` -as leaf node prefix and `0x01` as interior node prefix. - -In contrast to Certificate Transparency (CT) **there is no Maximum Merge Delay -(MMD)**.  New entries are merged into the log as soon as possible, and no client -should trust that something is logged until an inclusion proof can be provided -that references a trustworthy STH.  Therefore, **there are no "promises" of -public logging** as in CT. - -To produce trustworthy STHs a simple form of [witness -cosigning](https://arxiv.org/pdf/1503.08768.pdf) is built into the log. -Witnesses poll the log for the next stable STH, and verify that it is consistent -before posting a cosignature that can then be served by the log. - -## Acceptance criteria and scope -A log should accept a leaf submission if it is: -- Well-formed, see data structure definitions below. -- Digitally signed by a registered namespace. - -Rate limits may be applied per namespace to combat spam.  Namespaces may also be -used by clients to determine which entries belong to who.  It is up to the -submitters to communicate trusted namespaces to their own clients.  In other -words, there are no mappings from namespaces to identities built into the log. -There is also no revocation of namespaces: **we facilitate _detection_ of -compromised signing keys by making artifact hashes public, which is not to be -confused with _prevention_ or even _recovery_ after detection**. - -## Data structure definitions -Data structures are defined and serialized using the presentation language in -[RFC 5246, §4](https://tools.ietf.org/html/rfc5246).  A definition of the log's -Merkle tree can be found in [RFC 6962, -§2](https://tools.ietf.org/html/rfc6962#section-2). - -### Namespace -A _namespace_ is a versioned data structure that contains a public verification -key (or fingerprint), as well as enough information to determine its format, -signing, and verification operations.  Namespaces are used as identifiers, both -for the log itself and the parties that submit artifact hashes and cosignatures. - -``` -enum { -	reserved(0), -	ed25519_v1(1), -	(2^16-1) -} NamespaceFormat; - -struct { -	NamespaceFormat format; -	select (format) { -		case ed25519_v1: Ed25519V1; -	} message; -} Namespace; -``` - -Our namespace format is inspired by Keybase's -[key-id](https://keybase.io/docs/api/1.0/kid). - -#### Ed25519V1 -At this time the only supported namespace type is based on Ed25519.  The -namespace field contains the full verification key.  Signing operations and -serialized formats are defined by [RFC -8032](https://tools.ietf.org/html/rfc8032). -``` -struct { -	opaque namespace[32]; // public verification key -} Ed25519V1; -``` - -### `StItem` -A general-purpose `TransItem` is defined in [RFC 6962/bis, -§4.5](https://tools.ietf.org/html/draft-ietf-trans-rfc6962-bis-34#section-4.5). -We define our own `TransItem`, but name it `StItem` to emphasize that they are -not the same. - -``` -enum { -	reserved(0), -	signed_tree_head_v1(1), -	cosigned_tree_head_v1(2), -	consistency_proof_v1(3), -	inclusion_proof_v1(4), -	signed_checksum_v1(5), // leaf type -	(2^16-1) -} StFormat; - -struct { -	StFormat format; -	select (format) { -		case signed_tree_head_v1: SignedTreeHeadV1; -		case cosigned_tree_head_v1: CosignedTreeHeadV1; -		case consistency_proof_v1: ConsistencyProofV1; -		case inclusion_proof_v1: InclusionProofV1; -		case signed_checksum_v1: SignedChecksumV1; -	} message; -} StItem; - -struct { -	StItem items<0..2^32-1>; -} StItemList; -``` - -#### `signed_tree_head_v1` -We use the same tree head definition as in [RFC 6962/bis, -§4.9](https://tools.ietf.org/html/draft-ietf-trans-rfc6962-bis-34#section-4.9). -The resulting _signed_ tree head is packaged differently: a namespace is used as -log identifier, and it is communicated in a `SignatureV1` structure. -``` -struct { -	TreeHeadV1 tree_head; -	SignatureV1 signature; -} SignedTreeHeadV1; - -struct { -	uint64 timestamp; -	uint64 tree_size; -	NodeHash root_hash; -	Extension extensions<0..2^16-1>; -} TreeHeadV1; -opaque NodeHash<32..2^8-1>; - -struct { -	Namespace namespace; -	opaque signature<1..2^16-1>; -} SignatureV1; -``` - -#### `cosigned_tree_head_v1` -Transparency logs were designed to be cryptographically verifiable in the -presence of a gossip-audit model that ensures everyone observes _the same -cryptographically verifiable log_.  The gossip-audit model is largely undefined -in today's existing transparency logging ecosystems, which means that the logs -must be trusted to play by the rules.   We wanted to avoid that outcome in our -ecosystem.  Therefore, a gossip-audit model is built into the log. - -The basic idea is that an STH should only be considered valid if it is cosigned -by a number of witnesses that verify the append-only property.  Which witnesses -to trust and under what circumstances is defined by a client-side _witness -cosigning policy_.  For example, -	"require no witness cosigning", -	"must have at least `k` signatures from witnesses A...J", and -	"must have at least `k` signatures from witnesses A...J where one is from -		witness B". - -Witness cosigning policies are beyond the scope of this specification. - -A cosigned STH is composed of an STH and a list of cosignatures.  A cosignature -must cover the serialized STH as an `StItem`, and be produced with a witness -namespace of type `ed25519_v1`. - -``` -struct { -	SignedTreeHeadV1 signed_tree_head; -	SignatureV1 cosignatures<0..2^32-1>; // vector of cosignatures -} CosignedTreeHeadV1; -``` - -#### `consistency_proof_v1` -For the most part we use the same consistency proof definition as in [RFC -6962/bis, -§4.11](https://tools.ietf.org/html/draft-ietf-trans-rfc6962-bis-34#section-4.11). -There are two modifications: our log identifier is a namespace rather than an -[OID](https://tools.ietf.org/html/draft-ietf-trans-rfc6962-bis-34#section-4.4), -and a consistency proof may be empty. - -``` -struct { -	Namespace log_id; -	uint64 tree_size_1; -	uint64 tree_size_2; -	NodeHash consistency_path<0..2^16-1>; -} ConsistencyProofV1; -``` - -#### `inclusion_proof_v1` -For the most part we use the same inclusion proof definition as in [RFC -6962/bis, -§4.12](https://tools.ietf.org/html/draft-ietf-trans-rfc6962-bis-34#section-4.12). -There are two modifications: our log identifier is a namespace rather than an -[OID](https://tools.ietf.org/html/draft-ietf-trans-rfc6962-bis-34#section-4.4), -and an inclusion proof may be empty. -``` -struct { -	Namespace log_id; -	uint64 tree_size; -	uint64 leaf_index; -	NodeHash inclusion_path<0..2^16-1>; -} InclusionProofV1; -``` - -#### `signed_checksum_v1` -A checksum entry contains a package identifier like `foobar-1.2.3` and an -artifact hash.   It is then signed so that clients can distinguish artifact -hashes from two different software publishers A and B.  For example, the -`signed_checksum_v1` type can help [enforce public binary logging before -accepting a new software -update](https://wiki.mozilla.org/Security/Binary_Transparency). - -``` -struct { -	ChecksumV1 data; -	SignatureV1 signature; -} SignedChecksumV1; - -struct { -	opaque identifier<1..128>; -	opaque checksum<1..64>; -} ChecksumV1; -``` - -It is assumed that clients know how to find the real artifact source (if not -already at hand), such that the logged hash can be recomputed and compared for -equality.  The log is not aware of how artifact hashes are computed, which means -that it is up to the submitters to define hash functions, data formats, and -such. - -## Public endpoints -Clients talk to the log using HTTP(S). Successfully processed requests are -responded to with HTTP status code `200 OK`, and any returned data is -serialized.  Endpoints without input parameters use HTTP GET requests. -Endpoints that have input parameters HTTP POST a TLS-serialized data structure. -The HTTP content type `application/octet-stream` is used when sending data. - -### add-entry -``` -POST https://<base url>/st/v1/add-entry -``` - -Input: -- An `StItem` of type `signed_checksum_v1`. - -No output. - -### add-cosignature -``` -POST https://<base url>/st/v1/add-cosignature -``` - -Input: -- An `StItem` of type `cosigned_tree_head_v1`.  The list of cosignatures must -be of length one, the witness signature must cover the item's STH, and that STH -must additionally match the log's stable STH that is currently being cosigned. - -No output. - -### get-latest-sth -``` -GET https://<base url>/st/v1/get-latest-sth -``` - -No input. - -Output: -- An `StItem` of type `signed_tree_head_v1` that corresponds to the most -recent STH. - -### get-stable-sth -``` -GET https://<base url>/st/v1/get-stable-sth -``` - -No input. - -Output: -- An `StItem` of type `signed_tree_head_v1` that corresponds to a stable STH -that witnesses should cosign.  The same STH is returned for a period of time. - -### get-cosigned-sth -``` -GET https://<base url>/st/v1/get-cosigned-sth -``` - -No input. - -Output: -- An `StItem` of type `cosigned_tree_head_v1` that corresponds to the most -recent cosigned STH. - -### get-proof-by-hash -``` -POST https://<base url>/st/v1/get-proof-by-hash -``` - -Input: -``` -struct { -	opaque hash[32]; // leaf hash -	uint64 tree_size; // tree size that the proof should be based on -} GetProofByHashV1; -``` - -Output: -- An `StItem` of type `inclusion_proof_v1`. - -### get-consistency-proof -``` -POST https://<base url>/st/v1/get-consistency-proof -``` - -Input: -``` -struct { -	uint64 first; // first tree size that the proof should be based on -	uint64 second; // second tree size that the proof should be based on -} GetConsistencyProofV1; -``` - -Output: -- An `StItem` of type `consistency_proof_v1`. - -### get-entries -``` -POST https://<base url>/st/v1/get-entries -``` - -Input: -``` -struct { -	uint64 start; // 0-based index of first entry to retrieve -	uint64 end; // 0-based index of last entry to retrieve in decimal. -} GetEntriesV1; -``` - -Output: -- An `StItem` list where each entry is of type `signed_checksum_v1`.  The first -`StItem` corresponds to the start index, the second one to `start+1`, etc.  The -log may return fewer entries than requested. - -# Appendix A -In the future other namespace types might be supported.  For example, we could -add [RSASSA-PKCS1-v1_5](https://tools.ietf.org/html/rfc3447#section-8.2) as -follows: -1. Add `rsa_v1` format and RSAV1 namespace.  This is what we would register on -the server-side such that the server knows the namespace and complete key. -``` -struct { -	opaque namespace<32>; // key fingerprint -	// + some encoding of public key -} RSAV1; -``` -2. Add `rsassa_pkcs1_5_v1` format and `RSASSAPKCS1_5_v1`.  This is what the -submitter would use to communicate namespace and RSA signature mode. -``` -struct { -	opaque namespace<32>; // key fingerprint -	// + necessary parameters, e.g., SHA256 as hash function -} RSASSAPKCS1_5V1; -``` diff --git a/endpoint.go b/endpoint.go deleted file mode 100644 index d3da95e..0000000 --- a/endpoint.go +++ /dev/null @@ -1,185 +0,0 @@ -package stfe - -import ( -	"context" -	"fmt" -	"strings" - -	"net/http" - -	"github.com/golang/glog" -	"github.com/google/trillian" -	"github.com/system-transparency/stfe/types" -) - -// Endpoint is a named HTTP API endpoint -type Endpoint string - -const ( -	EndpointAddEntry            = Endpoint("add-entry") -	EndpointAddCosignature      = Endpoint("add-cosignature") -	EndpointGetLatestSth        = Endpoint("get-latest-sth") -	EndpointGetStableSth        = Endpoint("get-stable-sth") -	EndpointGetCosignedSth      = Endpoint("get-cosigned-sth") -	EndpointGetProofByHash      = Endpoint("get-proof-by-hash") -	EndpointGetConsistencyProof = Endpoint("get-consistency-proof") -	EndpointGetEntries          = Endpoint("get-entries") -) - -// Path joins a number of components to form a full endpoint path, e.g., base -// ("example.com"), prefix ("st/v1"), and the endpoint itself ("get-sth"). -func (e Endpoint) Path(components ...string) string { -	return strings.Join(append(components, string(e)), "/") -} - -func addEntry(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) { -	glog.V(3).Info("handling add-entry request") -	item, err := i.LogParameters.parseAddEntryV1Request(r) -	if err != nil { -		return http.StatusBadRequest, fmt.Errorf("parseAddEntryV1Request: %v", err) -	} -	leaf, err := types.Marshal(*item) -	if err != nil { -		return http.StatusInternalServerError, fmt.Errorf("Marshal: %v", err) // should never happen -	} -	trsp, err := i.Client.QueueLeaf(ctx, &trillian.QueueLeafRequest{ -		LogId: i.LogParameters.TreeId, -		Leaf: &trillian.LogLeaf{ -			LeafValue: leaf, -			ExtraData: nil, -		}, -	}) -	if errInner := checkQueueLeaf(trsp, err); errInner != nil { -		return http.StatusInternalServerError, fmt.Errorf("bad QueueLeafResponse: %v", errInner) -	} -	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") -	costh, err := i.LogParameters.parseAddCosignatureV1Request(r) -	if err != nil { -		return http.StatusBadRequest, err -	} -	if err := i.SthSource.AddCosignature(ctx, costh); err != nil { -		return http.StatusBadRequest, err -	} -	return http.StatusOK, nil -} - -func getLatestSth(ctx context.Context, i *Instance, w http.ResponseWriter, _ *http.Request) (int, error) { -	glog.V(3).Info("handling get-latest-sth request") -	sth, err := i.SthSource.Latest(ctx) -	if err != nil { -		return http.StatusInternalServerError, fmt.Errorf("Latest: %v", err) -	} -	if err := writeOctetResponse(w, *sth); err != nil { -		return http.StatusInternalServerError, fmt.Errorf("writeOctetResponse: %v", err) -	} -	return http.StatusOK, nil -} - -func getStableSth(ctx context.Context, i *Instance, w http.ResponseWriter, _ *http.Request) (int, error) { -	glog.V(3).Info("handling get-stable-sth request") -	sth, err := i.SthSource.Stable(ctx) -	if err != nil { -		return http.StatusInternalServerError, fmt.Errorf("Latest: %v", err) -	} -	if err := writeOctetResponse(w, *sth); err != nil { -		return http.StatusInternalServerError, fmt.Errorf("writeOctetResponse: %v", err) -	} -	return http.StatusOK, nil -} - -func getCosignedSth(ctx context.Context, i *Instance, w http.ResponseWriter, _ *http.Request) (int, error) { -	glog.V(3).Info("handling get-cosigned-sth request") -	costh, err := i.SthSource.Cosigned(ctx) -	if err != nil { -		return http.StatusInternalServerError, fmt.Errorf("Cosigned: %v", err) -	} -	if err := writeOctetResponse(w, *costh); err != nil { -		return http.StatusInternalServerError, fmt.Errorf("writeOctetResponse: %v", 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.LogParameters.parseGetConsistencyProofV1Request(r) -	if err != nil { -		return http.StatusBadRequest, err -	} - -	trsp, err := i.Client.GetConsistencyProof(ctx, &trillian.GetConsistencyProofRequest{ -		LogId:          i.LogParameters.TreeId, -		FirstTreeSize:  int64(req.First), -		SecondTreeSize: int64(req.Second), -	}) -	if errInner := checkGetConsistencyProof(i.LogParameters, trsp, err); errInner != nil { -		return http.StatusInternalServerError, fmt.Errorf("bad GetConsistencyProofResponse: %v", errInner) -	} - -	if err := writeOctetResponse(w, *types.NewConsistencyProofV1(i.LogParameters.LogId, req.First, req.Second, NewNodePathFromHashPath(trsp.Proof.Hashes))); err != nil { -		return http.StatusInternalServerError, fmt.Errorf("writeOctetResponse: %v", err) -	} -	return http.StatusOK, nil -} - -func getProofByHash(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.LogParameters.parseGetProofByHashV1Request(r) -	if err != nil { -		return http.StatusBadRequest, err -	} - -	trsp, err := i.Client.GetInclusionProofByHash(ctx, &trillian.GetInclusionProofByHashRequest{ -		LogId:           i.LogParameters.TreeId, -		LeafHash:        req.Hash[:], -		TreeSize:        int64(req.TreeSize), -		OrderBySequence: true, -	}) -	if errInner := checkGetInclusionProofByHash(i.LogParameters, trsp, err); errInner != nil { -		return http.StatusInternalServerError, fmt.Errorf("bad GetInclusionProofByHashResponse: %v", errInner) -	} - -	if err := writeOctetResponse(w, *types.NewInclusionProofV1(i.LogParameters.LogId, req.TreeSize, uint64(trsp.Proof[0].LeafIndex), NewNodePathFromHashPath(trsp.Proof[0].Hashes))); err != nil { -		return http.StatusInternalServerError, fmt.Errorf("writeOctetResponse: %v", err) -	} -	return http.StatusOK, nil -} - -func getEntries(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) { -	glog.V(3).Info("handling get-entries request") -	req, err := i.LogParameters.parseGetEntriesV1Request(r) -	if err != nil { -		return http.StatusBadRequest, err -	} - -	trsp, err := i.Client.GetLeavesByRange(ctx, &trillian.GetLeavesByRangeRequest{ -		LogId:      i.LogParameters.TreeId, -		StartIndex: int64(req.Start), -		Count:      int64(req.End-req.Start) + 1, -	}) -	if errInner := checkGetLeavesByRange(req, trsp, err); errInner != nil { -		return http.StatusInternalServerError, fmt.Errorf("checkGetLeavesByRangeResponse: %v", errInner) // there is one StatusBadRequest in here tho.. -	} - -	if rsp, err := NewStItemListFromLeaves(trsp.Leaves); err != nil { -		return http.StatusInternalServerError, fmt.Errorf("NewStItemListFromLeaves: %v", err) // should never happen -	} else if err := writeOctetResponse(w, *rsp); err != nil { -		return http.StatusInternalServerError, fmt.Errorf("writeOctetResponse: %v", err) -	} -	return http.StatusOK, nil -} - -func writeOctetResponse(w http.ResponseWriter, i interface{}) error { -	b, err := types.Marshal(i) -	if err != nil { -		return fmt.Errorf("Marshal: %v", err) -	} -	w.Header().Set("Content-Type", "application/octet-stream") -	if _, err := w.Write(b); err != nil { -		return fmt.Errorf("Write: %v", err) -	} -	return nil -} diff --git a/endpoint_test.go b/endpoint_test.go deleted file mode 100644 index aab2c54..0000000 --- a/endpoint_test.go +++ /dev/null @@ -1,529 +0,0 @@ -package stfe - -import ( -	"bytes" -	"context" -	"fmt" -	"reflect" -	"testing" - -	"net/http" -	"net/http/httptest" - -	"github.com/golang/mock/gomock" -	cttestdata "github.com/google/certificate-transparency-go/trillian/testdata" -	"github.com/google/trillian" -	"github.com/system-transparency/stfe/testdata" -	"github.com/system-transparency/stfe/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) -			} -		}() -	} -} - -func TestEndpointPath(t *testing.T) { -	base, prefix, proto := "http://example.com", "test", "st/v1" -	for _, table := range []struct { -		endpoint Endpoint -		want     string -	}{ -		{ -			endpoint: EndpointAddEntry, -			want:     "http://example.com/test/st/v1/add-entry", -		}, -		{ -			endpoint: EndpointAddCosignature, -			want:     "http://example.com/test/st/v1/add-cosignature", -		}, -		{ -			endpoint: EndpointGetLatestSth, -			want:     "http://example.com/test/st/v1/get-latest-sth", -		}, -		{ -			endpoint: EndpointGetStableSth, -			want:     "http://example.com/test/st/v1/get-stable-sth", -		}, -		{ -			endpoint: EndpointGetCosignedSth, -			want:     "http://example.com/test/st/v1/get-cosigned-sth", -		}, -		{ -			endpoint: EndpointGetConsistencyProof, -			want:     "http://example.com/test/st/v1/get-consistency-proof", -		}, -		{ -			endpoint: EndpointGetProofByHash, -			want:     "http://example.com/test/st/v1/get-proof-by-hash", -		}, -		{ -			endpoint: EndpointGetEntries, -			want:     "http://example.com/test/st/v1/get-entries", -		}, -	} { -		if got, want := table.endpoint.Path(base+"/"+prefix+"/"+proto), table.want; got != want { -			t.Errorf("got endpoint\n%s\n\tbut wanted\n%s\n\twith one component", got, want) -		} -		if got, want := table.endpoint.Path(base, prefix, proto), table.want; got != want { -			t.Errorf("got endpoint\n%s\n\tbut wanted\n%s\n\tmultiple components", got, want) -		} -	} -} - -// 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{}") -} @@ -181,6 +181,7 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw  github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=  github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=  github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=  github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=  github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=  github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -461,6 +462,7 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U  golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=  golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=  golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=  golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=  golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=  golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -645,6 +647,7 @@ golang.org/x/tools v0.0.0-20200630154851-b2d8b0336632/go.mod h1:EkVYQZoAsY45+roY  golang.org/x/tools v0.0.0-20200706234117-b22de6825cf7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=  golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=  golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=  golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=  google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=  google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= diff --git a/instance.go b/instance.go deleted file mode 100644 index 67336f8..0000000 --- a/instance.go +++ /dev/null @@ -1,74 +0,0 @@ -package stfe - -import ( -	"context" -	"fmt" -	"time" - -	"net/http" - -	"github.com/golang/glog" -	"github.com/google/trillian" -) - -// Instance is an instance of the system transparency front-end -type Instance struct { -	Client        trillian.TrillianLogClient -	LogParameters *LogParameters -	SthSource     SthSource -} - -// Handlers returns a list of STFE handlers -func (i *Instance) Handlers() []Handler { -	return []Handler{ -		Handler{Instance: i, Handler: addEntry, Endpoint: EndpointAddEntry, Method: http.MethodPost}, -		Handler{Instance: i, Handler: addCosignature, Endpoint: EndpointAddCosignature, Method: http.MethodPost}, -		Handler{Instance: i, Handler: getLatestSth, Endpoint: EndpointGetLatestSth, Method: http.MethodGet}, -		Handler{Instance: i, Handler: getStableSth, Endpoint: EndpointGetStableSth, Method: http.MethodGet}, -		Handler{Instance: i, Handler: getCosignedSth, Endpoint: EndpointGetCosignedSth, Method: http.MethodGet}, -		Handler{Instance: i, Handler: getProofByHash, Endpoint: EndpointGetProofByHash, Method: http.MethodPost}, -		Handler{Instance: i, Handler: getConsistencyProof, Endpoint: EndpointGetConsistencyProof, Method: http.MethodPost}, -		Handler{Instance: i, Handler: getEntries, Endpoint: EndpointGetEntries, Method: http.MethodPost}, -	} -} - -// 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 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 { -	return h.Endpoint.Path("", h.Instance.LogParameters.Prefix) -} - -// 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.LogParameters.LogIdStr, string(a.Endpoint), fmt.Sprintf("%d", statusCode)) -		latency.Observe(time.Now().Sub(now).Seconds(), a.Instance.LogParameters.LogIdStr, string(a.Endpoint), fmt.Sprintf("%d", statusCode)) -	}() -	reqcnt.Inc(a.Instance.LogParameters.LogIdStr, string(a.Endpoint)) - -	ctx, cancel := context.WithDeadline(r.Context(), now.Add(a.Instance.LogParameters.Deadline)) -	defer cancel() - -	if r.Method != a.Method { -		glog.Warningf("%s/%s: got HTTP %s, wanted HTTP %s", a.Instance.LogParameters.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.LogParameters.Prefix, a.Endpoint, err) -		http.Error(w, "", statusCode) -	} -} diff --git a/instance_test.go b/instance_test.go deleted file mode 100644 index de539a1..0000000 --- a/instance_test.go +++ /dev/null @@ -1,159 +0,0 @@ -package stfe - -import ( -	"crypto" -	"testing" - -	"net/http" -	"net/http/httptest" - -	"github.com/golang/mock/gomock" -	"github.com/google/certificate-transparency-go/trillian/mockclient" -	"github.com/system-transparency/stfe/testdata" -	"github.com/system-transparency/stfe/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/log_parameters.go b/log_parameters.go deleted file mode 100644 index a2a2d7a..0000000 --- a/log_parameters.go +++ /dev/null @@ -1,75 +0,0 @@ -package stfe - -import ( -	"crypto" -	"fmt" -	"time" - -	"github.com/system-transparency/stfe/types" -) - -// LogParameters is a collection of log parameters -type LogParameters struct { -	LogId           *types.Namespace     // log identifier -	LogIdBytes      []byte               // serialized log id -	LogIdStr        string               // serialized log id (hex) -	TreeId          int64                // used internally by Trillian -	Prefix          string               // e.g., "test" for <base>/test -	MaxRange        int64                // max entries per get-entries request -	SubmitterPolicy bool                 // if we have a submitter policy (true means that namespaces must be registered) -	WitnessPolicy   bool                 // if we have a witness policy (true means that namespaces must be registered) -	Submitters      *types.NamespacePool // trusted submitters -	Witnesses       *types.NamespacePool // trusted witnesses -	Deadline        time.Duration        // gRPC deadline -	Interval        time.Duration        // cosigning sth frequency -	HashType        crypto.Hash          // hash function used by Trillian -	Signer          crypto.Signer        // access to Ed25519 private key -} - -// NewLogParameters creates newly initialized log parameters -func NewLogParameters(signer crypto.Signer, logId *types.Namespace, treeId int64, prefix string, submitters, witnesses *types.NamespacePool, maxRange int64, interval, deadline time.Duration, submitterPolicy, witnessPolicy bool) (*LogParameters, error) { -	logIdBytes, err := types.Marshal(*logId) -	if err != nil { -		return nil, fmt.Errorf("Marshal failed for log identifier: %v", err) -	} -	return &LogParameters{ -		LogId:           logId, -		LogIdBytes:      logIdBytes, -		LogIdStr:        fmt.Sprintf("%x", logIdBytes), -		TreeId:          treeId, -		Prefix:          prefix, -		MaxRange:        maxRange, -		SubmitterPolicy: submitterPolicy, -		WitnessPolicy:   witnessPolicy, -		Submitters:      submitters, -		Witnesses:       witnesses, -		Deadline:        deadline, -		Interval:        interval, -		HashType:        crypto.SHA256, -		Signer:          signer, -	}, nil -} - -// SignTreeHeadV1 signs a TreeHeadV1 structure -func (lp *LogParameters) SignTreeHeadV1(th *types.TreeHeadV1) (*types.StItem, error) { -	serialized, err := types.Marshal(*th) -	if err != nil { -		return nil, fmt.Errorf("Marshal failed for TreeHeadV1: %v", err) -	} -	sig, err := lp.Signer.Sign(nil, serialized, crypto.Hash(0)) -	if err != nil { -		return nil, fmt.Errorf("Sign failed: %v", err) -	} -	lastSthTimestamp.Set(float64(time.Now().Unix()), lp.LogIdStr) -	lastSthSize.Set(float64(th.TreeSize), lp.LogIdStr) -	return &types.StItem{ -		Format: types.StFormatSignedTreeHeadV1, -		SignedTreeHeadV1: &types.SignedTreeHeadV1{ -			TreeHead: *th, -			Signature: types.SignatureV1{ -				Namespace: *lp.LogId, -				Signature: sig, -			}, -		}, -	}, nil -} diff --git a/log_parameters_test.go b/log_parameters_test.go deleted file mode 100644 index 88e83ad..0000000 --- a/log_parameters_test.go +++ /dev/null @@ -1,99 +0,0 @@ -package stfe - -import ( -	"crypto" -	"fmt" -	"reflect" -	"testing" - -	cttestdata "github.com/google/certificate-transparency-go/trillian/testdata" -	"github.com/system-transparency/stfe/testdata" -	"github.com/system-transparency/stfe/types" -) - -// newLogParameters must create new log parameters with an optional log signer -// based on the parameters in "github.com/system-transparency/stfe/testdata". -// The log's namespace is initialized with testdata.LogEd25519Vk, the submmiter -// namespace list is initialized with testdata.SubmmiterEd25519, and the witness -// namespace list is initialized with testdata.WitnessEd25519Vk.  The log's -// submitter and witness policies are set to reject unregistered namespace. -func newLogParameters(t *testing.T, signer crypto.Signer) *LogParameters { -	t.Helper() -	logId := testdata.NewNamespace(t, testdata.Ed25519VkLog) -	witnessPool := testdata.NewNamespacePool(t, []*types.Namespace{ -		testdata.NewNamespace(t, testdata.Ed25519VkWitness), -	}) -	submitPool := testdata.NewNamespacePool(t, []*types.Namespace{ -		testdata.NewNamespace(t, testdata.Ed25519VkSubmitter), -	}) -	lp, err := NewLogParameters(signer, logId, testdata.TreeId, testdata.Prefix, submitPool, witnessPool, testdata.MaxRange, testdata.Interval, testdata.Deadline, true, true) -	if err != nil { -		t.Fatalf("must create new log parameters: %v", err) -	} -	return lp -} - -func TestNewLogParameters(t *testing.T) { -	for _, table := range []struct { -		description string -		logId       *types.Namespace -		wantErr     bool -	}{ -		{ -			description: "invalid: cannot marshal log id", -			logId: &types.Namespace{ -				Format: types.NamespaceFormatReserved, -			}, -			wantErr: true, -		}, -		{ -			description: "valid", -			logId:       testdata.NewNamespace(t, testdata.Ed25519VkLog), -		}, -	} { -		_, err := NewLogParameters(nil, table.logId, testdata.TreeId, testdata.Prefix, nil, nil, testdata.MaxRange, testdata.Interval, testdata.Deadline, true, true) -		if got, want := err != nil, table.wantErr; got != want { -			t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) -		} -	} -} - -func TestSignTreeHeadV1(t *testing.T) { -	for _, table := range []struct { -		description string -		th          *types.TreeHeadV1 -		signer      crypto.Signer -		wantErr     bool -		wantSth     *types.StItem -	}{ -		{ -			description: "invalid: marshal failure", -			th:          types.NewTreeHeadV1(testdata.Timestamp, testdata.TreeSize, nil, testdata.Extension), -			wantErr:     true, -		}, -		{ -			description: "invalid: signature failure", -			th:          types.NewTreeHeadV1(testdata.Timestamp, testdata.TreeSize, testdata.NodeHash, testdata.Extension), -			signer:      cttestdata.NewSignerWithErr(nil, fmt.Errorf("signer failed")), -			wantErr:     true, -		}, -		{ -			description: "valid", -			th:          testdata.DefaultTh(t), -			signer:      cttestdata.NewSignerWithFixedSig(nil, testdata.Signature), -			wantSth:     testdata.DefaultSth(t, testdata.Ed25519VkLog), -		}, -	} { -		sth, err := newLogParameters(t, table.signer).SignTreeHeadV1(table.th) -		if got, want := err != nil, table.wantErr; got != want { -			t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) -		} -		if err != nil { -			continue -		} - -		if got, want := sth, table.wantSth; !reflect.DeepEqual(got, want) { -			t.Errorf("got \n%v\n\tbut wanted\n%v\n\tin test %q", got, want, table.description) -		} -	} -} diff --git a/metric.go b/metric.go deleted file mode 100644 index 7e3e8b2..0000000 --- a/metric.go +++ /dev/null @@ -1,23 +0,0 @@ -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/endpoint.go b/pkg/instance/endpoint.go new file mode 100644 index 0000000..5085c49 --- /dev/null +++ b/pkg/instance/endpoint.go @@ -0,0 +1,122 @@ +package stfe + +import ( +	"context" +	"net/http" + +	"github.com/golang/glog" +) + +func addLeaf(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) { +	glog.V(3).Info("handling add-entry request") +	req, err := i.leafRequestFromHTTP(r) +	if err != nil { +		return http.StatusBadRequest, err +	} +	if err := i.Client.AddLeaf(ctx, req); err != nil { +		return http.StatusInternalServerError, err +	} +	return http.StatusOK, nil +} + +func addCosignature(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) { +	glog.V(3).Info("handling add-cosignature request") +	req, err := i.cosignatureRequestFromHTTP(r) +	if err != nil { +		return http.StatusBadRequest, err +	} +	vk := i.Witnesses[*req.KeyHash] +	if err := i.Stateman.AddCosignature(ctx, &vk, req.Signature); err != nil { +		return http.StatusBadRequest, err +	} +	return http.StatusOK, nil +} + +func getTreeHeadLatest(ctx context.Context, i *Instance, w http.ResponseWriter, _ *http.Request) (int, error) { +	glog.V(3).Info("handling get-tree-head-latest request") +	sth, err := i.Stateman.Latest(ctx) +	if err != nil { +		return http.StatusInternalServerError, err +	} +	if err := sth.MarshalASCII(w); err != nil { +		return http.StatusInternalServerError, err +	} +	return http.StatusOK, nil +} + +func getTreeHeadToSign(ctx context.Context, i *Instance, w http.ResponseWriter, _ *http.Request) (int, error) { +	glog.V(3).Info("handling get-tree-head-to-sign request") +	sth, err := i.Stateman.ToSign(ctx) +	if err != nil { +		return http.StatusInternalServerError, err +	} +	if err := sth.MarshalASCII(w); err != nil { +		return http.StatusInternalServerError, err +	} +	return http.StatusOK, nil +} + +func getTreeHeadCosigned(ctx context.Context, i *Instance, w http.ResponseWriter, _ *http.Request) (int, error) { +	glog.V(3).Info("handling get-tree-head-cosigned request") +	sth, err := i.Stateman.Cosigned(ctx) +	if err != nil { +		return http.StatusInternalServerError, err +	} +	if err := sth.MarshalASCII(w); err != nil { +		return http.StatusInternalServerError, err +	} +	return http.StatusOK, nil +} + +func getConsistencyProof(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) { +	glog.V(3).Info("handling get-consistency-proof request") +	req, err := i.consistencyProofRequestFromHTTP(r) +	if err != nil { +		return http.StatusBadRequest, err +	} + +	proof, err := i.Client.GetConsistencyProof(ctx, req) +	if err != nil { +		return http.StatusInternalServerError, err +	} +	if err := proof.MarshalASCII(w); err != nil { +		return http.StatusInternalServerError, err +	} +	return http.StatusOK, nil +} + +func getInclusionProof(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) { +	glog.V(3).Info("handling get-proof-by-hash request") +	req, err := i.inclusionProofRequestFromHTTP(r) +	if err != nil { +		return http.StatusBadRequest, err +	} + +	proof, err := i.Client.GetInclusionProof(ctx, req) +	if err != nil { +		return http.StatusInternalServerError, err +	} +	if err := proof.MarshalASCII(w); err != nil { +		return http.StatusInternalServerError, err +	} +	return http.StatusOK, nil +} + +func getLeaves(ctx context.Context, i *Instance, w http.ResponseWriter, r *http.Request) (int, error) { +	glog.V(3).Info("handling get-leaves request") +	req, err := i.leavesRequestFromHTTP(r) +	if err != nil { +		return http.StatusBadRequest, err +	} + +	leaves, err := i.Client.GetLeaves(ctx, req) +	if err != nil { +		return http.StatusInternalServerError, err +	} +	for _, leaf := range *leaves { +		if err := leaf.MarshalASCII(w); err != nil { +			return http.StatusInternalServerError, err +		} +	} +	return http.StatusOK, nil +} diff --git a/pkg/instance/endpoint_test.go b/pkg/instance/endpoint_test.go new file mode 100644 index 0000000..efcd4c0 --- /dev/null +++ b/pkg/instance/endpoint_test.go @@ -0,0 +1,432 @@ +package stfe + +import ( +	"bytes" +	"encoding/hex" +	"fmt" +	"io" +	"net/http" +	"net/http/httptest" +	"testing" + +	"github.com/golang/mock/gomock" +	"github.com/system-transparency/stfe/pkg/mocks" +	"github.com/system-transparency/stfe/pkg/types" +) + +var ( +	testWitVK  = [types.VerificationKeySize]byte{} +	testConfig = Config{ +		LogID:    hex.EncodeToString(types.Hash([]byte("logid"))[:]), +		TreeID:   0, +		Prefix:   "testonly", +		MaxRange: 3, +		Deadline: 10, +		Interval: 10, +		Witnesses: map[[types.HashSize]byte][types.VerificationKeySize]byte{ +			*types.Hash(testWitVK[:]): testWitVK, +		}, +	} +	testSTH = &types.SignedTreeHead{ +		TreeHead: types.TreeHead{ +			Timestamp: 0, +			TreeSize:  0, +			RootHash:  types.Hash(nil), +		}, +		SigIdent: []*types.SigIdent{ +			&types.SigIdent{ +				Signature: &[types.SignatureSize]byte{}, +				KeyHash:   &[types.HashSize]byte{}, +			}, +		}, +	} +) + +func mustHandle(t *testing.T, i Instance, e types.Endpoint) Handler { +	for _, handler := range i.Handlers() { +		if handler.Endpoint == e { +			return handler +		} +	} +	t.Fatalf("must handle endpoint: %v", e) +	return Handler{} +} + +func TestAddLeaf(t *testing.T) { +	buf := func() io.Reader { +		// A valid leaf request that was created manually +		return bytes.NewBufferString(fmt.Sprintf( +			"%s%s%s%s"+"%s%s%s%s"+"%s%s%s%s"+"%s%s%s%s"+"%s%s%s%s", +			types.ShardHint, types.Delim, "0", types.EOL, +			types.Checksum, types.Delim, "0000000000000000000000000000000000000000000000000000000000000000", types.EOL, +			types.SignatureOverMessage, types.Delim, "4cb410a4d48f52f761a7c01abcc28fd71811b84ded5403caed5e21b374f6aac9637cecd36828f17529fd503413d30ab66d7bb37a31dbf09a90d23b9241c45009", types.EOL, +			types.VerificationKey, types.Delim, "f2b7a00b625469d32502e06e8b7fad1ef258d4ad0c6cd87b846142ab681957d5", types.EOL, +			types.DomainHint, types.Delim, "example.com", types.EOL, +		)) +	} +	for _, table := range []struct { +		description string +		ascii       io.Reader // buffer used to populate HTTP request +		expect      bool      // set if a mock answer is expected +		err         error     // error from Trillian client +		wantCode    int       // HTTP status ok +	}{ +		{ +			description: "invalid: bad request (parser error)", +			ascii:       bytes.NewBufferString("key=value\n"), +			wantCode:    http.StatusBadRequest, +		}, +		{ +			description: "invalid: bad request (signature error)", +			ascii: bytes.NewBufferString(fmt.Sprintf( +				"%s%s%s%s"+"%s%s%s%s"+"%s%s%s%s"+"%s%s%s%s"+"%s%s%s%s", +				types.ShardHint, types.Delim, "1", types.EOL, +				types.Checksum, types.Delim, "1111111111111111111111111111111111111111111111111111111111111111", types.EOL, +				types.SignatureOverMessage, types.Delim, "4cb410a4d48f52f761a7c01abcc28fd71811b84ded5403caed5e21b374f6aac9637cecd36828f17529fd503413d30ab66d7bb37a31dbf09a90d23b9241c45009", types.EOL, +				types.VerificationKey, types.Delim, "f2b7a00b625469d32502e06e8b7fad1ef258d4ad0c6cd87b846142ab681957d5", types.EOL, +				types.DomainHint, types.Delim, "example.com", types.EOL, +			)), +			wantCode: http.StatusBadRequest, +		}, +		{ +			description: "invalid: backend failure", +			ascii:       buf(), +			expect:      true, +			err:         fmt.Errorf("something went wrong"), +			wantCode:    http.StatusInternalServerError, +		}, +		{ +			description: "valid", +			ascii:       buf(), +			expect:      true, +			wantCode:    http.StatusOK, +		}, +	} { +		// Run deferred functions at the end of each iteration +		func() { +			ctrl := gomock.NewController(t) +			defer ctrl.Finish() +			client := mocks.NewMockClient(ctrl) +			if table.expect { +				client.EXPECT().AddLeaf(gomock.Any(), gomock.Any()).Return(table.err) +			} +			i := Instance{ +				Config: testConfig, +				Client: client, +			} + +			// Create HTTP request +			url := types.EndpointAddLeaf.Path("http://example.com", i.Prefix) +			req, err := http.NewRequest("POST", url, table.ascii) +			if err != nil { +				t.Fatalf("must create http request: %v", err) +			} + +			// Run HTTP request +			w := httptest.NewRecorder() +			mustHandle(t, i, types.EndpointAddLeaf).ServeHTTP(w, req) +			if got, want := w.Code, table.wantCode; got != want { +				t.Errorf("got HTTP status code %v but wanted %v in test %q", got, want, table.description) +			} +		}() +	} +} + +func TestAddCosignature(t *testing.T) { +	buf := func() io.Reader { +		return bytes.NewBufferString(fmt.Sprintf( +			"%s%s%x%s"+"%s%s%x%s", +			types.Signature, types.Delim, make([]byte, types.SignatureSize), types.EOL, +			types.KeyHash, types.Delim, *types.Hash(testWitVK[:]), types.EOL, +		)) +	} +	for _, table := range []struct { +		description string +		ascii       io.Reader // buffer used to populate HTTP request +		expect      bool      // set if a mock answer is expected +		err         error     // error from Trillian client +		wantCode    int       // HTTP status ok +	}{ +		{ +			description: "invalid: bad request (parser error)", +			ascii:       bytes.NewBufferString("key=value\n"), +			wantCode:    http.StatusBadRequest, +		}, +		{ +			description: "invalid: bad request (unknown witness)", +			ascii: bytes.NewBufferString(fmt.Sprintf( +				"%s%s%x%s"+"%s%s%x%s", +				types.Signature, types.Delim, make([]byte, types.SignatureSize), types.EOL, +				types.KeyHash, types.Delim, *types.Hash(testWitVK[1:]), types.EOL, +			)), +			wantCode: http.StatusBadRequest, +		}, +		{ +			description: "invalid: backend failure", +			ascii:       buf(), +			expect:      true, +			err:         fmt.Errorf("something went wrong"), +			wantCode:    http.StatusBadRequest, +		}, +		{ +			description: "valid", +			ascii:       buf(), +			expect:      true, +			wantCode:    http.StatusOK, +		}, +	} { +		// Run deferred functions at the end of each iteration +		func() { +			ctrl := gomock.NewController(t) +			defer ctrl.Finish() +			stateman := mocks.NewMockStateManager(ctrl) +			if table.expect { +				stateman.EXPECT().AddCosignature(gomock.Any(), gomock.Any(), gomock.Any()).Return(table.err) +			} +			i := Instance{ +				Config:   testConfig, +				Stateman: stateman, +			} + +			// Create HTTP request +			url := types.EndpointAddCosignature.Path("http://example.com", i.Prefix) +			req, err := http.NewRequest("POST", url, table.ascii) +			if err != nil { +				t.Fatalf("must create http request: %v", err) +			} + +			// Run HTTP request +			w := httptest.NewRecorder() +			mustHandle(t, i, types.EndpointAddCosignature).ServeHTTP(w, req) +			if got, want := w.Code, table.wantCode; got != want { +				t.Errorf("got HTTP status code %v but wanted %v in test %q", got, want, table.description) +			} +		}() +	} +} + +func TestGetTreeHeadLatest(t *testing.T) { +	for _, table := range []struct { +		description string +		expect      bool                  // set if a mock answer is expected +		rsp         *types.SignedTreeHead // signed tree head from Trillian client +		err         error                 // error from Trillian client +		wantCode    int                   // HTTP status ok +	}{ +		{ +			description: "invalid: backend failure", +			expect:      true, +			err:         fmt.Errorf("something went wrong"), +			wantCode:    http.StatusInternalServerError, +		}, +		{ +			description: "valid", +			expect:      true, +			rsp:         testSTH, +			wantCode:    http.StatusOK, +		}, +	} { +		// Run deferred functions at the end of each iteration +		func() { +			ctrl := gomock.NewController(t) +			defer ctrl.Finish() +			stateman := mocks.NewMockStateManager(ctrl) +			if table.expect { +				stateman.EXPECT().Latest(gomock.Any()).Return(table.rsp, table.err) +			} +			i := Instance{ +				Config:   testConfig, +				Stateman: stateman, +			} + +			// Create HTTP request +			url := types.EndpointGetTreeHeadLatest.Path("http://example.com", i.Prefix) +			req, err := http.NewRequest("GET", url, nil) +			if err != nil { +				t.Fatalf("must create http request: %v", err) +			} + +			// Run HTTP request +			w := httptest.NewRecorder() +			mustHandle(t, i, types.EndpointGetTreeHeadLatest).ServeHTTP(w, req) +			if got, want := w.Code, table.wantCode; got != want { +				t.Errorf("got HTTP status code %v but wanted %v in test %q", got, want, table.description) +			} +		}() +	} +} + +func TestGetTreeToSign(t *testing.T) { +	for _, table := range []struct { +		description string +		expect      bool                  // set if a mock answer is expected +		rsp         *types.SignedTreeHead // signed tree head from Trillian client +		err         error                 // error from Trillian client +		wantCode    int                   // HTTP status ok +	}{ +		{ +			description: "invalid: backend failure", +			expect:      true, +			err:         fmt.Errorf("something went wrong"), +			wantCode:    http.StatusInternalServerError, +		}, +		{ +			description: "valid", +			expect:      true, +			rsp:         testSTH, +			wantCode:    http.StatusOK, +		}, +	} { +		// Run deferred functions at the end of each iteration +		func() { +			ctrl := gomock.NewController(t) +			defer ctrl.Finish() +			stateman := mocks.NewMockStateManager(ctrl) +			if table.expect { +				stateman.EXPECT().ToSign(gomock.Any()).Return(table.rsp, table.err) +			} +			i := Instance{ +				Config:   testConfig, +				Stateman: stateman, +			} + +			// Create HTTP request +			url := types.EndpointGetTreeHeadToSign.Path("http://example.com", i.Prefix) +			req, err := http.NewRequest("GET", url, nil) +			if err != nil { +				t.Fatalf("must create http request: %v", err) +			} + +			// Run HTTP request +			w := httptest.NewRecorder() +			mustHandle(t, i, types.EndpointGetTreeHeadToSign).ServeHTTP(w, req) +			if got, want := w.Code, table.wantCode; got != want { +				t.Errorf("got HTTP status code %v but wanted %v in test %q", got, want, table.description) +			} +		}() +	} +} + +func TestGetTreeCosigned(t *testing.T) { +	for _, table := range []struct { +		description string +		expect      bool                  // set if a mock answer is expected +		rsp         *types.SignedTreeHead // signed tree head from Trillian client +		err         error                 // error from Trillian client +		wantCode    int                   // HTTP status ok +	}{ +		{ +			description: "invalid: backend failure", +			expect:      true, +			err:         fmt.Errorf("something went wrong"), +			wantCode:    http.StatusInternalServerError, +		}, +		{ +			description: "valid", +			expect:      true, +			rsp:         testSTH, +			wantCode:    http.StatusOK, +		}, +	} { +		// Run deferred functions at the end of each iteration +		func() { +			ctrl := gomock.NewController(t) +			defer ctrl.Finish() +			stateman := mocks.NewMockStateManager(ctrl) +			if table.expect { +				stateman.EXPECT().Cosigned(gomock.Any()).Return(table.rsp, table.err) +			} +			i := Instance{ +				Config:   testConfig, +				Stateman: stateman, +			} + +			// Create HTTP request +			url := types.EndpointGetTreeHeadCosigned.Path("http://example.com", i.Prefix) +			req, err := http.NewRequest("GET", url, nil) +			if err != nil { +				t.Fatalf("must create http request: %v", err) +			} + +			// Run HTTP request +			w := httptest.NewRecorder() +			mustHandle(t, i, types.EndpointGetTreeHeadCosigned).ServeHTTP(w, req) +			if got, want := w.Code, table.wantCode; got != want { +				t.Errorf("got HTTP status code %v but wanted %v in test %q", got, want, table.description) +			} +		}() +	} +} + +func TestGetConsistencyProof(t *testing.T) { +	buf := func(oldSize, newSize int) io.Reader { +		return bytes.NewBufferString(fmt.Sprintf( +			"%s%s%d%s"+"%s%s%d%s", +			types.OldSize, types.Delim, oldSize, types.EOL, +			types.NewSize, types.Delim, newSize, types.EOL, +		)) +	} +	// values in testProof are not relevant for the test, just need a path +	testProof := &types.ConsistencyProof{ +		OldSize: 1, +		NewSize: 2, +		Path: []*[types.HashSize]byte{ +			types.Hash(nil), +		}, +	} +	for _, table := range []struct { +		description string +		ascii       io.Reader               // buffer used to populate HTTP request +		expect      bool                    // set if a mock answer is expected +		rsp         *types.ConsistencyProof // consistency proof from Trillian client +		err         error                   // error from Trillian client +		wantCode    int                     // HTTP status ok +	}{ +		{ +			description: "invalid: bad request (parser error)", +			ascii:       bytes.NewBufferString("key=value\n"), +			wantCode:    http.StatusBadRequest, +		}, +		{ +			description: "valid", +			ascii:       buf(1, 2), +			expect:      true, +			rsp:         testProof, +			wantCode:    http.StatusOK, +		}, +	} { +		// Run deferred functions at the end of each iteration +		func() { +			ctrl := gomock.NewController(t) +			defer ctrl.Finish() +			client := mocks.NewMockClient(ctrl) +			if table.expect { +				client.EXPECT().GetConsistencyProof(gomock.Any(), gomock.Any()).Return(table.rsp, table.err) +			} +			i := Instance{ +				Config: testConfig, +				Client: client, +			} + +			// Create HTTP request +			url := types.EndpointGetConsistencyProof.Path("http://example.com", i.Prefix) +			req, err := http.NewRequest("POST", url, table.ascii) +			if err != nil { +				t.Fatalf("must create http request: %v", err) +			} + +			// Run HTTP request +			w := httptest.NewRecorder() +			mustHandle(t, i, types.EndpointGetConsistencyProof).ServeHTTP(w, req) +			if got, want := w.Code, table.wantCode; got != want { +				t.Errorf("got HTTP status code %v but wanted %v in test %q", got, want, table.description) +			} +		}() +	} +} + +func TestGetInclusionProof(t *testing.T) { +} + +func TestGetLeaves(t *testing.T) { +} diff --git a/pkg/instance/instance.go b/pkg/instance/instance.go new file mode 100644 index 0000000..c2fe8fa --- /dev/null +++ b/pkg/instance/instance.go @@ -0,0 +1,159 @@ +package stfe + +import ( +	"context" +	"crypto" +	"crypto/ed25519" +	"fmt" +	"net/http" +	"time" + +	"github.com/golang/glog" +	"github.com/system-transparency/stfe/pkg/state" +	"github.com/system-transparency/stfe/pkg/trillian" +	"github.com/system-transparency/stfe/pkg/types" +) + +// Config is a collection of log parameters +type Config struct { +	LogID    string        // H(public key), then hex-encoded +	TreeID   int64         // Merkle tree identifier used by Trillian +	Prefix   string        // The portion between base URL and st/v0 (may be "") +	MaxRange int64         // Maximum number of leaves per get-leaves request +	Deadline time.Duration // Deadline used for gRPC requests +	Interval time.Duration // Cosigning frequency + +	// Witnesses map trusted witness identifiers to public verification keys +	Witnesses map[[types.HashSize]byte][types.VerificationKeySize]byte +} + +// Instance is an instance of the log's front-end +type Instance struct { +	Config                      // configuration parameters +	Client   trillian.Client    // provides access to the Trillian backend +	Signer   crypto.Signer      // provides access to Ed25519 private key +	Stateman state.StateManager // coordinates access to (co)signed tree heads +} + +// Handler implements the http.Handler interface, and contains a reference +// to an STFE server instance as well as a function that uses it. +type Handler struct { +	Instance *Instance +	Endpoint types.Endpoint +	Method   string +	Handler  func(context.Context, *Instance, http.ResponseWriter, *http.Request) (int, error) +} + +// Handlers returns a list of STFE handlers +func (i *Instance) Handlers() []Handler { +	return []Handler{ +		Handler{Instance: i, Handler: addLeaf, Endpoint: types.EndpointAddLeaf, Method: http.MethodPost}, +		Handler{Instance: i, Handler: addCosignature, Endpoint: types.EndpointAddCosignature, Method: http.MethodPost}, +		Handler{Instance: i, Handler: getTreeHeadLatest, Endpoint: types.EndpointGetTreeHeadLatest, Method: http.MethodGet}, +		Handler{Instance: i, Handler: getTreeHeadToSign, Endpoint: types.EndpointGetTreeHeadToSign, Method: http.MethodGet}, +		Handler{Instance: i, Handler: getTreeHeadCosigned, Endpoint: types.EndpointGetTreeHeadCosigned, Method: http.MethodGet}, +		Handler{Instance: i, Handler: getConsistencyProof, Endpoint: types.EndpointGetConsistencyProof, Method: http.MethodPost}, +		Handler{Instance: i, Handler: getInclusionProof, Endpoint: types.EndpointGetProofByHash, Method: http.MethodPost}, +		Handler{Instance: i, Handler: getLeaves, Endpoint: types.EndpointGetLeaves, Method: http.MethodPost}, +	} +} + +// Path returns a path that should be configured for this handler +func (h Handler) Path() string { +	return h.Endpoint.Path(h.Instance.Prefix, "st", "v0") +} + +// ServeHTTP is part of the http.Handler interface +func (a Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { +	// export prometheus metrics +	var now time.Time = time.Now() +	var statusCode int +	defer func() { +		rspcnt.Inc(a.Instance.LogID, string(a.Endpoint), fmt.Sprintf("%d", statusCode)) +		latency.Observe(time.Now().Sub(now).Seconds(), a.Instance.LogID, string(a.Endpoint), fmt.Sprintf("%d", statusCode)) +	}() +	reqcnt.Inc(a.Instance.LogID, string(a.Endpoint)) + +	ctx, cancel := context.WithDeadline(r.Context(), now.Add(a.Instance.Deadline)) +	defer cancel() + +	if r.Method != a.Method { +		glog.Warningf("%s/%s: got HTTP %s, wanted HTTP %s", a.Instance.Prefix, string(a.Endpoint), r.Method, a.Method) +		http.Error(w, "", http.StatusMethodNotAllowed) +		return +	} + +	statusCode, err := a.Handler(ctx, a.Instance, w, r) +	if err != nil { +		glog.Warningf("handler error %s/%s: %v", a.Instance.Prefix, a.Endpoint, err) +		http.Error(w, fmt.Sprintf("%s%s%s%s", "Error", types.Delim, err.Error(), types.EOL), statusCode) +	} +} + +func (i *Instance) leafRequestFromHTTP(r *http.Request) (*types.LeafRequest, error) { +	var req types.LeafRequest +	if err := req.UnmarshalASCII(r.Body); err != nil { +		return nil, fmt.Errorf("UnmarshalASCII: %v", err) +	} + +	vk := ed25519.PublicKey(req.VerificationKey[:]) +	msg := req.Message.Marshal() +	sig := req.Signature[:] +	if !ed25519.Verify(vk, msg, sig) { +		return nil, fmt.Errorf("invalid signature") +	} +	// TODO: check shard hint +	// TODO: check domain hint +	return &req, nil +} + +func (i *Instance) cosignatureRequestFromHTTP(r *http.Request) (*types.CosignatureRequest, error) { +	var req types.CosignatureRequest +	if err := req.UnmarshalASCII(r.Body); err != nil { +		return nil, fmt.Errorf("unpackOctetPost: %v", err) +	} +	if _, ok := i.Witnesses[*req.KeyHash]; !ok { +		return nil, fmt.Errorf("Unknown witness: %x", req.KeyHash) +	} +	return &req, nil +} + +func (i *Instance) consistencyProofRequestFromHTTP(r *http.Request) (*types.ConsistencyProofRequest, error) { +	var req types.ConsistencyProofRequest +	if err := req.UnmarshalASCII(r.Body); err != nil { +		return nil, fmt.Errorf("UnmarshalASCII: %v", err) +	} +	if req.OldSize < 1 { +		return nil, fmt.Errorf("OldSize(%d) must be larger than zero", req.OldSize) +	} +	if req.NewSize <= req.OldSize { +		return nil, fmt.Errorf("NewSize(%d) must be larger than OldSize(%d)", req.NewSize, req.OldSize) +	} +	return &req, nil +} + +func (i *Instance) inclusionProofRequestFromHTTP(r *http.Request) (*types.InclusionProofRequest, error) { +	var req types.InclusionProofRequest +	if err := req.UnmarshalASCII(r.Body); err != nil { +		return nil, fmt.Errorf("UnmarshalASCII: %v", err) +	} +	if req.TreeSize < 1 { +		return nil, fmt.Errorf("TreeSize(%d) must be larger than zero", req.TreeSize) +	} +	return &req, nil +} + +func (i *Instance) leavesRequestFromHTTP(r *http.Request) (*types.LeavesRequest, error) { +	var req types.LeavesRequest +	if err := req.UnmarshalASCII(r.Body); err != nil { +		return nil, fmt.Errorf("UnmarshalASCII: %v", err) +	} + +	if req.StartSize > req.EndSize { +		return nil, fmt.Errorf("StartSize(%d) must be less than or equal to EndSize(%d)", req.StartSize, req.EndSize) +	} +	if req.EndSize-req.StartSize+1 > uint64(i.MaxRange) { +		req.EndSize = req.StartSize + uint64(i.MaxRange) - 1 +	} +	return &req, nil +} diff --git a/pkg/instance/instance_test.go b/pkg/instance/instance_test.go new file mode 100644 index 0000000..45a2837 --- /dev/null +++ b/pkg/instance/instance_test.go @@ -0,0 +1,9 @@ +package stfe + +import ( +	"testing" +) + +func TestHandlers(t *testing.T)  {} +func TestPath(t *testing.T)      {} +func TestServeHTTP(t *testing.T) {} diff --git a/pkg/instance/metric.go b/pkg/instance/metric.go new file mode 100644 index 0000000..db11bd2 --- /dev/null +++ b/pkg/instance/metric.go @@ -0,0 +1,19 @@ +package stfe + +import ( +	"github.com/google/trillian/monitoring" +	"github.com/google/trillian/monitoring/prometheus" +) + +var ( +	reqcnt  monitoring.Counter   // number of incoming http requests +	rspcnt  monitoring.Counter   // number of valid http responses +	latency monitoring.Histogram // request-response latency +) + +func init() { +	mf := prometheus.MetricFactory{} +	reqcnt = mf.NewCounter("http_req", "number of http requests", "logid", "endpoint") +	rspcnt = mf.NewCounter("http_rsp", "number of http requests", "logid", "endpoint", "status") +	latency = mf.NewHistogram("http_latency", "http request-response latency", "logid", "endpoint", "status") +} diff --git a/pkg/mocks/crypto.go b/pkg/mocks/crypto.go new file mode 100644 index 0000000..87c883a --- /dev/null +++ b/pkg/mocks/crypto.go @@ -0,0 +1,23 @@ +package mocks + +import ( +	"crypto" +	"crypto/ed25519" +	"io" +) + +// TestSign implements the signer interface.  It can be used to mock an Ed25519 +// signer that always return the same public key, signature, and error. +type TestSigner struct { +	PublicKey *[ed25519.PublicKeySize]byte +	Signature *[ed25519.SignatureSize]byte +	Error     error +} + +func (ts *TestSigner) Public() crypto.PublicKey { +	return ed25519.PublicKey(ts.PublicKey[:]) +} + +func (ts *TestSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { +	return ts.Signature[:], ts.Error +} diff --git a/pkg/mocks/state.go b/pkg/mocks/state.go new file mode 100644 index 0000000..41d8d08 --- /dev/null +++ b/pkg/mocks/state.go @@ -0,0 +1,107 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/system-transparency/stfe/pkg/state (interfaces: StateManager) + +// Package mocks is a generated GoMock package. +package mocks + +import ( +	context "context" +	reflect "reflect" + +	gomock "github.com/golang/mock/gomock" +	types "github.com/system-transparency/stfe/pkg/types" +) + +// MockStateManager is a mock of StateManager interface. +type MockStateManager struct { +	ctrl     *gomock.Controller +	recorder *MockStateManagerMockRecorder +} + +// MockStateManagerMockRecorder is the mock recorder for MockStateManager. +type MockStateManagerMockRecorder struct { +	mock *MockStateManager +} + +// NewMockStateManager creates a new mock instance. +func NewMockStateManager(ctrl *gomock.Controller) *MockStateManager { +	mock := &MockStateManager{ctrl: ctrl} +	mock.recorder = &MockStateManagerMockRecorder{mock} +	return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockStateManager) EXPECT() *MockStateManagerMockRecorder { +	return m.recorder +} + +// AddCosignature mocks base method. +func (m *MockStateManager) AddCosignature(arg0 context.Context, arg1 *[32]byte, arg2 *[64]byte) error { +	m.ctrl.T.Helper() +	ret := m.ctrl.Call(m, "AddCosignature", arg0, arg1, arg2) +	ret0, _ := ret[0].(error) +	return ret0 +} + +// AddCosignature indicates an expected call of AddCosignature. +func (mr *MockStateManagerMockRecorder) AddCosignature(arg0, arg1, arg2 interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddCosignature", reflect.TypeOf((*MockStateManager)(nil).AddCosignature), arg0, arg1, arg2) +} + +// Cosigned mocks base method. +func (m *MockStateManager) Cosigned(arg0 context.Context) (*types.SignedTreeHead, error) { +	m.ctrl.T.Helper() +	ret := m.ctrl.Call(m, "Cosigned", arg0) +	ret0, _ := ret[0].(*types.SignedTreeHead) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// Cosigned indicates an expected call of Cosigned. +func (mr *MockStateManagerMockRecorder) Cosigned(arg0 interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Cosigned", reflect.TypeOf((*MockStateManager)(nil).Cosigned), arg0) +} + +// Latest mocks base method. +func (m *MockStateManager) Latest(arg0 context.Context) (*types.SignedTreeHead, error) { +	m.ctrl.T.Helper() +	ret := m.ctrl.Call(m, "Latest", arg0) +	ret0, _ := ret[0].(*types.SignedTreeHead) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// Latest indicates an expected call of Latest. +func (mr *MockStateManagerMockRecorder) Latest(arg0 interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Latest", reflect.TypeOf((*MockStateManager)(nil).Latest), arg0) +} + +// Run mocks base method. +func (m *MockStateManager) Run(arg0 context.Context) { +	m.ctrl.T.Helper() +	m.ctrl.Call(m, "Run", arg0) +} + +// Run indicates an expected call of Run. +func (mr *MockStateManagerMockRecorder) Run(arg0 interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MockStateManager)(nil).Run), arg0) +} + +// ToSign mocks base method. +func (m *MockStateManager) ToSign(arg0 context.Context) (*types.SignedTreeHead, error) { +	m.ctrl.T.Helper() +	ret := m.ctrl.Call(m, "ToSign", arg0) +	ret0, _ := ret[0].(*types.SignedTreeHead) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// ToSign indicates an expected call of ToSign. +func (mr *MockStateManagerMockRecorder) ToSign(arg0 interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ToSign", reflect.TypeOf((*MockStateManager)(nil).ToSign), arg0) +} diff --git a/pkg/mocks/stfe.go b/pkg/mocks/stfe.go new file mode 100644 index 0000000..def5bc6 --- /dev/null +++ b/pkg/mocks/stfe.go @@ -0,0 +1,110 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/system-transparency/stfe/trillian (interfaces: Client) + +// Package mocks is a generated GoMock package. +package mocks + +import ( +	context "context" +	reflect "reflect" + +	gomock "github.com/golang/mock/gomock" +	types "github.com/system-transparency/stfe/pkg/types" +) + +// MockClient is a mock of Client interface. +type MockClient struct { +	ctrl     *gomock.Controller +	recorder *MockClientMockRecorder +} + +// MockClientMockRecorder is the mock recorder for MockClient. +type MockClientMockRecorder struct { +	mock *MockClient +} + +// NewMockClient creates a new mock instance. +func NewMockClient(ctrl *gomock.Controller) *MockClient { +	mock := &MockClient{ctrl: ctrl} +	mock.recorder = &MockClientMockRecorder{mock} +	return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockClient) EXPECT() *MockClientMockRecorder { +	return m.recorder +} + +// AddLeaf mocks base method. +func (m *MockClient) AddLeaf(arg0 context.Context, arg1 *types.LeafRequest) error { +	m.ctrl.T.Helper() +	ret := m.ctrl.Call(m, "AddLeaf", arg0, arg1) +	ret0, _ := ret[0].(error) +	return ret0 +} + +// AddLeaf indicates an expected call of AddLeaf. +func (mr *MockClientMockRecorder) AddLeaf(arg0, arg1 interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddLeaf", reflect.TypeOf((*MockClient)(nil).AddLeaf), arg0, arg1) +} + +// GetConsistencyProof mocks base method. +func (m *MockClient) GetConsistencyProof(arg0 context.Context, arg1 *types.ConsistencyProofRequest) (*types.ConsistencyProof, error) { +	m.ctrl.T.Helper() +	ret := m.ctrl.Call(m, "GetConsistencyProof", arg0, arg1) +	ret0, _ := ret[0].(*types.ConsistencyProof) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// GetConsistencyProof indicates an expected call of GetConsistencyProof. +func (mr *MockClientMockRecorder) GetConsistencyProof(arg0, arg1 interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConsistencyProof", reflect.TypeOf((*MockClient)(nil).GetConsistencyProof), arg0, arg1) +} + +// GetInclusionProof mocks base method. +func (m *MockClient) GetInclusionProof(arg0 context.Context, arg1 *types.InclusionProofRequest) (*types.InclusionProof, error) { +	m.ctrl.T.Helper() +	ret := m.ctrl.Call(m, "GetInclusionProof", arg0, arg1) +	ret0, _ := ret[0].(*types.InclusionProof) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// GetInclusionProof indicates an expected call of GetInclusionProof. +func (mr *MockClientMockRecorder) GetInclusionProof(arg0, arg1 interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInclusionProof", reflect.TypeOf((*MockClient)(nil).GetInclusionProof), arg0, arg1) +} + +// GetLeaves mocks base method. +func (m *MockClient) GetLeaves(arg0 context.Context, arg1 *types.LeavesRequest) (*types.LeafList, error) { +	m.ctrl.T.Helper() +	ret := m.ctrl.Call(m, "GetLeaves", arg0, arg1) +	ret0, _ := ret[0].(*types.LeafList) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// GetLeaves indicates an expected call of GetLeaves. +func (mr *MockClientMockRecorder) GetLeaves(arg0, arg1 interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLeaves", reflect.TypeOf((*MockClient)(nil).GetLeaves), arg0, arg1) +} + +// GetTreeHead mocks base method. +func (m *MockClient) GetTreeHead(arg0 context.Context) (*types.TreeHead, error) { +	m.ctrl.T.Helper() +	ret := m.ctrl.Call(m, "GetTreeHead", arg0) +	ret0, _ := ret[0].(*types.TreeHead) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// GetTreeHead indicates an expected call of GetTreeHead. +func (mr *MockClientMockRecorder) GetTreeHead(arg0 interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTreeHead", reflect.TypeOf((*MockClient)(nil).GetTreeHead), arg0) +} diff --git a/pkg/mocks/trillian.go b/pkg/mocks/trillian.go new file mode 100644 index 0000000..8aa3a58 --- /dev/null +++ b/pkg/mocks/trillian.go @@ -0,0 +1,317 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/google/trillian (interfaces: TrillianLogClient) + +// Package mocks is a generated GoMock package. +package mocks + +import ( +	context "context" +	reflect "reflect" + +	gomock "github.com/golang/mock/gomock" +	trillian "github.com/google/trillian" +	grpc "google.golang.org/grpc" +) + +// MockTrillianLogClient is a mock of TrillianLogClient interface. +type MockTrillianLogClient struct { +	ctrl     *gomock.Controller +	recorder *MockTrillianLogClientMockRecorder +} + +// MockTrillianLogClientMockRecorder is the mock recorder for MockTrillianLogClient. +type MockTrillianLogClientMockRecorder struct { +	mock *MockTrillianLogClient +} + +// NewMockTrillianLogClient creates a new mock instance. +func NewMockTrillianLogClient(ctrl *gomock.Controller) *MockTrillianLogClient { +	mock := &MockTrillianLogClient{ctrl: ctrl} +	mock.recorder = &MockTrillianLogClientMockRecorder{mock} +	return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockTrillianLogClient) EXPECT() *MockTrillianLogClientMockRecorder { +	return m.recorder +} + +// AddSequencedLeaf mocks base method. +func (m *MockTrillianLogClient) AddSequencedLeaf(arg0 context.Context, arg1 *trillian.AddSequencedLeafRequest, arg2 ...grpc.CallOption) (*trillian.AddSequencedLeafResponse, error) { +	m.ctrl.T.Helper() +	varargs := []interface{}{arg0, arg1} +	for _, a := range arg2 { +		varargs = append(varargs, a) +	} +	ret := m.ctrl.Call(m, "AddSequencedLeaf", varargs...) +	ret0, _ := ret[0].(*trillian.AddSequencedLeafResponse) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// AddSequencedLeaf indicates an expected call of AddSequencedLeaf. +func (mr *MockTrillianLogClientMockRecorder) AddSequencedLeaf(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	varargs := append([]interface{}{arg0, arg1}, arg2...) +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddSequencedLeaf", reflect.TypeOf((*MockTrillianLogClient)(nil).AddSequencedLeaf), varargs...) +} + +// AddSequencedLeaves mocks base method. +func (m *MockTrillianLogClient) AddSequencedLeaves(arg0 context.Context, arg1 *trillian.AddSequencedLeavesRequest, arg2 ...grpc.CallOption) (*trillian.AddSequencedLeavesResponse, error) { +	m.ctrl.T.Helper() +	varargs := []interface{}{arg0, arg1} +	for _, a := range arg2 { +		varargs = append(varargs, a) +	} +	ret := m.ctrl.Call(m, "AddSequencedLeaves", varargs...) +	ret0, _ := ret[0].(*trillian.AddSequencedLeavesResponse) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// AddSequencedLeaves indicates an expected call of AddSequencedLeaves. +func (mr *MockTrillianLogClientMockRecorder) AddSequencedLeaves(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	varargs := append([]interface{}{arg0, arg1}, arg2...) +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddSequencedLeaves", reflect.TypeOf((*MockTrillianLogClient)(nil).AddSequencedLeaves), varargs...) +} + +// GetConsistencyProof mocks base method. +func (m *MockTrillianLogClient) GetConsistencyProof(arg0 context.Context, arg1 *trillian.GetConsistencyProofRequest, arg2 ...grpc.CallOption) (*trillian.GetConsistencyProofResponse, error) { +	m.ctrl.T.Helper() +	varargs := []interface{}{arg0, arg1} +	for _, a := range arg2 { +		varargs = append(varargs, a) +	} +	ret := m.ctrl.Call(m, "GetConsistencyProof", varargs...) +	ret0, _ := ret[0].(*trillian.GetConsistencyProofResponse) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// GetConsistencyProof indicates an expected call of GetConsistencyProof. +func (mr *MockTrillianLogClientMockRecorder) GetConsistencyProof(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	varargs := append([]interface{}{arg0, arg1}, arg2...) +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConsistencyProof", reflect.TypeOf((*MockTrillianLogClient)(nil).GetConsistencyProof), varargs...) +} + +// GetEntryAndProof mocks base method. +func (m *MockTrillianLogClient) GetEntryAndProof(arg0 context.Context, arg1 *trillian.GetEntryAndProofRequest, arg2 ...grpc.CallOption) (*trillian.GetEntryAndProofResponse, error) { +	m.ctrl.T.Helper() +	varargs := []interface{}{arg0, arg1} +	for _, a := range arg2 { +		varargs = append(varargs, a) +	} +	ret := m.ctrl.Call(m, "GetEntryAndProof", varargs...) +	ret0, _ := ret[0].(*trillian.GetEntryAndProofResponse) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// GetEntryAndProof indicates an expected call of GetEntryAndProof. +func (mr *MockTrillianLogClientMockRecorder) GetEntryAndProof(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	varargs := append([]interface{}{arg0, arg1}, arg2...) +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEntryAndProof", reflect.TypeOf((*MockTrillianLogClient)(nil).GetEntryAndProof), varargs...) +} + +// GetInclusionProof mocks base method. +func (m *MockTrillianLogClient) GetInclusionProof(arg0 context.Context, arg1 *trillian.GetInclusionProofRequest, arg2 ...grpc.CallOption) (*trillian.GetInclusionProofResponse, error) { +	m.ctrl.T.Helper() +	varargs := []interface{}{arg0, arg1} +	for _, a := range arg2 { +		varargs = append(varargs, a) +	} +	ret := m.ctrl.Call(m, "GetInclusionProof", varargs...) +	ret0, _ := ret[0].(*trillian.GetInclusionProofResponse) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// GetInclusionProof indicates an expected call of GetInclusionProof. +func (mr *MockTrillianLogClientMockRecorder) GetInclusionProof(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	varargs := append([]interface{}{arg0, arg1}, arg2...) +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInclusionProof", reflect.TypeOf((*MockTrillianLogClient)(nil).GetInclusionProof), varargs...) +} + +// GetInclusionProofByHash mocks base method. +func (m *MockTrillianLogClient) GetInclusionProofByHash(arg0 context.Context, arg1 *trillian.GetInclusionProofByHashRequest, arg2 ...grpc.CallOption) (*trillian.GetInclusionProofByHashResponse, error) { +	m.ctrl.T.Helper() +	varargs := []interface{}{arg0, arg1} +	for _, a := range arg2 { +		varargs = append(varargs, a) +	} +	ret := m.ctrl.Call(m, "GetInclusionProofByHash", varargs...) +	ret0, _ := ret[0].(*trillian.GetInclusionProofByHashResponse) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// GetInclusionProofByHash indicates an expected call of GetInclusionProofByHash. +func (mr *MockTrillianLogClientMockRecorder) GetInclusionProofByHash(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	varargs := append([]interface{}{arg0, arg1}, arg2...) +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInclusionProofByHash", reflect.TypeOf((*MockTrillianLogClient)(nil).GetInclusionProofByHash), varargs...) +} + +// GetLatestSignedLogRoot mocks base method. +func (m *MockTrillianLogClient) GetLatestSignedLogRoot(arg0 context.Context, arg1 *trillian.GetLatestSignedLogRootRequest, arg2 ...grpc.CallOption) (*trillian.GetLatestSignedLogRootResponse, error) { +	m.ctrl.T.Helper() +	varargs := []interface{}{arg0, arg1} +	for _, a := range arg2 { +		varargs = append(varargs, a) +	} +	ret := m.ctrl.Call(m, "GetLatestSignedLogRoot", varargs...) +	ret0, _ := ret[0].(*trillian.GetLatestSignedLogRootResponse) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// GetLatestSignedLogRoot indicates an expected call of GetLatestSignedLogRoot. +func (mr *MockTrillianLogClientMockRecorder) GetLatestSignedLogRoot(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	varargs := append([]interface{}{arg0, arg1}, arg2...) +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLatestSignedLogRoot", reflect.TypeOf((*MockTrillianLogClient)(nil).GetLatestSignedLogRoot), varargs...) +} + +// GetLeavesByHash mocks base method. +func (m *MockTrillianLogClient) GetLeavesByHash(arg0 context.Context, arg1 *trillian.GetLeavesByHashRequest, arg2 ...grpc.CallOption) (*trillian.GetLeavesByHashResponse, error) { +	m.ctrl.T.Helper() +	varargs := []interface{}{arg0, arg1} +	for _, a := range arg2 { +		varargs = append(varargs, a) +	} +	ret := m.ctrl.Call(m, "GetLeavesByHash", varargs...) +	ret0, _ := ret[0].(*trillian.GetLeavesByHashResponse) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// GetLeavesByHash indicates an expected call of GetLeavesByHash. +func (mr *MockTrillianLogClientMockRecorder) GetLeavesByHash(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	varargs := append([]interface{}{arg0, arg1}, arg2...) +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLeavesByHash", reflect.TypeOf((*MockTrillianLogClient)(nil).GetLeavesByHash), varargs...) +} + +// GetLeavesByIndex mocks base method. +func (m *MockTrillianLogClient) GetLeavesByIndex(arg0 context.Context, arg1 *trillian.GetLeavesByIndexRequest, arg2 ...grpc.CallOption) (*trillian.GetLeavesByIndexResponse, error) { +	m.ctrl.T.Helper() +	varargs := []interface{}{arg0, arg1} +	for _, a := range arg2 { +		varargs = append(varargs, a) +	} +	ret := m.ctrl.Call(m, "GetLeavesByIndex", varargs...) +	ret0, _ := ret[0].(*trillian.GetLeavesByIndexResponse) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// GetLeavesByIndex indicates an expected call of GetLeavesByIndex. +func (mr *MockTrillianLogClientMockRecorder) GetLeavesByIndex(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	varargs := append([]interface{}{arg0, arg1}, arg2...) +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLeavesByIndex", reflect.TypeOf((*MockTrillianLogClient)(nil).GetLeavesByIndex), varargs...) +} + +// GetLeavesByRange mocks base method. +func (m *MockTrillianLogClient) GetLeavesByRange(arg0 context.Context, arg1 *trillian.GetLeavesByRangeRequest, arg2 ...grpc.CallOption) (*trillian.GetLeavesByRangeResponse, error) { +	m.ctrl.T.Helper() +	varargs := []interface{}{arg0, arg1} +	for _, a := range arg2 { +		varargs = append(varargs, a) +	} +	ret := m.ctrl.Call(m, "GetLeavesByRange", varargs...) +	ret0, _ := ret[0].(*trillian.GetLeavesByRangeResponse) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// GetLeavesByRange indicates an expected call of GetLeavesByRange. +func (mr *MockTrillianLogClientMockRecorder) GetLeavesByRange(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	varargs := append([]interface{}{arg0, arg1}, arg2...) +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLeavesByRange", reflect.TypeOf((*MockTrillianLogClient)(nil).GetLeavesByRange), varargs...) +} + +// GetSequencedLeafCount mocks base method. +func (m *MockTrillianLogClient) GetSequencedLeafCount(arg0 context.Context, arg1 *trillian.GetSequencedLeafCountRequest, arg2 ...grpc.CallOption) (*trillian.GetSequencedLeafCountResponse, error) { +	m.ctrl.T.Helper() +	varargs := []interface{}{arg0, arg1} +	for _, a := range arg2 { +		varargs = append(varargs, a) +	} +	ret := m.ctrl.Call(m, "GetSequencedLeafCount", varargs...) +	ret0, _ := ret[0].(*trillian.GetSequencedLeafCountResponse) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// GetSequencedLeafCount indicates an expected call of GetSequencedLeafCount. +func (mr *MockTrillianLogClientMockRecorder) GetSequencedLeafCount(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	varargs := append([]interface{}{arg0, arg1}, arg2...) +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSequencedLeafCount", reflect.TypeOf((*MockTrillianLogClient)(nil).GetSequencedLeafCount), varargs...) +} + +// InitLog mocks base method. +func (m *MockTrillianLogClient) InitLog(arg0 context.Context, arg1 *trillian.InitLogRequest, arg2 ...grpc.CallOption) (*trillian.InitLogResponse, error) { +	m.ctrl.T.Helper() +	varargs := []interface{}{arg0, arg1} +	for _, a := range arg2 { +		varargs = append(varargs, a) +	} +	ret := m.ctrl.Call(m, "InitLog", varargs...) +	ret0, _ := ret[0].(*trillian.InitLogResponse) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// InitLog indicates an expected call of InitLog. +func (mr *MockTrillianLogClientMockRecorder) InitLog(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	varargs := append([]interface{}{arg0, arg1}, arg2...) +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InitLog", reflect.TypeOf((*MockTrillianLogClient)(nil).InitLog), varargs...) +} + +// QueueLeaf mocks base method. +func (m *MockTrillianLogClient) QueueLeaf(arg0 context.Context, arg1 *trillian.QueueLeafRequest, arg2 ...grpc.CallOption) (*trillian.QueueLeafResponse, error) { +	m.ctrl.T.Helper() +	varargs := []interface{}{arg0, arg1} +	for _, a := range arg2 { +		varargs = append(varargs, a) +	} +	ret := m.ctrl.Call(m, "QueueLeaf", varargs...) +	ret0, _ := ret[0].(*trillian.QueueLeafResponse) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// QueueLeaf indicates an expected call of QueueLeaf. +func (mr *MockTrillianLogClientMockRecorder) QueueLeaf(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	varargs := append([]interface{}{arg0, arg1}, arg2...) +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueueLeaf", reflect.TypeOf((*MockTrillianLogClient)(nil).QueueLeaf), varargs...) +} + +// QueueLeaves mocks base method. +func (m *MockTrillianLogClient) QueueLeaves(arg0 context.Context, arg1 *trillian.QueueLeavesRequest, arg2 ...grpc.CallOption) (*trillian.QueueLeavesResponse, error) { +	m.ctrl.T.Helper() +	varargs := []interface{}{arg0, arg1} +	for _, a := range arg2 { +		varargs = append(varargs, a) +	} +	ret := m.ctrl.Call(m, "QueueLeaves", varargs...) +	ret0, _ := ret[0].(*trillian.QueueLeavesResponse) +	ret1, _ := ret[1].(error) +	return ret0, ret1 +} + +// QueueLeaves indicates an expected call of QueueLeaves. +func (mr *MockTrillianLogClientMockRecorder) QueueLeaves(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	varargs := append([]interface{}{arg0, arg1}, arg2...) +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueueLeaves", reflect.TypeOf((*MockTrillianLogClient)(nil).QueueLeaves), varargs...) +} diff --git a/pkg/state/state_manager.go b/pkg/state/state_manager.go new file mode 100644 index 0000000..dfa73f4 --- /dev/null +++ b/pkg/state/state_manager.go @@ -0,0 +1,154 @@ +package state + +import ( +	"context" +	"crypto" +	"fmt" +	"reflect" +	"sync" +	"time" + +	"github.com/golang/glog" +	"github.com/google/certificate-transparency-go/schedule" +	"github.com/system-transparency/stfe/pkg/trillian" +	"github.com/system-transparency/stfe/pkg/types" +) + +// StateManager coordinates access to the log's tree heads and (co)signatures +type StateManager interface { +	Latest(context.Context) (*types.SignedTreeHead, error) +	ToSign(context.Context) (*types.SignedTreeHead, error) +	Cosigned(context.Context) (*types.SignedTreeHead, error) +	AddCosignature(context.Context, *[types.VerificationKeySize]byte, *[types.SignatureSize]byte) error +	Run(context.Context) +} + +// StateManagerSingle implements the StateManager interface.  It is assumed that +// the log server is running on a single-instance machine.  So, no coordination. +type StateManagerSingle struct { +	client   trillian.Client +	signer   crypto.Signer +	interval time.Duration +	deadline time.Duration +	sync.RWMutex + +	// cosigned is the current cosigned tree head that is being served +	cosigned types.SignedTreeHead + +	// tosign is the current tree head that is being cosigned by witnesses +	tosign types.SignedTreeHead + +	// cosignature keeps track of all cosignatures for the tosign tree head +	cosignature map[[types.HashSize]byte]*types.SigIdent +} + +func NewStateManagerSingle(client trillian.Client, signer crypto.Signer, interval, deadline time.Duration) (*StateManagerSingle, error) { +	sm := &StateManagerSingle{ +		client:   client, +		signer:   signer, +		interval: interval, +		deadline: deadline, +	} + +	ctx, _ := context.WithTimeout(context.Background(), sm.deadline) +	sth, err := sm.Latest(ctx) +	if err != nil { +		return nil, fmt.Errorf("Latest: %v", err) +	} + +	sm.cosigned = *sth +	sm.tosign = *sth +	sm.cosignature = map[[types.HashSize]byte]*types.SigIdent{ +		*sth.SigIdent[0].KeyHash: sth.SigIdent[0], // log signature +	} +	return sm, nil +} + +func (sm *StateManagerSingle) Run(ctx context.Context) { +	schedule.Every(ctx, sm.interval, func(ctx context.Context) { +		ictx, _ := context.WithTimeout(ctx, sm.deadline) +		nextTreeHead, err := sm.Latest(ictx) +		if err != nil { +			glog.Warningf("rotate failed: Latest: %v", err) +			return +		} + +		sm.Lock() +		defer sm.Unlock() +		sm.rotate(nextTreeHead) +	}) +} + +func (sm *StateManagerSingle) Latest(ctx context.Context) (*types.SignedTreeHead, error) { +	th, err := sm.client.GetTreeHead(ctx) +	if err != nil { +		return nil, fmt.Errorf("LatestTreeHead: %v", err) +	} +	sth, err := th.Sign(sm.signer) +	if err != nil { +		return nil, fmt.Errorf("sign: %v", err) +	} +	return sth, nil +} + +func (sm *StateManagerSingle) ToSign(_ context.Context) (*types.SignedTreeHead, error) { +	sm.RLock() +	defer sm.RUnlock() +	return &sm.tosign, nil +} + +func (sm *StateManagerSingle) Cosigned(_ context.Context) (*types.SignedTreeHead, error) { +	sm.RLock() +	defer sm.RUnlock() +	return &sm.cosigned, nil +} + +func (sm *StateManagerSingle) AddCosignature(_ context.Context, vk *[types.VerificationKeySize]byte, sig *[types.SignatureSize]byte) error { +	sm.Lock() +	defer sm.Unlock() + +	if err := sm.tosign.TreeHead.Verify(vk, sig); err != nil { +		return fmt.Errorf("Verify: %v", err) +	} +	witness := types.Hash(vk[:]) +	if _, ok := sm.cosignature[*witness]; ok { +		return fmt.Errorf("signature-signer pair is a duplicate") +	} +	sm.cosignature[*witness] = &types.SigIdent{ +		Signature: sig, +		KeyHash:   witness, +	} + +	glog.V(3).Infof("accepted new cosignature from witness: %x", *witness) +	return nil +} + +// rotate rotates the log's cosigned and stable STH.  The caller must aquire the +// source's read-write lock if there are concurrent reads and/or writes. +func (sm *StateManagerSingle) rotate(next *types.SignedTreeHead) { +	if reflect.DeepEqual(sm.cosigned.TreeHead, sm.tosign.TreeHead) { +		// cosigned and tosign are the same.  So, we need to merge all +		// cosignatures that we already had with the new collected ones. +		for _, sigident := range sm.cosigned.SigIdent { +			if _, ok := sm.cosignature[*sigident.KeyHash]; !ok { +				sm.cosignature[*sigident.KeyHash] = sigident +			} +		} +		glog.V(3).Infof("cosigned tree head repeated, merged signatures") +	} +	var cosignatures []*types.SigIdent +	for _, sigident := range sm.cosignature { +		cosignatures = append(cosignatures, sigident) +	} + +	// Update cosigned tree head +	sm.cosigned.TreeHead = sm.tosign.TreeHead +	sm.cosigned.SigIdent = cosignatures + +	// Update to-sign tree head +	sm.tosign = *next +	sm.cosignature = map[[types.HashSize]byte]*types.SigIdent{ +		*next.SigIdent[0].KeyHash: next.SigIdent[0], // log signature +	} +	glog.V(3).Infof("rotated tree heads") +} diff --git a/pkg/state/state_manager_test.go b/pkg/state/state_manager_test.go new file mode 100644 index 0000000..08990cc --- /dev/null +++ b/pkg/state/state_manager_test.go @@ -0,0 +1,393 @@ +package state + +import ( +	"bytes" +	"context" +	"crypto" +	"crypto/ed25519" +	"crypto/rand" +	"fmt" +	"reflect" +	"testing" +	"time" + +	"github.com/golang/mock/gomock" +	"github.com/system-transparency/stfe/pkg/mocks" +	"github.com/system-transparency/stfe/pkg/types" +) + +var ( +	testSig = &[types.SignatureSize]byte{} +	testPub = &[types.VerificationKeySize]byte{} +	testTH  = &types.TreeHead{ +		Timestamp: 0, +		TreeSize:  0, +		RootHash:  types.Hash(nil), +	} +	testSigIdent = &types.SigIdent{ +		Signature: testSig, +		KeyHash:   types.Hash(testPub[:]), +	} +	testSTH = &types.SignedTreeHead{ +		TreeHead: *testTH, +		SigIdent: []*types.SigIdent{testSigIdent}, +	} +	testSignerOK  = &mocks.TestSigner{testPub, testSig, nil} +	testSignerErr = &mocks.TestSigner{testPub, testSig, fmt.Errorf("something went wrong")} +) + +func TestNewStateManagerSingle(t *testing.T) { +	for _, table := range []struct { +		description string +		signer      crypto.Signer +		rsp         *types.TreeHead +		err         error +		wantErr     bool +		wantSth     *types.SignedTreeHead +	}{ +		{ +			description: "invalid: backend failure", +			signer:      testSignerOK, +			err:         fmt.Errorf("something went wrong"), +			wantErr:     true, +		}, +		{ +			description: "valid", +			signer:      testSignerOK, +			rsp:         testTH, +			wantSth:     testSTH, +		}, +	} { +		// Run deferred functions at the end of each iteration +		func() { +			ctrl := gomock.NewController(t) +			defer ctrl.Finish() +			client := mocks.NewMockClient(ctrl) +			client.EXPECT().GetTreeHead(gomock.Any()).Return(table.rsp, table.err) + +			sm, err := NewStateManagerSingle(client, table.signer, time.Duration(0), time.Duration(0)) +			if got, want := err != nil, table.wantErr; got != want { +				t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) +			} +			if err != nil { +				return +			} +			if got, want := &sm.cosigned, table.wantSth; !reflect.DeepEqual(got, want) { +				t.Errorf("got cosigned tree head\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) +			} +			if got, want := &sm.tosign, table.wantSth; !reflect.DeepEqual(got, want) { +				t.Errorf("got tosign tree head\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) +			} +			// we only have log signature on startup +			if got, want := len(sm.cosignature), 1; got != want { +				t.Errorf("got %d cosignatures but wanted %d in test %q", got, want, table.description) +			} +		}() +	} +} + +func TestLatest(t *testing.T) { +	for _, table := range []struct { +		description string +		signer      crypto.Signer +		rsp         *types.TreeHead +		err         error +		wantErr     bool +		wantSth     *types.SignedTreeHead +	}{ +		{ +			description: "invalid: backend failure", +			signer:      testSignerOK, +			err:         fmt.Errorf("something went wrong"), +			wantErr:     true, +		}, +		{ +			description: "invalid: signature failure", +			rsp:         testTH, +			signer:      testSignerErr, +			wantErr:     true, +		}, +		{ +			description: "valid", +			signer:      testSignerOK, +			rsp:         testTH, +			wantSth:     testSTH, +		}, +	} { +		// Run deferred functions at the end of each iteration +		func() { +			ctrl := gomock.NewController(t) +			defer ctrl.Finish() +			client := mocks.NewMockClient(ctrl) +			client.EXPECT().GetTreeHead(gomock.Any()).Return(table.rsp, table.err) +			sm := StateManagerSingle{ +				client: client, +				signer: table.signer, +			} + +			sth, err := sm.Latest(context.Background()) +			if got, want := err != nil, table.wantErr; got != want { +				t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) +			} +			if err != nil { +				return +			} +			if got, want := sth, table.wantSth; !reflect.DeepEqual(got, want) { +				t.Errorf("got signed tree head\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) +			} +		}() +	} +} + +func TestToSign(t *testing.T) { +	description := "valid" +	sm := StateManagerSingle{ +		tosign: *testSTH, +	} +	sth, err := sm.ToSign(context.Background()) +	if err != nil { +		t.Errorf("ToSign should not fail with error: %v", err) +		return +	} +	if got, want := sth, testSTH; !reflect.DeepEqual(got, want) { +		t.Errorf("got signed tree head\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, description) +	} +} + +func TestCosigned(t *testing.T) { +	description := "valid" +	sm := StateManagerSingle{ +		cosigned: *testSTH, +	} +	sth, err := sm.Cosigned(context.Background()) +	if err != nil { +		t.Errorf("Cosigned should not fail with error: %v", err) +		return +	} +	if got, want := sth, testSTH; !reflect.DeepEqual(got, want) { +		t.Errorf("got signed tree head\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, description) +	} +} + +func TestAddCosignature(t *testing.T) { +	vk, sk, err := ed25519.GenerateKey(rand.Reader) +	if err != nil { +		t.Fatalf("GenerateKey: %v", err) +	} +	if bytes.Equal(vk[:], testPub[:]) { +		t.Fatalf("Sampled same key as testPub, aborting...") +	} +	var vkArray [types.VerificationKeySize]byte +	copy(vkArray[:], vk[:]) + +	for _, table := range []struct { +		description string +		signer      crypto.Signer +		vk          *[types.VerificationKeySize]byte +		th          *types.TreeHead +		wantErr     bool +	}{ +		{ +			description: "invalid: signature error", +			signer:      sk, +			vk:          testPub, // wrong key for message +			th:          testTH, +			wantErr:     true, +		}, +		{ +			description: "valid", +			signer:      sk, +			vk:          &vkArray, +			th:          testTH, +		}, +	} { +		sth, _ := table.th.Sign(testSignerOK) +		logKeyHash := sth.SigIdent[0].KeyHash +		logSigIdent := sth.SigIdent[0] +		sm := &StateManagerSingle{ +			signer:   testSignerOK, +			cosigned: *sth, +			tosign:   *sth, +			cosignature: map[[types.HashSize]byte]*types.SigIdent{ +				*logKeyHash: logSigIdent, +			}, +		} + +		// Prepare witness signature +		sth, err := table.th.Sign(table.signer) +		if err != nil { +			t.Fatalf("Sign: %v", err) +		} +		witnessKeyHash := sth.SigIdent[0].KeyHash +		witnessSigIdent := sth.SigIdent[0] + +		// Add witness signature +		err = sm.AddCosignature(context.Background(), table.vk, witnessSigIdent.Signature) +		if got, want := err != nil, table.wantErr; got != want { +			t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) +		} +		if err != nil { +			continue +		} + +		// We should have two signatures (log + witness) +		if got, want := len(sm.cosignature), 2; got != want { +			t.Errorf("got %d cosignatures but wanted %v in test %q", got, want, table.description) +			continue +		} +		// check that log signature is there +		sigident, ok := sm.cosignature[*logKeyHash] +		if !ok { +			t.Errorf("log signature is missing") +			continue +		} +		if got, want := sigident, logSigIdent; !reflect.DeepEqual(got, want) { +			t.Errorf("got log sigident\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) +		} +		// check that witness signature is there +		sigident, ok = sm.cosignature[*witnessKeyHash] +		if !ok { +			t.Errorf("witness signature is missing") +			continue +		} +		if got, want := sigident, witnessSigIdent; !reflect.DeepEqual(got, want) { +			t.Errorf("got witness sigident\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) +			continue +		} + +		// Adding a duplicate signature should give an error +		if err := sm.AddCosignature(context.Background(), table.vk, witnessSigIdent.Signature); err == nil { +			t.Errorf("duplicate witness signature accepted as valid") +		} +	} +} + +func TestRotate(t *testing.T) { +	log := testSigIdent +	wit1 := &types.SigIdent{ +		Signature: testSig, +		KeyHash:   types.Hash([]byte("wit1 key")), +	} +	wit2 := &types.SigIdent{ +		Signature: testSig, +		KeyHash:   types.Hash([]byte("wit2 key")), +	} +	th0 := testTH +	th1 := &types.TreeHead{ +		Timestamp: 1, +		TreeSize:  1, +		RootHash:  types.Hash([]byte("1")), +	} +	th2 := &types.TreeHead{ +		Timestamp: 2, +		TreeSize:  2, +		RootHash:  types.Hash([]byte("2")), +	} + +	for _, table := range []struct { +		description   string +		before, after *StateManagerSingle +		next          *types.SignedTreeHead +	}{ +		{ +			description: "tosign tree head repated, but got one new witnes signature", +			before: &StateManagerSingle{ +				cosigned: types.SignedTreeHead{ +					TreeHead: *th0, +					SigIdent: []*types.SigIdent{log, wit1}, +				}, +				tosign: types.SignedTreeHead{ +					TreeHead: *th0, +					SigIdent: []*types.SigIdent{log}, +				}, +				cosignature: map[[types.HashSize]byte]*types.SigIdent{ +					*log.KeyHash:  log, +					*wit2.KeyHash: wit2, // the new witness signature +				}, +			}, +			next: &types.SignedTreeHead{ +				TreeHead: *th1, +				SigIdent: []*types.SigIdent{log}, +			}, +			after: &StateManagerSingle{ +				cosigned: types.SignedTreeHead{ +					TreeHead: *th0, +					SigIdent: []*types.SigIdent{log, wit1, wit2}, +				}, +				tosign: types.SignedTreeHead{ +					TreeHead: *th1, +					SigIdent: []*types.SigIdent{log}, +				}, +				cosignature: map[[types.HashSize]byte]*types.SigIdent{ +					*log.KeyHash: log, // after rotate we always have log sig +				}, +			}, +		}, +		{ +			description: "tosign tree head did not repeat, it got one witness signature", +			before: &StateManagerSingle{ +				cosigned: types.SignedTreeHead{ +					TreeHead: *th0, +					SigIdent: []*types.SigIdent{log, wit1}, +				}, +				tosign: types.SignedTreeHead{ +					TreeHead: *th1, +					SigIdent: []*types.SigIdent{log}, +				}, +				cosignature: map[[types.HashSize]byte]*types.SigIdent{ +					*log.KeyHash:  log, +					*wit2.KeyHash: wit2, // the only witness that signed tosign +				}, +			}, +			next: &types.SignedTreeHead{ +				TreeHead: *th2, +				SigIdent: []*types.SigIdent{log}, +			}, +			after: &StateManagerSingle{ +				cosigned: types.SignedTreeHead{ +					TreeHead: *th1, +					SigIdent: []*types.SigIdent{log, wit2}, +				}, +				tosign: types.SignedTreeHead{ +					TreeHead: *th2, +					SigIdent: []*types.SigIdent{log}, +				}, +				cosignature: map[[types.HashSize]byte]*types.SigIdent{ +					*log.KeyHash: log, // after rotate we always have log sig +				}, +			}, +		}, +	} { +		table.before.rotate(table.next) +		if got, want := table.before.cosigned.TreeHead, table.after.cosigned.TreeHead; !reflect.DeepEqual(got, want) { +			t.Errorf("got cosigned tree head\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) +		} +		checkWitnessList(t, table.description, table.before.cosigned.SigIdent, table.after.cosigned.SigIdent) +		if got, want := table.before.tosign.TreeHead, table.after.tosign.TreeHead; !reflect.DeepEqual(got, want) { +			t.Errorf("got tosign tree head\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) +		} +		checkWitnessList(t, table.description, table.before.tosign.SigIdent, table.after.tosign.SigIdent) +		if got, want := table.before.cosignature, table.after.cosignature; !reflect.DeepEqual(got, want) { +			t.Errorf("got cosignature map\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) +		} +	} +} + +func checkWitnessList(t *testing.T, description string, got, want []*types.SigIdent) { +	t.Helper() +	for _, si := range got { +		found := false +		for _, sj := range want { +			if reflect.DeepEqual(si, sj) { +				found = true +				break +			} +		} +		if !found { +			t.Errorf("got unexpected signature-signer pair with key hash in test %q: %x", description, si.KeyHash[:]) +		} +	} +	if len(got) != len(want) { +		t.Errorf("got %d signature-signer pairs but wanted %d in test %q", len(got), len(want), description) +	} +} diff --git a/pkg/trillian/client.go b/pkg/trillian/client.go new file mode 100644 index 0000000..9523e56 --- /dev/null +++ b/pkg/trillian/client.go @@ -0,0 +1,178 @@ +package trillian + +import ( +	"context" +	"fmt" + +	"github.com/golang/glog" +	"github.com/google/trillian" +	ttypes "github.com/google/trillian/types" +	"github.com/system-transparency/stfe/pkg/types" +	"google.golang.org/grpc/codes" +) + +type Client interface { +	AddLeaf(context.Context, *types.LeafRequest) error +	GetConsistencyProof(context.Context, *types.ConsistencyProofRequest) (*types.ConsistencyProof, error) +	GetTreeHead(context.Context) (*types.TreeHead, error) +	GetInclusionProof(context.Context, *types.InclusionProofRequest) (*types.InclusionProof, error) +	GetLeaves(context.Context, *types.LeavesRequest) (*types.LeafList, error) +} + +// TrillianClient is a wrapper around the Trillian gRPC client. +type TrillianClient struct { +	// TreeID is a Merkle tree identifier that Trillian uses +	TreeID int64 + +	// GRPC is a Trillian gRPC client +	GRPC trillian.TrillianLogClient +} + +func (c *TrillianClient) AddLeaf(ctx context.Context, req *types.LeafRequest) error { +	leaf := types.Leaf{ +		Message: req.Message, +		SigIdent: types.SigIdent{ +			Signature: req.Signature, +			KeyHash:   types.Hash(req.VerificationKey[:]), +		}, +	} +	serialized := leaf.Marshal() + +	glog.V(3).Infof("queueing leaf request: %x", types.HashLeaf(serialized)) +	rsp, err := c.GRPC.QueueLeaf(ctx, &trillian.QueueLeafRequest{ +		LogId: c.TreeID, +		Leaf: &trillian.LogLeaf{ +			LeafValue: serialized, +		}, +	}) +	if err != nil { +		return fmt.Errorf("backend failure: %v", err) +	} +	if rsp == nil { +		return fmt.Errorf("no response") +	} +	if rsp.QueuedLeaf == nil { +		return fmt.Errorf("no queued leaf") +	} +	if codes.Code(rsp.QueuedLeaf.GetStatus().GetCode()) == codes.AlreadyExists { +		return fmt.Errorf("leaf is already queued or included") +	} +	return nil +} + +func (c *TrillianClient) GetTreeHead(ctx context.Context) (*types.TreeHead, error) { +	rsp, err := c.GRPC.GetLatestSignedLogRoot(ctx, &trillian.GetLatestSignedLogRootRequest{ +		LogId: c.TreeID, +	}) +	if err != nil { +		return nil, fmt.Errorf("backend failure: %v", err) +	} +	if rsp == nil { +		return nil, fmt.Errorf("no response") +	} +	if rsp.SignedLogRoot == nil { +		return nil, fmt.Errorf("no signed log root") +	} +	if rsp.SignedLogRoot.LogRoot == nil { +		return nil, fmt.Errorf("no log root") +	} +	var r ttypes.LogRootV1 +	if err := r.UnmarshalBinary(rsp.SignedLogRoot.LogRoot); err != nil { +		return nil, fmt.Errorf("no log root: unmarshal failed: %v", err) +	} +	if len(r.RootHash) != types.HashSize { +		return nil, fmt.Errorf("unexpected hash length: %d", len(r.RootHash)) +	} +	return treeHeadFromLogRoot(&r), nil +} + +func (c *TrillianClient) GetConsistencyProof(ctx context.Context, req *types.ConsistencyProofRequest) (*types.ConsistencyProof, error) { +	rsp, err := c.GRPC.GetConsistencyProof(ctx, &trillian.GetConsistencyProofRequest{ +		LogId:          c.TreeID, +		FirstTreeSize:  int64(req.OldSize), +		SecondTreeSize: int64(req.NewSize), +	}) +	if err != nil { +		return nil, fmt.Errorf("backend failure: %v", err) +	} +	if rsp == nil { +		return nil, fmt.Errorf("no response") +	} +	if rsp.Proof == nil { +		return nil, fmt.Errorf("no consistency proof") +	} +	if len(rsp.Proof.Hashes) == 0 { +		return nil, fmt.Errorf("not a consistency proof: empty") +	} +	path, err := nodePathFromHashes(rsp.Proof.Hashes) +	if err != nil { +		return nil, fmt.Errorf("not a consistency proof: %v", err) +	} +	return &types.ConsistencyProof{ +		OldSize: req.OldSize, +		NewSize: req.NewSize, +		Path:    path, +	}, nil +} + +func (c *TrillianClient) GetInclusionProof(ctx context.Context, req *types.InclusionProofRequest) (*types.InclusionProof, error) { +	rsp, err := c.GRPC.GetInclusionProofByHash(ctx, &trillian.GetInclusionProofByHashRequest{ +		LogId:           c.TreeID, +		LeafHash:        req.LeafHash[:], +		TreeSize:        int64(req.TreeSize), +		OrderBySequence: true, +	}) +	if err != nil { +		return nil, fmt.Errorf("backend failure: %v", err) +	} +	if rsp == nil { +		return nil, fmt.Errorf("no response") +	} +	if len(rsp.Proof) != 1 { +		return nil, fmt.Errorf("bad proof count: %d", len(rsp.Proof)) +	} +	proof := rsp.Proof[0] +	if len(proof.Hashes) == 0 { +		return nil, fmt.Errorf("not an inclusion proof: empty") +	} +	path, err := nodePathFromHashes(proof.Hashes) +	if err != nil { +		return nil, fmt.Errorf("not an inclusion proof: %v", err) +	} +	return &types.InclusionProof{ +		TreeSize:  req.TreeSize, +		LeafIndex: uint64(proof.LeafIndex), +		Path:      path, +	}, nil +} + +func (c *TrillianClient) GetLeaves(ctx context.Context, req *types.LeavesRequest) (*types.LeafList, error) { +	rsp, err := c.GRPC.GetLeavesByRange(ctx, &trillian.GetLeavesByRangeRequest{ +		LogId:      c.TreeID, +		StartIndex: int64(req.StartSize), +		Count:      int64(req.EndSize-req.StartSize) + 1, +	}) +	if err != nil { +		return nil, fmt.Errorf("backend failure: %v", err) +	} +	if rsp == nil { +		return nil, fmt.Errorf("no response") +	} +	if got, want := len(rsp.Leaves), int(req.EndSize-req.StartSize+1); got != want { +		return nil, fmt.Errorf("unexpected number of leaves: %d", got) +	} +	var list types.LeafList +	for i, leaf := range rsp.Leaves { +		leafIndex := int64(req.StartSize + uint64(i)) +		if leafIndex != leaf.LeafIndex { +			return nil, fmt.Errorf("unexpected leaf(%d): got index %d", leafIndex, leaf.LeafIndex) +		} + +		var l types.Leaf +		if err := l.Unmarshal(leaf.LeafValue); err != nil { +			return nil, fmt.Errorf("unexpected leaf(%d): %v", leafIndex, err) +		} +		list = append(list[:], &l) +	} +	return &list, nil +} diff --git a/pkg/trillian/client_test.go b/pkg/trillian/client_test.go new file mode 100644 index 0000000..6b3d881 --- /dev/null +++ b/pkg/trillian/client_test.go @@ -0,0 +1,533 @@ +package trillian + +import ( +	"context" +	"fmt" +	"reflect" +	"testing" + +	"github.com/golang/mock/gomock" +	"github.com/google/trillian" +	ttypes "github.com/google/trillian/types" +	"github.com/system-transparency/stfe/pkg/mocks" +	"github.com/system-transparency/stfe/pkg/types" +	"google.golang.org/grpc/codes" +	"google.golang.org/grpc/status" +) + +func TestAddLeaf(t *testing.T) { +	req := &types.LeafRequest{ +		Message: types.Message{ +			ShardHint: 0, +			Checksum:  &[types.HashSize]byte{}, +		}, +		Signature:       &[types.SignatureSize]byte{}, +		VerificationKey: &[types.VerificationKeySize]byte{}, +		DomainHint:      "example.com", +	} +	for _, table := range []struct { +		description string +		req         *types.LeafRequest +		rsp         *trillian.QueueLeafResponse +		err         error +		wantErr     bool +	}{ +		{ +			description: "invalid: backend failure", +			req:         req, +			err:         fmt.Errorf("something went wrong"), +			wantErr:     true, +		}, +		{ +			description: "invalid: no response", +			req:         req, +			wantErr:     true, +		}, +		{ +			description: "invalid: no queued leaf", +			req:         req, +			rsp:         &trillian.QueueLeafResponse{}, +			wantErr:     true, +		}, +		{ +			description: "invalid: leaf is already queued or included", +			req:         req, +			rsp: &trillian.QueueLeafResponse{ +				QueuedLeaf: &trillian.QueuedLogLeaf{ +					Leaf: &trillian.LogLeaf{ +						LeafValue: req.Message.Marshal(), +					}, +					Status: status.New(codes.AlreadyExists, "duplicate").Proto(), +				}, +			}, +			wantErr: true, +		}, +		{ +			description: "valid", +			req:         req, +			rsp: &trillian.QueueLeafResponse{ +				QueuedLeaf: &trillian.QueuedLogLeaf{ +					Leaf: &trillian.LogLeaf{ +						LeafValue: req.Message.Marshal(), +					}, +					Status: status.New(codes.OK, "ok").Proto(), +				}, +			}, +		}, +	} { +		// Run deferred functions at the end of each iteration +		func() { +			ctrl := gomock.NewController(t) +			defer ctrl.Finish() +			grpc := mocks.NewMockTrillianLogClient(ctrl) +			grpc.EXPECT().QueueLeaf(gomock.Any(), gomock.Any()).Return(table.rsp, table.err) +			client := TrillianClient{GRPC: grpc} + +			err := client.AddLeaf(context.Background(), table.req) +			if got, want := err != nil, table.wantErr; got != want { +				t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) +			} +		}() +	} +} + +func TestGetTreeHead(t *testing.T) { +	// valid root +	root := &ttypes.LogRootV1{ +		TreeSize:       0, +		RootHash:       make([]byte, types.HashSize), +		TimestampNanos: 1622585623133599429, +	} +	buf, err := root.MarshalBinary() +	if err != nil { +		t.Fatalf("must marshal log root: %v", err) +	} +	// invalid root +	root.RootHash = make([]byte, types.HashSize+1) +	bufBadHash, err := root.MarshalBinary() +	if err != nil { +		t.Fatalf("must marshal log root: %v", err) +	} + +	for _, table := range []struct { +		description string +		rsp         *trillian.GetLatestSignedLogRootResponse +		err         error +		wantErr     bool +		wantTh      *types.TreeHead +	}{ +		{ +			description: "invalid: backend failure", +			err:         fmt.Errorf("something went wrong"), +			wantErr:     true, +		}, +		{ +			description: "invalid: no response", +			wantErr:     true, +		}, +		{ +			description: "invalid: no signed log root", +			rsp:         &trillian.GetLatestSignedLogRootResponse{}, +			wantErr:     true, +		}, +		{ +			description: "invalid: no log root", +			rsp: &trillian.GetLatestSignedLogRootResponse{ +				SignedLogRoot: &trillian.SignedLogRoot{}, +			}, +			wantErr: true, +		}, +		{ +			description: "invalid: no log root: unmarshal failed", +			rsp: &trillian.GetLatestSignedLogRootResponse{ +				SignedLogRoot: &trillian.SignedLogRoot{ +					LogRoot: buf[1:], +				}, +			}, +			wantErr: true, +		}, +		{ +			description: "invalid: unexpected hash length", +			rsp: &trillian.GetLatestSignedLogRootResponse{ +				SignedLogRoot: &trillian.SignedLogRoot{ +					LogRoot: bufBadHash, +				}, +			}, +			wantErr: true, +		}, +		{ +			description: "valid", +			rsp: &trillian.GetLatestSignedLogRootResponse{ +				SignedLogRoot: &trillian.SignedLogRoot{ +					LogRoot: buf, +				}, +			}, +			wantTh: &types.TreeHead{ +				Timestamp: 1622585623, +				TreeSize:  0, +				RootHash:  &[types.HashSize]byte{}, +			}, +		}, +	} { +		// Run deferred functions at the end of each iteration +		func() { +			ctrl := gomock.NewController(t) +			defer ctrl.Finish() +			grpc := mocks.NewMockTrillianLogClient(ctrl) +			grpc.EXPECT().GetLatestSignedLogRoot(gomock.Any(), gomock.Any()).Return(table.rsp, table.err) +			client := TrillianClient{GRPC: grpc} + +			th, err := client.GetTreeHead(context.Background()) +			if got, want := err != nil, table.wantErr; got != want { +				t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) +			} +			if err != nil { +				return +			} +			if got, want := th, table.wantTh; !reflect.DeepEqual(got, want) { +				t.Errorf("got tree head\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) +			} +		}() +	} +} + +func TestGetConsistencyProof(t *testing.T) { +	req := &types.ConsistencyProofRequest{ +		OldSize: 1, +		NewSize: 3, +	} +	for _, table := range []struct { +		description string +		req         *types.ConsistencyProofRequest +		rsp         *trillian.GetConsistencyProofResponse +		err         error +		wantErr     bool +		wantProof   *types.ConsistencyProof +	}{ +		{ +			description: "invalid: backend failure", +			req:         req, +			err:         fmt.Errorf("something went wrong"), +			wantErr:     true, +		}, +		{ +			description: "invalid: no response", +			req:         req, +			wantErr:     true, +		}, +		{ +			description: "invalid: no consistency proof", +			req:         req, +			rsp:         &trillian.GetConsistencyProofResponse{}, +			wantErr:     true, +		}, +		{ +			description: "invalid: not a consistency proof (1/2)", +			req:         req, +			rsp: &trillian.GetConsistencyProofResponse{ +				Proof: &trillian.Proof{ +					Hashes: [][]byte{}, +				}, +			}, +			wantErr: true, +		}, +		{ +			description: "invalid: not a consistency proof (2/2)", +			req:         req, +			rsp: &trillian.GetConsistencyProofResponse{ +				Proof: &trillian.Proof{ +					Hashes: [][]byte{ +						make([]byte, types.HashSize), +						make([]byte, types.HashSize+1), +					}, +				}, +			}, +			wantErr: true, +		}, +		{ +			description: "valid", +			req:         req, +			rsp: &trillian.GetConsistencyProofResponse{ +				Proof: &trillian.Proof{ +					Hashes: [][]byte{ +						make([]byte, types.HashSize), +						make([]byte, types.HashSize), +					}, +				}, +			}, +			wantProof: &types.ConsistencyProof{ +				OldSize: 1, +				NewSize: 3, +				Path: []*[types.HashSize]byte{ +					&[types.HashSize]byte{}, +					&[types.HashSize]byte{}, +				}, +			}, +		}, +	} { +		// Run deferred functions at the end of each iteration +		func() { +			ctrl := gomock.NewController(t) +			defer ctrl.Finish() +			grpc := mocks.NewMockTrillianLogClient(ctrl) +			grpc.EXPECT().GetConsistencyProof(gomock.Any(), gomock.Any()).Return(table.rsp, table.err) +			client := TrillianClient{GRPC: grpc} + +			proof, err := client.GetConsistencyProof(context.Background(), table.req) +			if got, want := err != nil, table.wantErr; got != want { +				t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) +			} +			if err != nil { +				return +			} +			if got, want := proof, table.wantProof; !reflect.DeepEqual(got, want) { +				t.Errorf("got proof\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) +			} +		}() +	} +} + +func TestGetInclusionProof(t *testing.T) { +	req := &types.InclusionProofRequest{ +		TreeSize: 4, +		LeafHash: &[types.HashSize]byte{}, +	} +	for _, table := range []struct { +		description string +		req         *types.InclusionProofRequest +		rsp         *trillian.GetInclusionProofByHashResponse +		err         error +		wantErr     bool +		wantProof   *types.InclusionProof +	}{ +		{ +			description: "invalid: backend failure", +			req:         req, +			err:         fmt.Errorf("something went wrong"), +			wantErr:     true, +		}, +		{ +			description: "invalid: no response", +			req:         req, +			wantErr:     true, +		}, +		{ +			description: "invalid: bad proof count", +			req:         req, +			rsp: &trillian.GetInclusionProofByHashResponse{ +				Proof: []*trillian.Proof{ +					&trillian.Proof{}, +					&trillian.Proof{}, +				}, +			}, +			wantErr: true, +		}, +		{ +			description: "invalid: not an inclusion proof (1/2)", +			req:         req, +			rsp: &trillian.GetInclusionProofByHashResponse{ +				Proof: []*trillian.Proof{ +					&trillian.Proof{ +						LeafIndex: 1, +						Hashes:    [][]byte{}, +					}, +				}, +			}, +			wantErr: true, +		}, +		{ +			description: "invalid: not an inclusion proof (2/2)", +			req:         req, +			rsp: &trillian.GetInclusionProofByHashResponse{ +				Proof: []*trillian.Proof{ +					&trillian.Proof{ +						LeafIndex: 1, +						Hashes: [][]byte{ +							make([]byte, types.HashSize), +							make([]byte, types.HashSize+1), +						}, +					}, +				}, +			}, +			wantErr: true, +		}, +		{ +			description: "valid", +			req:         req, +			rsp: &trillian.GetInclusionProofByHashResponse{ +				Proof: []*trillian.Proof{ +					&trillian.Proof{ +						LeafIndex: 1, +						Hashes: [][]byte{ +							make([]byte, types.HashSize), +							make([]byte, types.HashSize), +						}, +					}, +				}, +			}, +			wantProof: &types.InclusionProof{ +				TreeSize:  4, +				LeafIndex: 1, +				Path: []*[types.HashSize]byte{ +					&[types.HashSize]byte{}, +					&[types.HashSize]byte{}, +				}, +			}, +		}, +	} { +		// Run deferred functions at the end of each iteration +		func() { +			ctrl := gomock.NewController(t) +			defer ctrl.Finish() +			grpc := mocks.NewMockTrillianLogClient(ctrl) +			grpc.EXPECT().GetInclusionProofByHash(gomock.Any(), gomock.Any()).Return(table.rsp, table.err) +			client := TrillianClient{GRPC: grpc} + +			proof, err := client.GetInclusionProof(context.Background(), table.req) +			if got, want := err != nil, table.wantErr; got != want { +				t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) +			} +			if err != nil { +				return +			} +			if got, want := proof, table.wantProof; !reflect.DeepEqual(got, want) { +				t.Errorf("got proof\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) +			} +		}() +	} +} + +func TestGetLeaves(t *testing.T) { +	req := &types.LeavesRequest{ +		StartSize: 1, +		EndSize:   2, +	} +	firstLeaf := &types.Leaf{ +		Message: types.Message{ +			ShardHint: 0, +			Checksum:  &[types.HashSize]byte{}, +		}, +		SigIdent: types.SigIdent{ +			Signature: &[types.SignatureSize]byte{}, +			KeyHash:   &[types.HashSize]byte{}, +		}, +	} +	secondLeaf := &types.Leaf{ +		Message: types.Message{ +			ShardHint: 0, +			Checksum:  &[types.HashSize]byte{}, +		}, +		SigIdent: types.SigIdent{ +			Signature: &[types.SignatureSize]byte{}, +			KeyHash:   &[types.HashSize]byte{}, +		}, +	} + +	for _, table := range []struct { +		description string +		req         *types.LeavesRequest +		rsp         *trillian.GetLeavesByRangeResponse +		err         error +		wantErr     bool +		wantLeaves  *types.LeafList +	}{ +		{ +			description: "invalid: backend failure", +			req:         req, +			err:         fmt.Errorf("something went wrong"), +			wantErr:     true, +		}, +		{ +			description: "invalid: no response", +			req:         req, +			wantErr:     true, +		}, +		{ +			description: "invalid: unexpected number of leaves", +			req:         req, +			rsp: &trillian.GetLeavesByRangeResponse{ +				Leaves: []*trillian.LogLeaf{ +					&trillian.LogLeaf{ +						LeafValue: firstLeaf.Marshal(), +						LeafIndex: 1, +					}, +				}, +			}, +			wantErr: true, +		}, +		{ +			description: "invalid: unexpected leaf (1/2)", +			req:         req, +			rsp: &trillian.GetLeavesByRangeResponse{ +				Leaves: []*trillian.LogLeaf{ +					&trillian.LogLeaf{ +						LeafValue: firstLeaf.Marshal(), +						LeafIndex: 1, +					}, +					&trillian.LogLeaf{ +						LeafValue: secondLeaf.Marshal(), +						LeafIndex: 3, +					}, +				}, +			}, +			wantErr: true, +		}, +		{ +			description: "invalid: unexpected leaf (2/2)", +			req:         req, +			rsp: &trillian.GetLeavesByRangeResponse{ +				Leaves: []*trillian.LogLeaf{ +					&trillian.LogLeaf{ +						LeafValue: firstLeaf.Marshal(), +						LeafIndex: 1, +					}, +					&trillian.LogLeaf{ +						LeafValue: secondLeaf.Marshal()[1:], +						LeafIndex: 2, +					}, +				}, +			}, +			wantErr: true, +		}, +		{ +			description: "valid", +			req:         req, +			rsp: &trillian.GetLeavesByRangeResponse{ +				Leaves: []*trillian.LogLeaf{ +					&trillian.LogLeaf{ +						LeafValue: firstLeaf.Marshal(), +						LeafIndex: 1, +					}, +					&trillian.LogLeaf{ +						LeafValue: secondLeaf.Marshal(), +						LeafIndex: 2, +					}, +				}, +			}, +			wantLeaves: &types.LeafList{ +				firstLeaf, +				secondLeaf, +			}, +		}, +	} { +		// Run deferred functions at the end of each iteration +		func() { +			ctrl := gomock.NewController(t) +			defer ctrl.Finish() +			grpc := mocks.NewMockTrillianLogClient(ctrl) +			grpc.EXPECT().GetLeavesByRange(gomock.Any(), gomock.Any()).Return(table.rsp, table.err) +			client := TrillianClient{GRPC: grpc} + +			leaves, err := client.GetLeaves(context.Background(), table.req) +			if got, want := err != nil, table.wantErr; got != want { +				t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) +			} +			if err != nil { +				return +			} +			if got, want := leaves, table.wantLeaves; !reflect.DeepEqual(got, want) { +				t.Errorf("got leaves\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) +			} +		}() +	} +} diff --git a/pkg/trillian/util.go b/pkg/trillian/util.go new file mode 100644 index 0000000..4cf31fb --- /dev/null +++ b/pkg/trillian/util.go @@ -0,0 +1,33 @@ +package trillian + +import ( +	"fmt" + +	trillian "github.com/google/trillian/types" +	siglog "github.com/system-transparency/stfe/pkg/types" +) + +func treeHeadFromLogRoot(lr *trillian.LogRootV1) *siglog.TreeHead { +	var hash [siglog.HashSize]byte +	th := siglog.TreeHead{ +		Timestamp: uint64(lr.TimestampNanos / 1000 / 1000 / 1000), +		TreeSize:  uint64(lr.TreeSize), +		RootHash:  &hash, +	} +	copy(th.RootHash[:], lr.RootHash) +	return &th +} + +func nodePathFromHashes(hashes [][]byte) ([]*[siglog.HashSize]byte, error) { +	var path []*[siglog.HashSize]byte +	for _, hash := range hashes { +		if len(hash) != siglog.HashSize { +			return nil, fmt.Errorf("unexpected hash length: %v", len(hash)) +		} + +		var h [siglog.HashSize]byte +		copy(h[:], hash) +		path = append(path, &h) +	} +	return path, nil +} diff --git a/pkg/types/ascii.go b/pkg/types/ascii.go new file mode 100644 index 0000000..d27d79b --- /dev/null +++ b/pkg/types/ascii.go @@ -0,0 +1,421 @@ +package types + +import ( +	"bytes" +	"encoding/hex" +	"fmt" +	"io" +	"io/ioutil" +	"strconv" +) + +const ( +	// Delim is a key-value separator +	Delim = "=" + +	// EOL is a line sepator +	EOL = "\n" + +	// NumField* is the number of unique keys in an incoming ASCII message +	NumFieldLeaf                    = 4 +	NumFieldSignedTreeHead          = 5 +	NumFieldConsistencyProof        = 3 +	NumFieldInclusionProof          = 3 +	NumFieldLeavesRequest           = 2 +	NumFieldInclusionProofRequest   = 2 +	NumFieldConsistencyProofRequest = 2 +	NumFieldLeafRequest             = 5 +	NumFieldCosignatureRequest      = 2 + +	// New leaf keys +	ShardHint            = "shard_hint" +	Checksum             = "checksum" +	SignatureOverMessage = "signature_over_message" +	VerificationKey      = "verification_key" +	DomainHint           = "domain_hint" + +	// Inclusion proof keys +	LeafHash      = "leaf_hash" +	LeafIndex     = "leaf_index" +	InclusionPath = "inclusion_path" + +	// Consistency proof keys +	NewSize         = "new_size" +	OldSize         = "old_size" +	ConsistencyPath = "consistency_path" + +	// Range of leaves keys +	StartSize = "start_size" +	EndSize   = "end_size" + +	// Tree head keys +	Timestamp = "timestamp" +	TreeSize  = "tree_size" +	RootHash  = "root_hash" + +	// Signature and signer-identity keys +	Signature = "signature" +	KeyHash   = "key_hash" +) + +// MessageASCI is a wrapper that manages ASCII key-value pairs +type MessageASCII struct { +	m map[string][]string +} + +// NewMessageASCII unpacks an incoming ASCII message +func NewMessageASCII(r io.Reader, numFieldExpected int) (*MessageASCII, error) { +	buf, err := ioutil.ReadAll(r) +	if err != nil { +		return nil, fmt.Errorf("ReadAll: %v", err) +	} +	lines := bytes.Split(buf, []byte(EOL)) +	if len(lines) <= 1 { +		return nil, fmt.Errorf("Not enough lines: empty") +	} +	lines = lines[:len(lines)-1] // valid message => split gives empty last line + +	msg := MessageASCII{make(map[string][]string)} +	for _, line := range lines { +		split := bytes.Index(line, []byte(Delim)) +		if split == -1 { +			return nil, fmt.Errorf("invalid line: %v", string(line)) +		} + +		key := string(line[:split]) +		value := string(line[split+len(Delim):]) +		values, ok := msg.m[key] +		if !ok { +			values = nil +			msg.m[key] = values +		} +		msg.m[key] = append(values, value) +	} + +	if msg.NumField() != numFieldExpected { +		return nil, fmt.Errorf("Unexpected number of keys: %v", msg.NumField()) +	} +	return &msg, nil +} + +// NumField returns the number of unique keys +func (msg *MessageASCII) NumField() int { +	return len(msg.m) +} + +// GetStrings returns a list of strings +func (msg *MessageASCII) GetStrings(key string) []string { +	strs, ok := msg.m[key] +	if !ok { +		return nil +	} +	return strs +} + +// GetString unpacks a string +func (msg *MessageASCII) GetString(key string) (string, error) { +	strs := msg.GetStrings(key) +	if len(strs) != 1 { +		return "", fmt.Errorf("expected one string: %v", strs) +	} +	return strs[0], nil +} + +// GetUint64 unpacks an uint64 +func (msg *MessageASCII) GetUint64(key string) (uint64, error) { +	str, err := msg.GetString(key) +	if err != nil { +		return 0, fmt.Errorf("GetString: %v", err) +	} +	num, err := strconv.ParseUint(str, 10, 64) +	if err != nil { +		return 0, fmt.Errorf("ParseUint: %v", err) +	} +	return num, nil +} + +// GetHash unpacks a hash +func (msg *MessageASCII) GetHash(key string) (*[HashSize]byte, error) { +	str, err := msg.GetString(key) +	if err != nil { +		return nil, fmt.Errorf("GetString: %v", err) +	} + +	var hash [HashSize]byte +	if err := decodeHex(str, hash[:]); err != nil { +		return nil, fmt.Errorf("decodeHex: %v", err) +	} +	return &hash, nil +} + +// GetSignature unpacks a signature +func (msg *MessageASCII) GetSignature(key string) (*[SignatureSize]byte, error) { +	str, err := msg.GetString(key) +	if err != nil { +		return nil, fmt.Errorf("GetString: %v", err) +	} + +	var signature [SignatureSize]byte +	if err := decodeHex(str, signature[:]); err != nil { +		return nil, fmt.Errorf("decodeHex: %v", err) +	} +	return &signature, nil +} + +// GetVerificationKey unpacks a verification key +func (msg *MessageASCII) GetVerificationKey(key string) (*[VerificationKeySize]byte, error) { +	str, err := msg.GetString(key) +	if err != nil { +		return nil, fmt.Errorf("GetString: %v", err) +	} + +	var vk [VerificationKeySize]byte +	if err := decodeHex(str, vk[:]); err != nil { +		return nil, fmt.Errorf("decodeHex: %v", err) +	} +	return &vk, nil +} + +// decodeHex decodes a hex-encoded string into an already-sized byte slice +func decodeHex(str string, out []byte) error { +	buf, err := hex.DecodeString(str) +	if err != nil { +		return fmt.Errorf("DecodeString: %v", err) +	} +	if len(buf) != len(out) { +		return fmt.Errorf("invalid length: %v", len(buf)) +	} +	copy(out, buf) +	return nil +} + +/* + * + * MarshalASCII wrappers for types that the log server outputs + * + */ +func (l *Leaf) MarshalASCII(w io.Writer) error { +	if err := writeASCII(w, ShardHint, strconv.FormatUint(l.ShardHint, 10)); err != nil { +		return fmt.Errorf("writeASCII: %v", err) +	} +	if err := writeASCII(w, Checksum, hex.EncodeToString(l.Checksum[:])); err != nil { +		return fmt.Errorf("writeASCII: %v", err) +	} +	if err := writeASCII(w, SignatureOverMessage, hex.EncodeToString(l.Signature[:])); err != nil { +		return fmt.Errorf("writeASCII: %v", err) +	} +	if err := writeASCII(w, KeyHash, hex.EncodeToString(l.KeyHash[:])); err != nil { +		return fmt.Errorf("writeASCII: %v", err) +	} +	return nil +} + +func (sth *SignedTreeHead) MarshalASCII(w io.Writer) error { +	if err := writeASCII(w, Timestamp, strconv.FormatUint(sth.Timestamp, 10)); err != nil { +		return fmt.Errorf("writeASCII: %v", err) +	} +	if err := writeASCII(w, TreeSize, strconv.FormatUint(sth.TreeSize, 10)); err != nil { +		return fmt.Errorf("writeASCII: %v", err) +	} +	if err := writeASCII(w, RootHash, hex.EncodeToString(sth.RootHash[:])); err != nil { +		return fmt.Errorf("writeASCII: %v", err) +	} +	for _, sigident := range sth.SigIdent { +		if err := sigident.MarshalASCII(w); err != nil { +			return fmt.Errorf("MarshalASCII: %v", err) +		} +	} +	return nil +} + +func (si *SigIdent) MarshalASCII(w io.Writer) error { +	if err := writeASCII(w, Signature, hex.EncodeToString(si.Signature[:])); err != nil { +		return fmt.Errorf("writeASCII: %v", err) +	} +	if err := writeASCII(w, KeyHash, hex.EncodeToString(si.KeyHash[:])); err != nil { +		return fmt.Errorf("writeASCII: %v", err) +	} +	return nil +} + +func (p *ConsistencyProof) MarshalASCII(w io.Writer) error { +	if err := writeASCII(w, NewSize, strconv.FormatUint(p.NewSize, 10)); err != nil { +		return fmt.Errorf("writeASCII: %v", err) +	} +	if err := writeASCII(w, OldSize, strconv.FormatUint(p.OldSize, 10)); err != nil { +		return fmt.Errorf("writeASCII: %v", err) +	} +	for _, hash := range p.Path { +		if err := writeASCII(w, ConsistencyPath, hex.EncodeToString(hash[:])); err != nil { +			return fmt.Errorf("writeASCII: %v", err) +		} +	} +	return nil +} + +func (p *InclusionProof) MarshalASCII(w io.Writer) error { +	if err := writeASCII(w, TreeSize, strconv.FormatUint(p.TreeSize, 10)); err != nil { +		return fmt.Errorf("writeASCII: %v", err) +	} +	if err := writeASCII(w, LeafIndex, strconv.FormatUint(p.LeafIndex, 10)); err != nil { +		return fmt.Errorf("writeASCII: %v", err) +	} +	for _, hash := range p.Path { +		if err := writeASCII(w, InclusionPath, hex.EncodeToString(hash[:])); err != nil { +			return fmt.Errorf("writeASCII: %v", err) +		} +	} +	return nil +} + +func writeASCII(w io.Writer, key, value string) error { +	if _, err := fmt.Fprintf(w, "%s%s%s%s", key, Delim, value, EOL); err != nil { +		return fmt.Errorf("Fprintf: %v", err) +	} +	return nil +} + +/* + * + * Unmarshal ASCII wrappers that the log server and/or log clients receive. + * + */ +func (ll *LeafList) UnmarshalASCII(r io.Reader) error { +	return nil +} + +func (sth *SignedTreeHead) UnmarshalASCII(r io.Reader) error { +	msg, err := NewMessageASCII(r, NumFieldSignedTreeHead) +	if err != nil { +		return fmt.Errorf("NewMessageASCII: %v", err) +	} + +	// TreeHead +	if sth.Timestamp, err = msg.GetUint64(Timestamp); err != nil { +		return fmt.Errorf("GetUint64(Timestamp): %v", err) +	} +	if sth.TreeSize, err = msg.GetUint64(TreeSize); err != nil { +		return fmt.Errorf("GetUint64(TreeSize): %v", err) +	} +	if sth.RootHash, err = msg.GetHash(RootHash); err != nil { +		return fmt.Errorf("GetHash(RootHash): %v", err) +	} + +	// SigIdent +	signatures := msg.GetStrings(Signature) +	if len(signatures) == 0 { +		return fmt.Errorf("no signer") +	} +	keyHashes := msg.GetStrings(KeyHash) +	if len(signatures) != len(keyHashes) { +		return fmt.Errorf("mismatched signature-signer count") +	} +	sth.SigIdent = make([]*SigIdent, 0, len(signatures)) +	for i, n := 0, len(signatures); i < n; i++ { +		var signature [SignatureSize]byte +		if err := decodeHex(signatures[i], signature[:]); err != nil { +			return fmt.Errorf("decodeHex: %v", err) +		} +		var hash [HashSize]byte +		if err := decodeHex(keyHashes[i], hash[:]); err != nil { +			return fmt.Errorf("decodeHex: %v", err) +		} +		sth.SigIdent = append(sth.SigIdent, &SigIdent{ +			Signature: &signature, +			KeyHash:   &hash, +		}) +	} +	return nil +} + +func (p *InclusionProof) UnmarshalASCII(r io.Reader) error { +	return nil +} + +func (p *ConsistencyProof) UnmarshalASCII(r io.Reader) error { +	return nil +} + +func (req *InclusionProofRequest) UnmarshalASCII(r io.Reader) error { +	msg, err := NewMessageASCII(r, NumFieldInclusionProofRequest) +	if err != nil { +		return fmt.Errorf("NewMessageASCII: %v", err) +	} + +	if req.LeafHash, err = msg.GetHash(LeafHash); err != nil { +		return fmt.Errorf("GetHash(LeafHash): %v", err) +	} +	if req.TreeSize, err = msg.GetUint64(TreeSize); err != nil { +		return fmt.Errorf("GetUint64(TreeSize): %v", err) +	} +	return nil +} + +func (req *ConsistencyProofRequest) UnmarshalASCII(r io.Reader) error { +	msg, err := NewMessageASCII(r, NumFieldConsistencyProofRequest) +	if err != nil { +		return fmt.Errorf("NewMessageASCII: %v", err) +	} + +	if req.NewSize, err = msg.GetUint64(NewSize); err != nil { +		return fmt.Errorf("GetUint64(NewSize): %v", err) +	} +	if req.OldSize, err = msg.GetUint64(OldSize); err != nil { +		return fmt.Errorf("GetUint64(OldSize): %v", err) +	} +	return nil +} + +func (req *LeavesRequest) UnmarshalASCII(r io.Reader) error { +	msg, err := NewMessageASCII(r, NumFieldLeavesRequest) +	if err != nil { +		return fmt.Errorf("NewMessageASCII: %v", err) +	} + +	if req.StartSize, err = msg.GetUint64(StartSize); err != nil { +		return fmt.Errorf("GetUint64(StartSize): %v", err) +	} +	if req.EndSize, err = msg.GetUint64(EndSize); err != nil { +		return fmt.Errorf("GetUint64(EndSize): %v", err) +	} +	return nil +} + +func (req *LeafRequest) UnmarshalASCII(r io.Reader) error { +	msg, err := NewMessageASCII(r, NumFieldLeafRequest) +	if err != nil { +		return fmt.Errorf("NewMessageASCII: %v", err) +	} + +	if req.ShardHint, err = msg.GetUint64(ShardHint); err != nil { +		return fmt.Errorf("GetUint64(ShardHint): %v", err) +	} +	if req.Checksum, err = msg.GetHash(Checksum); err != nil { +		return fmt.Errorf("GetHash(Checksum): %v", err) +	} +	if req.Signature, err = msg.GetSignature(SignatureOverMessage); err != nil { +		return fmt.Errorf("GetSignature: %v", err) +	} +	if req.VerificationKey, err = msg.GetVerificationKey(VerificationKey); err != nil { +		return fmt.Errorf("GetVerificationKey: %v", err) +	} +	if req.DomainHint, err = msg.GetString(DomainHint); err != nil { +		return fmt.Errorf("GetString(DomainHint): %v", err) +	} +	return nil +} + +func (req *CosignatureRequest) UnmarshalASCII(r io.Reader) error { +	msg, err := NewMessageASCII(r, NumFieldCosignatureRequest) +	if err != nil { +		return fmt.Errorf("NewMessageASCII: %v", err) +	} + +	if req.Signature, err = msg.GetSignature(Signature); err != nil { +		return fmt.Errorf("GetSignature: %v", err) +	} +	if req.KeyHash, err = msg.GetHash(KeyHash); err != nil { +		return fmt.Errorf("GetHash(KeyHash): %v", err) +	} +	return nil +} diff --git a/pkg/types/ascii_test.go b/pkg/types/ascii_test.go new file mode 100644 index 0000000..92732f9 --- /dev/null +++ b/pkg/types/ascii_test.go @@ -0,0 +1,465 @@ +package types + +import ( +	"bytes" +	"fmt" +	"io" +	"reflect" +	"testing" +) + +/* + * + * MessageASCII methods and helpers + * + */ +func TestNewMessageASCII(t *testing.T) { +	for _, table := range []struct { +		description string +		input       io.Reader +		wantErr     bool +		wantMap     map[string][]string +	}{ +		{ +			description: "invalid: not enough lines", +			input:       bytes.NewBufferString(""), +			wantErr:     true, +		}, +		{ +			description: "invalid: lines must end with new line", +			input:       bytes.NewBufferString("k1=v1\nk2=v2"), +			wantErr:     true, +		}, +		{ +			description: "invalid: lines must not be empty", +			input:       bytes.NewBufferString("k1=v1\n\nk2=v2\n"), +			wantErr:     true, +		}, +		{ +			description: "invalid: wrong number of fields", +			input:       bytes.NewBufferString("k1=v1\n"), +			wantErr:     true, +		}, +		{ +			description: "valid", +			input:       bytes.NewBufferString("k1=v1\nk2=v2\nk2=v3=4\n"), +			wantMap: map[string][]string{ +				"k1": []string{"v1"}, +				"k2": []string{"v2", "v3=4"}, +			}, +		}, +	} { +		msg, err := NewMessageASCII(table.input, len(table.wantMap)) +		if got, want := err != nil, table.wantErr; got != want { +			t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) +		} +		if err != nil { +			continue +		} +		if got, want := msg.m, table.wantMap; !reflect.DeepEqual(got, want) { +			t.Errorf("got\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) +		} +	} +} + +func TestNumField(t *testing.T)           {} +func TestGetStrings(t *testing.T)         {} +func TestGetString(t *testing.T)          {} +func TestGetUint64(t *testing.T)          {} +func TestGetHash(t *testing.T)            {} +func TestGetSignature(t *testing.T)       {} +func TestGetVerificationKey(t *testing.T) {} +func TestDecodeHex(t *testing.T)          {} + +/* + * + * MarshalASCII methods and helpers + * + */ +func TestLeafMarshalASCII(t *testing.T) { +	description := "valid: two leaves" +	leafList := []*Leaf{ +		&Leaf{ +			Message: Message{ +				ShardHint: 123, +				Checksum:  testBuffer32, +			}, +			SigIdent: SigIdent{ +				Signature: testBuffer64, +				KeyHash:   testBuffer32, +			}, +		}, +		&Leaf{ +			Message: Message{ +				ShardHint: 456, +				Checksum:  testBuffer32, +			}, +			SigIdent: SigIdent{ +				Signature: testBuffer64, +				KeyHash:   testBuffer32, +			}, +		}, +	} +	wantBuf := bytes.NewBufferString(fmt.Sprintf( +		"%s%s%d%s"+"%s%s%x%s"+"%s%s%x%s"+"%s%s%x%s"+ +			"%s%s%d%s"+"%s%s%x%s"+"%s%s%x%s"+"%s%s%x%s", +		// Leaf 1 +		ShardHint, Delim, 123, EOL, +		Checksum, Delim, testBuffer32[:], EOL, +		SignatureOverMessage, Delim, testBuffer64[:], EOL, +		KeyHash, Delim, testBuffer32[:], EOL, +		// Leaf 2 +		ShardHint, Delim, 456, EOL, +		Checksum, Delim, testBuffer32[:], EOL, +		SignatureOverMessage, Delim, testBuffer64[:], EOL, +		KeyHash, Delim, testBuffer32[:], EOL, +	)) +	buf := bytes.NewBuffer(nil) +	for _, leaf := range leafList { +		if err := leaf.MarshalASCII(buf); err != nil { +			t.Errorf("expected error %v but got %v in test %q: %v", false, true, description, err) +			return +		} +	} +	if got, want := buf.Bytes(), wantBuf.Bytes(); !bytes.Equal(got, want) { +		t.Errorf("got\n\t%v\nbut wanted\n\t%v\nin test %q", string(got), string(want), description) +	} +} + +func TestSignedTreeHeadMarshalASCII(t *testing.T) { +	description := "valid" +	sth := &SignedTreeHead{ +		TreeHead: TreeHead{ +			Timestamp: 123, +			TreeSize:  456, +			RootHash:  testBuffer32, +		}, +		SigIdent: []*SigIdent{ +			&SigIdent{ +				Signature: testBuffer64, +				KeyHash:   testBuffer32, +			}, +			&SigIdent{ +				Signature: testBuffer64, +				KeyHash:   testBuffer32, +			}, +		}, +	} +	wantBuf := bytes.NewBufferString(fmt.Sprintf( +		"%s%s%d%s"+"%s%s%d%s"+"%s%s%x%s"+"%s%s%x%s"+"%s%s%x%s"+"%s%s%x%s"+"%s%s%x%s", +		Timestamp, Delim, 123, EOL, +		TreeSize, Delim, 456, EOL, +		RootHash, Delim, testBuffer32[:], EOL, +		Signature, Delim, testBuffer64[:], EOL, +		KeyHash, Delim, testBuffer32[:], EOL, +		Signature, Delim, testBuffer64[:], EOL, +		KeyHash, Delim, testBuffer32[:], EOL, +	)) +	buf := bytes.NewBuffer(nil) +	if err := sth.MarshalASCII(buf); err != nil { +		t.Errorf("expected error %v but got %v in test %q", false, true, description) +		return +	} +	if got, want := buf.Bytes(), wantBuf.Bytes(); !bytes.Equal(got, want) { +		t.Errorf("got\n\t%v\nbut wanted\n\t%v\nin test %q", string(got), string(want), description) +	} +} + +func TestInclusionProofMarshalASCII(t *testing.T) { +	description := "valid" +	proof := InclusionProof{ +		TreeSize:  321, +		LeafIndex: 123, +		Path: []*[HashSize]byte{ +			testBuffer32, +			testBuffer32, +		}, +	} +	wantBuf := bytes.NewBufferString(fmt.Sprintf( +		"%s%s%d%s"+"%s%s%d%s"+"%s%s%x%s"+"%s%s%x%s", +		TreeSize, Delim, 321, EOL, +		LeafIndex, Delim, 123, EOL, +		InclusionPath, Delim, testBuffer32[:], EOL, +		InclusionPath, Delim, testBuffer32[:], EOL, +	)) +	buf := bytes.NewBuffer(nil) +	if err := proof.MarshalASCII(buf); err != nil { +		t.Errorf("expected error %v but got %v in test %q", false, true, description) +		return +	} +	if got, want := buf.Bytes(), wantBuf.Bytes(); !bytes.Equal(got, want) { +		t.Errorf("got\n\t%v\nbut wanted\n\t%v\nin test %q", string(got), string(want), description) +	} +} + +func TestConsistencyProofMarshalASCII(t *testing.T) { +	description := "valid" +	proof := ConsistencyProof{ +		NewSize: 321, +		OldSize: 123, +		Path: []*[HashSize]byte{ +			testBuffer32, +			testBuffer32, +		}, +	} +	wantBuf := bytes.NewBufferString(fmt.Sprintf( +		"%s%s%d%s"+"%s%s%d%s"+"%s%s%x%s"+"%s%s%x%s", +		NewSize, Delim, 321, EOL, +		OldSize, Delim, 123, EOL, +		ConsistencyPath, Delim, testBuffer32[:], EOL, +		ConsistencyPath, Delim, testBuffer32[:], EOL, +	)) +	buf := bytes.NewBuffer(nil) +	if err := proof.MarshalASCII(buf); err != nil { +		t.Errorf("expected error %v but got %v in test %q", false, true, description) +		return +	} +	if got, want := buf.Bytes(), wantBuf.Bytes(); !bytes.Equal(got, want) { +		t.Errorf("got\n\t%v\nbut wanted\n\t%v\nin test %q", string(got), string(want), description) +	} +} + +func TestWriteASCII(t *testing.T) { +} + +/* + * + * UnmarshalASCII methods and helpers + * + */ +func TestLeafListUnmarshalASCII(t *testing.T) {} + +func TestSignedTreeHeadUnmarshalASCII(t *testing.T) { +	for _, table := range []struct { +		description string +		buf         io.Reader +		wantErr     bool +		wantSth     *SignedTreeHead +	}{ +		{ +			description: "valid", +			buf: bytes.NewBufferString(fmt.Sprintf( +				"%s%s%d%s"+"%s%s%d%s"+"%s%s%x%s"+"%s%s%x%s"+"%s%s%x%s"+"%s%s%x%s"+"%s%s%x%s", +				Timestamp, Delim, 123, EOL, +				TreeSize, Delim, 456, EOL, +				RootHash, Delim, testBuffer32[:], EOL, +				Signature, Delim, testBuffer64[:], EOL, +				KeyHash, Delim, testBuffer32[:], EOL, +				Signature, Delim, testBuffer64[:], EOL, +				KeyHash, Delim, testBuffer32[:], EOL, +			)), +			wantSth: &SignedTreeHead{ +				TreeHead: TreeHead{ +					Timestamp: 123, +					TreeSize:  456, +					RootHash:  testBuffer32, +				}, +				SigIdent: []*SigIdent{ +					&SigIdent{ +						Signature: testBuffer64, +						KeyHash:   testBuffer32, +					}, +					&SigIdent{ +						Signature: testBuffer64, +						KeyHash:   testBuffer32, +					}, +				}, +			}, +		}, +	} { +		var sth SignedTreeHead +		err := sth.UnmarshalASCII(table.buf) +		if got, want := err != nil, table.wantErr; got != want { +			t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) +		} +		if err != nil { +			continue +		} +		if got, want := &sth, table.wantSth; !reflect.DeepEqual(got, want) { +			t.Errorf("got\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) +		} +	} +} + +func TestInclusionProofUnmarshalASCII(t *testing.T)   {} +func TestConsistencyProofUnmarshalASCII(t *testing.T) {} + +func TestInclusionProofRequestUnmarshalASCII(t *testing.T) { +	for _, table := range []struct { +		description string +		buf         io.Reader +		wantErr     bool +		wantReq     *InclusionProofRequest +	}{ +		{ +			description: "valid", +			buf: bytes.NewBufferString(fmt.Sprintf( +				"%s%s%x%s"+"%s%s%d%s", +				LeafHash, Delim, testBuffer32[:], EOL, +				TreeSize, Delim, 123, EOL, +			)), +			wantReq: &InclusionProofRequest{ +				LeafHash: testBuffer32, +				TreeSize: 123, +			}, +		}, +	} { +		var req InclusionProofRequest +		err := req.UnmarshalASCII(table.buf) +		if got, want := err != nil, table.wantErr; got != want { +			t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) +		} +		if err != nil { +			continue +		} +		if got, want := &req, table.wantReq; !reflect.DeepEqual(got, want) { +			t.Errorf("got\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) +		} +	} +} + +func TestConsistencyProofRequestUnmarshalASCII(t *testing.T) { +	for _, table := range []struct { +		description string +		buf         io.Reader +		wantErr     bool +		wantReq     *ConsistencyProofRequest +	}{ +		{ +			description: "valid", +			buf: bytes.NewBufferString(fmt.Sprintf( +				"%s%s%d%s"+"%s%s%d%s", +				NewSize, Delim, 321, EOL, +				OldSize, Delim, 123, EOL, +			)), +			wantReq: &ConsistencyProofRequest{ +				NewSize: 321, +				OldSize: 123, +			}, +		}, +	} { +		var req ConsistencyProofRequest +		err := req.UnmarshalASCII(table.buf) +		if got, want := err != nil, table.wantErr; got != want { +			t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) +		} +		if err != nil { +			continue +		} +		if got, want := &req, table.wantReq; !reflect.DeepEqual(got, want) { +			t.Errorf("got\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) +		} +	} +} + +func TestLeavesRequestUnmarshalASCII(t *testing.T) { +	for _, table := range []struct { +		description string +		buf         io.Reader +		wantErr     bool +		wantReq     *LeavesRequest +	}{ +		{ +			description: "valid", +			buf: bytes.NewBufferString(fmt.Sprintf( +				"%s%s%d%s"+"%s%s%d%s", +				StartSize, Delim, 123, EOL, +				EndSize, Delim, 456, EOL, +			)), +			wantReq: &LeavesRequest{ +				StartSize: 123, +				EndSize:   456, +			}, +		}, +	} { +		var req LeavesRequest +		err := req.UnmarshalASCII(table.buf) +		if got, want := err != nil, table.wantErr; got != want { +			t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) +		} +		if err != nil { +			continue +		} +		if got, want := &req, table.wantReq; !reflect.DeepEqual(got, want) { +			t.Errorf("got\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) +		} +	} +} + +func TestLeafRequestUnmarshalASCII(t *testing.T) { +	for _, table := range []struct { +		description string +		buf         io.Reader +		wantErr     bool +		wantReq     *LeafRequest +	}{ +		{ +			description: "valid", +			buf: bytes.NewBufferString(fmt.Sprintf( +				"%s%s%d%s"+"%s%s%x%s"+"%s%s%x%s"+"%s%s%x%s"+"%s%s%s%s", +				ShardHint, Delim, 123, EOL, +				Checksum, Delim, testBuffer32[:], EOL, +				SignatureOverMessage, Delim, testBuffer64[:], EOL, +				VerificationKey, Delim, testBuffer32[:], EOL, +				DomainHint, Delim, "example.com", EOL, +			)), +			wantReq: &LeafRequest{ +				Message: Message{ +					ShardHint: 123, +					Checksum:  testBuffer32, +				}, +				Signature:       testBuffer64, +				VerificationKey: testBuffer32, +				DomainHint:      "example.com", +			}, +		}, +	} { +		var req LeafRequest +		err := req.UnmarshalASCII(table.buf) +		if got, want := err != nil, table.wantErr; got != want { +			t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) +		} +		if err != nil { +			continue +		} +		if got, want := &req, table.wantReq; !reflect.DeepEqual(got, want) { +			t.Errorf("got\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) +		} +	} +} + +func TestCosignatureRequestUnmarshalASCII(t *testing.T) { +	for _, table := range []struct { +		description string +		buf         io.Reader +		wantErr     bool +		wantReq     *CosignatureRequest +	}{ +		{ +			description: "valid", +			buf: bytes.NewBufferString(fmt.Sprintf( +				"%s%s%x%s"+"%s%s%x%s", +				Signature, Delim, testBuffer64[:], EOL, +				KeyHash, Delim, testBuffer32[:], EOL, +			)), +			wantReq: &CosignatureRequest{ +				SigIdent: SigIdent{ +					Signature: testBuffer64, +					KeyHash:   testBuffer32, +				}, +			}, +		}, +	} { +		var req CosignatureRequest +		err := req.UnmarshalASCII(table.buf) +		if got, want := err != nil, table.wantErr; got != want { +			t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) +		} +		if err != nil { +			continue +		} +		if got, want := &req, table.wantReq; !reflect.DeepEqual(got, want) { +			t.Errorf("got\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) +		} +	} +} diff --git a/pkg/types/trunnel.go b/pkg/types/trunnel.go new file mode 100644 index 0000000..268f6f7 --- /dev/null +++ b/pkg/types/trunnel.go @@ -0,0 +1,60 @@ +package types + +import ( +	"encoding/binary" +	"fmt" +) + +const ( +	// MessageSize is the number of bytes in a Trunnel-encoded leaf message +	MessageSize = 8 + HashSize +	// LeafSize is the number of bytes in a Trunnel-encoded leaf +	LeafSize = MessageSize + SignatureSize + HashSize +) + +// Marshal returns a Trunnel-encoded message +func (m *Message) Marshal() []byte { +	buf := make([]byte, MessageSize) +	binary.BigEndian.PutUint64(buf, m.ShardHint) +	copy(buf[8:], m.Checksum[:]) +	return buf +} + +// Marshal returns a Trunnel-encoded leaf +func (l *Leaf) Marshal() []byte { +	buf := l.Message.Marshal() +	buf = append(buf, l.SigIdent.Signature[:]...) +	buf = append(buf, l.SigIdent.KeyHash[:]...) +	return buf +} + +// Marshal returns a Trunnel-encoded tree head +func (th *TreeHead) Marshal() []byte { +	buf := make([]byte, 8+8+HashSize) +	binary.BigEndian.PutUint64(buf[0:8], th.Timestamp) +	binary.BigEndian.PutUint64(buf[8:16], th.TreeSize) +	copy(buf[16:], th.RootHash[:]) +	return buf +} + +// Unmarshal parses the Trunnel-encoded buffer as a leaf +func (l *Leaf) Unmarshal(buf []byte) error { +	if len(buf) != LeafSize { +		return fmt.Errorf("invalid leaf size: %v", len(buf)) +	} +	// Shard hint +	l.ShardHint = binary.BigEndian.Uint64(buf) +	offset := 8 +	// Checksum +	l.Checksum = &[HashSize]byte{} +	copy(l.Checksum[:], buf[offset:offset+HashSize]) +	offset += HashSize +	// Signature +	l.Signature = &[SignatureSize]byte{} +	copy(l.Signature[:], buf[offset:offset+SignatureSize]) +	offset += SignatureSize +	// KeyHash +	l.KeyHash = &[HashSize]byte{} +	copy(l.KeyHash[:], buf[offset:]) +	return nil +} diff --git a/pkg/types/trunnel_test.go b/pkg/types/trunnel_test.go new file mode 100644 index 0000000..297578c --- /dev/null +++ b/pkg/types/trunnel_test.go @@ -0,0 +1,114 @@ +package types + +import ( +	"bytes" +	"reflect" +	"testing" +) + +var ( +	testBuffer32 = &[32]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31} +	testBuffer64 = &[64]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63} +) + +func TestMarshalMessage(t *testing.T) { +	description := "valid: shard hint 72623859790382856, checksum 0x00,0x01,..." +	message := &Message{ +		ShardHint: 72623859790382856, +		Checksum:  testBuffer32, +	} +	want := bytes.Join([][]byte{ +		[]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, +		testBuffer32[:], +	}, nil) +	if got := message.Marshal(); !bytes.Equal(got, want) { +		t.Errorf("got message\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, description) +	} +} + +func TestMarshalLeaf(t *testing.T) { +	description := "valid: shard hint 72623859790382856, buffers 0x00,0x01,..." +	leaf := &Leaf{ +		Message: Message{ +			ShardHint: 72623859790382856, +			Checksum:  testBuffer32, +		}, +		SigIdent: SigIdent{ +			Signature: testBuffer64, +			KeyHash:   testBuffer32, +		}, +	} +	want := bytes.Join([][]byte{ +		[]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, +		testBuffer32[:], testBuffer64[:], testBuffer32[:], +	}, nil) +	if got := leaf.Marshal(); !bytes.Equal(got, want) { +		t.Errorf("got leaf\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, description) +	} +} + +func TestMarshalTreeHead(t *testing.T) { +	description := "valid: timestamp 16909060, tree size 72623859790382856, root hash 0x00,0x01,..." +	th := &TreeHead{ +		Timestamp: 16909060, +		TreeSize:  72623859790382856, +		RootHash:  testBuffer32, +	} +	want := bytes.Join([][]byte{ +		[]byte{0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04}, +		[]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, +		testBuffer32[:], +	}, nil) +	if got := th.Marshal(); !bytes.Equal(got, want) { +		t.Errorf("got tree head\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, description) +	} +} + +func TestUnmarshalLeaf(t *testing.T) { +	for _, table := range []struct { +		description string +		serialized  []byte +		wantErr     bool +		want        *Leaf +	}{ +		{ +			description: "invalid: not enough bytes", +			serialized:  make([]byte, LeafSize-1), +			wantErr:     true, +		}, +		{ +			description: "invalid: too many bytes", +			serialized:  make([]byte, LeafSize+1), +			wantErr:     true, +		}, +		{ +			description: "valid: shard hint 72623859790382856, buffers 0x00,0x01,...", +			serialized: bytes.Join([][]byte{ +				[]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, +				testBuffer32[:], testBuffer64[:], testBuffer32[:], +			}, nil), +			want: &Leaf{ +				Message: Message{ +					ShardHint: 72623859790382856, +					Checksum:  testBuffer32, +				}, +				SigIdent: SigIdent{ +					Signature: testBuffer64, +					KeyHash:   testBuffer32, +				}, +			}, +		}, +	} { +		var leaf Leaf +		err := leaf.Unmarshal(table.serialized) +		if got, want := err != nil, table.wantErr; got != want { +			t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) +		} +		if err != nil { +			continue +		} +		if got, want := &leaf, table.want; !reflect.DeepEqual(got, want) { +			t.Errorf("got leaf\n\t%v\nbut wanted\n\t%v\nin test %q\n", got, want, table.description) +		} +	} +} diff --git a/pkg/types/types.go b/pkg/types/types.go new file mode 100644 index 0000000..9ca7db8 --- /dev/null +++ b/pkg/types/types.go @@ -0,0 +1,155 @@ +package types + +import ( +	"crypto" +	"crypto/ed25519" +	"crypto/sha256" +	"fmt" +	"strings" +) + +const ( +	HashSize            = sha256.Size +	SignatureSize       = ed25519.SignatureSize +	VerificationKeySize = ed25519.PublicKeySize + +	EndpointAddLeaf             = Endpoint("add-leaf") +	EndpointAddCosignature      = Endpoint("add-cosignature") +	EndpointGetTreeHeadLatest   = Endpoint("get-tree-head-latest") +	EndpointGetTreeHeadToSign   = Endpoint("get-tree-head-to-sign") +	EndpointGetTreeHeadCosigned = Endpoint("get-tree-head-cosigned") +	EndpointGetProofByHash      = Endpoint("get-proof-by-hash") +	EndpointGetConsistencyProof = Endpoint("get-consistency-proof") +	EndpointGetLeaves           = Endpoint("get-leaves") +) + +// Endpoint is a named HTTP API endpoint +type Endpoint string + +// Path joins a number of components to form a full endpoint path.  For example, +// EndpointAddLeaf.Path("example.com", "st/v0") -> example.com/st/v0/add-leaf. +func (e Endpoint) Path(components ...string) string { +	return strings.Join(append(components, string(e)), "/") +} + +// Leaf is the log's Merkle tree leaf. +type Leaf struct { +	Message +	SigIdent +} + +// Message is composed of a shard hint and a checksum.  The submitter selects +// these values to fit the log's shard interval and the opaque data in question. +type Message struct { +	ShardHint uint64 +	Checksum  *[HashSize]byte +} + +// SigIdent is composed of a signature-signer pair.  The signature is computed +// over the Trunnel-serialized leaf message.  KeyHash identifies the signer. +type SigIdent struct { +	Signature *[SignatureSize]byte +	KeyHash   *[HashSize]byte +} + +// SignedTreeHead is composed of a tree head and a list of signature-signer +// pairs.  Each signature is computed over the Trunnel-serialized tree head. +type SignedTreeHead struct { +	TreeHead +	SigIdent []*SigIdent +} + +// TreeHead is the log's tree head. +type TreeHead struct { +	Timestamp uint64 +	TreeSize  uint64 +	RootHash  *[HashSize]byte +} + +// ConsistencyProof is a consistency proof that proves the log's append-only +// property. +type ConsistencyProof struct { +	NewSize uint64 +	OldSize uint64 +	Path    []*[HashSize]byte +} + +// InclusionProof is an inclusion proof that proves a leaf is included in the +// log. +type InclusionProof struct { +	TreeSize  uint64 +	LeafIndex uint64 +	Path      []*[HashSize]byte +} + +// LeafList is a list of leaves +type LeafList []*Leaf + +// ConsistencyProofRequest is a get-consistency-proof request +type ConsistencyProofRequest struct { +	NewSize uint64 +	OldSize uint64 +} + +// InclusionProofRequest is a get-proof-by-hash request +type InclusionProofRequest struct { +	LeafHash *[HashSize]byte +	TreeSize uint64 +} + +// LeavesRequest is a get-leaves request +type LeavesRequest struct { +	StartSize uint64 +	EndSize   uint64 +} + +// LeafRequest is an add-leaf request +type LeafRequest struct { +	Message +	Signature       *[SignatureSize]byte +	VerificationKey *[VerificationKeySize]byte +	DomainHint      string +} + +// CosignatureRequest is an add-cosignature request +type CosignatureRequest struct { +	SigIdent +} + +// Sign signs the tree head using the log's signature scheme +func (th *TreeHead) Sign(signer crypto.Signer) (*SignedTreeHead, error) { +	sig, err := signer.Sign(nil, th.Marshal(), crypto.Hash(0)) +	if err != nil { +		return nil, fmt.Errorf("Sign: %v", err) +	} + +	sigident := SigIdent{ +		KeyHash:   Hash(signer.Public().(ed25519.PublicKey)[:]), +		Signature: &[SignatureSize]byte{}, +	} +	copy(sigident.Signature[:], sig) +	return &SignedTreeHead{ +		TreeHead: *th, +		SigIdent: []*SigIdent{ +			&sigident, +		}, +	}, nil +} + +// Verify verifies the tree head signature using the log's signature scheme +func (th *TreeHead) Verify(vk *[VerificationKeySize]byte, sig *[SignatureSize]byte) error { +	if !ed25519.Verify(ed25519.PublicKey(vk[:]), th.Marshal(), sig[:]) { +		return fmt.Errorf("invalid tree head signature") +	} +	return nil +} + +// Verify checks if a leaf is included in the log +func (p *InclusionProof) Verify(leaf *Leaf, th *TreeHead) error { // TODO +	return nil +} + +// Verify checks if two tree heads are consistent +func (p *ConsistencyProof) Verify(oldTH, newTH *TreeHead) error { // TODO +	return nil +} diff --git a/pkg/types/types_test.go b/pkg/types/types_test.go new file mode 100644 index 0000000..da89c59 --- /dev/null +++ b/pkg/types/types_test.go @@ -0,0 +1,58 @@ +package types + +import ( +	"testing" +) + +func TestEndpointPath(t *testing.T) { +	base, prefix, proto := "example.com", "log", "st/v0" +	for _, table := range []struct { +		endpoint Endpoint +		want     string +	}{ +		{ +			endpoint: EndpointAddLeaf, +			want:     "example.com/log/st/v0/add-leaf", +		}, +		{ +			endpoint: EndpointAddCosignature, +			want:     "example.com/log/st/v0/add-cosignature", +		}, +		{ +			endpoint: EndpointGetTreeHeadLatest, +			want:     "example.com/log/st/v0/get-tree-head-latest", +		}, +		{ +			endpoint: EndpointGetTreeHeadToSign, +			want:     "example.com/log/st/v0/get-tree-head-to-sign", +		}, +		{ +			endpoint: EndpointGetTreeHeadCosigned, +			want:     "example.com/log/st/v0/get-tree-head-cosigned", +		}, +		{ +			endpoint: EndpointGetConsistencyProof, +			want:     "example.com/log/st/v0/get-consistency-proof", +		}, +		{ +			endpoint: EndpointGetProofByHash, +			want:     "example.com/log/st/v0/get-proof-by-hash", +		}, +		{ +			endpoint: EndpointGetLeaves, +			want:     "example.com/log/st/v0/get-leaves", +		}, +	} { +		if got, want := table.endpoint.Path(base+"/"+prefix+"/"+proto), table.want; got != want { +			t.Errorf("got endpoint\n%s\n\tbut wanted\n%s\n\twith one component", got, want) +		} +		if got, want := table.endpoint.Path(base, prefix, proto), table.want; got != want { +			t.Errorf("got endpoint\n%s\n\tbut wanted\n%s\n\tmultiple components", got, want) +		} +	} +} + +func TestTreeHeadSign(t *testing.T)           {} +func TestTreeHeadVerify(t *testing.T)         {} +func TestInclusionProofVerify(t *testing.T)   {} +func TestConsistencyProofVerify(t *testing.T) {} diff --git a/pkg/types/util.go b/pkg/types/util.go new file mode 100644 index 0000000..3cd7dfa --- /dev/null +++ b/pkg/types/util.go @@ -0,0 +1,21 @@ +package types + +import ( +	"crypto/sha256" +) + +const ( +	LeafHashPrefix = 0x00 +) + +func Hash(buf []byte) *[HashSize]byte { +	var ret [HashSize]byte +	hash := sha256.New() +	hash.Write(buf) +	copy(ret[:], hash.Sum(nil)) +	return &ret +} + +func HashLeaf(buf []byte) *[HashSize]byte { +	return Hash(append([]byte{LeafHashPrefix}, buf...)) +} diff --git a/request.go b/request.go deleted file mode 100644 index 7c95f34..0000000 --- a/request.go +++ /dev/null @@ -1,116 +0,0 @@ -package stfe - -import ( -	"fmt" - -	"io/ioutil" -	"net/http" - -	"github.com/system-transparency/stfe/types" -) - -func (lp *LogParameters) parseAddEntryV1Request(r *http.Request) (*types.StItem, error) { -	var item types.StItem -	if err := unpackOctetPost(r, &item); err != nil { -		return nil, fmt.Errorf("unpackOctetPost: %v", err) -	} -	if item.Format != types.StFormatSignedChecksumV1 { -		return nil, fmt.Errorf("invalid StItem format: %v", item.Format) -	} - -	// Check that submitter namespace is valid -	namespace := &item.SignedChecksumV1.Signature.Namespace -	if lp.SubmitterPolicy { -		var ok bool -		if namespace, ok = lp.Submitters.Find(namespace); !ok { -			return nil, fmt.Errorf("unknown submitter namespace: %v", namespace) -		} -	} -	// Check that namespace signed add-entry request -	if msg, err := types.Marshal(item.SignedChecksumV1.Data); err != nil { -		return nil, fmt.Errorf("Marshal: %v", err) // should never happen -	} else if err := namespace.Verify(msg, item.SignedChecksumV1.Signature.Signature); err != nil { -		return nil, fmt.Errorf("Verify: %v", err) -	} -	return &item, nil -} - -func (lp *LogParameters) parseAddCosignatureV1Request(r *http.Request) (*types.StItem, error) { -	var item types.StItem -	if err := unpackOctetPost(r, &item); err != nil { -		return nil, fmt.Errorf("unpackOctetPost: %v", err) -	} -	if item.Format != types.StFormatCosignedTreeHeadV1 { -		return nil, fmt.Errorf("invalid StItem format: %v", item.Format) -	} -	if got, want := len(item.CosignedTreeHeadV1.Cosignatures), 1; got != want { -		return nil, fmt.Errorf("invalid number of cosignatures: %d", got) -	} - -	// Check that witness namespace is valid -	namespace := &item.CosignedTreeHeadV1.Cosignatures[0].Namespace -	if lp.WitnessPolicy { -		var ok bool -		if namespace, ok = lp.Witnesses.Find(namespace); !ok { -			return nil, fmt.Errorf("unknown witness namespace: %v", namespace) -		} -	} -	// Check that namespace signed add-cosignature request -	if msg, err := types.Marshal(*types.NewSignedTreeHeadV1(&item.CosignedTreeHeadV1.SignedTreeHead.TreeHead, &item.CosignedTreeHeadV1.SignedTreeHead.Signature).SignedTreeHeadV1); err != nil { -		return nil, fmt.Errorf("Marshal: %v", err) // should never happen -	} else if err := namespace.Verify(msg, item.CosignedTreeHeadV1.Cosignatures[0].Signature); err != nil { -		return nil, fmt.Errorf("Verify: %v", err) -	} -	return &item, nil -} - -func (lp *LogParameters) parseGetConsistencyProofV1Request(r *http.Request) (*types.GetConsistencyProofV1, error) { -	var item types.GetConsistencyProofV1 -	if err := unpackOctetPost(r, &item); err != nil { -		return nil, fmt.Errorf("unpackOctetPost: %v", err) -	} -	if item.First < 1 { -		return nil, fmt.Errorf("first(%d) must be larger than zero", item.First) -	} -	if item.Second <= item.First { -		return nil, fmt.Errorf("second(%d) must be larger than first(%d)", item.Second, item.First) -	} -	return &item, nil -} - -func (lp *LogParameters) parseGetProofByHashV1Request(r *http.Request) (*types.GetProofByHashV1, error) { -	var item types.GetProofByHashV1 -	if err := unpackOctetPost(r, &item); err != nil { -		return nil, fmt.Errorf("unpackOctetPost: %v", err) -	} -	if item.TreeSize < 1 { -		return nil, fmt.Errorf("TreeSize(%d) must be larger than zero", item.TreeSize) -	} -	return &item, nil -} - -func (lp *LogParameters) parseGetEntriesV1Request(r *http.Request) (*types.GetEntriesV1, error) { -	var item types.GetEntriesV1 -	if err := unpackOctetPost(r, &item); err != nil { -		return nil, fmt.Errorf("unpackOctetPost: %v", err) -	} - -	if item.Start > item.End { -		return nil, fmt.Errorf("start(%v) must be less than or equal to end(%v)", item.Start, item.End) -	} -	if item.End-item.Start+1 > uint64(lp.MaxRange) { -		item.End = item.Start + uint64(lp.MaxRange) - 1 -	} -	return &item, nil -} - -func unpackOctetPost(r *http.Request, out interface{}) error { -	body, err := ioutil.ReadAll(r.Body) -	if err != nil { -		return fmt.Errorf("failed reading request body: %v", err) -	} -	if err := types.Unmarshal(body, out); err != nil { -		return fmt.Errorf("Unmarshal: %v", err) -	} -	return nil -} diff --git a/request_test.go b/request_test.go deleted file mode 100644 index 102c56f..0000000 --- a/request_test.go +++ /dev/null @@ -1,318 +0,0 @@ -package stfe - -import ( -	"bytes" -	//"fmt" -	"reflect" -	"testing" -	//"testing/iotest" - -	"net/http" - -	"github.com/system-transparency/stfe/testdata" -	"github.com/system-transparency/stfe/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 -} diff --git a/server/main.go b/server/main.go deleted file mode 100644 index 74e4ad3..0000000 --- a/server/main.go +++ /dev/null @@ -1,175 +0,0 @@ -// Package main provides an STFE server binary -package main - -import ( -	"context" -	"flag" -	"fmt" -	"os" -	"strings" -	"sync" -	"syscall" -	"time" - -	"crypto/ed25519" -	"encoding/base64" -	"net/http" -	"os/signal" - -	"github.com/golang/glog" -	"github.com/google/trillian" -	"github.com/prometheus/client_golang/prometheus/promhttp" -	"github.com/system-transparency/stfe" -	"github.com/system-transparency/stfe/types" -	"google.golang.org/grpc" -) - -var ( -	httpEndpoint    = flag.String("http_endpoint", "localhost:6965", "host:port specification of where stfe serves clients") -	rpcBackend      = flag.String("log_rpc_server", "localhost:6962", "host:port specification of where Trillian serves clients") -	prefix          = flag.String("prefix", "st/v1", "a prefix that proceeds each endpoint path") -	trillianID      = flag.Int64("trillian_id", 0, "log identifier in the Trillian database") -	deadline        = flag.Duration("deadline", time.Second*10, "deadline for backend requests") -	key             = flag.String("key", "", "base64-encoded Ed25519 signing key") -	submitterPolicy = flag.Bool("submitter_policy", false, "whether there is any submitter namespace policy (default: none, accept unregistered submitter namespaces)") -	witnessPolicy   = flag.Bool("witness_policy", false, "whether there is any witness namespace policy (default: none, accept unregistered witness namespaces)") -	submitters      = flag.String("submitters", "", "comma-separated list of trusted submitter namespaces in base64 (default: none)") -	witnesses       = flag.String("witnesses", "", "comma-separated list of trusted submitter namespaces in base64 (default: none)") -	maxRange        = flag.Int64("max_range", 10, "maximum number of entries that can be retrived in a single request") -	interval        = flag.Duration("interval", time.Minute*10, "interval used to rotate the log's cosigned STH") -) - -func main() { -	flag.Parse() -	defer glog.Flush() - -	// wait for clean-up before exit -	var wg sync.WaitGroup -	defer wg.Wait() -	ctx, cancel := context.WithCancel(context.Background()) -	defer cancel() - -	glog.V(3).Infof("configuring stfe instance...") -	instance, err := setupInstanceFromFlags() -	if err != nil { -		glog.Errorf("setupInstance: %v", err) -		return -	} - -	glog.V(3).Infof("spawning SthSource") -	go func() { -		wg.Add(1) -		defer wg.Done() -		instance.SthSource.Run(ctx) -		glog.Errorf("SthSource shutdown") -		cancel() // must have SthSource running -	}() - -	glog.V(3).Infof("spawning await") -	server := http.Server{Addr: *httpEndpoint} -	go await(ctx, func() { -		wg.Add(1) -		defer wg.Done() -		ctxInner, _ := context.WithTimeout(ctx, time.Second*60) -		glog.Infof("Shutting down HTTP server...") -		server.Shutdown(ctxInner) -		glog.V(3).Infof("HTTP server shutdown") -		glog.Infof("Shutting down spawned go routines...") -		cancel() -	}) - -	glog.Infof("Serving on %v/%v", *httpEndpoint, *prefix) -	if err = server.ListenAndServe(); err != http.ErrServerClosed { -		glog.Errorf("ListenAndServe: %v", err) -	} -} - -// SetupInstance sets up a new STFE instance from flags -func setupInstanceFromFlags() (*stfe.Instance, error) { -	// Trillian gRPC connection -	dialOpts := []grpc.DialOption{grpc.WithInsecure(), grpc.WithBlock(), grpc.WithTimeout(*deadline)} -	conn, err := grpc.Dial(*rpcBackend, dialOpts...) -	if err != nil { -		return nil, fmt.Errorf("Dial: %v", err) -	} -	client := trillian.NewTrillianLogClient(conn) -	// HTTP multiplexer -	mux := http.NewServeMux() -	http.Handle("/", mux) -	// Prometheus metrics -	glog.V(3).Infof("Adding prometheus handler on path: /metrics") -	http.Handle("/metrics", promhttp.Handler()) -	// Trusted submitters -	submitters, err := newNamespacePoolFromString(*submitters) -	if err != nil { -		return nil, fmt.Errorf("submitters: newNamespacePoolFromString: %v", err) -	} -	// Trusted witnesses -	witnesses, err := newNamespacePoolFromString(*witnesses) -	if err != nil { -		return nil, fmt.Errorf("witnesses: NewNamespacePool: %v", err) -	} -	// Log identity -	sk, err := base64.StdEncoding.DecodeString(*key) -	if err != nil { -		return nil, fmt.Errorf("sk: DecodeString: %v", err) -	} -	signer := ed25519.PrivateKey(sk) -	logId, err := types.NewNamespaceEd25519V1([]byte(ed25519.PrivateKey(sk).Public().(ed25519.PublicKey))) -	if err != nil { -		return nil, fmt.Errorf("NewNamespaceEd25519V1: %v", err) -	} -	// Setup log parameters -	lp, err := stfe.NewLogParameters(signer, logId, *trillianID, *prefix, submitters, witnesses, *maxRange, *interval, *deadline, *submitterPolicy, *witnessPolicy) -	if err != nil { -		return nil, fmt.Errorf("NewLogParameters: %v", err) -	} -	// Setup STH source -	source, err := stfe.NewActiveSthSource(client, lp) -	if err != nil { -		return nil, fmt.Errorf("NewActiveSthSource: %v", err) -	} -	// Setup log instance -	i := &stfe.Instance{client, lp, source} -	for _, handler := range i.Handlers() { -		glog.V(3).Infof("adding handler: %s", handler.Path()) -		mux.Handle(handler.Path(), handler) -	} -	return i, nil -} - -// newNamespacePoolFromString creates a new namespace pool from a -// comma-separated list of serialized and base64-encoded namespaces. -func newNamespacePoolFromString(str string) (*types.NamespacePool, error) { -	var namespaces []*types.Namespace -	if len(str) > 0 { -		for _, b64 := range strings.Split(str, ",") { -			b, err := base64.StdEncoding.DecodeString(b64) -			if err != nil { -				return nil, fmt.Errorf("DecodeString: %v", err) -			} -			var namespace types.Namespace -			if err := types.Unmarshal(b, &namespace); err != nil { -				return nil, fmt.Errorf("Unmarshal: %v", err) -			} -			namespaces = append(namespaces, &namespace) -		} -	} -	pool, err := types.NewNamespacePool(namespaces) -	if err != nil { -		return nil, fmt.Errorf("NewNamespacePool: %v", err) -	} -	return pool, nil -} - -// await waits for a shutdown signal and then runs a clean-up function -func await(ctx context.Context, done func()) { -	sigs := make(chan os.Signal, 1) -	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) -	select { -	case <-sigs: -	case <-ctx.Done(): -	} -	glog.V(3).Info("received shutdown signal") -	done() -} @@ -1,155 +0,0 @@ -package stfe - -import ( -	"context" -	"fmt" -	"reflect" -	"sync" - -	"github.com/golang/glog" -	"github.com/google/certificate-transparency-go/schedule" -	"github.com/google/trillian" -	ttypes "github.com/google/trillian/types" -	"github.com/system-transparency/stfe/types" -) - -// SthSource provides access to the log's STHs. -type SthSource interface { -	// Latest returns the most reccent signed_tree_head_v*. -	Latest(context.Context) (*types.StItem, error) -	// Stable returns the most recent signed_tree_head_v* that is stable for -	// some period of time, e.g., 10 minutes. -	Stable(context.Context) (*types.StItem, error) -	// Cosigned returns the most recent cosigned_tree_head_v*. -	Cosigned(context.Context) (*types.StItem, error) -	// AddCosignature attempts to add a cosignature to the stable STH.  The -	// passed cosigned_tree_head_v* must have a single verified cosignature. -	AddCosignature(context.Context, *types.StItem) error -	// Run keeps the STH source updated until cancelled -	Run(context.Context) -} - -// ActiveSthSource implements the SthSource interface for an STFE instance that -// accepts new logging requests, i.e., the log is running in read+write mode. -type ActiveSthSource struct { -	client          trillian.TrillianLogClient -	logParameters   *LogParameters -	currCosth       *types.StItem                                 // current cosigned_tree_head_v1 (already finalized) -	nextCosth       *types.StItem                                 // next cosigned_tree_head_v1 (under preparation) -	cosignatureFrom map[[types.NamespaceFingerprintSize]byte]bool // track who we got cosignatures from in nextCosth -	mutex           sync.RWMutex -} - -// NewActiveSthSource returns an initialized ActiveSthSource -func NewActiveSthSource(cli trillian.TrillianLogClient, lp *LogParameters) (*ActiveSthSource, error) { -	s := ActiveSthSource{ -		client:        cli, -		logParameters: lp, -	} - -	ctx, _ := context.WithTimeout(context.Background(), lp.Deadline) -	sth, err := s.Latest(ctx) -	if err != nil { -		return nil, fmt.Errorf("Latest: %v", err) -	} - -	// TODO: load persisted cosigned STH? -	s.currCosth = types.NewCosignedTreeHeadV1(sth.SignedTreeHeadV1, nil) -	s.nextCosth = types.NewCosignedTreeHeadV1(sth.SignedTreeHeadV1, nil) -	s.cosignatureFrom = make(map[[types.NamespaceFingerprintSize]byte]bool) -	return &s, nil -} - -func (s *ActiveSthSource) Run(ctx context.Context) { -	schedule.Every(ctx, s.logParameters.Interval, func(ctx context.Context) { -		// get the next stable sth -		ictx, _ := context.WithTimeout(ctx, s.logParameters.Deadline) -		sth, err := s.Latest(ictx) -		if err != nil { -			glog.Warningf("cannot rotate without new sth: Latest: %v", err) -			return -		} -		// rotate -		s.mutex.Lock() -		defer s.mutex.Unlock() -		if err := s.rotate(sth); err != nil { -			glog.Warningf("rotate failed: %v", err) -		} -		// TODO: persist cosigned STH? -	}) -} - -func (s *ActiveSthSource) Latest(ctx context.Context) (*types.StItem, error) { -	trsp, err := s.client.GetLatestSignedLogRoot(ctx, &trillian.GetLatestSignedLogRootRequest{ -		LogId: s.logParameters.TreeId, -	}) -	var lr ttypes.LogRootV1 -	if errInner := checkGetLatestSignedLogRoot(s.logParameters, trsp, err, &lr); errInner != nil { -		return nil, fmt.Errorf("invalid signed log root response: %v", errInner) -	} -	return s.logParameters.SignTreeHeadV1(NewTreeHeadV1FromLogRoot(&lr)) -} - -func (s *ActiveSthSource) Stable(_ context.Context) (*types.StItem, error) { -	s.mutex.RLock() -	defer s.mutex.RUnlock() -	if s.nextCosth == nil { -		return nil, fmt.Errorf("no stable sth available") -	} -	return types.NewSignedTreeHeadV1(&s.nextCosth.CosignedTreeHeadV1.SignedTreeHead.TreeHead, &s.nextCosth.CosignedTreeHeadV1.SignedTreeHead.Signature), nil -} - -func (s *ActiveSthSource) Cosigned(_ context.Context) (*types.StItem, error) { -	s.mutex.RLock() -	defer s.mutex.RUnlock() -	if s.currCosth == nil { -		return nil, fmt.Errorf("no cosigned sth available") -	} -	return s.currCosth, nil -} - -func (s *ActiveSthSource) AddCosignature(_ context.Context, costh *types.StItem) error { -	s.mutex.Lock() -	defer s.mutex.Unlock() -	if !reflect.DeepEqual(s.nextCosth.CosignedTreeHeadV1.SignedTreeHead, costh.CosignedTreeHeadV1.SignedTreeHead) { -		return fmt.Errorf("cosignature covers a different tree head") -	} -	witness, err := costh.CosignedTreeHeadV1.Cosignatures[0].Namespace.Fingerprint() -	if err != nil { -		return fmt.Errorf("namespace without fingerprint: %v", err) -	} -	if _, ok := s.cosignatureFrom[*witness]; ok { -		return nil // duplicate -	} -	s.cosignatureFrom[*witness] = true -	s.nextCosth.CosignedTreeHeadV1.Cosignatures = append(s.nextCosth.CosignedTreeHeadV1.Cosignatures, costh.CosignedTreeHeadV1.Cosignatures[0]) -	return nil -} - -// rotate rotates the log's cosigned and stable STH.  The caller must aquire the -// source's read-write lock if there are concurrent reads and/or writes. -func (s *ActiveSthSource) rotate(fixedSth *types.StItem) error { -	// rotate stable -> cosigned -	if reflect.DeepEqual(&s.currCosth.CosignedTreeHeadV1.SignedTreeHead, &s.nextCosth.CosignedTreeHeadV1.SignedTreeHead) { -		for _, sigv1 := range s.currCosth.CosignedTreeHeadV1.Cosignatures { -			witness, err := sigv1.Namespace.Fingerprint() -			if err != nil { -				return fmt.Errorf("namespace without fingerprint: %v", err) -			} -			if _, ok := s.cosignatureFrom[*witness]; !ok { -				s.cosignatureFrom[*witness] = true -				s.nextCosth.CosignedTreeHeadV1.Cosignatures = append(s.nextCosth.CosignedTreeHeadV1.Cosignatures, sigv1) -			} -		} -	} -	s.currCosth.CosignedTreeHeadV1.SignedTreeHead = s.nextCosth.CosignedTreeHeadV1.SignedTreeHead -	s.currCosth.CosignedTreeHeadV1.Cosignatures = make([]types.SignatureV1, len(s.nextCosth.CosignedTreeHeadV1.Cosignatures)) -	copy(s.currCosth.CosignedTreeHeadV1.Cosignatures, s.nextCosth.CosignedTreeHeadV1.Cosignatures) - -	// rotate new stable -> stable -	if !reflect.DeepEqual(&s.nextCosth.CosignedTreeHeadV1.SignedTreeHead, fixedSth.SignedTreeHeadV1) { -		s.nextCosth = types.NewCosignedTreeHeadV1(fixedSth.SignedTreeHeadV1, nil) -		s.cosignatureFrom = make(map[[types.NamespaceFingerprintSize]byte]bool) -	} -	return nil -} diff --git a/sth_test.go b/sth_test.go deleted file mode 100644 index 0942ea1..0000000 --- a/sth_test.go +++ /dev/null @@ -1,466 +0,0 @@ -package stfe - -import ( -	"context" -	"crypto" -	"fmt" -	"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/testdata" -	"github.com/system-transparency/stfe/types" -) - -func TestNewActiveSthSource(t *testing.T) { -	for _, table := range []struct { -		description string -		signer      crypto.Signer -		trsp        *trillian.GetLatestSignedLogRootResponse -		terr        error -		wantErr     bool -		wantCosi    *types.StItem // current cosigned sth -		wantStable  *types.StItem // next stable sth that signatures are collected for -	}{ -		{ -			description: "invalid: no Trillian response", -			signer:      cttestdata.NewSignerWithFixedSig(nil, testdata.Signature), -			terr:        fmt.Errorf("internal server error"), -			wantErr:     true, -		}, -		{ -			description: "valid", -			signer:      cttestdata.NewSignerWithFixedSig(nil, testdata.Signature), -			trsp:        testdata.DefaultTSlr(t), -			wantCosi:    testdata.DefaultCosth(t, testdata.Ed25519VkLog, nil), -			wantStable:  testdata.DefaultCosth(t, testdata.Ed25519VkLog, nil), -		}, -	} { -		func() { // run deferred functions at the end of each iteration -			ti := newTestInstance(t, table.signer) -			defer ti.ctrl.Finish() -			ti.client.EXPECT().GetLatestSignedLogRoot(newDeadlineMatcher(), gomock.Any()).Return(table.trsp, table.terr) -			source, err := NewActiveSthSource(ti.client, ti.instance.LogParameters) -			if got, want := err != nil, table.wantErr; got != want { -				t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) -			} -			if err != nil { -				return -			} - -			if got, want := source.currCosth, table.wantCosi; !reflect.DeepEqual(got, want) { -				t.Errorf("got cosigned sth\n%v\n\tbut wanted\n%v\n\tin test %q", got, want, table.description) -			} -			if got, want := source.nextCosth, table.wantStable; !reflect.DeepEqual(got, want) { -				t.Errorf("got stable sth\n%v\n\tbut wanted\n%v\n\tin test %q", got, want, table.description) -			} -			cosignatureFrom := make(map[[types.NamespaceFingerprintSize]byte]bool) -			for _, cosig := range table.wantStable.CosignedTreeHeadV1.Cosignatures { -				cosignatureFrom[testdata.Fingerprint(t, &cosig.Namespace)] = true -			} -			if got, want := source.cosignatureFrom, cosignatureFrom; !reflect.DeepEqual(got, want) { -				if got == nil { -					t.Errorf("got uninitialized witness map\n%v\n\tbut wanted\n%v\n\tin test %q", nil, want, table.description) -				} else { -					t.Errorf("got witness map\n%v\n\t but wanted\n%v\n\tin test %q", got, want, table.description) -				} -			} -		}() -	} -} - -func TestLatest(t *testing.T) { -	for _, table := range []struct { -		description string -		signer      crypto.Signer -		trsp        *trillian.GetLatestSignedLogRootResponse -		terr        error -		wantErr     bool -		wantRsp     *types.StItem -	}{ -		{ -			description: "invalid: no Trillian response", -			signer:      cttestdata.NewSignerWithFixedSig(nil, testdata.Signature), -			terr:        fmt.Errorf("internal server error"), -			wantErr:     true, -		}, -		{ -			description: "invalid: no signature", -			signer:      cttestdata.NewSignerWithErr(nil, fmt.Errorf("signing failed")), -			terr:        fmt.Errorf("internal server error"), -			wantErr:     true, -		}, -		{ -			description: "valid", -			signer:      cttestdata.NewSignerWithFixedSig(nil, testdata.Signature), -			trsp:        testdata.DefaultTSlr(t), -			wantRsp:     testdata.DefaultSth(t, testdata.Ed25519VkLog), -		}, -	} { -		func() { // run deferred functions at the end of each iteration -			ti := newTestInstance(t, table.signer) -			defer ti.ctrl.Finish() -			ti.client.EXPECT().GetLatestSignedLogRoot(gomock.Any(), gomock.Any()).Return(table.trsp, table.terr) // no deadline matcher because context is set by the caller of Latest(), i.e., this test on the line below -			sth, err := ti.instance.SthSource.Latest(context.Background()) -			if got, want := err != nil, table.wantErr; got != want { -				t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) -			} -			if err != nil { -				return -			} -			if got, want := sth, table.wantRsp; !reflect.DeepEqual(got, want) { -				t.Errorf("got\n%v\n\tbut wanted\n%v\n\t in test %q", got, want, table.description) -			} -		}() -	} -} - -func TestStable(t *testing.T) { -	for _, table := range []struct { -		description string -		source      SthSource -		wantRsp     *types.StItem -		wantErr     bool -	}{ -		{ -			description: "invalid: no stable sth", -			source:      &ActiveSthSource{}, -			wantErr:     true, -		}, -		{ -			description: "valid", -			source: &ActiveSthSource{ -				nextCosth: testdata.DefaultCosth(t, testdata.Ed25519VkLog, nil), -			}, -			wantRsp: testdata.DefaultSth(t, testdata.Ed25519VkLog), -		}, -	} { -		sth, err := table.source.Stable(context.Background()) -		if got, want := err != nil, table.wantErr; got != want { -			t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) -		} -		if err != nil { -			continue -		} -		if got, want := sth, table.wantRsp; !reflect.DeepEqual(got, want) { -			t.Errorf("got\n%v\n\t but wanted\n%v\n\t in test %q", got, want, table.description) -		} -	} -} - -func TestCosigned(t *testing.T) { -	for _, table := range []struct { -		description string -		source      SthSource -		wantRsp     *types.StItem -		wantErr     bool -	}{ -		{ -			description: "invalid: no cosigned sth: nil", -			source:      &ActiveSthSource{}, -			wantErr:     true, -		}, -		{ -			description: "valid", -			source: &ActiveSthSource{ -				currCosth: testdata.DefaultCosth(t, testdata.Ed25519VkLog, [][32]byte{testdata.Ed25519VkWitness}), -			}, -			wantRsp: testdata.DefaultCosth(t, testdata.Ed25519VkLog, [][32]byte{testdata.Ed25519VkWitness}), -		}, -	} { -		cosi, err := table.source.Cosigned(context.Background()) -		if got, want := err != nil, table.wantErr; got != want { -			t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) -		} -		if err != nil { -			continue -		} -		if got, want := cosi, table.wantRsp; !reflect.DeepEqual(got, want) { -			t.Errorf("got\n%v\n\tbut wanted\n%v\n\tin test %q", got, want, table.description) -		} -	} -} - -func TestAddCosignature(t *testing.T) { -	for _, table := range []struct { -		description string -		source      *ActiveSthSource -		req         *types.StItem -		wantWit     []*types.Namespace -		wantErr     bool -	}{ -		{ -			description: "invalid: cosignature must target the stable sth", -			source: &ActiveSthSource{ -				nextCosth:       testdata.DefaultCosth(t, testdata.Ed25519VkLog, nil), -				cosignatureFrom: make(map[[types.NamespaceFingerprintSize]byte]bool), -			}, -			req:     testdata.DefaultCosth(t, testdata.Ed25519VkLog2, [][32]byte{testdata.Ed25519VkWitness}), -			wantErr: true, -		}, -		{ -			description: "valid: adding duplicate into a pool of cosignatures", -			source: &ActiveSthSource{ -				nextCosth: testdata.DefaultCosth(t, testdata.Ed25519VkLog, [][32]byte{testdata.Ed25519VkWitness}), -				cosignatureFrom: map[[types.NamespaceFingerprintSize]byte]bool{ -					testdata.Fingerprint(t, testdata.NewNamespace(t, testdata.Ed25519VkWitness)): true, -				}, -			}, -			req:     testdata.DefaultCosth(t, testdata.Ed25519VkLog, [][32]byte{testdata.Ed25519VkWitness}), -			wantWit: []*types.Namespace{testdata.NewNamespace(t, testdata.Ed25519VkWitness)}, -		}, -		{ -			description: "valid: adding into an empty pool of cosignatures", -			source: &ActiveSthSource{ -				nextCosth:       testdata.DefaultCosth(t, testdata.Ed25519VkLog, nil), -				cosignatureFrom: make(map[[types.NamespaceFingerprintSize]byte]bool), -			}, -			req:     testdata.DefaultCosth(t, testdata.Ed25519VkLog, [][32]byte{testdata.Ed25519VkWitness}), -			wantWit: []*types.Namespace{testdata.NewNamespace(t, testdata.Ed25519VkWitness)}, -		}, -		{ -			description: "valid: adding into a pool of cosignatures", -			source: &ActiveSthSource{ -				nextCosth: testdata.DefaultCosth(t, testdata.Ed25519VkLog, [][32]byte{testdata.Ed25519VkWitness}), -				cosignatureFrom: map[[types.NamespaceFingerprintSize]byte]bool{ -					testdata.Fingerprint(t, testdata.NewNamespace(t, testdata.Ed25519VkWitness)): true, -				}, -			}, -			req:     testdata.DefaultCosth(t, testdata.Ed25519VkLog, [][32]byte{testdata.Ed25519VkWitness2}), -			wantWit: []*types.Namespace{testdata.NewNamespace(t, testdata.Ed25519VkWitness), testdata.NewNamespace(t, testdata.Ed25519VkWitness2)}, -		}, -	} { -		err := table.source.AddCosignature(context.Background(), table.req) -		if got, want := err != nil, table.wantErr; got != want { -			t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) -		} -		if err != nil { -			continue -		} - -		// Check that the next cosigned sth is updated -		var sigs []types.SignatureV1 -		for _, wit := range table.wantWit { -			sigs = append(sigs, types.SignatureV1{ -				Namespace: *wit, -				Signature: testdata.Signature, -			}) -		} -		if got, want := table.source.nextCosth, types.NewCosignedTreeHeadV1(testdata.DefaultSth(t, testdata.Ed25519VkLog).SignedTreeHeadV1, sigs); !reflect.DeepEqual(got, want) { -			t.Errorf("got\n%v\n\tbut wanted\n%v\n\tin test %q", got, want, table.description) -		} -		// Check that the map tracking witness signatures is updated -		if got, want := len(table.source.cosignatureFrom), len(table.wantWit); got != want { -			t.Errorf("witness map got %d cosignatures but wanted %d in test %q", got, want, table.description) -		} else { -			for _, wit := range table.wantWit { -				if _, ok := table.source.cosignatureFrom[testdata.Fingerprint(t, wit)]; !ok { -					t.Errorf("missing signature from witness %X in test %q", testdata.Fingerprint(t, wit), table.description) -				} -			} -		} -	} -} - -func TestRotate(t *testing.T) { -	// distinct sths -	sth1 := testdata.DefaultSth(t, testdata.Ed25519VkLog) -	sth2 := testdata.DefaultSth(t, testdata.Ed25519VkLog2) -	sth3 := testdata.DefaultSth(t, testdata.Ed25519VkLog3) -	// distinct witnesses -	wit1 := testdata.NewNamespace(t, testdata.Ed25519VkWitness) -	wit2 := testdata.NewNamespace(t, testdata.Ed25519VkWitness2) -	wit3 := testdata.NewNamespace(t, testdata.Ed25519VkWitness3) -	for _, table := range []struct { -		description string -		source      *ActiveSthSource -		fixedSth    *types.StItem -		wantCurrSth *types.StItem -		wantNextSth *types.StItem -		wantWit     []*types.Namespace -	}{ -		{ -			description: "not repeated cosigned and not repeated stable", -			source: &ActiveSthSource{ -				currCosth: types.NewCosignedTreeHeadV1(sth1.SignedTreeHeadV1, nil), -				nextCosth: types.NewCosignedTreeHeadV1(sth2.SignedTreeHeadV1, []types.SignatureV1{ -					types.SignatureV1{ -						Namespace: *wit1, -						Signature: testdata.Signature, -					}, -				}), -				cosignatureFrom: map[[types.NamespaceFingerprintSize]byte]bool{ -					testdata.Fingerprint(t, wit1): true, -				}, -			}, -			fixedSth: sth3, -			wantCurrSth: types.NewCosignedTreeHeadV1(sth2.SignedTreeHeadV1, []types.SignatureV1{ -				types.SignatureV1{ -					Namespace: *wit1, -					Signature: testdata.Signature, -				}, -			}), -			wantNextSth: types.NewCosignedTreeHeadV1(sth3.SignedTreeHeadV1, nil), -			wantWit:     nil, // no cosignatures for the next stable sth yet -		}, -		{ -			description: "not repeated cosigned and repeated stable", -			source: &ActiveSthSource{ -				currCosth: types.NewCosignedTreeHeadV1(sth1.SignedTreeHeadV1, nil), -				nextCosth: types.NewCosignedTreeHeadV1(sth2.SignedTreeHeadV1, []types.SignatureV1{ -					types.SignatureV1{ -						Namespace: *wit1, -						Signature: testdata.Signature, -					}, -				}), -				cosignatureFrom: map[[types.NamespaceFingerprintSize]byte]bool{ -					testdata.Fingerprint(t, wit1): true, -				}, -			}, -			fixedSth: sth2, -			wantCurrSth: types.NewCosignedTreeHeadV1(sth2.SignedTreeHeadV1, []types.SignatureV1{ -				types.SignatureV1{ -					Namespace: *wit1, -					Signature: testdata.Signature, -				}, -			}), -			wantNextSth: types.NewCosignedTreeHeadV1(sth2.SignedTreeHeadV1, []types.SignatureV1{ -				types.SignatureV1{ -					Namespace: *wit1, -					Signature: testdata.Signature, -				}, -			}), -			wantWit: []*types.Namespace{wit1}, -		}, -		{ -			description: "repeated cosigned and not repeated stable", -			source: &ActiveSthSource{ -				currCosth: types.NewCosignedTreeHeadV1(sth1.SignedTreeHeadV1, []types.SignatureV1{ -					types.SignatureV1{ -						Namespace: *wit1, -						Signature: testdata.Signature, -					}, -					types.SignatureV1{ -						Namespace: *wit2, -						Signature: testdata.Signature, -					}, -				}), -				nextCosth: types.NewCosignedTreeHeadV1(sth1.SignedTreeHeadV1, []types.SignatureV1{ -					types.SignatureV1{ -						Namespace: *wit2, -						Signature: testdata.Signature, -					}, -					types.SignatureV1{ -						Namespace: *wit3, -						Signature: testdata.Signature, -					}, -				}), -				cosignatureFrom: map[[types.NamespaceFingerprintSize]byte]bool{ -					testdata.Fingerprint(t, wit2): true, -					testdata.Fingerprint(t, wit3): true, -				}, -			}, -			fixedSth: sth3, -			wantCurrSth: types.NewCosignedTreeHeadV1(sth1.SignedTreeHeadV1, []types.SignatureV1{ -				types.SignatureV1{ -					Namespace: *wit2, -					Signature: testdata.Signature, -				}, -				types.SignatureV1{ -					Namespace: *wit3, -					Signature: testdata.Signature, -				}, -				types.SignatureV1{ -					Namespace: *wit1, -					Signature: testdata.Signature, -				}, -			}), -			wantNextSth: types.NewCosignedTreeHeadV1(sth3.SignedTreeHeadV1, nil), -			wantWit:     nil, // no cosignatures for the next stable sth yet -		}, -		{ -			description: "repeated cosigned and repeated stable", -			source: &ActiveSthSource{ -				currCosth: types.NewCosignedTreeHeadV1(sth1.SignedTreeHeadV1, []types.SignatureV1{ -					types.SignatureV1{ -						Namespace: *wit1, -						Signature: testdata.Signature, -					}, -					types.SignatureV1{ -						Namespace: *wit2, -						Signature: testdata.Signature, -					}, -				}), -				nextCosth: types.NewCosignedTreeHeadV1(sth1.SignedTreeHeadV1, []types.SignatureV1{ -					types.SignatureV1{ -						Namespace: *wit2, -						Signature: testdata.Signature, -					}, -					types.SignatureV1{ -						Namespace: *wit3, -						Signature: testdata.Signature, -					}, -				}), -				cosignatureFrom: map[[types.NamespaceFingerprintSize]byte]bool{ -					testdata.Fingerprint(t, wit2): true, -					testdata.Fingerprint(t, wit3): true, -				}, -			}, -			fixedSth: sth1, -			wantCurrSth: types.NewCosignedTreeHeadV1(sth1.SignedTreeHeadV1, []types.SignatureV1{ -				types.SignatureV1{ -					Namespace: *wit2, -					Signature: testdata.Signature, -				}, -				types.SignatureV1{ -					Namespace: *wit3, -					Signature: testdata.Signature, -				}, -				types.SignatureV1{ -					Namespace: *wit1, -					Signature: testdata.Signature, -				}, -			}), -			wantNextSth: types.NewCosignedTreeHeadV1(sth1.SignedTreeHeadV1, []types.SignatureV1{ -				types.SignatureV1{ -					Namespace: *wit2, -					Signature: testdata.Signature, -				}, -				types.SignatureV1{ -					Namespace: *wit3, -					Signature: testdata.Signature, -				}, -				types.SignatureV1{ -					Namespace: *wit1, -					Signature: testdata.Signature, -				}, -			}), -			wantWit: []*types.Namespace{wit1, wit2, wit3}, -		}, -	} { -		table.source.rotate(table.fixedSth) -		if got, want := table.source.currCosth, table.wantCurrSth; !reflect.DeepEqual(got, want) { -			t.Errorf("got currCosth\n%v\n\tbut wanted \n%v\n\tin test %q", got, want, table.description) -		} -		if got, want := table.source.nextCosth, table.wantNextSth; !reflect.DeepEqual(got, want) { -			t.Errorf("got nextCosth\n%v\n\tbut wanted\n%v\n\tin test %q", got, want, table.description) -		} -		if got, want := len(table.source.cosignatureFrom), len(table.wantWit); got != want { -			t.Errorf("witness map got %d cosignatures but wanted %d in test %q", got, want, table.description) -		} else { -			for _, wit := range table.wantWit { -				if _, ok := table.source.cosignatureFrom[testdata.Fingerprint(t, wit)]; !ok { -					t.Errorf("missing signature from witness %X in test %q", testdata.Fingerprint(t, wit), table.description) -				} -			} -		} -		// check that adding cosignatures to stable will not effect cosigned sth -		wantLen := len(table.source.currCosth.CosignedTreeHeadV1.Cosignatures) -		table.source.nextCosth.CosignedTreeHeadV1.Cosignatures = append(table.source.nextCosth.CosignedTreeHeadV1.Cosignatures, types.SignatureV1{Namespace: *wit1, Signature: testdata.Signature}) -		if gotLen := len(table.source.currCosth.CosignedTreeHeadV1.Cosignatures); gotLen != wantLen { -			t.Errorf("adding cosignatures to the stable sth modifies the fixated cosigned sth in test %q", table.description) -		} -	} -} diff --git a/testdata/data.go b/testdata/data.go deleted file mode 100644 index ac958e5..0000000 --- a/testdata/data.go +++ /dev/null @@ -1,287 +0,0 @@ -package testdata - -import ( -	"bytes" -	"testing" -	"time" - -	"crypto/ed25519" - -	"github.com/google/trillian" -	ttypes "github.com/google/trillian/types" -	"github.com/system-transparency/stfe/types" -	"google.golang.org/grpc/codes" -	"google.golang.org/grpc/status" -) - -var ( -	Ed25519VkLog  = [32]byte{} -	Ed25519VkLog2 = [32]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1} -	Ed25519VkLog3 = [32]byte{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2} -	//Ed25519VkWitness   = [32]byte{3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3} -	//	Ed25519VkWitness2  = [32]byte{4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4} -	Ed25519VkWitness3 = [32]byte{5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5} -	//Ed25519VkSubmitter = [32]byte{6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6} - -	TreeId   = int64(0) -	Prefix   = "test" -	MaxRange = int64(3) -	Interval = time.Second * 10 -	Deadline = time.Second * 5 - -	Timestamp  = uint64(0) -	TreeSize   = uint64(0) -	Extension  = make([]byte, 0) -	NodeHash   = make([]byte, 32) -	Signature  = make([]byte, 64) -	Identifier = []byte("foobar-1.2.3") -	Checksum   = make([]byte, 32) -	Index      = int64(0) -	HashPath   = [][]byte{ -		NodeHash, -	} -	NodePath = []types.NodeHash{ -		types.NodeHash{NodeHash}, -	} -	LeafHash = [32]byte{} - -	// TODO: make these unique and load more pretty maybe -	Ed25519SkWitness = [64]byte{230, 122, 195, 152, 194, 195, 147, 153, 80, 120, 153, 79, 102, 27, 52, 187, 136, 218, 150, 234, 107, 9, 167, 4, 92, 21, 11, 113, 42, 29, 129, 69, 75, 60, 249, 150, 229, 93, 75, 32, 103, 126, 244, 37, 53, 182, 68, 82, 249, 109, 49, 94, 10, 19, 146, 244, 58, 191, 169, 107, 78, 37, 45, 210} -	Ed25519VkWitness = [32]byte{75, 60, 249, 150, 229, 93, 75, 32, 103, 126, 244, 37, 53, 182, 68, 82, 249, 109, 49, 94, 10, 19, 146, 244, 58, 191, 169, 107, 78, 37, 45, 210} - -	Ed25519SkWitness2 = [64]byte{98, 65, 92, 117, 33, 167, 138, 36, 252, 147, 87, 173, 44, 62, 17, 66, 126, 70, 218, 87, 91, 148, 64, 194, 241, 248, 62, 90, 140, 122, 234, 76, 144, 6, 250, 185, 37, 217, 77, 201, 180, 42, 81, 37, 165, 27, 22, 32, 25, 8, 156, 228, 78, 207, 208, 18, 91, 77, 189, 51, 112, 31, 237, 6} -	Ed25519VkWitness2 = [32]byte{144, 6, 250, 185, 37, 217, 77, 201, 180, 42, 81, 37, 165, 27, 22, 32, 25, 8, 156, 228, 78, 207, 208, 18, 91, 77, 189, 51, 112, 31, 237, 6} - -	Ed25519SkSubmitter  = [64]byte{230, 122, 195, 152, 194, 195, 147, 153, 80, 120, 153, 79, 102, 27, 52, 187, 136, 218, 150, 234, 107, 9, 167, 4, 92, 21, 11, 113, 42, 29, 129, 69, 75, 60, 249, 150, 229, 93, 75, 32, 103, 126, 244, 37, 53, 182, 68, 82, 249, 109, 49, 94, 10, 19, 146, 244, 58, 191, 169, 107, 78, 37, 45, 210} -	Ed25519VkSubmitter  = [32]byte{75, 60, 249, 150, 229, 93, 75, 32, 103, 126, 244, 37, 53, 182, 68, 82, 249, 109, 49, 94, 10, 19, 146, 244, 58, 191, 169, 107, 78, 37, 45, 210} -	Ed25519SkSubmitter2 = [64]byte{98, 65, 92, 117, 33, 167, 138, 36, 252, 147, 87, 173, 44, 62, 17, 66, 126, 70, 218, 87, 91, 148, 64, 194, 241, 248, 62, 90, 140, 122, 234, 76, 144, 6, 250, 185, 37, 217, 77, 201, 180, 42, 81, 37, 165, 27, 22, 32, 25, 8, 156, 228, 78, 207, 208, 18, 91, 77, 189, 51, 112, 31, 237, 6} -	Ed25519VkSubmitter2 = [32]byte{144, 6, 250, 185, 37, 217, 77, 201, 180, 42, 81, 37, 165, 27, 22, 32, 25, 8, 156, 228, 78, 207, 208, 18, 91, 77, 189, 51, 112, 31, 237, 6} -) - -// TODO: reorder and docdoc where need be -// -// Helpers that must create default values for different STFE types -// - -func DefaultCosth(t *testing.T, logVk [32]byte, witVk [][32]byte) *types.StItem { -	t.Helper() -	cosigs := make([]types.SignatureV1, 0) -	for _, vk := range witVk { -		cosigs = append(cosigs, types.SignatureV1{*NewNamespace(t, vk), Signature}) -	} -	return types.NewCosignedTreeHeadV1(DefaultSth(t, logVk).SignedTreeHeadV1, cosigs) -} - -func DefaultSth(t *testing.T, vk [32]byte) *types.StItem { -	t.Helper() -	return types.NewSignedTreeHeadV1(DefaultTh(t), DefaultSig(t, vk)) -} - -func DefaultSignedChecksum(t *testing.T, vk [32]byte) *types.StItem { -	t.Helper() -	return types.NewSignedChecksumV1(DefaultChecksum(t), DefaultSig(t, vk)) -} - -func DefaultTh(t *testing.T) *types.TreeHeadV1 { -	t.Helper() -	return types.NewTreeHeadV1(Timestamp, TreeSize, NodeHash, Extension) -} - -func DefaultSig(t *testing.T, vk [32]byte) *types.SignatureV1 { -	t.Helper() -	return &types.SignatureV1{*NewNamespace(t, vk), Signature} -} - -func DefaultChecksum(t *testing.T) *types.ChecksumV1 { -	t.Helper() -	return &types.ChecksumV1{Identifier, Checksum} -} - -func AddCosignatureBuffer(t *testing.T, sth *types.StItem, sk *[64]byte, vk *[32]byte) *bytes.Buffer { -	t.Helper() -	var cosigs []types.SignatureV1 -	if vk != nil { -		cosigs = []types.SignatureV1{ -			types.SignatureV1{ -				Namespace: *NewNamespace(t, *vk), -				Signature: ed25519.Sign(ed25519.PrivateKey((*sk)[:]), marshal(t, *sth.SignedTreeHeadV1)), -			}, -		} -	} -	return bytes.NewBuffer(marshal(t, *types.NewCosignedTreeHeadV1(sth.SignedTreeHeadV1, cosigs))) -} - -func AddSignedChecksumBuffer(t *testing.T, sk [64]byte, vk [32]byte) *bytes.Buffer { -	t.Helper() -	data := DefaultChecksum(t) -	return bytes.NewBuffer(marshal(t, *types.NewSignedChecksumV1( -		data, -		&types.SignatureV1{ -			Namespace: *NewNamespace(t, vk), -			Signature: ed25519.Sign(ed25519.PrivateKey(sk[:]), marshal(t, *data)), -		}, -	))) -} - -func NewNamespacePool(t *testing.T, namespaces []*types.Namespace) *types.NamespacePool { -	pool, err := types.NewNamespacePool(namespaces) -	if err != nil { -		t.Fatalf("must make namespace pool: %v", err) -	} -	return pool -} - -func NewNamespace(t *testing.T, vk [32]byte) *types.Namespace { -	namespace, err := types.NewNamespaceEd25519V1(vk[:]) -	if err != nil { -		t.Fatalf("must make Ed25519V1 namespace: %v", err) -	} -	return namespace -} - -// -// Helpers that must create default values for different Trillian types -// - -// DefaultTLr creates a default Trillian log root -func DefaultTLr(t *testing.T) *ttypes.LogRootV1 { -	t.Helper() -	return Tlr(t, TreeSize, Timestamp, NodeHash) -} - -// Tlr creates a Trillian log root -func Tlr(t *testing.T, size, timestamp uint64, hash []byte) *ttypes.LogRootV1 { -	t.Helper() -	return &ttypes.LogRootV1{ -		TreeSize:       size, -		RootHash:       hash, -		TimestampNanos: timestamp, -		Revision:       0,   // not used by stfe -		Metadata:       nil, // not used by stfe -	} -} - -// DefaultTSlr creates a default Trillian signed log root -func DefaultTSlr(t *testing.T) *trillian.GetLatestSignedLogRootResponse { -	t.Helper() -	return Tslr(t, DefaultTLr(t)) -} - -// Tslr creates a Trillian signed log root -func Tslr(t *testing.T, lr *ttypes.LogRootV1) *trillian.GetLatestSignedLogRootResponse { -	t.Helper() -	b, err := lr.MarshalBinary() -	if err != nil { -		t.Fatalf("must marshal Trillian log root: %v", err) -	} -	return &trillian.GetLatestSignedLogRootResponse{ -		SignedLogRoot: &trillian.SignedLogRoot{ -			KeyHint:          nil, // not used by stfe -			LogRoot:          b, -			LogRootSignature: nil, // not used by stfe -		}, -		Proof: nil, // not used by stfe -	} -} - -// DefaultTQlr creates a default Trillian queue leaf response -func DefaultTQlr(t *testing.T, withDupCode bool) *trillian.QueueLeafResponse { -	t.Helper() -	s := status.New(codes.OK, "ok").Proto() -	if withDupCode { -		s = status.New(codes.AlreadyExists, "duplicate").Proto() -	} -	return &trillian.QueueLeafResponse{ -		QueuedLeaf: &trillian.QueuedLogLeaf{ -			Leaf: &trillian.LogLeaf{ -				MerkleLeafHash:   nil, // not used by stfe -				LeafValue:        marshal(t, *DefaultSignedChecksum(t, Ed25519VkSubmitter)), -				ExtraData:        nil, // not used by stfe -				LeafIndex:        0,   // not applicable (log is not pre-ordered) -				LeafIdentityHash: nil, // not used by stfe -			}, -			Status: s, -		}, -	} -} - -// DefaultTglbrr creates a default Trillian get leaves by range response -func DefaultTGlbrr(t *testing.T, start, end int64) *trillian.GetLeavesByRangeResponse { -	t.Helper() -	leaves := make([]*trillian.LogLeaf, 0, end-start+1) -	for i, n := start, end+1; i < n; i++ { -		leaves = append(leaves, &trillian.LogLeaf{ -			MerkleLeafHash:   nil, // not usedb y stfe -			LeafValue:        marshal(t, *DefaultSignedChecksum(t, Ed25519VkSubmitter)), -			ExtraData:        nil, // not used by stfe -			LeafIndex:        i, -			LeafIdentityHash: nil, // not used by stfe -		}) -	} -	return &trillian.GetLeavesByRangeResponse{ -		Leaves:        leaves, -		SignedLogRoot: Tslr(t, Tlr(t, uint64(end)+1, Timestamp, NodeHash)).SignedLogRoot, -	} -} - -func DefaultStItemList(t *testing.T, start, end uint64) *types.StItemList { -	items := make([]types.StItem, 0, end-start+1) -	for i, n := start, end+1; i < n; i++ { -		items = append(items, *DefaultSignedChecksum(t, Ed25519VkSubmitter)) -	} -	return &types.StItemList{items} -} - -// DefaultTGipbhr creates a default Trillian get inclusion proof by hash response -func DefaultTGipbhr(t *testing.T) *trillian.GetInclusionProofByHashResponse { -	t.Helper() -	return &trillian.GetInclusionProofByHashResponse{ -		Proof: []*trillian.Proof{ -			&trillian.Proof{ -				LeafIndex: Index, -				Hashes:    HashPath, -			}, -		}, -		SignedLogRoot: nil, // not used by stfe -	} -} - -func DefaultInclusionProof(t *testing.T, size uint64) *types.StItem { -	return types.NewInclusionProofV1(NewNamespace(t, Ed25519VkLog), size, uint64(Index), NodePath) -} - -// DefaultTGcpr creates a default Trillian get consistency proof response -func DefaultTGcpr(t *testing.T) *trillian.GetConsistencyProofResponse { -	t.Helper() -	return &trillian.GetConsistencyProofResponse{ -		Proof: &trillian.Proof{ -			LeafIndex: 0, // not applicable for consistency proofs -			Hashes:    HashPath, -		}, -		SignedLogRoot: nil, // not used by stfe -	} -} - -func DefaultConsistencyProof(t *testing.T, first, second uint64) *types.StItem { -	return types.NewConsistencyProofV1(NewNamespace(t, Ed25519VkLog), first, second, NodePath) -} - -// -// Other helpers -// - -func Fingerprint(t *testing.T, namespace *types.Namespace) [types.NamespaceFingerprintSize]byte { -	fpr, err := namespace.Fingerprint() -	if err != nil { -		t.Fatalf("must have namespace fingerprint: %v", err) -	} -	return *fpr -} - -func marshal(t *testing.T, i interface{}) []byte { -	b, err := types.Marshal(i) -	if err != nil { -		t.Fatalf("must marshal interface: %v", err) -	} -	return b -} diff --git a/trillian.go b/trillian.go deleted file mode 100644 index 2adf567..0000000 --- a/trillian.go +++ /dev/null @@ -1,125 +0,0 @@ -package stfe - -import ( -	"fmt" - -	"github.com/golang/glog" -	"github.com/google/trillian" -	"github.com/google/trillian/types" -	stfetypes "github.com/system-transparency/stfe/types" -	"google.golang.org/grpc/codes" -) - -func checkQueueLeaf(rsp *trillian.QueueLeafResponse, err error) error { -	if err != nil { -		return fmt.Errorf("Trillian error: %v", err) -	} -	if rsp == nil { -		return fmt.Errorf("Trillian error: empty response") -	} -	if rsp.QueuedLeaf == nil { -		return fmt.Errorf("Trillian error: empty QueuedLeaf") -	} -	if codes.Code(rsp.QueuedLeaf.GetStatus().GetCode()) == codes.AlreadyExists { -		glog.V(3).Infof("queued leaf is a duplicate => %X", rsp.QueuedLeaf.Leaf.LeafValue) -	} -	return nil -} - -func checkGetLeavesByRange(req *stfetypes.GetEntriesV1, rsp *trillian.GetLeavesByRangeResponse, err error) error { -	if err != nil { -		return fmt.Errorf("Trillian Error: %v", err) -	} -	if rsp == nil { -		return fmt.Errorf("Trillian error: empty response") -	} -	if rsp.SignedLogRoot == nil { -		return fmt.Errorf("Trillian error: no signed log root") -	} -	if rsp.SignedLogRoot.LogRoot == nil { -		return fmt.Errorf("Trillian error: no log root") -	} -	if len(rsp.Leaves) == 0 { -		return fmt.Errorf("Trillian error: no leaves") -	} -	if len(rsp.Leaves) > int(req.End-req.Start+1) { -		return fmt.Errorf("too many leaves: %d for [%d,%d]", len(rsp.Leaves), req.Start, req.End) -	} - -	// Ensure that a bad start parameter results in an error -	var lr types.LogRootV1 -	if err := lr.UnmarshalBinary(rsp.SignedLogRoot.LogRoot); err != nil { -		return fmt.Errorf("cannot unmarshal log root: %v", err) -	} -	if uint64(req.Start) >= lr.TreeSize { -		return fmt.Errorf("invalid start(%d): tree size is %d", req.Start, lr.TreeSize) -	} - -	// Ensure that we got and return expected leaf indices -	for i, leaf := range rsp.Leaves { -		if got, want := leaf.LeafIndex, int64(req.Start+uint64(i)); got != want { -			return fmt.Errorf("invalid leaf index(%d): wanted %d", got, want) -		} -	} -	return nil -} - -func checkGetInclusionProofByHash(lp *LogParameters, rsp *trillian.GetInclusionProofByHashResponse, err error) error { -	if err != nil { -		return fmt.Errorf("Trillian Error: %v", err) -	} -	if rsp == nil { -		return fmt.Errorf("Trillian error: empty response") -	} -	if len(rsp.Proof) == 0 { -		return fmt.Errorf("Trillian error: no proofs") -	} -	if rsp.Proof[0] == nil { -		return fmt.Errorf("Trillian error: no proof") -	} -	return checkHashPath(lp.HashType.Size(), rsp.Proof[0].Hashes) -} - -func checkGetConsistencyProof(lp *LogParameters, rsp *trillian.GetConsistencyProofResponse, err error) error { -	if err != nil { -		return fmt.Errorf("Trillian Error: %v", err) -	} -	if rsp == nil { -		return fmt.Errorf("Trillian error: empty response") -	} -	if rsp.Proof == nil { -		return fmt.Errorf("Trillian error: no proof") -	} -	return checkHashPath(lp.HashType.Size(), rsp.Proof.Hashes) -} - -func checkGetLatestSignedLogRoot(lp *LogParameters, rsp *trillian.GetLatestSignedLogRootResponse, err error, out *types.LogRootV1) error { -	if err != nil { -		return fmt.Errorf("Trillian Error: %v", err) -	} -	if rsp == nil { -		return fmt.Errorf("Trillian error: empty response") -	} -	if rsp.SignedLogRoot == nil { -		return fmt.Errorf("Trillian error: no signed log root") -	} -	if rsp.SignedLogRoot.LogRoot == nil { -		return fmt.Errorf("Trillian error: no log root") -	} -	if err := out.UnmarshalBinary(rsp.SignedLogRoot.LogRoot); err != nil { -		return fmt.Errorf("cannot unmarshal log root: %v", err) -	} -	if len(out.RootHash) != lp.HashType.Size() { -		return fmt.Errorf("invalid root hash: %v", out.RootHash) -	} -	return nil -} - -func checkHashPath(hashSize int, path [][]byte) error { -	for _, hash := range path { -		if len(hash) != hashSize { -			return fmt.Errorf("invalid proof: %v", path) -		} -	} -	return nil -} diff --git a/trillian_test.go b/trillian_test.go deleted file mode 100644 index 1b0c923..0000000 --- a/trillian_test.go +++ /dev/null @@ -1,282 +0,0 @@ -package stfe - -import ( -	"fmt" -	"testing" - -	"github.com/google/trillian" -	ttypes "github.com/google/trillian/types" -	"github.com/system-transparency/stfe/testdata" -	"github.com/system-transparency/stfe/types" -) - -func TestCheckQueueLeaf(t *testing.T) { -	for _, table := range []struct { -		description string -		rsp         *trillian.QueueLeafResponse -		err         error -		wantErr     bool -	}{ -		{ -			description: "invalid: no Trillian response: error", -			err:         fmt.Errorf("backend error"), -			wantErr:     true, -		}, -		{ -			description: "invalid: no Trillian response: nil", -			wantErr:     true, -		}, -		{ -			description: "invalid: no Trillian response: empty", -			rsp:         &trillian.QueueLeafResponse{}, -			wantErr:     true, -		}, -		{ -			description: "valid: gRPC status: duplicate", -			rsp:         testdata.DefaultTQlr(t, true), -		}, -		{ -			description: "valid: gRPC status: ok", -			rsp:         testdata.DefaultTQlr(t, false), -		}, -	} { -		err := checkQueueLeaf(table.rsp, table.err) -		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 TestCheckGetLeavesByRange(t *testing.T) { -	for _, table := range []struct { -		description string -		req         *types.GetEntriesV1 -		rsp         *trillian.GetLeavesByRangeResponse -		err         error -		wantErr     bool -	}{ -		{ -			description: "invalid: no Trillian response: error", -			req:         &types.GetEntriesV1{Start: 0, End: 1}, -			err:         fmt.Errorf("backend error"), -			wantErr:     true, -		}, -		{ -			description: "invalid: no Trillian response: nil", -			req:         &types.GetEntriesV1{Start: 0, End: 1}, -			wantErr:     true, -		}, -		{ -			description: "invalid: bad Trillian response: no leaves", -			req:         &types.GetEntriesV1{Start: 0, End: 1}, -			rsp: func(rsp *trillian.GetLeavesByRangeResponse) *trillian.GetLeavesByRangeResponse { -				rsp.Leaves = nil -				return rsp -			}(testdata.DefaultTGlbrr(t, 0, 1)), -			wantErr: true, -		}, -		{ -			description: "invalid: bad Trillian response: no signed log root", -			req:         &types.GetEntriesV1{Start: 0, End: 1}, -			rsp: func(rsp *trillian.GetLeavesByRangeResponse) *trillian.GetLeavesByRangeResponse { -				rsp.SignedLogRoot = nil -				return rsp -			}(testdata.DefaultTGlbrr(t, 0, 1)), -			wantErr: true, -		}, -		{ -			description: "invalid: bad Trillian response: no log root", -			req:         &types.GetEntriesV1{Start: 0, End: 1}, -			rsp: func(rsp *trillian.GetLeavesByRangeResponse) *trillian.GetLeavesByRangeResponse { -				rsp.SignedLogRoot.LogRoot = nil -				return rsp -			}(testdata.DefaultTGlbrr(t, 0, 1)), -			wantErr: true, -		}, -		{ -			description: "invalid: bad Trillian response: truncated log root", -			req:         &types.GetEntriesV1{Start: 0, End: 1}, -			rsp: func(rsp *trillian.GetLeavesByRangeResponse) *trillian.GetLeavesByRangeResponse { -				rsp.SignedLogRoot.LogRoot = rsp.SignedLogRoot.LogRoot[1:] -				return rsp -			}(testdata.DefaultTGlbrr(t, 0, 1)), -			wantErr: true, -		}, -		{ -			description: "invalid: bad Trillian response: too many leaves", -			req:         &types.GetEntriesV1{Start: 0, End: 1}, -			rsp:         testdata.DefaultTGlbrr(t, 0, 2), -			wantErr:     true, -		}, -		{ -			description: "invalid: bad Trillian response: start is not a valid index", -			req:         &types.GetEntriesV1{Start: 10, End: 10}, -			rsp:         testdata.DefaultTGlbrr(t, 9, 9), -			wantErr:     true, -		}, -		{ -			description: "invalid: bad Trillian response: invalid leaf indices", -			req:         &types.GetEntriesV1{Start: 10, End: 11}, -			rsp:         testdata.DefaultTGlbrr(t, 11, 12), -			wantErr:     true, -		}, -		{ -			description: "valid", -			req:         &types.GetEntriesV1{Start: 10, End: 20}, -			rsp:         testdata.DefaultTGlbrr(t, 10, 20), -		}, -	} { -		err := checkGetLeavesByRange(table.req, table.rsp, table.err) -		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 TestCheckGetInclusionProofByHash(t *testing.T) { -	for _, table := range []struct { -		description string -		rsp         *trillian.GetInclusionProofByHashResponse -		err         error -		wantErr     bool -	}{ -		{ -			description: "invalid: no Trillian response: error", -			err:         fmt.Errorf("backend failure"), -			wantErr:     true, -		}, -		{ -			description: "invalid: no Trillian response: nil", -			wantErr:     true, -		}, -		{ -			description: "invalid: bad Trillian response: no proofs", -			rsp:         &trillian.GetInclusionProofByHashResponse{}, -			wantErr:     true, -		}, -		{ -			description: "bad response: no proof", -			rsp: func(rsp *trillian.GetInclusionProofByHashResponse) *trillian.GetInclusionProofByHashResponse { -				rsp.Proof[0] = nil -				return rsp -			}(testdata.DefaultTGipbhr(t)), -			wantErr: true, -		}, -		{ -			description: "bad response: proof with invalid node hash", -			rsp: func(rsp *trillian.GetInclusionProofByHashResponse) *trillian.GetInclusionProofByHashResponse { -				rsp.Proof[0].Hashes = append(rsp.Proof[0].Hashes, make([]byte, 0)) -				return rsp -			}(testdata.DefaultTGipbhr(t)), -			wantErr: true, -		}, -		{ -			description: "valid", -			rsp:         testdata.DefaultTGipbhr(t), -		}, -	} { -		err := checkGetInclusionProofByHash(newLogParameters(t, nil), table.rsp, table.err) -		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 TestCheckGetConsistencyProof(t *testing.T) { -	for _, table := range []struct { -		description string -		rsp         *trillian.GetConsistencyProofResponse -		err         error -		wantErr     bool -	}{ -		{ -			description: "invalid: no Trillian response: error", -			err:         fmt.Errorf("backend failure"), -			wantErr:     true, -		}, -		{ -			description: "invalid: no Trillian response: nil", -			wantErr:     true, -		}, -		{ -			description: "invalid: bad Trillian response: no proof", -			rsp:         &trillian.GetConsistencyProofResponse{}, -			wantErr:     true, -		}, -		{ -			description: "invalid: bad Trillian response: proof with invalid node hash", -			rsp: func(rsp *trillian.GetConsistencyProofResponse) *trillian.GetConsistencyProofResponse { -				rsp.Proof.Hashes = append(rsp.Proof.Hashes, make([]byte, 0)) -				return rsp -			}(testdata.DefaultTGcpr(t)), -			wantErr: true, -		}, -		{ -			description: "valid", -			rsp:         testdata.DefaultTGcpr(t), -		}, -	} { -		err := checkGetConsistencyProof(newLogParameters(t, nil), table.rsp, table.err) -		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 TestCheckGetLatestSignedLogRoot(t *testing.T) { -	for _, table := range []struct { -		description string -		rsp         *trillian.GetLatestSignedLogRootResponse -		err         error -		wantErr     bool -	}{ -		{ -			description: "invalid: no Trillian response: error", -			err:         fmt.Errorf("backend failure"), -			wantErr:     true, -		}, -		{ -			description: "invalid: no Trillian response: nil", -			wantErr:     true, -		}, -		{ -			description: "invalid: bad Trillian response: no signed log root", -			rsp: func(rsp *trillian.GetLatestSignedLogRootResponse) *trillian.GetLatestSignedLogRootResponse { -				rsp.SignedLogRoot = nil -				return rsp -			}(testdata.DefaultTSlr(t)), -			wantErr: true, -		}, -		{ -			description: "invalid: bad Trillian response: no log root", -			rsp: func(rsp *trillian.GetLatestSignedLogRootResponse) *trillian.GetLatestSignedLogRootResponse { -				rsp.SignedLogRoot.LogRoot = nil -				return rsp -			}(testdata.DefaultTSlr(t)), -			wantErr: true, -		}, -		{ -			description: "invalid: bad Trillian response: truncated log root", -			rsp: func(rsp *trillian.GetLatestSignedLogRootResponse) *trillian.GetLatestSignedLogRootResponse { -				rsp.SignedLogRoot.LogRoot = rsp.SignedLogRoot.LogRoot[1:] -				return rsp -			}(testdata.DefaultTSlr(t)), -			wantErr: true, -		}, -		{ -			description: "invalid: bad Trillian response: truncated root hash", -			rsp:         testdata.Tslr(t, testdata.Tlr(t, testdata.TreeSize, testdata.Timestamp, make([]byte, 31))), -			wantErr:     true, -		}, -		{ -			description: "valid", -			rsp:         testdata.DefaultTSlr(t), -		}, -	} { -		var lr ttypes.LogRootV1 -		err := checkGetLatestSignedLogRoot(newLogParameters(t, nil), table.rsp, table.err, &lr) -		if got, want := err != nil, table.wantErr; got != want { -			t.Errorf("got error %v but wanted %v in test %q", got, want, table.description) -		} -	} -} diff --git a/types/cmd/new-namespace/main.go b/types/cmd/new-namespace/main.go deleted file mode 100644 index e338d7c..0000000 --- a/types/cmd/new-namespace/main.go +++ /dev/null @@ -1,56 +0,0 @@ -// Package main outputs the private and public parts of a new namespace -package main - -import ( -	"flag" -	"fmt" - -	"crypto/ed25519" -	"crypto/rand" -	"encoding/base64" - -	"github.com/golang/glog" -	"github.com/system-transparency/stfe/types" -) - -var ( -	format = flag.String("format", string(types.NamespaceFormatEd25519V1), "namespace format") -) - -func main() { -	flag.Parse() -	defer glog.Flush() - -	switch *format { -	case string(types.NamespaceFormatEd25519V1): -		glog.Infof("generating new ed25519_v1 namespace") -		sk, vk, namespace, err := genEd25519V1Namespace() -		if err != nil { -			glog.Errorf("genEd25519V1Namespace: %v", err) -			break -		} -		fmt.Printf("sk: %s\n", base64.StdEncoding.EncodeToString(sk)) -		fmt.Printf("vk: %s\n", base64.StdEncoding.EncodeToString(vk)) -		fmt.Printf("ed25519_v1: %s\n", base64.StdEncoding.EncodeToString(namespace)) -	default: -		glog.Errorf("unsupported namespace format: %s", format) -	} -} - -// genEd25519V1Namespace generates an Ed25519 secret key, verification key, and -// serialized ed25519_v1 namespace. -func genEd25519V1Namespace() ([]byte, []byte, []byte, error) { -	vk, sk, err := ed25519.GenerateKey(rand.Reader) -	if err != nil { -		return nil, nil, nil, fmt.Errorf("ed25519.GenerateKey: %v", err) -	} -	namespace, err := types.NewNamespaceEd25519V1(vk[:]) -	if err != nil { -		return nil, nil, nil, fmt.Errorf("types.NewNamespaceEd25519V1: %v", err) -	} -	serialized, err := types.Marshal(*namespace) -	if err != nil { -		fmt.Errorf("types.Marshal: %v", err) -	} -	return sk, vk, serialized, nil -} diff --git a/types/namespace.go b/types/namespace.go deleted file mode 100644 index 376ebcd..0000000 --- a/types/namespace.go +++ /dev/null @@ -1,91 +0,0 @@ -package types - -import ( -	"fmt" - -	"crypto/ed25519" - -	"github.com/google/certificate-transparency-go/tls" -) - -// NamespaceFormat defines a particular namespace type that is versioend -type NamespaceFormat tls.Enum - -const ( -	NamespaceFormatReserved  NamespaceFormat = 0 -	NamespaceFormatEd25519V1 NamespaceFormat = 1 - -	NamespaceFingerprintSize = 32 -) - -// Namespace references a versioned namespace based on a given format specifier -type Namespace struct { -	Format    NamespaceFormat `tls:"maxval:65535"` -	Ed25519V1 *Ed25519V1      `tls:"selector:Format,val:1"` -} - -// Ed25519V1 uses an Ed25519 verification key as namespace.  Encoding, -// signing, and verification operations are defined by RFC 8032. -type Ed25519V1 struct { -	Namespace [32]byte -} - -func (f NamespaceFormat) String() string { -	switch f { -	case NamespaceFormatReserved: -		return "reserved" -	case NamespaceFormatEd25519V1: -		return "ed25519_v1" -	default: -		return fmt.Sprintf("unknown NamespaceFormat: %d", f) -	} -} - -func (n Namespace) String() string { -	switch n.Format { -	case NamespaceFormatReserved: -		return fmt.Sprintf("Format(%s)", n.Format) -	case NamespaceFormatEd25519V1: -		return fmt.Sprintf("Format(%s): %+v", n.Format, n.Ed25519V1) -	default: -		return fmt.Sprintf("unknown Namespace: %v", n.Format) -	} -} - -// Fingerprint returns a fixed-size namespace fingerprint that is unique. -func (n *Namespace) Fingerprint() (*[NamespaceFingerprintSize]byte, error) { -	switch n.Format { -	case NamespaceFormatEd25519V1: -		return &n.Ed25519V1.Namespace, nil -	default: -		return nil, fmt.Errorf("unsupported NamespaceFormat: %v", n.Format) -	} -} - -// Verify checks that signature is valid over message for this namespace -func (ns *Namespace) Verify(message, signature []byte) error { -	switch ns.Format { -	case NamespaceFormatEd25519V1: -		if !ed25519.Verify(ed25519.PublicKey(ns.Ed25519V1.Namespace[:]), message, signature) { -			return fmt.Errorf("ed25519 signature verification failed") -		} -	default: -		return fmt.Errorf("namespace not supported: %v", ns.Format) -	} -	return nil -} - -// NewNamespaceEd25519V1 returns an new Ed25519V1 namespace based on a -// verification key. -func NewNamespaceEd25519V1(vk []byte) (*Namespace, error) { -	if len(vk) != 32 { -		return nil, fmt.Errorf("invalid verification key: must be 32 bytes") -	} - -	var ed25519v1 Ed25519V1 -	copy(ed25519v1.Namespace[:], vk) -	return &Namespace{ -		Format:    NamespaceFormatEd25519V1, -		Ed25519V1: &ed25519v1, -	}, nil -} diff --git a/types/namespace_pool.go b/types/namespace_pool.go deleted file mode 100644 index 1e9e8f6..0000000 --- a/types/namespace_pool.go +++ /dev/null @@ -1,69 +0,0 @@ -package types - -import ( -	"fmt" -) - -// NamespacePool is a pool of namespaces that contain complete verification keys -type NamespacePool struct { -	pool map[[NamespaceFingerprintSize]byte]*Namespace -	list []*Namespace -	// If we need to update this structure without a restart => add mutex. -} - -// NewNameSpacePool creates a new namespace pool from a list of namespaces.  An -// error is returned if there are duplicate namespaces or namespaces without a -// complete verification key.  The latter is determined by namespaceWithKey(). -func NewNamespacePool(namespaces []*Namespace) (*NamespacePool, error) { -	np := &NamespacePool{ -		pool: make(map[[NamespaceFingerprintSize]byte]*Namespace), -		list: make([]*Namespace, 0), -	} -	for _, namespace := range namespaces { -		if !namespaceWithKey(namespace.Format) { -			return nil, fmt.Errorf("need verification key in namespace pool: %v", namespace.Format) -		} -		fpr, err := namespace.Fingerprint() -		if err != nil { -			return nil, fmt.Errorf("need fingerprint in namespace pool: %v", err) -		} -		if _, ok := np.pool[*fpr]; ok { -			return nil, fmt.Errorf("duplicate namespace: %v", namespace.String()) -		} -		np.pool[*fpr] = namespace -		np.list = append(np.list, namespace) -	} -	return np, nil -} - -// Find checks if namespace is a member of the namespace pool. -func (np *NamespacePool) Find(namespace *Namespace) (*Namespace, bool) { -	fpr, err := namespace.Fingerprint() -	if err != nil { -		return nil, false -	} -	if _, ok := np.pool[*fpr]; !ok { -		return nil, false -	} -	// If the passed namespace is a key fingerprint the actual key needs to be -	// attached before returning.  Not applicable for Ed25519.  Docdoc later. -	return namespace, true -} - -// List returns a copied list of namespaces that is used by this pool. -func (np *NamespacePool) List() []*Namespace { -	namespaces := make([]*Namespace, len(np.list)) -	copy(namespaces, np.list) -	return namespaces -} - -// namespaceWithKey returns true if a namespace format contains a complete -// verification key.  I.e., some formats might have a key fingerprint instead. -func namespaceWithKey(format NamespaceFormat) bool { -	switch format { -	case NamespaceFormatEd25519V1: -		return true -	default: -		return false -	} -} diff --git a/types/namespace_pool_test.go b/types/namespace_pool_test.go deleted file mode 100644 index f5810a2..0000000 --- a/types/namespace_pool_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package types - -import ( -	"bytes" -	"reflect" -	"testing" -) - -func TestNewNamespacePool(t *testing.T) { -	ns1 := mustInitNamespaceEd25519V1(t, 0x00) -	ns2 := mustInitNamespaceEd25519V1(t, 0xff) -	nsr := &Namespace{Format: NamespaceFormatReserved} -	for _, table := range []struct { -		description string -		namespaces  []*Namespace -		wantErr     bool -	}{ -		{ -			description: "invalid: duplicate namespace", -			namespaces:  []*Namespace{ns1, ns1, ns2}, -			wantErr:     true, -		}, -		{ -			description: "invalid: namespace without key", -			namespaces:  []*Namespace{ns1, nsr, ns2}, -			wantErr:     true, -		}, -		{ -			description: "valid: empty", -			namespaces:  []*Namespace{}, -		}, -		{ -			description: "valid: one namespace", -			namespaces:  []*Namespace{ns1}, -		}, -		{ -			description: "valid: two namespaces", -			namespaces:  []*Namespace{ns1, ns2}, -		}, -	} { -		_, err := NewNamespacePool(table.namespaces) -		if got, want := err != nil, table.wantErr; got != want { -			t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) -		} -	} -} - -func TestFind(t *testing.T) { -	ns1 := mustInitNamespaceEd25519V1(t, 0x00) -	ns2 := mustInitNamespaceEd25519V1(t, 0xff) - -	// Empty pool -	pool, err := NewNamespacePool(nil) -	if err != nil { -		t.Fatalf("must create new namespace pool: %v", err) -	} -	if _, ok := pool.Find(ns1); ok { -		t.Errorf("found namespace in empty pool") -	} - -	// Pool with one namespace -	pool, err = NewNamespacePool([]*Namespace{ns1}) -	if err != nil { -		t.Fatalf("must create new namespace pool: %v", err) -	} -	if ns, ok := pool.Find(ns1); !ok { -		t.Errorf("could not find namespace that is a member of the pool") -	} else if !reflect.DeepEqual(ns, ns1) { -		t.Errorf("found namespace but it is wrong") -	} -	if _, ok := pool.Find(ns2); ok { -		t.Errorf("found namespace although it is not a member of the pool") -	} -} - -func TestList(t *testing.T) { -	ns1 := mustInitNamespaceEd25519V1(t, 0x00) -	ns2 := mustInitNamespaceEd25519V1(t, 0xff) -	namespaces := []*Namespace{ns1, ns2} -	pool, err := NewNamespacePool(namespaces) -	if err != nil { -		t.Fatalf("must create new namespace pool: %v", err) -	} -	if got, want := len(pool.List()), len(namespaces); got != want { -		t.Errorf("got len %v but wanted %v", got, want) -	} -	pool.List()[0] = ns2 -	if got, want := pool.List()[0].Ed25519V1.Namespace[:], ns1.Ed25519V1.Namespace[:]; !bytes.Equal(got, want) { -		t.Errorf("returned list is not a copy") -	} -} diff --git a/types/namespace_test.go b/types/namespace_test.go deleted file mode 100644 index a5847ef..0000000 --- a/types/namespace_test.go +++ /dev/null @@ -1,200 +0,0 @@ -package types - -import ( -	"bytes" -	"strings" -	"testing" - -	"crypto/ed25519" -) - -// TestNamespaceString checks that the String() function prints the right -// format, and that the body is printed without a nil-pointer panic. -func TestNamespaceString(t *testing.T) { -	wantPrefix := map[NamespaceFormat]string{ -		NamespaceFormatReserved:    "Format(reserved)", -		NamespaceFormatEd25519V1:   "Format(ed25519_v1): &{Namespace", -		NamespaceFormat(1<<16 - 1): "unknown Namespace: unknown NamespaceFormat: 65535", -	} -	tests := append(test_cases_namespace(t), testCaseSerialize{ -		description: "valid: unknown Namespace", -		item: Namespace{ -			Format: NamespaceFormat(1<<16 - 1), -		}, -	}) -	for _, table := range tests { -		namespace, ok := table.item.(Namespace) -		if !ok { -			t.Fatalf("must cast to Namespace in test %q", table.description) -		} - -		prefix, ok := wantPrefix[namespace.Format] -		if !ok { -			t.Fatalf("must have prefix for StFormat %v in test %q", namespace.Format, table.description) -		} -		if got, want := namespace.String(), prefix; !strings.HasPrefix(got, want) { -			t.Errorf("got %q but wanted prefix %q in test %q", got, want, table.description) -		} -	} -} - -func TestFingerprint(t *testing.T) { -	for _, table := range []struct { -		description string -		namespace   *Namespace -		wantErr     bool -		wantFpr     [NamespaceFingerprintSize]byte -	}{ -		{ -			description: "invalid: no fingerprint for type", -			namespace: &Namespace{ -				Format: NamespaceFormatReserved, -			}, -			wantErr: true, -		}, -		{ -			description: "valid: ed25519_v1", -			namespace:   mustInitNamespaceEd25519V1(t, 0xaf), -			wantFpr: func() (ret [NamespaceFingerprintSize]byte) { -				for i, _ := range ret { -					ret[i] = 0xaf -				} -				return -			}(), -		}, -	} { -		fpr, err := table.namespace.Fingerprint() -		if got, want := err != nil, table.wantErr; got != want { -			t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) -		} -		if err != nil { -			continue -		} -		if got, want := *fpr, table.wantFpr; !bytes.Equal(got[:], want[:]) { -			t.Errorf("got fpr %v but wanted %v in test %q", got, want, table.description) -		} -	} -} - -func TestVerify(t *testing.T) { -	var tests []testCaseNamespace -	tests = append(tests, test_cases_verify(t)...) -	tests = append(tests, test_cases_verify_ed25519v1(t)...) -	for _, table := range tests { -		err := table.namespace.Verify(table.msg, table.sig) -		if got, want := err != nil, table.wantErr; got != want { -			t.Errorf("got error=%v but wanted %v in test %q: %v", got, want, table.description, err) -		} -	} -} - -func TestNewNamespaceEd25519V1(t *testing.T) { -	size := 32 // verification key size -	for _, table := range []struct { -		description string -		vk          []byte -		wantErr     bool -	}{ -		{ -			description: "invalid", -			vk:          make([]byte, size+1), -			wantErr:     true, -		}, -		{ -			description: "valid", -			vk:          make([]byte, size), -		}, -	} { -		n, err := NewNamespaceEd25519V1(table.vk) -		if got, want := err != nil, table.wantErr; got != want { -			t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) -		} -		if err != nil { -			continue -		} -		if got, want := n.Format, NamespaceFormatEd25519V1; got != want { -			t.Errorf("got namespace format %v but wanted %v in test %q", got, want, table.description) -			continue -		} -		if got, want := n.Ed25519V1.Namespace[:], table.vk; !bytes.Equal(got, want) { -			t.Errorf("got namespace %X but wanted %X in test %q", got, want, table.description) -		} -	} -} - -// testCaseNamespace is a common test case used for Namespace.Verify() tests -type testCaseNamespace struct { -	description string -	namespace   *Namespace -	msg, sig    []byte -	wantErr     bool -} - -// test_cases_verify returns basic namespace.Verify() tests -func test_cases_verify(t *testing.T) []testCaseNamespace { -	return []testCaseNamespace{ -		{ -			description: "test_cases_verify: invalid: unsupported namespace", -			namespace:   &Namespace{Format: NamespaceFormatReserved}, -			msg:         []byte("msg"), -			sig:         []byte("sig"), -			wantErr:     true, -		}, -	} -} - -// test_cases_verify_ed25519v1 returns ed25519_v1 Namespace.Verify() tests -func test_cases_verify_ed25519v1(t *testing.T) []testCaseNamespace { -	testEd25519Sk := [64]byte{230, 122, 195, 152, 194, 195, 147, 153, 80, 120, 153, 79, 102, 27, 52, 187, 136, 218, 150, 234, 107, 9, 167, 4, 92, 21, 11, 113, 42, 29, 129, 69, 75, 60, 249, 150, 229, 93, 75, 32, 103, 126, 244, 37, 53, 182, 68, 82, 249, 109, 49, 94, 10, 19, 146, 244, 58, 191, 169, 107, 78, 37, 45, 210} -	testEd25519Vk := [32]byte{75, 60, 249, 150, 229, 93, 75, 32, 103, 126, 244, 37, 53, 182, 68, 82, 249, 109, 49, 94, 10, 19, 146, 244, 58, 191, 169, 107, 78, 37, 45, 210} -	return []testCaseNamespace{ -		{ -			description: "test_cases_verify_ed25519v1: invalid: sk signed message, but vk is not for sk", -			namespace: &Namespace{ -				Format: NamespaceFormatEd25519V1, -				Ed25519V1: &Ed25519V1{ -					Namespace: [32]byte{}, -				}, -			}, -			msg:     []byte("message"), -			sig:     ed25519.Sign(ed25519.PrivateKey(testEd25519Sk[:]), []byte("message")), -			wantErr: true, -		}, -		{ -			description: "test_cases_verify_ed25519v1: invalid: vk is for sk, but sk did not sign message", -			namespace: &Namespace{ -				Format: NamespaceFormatEd25519V1, -				Ed25519V1: &Ed25519V1{ -					Namespace: testEd25519Vk, -				}, -			}, -			msg:     []byte("some message"), -			sig:     ed25519.Sign(ed25519.PrivateKey(testEd25519Sk[:]), []byte("another message")), -			wantErr: true, -		}, -		{ -			description: "test_cases_verify_ed25519v1: valid", -			namespace: &Namespace{ -				Format: NamespaceFormatEd25519V1, -				Ed25519V1: &Ed25519V1{ -					Namespace: testEd25519Vk, -				}, -			}, -			msg: []byte("message"), -			sig: ed25519.Sign(ed25519.PrivateKey(testEd25519Sk[:]), []byte("message")), -		}, -	} -} - -func mustInitNamespaceEd25519V1(t *testing.T, initByte byte) *Namespace { -	t.Helper() -	buf := make([]byte, 32) -	for i := 0; i < len(buf); i++ { -		buf[i] = initByte -	} -	ns, err := NewNamespaceEd25519V1(buf) -	if err != nil { -		t.Fatalf("must make Ed25519v1 namespace: %v", err) -	} -	return ns -} diff --git a/types/serialize.go b/types/serialize.go deleted file mode 100644 index fd93336..0000000 --- a/types/serialize.go +++ /dev/null @@ -1,50 +0,0 @@ -package types - -import ( -	"fmt" - -	"github.com/google/certificate-transparency-go/tls" -) - -const ( -	HashSizeV1 = 32 -) - -// GetProofByHashV1 is a serializable get-proof-by-hash request -type GetProofByHashV1 struct { -	Hash     [HashSizeV1]byte -	TreeSize uint64 -} - -// GetConsistencyProofV1 is a serializable get-consistency-proof request -type GetConsistencyProofV1 struct { -	First  uint64 -	Second uint64 -} - -// GetEntriesV1 is a serializable get-entries request -type GetEntriesV1 struct { -	Start uint64 -	End   uint64 -} - -// Marshal marshals a TLS-encodable structure -func Marshal(item interface{}) ([]byte, error) { -	serialized, err := tls.Marshal(item) -	if err != nil { -		return nil, fmt.Errorf("tls.Marshal: %v", err) -	} -	return serialized, nil -} - -// Unmarshal unmarshals a TLS-encoded structure -func Unmarshal(serialized []byte, out interface{}) error { -	extra, err := tls.Unmarshal(serialized, out) -	if err != nil { -		return fmt.Errorf("tls.Unmarshal: %v", err) -	} -	if len(extra) > 0 { -		return fmt.Errorf("tls.Unmarshal: extra data: %X", extra) -	} -	return nil -} diff --git a/types/serialize_test.go b/types/serialize_test.go deleted file mode 100644 index a06effe..0000000 --- a/types/serialize_test.go +++ /dev/null @@ -1,736 +0,0 @@ -package types - -import ( -	"bytes" -	"testing" - -	"encoding/binary" -) - -// testCaseSerialize is a common test case used for ST log types -type testCaseSerialize struct { -	description string -	item        interface{} -	wantErr     bool -	wantBytes   []byte // only used if no error and not equal to nil -} - -// TestMarshalUnmarshal tests that valid ST log structures can be marshalled and -// then unmarshalled without error, and that invalid ST log structures cannot be -// marshalled.  If wantBytes is non-nil the marshalled result must also match. -func TestMarshalUnmarshal(t *testing.T) { -	var tests []testCaseSerialize -	tests = append(tests, test_cases_stitemlist(t)...) -	tests = append(tests, test_cases_stitem(t)...) -	tests = append(tests, test_cases_sthv1(t)...) -	tests = append(tests, test_cases_costhv1(t)...) -	tests = append(tests, test_cases_cpv1(t)...) -	tests = append(tests, test_cases_ipv1(t)...) -	tests = append(tests, test_cases_signed_checksumv1(t)...) -	tests = append(tests, test_cases_checksumv1(t)...) -	tests = append(tests, test_cases_thv1(t)...) -	tests = append(tests, test_cases_nh(t)...) -	tests = append(tests, test_cases_sigv1(t)...) -	tests = append(tests, test_cases_namespace(t)...) -	tests = append(tests, test_cases_ed25519v1(t)...) -	tests = append(tests, test_cases_requests(t)...) -	for _, table := range tests { -		b, err := Marshal(table.item) -		if got, want := err != nil, table.wantErr; got != want { -			t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) -		} -		if err != nil { -			continue // nothing to unmarshal -		} -		if got, want := b, table.wantBytes; want != nil && !bytes.Equal(got, want) { -			t.Errorf("got bytes \n%v\n\tbut wanted\n%v\n\t in test %q: %v", got, want, table.description, err) -		} - -		switch table.item.(type) { -		case StItemList: -			var item StItemList -			err = Unmarshal(b, &item) -		case StItem: -			var item StItem -			err = Unmarshal(b, &item) -		case SignedTreeHeadV1: -			var item SignedTreeHeadV1 -			err = Unmarshal(b, &item) -		case CosignedTreeHeadV1: -			var item CosignedTreeHeadV1 -			err = Unmarshal(b, &item) -		case ConsistencyProofV1: -			var item ConsistencyProofV1 -			err = Unmarshal(b, &item) -		case InclusionProofV1: -			var item InclusionProofV1 -			err = Unmarshal(b, &item) -		case SignedChecksumV1: -			var item SignedChecksumV1 -			err = Unmarshal(b, &item) -		case ChecksumV1: -			var item ChecksumV1 -			err = Unmarshal(b, &item) -		case TreeHeadV1: -			var item TreeHeadV1 -			err = Unmarshal(b, &item) -		case NodeHash: -			var item NodeHash -			err = Unmarshal(b, &item) -		case SignatureV1: -			var item SignatureV1 -			err = Unmarshal(b, &item) -		case Namespace: -			var item Namespace -			err = Unmarshal(b, &item) -		case Ed25519V1: -			var item Ed25519V1 -			err = Unmarshal(b, &item) -		case GetProofByHashV1: -			var item GetProofByHashV1 -			err = Unmarshal(b, &item) -		case GetConsistencyProofV1: -			var item GetConsistencyProofV1 -			err = Unmarshal(b, &item) -		case GetEntriesV1: -			var item GetEntriesV1 -			err = Unmarshal(b, &item) -		default: -			t.Errorf("unhandled type in test %q", table.description) -		} -		if err != nil { -			t.Errorf("unmarshal failed but wanted success in test %q: %v", table.description, err) -		} -	} -} - -// TestUnmarshalStItem tests that invalid StItems cannot be unmarshalled -func TestUnmarshalStItem(t *testing.T) { -	tests := test_cases_stitem(t)[1:] // skip reserved type -	for _, table := range tests { -		description := table.description[7:] // skip "valid: " prefix -		b, err := Marshal(table.item) -		if err != nil { -			t.Fatalf("must marshal in test %q: %v", description, err) -		} - -		var item StItem -		if err := Unmarshal(append(b[:], []byte{0}...), &item); err == nil { -			t.Errorf("unmarshal suceeded with one extra byte in test %q", description) -		} -		if err := Unmarshal(b[:len(b)-1], &item); err == nil { -			t.Errorf("unmarshal suceeded with one byte short in test %q", description) -		} -		if err := Unmarshal(append(b[:], b[:]...), &item); err == nil { -			t.Errorf("unmarshal succeeded with appended StItem in test %q", description) -		} -		if err := Unmarshal([]byte{0}, &item); err == nil { -			t.Errorf("unmarshal succeeded with a single byte in test %q", description) -		} -	} -} - -// test_cases_stitemlist returns test cases for the StItemList type -func test_cases_stitemlist(t *testing.T) []testCaseSerialize { -	t.Helper() -	return []testCaseSerialize{ -		testCaseSerialize{ -			description: "test_cases_stitemlist: valid: StItemList: empty", -			item:        StItemList{}, -			wantBytes:   []byte{0x00, 0x00, 0x00, 0x00}, -		}, // skip max len check because it is huge -		testCaseSerialize{ -			description: "test_cases_stitemlist: valid: mixed content", -			item:        testStItemList, -			wantBytes:   testStItemListBytes, -		}, // other invalid bounds are already tested in subtypes -	} -} - -// test_cases_stitem returns test cases for the different StItem types -func test_cases_stitem(t *testing.T) []testCaseSerialize { -	t.Helper() -	return []testCaseSerialize{ -		{ -			description: "invalid: StItem: reserved", -			item:        testStItemReserved, -			wantErr:     true, -		}, -		{ -			description: "valid: StItem: signed_tree_head_v1", -			item:        testStItemSignedTreeHeadV1, -			wantBytes:   testStItemSignedTreeHeadV1Bytes, -		}, -		{ -			description: "valid: StItem: cosigned_tree_head_v1", -			item:        testStItemCosignedTreeHeadV1, -			wantBytes:   testStItemCosignedTreeHeadV1Bytes, -		}, -		{ -			description: "valid: StItem: consistency_proof_v1", -			item:        testStItemConsistencyProofV1, -			wantBytes:   testStItemConsistencyProofV1Bytes, -		}, -		{ -			description: "valid: StItem: inclusion_proof_v1", -			item:        testStItemInclusionProofV1, -			wantBytes:   testStItemInclusionProofV1Bytes, -		}, -		{ -			description: "valid: StItem: signed_checksum_v1", -			item:        testStItemSignedChecksumV1, -			wantBytes:   testStItemSignedChecksumV1Bytes, -		}, // other invalid bounds are already tested in subtypes -	} -} - -// test_cases_sthv1 returns test cases for the SignedTreeHeadV1 structure -func test_cases_sthv1(t *testing.T) []testCaseSerialize { -	t.Helper() -	return []testCaseSerialize{ -		{ -			description: "valid: testSignedTreeHeadV1", -			item:        testSignedTreeHeadV1, -			wantBytes:   testSignedTreeHeadV1Bytes, -		}, // other invalid bounds are already tested in subtypes -	} -} - -// test_cases_costhv1 returns test cases for the CosignedTreeHeadV1 structure -func test_cases_costhv1(t *testing.T) []testCaseSerialize { -	t.Helper() -	return []testCaseSerialize{ -		{ -			description: "test_cases_costhv1: valid: min", -			item: CosignedTreeHeadV1{ -				SignedTreeHead: testSignedTreeHeadV1, -				Cosignatures:   make([]SignatureV1, 0), -			}, -		}, // skipping "valid: max" because it is huge -		{ -			description: "test_cases_costhv1: testCosignedTreeHeadV1", -			item:        testCosignedTreeHeadV1, -			wantBytes:   testCosignedTreeHeadV1Bytes, -		}, // other invalid bounds are already tested in subtypes -	} -} - -// test_cases_cpv1 returns test cases for the ConsistencyProofV1 structure -func test_cases_cpv1(t *testing.T) []testCaseSerialize { -	t.Helper() -	max := 65535 // max consistency proof -	return []testCaseSerialize{ -		{ -			description: "test_cases_cpv1: invalid: >max", -			item: ConsistencyProofV1{ -				LogId:     testNamespace, -				TreeSize1: 0, -				TreeSize2: 0, -				ConsistencyPath: func() []NodeHash { -					var path []NodeHash -					for sum := 0; sum < max+1; sum += 1 + len(testNodeHash.Data) { -						path = append(path, testNodeHash) -					} -					return path -				}(), -			}, -			wantErr: true, -		}, -		{ -			description: "test_cases_cpv1: valid: min", -			item: ConsistencyProofV1{ -				LogId:           testNamespace, -				TreeSize1:       0, -				TreeSize2:       0, -				ConsistencyPath: make([]NodeHash, 0), -			}, -		}, -		{ -			description: "test_cases_cpv1: valid: testConsistencyProofV1", -			item:        testConsistencyProofV1, -			wantBytes:   testConsistencyProofV1Bytes, -		}, // other invalid bounds are already tested in subtypes -	} -} - -// test_cases_ipv1 returns test cases for the InclusionProofV1 structure -func test_cases_ipv1(t *testing.T) []testCaseSerialize { -	t.Helper() -	max := 65535 // max inclusion proof -	return []testCaseSerialize{ -		{ -			description: "test_cases_ipv1: invalid: >max", -			item: InclusionProofV1{ -				LogId:     testNamespace, -				TreeSize:  0, -				LeafIndex: 0, -				InclusionPath: func() []NodeHash { -					var path []NodeHash -					for sum := 0; sum < max+1; sum += 1 + len(testNodeHash.Data) { -						path = append(path, testNodeHash) -					} -					return path -				}(), -			}, -			wantErr: true, -		}, -		{ -			description: "test_cases_ipv1: valid: min", -			item: InclusionProofV1{ -				LogId:         testNamespace, -				TreeSize:      0, -				LeafIndex:     0, -				InclusionPath: make([]NodeHash, 0), -			}, -		}, -		{ -			description: "test_cases_ipv1: valid: testInclusionProofV1", -			item:        testInclusionProofV1, -			wantBytes:   testInclusionProofV1Bytes, -		}, // other invalid bounds are already tested in subtypes -	} -} - -// test_cases_signed_checksumv1 returns test cases for the SignedChecksumV1 structure -func test_cases_signed_checksumv1(t *testing.T) []testCaseSerialize { -	t.Helper() -	return []testCaseSerialize{ -		{ -			description: "test_cases_signed_checksumv1: valid: testSignedChecksumV1", -			item:        testSignedChecksumV1, -			wantBytes:   testSignedChecksumV1Bytes, -		}, // other invalid bounds are already tested in subtypes -	} -} - -// test_cases_checksumv1 returns test cases for the ChecksumV1 structure -func test_cases_checksumv1(t *testing.T) []testCaseSerialize { -	t.Helper() -	minIdentifier, maxIdentifier, identifier := 1, 128, []byte("foobar-1-2-3") -	minChecksum, maxChecksum, checksum := 1, 64, make([]byte, 32) -	return []testCaseSerialize{ -		{ -			description: "test_cases_checksumv1: invalid: identifier: min", -			item: ChecksumV1{ -				Identifier: make([]byte, minIdentifier-1), -				Checksum:   checksum, -			}, -			wantErr: true, -		}, -		{ -			description: "test_cases_checksumv1: invalid: identifier: max", -			item: ChecksumV1{ -				Identifier: make([]byte, maxIdentifier+1), -				Checksum:   checksum, -			}, -			wantErr: true, -		}, -		{ -			description: "test_cases_checksumv1: invalid: checksum: min", -			item: ChecksumV1{ -				Identifier: identifier, -				Checksum:   make([]byte, minChecksum-1), -			}, -			wantErr: true, -		}, -		{ -			description: "test_cases_checksumv1: invalid: checksum: max", -			item: ChecksumV1{ -				Identifier: identifier, -				Checksum:   make([]byte, maxChecksum+1), -			}, -			wantErr: true, -		}, -		{ -			description: "test_cases_checksumv1: valid: testChecksumV1", -			item:        testChecksumV1, -			wantBytes:   testChecksumV1Bytes, -		}, -	} -} - -// test_cases_thv1 returns test cases for the TreeHeadV1 structure -func test_cases_thv1(t *testing.T) []testCaseSerialize { -	t.Helper() -	min, max := 0, 1<<16-1 // extensions min and max -	return []testCaseSerialize{ -		{ -			description: "test_cases_thv1: invalid: max", -			item: TreeHeadV1{ -				Timestamp: 0, -				TreeSize:  0, -				RootHash:  testNodeHash, -				Extension: make([]byte, max+1), -			}, -			wantErr: true, -		}, -		{ -			description: "test_cases_thv1: valid: min", -			item: TreeHeadV1{ -				Timestamp: 0, -				TreeSize:  0, -				RootHash:  testNodeHash, -				Extension: make([]byte, min), -			}, -		}, -		{ -			description: "test_cases_thv1: valid: max", -			item: TreeHeadV1{ -				Timestamp: 0, -				TreeSize:  0, -				RootHash:  testNodeHash, -				Extension: make([]byte, max), -			}, -		}, -		{ -			description: "test_cases_thv1: valid: testTreeHeadV1", -			item:        testTreeHeadV1, -			wantBytes:   testTreeHeadV1Bytes, -		}, // other invalid bounds are already tested in subtypes -	} -} - -// test_cases_nh returns test cases for the NodeHash structure -func test_cases_nh(t *testing.T) []testCaseSerialize { -	t.Helper() -	min, max := 32, 1<<8-1 // NodeHash min and max -	return []testCaseSerialize{ -		{ -			description: "test_cases_nh: invalid: min", -			item:        NodeHash{make([]byte, min-1)}, -			wantErr:     true, -		}, -		{ -			description: "test_cases_nh: invalid: max", -			item:        NodeHash{make([]byte, max+1)}, -			wantErr:     true, -		}, -		{ -			description: "test_cases_nh: valid: min", -			item:        NodeHash{make([]byte, min)}, -		}, -		{ -			description: "test_cases_nh: valid: max", -			item:        NodeHash{make([]byte, max)}, -		}, -		{ -			description: "test_cases_nh: valid: testNodeHash", -			item:        testNodeHash, -			wantBytes:   testNodeHashBytes, -		}, // other invalid bounds are already tested in subtypes -	} -} - -// test_cases_sigv1 returns test cases for the SignatureV1 structure -func test_cases_sigv1(t *testing.T) []testCaseSerialize { -	t.Helper() -	min, max := 1, 1<<16-1 // signature min and max -	return []testCaseSerialize{ -		{ -			description: "test_cases_sigv1: invalid: min", -			item: SignatureV1{ -				Namespace: testNamespace, -				Signature: make([]byte, min-1), -			}, -			wantErr: true, -		}, -		{ -			description: "test_cases_sigv1: invalid: max", -			item: SignatureV1{ -				Namespace: testNamespace, -				Signature: make([]byte, max+1), -			}, -			wantErr: true, -		}, -		{ -			description: "test_cases_sigv1: valid: min", -			item: SignatureV1{ -				Namespace: testNamespace, -				Signature: make([]byte, min), -			}, -		}, -		{ -			description: "test_cases_sigv1: valid: max", -			item: SignatureV1{ -				Namespace: testNamespace, -				Signature: make([]byte, max), -			}, -		}, -		{ -			description: "test_cases_sigV1: valid: testSignatureV1", -			item:        testSignatureV1, -			wantBytes:   testSignatureV1Bytes, -		}, -	} -} - -// test_cases_namespace returns test cases for the different Namespace types. -func test_cases_namespace(t *testing.T) []testCaseSerialize { -	return []testCaseSerialize{ -		{ -			description: "invalid: Namespace: reserved", -			item:        testNamespaceReserved, -			wantErr:     true, -		}, -		{ -			description: "valid: Namespace: ed25519_v1", -			item:        testNamespaceEd25519V1, -			wantBytes:   testNamespaceEd25519V1Bytes, -		}, -	} -} - -// test_cases_ed25519v1 returns test cases for the Ed25519V1 structure -func test_cases_ed25519v1(t *testing.T) []testCaseSerialize { -	return []testCaseSerialize{ -		{ -			description: "valid: testNamespaceEd25519V1", -			item:        testEd25519V1, -			wantBytes:   testEd25519V1Bytes, -		}, -	} -} - -// test_cases_requests returns test cases for proof request types -func test_cases_requests(t *testing.T) []testCaseSerialize { -	return []testCaseSerialize{ -		{ -			description: "valid: GetProofByHashV1", -			item: GetProofByHashV1{ -				Hash:     [HashSizeV1]byte{}, -				TreeSize: 16909060, -			}, -			wantBytes: bytes.Join([][]byte{ -				make([]byte, 32), // hash -				[]byte{0x00, 0x00, 0x00, 0x00, 0x1, 0x2, 0x3, 0x4}, // tree size -			}, nil), -		}, -		{ -			description: "valid: GetConsistencyProofV1", -			item: GetConsistencyProofV1{ -				First:  0, -				Second: 16909060, -			}, -			wantBytes: bytes.Join([][]byte{ -				make([]byte, 8), // first -				[]byte{0x00, 0x00, 0x00, 0x00, 0x1, 0x2, 0x3, 0x4}, // second -			}, nil), -		}, -		{ -			description: "valid: GetEntriesV1", -			item: GetEntriesV1{ -				Start: 0, -				End:   16909060, -			}, -			wantBytes: bytes.Join([][]byte{ -				make([]byte, 8), // start -				[]byte{0x00, 0x00, 0x00, 0x00, 0x1, 0x2, 0x3, 0x4}, // end -			}, nil), -		}, -	} -} - -var ( -	// StItemList -	testStItemList = StItemList{ -		Items: []StItem{ -			testStItemSignedChecksumV1, -			testStItemInclusionProofV1, -			testStItemCosignedTreeHeadV1, -		}, -	} -	testStItemListBytes = bytes.Join([][]byte{ -		func() []byte { -			sum := uint32(len(testStItemSignedChecksumV1Bytes)) -			sum += uint32(len(testStItemInclusionProofV1Bytes)) -			sum += uint32(len(testStItemCosignedTreeHeadV1Bytes)) -			buf := make([]byte, 4) -			binary.BigEndian.PutUint32(buf, sum) -			return buf -		}(), // length specifier list -		testStItemSignedChecksumV1Bytes,   // first StItem -		testStItemInclusionProofV1Bytes,   // second StItem -		testStItemCosignedTreeHeadV1Bytes, // third StItem -	}, nil) - -	// StItem -	testStItemReserved = StItem{ -		Format: StFormatReserved, -	} - -	testStItemSignedTreeHeadV1 = StItem{ -		Format:           StFormatSignedTreeHeadV1, -		SignedTreeHeadV1: &testSignedTreeHeadV1, -	} -	testStItemSignedTreeHeadV1Bytes = bytes.Join([][]byte{ -		[]byte{0x00, 0x01},        // format signed_tree_head_v1 -		testSignedTreeHeadV1Bytes, // SignedTreeHeadV1 -	}, nil) - -	testStItemCosignedTreeHeadV1 = StItem{ -		Format:             StFormatCosignedTreeHeadV1, -		CosignedTreeHeadV1: &testCosignedTreeHeadV1, -	} -	testStItemCosignedTreeHeadV1Bytes = bytes.Join([][]byte{ -		[]byte{0x00, 0x02},          // format cosigned_tree_head_v1 -		testCosignedTreeHeadV1Bytes, // CosignedTreeHeadV1, -	}, nil) - -	testStItemConsistencyProofV1 = StItem{ -		Format:             StFormatConsistencyProofV1, -		ConsistencyProofV1: &testConsistencyProofV1, -	} -	testStItemConsistencyProofV1Bytes = bytes.Join([][]byte{ -		[]byte{0x00, 0x03},          // format consistency_proof_v1 -		testConsistencyProofV1Bytes, // ConsistencyProofV1 -	}, nil) - -	testStItemInclusionProofV1 = StItem{ -		Format:           StFormatInclusionProofV1, -		InclusionProofV1: &testInclusionProofV1, -	} -	testStItemInclusionProofV1Bytes = bytes.Join([][]byte{ -		[]byte{0x00, 0x04},        // format inclusion_proof_v1 -		testInclusionProofV1Bytes, // InclusionProofV1 -	}, nil) - -	testStItemSignedChecksumV1 = StItem{ -		Format:           StFormatSignedChecksumV1, -		SignedChecksumV1: &testSignedChecksumV1, -	} -	testStItemSignedChecksumV1Bytes = bytes.Join([][]byte{ -		[]byte{0x00, 0x05},        // format signed_checksum_v1 -		testSignedChecksumV1Bytes, // SignedChecksumV1 -	}, nil) - -	// Subtypes used by StItem -	testSignedTreeHeadV1 = SignedTreeHeadV1{ -		TreeHead:  testTreeHeadV1, -		Signature: testSignatureV1, -	} -	testSignedTreeHeadV1Bytes = bytes.Join([][]byte{ -		testTreeHeadV1Bytes,  // tree head -		testSignatureV1Bytes, // signature -	}, nil) - -	testCosignedTreeHeadV1 = CosignedTreeHeadV1{ -		SignedTreeHead: testSignedTreeHeadV1, -		Cosignatures: []SignatureV1{ -			testSignatureV1, -		}, -	} -	testCosignedTreeHeadV1Bytes = bytes.Join([][]byte{ -		testSignedTreeHeadV1Bytes,                                 // signed tree head -		[]byte{0x00, 0x00, 0x00, byte(len(testSignatureV1Bytes))}, // cosignature length specifier -		testSignatureV1Bytes,                                      // the only cosignature in this list -	}, nil) - -	testConsistencyProofV1 = ConsistencyProofV1{ -		LogId:     testNamespace, -		TreeSize1: 16909060, -		TreeSize2: 16909060, -		ConsistencyPath: []NodeHash{ -			testNodeHash, -		}, -	} -	testConsistencyProofV1Bytes = bytes.Join([][]byte{ -		testNamespaceBytes, // log id -		[]byte{0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04}, // tree size 1 -		[]byte{0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04}, // tree size 2 -		[]byte{0x00, byte(len(testNodeHashBytes))},             // consistency path length specifier -		testNodeHashBytes, // the only node hash in this proof -	}, nil) - -	testInclusionProofV1 = InclusionProofV1{ -		LogId:     testNamespace, -		TreeSize:  16909060, -		LeafIndex: 16909060, -		InclusionPath: []NodeHash{ -			testNodeHash, -		}, -	} -	testInclusionProofV1Bytes = bytes.Join([][]byte{ -		testNamespaceBytes, // log id -		[]byte{0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04}, // tree size -		[]byte{0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04}, // leaf index -		[]byte{0x00, byte(len(testNodeHashBytes))},             // inclusion path length specifier -		testNodeHashBytes, // the only node hash in this proof -	}, nil) - -	testSignedChecksumV1 = SignedChecksumV1{ -		Data:      testChecksumV1, -		Signature: testSignatureV1, -	} -	testSignedChecksumV1Bytes = bytes.Join([][]byte{ -		testChecksumV1Bytes,  // data -		testSignatureV1Bytes, // signature -	}, nil) - -	// Additional subtypes -	testChecksumV1 = ChecksumV1{ -		Identifier: []byte("foobar-1-2-3"), -		Checksum:   make([]byte, 32), -	} -	testChecksumV1Bytes = bytes.Join([][]byte{ -		[]byte{12},             // identifier length specifier -		[]byte("foobar-1-2-3"), // identifier -		[]byte{32},             // checksum length specifier -		make([]byte, 32),       // checksum -	}, nil) - -	testTreeHeadV1 = TreeHeadV1{ -		Timestamp: 16909060, -		TreeSize:  16909060, -		RootHash:  testNodeHash, -		Extension: make([]byte, 0), -	} -	testTreeHeadV1Bytes = bytes.Join([][]byte{ -		[]byte{0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04}, // timestamp -		[]byte{0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04}, // tree size -		testNodeHashBytes,  // root hash -		[]byte{0x00, 0x00}, // extension length specifier -		// no extension -	}, nil) - -	testNodeHash = NodeHash{ -		Data: make([]byte, 32), -	} -	testNodeHashBytes = bytes.Join([][]byte{ -		[]byte{32}, // node hash length specifier -		make([]byte, 32), -	}, nil) - -	testSignatureV1 = SignatureV1{ -		Namespace: testNamespace, -		Signature: make([]byte, 64), -	} -	testSignatureV1Bytes = bytes.Join([][]byte{ -		testNamespaceBytes, // namespace field -		[]byte{0, 64},      // signature length specifier -		make([]byte, 64),   // signature -	}, nil) - -	// Namespace -	testNamespaceReserved = Namespace{ -		Format: NamespaceFormatReserved, -	} - -	testNamespace          = testNamespaceEd25519V1 -	testNamespaceBytes     = testNamespaceEd25519V1Bytes -	testNamespaceEd25519V1 = Namespace{ -		Format:    NamespaceFormatEd25519V1, -		Ed25519V1: &testEd25519V1, -	} -	testNamespaceEd25519V1Bytes = bytes.Join([][]byte{ -		[]byte{0x00, 0x01}, // format ed25519_v1 -		testEd25519V1Bytes, // Ed25519V1 -	}, nil) - -	// Subtypes used by Namespace -	testEd25519V1 = Ed25519V1{ -		Namespace: [32]byte{}, -	} -	testEd25519V1Bytes = bytes.Join([][]byte{ -		make([]byte, 32), // namespace, no length specifier because fixed size -	}, nil) -) diff --git a/types/stitem.go b/types/stitem.go deleted file mode 100644 index 447cad0..0000000 --- a/types/stitem.go +++ /dev/null @@ -1,192 +0,0 @@ -package types - -import ( -	"fmt" - -	"github.com/google/certificate-transparency-go/tls" -) - -// StFormat defines a particular StItem type that is versioned -type StFormat tls.Enum - -const ( -	StFormatReserved           StFormat = 0 -	StFormatSignedTreeHeadV1   StFormat = 1 -	StFormatCosignedTreeHeadV1 StFormat = 2 -	StFormatConsistencyProofV1 StFormat = 3 -	StFormatInclusionProofV1   StFormat = 4 -	StFormatSignedChecksumV1   StFormat = 5 -) - -// StItem references a versioned item based on a given format specifier -type StItem struct { -	Format             StFormat            `tls:"maxval:65535"` -	SignedTreeHeadV1   *SignedTreeHeadV1   `tls:"selector:Format,val:1"` -	CosignedTreeHeadV1 *CosignedTreeHeadV1 `tls:"selector:Format,val:2"` -	ConsistencyProofV1 *ConsistencyProofV1 `tls:"selector:Format,val:3"` -	InclusionProofV1   *InclusionProofV1   `tls:"selector:Format,val:4"` -	SignedChecksumV1   *SignedChecksumV1   `tls:"selector:Format,val:5"` -} - -type StItemList struct { -	Items []StItem `tls:"minlen:0,maxlen:4294967295"` -} - -type SignedTreeHeadV1 struct { -	TreeHead  TreeHeadV1 -	Signature SignatureV1 -} - -type CosignedTreeHeadV1 struct { -	SignedTreeHead SignedTreeHeadV1 -	Cosignatures   []SignatureV1 `tls:"minlen:0,maxlen:4294967295"` -} - -type ConsistencyProofV1 struct { -	LogId           Namespace -	TreeSize1       uint64 -	TreeSize2       uint64 -	ConsistencyPath []NodeHash `tls:"minlen:0,maxlen:65535"` -} - -type InclusionProofV1 struct { -	LogId         Namespace -	TreeSize      uint64 -	LeafIndex     uint64 -	InclusionPath []NodeHash `tls:"minlen:0,maxlen:65535"` -} - -type SignedChecksumV1 struct { -	Data      ChecksumV1 -	Signature SignatureV1 -} - -type ChecksumV1 struct { -	Identifier []byte `tls:"minlen:1,maxlen:128"` -	Checksum   []byte `tls:"minlen:1,maxlen:64"` -} - -type TreeHeadV1 struct { -	Timestamp uint64 -	TreeSize  uint64 -	RootHash  NodeHash -	Extension []byte `tls:"minlen:0,maxlen:65535"` -} - -type NodeHash struct { -	Data []byte `tls:"minlen:32,maxlen:255"` -} - -type SignatureV1 struct { -	Namespace Namespace -	Signature []byte `tls:"minlen:1,maxlen:65535"` -} - -func (f StFormat) String() string { -	switch f { -	case StFormatReserved: -		return "reserved" -	case StFormatSignedTreeHeadV1: -		return "signed_tree_head_v1" -	case StFormatCosignedTreeHeadV1: -		return "cosigned_tree_head_v1" -	case StFormatConsistencyProofV1: -		return "consistency_proof_v1" -	case StFormatInclusionProofV1: -		return "inclusion_proof_v1" -	case StFormatSignedChecksumV1: -		return "signed_checksum_v1" -	default: -		return fmt.Sprintf("unknown StFormat: %d", f) -	} -} - -func (i StItem) String() string { -	switch i.Format { -	case StFormatReserved: -		return fmt.Sprintf("Format(%s)", i.Format) -	case StFormatSignedTreeHeadV1: -		return fmt.Sprintf("Format(%s): %+v", i.Format, i.SignedTreeHeadV1) -	case StFormatCosignedTreeHeadV1: -		return fmt.Sprintf("Format(%s): %+v", i.Format, i.CosignedTreeHeadV1) -	case StFormatConsistencyProofV1: -		return fmt.Sprintf("Format(%s): %+v", i.Format, i.ConsistencyProofV1) -	case StFormatInclusionProofV1: -		return fmt.Sprintf("Format(%s): %+v", i.Format, i.InclusionProofV1) -	case StFormatSignedChecksumV1: -		return fmt.Sprintf("Format(%s): %+v", i.Format, i.SignedChecksumV1) -	default: -		return fmt.Sprintf("unknown StItem: %v", i.Format) -	} -} - -func NewSignedTreeHeadV1(th *TreeHeadV1, sig *SignatureV1) *StItem { -	return &StItem{ -		Format: StFormatSignedTreeHeadV1, -		SignedTreeHeadV1: &SignedTreeHeadV1{ -			TreeHead:  *th, -			Signature: *sig, -		}, -	} -} - -func NewCosignedTreeHeadV1(sth *SignedTreeHeadV1, cosig []SignatureV1) *StItem { -	if cosig == nil { -		cosig = make([]SignatureV1, 0) -	} -	return &StItem{ -		Format: StFormatCosignedTreeHeadV1, -		CosignedTreeHeadV1: &CosignedTreeHeadV1{ -			SignedTreeHead: *sth, -			Cosignatures:   cosig, -		}, -	} -} - -func NewConsistencyProofV1(id *Namespace, size1, size2 uint64, path []NodeHash) *StItem { -	return &StItem{ -		Format: StFormatConsistencyProofV1, -		ConsistencyProofV1: &ConsistencyProofV1{ -			LogId:           *id, -			TreeSize1:       size1, -			TreeSize2:       size2, -			ConsistencyPath: path, -		}, -	} -} - -func NewInclusionProofV1(id *Namespace, size, index uint64, path []NodeHash) *StItem { -	return &StItem{ -		Format: StFormatInclusionProofV1, -		InclusionProofV1: &InclusionProofV1{ -			LogId:         *id, -			TreeSize:      size, -			LeafIndex:     index, -			InclusionPath: path, -		}, -	} -} - -func NewSignedChecksumV1(data *ChecksumV1, sig *SignatureV1) *StItem { -	return &StItem{ -		Format: StFormatSignedChecksumV1, -		SignedChecksumV1: &SignedChecksumV1{ -			Data:      *data, -			Signature: *sig, -		}, -	} -} - -func NewTreeHeadV1(timestamp, size uint64, hash, extension []byte) *TreeHeadV1 { -	if extension == nil { -		extension = make([]byte, 0) -	} -	return &TreeHeadV1{ -		Timestamp: timestamp, -		TreeSize:  size, -		RootHash: NodeHash{ -			Data: hash, -		}, -		Extension: extension, -	} -} diff --git a/types/stitem_test.go b/types/stitem_test.go deleted file mode 100644 index 90d6808..0000000 --- a/types/stitem_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package types - -import ( -	"strings" -	"testing" -) - -// TestStItemString checks that the String() function prints the right format, -// and that the body is printed without a nil-pointer panic. -func TestStItemString(t *testing.T) { -	wantPrefix := map[StFormat]string{ -		StFormatReserved:           "Format(reserved)", -		StFormatSignedTreeHeadV1:   "Format(signed_tree_head_v1): &{TreeHead", -		StFormatCosignedTreeHeadV1: "Format(cosigned_tree_head_v1): &{SignedTreeHead", -		StFormatConsistencyProofV1: "Format(consistency_proof_v1): &{LogId", -		StFormatInclusionProofV1:   "Format(inclusion_proof_v1): &{LogId", -		StFormatSignedChecksumV1:   "Format(signed_checksum_v1): &{Data", -		StFormat(1<<16 - 1):        "unknown StItem: unknown StFormat: 65535", -	} -	tests := append(test_cases_stitem(t), testCaseSerialize{ -		description: "valid: unknown StItem", -		item: StItem{ -			Format: StFormat(1<<16 - 1), -		}, -	}) -	for _, table := range tests { -		item, ok := table.item.(StItem) -		if !ok { -			t.Fatalf("must cast to StItem in test %q", table.description) -		} - -		prefix, ok := wantPrefix[item.Format] -		if !ok { -			t.Fatalf("must have prefix for StFormat %v in test %q", item.Format, table.description) -		} -		if got, want := item.String(), prefix; !strings.HasPrefix(got, want) { -			t.Errorf("got %q but wanted prefix %q in test %q", got, want, table.description) -		} -	} -} - -// TODO: TestNewSignedTreeHeadV1 -func TestNewSignedTreeHeadV1(t *testing.T) { -} - -// TODO: TestNewCosignedTreeHeadV1 -func TestNewCosignedTreeHeadV1(t *testing.T) { -} - -// TODO: TestNewConsistencyProofV1 -func TestNewConsistencyProofV1(t *testing.T) { -} - -// TODO: TestNewInclusionProofV1 -func TestNewInclusionProofV1(t *testing.T) { -} - -// TODO: TestNewSignedChecksumV1 -func TestNewSignedChecksumV1(t *testing.T) { -} - -// TODO: TestNewTreeHeadV1 -func TestNewTreeHeadV1(t *testing.T) { -} diff --git a/util.go b/util.go deleted file mode 100644 index 847c3f7..0000000 --- a/util.go +++ /dev/null @@ -1,40 +0,0 @@ -package stfe - -import ( -	"fmt" - -	"github.com/google/trillian" -	ttypes "github.com/google/trillian/types" -	"github.com/system-transparency/stfe/types" -) - -func NewTreeHeadV1FromLogRoot(lr *ttypes.LogRootV1) *types.TreeHeadV1 { -	return &types.TreeHeadV1{ -		Timestamp: uint64(lr.TimestampNanos / 1000 / 1000), -		TreeSize:  uint64(lr.TreeSize), -		RootHash: types.NodeHash{ -			Data: lr.RootHash, -		}, -		Extension: make([]byte, 0), -	} -} - -func NewNodePathFromHashPath(hashes [][]byte) []types.NodeHash { -	path := make([]types.NodeHash, 0, len(hashes)) -	for _, hash := range hashes { -		path = append(path, types.NodeHash{hash}) -	} -	return path -} - -func NewStItemListFromLeaves(leaves []*trillian.LogLeaf) (*types.StItemList, error) { -	items := make([]types.StItem, 0, len(leaves)) -	for _, leaf := range leaves { -		var item types.StItem -		if err := types.Unmarshal(leaf.LeafValue, &item); err != nil { -			return nil, fmt.Errorf("Unmarshal failed: %v", err) -		} -		items = append(items, item) -	} -	return &types.StItemList{items}, nil -} diff --git a/util_test.go b/util_test.go deleted file mode 100644 index b40a672..0000000 --- a/util_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package stfe - -import ( -	"testing" -) - -// TODO: TestNewTreeHeadV1FromLogRoot -func TestNewTreeHeadV1FromLogRoot(t *testing.T) { -} - -// TODO: TestNewNodePathFromHashPath -func TestNewNodePathFromHashPath(t *testing.T) { -} - -// TODO: TestStItemListFromLeaves -func TestStItemListFromLeaves(t *testing.T) { -} | 
