diff options
author | agl@chromium.org <agl@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-04-12 16:26:26 +0000 |
---|---|---|
committer | agl@chromium.org <agl@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-04-12 16:26:26 +0000 |
commit | 2a89c4c490dc98307f502d698d7d738371a1b5c6 (patch) | |
tree | 865a7fd7300fa5b7eadba519c36357f576f8baa7 /net/base | |
parent | 60f36f9c86ca3e2203eadaf0db2dc4f3d7382c6b (diff) | |
download | chromium_src-2a89c4c490dc98307f502d698d7d738371a1b5c6.zip chromium_src-2a89c4c490dc98307f502d698d7d738371a1b5c6.tar.gz chromium_src-2a89c4c490dc98307f502d698d7d738371a1b5c6.tar.bz2 |
net: add support for checking if a known public key is in a chain.
This is intended for use with future support for HSTS or CAA pinning of
certificate chains. Since we build certificate chains from a pool of
certificates, and certificates get reissued with updated expiries etc, only the
public keys in the chain will always match. Thus we match on SHA1 hashes of
SubjectPublicKeyInfos.
BUG=none
TEST=net_unittests
Review URL: http://codereview.chromium.org/6821010
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@81259 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'net/base')
-rw-r--r-- | net/base/asn1_util.cc | 127 | ||||
-rw-r--r-- | net/base/asn1_util.h | 55 | ||||
-rw-r--r-- | net/base/cert_verify_result.h | 10 | ||||
-rw-r--r-- | net/base/x509_certificate.h | 1 | ||||
-rw-r--r-- | net/base/x509_certificate_mac.cc | 24 | ||||
-rw-r--r-- | net/base/x509_certificate_nss.cc | 23 | ||||
-rw-r--r-- | net/base/x509_certificate_unittest.cc | 68 | ||||
-rw-r--r-- | net/base/x509_certificate_win.cc | 28 |
8 files changed, 335 insertions, 1 deletions
diff --git a/net/base/asn1_util.cc b/net/base/asn1_util.cc new file mode 100644 index 0000000..701f53f --- /dev/null +++ b/net/base/asn1_util.cc @@ -0,0 +1,127 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "net/base/asn1_util.h" + +namespace net { + +namespace asn1 { + +bool ParseElement(base::StringPiece* in, + unsigned tag_value, + base::StringPiece* out, + unsigned *out_header_len) { + const uint8* data = reinterpret_cast<const uint8*>(in->data()); + + if (in->size() < 2) + return false; + + if (static_cast<unsigned char>(data[0]) != tag_value) + return false; + + size_t len = 0; + if ((data[1] & 0x80) == 0) { + // short form length + if (out_header_len) + *out_header_len = 2; + len = static_cast<size_t>(data[1]) + 2; + } else { + // long form length + const unsigned num_bytes = data[1] & 0x7f; + if (num_bytes == 0 || num_bytes > 2) + return false; + if (in->size() < 2 + num_bytes) + return false; + len = data[2]; + if (num_bytes == 2) { + if (len == 0) { + // the length encoding must be minimal. + return false; + } + len <<= 8; + len += data[3]; + } + if (len < 128) { + // the length should have been encoded in short form. This distinguishes + // DER from BER encoding. + return false; + } + if (out_header_len) + *out_header_len = 2 + num_bytes; + len += 2 + num_bytes; + } + + if (in->size() < len) + return false; + if (out) + *out = base::StringPiece(in->data(), len); + in->remove_prefix(len); + return true; +} + +bool GetElement(base::StringPiece* in, + unsigned tag_value, + base::StringPiece* out) { + unsigned header_len; + if (!ParseElement(in, tag_value, out, &header_len)) + return false; + if (out) + out->remove_prefix(header_len); + return true; +} + +bool ExtractSPKIFromDERCert(base::StringPiece cert, + base::StringPiece* spki_out) { + // From RFC 5280, section 4.1 + // Certificate ::= SEQUENCE { + // tbsCertificate TBSCertificate, + // signatureAlgorithm AlgorithmIdentifier, + // signatureValue BIT STRING } + + // TBSCertificate ::= SEQUENCE { + // version [0] EXPLICIT Version DEFAULT v1, + // serialNumber CertificateSerialNumber, + // signature AlgorithmIdentifier, + // issuer Name, + // validity Validity, + // subject Name, + // subjectPublicKeyInfo SubjectPublicKeyInfo, + + base::StringPiece certificate; + if (!asn1::GetElement(&cert, asn1::kSEQUENCE, &certificate)) + return false; + + base::StringPiece tbs_certificate; + if (!asn1::GetElement(&certificate, asn1::kSEQUENCE, &tbs_certificate)) + return false; + + // The version is optional, so a failure to parse it is fine. + asn1::GetElement(&tbs_certificate, + asn1::kCompound | asn1::kContextSpecific | 0, + NULL); + + // serialNumber + if (!asn1::GetElement(&tbs_certificate, asn1::kINTEGER, NULL)) + return false; + // signature + if (!asn1::GetElement(&tbs_certificate, asn1::kSEQUENCE, NULL)) + return false; + // issuer + if (!asn1::GetElement(&tbs_certificate, asn1::kSEQUENCE, NULL)) + return false; + // validity + if (!asn1::GetElement(&tbs_certificate, asn1::kSEQUENCE, NULL)) + return false; + // subject + if (!asn1::GetElement(&tbs_certificate, asn1::kSEQUENCE, NULL)) + return false; + // subjectPublicKeyInfo + if (!asn1::ParseElement(&tbs_certificate, asn1::kSEQUENCE, spki_out, NULL)) + return false; + return true; +} + +} // namespace asn1 + +} // namespace net diff --git a/net/base/asn1_util.h b/net/base/asn1_util.h new file mode 100644 index 0000000..507e79c --- /dev/null +++ b/net/base/asn1_util.h @@ -0,0 +1,55 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef NET_BASE_ASN1_UTIL_H_ +#define NET_BASE_ASN1_UTIL_H_ +#pragma once + +#include "base/string_piece.h" + +namespace net { + +namespace asn1 { + +// These are the DER encodings of the tag byte for ASN.1 objects. +static const unsigned kINTEGER = 0x02; +static const unsigned kOID = 0x06; +static const unsigned kSEQUENCE = 0x30; + +// These are flags that can be ORed with the above tag numbers. +static const unsigned kContextSpecific = 0x80; +static const unsigned kCompound = 0x20; + +// ParseElement parses a DER encoded ASN1 element from |in|, requiring that +// it have the given |tag_value|. It returns true on success. The following +// limitations are imposed: +// 1) tag numbers > 31 are not permitted. +// 2) lengths > 65535 are not permitted. +// On successful return: +// |in| is advanced over the element +// |out| contains the element, including the tag and length bytes. +// |out_header_len| contains the length of the tag and length bytes in |out|. +bool ParseElement(base::StringPiece* in, + unsigned tag_value, + base::StringPiece* out, + unsigned *out_header_len); + +// GetElement performs the same actions as ParseElement, except that the header +// bytes are not included in the output. +bool GetElement(base::StringPiece* in, + unsigned tag_value, + base::StringPiece* out); + + +// ExtractSPKIFromDERCert parses the DER encoded certificate in |cert| and +// extracts the bytes of the SubjectPublicKeyInfo. On successful return, +// |spki_out| is set to contain the SPKI, pointing into |cert|. +bool ExtractSPKIFromDERCert(base::StringPiece cert, + base::StringPiece* spki_out); + +} // namespace asn1 + +} // namespace net + +#endif // NET_BASE_ASN1_UTIL_H_ diff --git a/net/base/cert_verify_result.h b/net/base/cert_verify_result.h index 2939c3a..96b50e2 100644 --- a/net/base/cert_verify_result.h +++ b/net/base/cert_verify_result.h @@ -6,6 +6,10 @@ #define NET_BASE_CERT_VERIFY_RESULT_H_ #pragma once +#include <vector> + +#include "net/base/x509_cert_types.h" + namespace net { // The result of certificate verification. Eventually this may contain the @@ -22,6 +26,7 @@ class CertVerifyResult { has_md5_ca = false; has_md2_ca = false; is_issued_by_known_root = false; + public_key_hashes.clear(); } // Bitmask of CERT_STATUS_* from net/base/cert_status_flags.h @@ -34,6 +39,11 @@ class CertVerifyResult { bool has_md5_ca; bool has_md2_ca; + // If the certificate was successfully verified then this contains the SHA1 + // fingerprints of the SubjectPublicKeyInfos of the chain. The fingerprint + // from the leaf certificate will be the first element of the vector. + std::vector<SHA1Fingerprint> public_key_hashes; + // is_issued_by_known_root is true if we recognise the root CA as a standard // root. If it isn't then it's probably the case that this certificate was // generated by a MITM proxy whose root has been installed locally. This is diff --git a/net/base/x509_certificate.h b/net/base/x509_certificate.h index 986f867..a6896e9 100644 --- a/net/base/x509_certificate.h +++ b/net/base/x509_certificate.h @@ -37,6 +37,7 @@ struct CERTCertificateStr; class Pickle; namespace base { +class StringPiece; class RSAPrivateKey; } // namespace base diff --git a/net/base/x509_certificate_mac.cc b/net/base/x509_certificate_mac.cc index fd3f665..3156e56 100644 --- a/net/base/x509_certificate_mac.cc +++ b/net/base/x509_certificate_mac.cc @@ -20,6 +20,7 @@ #include "base/pickle.h" #include "base/sha1.h" #include "base/sys_string_conversions.h" +#include "net/base/asn1_util.h" #include "net/base/cert_status_flags.h" #include "net/base/cert_verify_result.h" #include "net/base/net_errors.h" @@ -495,6 +496,28 @@ private: CSSM_TP_RESULT_SET* results_; }; +void AppendPublicKeyHashes(CFArrayRef chain, + std::vector<SHA1Fingerprint>* hashes) { + const CFIndex n = CFArrayGetCount(chain); + for (CFIndex i = 0; i < n; i++) { + SecCertificateRef cert = reinterpret_cast<SecCertificateRef>( + const_cast<void*>(CFArrayGetValueAtIndex(chain, i))); + + CSSM_DATA cert_data; + OSStatus err = SecCertificateGetData(cert, &cert_data); + DCHECK_EQ(err, noErr); + base::StringPiece der_bytes(reinterpret_cast<const char*>(cert_data.Data), + cert_data.Length); + base::StringPiece spki_bytes; + if (!asn1::ExtractSPKIFromDERCert(der_bytes, &spki_bytes)) + continue; + + SHA1Fingerprint hash; + CC_SHA1(spki_bytes.data(), spki_bytes.size(), hash.data); + hashes->push_back(hash); + } +} + } // namespace void X509Certificate::Initialize() { @@ -924,6 +947,7 @@ int X509Certificate::Verify(const std::string& hostname, int flags, } } + AppendPublicKeyHashes(completed_chain, &verify_result->public_key_hashes); verify_result->is_issued_by_known_root = IsIssuedByKnownRoot(completed_chain); return OK; diff --git a/net/base/x509_certificate_nss.cc b/net/base/x509_certificate_nss.cc index a14690a..4b39f9e 100644 --- a/net/base/x509_certificate_nss.cc +++ b/net/base/x509_certificate_nss.cc @@ -611,6 +611,25 @@ CollectCertsCallback(void* arg, SECItem** certs, int num_certs) { return SECSuccess; } +SHA1Fingerprint CertPublicKeyHash(CERTCertificate* cert) { + SHA1Fingerprint hash; + SECStatus rv = HASH_HashBuf(HASH_AlgSHA1, hash.data, + cert->derPublicKey.data, cert->derPublicKey.len); + DCHECK_EQ(rv, SECSuccess); + return hash; +} + +void AppendPublicKeyHashes(CERTCertList* cert_list, + CERTCertificate* root_cert, + std::vector<SHA1Fingerprint>* hashes) { + for (CERTCertListNode* node = CERT_LIST_HEAD(cert_list); + !CERT_LIST_END(node, cert_list); + node = CERT_LIST_NEXT(node)) { + hashes->push_back(CertPublicKeyHash(node->cert)); + } + hashes->push_back(CertPublicKeyHash(root_cert)); +} + } // namespace void X509Certificate::Initialize() { @@ -823,6 +842,10 @@ int X509Certificate::Verify(const std::string& hostname, if (IsCertStatusError(verify_result->cert_status)) return MapCertStatusToNetError(verify_result->cert_status); + AppendPublicKeyHashes(cvout[cvout_cert_list_index].value.pointer.chain, + cvout[cvout_trust_anchor_index].value.pointer.cert, + &verify_result->public_key_hashes); + verify_result->is_issued_by_known_root = IsKnownRoot(cvout[cvout_trust_anchor_index].value.pointer.cert); diff --git a/net/base/x509_certificate_unittest.cc b/net/base/x509_certificate_unittest.cc index c7dd8fb..47a931a 100644 --- a/net/base/x509_certificate_unittest.cc +++ b/net/base/x509_certificate_unittest.cc @@ -7,7 +7,9 @@ #include "base/file_util.h" #include "base/path_service.h" #include "base/pickle.h" +#include "base/sha1.h" #include "base/string_split.h" +#include "net/base/asn1_util.h" #include "net/base/cert_status_flags.h" #include "net/base/cert_test_util.h" #include "net/base/cert_verify_result.h" @@ -503,13 +505,77 @@ TEST(X509CertificateTest, TestKnownRoot) { int flags = 0; CertVerifyResult verify_result; // This is going to blow up in Feb 2012. Sorry! Disable and file a bug - // against agl. + // against agl. Also see PublicKeyHashes in this file. int error = cert_chain->Verify("www.nist.gov", flags, &verify_result); EXPECT_EQ(OK, error); EXPECT_EQ(0, verify_result.cert_status); EXPECT_TRUE(verify_result.is_issued_by_known_root); } +// This is the SHA1 hash of the SubjectPublicKeyInfo of nist.der. +static const char nistSPKIHash[] = + "\x15\x60\xde\x65\x4e\x03\x9f\xd0\x08\x82" + "\xa9\x6a\xc4\x65\x8e\x6f\x92\x06\x84\x35"; + +TEST(X509CertificateTest, ExtractSPKIFromDERCert) { + FilePath certs_dir = GetTestCertsDirectory(); + scoped_refptr<X509Certificate> cert = + ImportCertFromFile(certs_dir, "nist.der"); + ASSERT_NE(static_cast<X509Certificate*>(NULL), cert); + + std::string derBytes; + EXPECT_TRUE(cert->GetDEREncoded(&derBytes)); + + base::StringPiece spkiBytes; + EXPECT_TRUE(asn1::ExtractSPKIFromDERCert(derBytes, &spkiBytes)); + + uint8 hash[base::SHA1_LENGTH]; + base::SHA1HashBytes(reinterpret_cast<const uint8*>(spkiBytes.data()), + spkiBytes.size(), hash); + + EXPECT_TRUE(0 == memcmp(hash, nistSPKIHash, sizeof(hash))); +} + +TEST(X509CertificateTest, PublicKeyHashes) { + FilePath certs_dir = GetTestCertsDirectory(); + // This is going to blow up in Feb 2012. Sorry! Disable and file a bug + // against agl. Also see TestKnownRoot in this file. + scoped_refptr<X509Certificate> cert = + ImportCertFromFile(certs_dir, "nist.der"); + ASSERT_NE(static_cast<X509Certificate*>(NULL), cert); + + // This intermediate is only needed for old Linux machines. Modern NSS + // includes it as a root already. + scoped_refptr<X509Certificate> intermediate_cert = + ImportCertFromFile(certs_dir, "nist_intermediate.der"); + ASSERT_NE(static_cast<X509Certificate*>(NULL), intermediate_cert); + + TestRootCerts::GetInstance()->Add(intermediate_cert.get()); + + X509Certificate::OSCertHandles intermediates; + intermediates.push_back(intermediate_cert->os_cert_handle()); + scoped_refptr<X509Certificate> cert_chain = + X509Certificate::CreateFromHandle(cert->os_cert_handle(), + X509Certificate::SOURCE_FROM_NETWORK, + intermediates); + + int flags = 0; + CertVerifyResult verify_result; + + int error = cert_chain->Verify("www.nist.gov", flags, &verify_result); + EXPECT_EQ(OK, error); + EXPECT_EQ(0, verify_result.cert_status); + ASSERT_LE(2u, verify_result.public_key_hashes.size()); + EXPECT_TRUE(0 == memcmp(verify_result.public_key_hashes[0].data, + nistSPKIHash, base::SHA1_LENGTH)); + EXPECT_TRUE(0 == memcmp(verify_result.public_key_hashes[1].data, + "\x83\x24\x42\x23\xd6\xcb\xf0\xa2\x6f\xc7" + "\xde\x27\xce\xbc\xa4\xbd\xa3\x26\x12\xad", + base::SHA1_LENGTH)); + + TestRootCerts::GetInstance()->Clear(); +} + // A regression test for http://crbug.com/70293. // The Key Usage extension in this RSA SSL server certificate does not have // the keyEncipherment bit. diff --git a/net/base/x509_certificate_win.cc b/net/base/x509_certificate_win.cc index 3fb9a5c..f42fdc4 100644 --- a/net/base/x509_certificate_win.cc +++ b/net/base/x509_certificate_win.cc @@ -13,6 +13,7 @@ #include "base/string_tokenizer.h" #include "base/string_util.h" #include "base/utf_string_conversions.h" +#include "net/base/asn1_util.h" #include "net/base/cert_status_flags.h" #include "net/base/cert_verify_result.h" #include "net/base/ev_root_ca_metadata.h" @@ -464,6 +465,32 @@ X509Certificate::OSCertHandles ParsePKCS7(const char* data, size_t length) { return results; } +void AppendPublicKeyHashes(PCCERT_CHAIN_CONTEXT chain, + std::vector<SHA1Fingerprint>* hashes) { + if (chain->cChain == 0) + return; + + PCERT_SIMPLE_CHAIN first_chain = chain->rgpChain[0]; + PCERT_CHAIN_ELEMENT* const element = first_chain->rgpElement; + + const DWORD num_elements = first_chain->cElement; + for (DWORD i = 0; i < num_elements; i++) { + PCCERT_CONTEXT cert = element[i]->pCertContext; + + base::StringPiece der_bytes( + reinterpret_cast<const char*>(cert->pbCertEncoded), + cert->cbCertEncoded); + base::StringPiece spki_bytes; + if (!asn1::ExtractSPKIFromDERCert(der_bytes, &spki_bytes)) + continue; + + SHA1Fingerprint hash; + base::SHA1HashBytes(reinterpret_cast<const uint8*>(spki_bytes.data()), + spki_bytes.size(), hash.data); + hashes->push_back(hash); + } +} + } // namespace void X509Certificate::Initialize() { @@ -885,6 +912,7 @@ int X509Certificate::Verify(const std::string& hostname, if (IsCertStatusError(verify_result->cert_status)) return MapCertStatusToNetError(verify_result->cert_status); + AppendPublicKeyHashes(chain_context, &verify_result->public_key_hashes); verify_result->is_issued_by_known_root = IsIssuedByKnownRoot(chain_context); if (ev_policy_oid && CheckEV(chain_context, ev_policy_oid)) |