diff options
Diffstat (limited to 'types')
-rw-r--r-- | types/item.go | 140 | ||||
-rw-r--r-- | types/item_test.go | 626 | ||||
-rw-r--r-- | types/namespace.go | 135 | ||||
-rw-r--r-- | types/namespace_test.go | 301 |
4 files changed, 1202 insertions, 0 deletions
diff --git a/types/item.go b/types/item.go new file mode 100644 index 0000000..5e8836e --- /dev/null +++ b/types/item.go @@ -0,0 +1,140 @@ +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"` +} + +// TODO: StItemList + +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) + } +} + +// Marshal marshals a TLS-encodable item +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 item +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/item_test.go b/types/item_test.go new file mode 100644 index 0000000..01c3376 --- /dev/null +++ b/types/item_test.go @@ -0,0 +1,626 @@ +package types + +import ( + "bytes" + "strings" + "testing" +) + +// testCaseType is a common test case used for ST log types +type testCaseType 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 []testCaseType + 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)...) + 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 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) + 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) + } + } +} + +// TestStItemString checks that the String() function prints the right format, +// and that following body is printed in a verbose mode without a nil-ptr 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), testCaseType{ + 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) + } + } +} + +var ( + // 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) +) + +// test_cases_stitem returns test cases for the different StItem types +func test_cases_stitem(t *testing.T) []testCaseType { + t.Helper() + return []testCaseType{ + { + 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) []testCaseType { + t.Helper() + return []testCaseType{ + { + 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) []testCaseType { + t.Helper() + return []testCaseType{ + { + 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) []testCaseType { + t.Helper() + max := 65535 // max consistency proof + return []testCaseType{ + { + 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) []testCaseType { + t.Helper() + max := 65535 // max inclusion proof + return []testCaseType{ + { + 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) []testCaseType { + t.Helper() + return []testCaseType{ + { + 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) []testCaseType { + t.Helper() + minIdentifier, maxIdentifier, identifier := 1, 128, []byte("foobar-1-2-3") + minChecksum, maxChecksum, checksum := 1, 64, make([]byte, 32) + return []testCaseType{ + { + 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) []testCaseType { + t.Helper() + min, max := 0, 1<<16-1 // extensions min and max + return []testCaseType{ + { + 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) []testCaseType { + t.Helper() + min, max := 32, 1<<8-1 // NodeHash min and max + return []testCaseType{ + { + 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) []testCaseType { + t.Helper() + min, max := 1, 1<<16-1 // signature min and max + return []testCaseType{ + { + 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, + }, + } +} diff --git a/types/namespace.go b/types/namespace.go new file mode 100644 index 0000000..8a2ad17 --- /dev/null +++ b/types/namespace.go @@ -0,0 +1,135 @@ +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 +) + +// 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) + } +} + +// 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 +} + +// 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 +} + +// NamespacePool is a pool of namespaces that contain complete verification keys +type NamespacePool struct { + pool map[string]*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[string]*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) + } + if _, ok := np.pool[namespace.String()]; ok { + return nil, fmt.Errorf("duplicate namespace: %v", namespace.String()) + } + np.pool[namespace.String()] = 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) { + if _, ok := np.pool[namespace.String()]; !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_test.go b/types/namespace_test.go new file mode 100644 index 0000000..da18b13 --- /dev/null +++ b/types/namespace_test.go @@ -0,0 +1,301 @@ +package types + +import ( + "bytes" + "reflect" + "strings" + "testing" + + "crypto/ed25519" +) + +var ( + // 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) +) + +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) + } + } +} + +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), testCaseType{ + 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 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 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") + } +} + +// test_cases_namespace returns test cases for the different Namespace types. +// It is used by TestMarshalUnmarshal(), see test_item.go. +func test_cases_namespace(t *testing.T) []testCaseType { + return []testCaseType{ + { + 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 +// It is used by TestMarshalUnmarshal(), see test_item.go. +func test_cases_ed25519v1(t *testing.T) []testCaseType { + return []testCaseType{ + { + description: "valid: testNamespaceEd25519V1", + item: testEd25519V1, + wantBytes: testEd25519V1Bytes, + }, + } +} + +// 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 +} |