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..c29b34c10c63aba50acc8850d754412947fc7c76 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,105 @@ 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); |
+ |
+ // Any OCSP response is unlikely to be even close to 2^24 bytes; further, CT |
+ // only uses stapled OCSP responses which have this limit imposed by the TLS |
+ // protocol. |
+ if (ocsp_response.size() > 0xffffff) |
+ return false; |
+ |
+ 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())), |
+ static_cast<unsigned int>(ocsp_response.size()) }; |
+ |
+ // |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 |