From 401b8dd61eebc05e937a7f665b5677be18d86ba6 Mon Sep 17 00:00:00 2001 From: David Drysdale Date: Tue, 15 Nov 2016 08:25:41 +0000 Subject: [PATCH 1/5] go/tls: test empty slice of sized data Some TLS types have size fields like: opaque nonEmptyType<2..4> nonEmptyType values<0..8> where the outer holder can explicitly be empty, but the element type is always >0 in size. Add a test case to check that this is processed correctly. --- tls/tls_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tls/tls_test.go b/tls/tls_test.go index b18e2b03a2..ad72bb4baa 100644 --- a/tls/tls_test.go +++ b/tls/tls_test.go @@ -42,6 +42,11 @@ type testNonByteSlice struct { Vals []uint16 `tls:"minlen:2,maxlen:6"` } +type testNoSlices struct { + // Each entry is size >= 2, but there can be zero entries. + Entries []testNonByteSlice `tls:"minlen:0,maxlen:12"` +} + type testSliceOfStructs struct { Vals []testVariant `tls:"minlen:0,maxlen:100"` } @@ -195,6 +200,7 @@ func TestUnmarshalMarshalWithParamsRoundTrip(t *testing.T) { }, }, }, + {"00", "", &testNoSlices{Entries: []testNonByteSlice{}}}, {"011011", "", &testAliasEnum{Val: 1, Val16: newUint16(0x1011)}}, {"0403", "", &SignatureAndHashAlgorithm{Hash: SHA256, Signature: ECDSA}}, {"04030003010203", "", @@ -282,6 +288,7 @@ func TestUnmarshalWithParamsFailures(t *testing.T) { {"0102", "", &testMissingSelector{Val: newUint16(1)}, "selector not seen"}, {"000007", "", &testChoiceNotPointer{Which: 0, Val: 7}, "choice field not a pointer type"}, {"05010102020303", "", &testNonByteSlice{Vals: []uint16{0x101, 0x202, 0x303}}, "truncated"}, + {"0100", "", &testNoSlices{}, "value 0 too small"}, {"0101", "size:2", newNonEnumAlias(0x0102), "unsupported type"}, {"0403010203", "", &DigitallySigned{ From 0169dde810f55389b808f48c3e24ff1d30d940f2 Mon Sep 17 00:00:00 2001 From: David Drysdale Date: Mon, 31 Oct 2016 12:08:25 +0000 Subject: [PATCH 2/5] go/asn1: don't check element type in []RawValue To allow for SET OF ANY / SEQUENCE OF ANY, don't check the element type tags for []asn1.RawValue. --- asn1/asn1.go | 10 ++++++---- asn1/asn1_test.go | 12 ++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/asn1/asn1.go b/asn1/asn1.go index ac2edae95c..04a2ee8ef4 100644 --- a/asn1/asn1.go +++ b/asn1/asn1.go @@ -556,10 +556,12 @@ func parseSequenceOf(bytes []byte, sliceType reflect.Type, elemType reflect.Type // Likewise, both time types are treated the same. t.tag = TagUTCTime } - - if t.class != ClassUniversal || t.isCompound != compoundType || t.tag != expectedTag { - err = StructuralError{"sequence tag mismatch"} - return + // Don't check the element type if this is []asn1.RawValue, to allow for SET OF ANY / SEQUENCE OF ANY. + if elemType != rawValueType { + if t.class != ClassUniversal || t.isCompound != compoundType || t.tag != expectedTag { + err = StructuralError{fmt.Sprintf("sequence tag mismatch (0/%d/%t vs %+v)", expectedTag, compoundType, t)} + return + } } if invalidLength(offset, t.length, len(bytes)) { err = SyntaxError{"truncated sequence"} diff --git a/asn1/asn1_test.go b/asn1/asn1_test.go index 4b6da22798..819b5d40e6 100644 --- a/asn1/asn1_test.go +++ b/asn1/asn1_test.go @@ -467,6 +467,11 @@ type TestSet struct { Ints []int `asn1:"set"` } +type TestSetOfAny struct { + Values anySET +} +type anySET []RawValue + var unmarshalTestData = []struct { in []byte out interface{} @@ -488,6 +493,13 @@ var unmarshalTestData = []struct { {[]byte{0x30, 0x0b, 0x13, 0x03, 0x66, 0x6f, 0x6f, 0x02, 0x01, 0x22, 0x02, 0x01, 0x33}, &TestElementsAfterString{"foo", 0x22, 0x33}}, {[]byte{0x30, 0x05, 0x02, 0x03, 0x12, 0x34, 0x56}, &TestBigInt{big.NewInt(0x123456)}}, {[]byte{0x30, 0x0b, 0x31, 0x09, 0x02, 0x01, 0x01, 0x02, 0x01, 0x02, 0x02, 0x01, 0x03}, &TestSet{Ints: []int{1, 2, 3}}}, + {[]byte{0x30, 0x05, 0x31, 0x03, 0x02, 0x01, 0x42}, + &TestSetOfAny{ + Values: []RawValue{ + RawValue{Class: 0, Tag: 2, Bytes: []byte{0x42}, FullBytes: []byte{0x02, 0x01, 0x42}}, + }, + }, + }, } func TestUnmarshal(t *testing.T) { From 5a40209b77923169fd866c55383e1879f3e63087 Mon Sep 17 00:00:00 2001 From: David Drysdale Date: Tue, 18 Oct 2016 13:52:47 +0100 Subject: [PATCH 3/5] go/clientv2: Go types for RFC6962-bis - Go types for all TLS types - Go types for all JSON request/response structures - JSON encoding methods for TransItem type --- types.go | 391 +++++++++++++++++++++++++++++++++++++++++++++++++- types_test.go | 383 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 772 insertions(+), 2 deletions(-) diff --git a/types.go b/types.go index 2e858cc463..c909bd0a87 100644 --- a/types.go +++ b/types.go @@ -4,8 +4,10 @@ import ( "crypto/sha256" "encoding/base64" "encoding/json" + "errors" "fmt" + "github.com/google/certificate-transparency/go/asn1" "github.com/google/certificate-transparency/go/tls" "github.com/google/certificate-transparency/go/x509" ) @@ -94,8 +96,10 @@ func (st SignatureType) String() string { } } -// ASN1Cert type for holding the raw DER bytes of an ASN.1 Certificate -// (section 3.1). +// ASN1Cert holds an ASN.1 DER-encoded X.509 certificate; it represents the +// ASN.1Cert TLS type from section 3.1; the same type is also described in +// RFC6962-bis in section 5.2. (The struct wrapper is needed so that +// Data becomes a field and can have a field tag.) type ASN1Cert struct { Data []byte `tls:"minlen:1,maxlen:16777215"` } @@ -419,3 +423,386 @@ type GetEntryAndProofResponse struct { ExtraData []byte `json:"extra_data"` // any chain provided when the entry was added to the log AuditPath [][]byte `json:"audit_path"` // the corresponding proof } + +/////////////////////////////////////////////////////////////////////////////// +// The following structures are for Certificate Transparency V2. +// This is based on draft-ietf-trans-rfc6962-bis-19.txt; below here, any +// references to a section number on its own refer to this document. +/////////////////////////////////////////////////////////////////////////////// + +// The first section holds TLS types needed for Certificate Transparency V2. + +// X509ChainEntry holds a leaf certificate together with a chain of 0 or more +// entries that are needed to verify the leaf. Each entry in the chain +// verifies the preceding entry, and the first entry in the chain verifies the +// leaf. This represents the X509ChainEntry TLS type from section 5.2. +// (The same type is also described in section 3.1 of RFC 6962 but is not +// directly used there.) +type X509ChainEntry struct { + LeafCertificate ASN1Cert `tls:"minlen:1,maxlen:16777215"` + CertificateChain []ASN1Cert `tls:"minlen:0,maxlen:16777215"` +} + +// CMSPrecert holds the ASN.1 DER encoding of a CMS-encoded pre-certificate, +// where the CMS encoding is described in section 3.2. This represents the +// CMSPrecert TLS type from section 5.2. +type CMSPrecert []byte // tls:"minlen:1,maxlen:16777215" + +// PrecertChainEntryV2 holds a pre-certificate together with a chain of 0 or +// more entries that are needed to verify it. Each entry in the chain +// verifies the preceding entry, and the first entry in the chain verifies the +// pre-certificate. This represents the PrecertChainEntryV2 TLS type from +// section 5.2. +type PrecertChainEntryV2 struct { + PreCertificate CMSPrecert `tls:"minlen:1,maxlen:16777215"` + PrecertificateChain []ASN1Cert `tls:"minlen:1,maxlen:16777215"` +} + +// LogIDV2 identifies a particular Log, as the contents of an ASN.1 DER-encoded +// OBJECT IDENTIFIER. +// +// This OID is required to be less than 127 bytes, which means the TLS and +// ASN.1 encodings are compatible by adding a prefix byte 0x06: +// TLS encoding: 1-byte length, plus L bytes of DER-encoded OID. +// DER encoding: 1-byte 0x06 (universal/primitive/OBJECT IDENTIFIER), then +// 1-byte length, plus L bytes of DER-encoded OID. +// +// This represents the LogID TLS type from section 5.3; it has the ..V2 suffix +// to distinguish it from the RFC 6962 LogID type. +type LogIDV2 []byte // tls:"minlen:2,maxlen:127" + +// LogIDV2FromOID creates a LogIDV2 object from an asn1.ObjectIdentifier. +func LogIDV2FromOID(oid asn1.ObjectIdentifier) (LogIDV2, error) { + der, err := asn1.Marshal(oid) + if err != nil { + return nil, err + } + // Unmarshal back again so we can extract the gooey centre. + var val asn1.RawValue + if _, err = asn1.Unmarshal(der, &val); err != nil { + return nil, err + } + data := val.Bytes + if len(data) > 127 { + return nil, fmt.Errorf("ObjectIdentifier %v too long for LogIDV2", oid) + } + return data, nil +} + +// OIDFromLogIDV2 returns the OID associated with a LogIDV2. +func OIDFromLogIDV2(logID LogIDV2) (asn1.ObjectIdentifier, error) { + if len(logID) > 127 { + return nil, fmt.Errorf("log ID too long") + } + der := make([]byte, len(logID)+2) + der[0] = asn1.TagOID // and asn1.ClassUniversal + der[1] = byte(len(logID)) + copy(der[2:], logID) + var oid asn1.ObjectIdentifier + if _, err := asn1.Unmarshal(der, &oid); err != nil { + return nil, fmt.Errorf("malformed LogIDV2: %q", err.Error()) + } + return oid, nil +} + +// VersionedTransType indicates the variant content of a TransItem; it +// represents the VersionedTransType TLS enum from section 5.4. +type VersionedTransType tls.Enum // tls:"maxval:65535" + +// VersionedTransType constants from section 5.4. +const ( + X509EntryV2 VersionedTransType = 1 + PrecertEntryV2 VersionedTransType = 2 + X509SCTV2 VersionedTransType = 3 + PrecertSCTV2 VersionedTransType = 4 + SignedTreeHeadV2 VersionedTransType = 5 + ConsistencyProofV2 VersionedTransType = 6 + InclusionProofV2 VersionedTransType = 7 + X509SCTWithProofV2 VersionedTransType = 8 + PrecertSCTWithProofV2 VersionedTransType = 9 +) + +// TransItem encapsulates various pieces of CT information; it represents the +// TransItem TLS type from section 5.4. +type TransItem struct { + VersionedType VersionedTransType `tls:"maxval:65535"` + X509EntryV2Data *TimestampedCertificateEntryDataV2 `tls:"selector:VersionedType,val:1"` + PrecertEntryV2Data *TimestampedCertificateEntryDataV2 `tls:"selector:VersionedType,val:2"` + X509SCTV2Data *SignedCertificateTimestampDataV2 `tls:"selector:VersionedType,val:3"` + PrecertSCTV2Data *SignedCertificateTimestampDataV2 `tls:"selector:VersionedType,val:4"` + SignedTreeHeadV2Data *SignedTreeHeadDataV2 `tls:"selector:VersionedType,val:5"` + ConsistencyProofV2Data *ConsistencyProofDataV2 `tls:"selector:VersionedType,val:6"` + InclusionProofV2Data *InclusionProofDataV2 `tls:"selector:VersionedType,val:7"` + X509SCTWithProofV2Data *SCTWithProofDataV2 `tls:"selector:VersionedType,val:8"` + PrecertSCTWithProofV2Data *SCTWithProofDataV2 `tls:"selector:VersionedType,val:9"` +} + +// MarshalJSON implements the json.Marshaller interface, so that fields of type TransItem +// are JSON encoded as base64(TLS-encode(contents)). +func (item TransItem) MarshalJSON() ([]byte, error) { + data, err := tls.Marshal(item) + if err != nil { + return []byte{}, err + } + data64 := base64.StdEncoding.EncodeToString(data) + return []byte(`"` + data64 + `"`), nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface, so that fields of type TransItem +// can be decoded from JSON values that hold base64(TLS-encode(contents)). +func (item *TransItem) UnmarshalJSON(b []byte) error { + var data64 string + if err := json.Unmarshal(b, &data64); err != nil { + return fmt.Errorf("failed to json.Unmarshal TransItem: %v", err) + } + data, err := base64.StdEncoding.DecodeString(data64) + if err != nil { + return fmt.Errorf("failed to unbase64 TransItem: %v", err) + } + rest, err := tls.Unmarshal(data, item) + if err != nil { + return fmt.Errorf("failed to tls.Unmarshal TransItem: %v", err) + } else if len(rest) > 0 { + return errors.New("trailing data in TransItem") + } + return nil +} + +// TBSCertificate holds an ASN.1 DER-encoded TBSCertificate, as defined in RFC +// 5280 section 4.1. It represents the TBSCertificate TLS type from section +// 5.5. +type TBSCertificate []byte // tls:"minlen:1,maxlen:16777215" + +// TimestampedCertificateEntryDataV2 describes a Log entry; it represents the +// TimestampedCertificateEntryDataV2 TLS type from section 5.5. +type TimestampedCertificateEntryDataV2 struct { + Timestamp uint64 + IssuerKeyHash []byte `tls:"minlen:32,maxlen:255"` + TBSCertificate TBSCertificate `tls:"minlen:1,maxlen:16777215"` + // Entries in the following MUST be ordered in increasing order + // according to their SCTExtensionType values. + SCTExtensions []SCTExtension `tls:"minlen:0,maxlen:65535"` +} + +// SCTExtensionType indicates the type of extension data associated with an +// SCT; it represents the SctExtensionType enum from section 5.6. +type SCTExtensionType tls.Enum // tls:"maxval:65535" + +// SCTExtension provides extended information about an SCT; it represents the +// SCTExtension TLS type from section 5.6. +type SCTExtension struct { + SCTExtensionType SCTExtensionType `tls:"maxval:65535"` + SCTExtensionData []byte `tls:"minlen:0,maxlen:65535"` +} + +// SignedCertificateTimestampDataV2 holds an SCT generated by the Log. This +// represents the SignedCertificateTimestampDataV2 TLS type from section 5.6. +type SignedCertificateTimestampDataV2 struct { + LogID LogIDV2 `tls:"minlen:2,maxlen:127"` + Timestamp uint64 + // Entries in the following MUST be ordered in increasing order + // according to their SCTExtensionType values. + SCTExtensions []SCTExtension `tls:"minlen:0,maxlen:65535"` + // The following signature is over a TransItem that MUST have + // VersionedType of X509EntryV2 or PrecertEntryV2. + Signature tls.DigitallySigned +} + +// NodeHash holds a hash value generated by the Log; it represents the +// NodeHash TLS type from section 5.7. (The struct wrapper is needed so +// that Value becomes a field and can have a field tag.) +type NodeHash struct { + Value []byte `tls:"minval:32,maxval:255"` +} + +// STHExtensionType indicates the type of extension data associated with an +// STH; it represents the SthExtensionType enum from section 5.7. +type STHExtensionType tls.Enum // tls:"maxval:65535" + +// STHExtension provides extended information about an STH; it represents the +// STHExtension TLS type from section 5.6. +type STHExtension struct { + STHExtensionType STHExtensionType `tls:"maxval:65535"` + STHExtensionData []byte `tls:"minlen:0,maxlen:65535"` +} + +// TreeHeadDataV2 holds information about a Log's Merkle tree head; it +// represents the TreeHeadDataV2 TLS type from section 5.7. +type TreeHeadDataV2 struct { + Timestamp uint64 + TreeSize uint64 + RootHash NodeHash `tls:"minval:32,maxval:255"` + // Entries in the following MUST be ordered in increasing order + // according to their STHExtensionType values. + STHExtensions []STHExtension `tls:"minlen:0,maxlen:65535"` +} + +// SignedTreeHeadDataV2 gives signed information about a Log's Merkle tree +// head; it represents the SignedTreeHeadDataV2 TLS type from section 5.8. +type SignedTreeHeadDataV2 struct { + LogID LogIDV2 `tls:"minlen:2,maxlen:127"` + TreeHead TreeHeadDataV2 + // The following signature is over the TLS encoding of the TreeHead value. + Signature tls.DigitallySigned +} + +// ConsistencyProofDataV2 holds hash values that prove the consistency of the +// Merkle tree between two tree sizes; it represents the +// ConsistencyProofDataV2 TLS type from section 5.9. +type ConsistencyProofDataV2 struct { + LogID LogIDV2 `tls:"minlen:2,maxlen:127"` + TreeSize1 uint64 + TreeSize2 uint64 + ConsistencyPath []NodeHash `tls:"minlen:1,maxlen:65535"` +} + +// InclusionProofDataV2 holds hash values that prove the inclusion of a given +// entry in the Log; it represents the InclusionProofDataV2 TLS structure from +// section 5.10. +type InclusionProofDataV2 struct { + LogID LogIDV2 `tls:"minlen:2,maxlen:127"` + TreeSize uint64 + LeafIndex uint64 + InclusionPath []NodeHash `tls:"minlen:1,maxlen:65535"` +} + +// SerializedTransItem holds a TLS-encoded TransItem structure; it represents +// the SerializedTransItem TLS type from section 8.2. (The struct wrapper is +// needed so that Data becomes a field and can have a field tag.) +type SerializedTransItem struct { + Data []byte `tls:"minlen:1,maxlen:65535"` +} + +// TransItemList holds multiple pieces of information from the same Log; it +// represents the TransItemList TLS type from section 8.2. +type TransItemList struct { + TransItemList []SerializedTransItem `tls:"minlen:1,maxlen:65535"` +} + +// SCTWithProofDataV2 provides combined information about an entry in the Log, +// including leaf and root information together. This represents the +// SCTWithProofDataV2 structure from section 8.3. +type SCTWithProofDataV2 struct { + SCT SignedCertificateTimestampDataV2 + STH SignedTreeHeadDataV2 + InclusionProof InclusionProofDataV2 +} + +// The second section holds code related to the web API for Certificate Transparency V2. + +// URI paths for client messages, from section 6. +const ( + // POST methods + AddChainPathV2 = "/ct/v2/add-chain" + AddPreChainPathV2 = "/ct/v2/add-pre-chain" + // GET methods + GetSTHPathV2 = "/ct/v2/get-sth" + GetSTHConsistencyPathV2 = "/ct/v2/get-sth-consistency" + GetProofByHashPathV2 = "/ct/v2/get-proof-by-hash" + GetAllByHashPathV2 = "/ct/v2/get-all-by-hash" + GetEntriesPathV2 = "/ct/v2/get-entries" + GetAnchorsPathV2 = "/ct/v2/get-anchors" + // Optional GET methods + GetEntryForSCTPathV2 = "/ct/v2/get-entry-for-sct" + GetEntryForTBSCertificatePathV2 = "/ct/v2/get-entry-for-tbscertificate" +) + +// Requests and responses are encoded as JSON objects (section 6) and so are +// represented here by structures with encoding/json field tags. + +// ErrorV2Response holds a general error response (when HTTP response code is 4xx/5xx). +type ErrorV2Response struct { + ErrorMessage string `json:"error_message"` + ErrorCode string `json:"error_code"` // One of validErrors +} + +// AddChainV2Request is used to add a chain to a Log (section 6.1). +type AddChainV2Request struct { + Chain []ASN1Cert `json:"chain"` +} + +// AddChainV2Response is the corresponding response contents. +type AddChainV2Response struct { + SCT TransItem `json:"sct"` // SCT.VersionedType == X509SCTV2 +} + +// AddPreChainV2Request is used to a pre-certificate to a Log (section 6.2). +type AddPreChainV2Request struct { + Precertificate CMSPrecert `json:"precertificate"` + Chain []ASN1Cert `json:"chain"` +} + +// AddPreChainV2Response is the corresponding response. +type AddPreChainV2Response struct { + SCT TransItem `json:"sct"` // SCT.VersionedType == PrecertSCTV2 +} + +// GetSTHV2Response is the data retrieved for the latest Signed Tree Head (section 6.3). +type GetSTHV2Response struct { + STH TransItem `json:"sth"` // STH.VersionedType == SignedTreeHeadV2 +} + +// GetSTHConsistencyV2Response holds the Merkle consistency proof between two signed tree +// heads (section 6.4). +type GetSTHConsistencyV2Response struct { + Consistency TransItem `json:"consistency"` // Consistency.VersionedType == ConsistencyProofV2 + STH TransItem `json:"sth"` // STH.VersionedType == SignedTreeHeadV2 +} + +// GetProofByHashV2Response holds the Merkle inclusion proof for a leaf hash (section 6.5). +type GetProofByHashV2Response struct { + Inclusion TransItem `json:"inclusion"` // Inclusion.VersionedType == InclusionProofV2 + STH TransItem `json:"sth"` // STH.VersionedType == SignedTreeHeadV2 +} + +// GetAllByHashV2Response holds a Merkle inclusion proof, STH and consistency proof for a +// leaf hash (section 6.6). +type GetAllByHashV2Response struct { + Inclusion TransItem `json:"inclusion"` // Inclusion.VersionedType == InclusionProofV2 + STH TransItem `json:"sth"` // STH.VersionedType == SignedTreeHeadV2 + Consistency TransItem `json:"consistency"` // Consistency.VersionedType == ConsistencyProofV2 +} + +// LogEntryDetail holds the details of an individual log entry (section 6.7). +type LogEntryDetail struct { + LeafInput TransItem `json:"leaf_input"` // LeafInput.VersionedType == X509EntryV2 or PrecertEntryV2 + LogEntry []byte `json:"log_entry"` // Either X509ChainEntry or PrecertChainEntryV2, TLS-encoded. + SCT TransItem `json:"sct"` // SCT.VersionedType == X509SCTV2 or PrecertSCTV2 +} + +// GetEntriesV2Response holds a collection of entries from a Log (section 6.7). +type GetEntriesV2Response struct { + Entries []LogEntryDetail `json:"entries"` + STH TransItem `json:"sth"` // STH.VersionedType == SignedTreeHeadV2 +} + +// GetAnchorsV2Response holds the accepted trust anchors for a Log (section 6.8). +type GetAnchorsV2Response struct { + Certificates [][]byte `json:"certificates"` + MaxChain uint64 `json:"max_chain,omitempty"` +} + +// GetEntryForSCTV2Response holds the entry number for an SCT (section 7.1). +type GetEntryForSCTV2Response struct { + Entry uint64 `json:"entry"` +} + +// GetEntriesForTBSCertificateV2Response holds a collection of log entries for a +// TBSCertificate (section 7.2). +type GetEntriesForTBSCertificateV2Response struct { + Entries []uint64 `json:"entries"` +} + +// ValidV2Errors holds the valid error codes for each client request. +var ValidV2Errors = map[string][]string{ + AddChainPathV2: []string{"not compliant", "unknown anchor", "bad chain", "bad certificate", "shutdown"}, + AddPreChainPathV2: []string{"not compliant", "unknown anchor", "bad chain", "bad certificate", "shutdown"}, + GetSTHPathV2: []string{"not compliant"}, + GetSTHConsistencyPathV2: []string{"not compliant", "first unknown", "second unknown"}, + GetProofByHashPathV2: []string{"not compliant", "hash unknown", "tree_size unknown"}, + GetAllByHashPathV2: []string{"not compliant", "hash unknown", "tree_size unknown"}, + GetEntriesPathV2: []string{"not compliant"}, + GetAnchorsPathV2: []string{"not compliant"}, + GetEntryForSCTPathV2: []string{"not compliant", "bad signature", "not found"}, + GetEntryForTBSCertificatePathV2: []string{"not compliant", "bad hash", "not found"}, +} diff --git a/types_test.go b/types_test.go index ff94295e68..e215fadb28 100644 --- a/types_test.go +++ b/types_test.go @@ -1,10 +1,17 @@ package ct import ( + "bytes" + "encoding/base64" "encoding/hex" + "encoding/json" + "fmt" + "reflect" "strings" "testing" + "github.com/google/certificate-transparency/go/asn1" + "github.com/google/certificate-transparency/go/testdata" "github.com/google/certificate-transparency/go/tls" ) @@ -50,3 +57,379 @@ func TestUnmarshalMerkleTreeLeaf(t *testing.T) { } } } + +func newVersionedTransType(v VersionedTransType) *VersionedTransType { return &v } + +var aHash = []byte{ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x01, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, +} + +func TestUnmarshalMarshalRoundTrip(t *testing.T) { + var tests = []struct { + data string // hex encoded + params string + item interface{} + }{ + {"00000401020304", "", &ASN1Cert{[]byte{1, 2, 3, 4}}}, + {"00000401020304" + "00000a" + "0000020506" + "0000020708", "", + &X509ChainEntry{ + LeafCertificate: ASN1Cert{[]byte{1, 2, 3, 4}}, + CertificateChain: []ASN1Cert{ + ASN1Cert{[]byte{5, 6}}, + ASN1Cert{[]byte{7, 8}}, + }, + }, + }, + {"00000401020304" + "000000", "", + &X509ChainEntry{ + LeafCertificate: ASN1Cert{[]byte{1, 2, 3, 4}}, + CertificateChain: []ASN1Cert{}, + }, + }, + {"00000401020304", "minlen:1,maxlen:16777215", &CMSPrecert{1, 2, 3, 4}}, + {"00000401020304" + "00000a" + "0000020506" + "0000020708", "", + &PrecertChainEntryV2{ + PreCertificate: CMSPrecert{1, 2, 3, 4}, + PrecertificateChain: []ASN1Cert{ + ASN1Cert{[]byte{5, 6}}, + ASN1Cert{[]byte{7, 8}}, + }, + }, + }, + {"0009", "maxval:65535", newVersionedTransType(PrecertSCTWithProofV2)}, + {"040a0b0c0d", "minlen:2,maxlen:127", &LogIDV2{0x0a, 0x0b, 0x0c, 0x0d}}, + {"0000000000001001" + "20" + hex.EncodeToString(aHash) + "00000410101111" + "0000", "", + &TimestampedCertificateEntryDataV2{ + Timestamp: 0x1001, + IssuerKeyHash: aHash, + TBSCertificate: TBSCertificate{16, 16, 17, 17}, + SCTExtensions: []SCTExtension{}, + }, + }, + {"0001" + "0000000000001001" + "20" + hex.EncodeToString(aHash) + "00000410101111" + "0000", "", + &TransItem{ + VersionedType: X509EntryV2, + X509EntryV2Data: &TimestampedCertificateEntryDataV2{ + Timestamp: 0x1001, + IssuerKeyHash: aHash, + TBSCertificate: TBSCertificate{16, 16, 17, 17}, + SCTExtensions: []SCTExtension{}, + }, + }, + }, + {"0001" + "000401020304", "", + &SCTExtension{ + SCTExtensionType: 1, + SCTExtensionData: []byte{1, 2, 3, 4}}}, + {"022a03" + "0000000022112233" + "0000" + "04030001ee", "", + &SignedCertificateTimestampDataV2{ + LogID: LogIDV2{0x2a, 0x03}, + Timestamp: 0x22112233, + SCTExtensions: []SCTExtension{}, + Signature: tls.DigitallySigned{ + Algorithm: tls.SignatureAndHashAlgorithm{ + Hash: tls.SHA256, + Signature: tls.ECDSA}, + Signature: []byte{0xee}, + }, + }, + }, + {"0001" + "000401020304", "", + &STHExtension{ + STHExtensionType: 1, + STHExtensionData: []byte{1, 2, 3, 4}, + }, + }, + {"1122334455667788" + "0000000000000100" + "02cafe" + "0000", "", + &TreeHeadDataV2{ + Timestamp: 0x1122334455667788, + TreeSize: 0x0100, + RootHash: NodeHash{Value: []byte{0xca, 0xfe}}, + STHExtensions: []STHExtension{}, + }, + }, + {"022a03" + ("1122334455667788" + "0000000000000100" + "02cafe" + "0000") + "04030001ee", "", + &SignedTreeHeadDataV2{ + LogID: LogIDV2{0x2a, 0x03}, + TreeHead: TreeHeadDataV2{ + Timestamp: 0x1122334455667788, + TreeSize: 0x0100, + RootHash: NodeHash{Value: []byte{0xca, 0xfe}}, + STHExtensions: []STHExtension{}, + }, + Signature: tls.DigitallySigned{ + Algorithm: tls.SignatureAndHashAlgorithm{ + Hash: tls.SHA256, + Signature: tls.ECDSA}, + Signature: []byte{0xee}, + }, + }, + }, + {"0005" + "022a03" + ("1122334455667788" + "0000000000000100" + "02cafe" + "0000") + "04030047" + testdata.EcdsaSignedAbcdHex, "", + &TransItem{ + VersionedType: SignedTreeHeadV2, + SignedTreeHeadV2Data: &SignedTreeHeadDataV2{ + LogID: LogIDV2{0x2a, 0x03}, + TreeHead: TreeHeadDataV2{ + Timestamp: 0x1122334455667788, + TreeSize: 0x0100, + RootHash: NodeHash{Value: []byte{0xca, 0xfe}}, + STHExtensions: []STHExtension{}, + }, + Signature: tls.DigitallySigned{ + Algorithm: tls.SignatureAndHashAlgorithm{ + Hash: tls.SHA256, + Signature: tls.ECDSA}, + Signature: testdata.FromHex(testdata.EcdsaSignedAbcdHex), + }, + }, + }, + }, + } + for _, test := range tests { + inVal := reflect.ValueOf(test.item).Elem() + pv := reflect.New(reflect.TypeOf(test.item).Elem()) + val := pv.Interface() + inData, _ := hex.DecodeString(test.data) + if _, err := tls.UnmarshalWithParams(inData, val, test.params); err != nil { + t.Errorf("Unmarshal(%s)=nil,%q; want %+v", test.data, err.Error(), inVal) + } else if !reflect.DeepEqual(val, test.item) { + t.Errorf("Unmarshal(%s)=%+v,nil; want %+v", test.data, reflect.ValueOf(val).Elem(), inVal) + } + + if data, err := tls.MarshalWithParams(inVal.Interface(), test.params); err != nil { + t.Errorf("Marshal(%+v)=nil,%q; want %s", inVal, err.Error(), test.data) + } else if !bytes.Equal(data, inData) { + t.Errorf("Marshal(%+v)=%s,nil; want %s", inVal, hex.EncodeToString(data), test.data) + } + } +} + +func TestLogIDV2FromOID(t *testing.T) { + var tests = []struct { + oid asn1.ObjectIdentifier + want string // hex encoded + errstr string + }{ + {asn1.ObjectIdentifier{}, "", "invalid object identifier"}, + {asn1.ObjectIdentifier{1, 2, 3}, "2a03", ""}, + {asn1.ObjectIdentifier{1, 2, 3, 4, 5, 6, 7, 8, 9}, "2a03040506070809", ""}, + // 128 values, first 2 get squished into a single byte => len=127, which is OK + {asn1.ObjectIdentifier{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, + 1, 2, 3, 4, 5, 6, 7, 8}, + "2a030405060708090001020304050607080900" + + "0102030405060708090001020304050607080900" + + "0102030405060708090001020304050607080900" + + "0102030405060708090001020304050607080900" + + "0102030405060708090001020304050607080900" + + "0102030405060708090001020304050607080900" + + "0102030405060708", ""}, + {asn1.ObjectIdentifier{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, + 1, 2, 3, 4, 5, 6, 7, 8, 9}, "", "too long"}, + } + for _, test := range tests { + got, err := LogIDV2FromOID(test.oid) + if test.errstr != "" { + if err == nil { + t.Errorf("LogIDV2FromOID(%v)=%v,nil; want error %q", test.oid, got, test.errstr) + } else if !strings.Contains(err.Error(), test.errstr) { + t.Errorf("LogIDV2FromOID(%v)=nil,%q; want error %q", test.oid, err.Error(), test.errstr) + } + continue + } + if err != nil { + t.Errorf("LogIDV2FromOID(%v)=nil,%q; want %v", test.oid, err.Error(), test.want) + } else if hex.EncodeToString(got) != test.want { + t.Errorf("LogIDV2FromOID(%v)=%q,nil; want %v", test.oid, hex.EncodeToString(got), test.want) + } + } +} + +func TestOIDFromLogIDV2(t *testing.T) { + var tests = []struct { + logID LogIDV2 + want asn1.ObjectIdentifier + errstr string + }{ + {logID: dehex("2a03"), want: asn1.ObjectIdentifier{1, 2, 3}}, + { + logID: dehex("2a030405060708090001020304050607080900" + + "0102030405060708090001020304050607080900" + + "0102030405060708090001020304050607080900" + + "0102030405060708090001020304050607080900" + + "0102030405060708090001020304050607080900" + + "0102030405060708090001020304050607080900" + + "0102030405060708"), + want: asn1.ObjectIdentifier{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, + 1, 2, 3, 4, 5, 6, 7, 8}, + }, + { + logID: dehex("2a030405060708090001020304050607080900" + + "0102030405060708090001020304050607080900" + + "0102030405060708090001020304050607080900" + + "0102030405060708090001020304050607080900" + + "0102030405060708090001020304050607080900" + + "0102030405060708090001020304050607080900" + + "010203040506070809"), + errstr: "log ID too long", + }, + {logID: dehex(""), errstr: "malformed LogIDV2"}, + } + for _, test := range tests { + got, err := OIDFromLogIDV2(test.logID) + if test.errstr != "" { + if err == nil { + t.Errorf("OIDFromLogIDV2(%v)=%v,nil; want error %q", test.logID, got, test.errstr) + } else if !strings.Contains(err.Error(), test.errstr) { + t.Errorf("OIDFromLogIDV2(%v)=nil,%q; want error %q", test.logID, err.Error(), test.errstr) + } + continue + } + if err != nil { + t.Errorf("OIDFromLogIDV2(%v)=nil,%q; want %q", test.logID, err.Error(), test.want) + } else if !test.want.Equal(got) { + t.Errorf("OIDFromLogIDV2(%v)=%q,nil; want %q", test.logID, got, test.want) + } + } +} + +type testTransItemHolder struct { + Val TransItem `json:"val"` +} + +func TestJSONUnmarshalTransItem(t *testing.T) { + var tests = []struct { + json string + item TransItem + errstr string + }{ + {json: bv64("0001" + "0000000000001001" + "20" + hex.EncodeToString(aHash) + "00000410101111" + "0000"), + item: TransItem{ + VersionedType: X509EntryV2, + X509EntryV2Data: &TimestampedCertificateEntryDataV2{ + Timestamp: 0x1001, + IssuerKeyHash: aHash, + TBSCertificate: TBSCertificate{16, 16, 17, 17}, + SCTExtensions: []SCTExtension{}, + }, + }, + }, + // Extra keys can be present in the JSON but are ignored. + {json: fmt.Sprintf("{\"val\":\"%s\",\"extra\":\"%s\"}", + b64("0001"+"0000000000001001"+"20"+hex.EncodeToString(aHash)+"00000410101111"+"0000"), + b64("01020304")), + item: TransItem{ + VersionedType: X509EntryV2, + X509EntryV2Data: &TimestampedCertificateEntryDataV2{ + Timestamp: 0x1001, + IssuerKeyHash: aHash, + TBSCertificate: TBSCertificate{16, 16, 17, 17}, + SCTExtensions: []SCTExtension{}, + }, + }, + }, + {json: `{"val": "not base 64 encoded"}`, errstr: "failed to unbase64"}, + {json: `{"val": 99}`, errstr: "failed to json.Unmarshal"}, + {json: `{"val": "abcd"}`, errstr: "failed to tls.Unmarshal"}, + {json: bv64("0001" + "0000000000001001" + "20" + hex.EncodeToString(aHash) + "00000410101111" + "0000eeff"), errstr: "trailing data"}, + } + for _, test := range tests { + var item testTransItemHolder + err := json.Unmarshal([]byte(test.json), &item) + if test.errstr != "" { + if err == nil { + t.Errorf("json.Unmarshal('%s')=%+v,nil; want error %q", test.json, item, test.errstr) + } else if !strings.Contains(err.Error(), test.errstr) { + t.Errorf("json.Unmarshal('%s')=nil,%q; want error %q", test.json, err.Error(), test.errstr) + } + continue + } + if err != nil { + t.Errorf("json.Unmarshal('%s')=nil,%q; want %v", test.json, err.Error, test.item) + } else if !reflect.DeepEqual(item.Val, test.item) { + t.Errorf("json.Unmarshal('%s')=%v,nil; want %v", test.json, item, test.item) + } + } +} + +func TestJSONMarshalTransItem(t *testing.T) { + var tests = []struct { + item TransItem + json string + errstr string + }{ + { + item: TransItem{ + VersionedType: X509EntryV2, + X509EntryV2Data: &TimestampedCertificateEntryDataV2{ + Timestamp: 0x1001, + IssuerKeyHash: aHash, + TBSCertificate: TBSCertificate{16, 16, 17, 17}, + SCTExtensions: []SCTExtension{}, + }, + }, + json: bv64("0001" + "0000000000001001" + "20" + hex.EncodeToString(aHash) + "00000410101111" + "0000"), + }, + { + item: TransItem{ + VersionedType: 99, + X509EntryV2Data: &TimestampedCertificateEntryDataV2{ + Timestamp: 0x1001, + IssuerKeyHash: aHash, + TBSCertificate: TBSCertificate{16, 16, 17, 17}, + SCTExtensions: []SCTExtension{}, + }, + }, + errstr: "unchosen field is non-nil", + }, + {item: TransItem{VersionedType: 99}, errstr: "unhandled value for selector"}, + {item: TransItem{VersionedType: X509EntryV2}, errstr: "chosen field is nil"}, + } + for _, test := range tests { + var item testTransItemHolder + item.Val = test.item + data, err := json.Marshal(item) + if test.errstr != "" { + if err == nil { + t.Errorf("json.Marshal(%+v)=%s,nil; want error %q", test.item, hex.EncodeToString(data), test.errstr) + } else if !strings.Contains(err.Error(), test.errstr) { + t.Errorf("json.Marshal(%+v)=nil,%q; want error %q", test.item, err.Error(), test.errstr) + } + continue + } + if err != nil { + t.Errorf("json.Marshal(%+v)=nil,%q; want %s", test.item, err.Error(), test.json) + } else if string(data) != test.json { + t.Errorf("json.Marshal(%+v)=%s,nil; want %s", test.item, data, test.json) + } + } +} + +var dehex = testdata.FromHex + +func b64(hexData string) string { + return base64.StdEncoding.EncodeToString(dehex(hexData)) +} + +func bv64(hexData string) string { + return fmt.Sprintf(`{"val":"%s"}`, b64(hexData)) +} From c49441da6e0baf69110d0ea3d6a666a46ffa12bb Mon Sep 17 00:00:00 2001 From: David Drysdale Date: Thu, 27 Oct 2016 15:02:02 +0100 Subject: [PATCH 4/5] go/clientv2: enough CMS support for CT v2 --- clientv2/cms.go | 158 ++++++++++++++++++++++++++++++ clientv2/cms_test.go | 212 +++++++++++++++++++++++++++++++++++++++++ clientv2/precert.ascii | 97 +++++++++++++++++++ 3 files changed, 467 insertions(+) create mode 100644 clientv2/cms.go create mode 100644 clientv2/cms_test.go create mode 100644 clientv2/precert.ascii diff --git a/clientv2/cms.go b/clientv2/cms.go new file mode 100644 index 0000000000..de04c84f98 --- /dev/null +++ b/clientv2/cms.go @@ -0,0 +1,158 @@ +package clientv2 + +import ( + "fmt" + "math/big" + + ct "github.com/google/certificate-transparency/go" + "github.com/google/certificate-transparency/go/asn1" + "github.com/google/certificate-transparency/go/x509/pkix" +) + +// This file holds code related to the Cryptographic Message Syntax (CMS) +// defined by RFC 5652; any references to a section number refer to this +// document. + +// CMSSignedData is the equivalent of the SignedData ASN.1 structure from section 5.1. +type CMSSignedData struct { + Version int + DigestAlgorithms DigestAlgorithmIdentifiersSET + EncapContentInfo EncapsulatedContentInfo + Certificates CertificatesSET `asn1:"tag:0,optional"` + CrlsSET RevocationInfoChoicesSET `asn1:"tag:1,optional"` + SignerInfos SignerInfosSET +} + +// The following types all have "..SET" in the type name to indicate to the asn1 encoder that they +// are SETs not SEQUENCEs. + +// DigestAlgorithmIdentifiersSET is the equivalent of the DigestAlgorithmIdentifiers +// ASN.1 type from section 5.1. +type DigestAlgorithmIdentifiersSET []pkix.AlgorithmIdentifier + +// CertificatesSET is the equivalent of the CertificateSet ASN.1 type from section 5.1. +type CertificatesSET []asn1.RawValue + +// RevocationInfoChoicesSET is the equivalent of the RevocationInfoChoices ASN.1 type from section 5.1. +type RevocationInfoChoicesSET []asn1.RawValue + +// SignerInfosSET is the equivalent of the SignerInfos ASN.1 type from section 5.1. +type SignerInfosSET []SignerInfo + +// AttributesSET is the equivalent of the SignedAttributes and UnsignedAttributes ASN.1 types from section 5.3. +type AttributesSET []Attribute + +// AttrValuesSET is the equivalent of the 'SET OF AttributeValue' ASN.1 type from section 5.3 +type AttrValuesSET []asn1.RawValue + +// EncapsulatedContentInfo is the equivalent of the EncapsulatedContentInfo ASN.1 type from section 5.2. +type EncapsulatedContentInfo struct { + EContentType asn1.ObjectIdentifier + EContent []byte `asn1:"tag:0,explicit,optional"` +} + +// SignerInfo is the equivalent of the SignerInfo ASN.1 type from section 5.3. +type SignerInfo struct { + Version int + SID SignerIdentifier + DigestAlgorithm pkix.AlgorithmIdentifier + SignedAttributes AttributesSET `asn1:"tag:0,optional"` + SignatureAlgorithm pkix.AlgorithmIdentifier + Signature []byte + UnsignedAttrs AttributesSET `asn1:"tag:1,optional"` +} + +// SignerIdentifier corresponds to a single variant of the ASN.1 SignerIdentifier type (which is a CHOICE), from section 5.3. +type SignerIdentifier IssuerAndSerialNumber + +// IssuerAndSerialNumber is the equivalent of the IssuerAndSerialNumber ASN.1 type from section 10.2.4. +type IssuerAndSerialNumber struct { + Issuer pkix.RDNSequence // For a pkix.Name. + SerialNumber *big.Int +} + +// Attribute is equivalent of the Attribute ASN.1 type from section 5.3. +type Attribute struct { + AttrType asn1.ObjectIdentifier + AttrValues AttrValuesSET +} + +// OID value to identify the content-type attribute, from section 11.1. +var cmsContentTypeOID = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 3} + +// OID value to identify the message digest attribute, from section 11.2. +var cmsMessageDigestOID = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 4} + +// OID value to identify a pre-certificate content type, from RFC 6962-bis section 3.2. +var cmsPrecertEContentTypeOID = asn1.ObjectIdentifier{1, 3, 101, 78} + +// Given that section 3.2 indicates the certificates field is omitted, and the +// eContentType is not id-data, the version algorithm of RFC 5652 section 5.1 +// indicates that version 3 should be used. +const cmsExpectedVersion = 3 + +// CMSExtractPrecert retrieves a CMS encoded precertificate from an ASN.1 +// encoded version. +func CMSExtractPrecert(precert ct.CMSPrecert) (*CMSSignedData, error) { + var cms CMSSignedData + rest, err := asn1.Unmarshal(precert, &cms) + if err != nil { + return nil, fmt.Errorf("failed to parse CMS-encoded precert: %s", err.Error()) + } else if len(rest) > 0 { + return nil, fmt.Errorf("trailing data present in CMS-encoded precert") + } + if !cmsPrecertEContentTypeOID.Equal(cms.EncapContentInfo.EContentType) { + return nil, fmt.Errorf("unexpected content OID %v in CMS-encoded precert", cms.EncapContentInfo.EContentType) + } + if len(cms.Certificates) > 0 { + return nil, fmt.Errorf("unexpected certificates present in CMS-encoded precert") + } + if len(cms.SignerInfos) != 1 { + return nil, fmt.Errorf("unexpected number (%d) of signer-infos present in CMS-encoded precert", len(cms.SignerInfos)) + } + messageDigest := []byte{} + seenContentType := false + for _, attr := range cms.SignerInfos[0].SignedAttributes { + switch { + case cmsContentTypeOID.Equal(attr.AttrType): + if len(attr.AttrValues) != 1 { + return nil, fmt.Errorf("content-type attribute with unexpected number (%d) of values in CMS-encoded precert", len(attr.AttrValues)) + } + var oid asn1.ObjectIdentifier + rest, err := asn1.Unmarshal(attr.AttrValues[0].FullBytes, &oid) + if err != nil { + return nil, fmt.Errorf("failed to decode content-type OID in CMS-encoded precert") + } else if len(rest) > 0 { + return nil, fmt.Errorf("trailing data after content-type OID in CMS-encoded precert") + } + if !cmsPrecertEContentTypeOID.Equal(oid) { + return nil, fmt.Errorf("incorrect OID %s in content-type of CMS-encoded precert", oid) + } + seenContentType = true + case cmsMessageDigestOID.Equal(attr.AttrType): + if len(attr.AttrValues) != 1 { + return nil, fmt.Errorf("message-digest attribute with unexpected number (%d) of values in CMS-encoded precert", len(attr.AttrValues)) + } + // TODO(drysdale): replace magic numbers with asn1.Universal/asn1.OctetString when available + if attr.AttrValues[0].Class != 0 || attr.AttrValues[0].Tag != 4 { + return nil, fmt.Errorf("message-digest attribute value of wrong type in CMS-encoded precert", len(attr.AttrValues)) + } + messageDigest = attr.AttrValues[0].Bytes + } + } + if len(messageDigest) == 0 { + return nil, fmt.Errorf("missing required message-digest signedAttrs in CMS-encoded precert") + } + if !seenContentType { + return nil, fmt.Errorf("missing required content-type signedAttrs in CMS-encoded precert") + } + return &cms, nil +} + +func cmsCheckSignature(cms CMSSignedData, pubKey interface{}) error { + // TODO(drysdale): check that the signature: + // - check the digest is correct for eContent + // - check the signature over the hash using pubKey + + return nil +} diff --git a/clientv2/cms_test.go b/clientv2/cms_test.go new file mode 100644 index 0000000000..c7ede3fda3 --- /dev/null +++ b/clientv2/cms_test.go @@ -0,0 +1,212 @@ +package clientv2 + +import ( + "encoding/hex" + "math/big" + "reflect" + "strings" + "testing" + + "github.com/google/certificate-transparency/go/asn1" + "github.com/google/certificate-transparency/go/testdata" + "github.com/google/certificate-transparency/go/x509/pkix" +) + +func TestCMSExtractPrecert(t *testing.T) { + // Make some broken copies of the valid precert. + wrongOID := testPrecert() + wrongOID.EncapContentInfo.EContentType = asn1.ObjectIdentifier{1, 2, 3, 4, 5} + hasCerts := testPrecert() + hasCerts.Certificates = CertificatesSET{ + asn1.RawValue{ + Class: asn1.ClassUniversal, + Tag: asn1.TagOctetString, + Bytes: testdata.FromHex(testdata.AbcdSHA256), + FullBytes: fullBytes(byte(asn1.TagOctetString), testdata.FromHex(testdata.AbcdSHA256)), + }, + } + noSignerInfo := testPrecert() + noSignerInfo.SignerInfos = SignerInfosSET{} + multipleOIDAttrValue := testPrecert() + attrValues := &multipleOIDAttrValue.SignerInfos[0].SignedAttributes[0].AttrValues + *attrValues = append(*attrValues, (*attrValues)[0]) + wrongOIDAttrValue := testPrecert() + wrongOIDAttrValue.SignerInfos[0].SignedAttributes[0].AttrValues = AttrValuesSET{ + asn1.RawValue{ + Class: asn1.ClassUniversal, + Tag: asn1.TagOID, + Bytes: []byte{0x2b, 0x65, 0x11}, + FullBytes: fullBytes(byte(asn1.TagOID), []byte{0x2b, 0x65, 0x11}), + }, + } + nonOIDAttrValue := testPrecert() + nonOIDAttrValue.SignerInfos[0].SignedAttributes[0].AttrValues = AttrValuesSET{ + asn1.RawValue{ + Class: asn1.ClassUniversal, + Tag: asn1.TagOctetString, // Not OID + Bytes: []byte{0x2b, 0x65, 0x11}, + FullBytes: fullBytes(byte(asn1.TagOctetString), []byte{0x2b, 0x65, 0x11}), + }, + } + multipleDigestAttrValue := testPrecert() + attrValues = &multipleDigestAttrValue.SignerInfos[0].SignedAttributes[1].AttrValues + *attrValues = append(*attrValues, (*attrValues)[0]) + nonOctetStringAttrValue := testPrecert() + nonOctetStringAttrValue.SignerInfos[0].SignedAttributes[1].AttrValues = AttrValuesSET{ + asn1.RawValue{ + Class: asn1.ClassUniversal, + Tag: asn1.TagOID, // Not OCTET STRING + Bytes: []byte{0x2b, 0x65, 0x11}, + FullBytes: fullBytes(byte(asn1.TagOID), []byte{0x2b, 0x65, 0x11}), + }, + } + noContentType := testPrecert() + noContentType.SignerInfos[0].SignedAttributes = noContentType.SignerInfos[0].SignedAttributes[1:] + noDigest := testPrecert() + noDigest.SignerInfos[0].SignedAttributes = noDigest.SignerInfos[0].SignedAttributes[:1] + + var tests = []struct { + in string // hex-encoded + want *CMSSignedData + errstr string + }{ + {"", nil, "failed to parse"}, + {testCMSPrecertHex, &testCMSPrecert, ""}, + {testCMSPrecertHex + "020100", &testCMSPrecert, "trailing data"}, + {m2h(wrongOID), nil, "unexpected content OID"}, + {m2h(hasCerts), nil, "unexpected certificates"}, + {m2h(noSignerInfo), nil, "signer-infos present"}, + {m2h(multipleOIDAttrValue), nil, "content-type attribute with unexpected number"}, + {m2h(wrongOIDAttrValue), nil, "incorrect OID"}, + {m2h(nonOIDAttrValue), nil, "failed to decode content-type OID"}, + {m2h(multipleDigestAttrValue), nil, "message-digest attribute with unexpected number"}, + {m2h(nonOctetStringAttrValue), nil, "message-digest attribute value of wrong type"}, + {m2h(noContentType), nil, "missing required content-type"}, + {m2h(noDigest), nil, "missing required message-digest"}, + } + + for _, test := range tests { + input, _ := hex.DecodeString(test.in) + got, err := CMSExtractPrecert(input) + if test.errstr != "" { + if err == nil { + t.Errorf("cmsExtractPrecert(%q)=%+v,nil; want error %q", test.in, got, test.errstr) + } else if !strings.Contains(err.Error(), test.errstr) { + t.Errorf("cmsExtractPrecert(%q)=nil,%q; want error %q", test.in, err.Error(), test.errstr) + } + continue + } + if err != nil { + t.Errorf("cmsExtractPrecert(%q)=nil,%q; want %+v", test.in, err.Error(), test.want) + } else if !reflect.DeepEqual(got, test.want) { + t.Errorf("cmsExtractPrecert(%q)=%+v,nil; want %+v", test.in, got, test.want) + } + } +} + +// Test data. Generated from precert.ascii using the the ascii2der tool from +// github.com/google/der-ascii. +var testCMSPrecertHex = "3082010a020103310d300b06092a864886f70d01010b300d06032b654ea00604" + + "04616263643181e63081e302010330793071310b300906035504061302474231" + + "0f300d060355040813064c6f6e646f6e310f300d060355040713064c6f6e646f" + + "6e310f300d060355040a1306476f6f676c65310c300a060355040b1303456e67" + + "3121301f0603550403131846616b654365727469666963617465417574686f72" + + "69747902040406cafe300b0609608648016503040201a045301206092a864886" + + "f70d010903310506032b654e302f06092a864886f70d0109043122042088d426" + + "6fd4e6338d13b845fcf289579d209c897823b9217da3e161936f031589300b06" + + "092a864886f70d01010b0402abcd" + +// Generated by running asn1.Marshal on testCMSPrecert below. +var testCMSPrecertMarshalled = "3082010a020103310d300b06092a864886f70d01010b300d06032b654ea0060404616263643181e63081e302010330793071310b3009060355040613024742310f300d060355040813064c6f6e646f6e310f300d060355040713064c6f6e646f6e310f300d060355040a1306476f6f676c65310c300a060355040b1303456e673121301f0603550403131846616b654365727469666963617465417574686f7269747902040406cafe300b0609608648016503040201a045301206092a864886f70d010903310506032b654e302f06092a864886f70d0109043122042088d4266fd4e6338d13b845fcf289579d209c897823b9217da3e161936f031589300b06092a864886f70d01010b0402abcd" + +// Make a copy of the test precertificate. +func testPrecert() CMSSignedData { + data, err := asn1.Marshal(testCMSPrecert) + if err != nil { + panic("Failed to marshal test precertificate: " + err.Error()) + } + var result CMSSignedData + if rest, err := asn1.Unmarshal(data, &result); err != nil { + panic("Failed to re-unmarshal test precertificate: " + err.Error()) + } else if len(rest) > 0 { + panic("Excess data on re-unmarshalling test precertificate") + } + return result +} + +// Marshal a precertificate to a hex string. +func m2h(precert CMSSignedData) string { + data, err := asn1.Marshal(precert) + if err != nil { + panic("Failed to marshal test precertificate: " + err.Error()) + } + return hex.EncodeToString(data) +} + +var testCMSPrecert = CMSSignedData{ + Version: 3, + DigestAlgorithms: []pkix.AlgorithmIdentifier{ + pkix.AlgorithmIdentifier{Algorithm: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 11}}, + }, + EncapContentInfo: EncapsulatedContentInfo{ + EContentType: asn1.ObjectIdentifier{1, 3, 101, 78}, + EContent: []byte{0x61, 0x62, 0x63, 0x64}, // should be a DER-encoded TBSCertificate, but here is "abcd" + }, + SignerInfos: SignerInfosSET{ + SignerInfo{ + Version: 3, + SID: SignerIdentifier{ + Issuer: pkix.RDNSequence{ + pkix.RelativeDistinguishedNameSET{pkix.AttributeTypeAndValue{Type: asn1.ObjectIdentifier{2, 5, 4, 6}, Value: "GB"}}, + pkix.RelativeDistinguishedNameSET{pkix.AttributeTypeAndValue{Type: asn1.ObjectIdentifier{2, 5, 4, 8}, Value: "London"}}, + pkix.RelativeDistinguishedNameSET{pkix.AttributeTypeAndValue{Type: asn1.ObjectIdentifier{2, 5, 4, 7}, Value: "London"}}, + pkix.RelativeDistinguishedNameSET{pkix.AttributeTypeAndValue{Type: asn1.ObjectIdentifier{2, 5, 4, 10}, Value: "Google"}}, + pkix.RelativeDistinguishedNameSET{pkix.AttributeTypeAndValue{Type: asn1.ObjectIdentifier{2, 5, 4, 11}, Value: "Eng"}}, + pkix.RelativeDistinguishedNameSET{pkix.AttributeTypeAndValue{Type: asn1.ObjectIdentifier{2, 5, 4, 3}, Value: "FakeCertificateAuthority"}}, + }, + SerialNumber: big.NewInt(0x0406cafe)}, + DigestAlgorithm: pkix.AlgorithmIdentifier{Algorithm: asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1}}, + SignatureAlgorithm: pkix.AlgorithmIdentifier{Algorithm: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 11}}, + SignedAttributes: AttributesSET{ + Attribute{ + AttrType: cmsContentTypeOID, + AttrValues: AttrValuesSET{ + // OID 1.3.101.78 as in EncapContentInfo.EContentType. + asn1.RawValue{ + Class: asn1.ClassUniversal, + Tag: asn1.TagOID, + Bytes: []byte{0x2b, 0x65, 0x4e}, + FullBytes: fullBytes(byte(asn1.TagOID), []byte{0x2b, 0x65, 0x4e}), + }, + }, + }, + Attribute{ + AttrType: cmsMessageDigestOID, + AttrValues: AttrValuesSET{ + asn1.RawValue{ + Class: asn1.ClassUniversal, + Tag: asn1.TagOctetString, + Bytes: testdata.FromHex(testdata.AbcdSHA256), + FullBytes: fullBytes(byte(asn1.TagOctetString), testdata.FromHex(testdata.AbcdSHA256)), + }, + }, + }, + }, + Signature: []byte{0xab, 0xcd}, + }, + }, +} + +// Given some data, return the DER-encoded equivalent with the supplied +// tag (and assuming the class is asn1.ClassUniversal), by prefixing with +// the type/length. +func fullBytes(tag byte, data []byte) []byte { + result := make([]byte, len(data)+2) + result[0] = tag + if len(data) > 255 { + panic("len too long") + } + result[1] = byte(len(data)) + copy(result[2:], data) + return result +} diff --git a/clientv2/precert.ascii b/clientv2/precert.ascii new file mode 100644 index 0000000000..04658843d6 --- /dev/null +++ b/clientv2/precert.ascii @@ -0,0 +1,97 @@ +SEQUENCE { # SignedData + INTEGER { 3 } # version = v3(3) + SET { # DigestAlgorithmIdentifiers + SEQUENCE { # AlgorithmIdentifier + # sha256WithRSAEncryption + OBJECT_IDENTIFIER { 1.2.840.113549.1.1.11 } # algorithm + } + } # digestAlgorithms + SEQUENCE { # EncapsulatedContentInfo + OBJECT_IDENTIFIER { 1.3.101.78 } # eContentType + [0] { + OCTET_STRING { "abcd" } + } + } # encapContentInfo + # no certificates + # no crls + SET { # SignerInfos + SEQUENCE { # SignerInfo + INTEGER { 3 } # version = v3(3) + SEQUENCE { # IssuerAndSerialNumber + SEQUENCE { # Name + SET { + SEQUENCE { + # countryName + OBJECT_IDENTIFIER { 2.5.4.6 } + PrintableString { "GB" } + } + } + SET { + SEQUENCE { + # stateOrProvinceName + OBJECT_IDENTIFIER { 2.5.4.8 } + PrintableString { "London" } + } + } + SET { + SEQUENCE { + # localityName + OBJECT_IDENTIFIER { 2.5.4.7 } + PrintableString { "London" } + } + } + SET { + SEQUENCE { + # organizationName + OBJECT_IDENTIFIER { 2.5.4.10 } + PrintableString { "Google" } + } + } + SET { + SEQUENCE { + # organizationUnitName + OBJECT_IDENTIFIER { 2.5.4.11 } + PrintableString { "Eng" } + } + } + SET { + SEQUENCE { + # commonName + OBJECT_IDENTIFIER { 2.5.4.3 } + PrintableString { "FakeCertificateAuthority" } + } + } + } # issuer + INTEGER { `0406cafe` } # serialNumber + } # sid + SEQUENCE { # AlgorithmIdentifier + # sha256 + OBJECT_IDENTIFIER { 2.16.840.1.101.3.4.2.1 } # algorithm + } # digestAlgorithm + [0] { # SignedAttributes + SEQUENCE { # Attribute + OBJECT_IDENTIFIER { 1.2.840.113549.1.9.3 } + SET { + OBJECT_IDENTIFIER { 1.3.101.78 } + } + } + SEQUENCE { # Attribute + OBJECT_IDENTIFIER { 1.2.840.113549.1.9.4 } + SET { + OCTET_STRING { `88d4266fd4e6338d13b845fcf289579d209c897823b9217da3e161936f031589` } + } + } + } # signedAttrs + # no signedAttrs + SEQUENCE { # AlgorithmIdentifier + # sha256WithRSAEncryption + OBJECT_IDENTIFIER { 1.2.840.113549.1.1.11 } # algorithm + } # signatureAlgorithm + OCTET_STRING { # SignatureValue + `abcd` # data here + } # signature + # no unsignedAttrs + } + } # signerInfos +} + From 5ed50b35e856feb861860f6ab2b5ac785ea4d378 Mon Sep 17 00:00:00 2001 From: David Drysdale Date: Fri, 18 Nov 2016 10:16:47 +0000 Subject: [PATCH 5/5] go/clientv2: start on v2 log client implementation --- clientv2/logclient.go | 205 +++++++++++++++++++++++++++++++++++++ clientv2/logclient_test.go | 189 ++++++++++++++++++++++++++++++++++ 2 files changed, 394 insertions(+) create mode 100644 clientv2/logclient.go create mode 100644 clientv2/logclient_test.go diff --git a/clientv2/logclient.go b/clientv2/logclient.go new file mode 100644 index 0000000000..45e3e27587 --- /dev/null +++ b/clientv2/logclient.go @@ -0,0 +1,205 @@ +package clientv2 + +import ( + "crypto" + "errors" + "fmt" + "hash" + "net/http" + + ct "github.com/google/certificate-transparency/go" + "github.com/google/certificate-transparency/go/jsonclient" + "github.com/google/certificate-transparency/go/tls" + "github.com/google/certificate-transparency/go/x509" + "golang.org/x/net/context" +) + +// This code is based on draft-ietf-trans-rfc6962-bis-19.txt; any +// references to a section number on its own refer to this document. + +// Options provides configuration options when constructing a LogClient instance. +type Options struct { + jsonclient.Options + hashAlgo tls.HashAlgorithm +} + +// LogClient represents a client for a given v2 CT Log instance. +type LogClient struct { + jsonclient.JSONClient + hasher hash.Hash +} + +// New constructs a new LogClient instance for interaction with a v2 +// Certificate Transparency Log. +func New(uri string, hc *http.Client, opts Options) (*LogClient, error) { + hasher, err := hasherForAlgorithm(opts.hashAlgo) + if err != nil { + return nil, err + } + logClient, err := jsonclient.New(uri, hc, opts.Options) + if err != nil { + return nil, err + } + return &LogClient{*logClient, hasher}, nil +} + +func hasherForAlgorithm(hashAlgo tls.HashAlgorithm) (hash.Hash, error) { + switch hashAlgo { + case tls.SHA256: + // SHA256 is the only allowed hash algorithm, see section 12.3. + return crypto.SHA256.New(), nil + default: + return nil, fmt.Errorf("unsupported hash algorithm %d for Log", hashAlgo) + } +} + +// AddChain adds the provided chain of (DER-encoded) X.509 certificates to the Log. +func (c *LogClient) AddChain(ctx context.Context, chain []ct.ASN1Cert) (*ct.SignedCertificateTimestampDataV2, error) { + if len(chain) == 0 { + return nil, errors.New("no certificate provided") + } + req := ct.AddChainV2Request{Chain: chain} + var rsp ct.AddChainV2Response + if _, err := c.PostAndParseWithRetry(ctx, ct.AddChainPathV2, req, &rsp); err != nil { + return nil, err + } + if rsp.SCT.VersionedType != ct.X509SCTV2 { + return nil, fmt.Errorf("received unexpected TransItem type %d not X509SCTV2(%d)", rsp.SCT.VersionedType, ct.X509SCTV2) + } + result := rsp.SCT.X509SCTV2Data + if err := checkSCTExtensions(result.SCTExtensions); err != nil { + return nil, err + } + + if c.Verifier != nil { + // result.Signature is over a TransItem of type X509EntryV2. + cert, err := x509.ParseCertificate(chain[0].Data) + if err != nil { + return nil, fmt.Errorf("failed to parse leaf certificate: %s", err.Error()) + } + if len(chain) < 2 { + return nil, fmt.Errorf("cannot validate as issuer key unavailable for %s", cert.Issuer) + } + issuerCert, err := x509.ParseCertificate(chain[1].Data) + if err != nil { + return nil, fmt.Errorf("failed to parse issuer certificate: %s", err.Error()) + } + keyHash := c.hasher.Sum(issuerCert.RawSubjectPublicKeyInfo) + signedData := ct.TransItem{ + VersionedType: ct.X509EntryV2, + X509EntryV2Data: &ct.TimestampedCertificateEntryDataV2{ + Timestamp: result.Timestamp, + IssuerKeyHash: keyHash, + TBSCertificate: cert.RawTBSCertificate, + SCTExtensions: result.SCTExtensions, + }, + } + // TLS-encode the TransItem + data, err := tls.Marshal(signedData) + if err != nil { + return nil, fmt.Errorf("failed to marshal data for signature check: %s", err.Error()) + } + if err := c.Verifier.VerifySignature(data, result.Signature); err != nil { + return nil, fmt.Errorf("failed to verify SCT signature: %s", err.Error()) + } + } + return result, nil +} + +// AddPreChain adds the provided precertificate to the Log, where precert is a +// DER-encoded CMS SignedData structure filled out as described in section 3.2, +// and chain is a list (DER-encoded) X.509 certificates needed to validate the +// precertificate. +func (c *LogClient) AddPreChain(ctx context.Context, precert ct.CMSPrecert, chain []ct.ASN1Cert) (*ct.SignedCertificateTimestampDataV2, error) { + if len(chain) == 0 { + return nil, errors.New("no issuer certificate provided") + } + // Check the CMS data is valid. + parsedPrecert, err := CMSExtractPrecert(precert) + if err != nil { + return nil, err + } + issuerCert, err := x509.ParseCertificate(chain[0].Data) + if err != nil { + return nil, fmt.Errorf("failed to parse issuer certificate: %s", err.Error()) + } + err = cmsCheckSignature(*parsedPrecert, issuerCert.PublicKey) + if err != nil { + return nil, err + } + req := ct.AddPreChainV2Request{Precertificate: precert, Chain: chain} + var rsp ct.AddPreChainV2Response + if _, err := c.PostAndParseWithRetry(ctx, ct.AddPreChainPathV2, req, &rsp); err != nil { + return nil, err + } + if rsp.SCT.VersionedType != ct.PrecertSCTV2 { + return nil, fmt.Errorf("received unexpected TransItem type %d not X509SCTV2(%d)", rsp.SCT.VersionedType, ct.PrecertSCTV2) + } + + result := rsp.SCT.X509SCTV2Data + if err := checkSCTExtensions(result.SCTExtensions); err != nil { + return nil, err + } + + if c.Verifier != nil { + // result.Signature is over a TransItem of type PrecertEntryV2. + keyHash := c.hasher.Sum(issuerCert.RawSubjectPublicKeyInfo) + signedData := ct.TransItem{ + VersionedType: ct.PrecertEntryV2, + PrecertEntryV2Data: &ct.TimestampedCertificateEntryDataV2{ + Timestamp: result.Timestamp, + IssuerKeyHash: keyHash, + TBSCertificate: parsedPrecert.EncapContentInfo.EContent, + SCTExtensions: result.SCTExtensions, + }, + } + // TLS-encode the TransItem + data, err := tls.Marshal(signedData) + if err != nil { + return nil, fmt.Errorf("failed to marshal data for signature check: %s", err.Error()) + } + if err := c.Verifier.VerifySignature(data, result.Signature); err != nil { + return nil, fmt.Errorf("failed to verify SCT signature: %s", err.Error()) + } + } + return result, nil +} + +// GetSTH retrieves the signed tree head (STH) of the log, as described in section 3.3. +func (c *LogClient) GetSTH(ctx context.Context) (*ct.SignedTreeHeadDataV2, error) { + var rsp ct.GetSTHV2Response + if _, err := c.GetAndParse(ctx, ct.GetSTHPathV2, nil, &rsp); err != nil { + return nil, err + } + if rsp.STH.VersionedType != ct.SignedTreeHeadV2 { + return nil, fmt.Errorf("received unexpected TransItem type %d not SignedTreeHeadV2(%d)", rsp.STH.VersionedType, ct.SignedTreeHeadV2) + } + result := rsp.STH.SignedTreeHeadV2Data + if c.Verifier != nil { + // result.Signature is over the TLS encoding of result.TreeHead + data, err := tls.Marshal(result.TreeHead) + if err != nil { + return nil, fmt.Errorf("failed to marshal data for signature check: %s", err.Error()) + } + if err := c.Verifier.VerifySignature(data, result.Signature); err != nil { + return nil, fmt.Errorf("failed to verify STH signature: %s", err.Error()) + } + + } + return result, nil +} + +// TODO(drysdale): all the other entrypoints + +// Check extensions are sane. +func checkSCTExtensions(exts []ct.SCTExtension) error { + lastExtType := -1 + for _, ext := range exts { + extType := int(ext.SCTExtensionType) + if extType <= lastExtType { + return fmt.Errorf("SCT extensions not ordered correctly, %d then %d", lastExtType, extType) + } + lastExtType = extType + } + return nil +} diff --git a/clientv2/logclient_test.go b/clientv2/logclient_test.go new file mode 100644 index 0000000000..9722d11501 --- /dev/null +++ b/clientv2/logclient_test.go @@ -0,0 +1,189 @@ +package clientv2 + +import ( + "encoding/base64" + "fmt" + "net/http" + "net/http/httptest" + "reflect" + "strings" + "testing" + + ct "github.com/google/certificate-transparency/go" + "github.com/google/certificate-transparency/go/jsonclient" + "github.com/google/certificate-transparency/go/testdata" + "github.com/google/certificate-transparency/go/tls" + "golang.org/x/net/context" +) + +func TestHasherForAlgorithm(t *testing.T) { + var tests = []struct { + algo tls.HashAlgorithm + errstr string + }{ + {99, "unsupported hash algorithm"}, + {tls.SHA256, ""}, + } + for _, test := range tests { + got, err := hasherForAlgorithm(test.algo) + if test.errstr != "" { + if err == nil { + t.Errorf("hasherForAlgorithm(%d)=%T, nil; want error %q", test.algo, got, test.errstr) + } else if !strings.Contains(err.Error(), test.errstr) { + t.Errorf("hasherForAlgorithm(%d)=nil,%q; want error %q", test.algo, err.Error(), test.errstr) + } + continue + } + if err != nil { + t.Errorf("hasherForAlgorithm(%d)=nil,%q; want error nil", test.algo, err.Error()) + } + } +} + +func TestNewLogClient(t *testing.T) { + var tests = []struct { + algo tls.HashAlgorithm + pubKey string + errstr string + }{ + {tls.SHA256, "", ""}, + {tls.SHA256, testdata.EcdsaPublicKeyPEM, ""}, + {99, testdata.EcdsaPublicKeyPEM, "unsupported hash algorithm"}, + {tls.SHA256, testdata.EcdsaPublicKeyPEM + "junk", "extra data"}, + } + for _, test := range tests { + opts := Options{Options: jsonclient.Options{PublicKey: test.pubKey}, hashAlgo: test.algo} + got, err := New("http://localhost", nil, opts) + if test.errstr != "" { + if err == nil { + t.Errorf("clientv2.New(%q, %d)=%T, nil; want error %q", test.pubKey, test.algo, got, test.errstr) + } else if !strings.Contains(err.Error(), test.errstr) { + t.Errorf("clientv2.New(%q, %d)=nil,%q; want error %q", test.pubKey, test.algo, err.Error(), test.errstr) + } + continue + } + if err != nil { + t.Errorf("clientv2.New(%d)=nil,%q; want error nil", test.pubKey, test.algo, err.Error()) + } + } +} + +func TestCheckSCTExtensions(t *testing.T) { + var tests = []struct { + exts []ct.SCTExtension + errstr string + }{ + { + exts: []ct.SCTExtension{ + ct.SCTExtension{SCTExtensionType: 1}, + ct.SCTExtension{SCTExtensionType: 2}, + ct.SCTExtension{SCTExtensionType: 4}, + }, + }, + { + exts: []ct.SCTExtension{ + ct.SCTExtension{SCTExtensionType: 1}, + ct.SCTExtension{SCTExtensionType: 4}, + ct.SCTExtension{SCTExtensionType: 2}, + }, + errstr: "not ordered correctly", + }, + { + exts: []ct.SCTExtension{ + ct.SCTExtension{SCTExtensionType: 1}, + ct.SCTExtension{SCTExtensionType: 2}, + ct.SCTExtension{SCTExtensionType: 2}, + }, + errstr: "not ordered correctly", + }, + } + for _, test := range tests { + err := checkSCTExtensions(test.exts) + if test.errstr != "" { + if err == nil { + t.Errorf("checkSCTExtensions(%+v)=nil; want error %q", test.exts, test.errstr) + } else if !strings.Contains(err.Error(), test.errstr) { + t.Errorf("checkSCTExtensions(%+v)=%q; want error %q", test.exts, err.Error(), test.errstr) + } + continue + } + if err != nil { + t.Errorf("checkSCTExtensions(%+v)=%q; want nil", test.exts, err.Error()) + } + } +} + +func TestGetSTH(t *testing.T) { + makeSTH := func(s string) string { + return fmt.Sprintf(`{"sth":"%s"}`, b64(s)) + } + tests := []struct { + rsp string + want *ct.SignedTreeHeadDataV2 + noverify bool + errstr string + }{ + {rsp: `{"sth":"not b64"}`, errstr: "illegal base64 data"}, + {rsp: `Not JSON data`, errstr: "invalid character"}, + {rsp: `{"sth-key":"missing"}`, errstr: "unexpected"}, + {rsp: makeSTH("61626364"), errstr: "syntax error"}, + {rsp: makeSTH("0005" + "022a03" + ("1122334455667788" + "0000000000000100" + "02cafe" + "0000") + "04030047" + testdata.EcdsaSignedAbcdHex), errstr: "failed to verify STH signature"}, + { + rsp: makeSTH("0005" + "022a03" + ("1122334455667788" + "0000000000000100" + "02cafe" + "0000") + "04030047" + testdata.EcdsaSignedAbcdHex), + noverify: true, + want: &ct.SignedTreeHeadDataV2{ + LogID: []byte{0x2a, 0x03}, + TreeHead: ct.TreeHeadDataV2{ + Timestamp: 0x1122334455667788, + TreeSize: 0x0100, + RootHash: ct.NodeHash{Value: []byte{0xca, 0xfe}}, + STHExtensions: []ct.STHExtension{}, + }, + Signature: tls.DigitallySigned{ + Algorithm: tls.SignatureAndHashAlgorithm{ + Hash: tls.SHA256, + Signature: tls.ECDSA}, + Signature: testdata.FromHex(testdata.EcdsaSignedAbcdHex), + }, + }, + }, + } + + for _, test := range tests { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/ct/v2/get-sth" { + t.Fatalf("Incorrect URL path: %s", r.URL.Path) + } + fmt.Fprint(w, test.rsp) + })) + defer ts.Close() + + opts := Options{hashAlgo: tls.SHA256} + if !test.noverify { + opts.Options.PublicKey = testdata.EcdsaPublicKeyPEM + } + client, _ := New(ts.URL, nil, opts) + got, err := client.GetSTH(context.Background()) + + if test.errstr != "" { + if err == nil { + t.Errorf("GetSTH()=%+v,nil; want error %q", got, test.errstr) + } else if !strings.Contains(err.Error(), test.errstr) { + t.Errorf("GetSTH()=nil,%q; want error %q", err.Error(), test.errstr) + } + continue + } + if err != nil { + t.Errorf("GetSTH()=nil,%q; want %+v", err.Error(), test.want) + } else if !reflect.DeepEqual(test.want, got) { + t.Errorf("GetSTH()=%+v,nil; want %+v", got, test.want) + } + + } +} + +var dehex = testdata.FromHex + +func b64(hexData string) string { + return base64.StdEncoding.EncodeToString(dehex(hexData)) +}