Chromium Code Reviews| Index: net/cert/ct_objects_extractor_nss.cc |
| diff --git a/net/cert/ct_objects_extractor_nss.cc b/net/cert/ct_objects_extractor_nss.cc |
| index 0f353489e3d33a585dca01befc6006ff00ef28bd..f80ad2f3ab221521ba9214fbe11a20c31cff3b29 100644 |
| --- a/net/cert/ct_objects_extractor_nss.cc |
| +++ b/net/cert/ct_objects_extractor_nss.cc |
| @@ -10,6 +10,7 @@ |
| #include <secoid.h> |
| #include "base/lazy_instance.h" |
| +#include "base/sha1.h" |
| #include "crypto/scoped_nss_types.h" |
| #include "crypto/sha2.h" |
| #include "net/cert/asn1_util.h" |
| @@ -22,6 +23,12 @@ namespace ct { |
| namespace { |
| +// NSS black magic to get the address of externally defined template at runtime. |
| +SEC_ASN1_MKSUB(SEC_IntegerTemplate) |
| +SEC_ASN1_MKSUB(SECOID_AlgorithmIDTemplate) |
| +SEC_ASN1_MKSUB(SEC_GeneralizedTimeTemplate) |
| +SEC_ASN1_MKSUB(CERT_SequenceOfCertExtensionTemplate) |
| + |
| // Wrapper class to convert a X509Certificate::OSCertHandle directly |
| // into a CERTCertificate* usable with other NSS functions. This is used for |
| // platforms where X509Certificate::OSCertHandle refers to a different type |
| @@ -62,6 +69,27 @@ const char kEmbeddedSCTDescription[] = |
| "X.509v3 Certificate Transparency Embedded Signed Certificate Timestamp " |
| "List"; |
| +// The wire form of the OID 1.3.6.1.4.1.11129.2.4.5 - OCSP SingleExtension for |
| +// X.509v3 Certificate Transparency Signed Certificate Timestamp List, see |
| +// Section 3.3 of RFC6962. |
| +const unsigned char kOCSPExtensionOid[] = {0x2B, 0x06, 0x01, 0x04, 0x01, |
| + 0xD6, 0x79, 0x02, 0x04, 0x05}; |
| + |
| +const SECItem kOCSPExtensionOidItem = { |
| + siBuffer, const_cast<unsigned char*>(kOCSPExtensionOid), |
| + sizeof(kOCSPExtensionOid) |
| +}; |
| + |
| +// id-ad-ocsp: 1.3.6.1.5.5.7.48.1.1 |
| +const unsigned char kBasicOCSPResponseOid[] = {0x2B, 0x06, 0x01, 0x05, 0x05, |
| + 0x07, 0x30, 0x01, 0x01}; |
| + |
| +const SECItem kBasicOCSPResponseOidItem = { |
| + siBuffer, const_cast<unsigned char*>(kBasicOCSPResponseOid), |
| + sizeof(kBasicOCSPResponseOid) |
| +}; |
| + |
| + |
| // Initializes the necessary NSS internals for use with Certificate |
| // Transparency. |
| class CTInitSingleton { |
| @@ -108,12 +136,12 @@ base::LazyInstance<CTInitSingleton>::Leaky g_ct_singleton = |
| LAZY_INSTANCE_INITIALIZER; |
| // Obtains the data for an X.509v3 certificate extension identified by |oid| |
| -// and encoded as an OCTET STRING. Returns true if the extension was found, |
| -// updating |ext_data| to be the extension data after removing the DER |
| -// encoding of OCTET STRING. |
| -bool GetOctetStringExtension(CERTCertificate* cert, |
| - SECOidTag oid, |
| - std::string* extension_data) { |
| +// and encoded as an OCTET STRING. Returns true if the extension was found in |
| +// the certificate, updating |ext_data| to be the extension data after removing |
| +// the DER encoding of OCTET STRING. |
| +bool GetCertOctetStringExtension(CERTCertificate* cert, |
| + SECOidTag oid, |
| + std::string* extension_data) { |
| SECItem extension; |
| SECStatus rv = CERT_FindCertExtension(cert, oid, &extension); |
| if (rv != SECSuccess) |
| @@ -133,6 +161,48 @@ bool GetOctetStringExtension(CERTCertificate* cert, |
| return rv == SECSuccess; |
| } |
| +// NSS offers CERT_FindCertExtension for certificates, but that only accepts |
| +// CERTCertificate* inputs, so the method below extracts the SCT extension |
| +// directly from the CERTCertExtension** of an OCSP response. |
| +// |
| +// Obtains the data for an OCSP extension identified by kOCSPExtensionOidItem |
| +// and encoded as an OCTET STRING. Returns true if the extension was found in |
| +// |extensions|, updating |extension_data| to be the extension data after |
| +// removing the DER encoding of OCTET STRING. |
| +bool GetSCTListFromOCSPExtension(PLArenaPool* arena, |
| + const CERTCertExtension* const* extensions, |
| + std::string* extension_data) { |
| + if (!extensions) |
| + return false; |
| + |
| + const CERTCertExtension* match = NULL; |
| + |
| + for (const CERTCertExtension* const* exts = extensions; *exts; ++exts) { |
| + const CERTCertExtension* ext = *exts; |
| + if (SECITEM_ItemsAreEqual(&kOCSPExtensionOidItem, &ext->id)) { |
| + match = ext; |
| + break; |
| + } |
| + } |
| + |
| + if (!match) |
| + return false; |
| + |
| + SECItem contents; |
| + // SEC_QuickDERDecodeItem sets |contents| to point to |match|, so it is not |
| + // necessary to free the contents of |contents|. |
| + SECStatus rv = SEC_QuickDERDecodeItem(arena, &contents, |
| + SEC_ASN1_GET(SEC_OctetStringTemplate), |
| + &match->value); |
| + if (rv != SECSuccess) |
| + return false; |
| + |
| + base::StringPiece parsed_data(reinterpret_cast<char*>(contents.data), |
| + contents.len); |
| + parsed_data.CopyToString(extension_data); |
| + return true; |
| +} |
| + |
| // Given a |cert|, extract the TBSCertificate from this certificate, also |
| // removing the X.509 extension with OID 1.3.6.1.4.1.11129.2.4.2 (that is, |
| // the embedded SCT) |
| @@ -180,6 +250,179 @@ bool ExtractTBSCertWithoutSCTs(CERTCertificate* cert, |
| return true; |
| } |
| +// The following code is adapted from the NSS OCSP module, in order to expose |
| +// the internal structure of an OCSP response. |
| + |
| +// ResponseBytes ::= SEQUENCE { |
| +// responseType OBJECT IDENTIFIER, |
| +// response OCTET STRING } |
| +struct ResponseBytes { |
| + SECItem response_type; |
| + SECItem der_response; |
| +}; |
| + |
| +const SEC_ASN1Template kResponseBytesTemplate[] = { |
| + { SEC_ASN1_SEQUENCE, 0, NULL, sizeof(ResponseBytes) }, |
| + { SEC_ASN1_OBJECT_ID, offsetof(ResponseBytes, response_type) }, |
| + { SEC_ASN1_OCTET_STRING, offsetof(ResponseBytes, der_response) }, |
| + { 0 } |
| +}; |
| + |
| +// OCSPResponse ::= SEQUENCE { |
| +// responseStatus OCSPResponseStatus, |
| +// responseBytes [0] EXPLICIT ResponseBytes OPTIONAL } |
| +struct OCSPResponse { |
| + SECItem response_status; |
| + // This indirection is needed because |response_bytes| is an optional |
| + // component and we need a way to determine if it is missing. |
| + ResponseBytes* response_bytes; |
| +}; |
| + |
| +const SEC_ASN1Template kPointerToResponseBytesTemplate[] = { |
| + { SEC_ASN1_POINTER, 0, kResponseBytesTemplate } |
| +}; |
| + |
| +const SEC_ASN1Template kOCSPResponseTemplate[] = { |
| + { SEC_ASN1_SEQUENCE, 0, NULL, sizeof(OCSPResponse) }, |
| + { SEC_ASN1_ENUMERATED, offsetof(OCSPResponse, response_status) }, |
| + { SEC_ASN1_OPTIONAL | SEC_ASN1_EXPLICIT | SEC_ASN1_CONSTRUCTED | |
| + SEC_ASN1_CONTEXT_SPECIFIC | 0, offsetof(OCSPResponse, response_bytes), |
| + kPointerToResponseBytesTemplate }, |
| + { 0 } |
| +}; |
| + |
| +// CertID ::= SEQUENCE { |
| +// hashAlgorithm AlgorithmIdentifier, |
| +// issuerNameHash OCTET STRING, -- Hash of Issuer's DN |
| +// issuerKeyHash OCTET STRING, -- Hash of Issuers public key |
| +// serialNumber CertificateSerialNumber } |
| +struct CertID { |
| + SECAlgorithmID hash_algorithm; |
| + SECItem issuer_name_hash; |
| + SECItem issuer_key_hash; |
| + SECItem serial_number; |
| +}; |
| + |
| +const SEC_ASN1Template kCertIDTemplate[] = { |
| + { SEC_ASN1_SEQUENCE, 0, NULL, sizeof(CertID) }, |
| + { SEC_ASN1_INLINE | SEC_ASN1_XTRN, offsetof(CertID, hash_algorithm), |
| + SEC_ASN1_SUB(SECOID_AlgorithmIDTemplate) }, |
| + { SEC_ASN1_OCTET_STRING, offsetof(CertID, issuer_name_hash) }, |
| + { SEC_ASN1_OCTET_STRING, offsetof(CertID, issuer_key_hash) }, |
| + { SEC_ASN1_INTEGER, offsetof(CertID, serial_number) }, |
| + { 0 } |
| +}; |
| + |
| +// SingleResponse ::= SEQUENCE { |
| +// certID CertID, |
| +// certStatus CertStatus, |
| +// thisUpdate GeneralizedTime, |
| +// nextUpdate [0] EXPLICIT GeneralizedTime OPTIONAL, |
| +// singleExtensions [1] EXPLICIT Extensions OPTIONAL } |
| +struct SingleResponse { |
| + CertID cert_id; |
| + // The following three fields are not used. |
| + SECItem der_cert_status; |
| + SECItem this_update; |
| + SECItem next_update; |
| + CERTCertExtension** single_extensions; |
| +}; |
| + |
| +const SEC_ASN1Template kSingleResponseTemplate[] = { |
| + { SEC_ASN1_SEQUENCE, 0, NULL, sizeof(SingleResponse) }, |
| + { SEC_ASN1_INLINE, offsetof(SingleResponse, cert_id), kCertIDTemplate }, |
| + // Really a CHOICE but we make it an ANY because we don't care about the |
| + // contents of this field. |
| + // TODO(ekasper): use SEC_ASN1_CHOICE. |
| + { SEC_ASN1_ANY, offsetof(SingleResponse, der_cert_status) }, |
| + { SEC_ASN1_GENERALIZED_TIME, offsetof(SingleResponse, this_update) }, |
| + { SEC_ASN1_OPTIONAL | SEC_ASN1_EXPLICIT | |
| + SEC_ASN1_CONSTRUCTED | SEC_ASN1_CONTEXT_SPECIFIC | SEC_ASN1_XTRN | 0, |
| + offsetof(SingleResponse, next_update), |
| + SEC_ASN1_SUB(SEC_GeneralizedTimeTemplate) }, |
| + { SEC_ASN1_OPTIONAL | SEC_ASN1_EXPLICIT | SEC_ASN1_CONSTRUCTED | |
| + SEC_ASN1_CONTEXT_SPECIFIC | SEC_ASN1_XTRN | 1, |
| + offsetof(SingleResponse, single_extensions), |
| + SEC_ASN1_SUB(CERT_SequenceOfCertExtensionTemplate) }, |
| + { 0 } |
| +}; |
| + |
| +// ResponseData ::= SEQUENCE { |
| +// version [0] EXPLICIT Version DEFAULT v1, |
| +// responderID ResponderID, |
| +// producedAt GeneralizedTime, |
| +// responses SEQUENCE OF SingleResponse, |
| +// responseExtensions [1] EXPLICIT Extensions OPTIONAL } |
| +struct ResponseData { |
| + // The first three fields are not used. |
| + SECItem version; |
| + SECItem der_responder_id; |
| + SECItem produced_at; |
| + SingleResponse** single_responses; |
| + // Skip extensions. |
| +}; |
| + |
| +const SEC_ASN1Template kResponseDataTemplate[] = { |
| + { SEC_ASN1_SEQUENCE, 0, NULL, sizeof(ResponseData) }, |
| + { SEC_ASN1_OPTIONAL | SEC_ASN1_EXPLICIT | SEC_ASN1_CONSTRUCTED | |
| + SEC_ASN1_CONTEXT_SPECIFIC | SEC_ASN1_XTRN | 0, |
| + offsetof(ResponseData, version), SEC_ASN1_SUB(SEC_IntegerTemplate) }, |
| + // Really a CHOICE but we make it an ANY because we don't care about the |
| + // contents of this field. |
| + // TODO(ekasper): use SEC_ASN1_CHOICE. |
| + { SEC_ASN1_ANY, offsetof(ResponseData, der_responder_id) }, |
| + { SEC_ASN1_GENERALIZED_TIME, offsetof(ResponseData, produced_at) }, |
| + { SEC_ASN1_SEQUENCE_OF, offsetof(ResponseData, single_responses), |
| + kSingleResponseTemplate }, |
| + { SEC_ASN1_SKIP_REST }, |
| + { 0 } |
| +}; |
| + |
| +// BasicOCSPResponse ::= SEQUENCE { |
| +// tbsResponseData ResponseData, |
| +// signatureAlgorithm AlgorithmIdentifier, |
| +// signature BIT STRING, |
| +// certs [0] EXPLICIT SEQUENCE OF Certificate OPTIONAL } |
| +struct BasicOCSPResponse { |
| + ResponseData tbs_response_data; |
| + // We do not care about the rest. |
| +}; |
| + |
| +const SEC_ASN1Template kBasicOCSPResponseTemplate[] = { |
| + { SEC_ASN1_SEQUENCE, 0, NULL, sizeof(BasicOCSPResponse) }, |
| + { SEC_ASN1_INLINE, offsetof(BasicOCSPResponse, tbs_response_data), |
| + kResponseDataTemplate }, |
| + { SEC_ASN1_SKIP_REST }, |
| + { 0 } |
| +}; |
| + |
| +bool StringEqualToSECItem(const std::string& value1, const SECItem& value2) { |
| + if (value1.size() != value2.len) |
| + return false; |
| + return memcmp(value1.data(), value2.data, value2.len) == 0; |
| +} |
| + |
| +// TODO(ekasper): also use the issuer name hash in matching. |
| +bool CertIDMatches(const CertID& cert_id, |
| + const std::string& serial_number, |
| + const std::string& issuer_key_sha1_hash, |
| + const std::string& issuer_key_sha256_hash) { |
| + if (!StringEqualToSECItem(serial_number, cert_id.serial_number)) |
| + return false; |
| + |
| + SECOidTag hash_alg = SECOID_FindOIDTag(&cert_id.hash_algorithm.algorithm); |
| + switch (hash_alg) { |
| + case SEC_OID_SHA1: |
| + return StringEqualToSECItem(issuer_key_sha1_hash, |
| + cert_id.issuer_key_hash); |
| + case SEC_OID_SHA256: |
| + return StringEqualToSECItem(issuer_key_sha256_hash, |
| + cert_id.issuer_key_hash); |
| + default: |
| + return false; |
| + } |
| +} |
| + |
| } // namespace |
| bool ExtractEmbeddedSCTList(X509Certificate::OSCertHandle cert, |
| @@ -190,8 +433,9 @@ bool ExtractEmbeddedSCTList(X509Certificate::OSCertHandle cert, |
| if (!leaf_cert.cert) |
| return false; |
| - return GetOctetStringExtension( |
| - leaf_cert.cert.get(), g_ct_singleton.Get().embedded_oid(), sct_list); |
| + return GetCertOctetStringExtension(leaf_cert.cert.get(), |
| + g_ct_singleton.Get().embedded_oid(), |
| + sct_list); |
| } |
| bool GetPrecertLogEntry(X509Certificate::OSCertHandle leaf, |
| @@ -269,6 +513,97 @@ bool GetX509LogEntry(X509Certificate::OSCertHandle leaf, LogEntry* result) { |
| return true; |
| } |
| +bool ExtractSCTListFromOCSPResponse(X509Certificate::OSCertHandle issuer, |
| + const std::string& cert_serial_number, |
| + const std::string& ocsp_response, |
| + std::string* sct_list) { |
| + DCHECK(issuer); |
| + crypto::ScopedPLArenaPool arena(PORT_NewArena(DER_DEFAULT_CHUNKSIZE)); |
| + |
| + OCSPResponse response; |
| + memset(&response, 0, sizeof(response)); |
| + |
| + SECItem src = { siBuffer, |
| + reinterpret_cast<unsigned char*>(const_cast<char*>( |
| + ocsp_response.data())), ocsp_response.size() }; |
|
wtc
2013/12/15 05:31:14
Emilia: you can add a static_cast<unsigned int> to
ekasper
2013/12/16 12:35:42
We know that we're safe given the source of those
|
| + |
| + // |response| will point directly into |src|, so it's not necessary to |
| + // free the |response| contents, but they may only be used while |src| |
| + // is valid (i.e., in this method). |
| + SECStatus rv = SEC_QuickDERDecodeItem(arena.get(), &response, |
| + kOCSPResponseTemplate, &src); |
| + if (rv != SECSuccess) |
| + return false; |
| + |
| + if (!response.response_bytes) |
| + return false; |
| + |
| + if (!SECITEM_ItemsAreEqual(&kBasicOCSPResponseOidItem, |
| + &response.response_bytes->response_type)) { |
| + return false; |
| + } |
| + |
| + BasicOCSPResponse basic_response; |
| + memset(&basic_response, 0, sizeof(basic_response)); |
| + |
| + rv = SEC_QuickDERDecodeItem(arena.get(), &basic_response, |
| + kBasicOCSPResponseTemplate, |
| + &response.response_bytes->der_response); |
| + if (rv != SECSuccess) |
| + return false; |
| + |
| + SingleResponse** responses = |
| + basic_response.tbs_response_data.single_responses; |
| + if (!responses) |
| + return false; |
| + |
| + std::string issuer_der; |
| + if (!X509Certificate::GetDEREncoded(issuer, &issuer_der)) |
| + return false; |
| + |
| + base::StringPiece issuer_spki; |
| + if (!asn1::ExtractSPKIFromDERCert(issuer_der, &issuer_spki)) |
| + return false; |
| + |
| + // In OCSP, only the key itself is under hash. |
| + base::StringPiece issuer_spk; |
| + if (!asn1::ExtractSubjectPublicKeyFromSPKI(issuer_spki, &issuer_spk)) |
| + return false; |
| + |
| + // ExtractSubjectPublicKey... does not remove the initial octet encoding the |
| + // number of unused bits in the ASN.1 BIT STRING so we do it here. For public |
| + // keys, the bitstring is in practice always byte-aligned. |
| + if (issuer_spk.empty() || issuer_spk[0] != 0) |
| + return false; |
| + issuer_spk.remove_prefix(1); |
| + |
| + // NSS OCSP lib recognizes SHA1, MD5 and MD2; MD5 and MD2 are dead but |
| + // https://bugzilla.mozilla.org/show_bug.cgi?id=663315 will add SHA-256 |
| + // and SHA-384. |
| + // TODO(ekasper): add SHA-384 to crypto/sha2.h and here if it proves |
| + // necessary. |
| + // TODO(ekasper): only compute the hashes on demand. |
| + std::string issuer_key_sha256_hash = crypto::SHA256HashString(issuer_spk); |
| + std::string issuer_key_sha1_hash = base::SHA1HashString( |
| + issuer_spk.as_string()); |
| + |
| + const SingleResponse* match = NULL; |
| + for (const SingleResponse* const* resps = responses; *resps; ++resps) { |
| + const SingleResponse* resp = *resps; |
| + if (CertIDMatches(resp->cert_id, cert_serial_number, |
| + issuer_key_sha1_hash, issuer_key_sha256_hash)) { |
| + match = resp; |
| + break; |
| + } |
| + } |
| + |
| + if (!match) |
| + return false; |
| + |
| + return GetSCTListFromOCSPExtension(arena.get(), match->single_extensions, |
| + sct_list); |
| +} |
| + |
| } // namespace ct |
| } // namespace net |