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 /pkg/types | |
| parent | bdf7a53d61cf044e526cc9123ca296615f838288 (diff) | |
| parent | 345fe658fa8a4306caa74f72a618e499343675c2 (diff) | |
Merge branch 'design' into main
Diffstat (limited to 'pkg/types')
| -rw-r--r-- | pkg/types/ascii.go | 421 | ||||
| -rw-r--r-- | pkg/types/ascii_test.go | 465 | ||||
| -rw-r--r-- | pkg/types/trunnel.go | 60 | ||||
| -rw-r--r-- | pkg/types/trunnel_test.go | 114 | ||||
| -rw-r--r-- | pkg/types/types.go | 155 | ||||
| -rw-r--r-- | pkg/types/types_test.go | 58 | ||||
| -rw-r--r-- | pkg/types/util.go | 21 | 
7 files changed, 1294 insertions, 0 deletions
| 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...)) +} | 
