diff options
author | Rasmus Dahlberg <rasmus.dahlberg@kau.se> | 2021-06-07 00:19:40 +0200 |
---|---|---|
committer | Rasmus Dahlberg <rasmus.dahlberg@kau.se> | 2021-06-07 00:19:40 +0200 |
commit | 932d29fd08c8ff401e471b4f764537493ccbd483 (patch) | |
tree | e840a4c62db92e84201fe9ceaa0594d99176792c | |
parent | bdf7a53d61cf044e526cc9123ca296615f838288 (diff) | |
parent | 345fe658fa8a4306caa74f72a618e499343675c2 (diff) |
Merge branch 'design' into main
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) { -} |