diff options
-rw-r--r-- | types/namespace.go | 56 | ||||
-rw-r--r-- | types/namespace_pool.go | 61 | ||||
-rw-r--r-- | types/namespace_pool_test.go | 91 | ||||
-rw-r--r-- | types/namespace_test.go | 198 | ||||
-rw-r--r-- | types/serialize.go | 28 | ||||
-rw-r--r-- | types/serialize_test.go (renamed from types/item_test.go) | 502 | ||||
-rw-r--r-- | types/stitem.go (renamed from types/item.go) | 22 | ||||
-rw-r--r-- | types/stitem_test.go | 40 |
8 files changed, 509 insertions, 489 deletions
diff --git a/types/namespace.go b/types/namespace.go index f221960..3c6b90a 100644 --- a/types/namespace.go +++ b/types/namespace.go @@ -89,59 +89,3 @@ func (ns *Namespace) Verify(message, signature []byte) error { } 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_pool.go b/types/namespace_pool.go new file mode 100644 index 0000000..0f30567 --- /dev/null +++ b/types/namespace_pool.go @@ -0,0 +1,61 @@ +package types + +import ( + "fmt" +) + +// 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_pool_test.go b/types/namespace_pool_test.go new file mode 100644 index 0000000..f5810a2 --- /dev/null +++ b/types/namespace_pool_test.go @@ -0,0 +1,91 @@ +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 index e7e89ad..cd151d8 100644 --- a/types/namespace_test.go +++ b/types/namespace_test.go @@ -2,80 +2,21 @@ 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) - } - } -} - +// 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), testCaseType{ + tests := append(test_cases_namespace(t), testCaseSerialize{ description: "valid: unknown Namespace", item: Namespace{ Format: NamespaceFormat(1<<16 - 1), @@ -135,128 +76,49 @@ func TestFingerprint(t *testing.T) { } } -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} +func TestNewNamespaceEd25519V1(t *testing.T) { + size := 32 // verification key size for _, table := range []struct { description string - namespaces []*Namespace + vk []byte wantErr bool }{ { - description: "invalid: duplicate namespace", - namespaces: []*Namespace{ns1, ns1, ns2}, - wantErr: true, - }, - { - description: "invalid: namespace without key", - namespaces: []*Namespace{ns1, nsr, ns2}, + description: "invalid", + vk: make([]byte, size+1), wantErr: true, }, { - description: "valid: empty", - namespaces: []*Namespace{}, - }, - { - description: "valid: one namespace", - namespaces: []*Namespace{ns1}, - }, - { - description: "valid: two namespaces", - namespaces: []*Namespace{ns1, ns2}, + description: "valid", + vk: make([]byte, size), }, } { - _, err := NewNamespacePool(table.namespaces) + 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 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, - }, +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) + } } } diff --git a/types/serialize.go b/types/serialize.go new file mode 100644 index 0000000..da4bd9f --- /dev/null +++ b/types/serialize.go @@ -0,0 +1,28 @@ +package types + +import ( + "fmt" + + "github.com/google/certificate-transparency-go/tls" +) + +// 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/item_test.go b/types/serialize_test.go index 6788f51..e835ad2 100644 --- a/types/item_test.go +++ b/types/serialize_test.go @@ -2,14 +2,13 @@ package types import ( "bytes" - "strings" "testing" "encoding/binary" ) -// testCaseType is a common test case used for ST log types -type testCaseType struct { +// testCaseSerialize is a common test case used for ST log types +type testCaseSerialize struct { description string item interface{} wantErr bool @@ -20,7 +19,7 @@ type testCaseType struct { // 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 + var tests []testCaseSerialize tests = append(tests, test_cases_stitemlist(t)...) tests = append(tests, test_cases_stitem(t)...) tests = append(tests, test_cases_sthv1(t)...) @@ -121,231 +120,16 @@ func TestUnmarshalStItem(t *testing.T) { } } -// 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 ( - // 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) -) - // test_cases_stitemlist returns test cases for the StItemList type -func test_cases_stitemlist(t *testing.T) []testCaseType { +func test_cases_stitemlist(t *testing.T) []testCaseSerialize { t.Helper() - return []testCaseType{ - testCaseType{ + 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 - testCaseType{ + testCaseSerialize{ description: "test_cases_stitemlist: valid: mixed content", item: testStItemList, wantBytes: testStItemListBytes, @@ -354,9 +138,9 @@ func test_cases_stitemlist(t *testing.T) []testCaseType { } // test_cases_stitem returns test cases for the different StItem types -func test_cases_stitem(t *testing.T) []testCaseType { +func test_cases_stitem(t *testing.T) []testCaseSerialize { t.Helper() - return []testCaseType{ + return []testCaseSerialize{ { description: "invalid: StItem: reserved", item: testStItemReserved, @@ -391,9 +175,9 @@ func test_cases_stitem(t *testing.T) []testCaseType { } // test_cases_sthv1 returns test cases for the SignedTreeHeadV1 structure -func test_cases_sthv1(t *testing.T) []testCaseType { +func test_cases_sthv1(t *testing.T) []testCaseSerialize { t.Helper() - return []testCaseType{ + return []testCaseSerialize{ { description: "valid: testSignedTreeHeadV1", item: testSignedTreeHeadV1, @@ -403,9 +187,9 @@ func test_cases_sthv1(t *testing.T) []testCaseType { } // test_cases_costhv1 returns test cases for the CosignedTreeHeadV1 structure -func test_cases_costhv1(t *testing.T) []testCaseType { +func test_cases_costhv1(t *testing.T) []testCaseSerialize { t.Helper() - return []testCaseType{ + return []testCaseSerialize{ { description: "test_cases_costhv1: valid: min", item: CosignedTreeHeadV1{ @@ -422,10 +206,10 @@ func test_cases_costhv1(t *testing.T) []testCaseType { } // test_cases_cpv1 returns test cases for the ConsistencyProofV1 structure -func test_cases_cpv1(t *testing.T) []testCaseType { +func test_cases_cpv1(t *testing.T) []testCaseSerialize { t.Helper() max := 65535 // max consistency proof - return []testCaseType{ + return []testCaseSerialize{ { description: "test_cases_cpv1: invalid: >max", item: ConsistencyProofV1{ @@ -460,10 +244,10 @@ func test_cases_cpv1(t *testing.T) []testCaseType { } // test_cases_ipv1 returns test cases for the InclusionProofV1 structure -func test_cases_ipv1(t *testing.T) []testCaseType { +func test_cases_ipv1(t *testing.T) []testCaseSerialize { t.Helper() max := 65535 // max inclusion proof - return []testCaseType{ + return []testCaseSerialize{ { description: "test_cases_ipv1: invalid: >max", item: InclusionProofV1{ @@ -498,9 +282,9 @@ func test_cases_ipv1(t *testing.T) []testCaseType { } // test_cases_signed_checksumv1 returns test cases for the SignedChecksumV1 structure -func test_cases_signed_checksumv1(t *testing.T) []testCaseType { +func test_cases_signed_checksumv1(t *testing.T) []testCaseSerialize { t.Helper() - return []testCaseType{ + return []testCaseSerialize{ { description: "test_cases_signed_checksumv1: valid: testSignedChecksumV1", item: testSignedChecksumV1, @@ -510,11 +294,11 @@ func test_cases_signed_checksumv1(t *testing.T) []testCaseType { } // test_cases_checksumv1 returns test cases for the ChecksumV1 structure -func test_cases_checksumv1(t *testing.T) []testCaseType { +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 []testCaseType{ + return []testCaseSerialize{ { description: "test_cases_checksumv1: invalid: identifier: min", item: ChecksumV1{ @@ -556,10 +340,10 @@ func test_cases_checksumv1(t *testing.T) []testCaseType { } // test_cases_thv1 returns test cases for the TreeHeadV1 structure -func test_cases_thv1(t *testing.T) []testCaseType { +func test_cases_thv1(t *testing.T) []testCaseSerialize { t.Helper() min, max := 0, 1<<16-1 // extensions min and max - return []testCaseType{ + return []testCaseSerialize{ { description: "test_cases_thv1: invalid: max", item: TreeHeadV1{ @@ -597,10 +381,10 @@ func test_cases_thv1(t *testing.T) []testCaseType { } // test_cases_nh returns test cases for the NodeHash structure -func test_cases_nh(t *testing.T) []testCaseType { +func test_cases_nh(t *testing.T) []testCaseSerialize { t.Helper() min, max := 32, 1<<8-1 // NodeHash min and max - return []testCaseType{ + return []testCaseSerialize{ { description: "test_cases_nh: invalid: min", item: NodeHash{make([]byte, min-1)}, @@ -628,10 +412,10 @@ func test_cases_nh(t *testing.T) []testCaseType { } // test_cases_sigv1 returns test cases for the SignatureV1 structure -func test_cases_sigv1(t *testing.T) []testCaseType { +func test_cases_sigv1(t *testing.T) []testCaseSerialize { t.Helper() min, max := 1, 1<<16-1 // signature min and max - return []testCaseType{ + return []testCaseSerialize{ { description: "test_cases_sigv1: invalid: min", item: SignatureV1{ @@ -669,3 +453,235 @@ func test_cases_sigv1(t *testing.T) []testCaseType { }, } } + +// 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, + }, + } +} + +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/item.go b/types/stitem.go index 8596ce4..b214082 100644 --- a/types/item.go +++ b/types/stitem.go @@ -28,7 +28,6 @@ type StItem struct { SignedChecksumV1 *SignedChecksumV1 `tls:"selector:Format,val:5"` } -// StItemList is an StItem List that is at most 2^32-1 bytes when serialized. type StItemList struct { Items []StItem `tls:"minlen:0,maxlen:4294967295"` } @@ -120,24 +119,3 @@ func (i StItem) String() string { 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/stitem_test.go b/types/stitem_test.go new file mode 100644 index 0000000..c6e413a --- /dev/null +++ b/types/stitem_test.go @@ -0,0 +1,40 @@ +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) + } + } +} |