diff options
author | Fabian Weissberg <[email protected]> | 2017-11-29 12:29:56 +0100 |
---|---|---|
committer | Fabian Weissberg <[email protected]> | 2017-12-20 13:32:51 +0100 |
commit | 02e756dba4c1001b790c3496049f40ebfe89539b (patch) | |
tree | 30f36cd1faa600dd61f7ffbf6d699d4fefafe127 /src/lib | |
parent | 2918801d97ccdad5327320ee29bdc2cf666fb08a (diff) |
Fix various x509 path validation bugs + path building with ambiguous DNs
Signed-off-by: Fabian Weissberg <[email protected]>
Diffstat (limited to 'src/lib')
-rw-r--r-- | src/lib/asn1/oids.cpp | 4 | ||||
-rw-r--r-- | src/lib/x509/asn1_alt_name.cpp | 22 | ||||
-rw-r--r-- | src/lib/x509/cert_status.cpp | 13 | ||||
-rw-r--r-- | src/lib/x509/cert_status.h | 9 | ||||
-rw-r--r-- | src/lib/x509/certstor.cpp | 23 | ||||
-rw-r--r-- | src/lib/x509/certstor.h | 16 | ||||
-rw-r--r-- | src/lib/x509/certstor_sql/certstor_sql.cpp | 37 | ||||
-rw-r--r-- | src/lib/x509/certstor_sql/certstor_sql.h | 7 | ||||
-rw-r--r-- | src/lib/x509/x509_crl.cpp | 30 | ||||
-rw-r--r-- | src/lib/x509/x509_crl.h | 6 | ||||
-rw-r--r-- | src/lib/x509/x509_dn.cpp | 1 | ||||
-rw-r--r-- | src/lib/x509/x509_dn_ub.cpp | 58 | ||||
-rw-r--r-- | src/lib/x509/x509_dn_ub.h | 24 | ||||
-rw-r--r-- | src/lib/x509/x509_ext.cpp | 56 | ||||
-rw-r--r-- | src/lib/x509/x509_ext.h | 38 | ||||
-rw-r--r-- | src/lib/x509/x509cert.cpp | 9 | ||||
-rw-r--r-- | src/lib/x509/x509cert.h | 6 | ||||
-rw-r--r-- | src/lib/x509/x509path.cpp | 358 | ||||
-rw-r--r-- | src/lib/x509/x509path.h | 18 |
19 files changed, 686 insertions, 49 deletions
diff --git a/src/lib/asn1/oids.cpp b/src/lib/asn1/oids.cpp index 3e5d02a7b..2c97f1d29 100644 --- a/src/lib/asn1/oids.cpp +++ b/src/lib/asn1/oids.cpp @@ -1,7 +1,7 @@ /* * OID maps * -* This file was automatically generated by .\src\scripts\oids.py on 2017-10-19 +* This file was automatically generated by ./src/scripts/oids.py on 2017-12-05 * * All manual edits to this file will be lost. Edit the script * then regenerate this source file. @@ -192,6 +192,7 @@ std::string lookup(const OID& oid) if(oid_str == "2.5.29.21") return "X509v3.ReasonCode"; if(oid_str == "2.5.29.23") return "X509v3.HoldInstructionCode"; if(oid_str == "2.5.29.24") return "X509v3.InvalidityDate"; + if(oid_str == "2.5.29.28") return "X509v3.CRLIssuingDistributionPoint"; if(oid_str == "2.5.29.30") return "X509v3.NameConstraints"; if(oid_str == "2.5.29.31") return "X509v3.CRLDistributionPoints"; if(oid_str == "2.5.29.32") return "X509v3.CertificatePolicies"; @@ -364,6 +365,7 @@ OID lookup(const std::string& name) if(name == "X509v3.AuthorityKeyIdentifier") return OID("2.5.29.35"); if(name == "X509v3.BasicConstraints") return OID("2.5.29.19"); if(name == "X509v3.CRLDistributionPoints") return OID("2.5.29.31"); + if(name == "X509v3.CRLIssuingDistributionPoint") return OID("2.5.29.28"); if(name == "X509v3.CRLNumber") return OID("2.5.29.20"); if(name == "X509v3.CertificatePolicies") return OID("2.5.29.32"); if(name == "X509v3.ExtendedKeyUsage") return OID("2.5.29.37"); diff --git a/src/lib/x509/asn1_alt_name.cpp b/src/lib/x509/asn1_alt_name.cpp index 6b3eda917..c9f7c780b 100644 --- a/src/lib/x509/asn1_alt_name.cpp +++ b/src/lib/x509/asn1_alt_name.cpp @@ -13,6 +13,9 @@ #include <botan/internal/stl_util.h> #include <botan/parsing.h> #include <botan/loadstor.h> +#include <botan/x509_dn.h> + +#include <sstream> namespace Botan { @@ -131,6 +134,13 @@ void encode_entries(DER_Encoder& encoder, store_be(ip, ip_buf); encoder.add_object(tagging, CONTEXT_SPECIFIC, ip_buf, 4); } + else if (type == "DN") + { + std::stringstream ss(i->second); + X509_DN dn; + ss >> dn; + encoder.encode(dn); + } } } @@ -145,6 +155,7 @@ void AlternativeName::encode_into(DER_Encoder& der) const encode_entries(der, m_alt_info, "RFC822", ASN1_Tag(1)); encode_entries(der, m_alt_info, "DNS", ASN1_Tag(2)); + encode_entries(der, m_alt_info, "DN", ASN1_Tag(4)); encode_entries(der, m_alt_info, "URI", ASN1_Tag(6)); encode_entries(der, m_alt_info, "IP", ASN1_Tag(7)); @@ -213,6 +224,17 @@ void AlternativeName::decode_from(BER_Decoder& source) if(tag == 2) add_attribute("DNS", ASN1::to_string(obj)); if(tag == 6) add_attribute("URI", ASN1::to_string(obj)); } + else if(tag == 4) + { + BER_Decoder dec(obj.value); + X509_DN dn; + std::stringstream ss; + + dec.decode(dn); + ss << dn; + + add_attribute("DN", ss.str()); + } else if(tag == 7) { if(obj.value.size() == 4) diff --git a/src/lib/x509/cert_status.cpp b/src/lib/x509/cert_status.cpp index 9561cae57..2c0e74d36 100644 --- a/src/lib/x509/cert_status.cpp +++ b/src/lib/x509/cert_status.cpp @@ -22,6 +22,11 @@ const char* to_string(Certificate_Status_Code code) case Certificate_Status_Code::VALID_CRL_CHECKED: return "Valid CRL examined"; + case Certificate_Status_Code::CERT_SERIAL_NEGATIVE: + return "Certificate serial number is negative"; + case Certificate_Status_Code::DN_TOO_LONG: + return "Distinguished name too long"; + case Certificate_Status_Code::NO_REVOCATION_DATA: return "No revocation data"; case Certificate_Status_Code::SIGNATURE_METHOD_TOO_WEAK: @@ -55,6 +60,8 @@ const char* to_string(Certificate_Status_Code code) case Certificate_Status_Code::POLICY_ERROR: return "Certificate policy error"; + case Certificate_Status_Code::DUPLICATE_CERT_POLICY: + return "Certificate contains duplicate policy"; case Certificate_Status_Code::INVALID_USAGE: return "Certificate does not allow the requested usage"; case Certificate_Status_Code::CERT_CHAIN_TOO_LONG: @@ -63,6 +70,8 @@ const char* to_string(Certificate_Status_Code code) return "CA certificate not allowed to issue certs"; case Certificate_Status_Code::CA_CERT_NOT_FOR_CRL_ISSUER: return "CA certificate not allowed to issue CRLs"; + case Certificate_Status_Code::NO_MATCHING_CRLDP: + return "No CRL with matching distribution point for certificate"; case Certificate_Status_Code::OCSP_CERT_NOT_LISTED: return "OCSP cert not listed"; case Certificate_Status_Code::OCSP_BAD_STATUS: @@ -73,6 +82,10 @@ const char* to_string(Certificate_Status_Code code) return "Certificate does not pass name constraint"; case Certificate_Status_Code::UNKNOWN_CRITICAL_EXTENSION: return "Unknown critical extension encountered"; + case Certificate_Status_Code::DUPLICATE_CERT_EXTENSION: + return "Duplicate certificate extension encountered"; + case Certificate_Status_Code::EXT_IN_V1_V2_CERT: + return "Encountered extension in certificate with version < 3"; case Certificate_Status_Code::OCSP_SIGNATURE_ERROR: return "OCSP signature error"; case Certificate_Status_Code::OCSP_ISSUER_NOT_FOUND: diff --git a/src/lib/x509/cert_status.h b/src/lib/x509/cert_status.h index 0dd9f7b84..76dc9252b 100644 --- a/src/lib/x509/cert_status.h +++ b/src/lib/x509/cert_status.h @@ -25,12 +25,18 @@ enum class Certificate_Status_Code { VALID_CRL_CHECKED = 3, OCSP_NO_HTTP = 4, + // Warnings + FIRST_WARNING_STATUS = 500, + CERT_SERIAL_NEGATIVE = 500, + DN_TOO_LONG = 501, + // Errors FIRST_ERROR_STATUS = 1000, SIGNATURE_METHOD_TOO_WEAK = 1000, UNTRUSTED_HASH = 1001, NO_REVOCATION_DATA = 1002, + NO_MATCHING_CRLDP = 1003, // Time problems CERT_NOT_YET_VALID = 2000, @@ -62,10 +68,13 @@ enum class Certificate_Status_Code { // Other problems CERT_NAME_NOMATCH = 4008, UNKNOWN_CRITICAL_EXTENSION = 4009, + DUPLICATE_CERT_EXTENSION = 4010, OCSP_SIGNATURE_ERROR = 4501, OCSP_ISSUER_NOT_FOUND = 4502, OCSP_RESPONSE_MISSING_KEYUSAGE = 4503, OCSP_RESPONSE_INVALID = 4504, + EXT_IN_V1_V2_CERT = 4505, + DUPLICATE_CERT_POLICY = 4506, // Hard failures CERT_IS_REVOKED = 5000, diff --git a/src/lib/x509/certstor.cpp b/src/lib/x509/certstor.cpp index df4fc3365..23e8185c4 100644 --- a/src/lib/x509/certstor.cpp +++ b/src/lib/x509/certstor.cpp @@ -1,6 +1,7 @@ /* * Certificate Store * (C) 1999-2010,2013 Jack Lloyd +* (C) 2017 Fabian Weissberg, Rohde & Schwarz Cybersecurity * * Botan is released under the Simplified BSD License (see license.txt) */ @@ -64,6 +65,28 @@ Certificate_Store_In_Memory::find_cert(const X509_DN& subject_dn, return nullptr; } +std::vector<std::shared_ptr<const X509_Certificate>> Certificate_Store_In_Memory::find_all_certs( + const X509_DN& subject_dn, + const std::vector<uint8_t>& key_id) const + { + std::vector<std::shared_ptr<const X509_Certificate>> matches; + + for(const auto& cert : m_certs) + { + if(key_id.size()) + { + std::vector<uint8_t> skid = cert->subject_key_id(); + + if(skid.size() && skid != key_id) // no match + continue; + } + + if(cert->subject_dn() == subject_dn) + matches.push_back(cert); + } + + return matches; + } std::shared_ptr<const X509_Certificate> Certificate_Store_In_Memory::find_cert_by_pubkey_sha1(const std::vector<uint8_t>& key_hash) const diff --git a/src/lib/x509/certstor.h b/src/lib/x509/certstor.h index f08e03bae..36d2e4abd 100644 --- a/src/lib/x509/certstor.h +++ b/src/lib/x509/certstor.h @@ -31,6 +31,14 @@ class BOTAN_PUBLIC_API(2,0) Certificate_Store find_cert(const X509_DN& subject_dn, const std::vector<uint8_t>& key_id) const = 0; /** + * Find all certificates with a given Subject DN. + * Subject DN and even the key identifier might not be unique. + */ + virtual std::vector<std::shared_ptr<const X509_Certificate>> find_all_certs( + const X509_DN& subject_dn, const std::vector<uint8_t>& key_id) const = 0; + + + /** * Find a certificate by searching for one with a matching SHA-1 hash of * public key. Used for OCSP. * @param key_hash SHA-1 hash of the subject's public key @@ -121,11 +129,19 @@ class BOTAN_PUBLIC_API(2,0) Certificate_Store_In_Memory final : public Certifica /* * Find a certificate by Subject DN and (optionally) key identifier + * @return the first certificate that matches */ std::shared_ptr<const X509_Certificate> find_cert( const X509_DN& subject_dn, const std::vector<uint8_t>& key_id) const override; + /* + * Find all certificates with a given Subject DN. + * Subject DN and even the key identifier might not be unique. + */ + std::vector<std::shared_ptr<const X509_Certificate>> find_all_certs( + const X509_DN& subject_dn, const std::vector<uint8_t>& key_id) const override; + std::shared_ptr<const X509_Certificate> find_cert_by_pubkey_sha1(const std::vector<uint8_t>& key_hash) const override; diff --git a/src/lib/x509/certstor_sql/certstor_sql.cpp b/src/lib/x509/certstor_sql/certstor_sql.cpp index 6acfed060..d2991a019 100644 --- a/src/lib/x509/certstor_sql/certstor_sql.cpp +++ b/src/lib/x509/certstor_sql/certstor_sql.cpp @@ -76,6 +76,40 @@ Certificate_Store_In_SQL::find_cert(const X509_DN& subject_dn, const std::vector return cert; } +std::vector<std::shared_ptr<const X509_Certificate>> +Certificate_Store_In_SQL::find_all_certs(const X509_DN& subject_dn, const std::vector<uint8_t>& key_id) const + { + std::vector<std::shared_ptr<const X509_Certificate>> certs; + + DER_Encoder enc; + std::shared_ptr<SQL_Database::Statement> stmt; + + subject_dn.encode_into(enc); + + if(key_id.empty()) + { + stmt = m_database->new_statement("SELECT certificate FROM " + m_prefix + "certificates WHERE subject_dn == ?1"); + stmt->bind(1,enc.get_contents_unlocked()); + } + else + { + stmt = m_database->new_statement("SELECT certificate FROM " + m_prefix + "certificates WHERE\ + subject_dn == ?1 AND (key_id == NULL OR key_id == ?2)"); + stmt->bind(1,enc.get_contents_unlocked()); + stmt->bind(2,key_id); + } + + std::shared_ptr<const X509_Certificate> cert; + while(stmt->step()) + { + auto blob = stmt->get_blob(0); + certs.push_back(std::make_shared<X509_Certificate>( + std::vector<uint8_t>(blob.first,blob.first + blob.second))); + } + + return certs; + } + std::shared_ptr<const X509_Certificate> Certificate_Store_In_SQL::find_cert_by_pubkey_sha1(const std::vector<uint8_t>& /*key_hash*/) const { @@ -123,9 +157,6 @@ std::vector<X509_DN> Certificate_Store_In_SQL::all_subjects() const bool Certificate_Store_In_SQL::insert_cert(const X509_Certificate& cert) { - if(find_cert(cert.subject_dn(),cert.subject_key_id())) - return false; - DER_Encoder enc; auto stmt = m_database->new_statement("INSERT OR REPLACE INTO " + m_prefix + "certificates (\ diff --git a/src/lib/x509/certstor_sql/certstor_sql.h b/src/lib/x509/certstor_sql/certstor_sql.h index 88e3968bf..fd80eb191 100644 --- a/src/lib/x509/certstor_sql/certstor_sql.h +++ b/src/lib/x509/certstor_sql/certstor_sql.h @@ -43,6 +43,13 @@ class BOTAN_PUBLIC_API(2,0) Certificate_Store_In_SQL : public Certificate_Store std::shared_ptr<const X509_Certificate> find_cert(const X509_DN& subject_dn, const std::vector<uint8_t>& key_id) const override; + /* + * Find all certificates with a given Subject DN. + * Subject DN and even the key identifier might not be unique. + */ + std::vector<std::shared_ptr<const X509_Certificate>> find_all_certs( + const X509_DN& subject_dn, const std::vector<uint8_t>& key_id) const override; + std::shared_ptr<const X509_Certificate> find_cert_by_pubkey_sha1(const std::vector<uint8_t>& key_hash) const override; diff --git a/src/lib/x509/x509_crl.cpp b/src/lib/x509/x509_crl.cpp index a739d2f60..c6449baf8 100644 --- a/src/lib/x509/x509_crl.cpp +++ b/src/lib/x509/x509_crl.cpp @@ -10,6 +10,8 @@ #include <botan/x509cert.h> #include <botan/ber_dec.h> +#include <sstream> + namespace Botan { struct CRL_Data @@ -23,6 +25,7 @@ struct CRL_Data // cached values from extensions size_t m_crl_number = 0; std::vector<uint8_t> m_auth_key_id; + std::string m_issuing_distribution_point; }; std::string X509_CRL::PEM_label() const @@ -164,6 +167,26 @@ std::unique_ptr<CRL_Data> decode_crl_body(const std::vector<uint8_t>& body, tbs_crl.verify_end(); + // Now cache some fields from the extensions + if(auto ext = data->m_extensions.get_extension_object_as<Cert_Extension::CRL_Number>()) + { + data->m_crl_number = ext->get_crl_number(); + } + if(auto ext = data->m_extensions.get_extension_object_as<Cert_Extension::Authority_Key_ID>()) + { + data->m_auth_key_id = ext->get_key_id(); + } + if(auto ext = data->m_extensions.get_extension_object_as<Cert_Extension::CRL_Issuing_Distribution_Point>()) + { + std::stringstream ss; + + for(const auto& pair : ext->get_point().contents()) + { + ss << pair.first << ": " << pair.second << " "; + } + data->m_issuing_distribution_point = ss.str(); + } + return data; } @@ -236,4 +259,11 @@ const X509_Time& X509_CRL::next_update() const return data().m_next_update; } +/* +* Return the CRL's distribution point +*/ +std::string X509_CRL::crl_issuing_distribution_point() const + { + return data().m_issuing_distribution_point; + } } diff --git a/src/lib/x509/x509_crl.h b/src/lib/x509/x509_crl.h index fb8307d5a..89925aa04 100644 --- a/src/lib/x509/x509_crl.h +++ b/src/lib/x509/x509_crl.h @@ -83,6 +83,12 @@ class BOTAN_PUBLIC_API(2,0) X509_CRL final : public X509_Object const X509_Time& next_update() const; /** + * Get the CRL's distribution point + * @return CRL.IssuingDistributionPoint from the CRL's Data_Store + */ + std::string crl_issuing_distribution_point() const; + + /** * Create an uninitialized CRL object. Any attempts to access * this object will throw an exception. */ diff --git a/src/lib/x509/x509_dn.cpp b/src/lib/x509/x509_dn.cpp index d07344aae..1561a10f9 100644 --- a/src/lib/x509/x509_dn.cpp +++ b/src/lib/x509/x509_dn.cpp @@ -11,6 +11,7 @@ #include <botan/parsing.h> #include <botan/internal/stl_util.h> #include <botan/oids.h> +#include <botan/x509_dn_ub.h> #include <ostream> #include <cctype> diff --git a/src/lib/x509/x509_dn_ub.cpp b/src/lib/x509/x509_dn_ub.cpp new file mode 100644 index 000000000..20c88d97e --- /dev/null +++ b/src/lib/x509/x509_dn_ub.cpp @@ -0,0 +1,58 @@ +/* +* DN_UB maps: Upper bounds on the length of DN strings +* +* This file was automatically generated by ./src/scripts/oids.py on 2017-12-20 +* +* All manual edits to this file will be lost. Edit the script +* then regenerate this source file. +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#include <botan/asn1_oid.h> +#include <botan/x509_dn_ub.h> +#include <map> +#include <stdint.h> + +namespace { +/** + * Upper bounds for the length of distinguished name fields as given in RFC 5280, Appendix A. + * Only OIDS recognized by botan are considered, so far. + * Maps OID string representations instead of human readable strings in order + * to avoid an additional lookup. + */ +static const std::map<Botan::OID, size_t> DN_UB = + { + { Botan::OID("2.5.4.10"), 64 }, // X520.Organization + { Botan::OID("2.5.4.11"), 64 }, // X520.OrganizationalUnit + { Botan::OID("2.5.4.12"), 64 }, // X520.Title + { Botan::OID("2.5.4.3"), 64 }, // X520.CommonName + { Botan::OID("2.5.4.4"), 40 }, // X520.Surname + { Botan::OID("2.5.4.42"), 32768 }, // X520.GivenName + { Botan::OID("2.5.4.43"), 32768 }, // X520.Initials + { Botan::OID("2.5.4.44"), 32768 }, // X520.GenerationalQualifier + { Botan::OID("2.5.4.46"), 64 }, // X520.DNQualifier + { Botan::OID("2.5.4.5"), 64 }, // X520.SerialNumber + { Botan::OID("2.5.4.6"), 3 }, // X520.Country + { Botan::OID("2.5.4.65"), 128 }, // X520.Pseudonym + { Botan::OID("2.5.4.7"), 128 }, // X520.Locality + { Botan::OID("2.5.4.8"), 128 } // X520.State + }; +} + +namespace Botan { + +size_t lookup_ub(const OID& oid) + { + auto ub_entry = DN_UB.find(oid); + if(ub_entry != DN_UB.end()) + { + return ub_entry->second; + } + else + { + return SIZE_MAX; + } + } +} + diff --git a/src/lib/x509/x509_dn_ub.h b/src/lib/x509/x509_dn_ub.h new file mode 100644 index 000000000..b4433eb53 --- /dev/null +++ b/src/lib/x509/x509_dn_ub.h @@ -0,0 +1,24 @@ +/* +* (C) 2017 Fabian Weissberg, Rohde & Schwarz Cybersecurity +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ +#ifndef BOTAN_X509_DN_UB_H_ +#define BOTAN_X509_DN_UB_H_ + +#include <botan/asn1_oid.h> + +namespace Botan { + +/** +* Lookup upper bounds in characters for the length of distinguished name fields +* as given in RFC 5280, Appendix A. +* +* @param oid the oid of the DN to lookup +* @return the upper bound, or SIZE_MAX if no ub is known to Botan +*/ +size_t lookup_ub(const OID& oid); + +} + +#endif diff --git a/src/lib/x509/x509_ext.cpp b/src/lib/x509/x509_ext.cpp index 1b13d36e1..d98818a4c 100644 --- a/src/lib/x509/x509_ext.cpp +++ b/src/lib/x509/x509_ext.cpp @@ -2,6 +2,7 @@ * X.509 Certificate Extensions * (C) 1999-2010,2012 Jack Lloyd * (C) 2016 René Korthaus, Rohde & Schwarz Cybersecurity +* (C) 2017 Fabian Weissberg, Rohde & Schwarz Cybersecurity * * Botan is released under the Simplified BSD License (see license.txt) */ @@ -15,6 +16,7 @@ #include <botan/hash.h> #include <botan/internal/bit_ops.h> #include <algorithm> +#include <set> #include <sstream> namespace Botan { @@ -71,6 +73,10 @@ Extensions::create_extn_obj(const OID& oid, { extn.reset(new Cert_Extension::CRL_Distribution_Points); } + else if(oid == Cert_Extension::CRL_Issuing_Distribution_Point::static_oid()) + { + extn.reset(new Cert_Extension::CRL_Issuing_Distribution_Point); + } else if(oid == Cert_Extension::Certificate_Policies::static_oid()) { extn.reset(new Cert_Extension::Certificate_Policies); @@ -708,7 +714,6 @@ void Certificate_Policies::decode_inner(const std::vector<uint8_t>& in) std::vector<Policy_Information> policies; BER_Decoder(in).decode_list(policies); - m_oids.clear(); for(size_t i = 0; i != policies.size(); ++i) m_oids.push_back(policies[i].oid()); @@ -723,6 +728,18 @@ void Certificate_Policies::contents_to(Data_Store& info, Data_Store&) const info.add("X509v3.CertificatePolicies", m_oids[i].as_string()); } +void Certificate_Policies::validate(const X509_Certificate& subject, const X509_Certificate& issuer, + const std::vector<std::shared_ptr<const X509_Certificate>>& cert_path, + std::vector<std::set<Certificate_Status_Code>>& cert_status, + size_t pos) + { + std::set<OID> oid_set(m_oids.begin(), m_oids.end()); + if(oid_set.size() != m_oids.size()) + { + cert_status.at(pos).insert(Certificate_Status_Code::DUPLICATE_CERT_POLICY); + } + } + std::vector<uint8_t> Authority_Information_Access::encode_inner() const { ASN1_String url(m_ocsp_responder, IA5_STRING); @@ -801,6 +818,7 @@ std::vector<uint8_t> CRL_Number::encode_inner() const void CRL_Number::decode_inner(const std::vector<uint8_t>& in) { BER_Decoder(in).decode(m_crl_number); + m_has_value = true; } /* @@ -850,14 +868,19 @@ void CRL_Distribution_Points::decode_inner(const std::vector<uint8_t>& buf) .decode_list(m_distribution_points) .verify_end(); + std::stringstream ss; + for(size_t i = 0; i != m_distribution_points.size(); ++i) { - auto point = m_distribution_points[i].point().contents(); + auto contents = m_distribution_points[i].point().contents(); - auto uris = point.equal_range("URI"); - for(auto uri = uris.first; uri != uris.second; ++uri) - m_crl_distribution_urls.push_back(uri->second); + for(const auto& pair : contents) + { + ss << pair.first << ": " << pair.second << " "; + } } + + m_crl_distribution_urls.push_back(ss.str()); } void CRL_Distribution_Points::contents_to(Data_Store& subject, Data_Store&) const @@ -881,6 +904,29 @@ void CRL_Distribution_Points::Distribution_Point::decode_from(class BER_Decoder& .end_cons().end_cons(); } +std::vector<uint8_t> CRL_Issuing_Distribution_Point::encode_inner() const + { + throw Not_Implemented("CRL_Issuing_Distribution_Point encoding"); + } + +void CRL_Issuing_Distribution_Point::decode_inner(const std::vector<uint8_t>& buf) + { + BER_Decoder(buf).decode(m_distribution_point).verify_end(); + } + +void CRL_Issuing_Distribution_Point::contents_to(Data_Store& info, Data_Store&) const + { + auto contents = m_distribution_point.point().contents(); + std::stringstream ss; + + for(const auto& pair : contents) + { + ss << pair.first << ": " << pair.second << " "; + } + + info.add("X509v3.CRLIssuingDistributionPoint", ss.str()); + } + std::vector<uint8_t> Unknown_Extension::encode_inner() const { return m_bytes; diff --git a/src/lib/x509/x509_ext.h b/src/lib/x509/x509_ext.h index 1680bd9dd..235496cbd 100644 --- a/src/lib/x509/x509_ext.h +++ b/src/lib/x509/x509_ext.h @@ -528,6 +528,10 @@ class BOTAN_PUBLIC_API(2,0) Certificate_Policies final : public Certificate_Exte static OID static_oid() { return OID("2.5.29.32"); } OID oid_of() const override { return static_oid(); } + void validate(const X509_Certificate& subject, const X509_Certificate& issuer, + const std::vector<std::shared_ptr<const X509_Certificate>>& cert_path, + std::vector<std::set<Certificate_Status_Code>>& cert_status, + size_t pos) override; private: std::string oid_name() const override { return "X509v3.CertificatePolicies"; } @@ -627,6 +631,7 @@ class BOTAN_PUBLIC_API(2,0) CRL_ReasonCode final : public Certificate_Extension /** * CRL Distribution Points Extension +* todo enforce restrictions from RFC 5280 4.2.1.13 */ class BOTAN_PUBLIC_API(2,0) CRL_Distribution_Points final : public Certificate_Extension { @@ -675,6 +680,39 @@ class BOTAN_PUBLIC_API(2,0) CRL_Distribution_Points final : public Certificate_E }; /** +* CRL Issuing Distribution Point Extension +* todo enforce restrictions from RFC 5280 5.2.5 +*/ +class CRL_Issuing_Distribution_Point final : public Certificate_Extension + { + public: + CRL_Issuing_Distribution_Point() = default; + + explicit CRL_Issuing_Distribution_Point(const CRL_Distribution_Points::Distribution_Point& distribution_point) : + m_distribution_point(distribution_point) {} + + CRL_Issuing_Distribution_Point* copy() const override + { return new CRL_Issuing_Distribution_Point(m_distribution_point); } + + const AlternativeName& get_point() const + { return m_distribution_point.point(); } + + static OID static_oid() { return OID("2.5.29.28"); } + OID oid_of() const override { return static_oid(); } + + private: + std::string oid_name() const override + { return "X509v3.CRLIssuingDistributionPoint"; } + + bool should_encode() const override { return true; } + std::vector<uint8_t> encode_inner() const override; + void decode_inner(const std::vector<uint8_t>&) override; + void contents_to(Data_Store&, Data_Store&) const override; + + CRL_Distribution_Points::Distribution_Point m_distribution_point; + }; + +/** * An unknown X.509 extension * Will add a failure to the path validation result, if critical */ diff --git a/src/lib/x509/x509cert.cpp b/src/lib/x509/x509cert.cpp index b1229679c..5a5521e37 100644 --- a/src/lib/x509/x509cert.cpp +++ b/src/lib/x509/x509cert.cpp @@ -25,6 +25,7 @@ struct X509_Certificate_Data { size_t m_version = 0; std::vector<uint8_t> m_serial; + bool m_serial_negative; AlgorithmIdentifier m_sig_algo_inner; X509_DN m_issuer_dn; X509_DN m_subject_dn; @@ -113,6 +114,8 @@ std::unique_ptr<X509_Certificate_Data> parse_x509_cert_body(const X509_Object& o throw Decoding_Error("Unknown X.509 cert version " + std::to_string(data->m_version)); if(obj.signature_algorithm() != data->m_sig_algo_inner) throw Decoding_Error("X.509 Certificate had differing algorithm identifers in inner and outer ID fields"); + // crude method to save the serial's sign; will get lost during decoding, otherwise + data->m_serial_negative = serial_bn.is_negative(); // for general sanity convert wire version (0 based) to standards version (v1 .. v3) data->m_version += 1; @@ -384,6 +387,12 @@ const std::vector<uint8_t>& X509_Certificate::serial_number() const return data().m_serial; } +bool X509_Certificate::is_serial_negative() const + { + return data().m_serial_negative; + } + + const X509_DN& X509_Certificate::issuer_dn() const { return data().m_issuer_dn; diff --git a/src/lib/x509/x509cert.h b/src/lib/x509/x509cert.h index dc32e70c1..9ee3d4eb8 100644 --- a/src/lib/x509/x509cert.h +++ b/src/lib/x509/x509cert.h @@ -186,6 +186,12 @@ class BOTAN_PUBLIC_API(2,0) X509_Certificate : public X509_Object const std::vector<uint8_t>& serial_number() const; /** + * Get the serial number's sign + * @return 1 iff the serial is negative. + */ + bool is_serial_negative() const; + + /** * Get the DER encoded AuthorityKeyIdentifier of this certificate. * @return DER encoded AuthorityKeyIdentifier */ diff --git a/src/lib/x509/x509path.cpp b/src/lib/x509/x509path.cpp index c2a22e7f4..88fd578b2 100644 --- a/src/lib/x509/x509path.cpp +++ b/src/lib/x509/x509path.cpp @@ -1,18 +1,23 @@ /* * X.509 Certificate Path Validation * (C) 2010,2011,2012,2014,2016 Jack Lloyd +* (C) 2017 Fabian Weissberg, Rohde & Schwarz Cybersecurity * * Botan is released under the Simplified BSD License (see license.txt) */ #include <botan/x509path.h> #include <botan/x509_ext.h> +#include <botan/x509_dn_ub.h> #include <botan/pk_keys.h> #include <botan/ocsp.h> +#include <botan/oids.h> #include <algorithm> #include <chrono> #include <vector> #include <set> +#include <string> +#include <sstream> #if defined(BOTAN_HAS_ONLINE_REVOCATION_CHECKS) #include <future> @@ -81,6 +86,22 @@ PKIX::check_chain(const std::vector<std::shared_ptr<const X509_Certificate>>& ce status.insert(Certificate_Status_Code::CHAIN_NAME_MISMATCH); } + // Check the serial number + if(subject->is_serial_negative()) + { + status.insert(Certificate_Status_Code::CERT_SERIAL_NEGATIVE); + } + + // Check the subject's DN components' length + for(const auto& dn_pair : subject->subject_dn().get_attributes()) + { + // dn_pair = <OID,str> + if(lookup_ub(dn_pair.first) < dn_pair.second.size()) + { + status.insert(Certificate_Status_Code::DN_TOO_LONG); + } + } + // Check all certs for valid time range if(validation_time < subject->not_before()) status.insert(Certificate_Status_Code::CERT_NOT_YET_VALID); @@ -94,34 +115,52 @@ PKIX::check_chain(const std::vector<std::shared_ptr<const X509_Certificate>>& ce std::unique_ptr<Public_Key> issuer_key(issuer->subject_public_key()); - if(!issuer_key) + // Check the signature algorithm + if(OIDS::lookup(subject->signature_algorithm().oid).empty()) { - status.insert(Certificate_Status_Code::CERT_PUBKEY_INVALID); + status.insert(Certificate_Status_Code::SIGNATURE_ALGO_UNKNOWN); } + // only perform the following checks if the signature algorithm is known else { - const Certificate_Status_Code sig_status = subject->verify_signature(*issuer_key); + if(!issuer_key) + { + status.insert(Certificate_Status_Code::CERT_PUBKEY_INVALID); + } + else + { + const Certificate_Status_Code sig_status = subject->verify_signature(*issuer_key); - if(sig_status != Certificate_Status_Code::VERIFIED) - status.insert(sig_status); + if(sig_status != Certificate_Status_Code::VERIFIED) + status.insert(sig_status); - if(issuer_key->estimated_strength() < min_signature_algo_strength) - status.insert(Certificate_Status_Code::SIGNATURE_METHOD_TOO_WEAK); - } + if(issuer_key->estimated_strength() < min_signature_algo_strength) + status.insert(Certificate_Status_Code::SIGNATURE_METHOD_TOO_WEAK); + } - // Ignore untrusted hashes on self-signed roots - if(trusted_hashes.size() > 0 && !at_self_signed_root) - { - if(trusted_hashes.count(subject->hash_used_for_signature()) == 0) - status.insert(Certificate_Status_Code::UNTRUSTED_HASH); + // Ignore untrusted hashes on self-signed roots + if(trusted_hashes.size() > 0 && !at_self_signed_root) + { + if(trusted_hashes.count(subject->hash_used_for_signature()) == 0) + status.insert(Certificate_Status_Code::UNTRUSTED_HASH); + } } // Check cert extensions Extensions extensions = subject->v3_extensions(); - for(auto& extension : extensions.extensions()) + const auto& extensions_vec = extensions.extensions(); + if(subject->x509_version() < 3 && !extensions_vec.empty()) + { + status.insert(Certificate_Status_Code::EXT_IN_V1_V2_CERT); + } + for(auto& extension : extensions_vec) { extension.first->validate(*subject, *issuer, cert_path, cert_status, i); } + if(extensions.extensions().size() != extensions.get_extension_oids().size()) + { + status.insert(Certificate_Status_Code::DUPLICATE_CERT_EXTENSION); + } } // path len check @@ -245,6 +284,28 @@ PKIX::check_crl(const std::vector<std::shared_ptr<const X509_Certificate>>& cert if(crls[i]->is_revoked(*subject)) status.insert(Certificate_Status_Code::CERT_IS_REVOKED); + + std::string dp = subject->crl_distribution_point(); + if(!dp.empty()) + { + if(dp != crls[i]->crl_issuing_distribution_point()) + { + status.insert(Certificate_Status_Code::NO_MATCHING_CRLDP); + } + } + + for(const auto& extension : crls[i]->extensions().extensions()) + { + // is the extension critical and unknown? + if(extension.second && OIDS::lookup(extension.first->oid_of()) == "") + { + /* NIST Certificate Path Valiadation Testing document: "When an implementation does not recognize a critical extension in the + * crlExtensions field, it shall assume that identified certificates have been revoked and are no longer valid" + */ + status.insert(Certificate_Status_Code::CERT_IS_REVOKED); + } + } + } } @@ -514,6 +575,176 @@ PKIX::build_certificate_path(std::vector<std::shared_ptr<const X509_Certificate> } } +/** + * utilities for PKIX::build_all_certificate_paths + */ +namespace +{ +// <certificate, trusted?> +using cert_maybe_trusted = std::pair<std::shared_ptr<const X509_Certificate>,bool>; +} + +/** + * Build all possible certificate paths from the end certificate to self-signed trusted roots. + * + * All potentially valid paths are put into the cert_paths vector. If no potentially valid paths are found, + * one of the encountered errors is returned arbitrarily. + * + * todo add a path building function that returns detailed information on errors encountered while building + * the potentially numerous path candidates. + * + * Basically, a DFS is performed starting from the end certificate. A stack (vector) serves to control the DFS. + * At the beginning of each iteration, a pair is popped from the stack that contains (1) the next certificate + * to add to the path (2) a bool that indicates if the certificate is part of a trusted certstore. Ideally, we + * follow the unique issuer of the current certificate until a trusted root is reached. However, the issuer DN + + * authority key id need not be unique among the certificates used for building the path. In such a case, + * we consider all the matching issuers by pushing <IssuerCert, trusted?> on the stack for each of them. + * + */ +Certificate_Status_Code +PKIX::build_all_certificate_paths(std::vector<std::vector<std::shared_ptr<const X509_Certificate>>>& cert_paths_out, + const std::vector<Certificate_Store*>& trusted_certstores, + const std::shared_ptr<const X509_Certificate>& end_entity, + const std::vector<std::shared_ptr<const X509_Certificate>>& end_entity_extra) + { + if(!cert_paths_out.empty()) + { + throw Invalid_Argument("PKIX::build_all_certificate_paths: cert_paths_out must be empty"); + } + + if(end_entity->is_self_signed()) + { + return Certificate_Status_Code::CANNOT_ESTABLISH_TRUST; + } + + /* + * Pile up error messages + */ + std::vector<Certificate_Status_Code> stats; + + Certificate_Store_In_Memory ee_extras; + for(size_t i = 0; i != end_entity_extra.size(); ++i) + { + ee_extras.add_certificate(end_entity_extra[i]); + } + + /* + * This is an inelegant but functional way of preventing path loops + * (where C1 -> C2 -> C3 -> C1). We store a set of all the certificate + * fingerprints in the path. If there is a duplicate, we error out. + * TODO: save fingerprints in result struct? Maybe useful for blacklists, etc. + */ + std::set<std::string> certs_seen; + + // new certs are added and removed from the path during the DFS + // it is copied into cert_paths_out when we encounter a trusted root + std::vector<std::shared_ptr<const X509_Certificate>> path_so_far; + + // todo can we assume that the end certificate is not trusted? + std::vector<cert_maybe_trusted> stack = { {end_entity, false} }; + + while(!stack.empty()) + { + // found a deletion marker that guides the DFS, backtracing + if(stack.back().first == nullptr) + { + stack.pop_back(); + std::string fprint = path_so_far.back()->fingerprint("SHA-256"); + certs_seen.erase(fprint); + path_so_far.pop_back(); + } + // process next cert on the path + else + { + std::shared_ptr<const X509_Certificate> last = stack.back().first; + bool trusted = stack.back().second; + stack.pop_back(); + + // certificate already seen? + const std::string fprint = last->fingerprint("SHA-256"); + if(certs_seen.count(fprint) == 1) + { + stats.push_back(Certificate_Status_Code::CERT_CHAIN_LOOP); + // the current path ended in a loop + continue; + } + + // the current path ends here + if(last->is_self_signed()) + { + // found a trust anchor + if(trusted) + { + cert_paths_out.push_back(path_so_far); + cert_paths_out.back().push_back(last); + + continue; + } + // found an untrustworthy root + else + { + stats.push_back(Certificate_Status_Code::CANNOT_ESTABLISH_TRUST); + continue; + } + } + + const X509_DN issuer_dn = last->issuer_dn(); + const std::vector<uint8_t> auth_key_id = last->authority_key_id(); + + // search for trusted issuers + std::vector<std::shared_ptr<const X509_Certificate>> trusted_issuers; + for(Certificate_Store* store : trusted_certstores) + { + auto new_issuers = store->find_all_certs(issuer_dn, auth_key_id); + trusted_issuers.insert(trusted_issuers.end(), new_issuers.begin(), new_issuers.end()); + } + + // search the supplemental certs + std::vector<std::shared_ptr<const X509_Certificate>> misc_issuers = + ee_extras.find_all_certs(issuer_dn, auth_key_id); + + // if we could not find any issuers, the current path ends here + if(trusted_issuers.size() + misc_issuers.size() == 0) + { + stats.push_back(Certificate_Status_Code::CERT_ISSUER_NOT_FOUND); + continue; + } + + // push the latest certificate onto the path_so_far + path_so_far.push_back(last); + certs_seen.emplace(fprint); + + // push a deletion marker on the stack for backtracing later + stack.push_back({std::shared_ptr<const X509_Certificate>(nullptr),false}); + + for(const auto trusted : trusted_issuers) + { + stack.push_back({trusted,true}); + } + + for(const auto misc : misc_issuers) + { + stack.push_back({misc,false}); + } + } + } + + // could not construct any potentially valid path + if(cert_paths_out.empty()) + { + if(stats.empty()) + throw Exception("X509 path building failed for unknown reasons"); + else + // arbitrarily return the first error + return stats[0]; + } + else + { + return Certificate_Status_Code::OK; + } + } + + void PKIX::merge_revocation_status(CertificatePathStatusCodes& chain_status, const CertificatePathStatusCodes& crl, const CertificatePathStatusCodes& ocsp, @@ -597,7 +828,9 @@ Path_Validation_Result x509_path_validate( const std::vector<std::shared_ptr<const OCSP::Response>>& ocsp_resp) { if(end_certs.empty()) + { throw Invalid_Argument("x509_path_validate called with no subjects"); + } std::shared_ptr<const X509_Certificate> end_entity(std::make_shared<const X509_Certificate>(end_certs[0])); std::vector<std::shared_ptr<const X509_Certificate>> end_entity_extra; @@ -606,9 +839,8 @@ Path_Validation_Result x509_path_validate( end_entity_extra.push_back(std::make_shared<const X509_Certificate>(end_certs[i])); } - std::vector<std::shared_ptr<const X509_Certificate>> cert_path; - Certificate_Status_Code path_building_result = - PKIX::build_certificate_path(cert_path, trusted_roots, end_entity, end_entity_extra); + std::vector<std::vector<std::shared_ptr<const X509_Certificate>>> cert_paths; + Certificate_Status_Code path_building_result = PKIX::build_all_certificate_paths(cert_paths, trusted_roots, end_entity, end_entity_extra); // If we cannot successfully build a chain to a trusted self-signed root, stop now if(path_building_result != Certificate_Status_Code::OK) @@ -616,38 +848,52 @@ Path_Validation_Result x509_path_validate( return Path_Validation_Result(path_building_result); } - CertificatePathStatusCodes status = - PKIX::check_chain(cert_path, ref_time, - hostname, usage, - restrictions.minimum_key_strength(), - restrictions.trusted_hashes()); + std::vector<Path_Validation_Result> error_results; + // Try validating all the potentially valid paths and return the first one to validate properly + for(auto cert_path : cert_paths) + { + CertificatePathStatusCodes status = + PKIX::check_chain(cert_path, ref_time, + hostname, usage, + restrictions.minimum_key_strength(), + restrictions.trusted_hashes()); - CertificatePathStatusCodes crl_status = - PKIX::check_crl(cert_path, trusted_roots, ref_time); + CertificatePathStatusCodes crl_status = + PKIX::check_crl(cert_path, trusted_roots, ref_time); - CertificatePathStatusCodes ocsp_status; + CertificatePathStatusCodes ocsp_status; - if(ocsp_resp.size() > 0) - { - ocsp_status = PKIX::check_ocsp(cert_path, ocsp_resp, trusted_roots, ref_time); - } + if(ocsp_resp.size() > 0) + { + ocsp_status = PKIX::check_ocsp(cert_path, ocsp_resp, trusted_roots, ref_time); + } - if(ocsp_status.empty() && ocsp_timeout != std::chrono::milliseconds(0)) - { + if(ocsp_status.empty() && ocsp_timeout != std::chrono::milliseconds(0)) + { #if defined(BOTAN_TARGET_OS_HAS_THREADS) && defined(BOTAN_HAS_HTTP_UTIL) - ocsp_status = PKIX::check_ocsp_online(cert_path, trusted_roots, ref_time, - ocsp_timeout, restrictions.ocsp_all_intermediates()); + ocsp_status = PKIX::check_ocsp_online(cert_path, trusted_roots, ref_time, + ocsp_timeout, restrictions.ocsp_all_intermediates()); #else - ocsp_status.resize(1); - ocsp_status[0].insert(Certificate_Status_Code::OCSP_NO_HTTP); + ocsp_status.resize(1); + ocsp_status[0].insert(Certificate_Status_Code::OCSP_NO_HTTP); #endif - } + } - PKIX::merge_revocation_status(status, crl_status, ocsp_status, - restrictions.require_revocation_information(), - restrictions.ocsp_all_intermediates()); + PKIX::merge_revocation_status(status, crl_status, ocsp_status, + restrictions.require_revocation_information(), + restrictions.ocsp_all_intermediates()); - return Path_Validation_Result(status, std::move(cert_path)); + Path_Validation_Result pvd(status, std::move(cert_path)); + if(pvd.successful_validation()) + { + return pvd; + } + else + { + error_results.push_back(std::move(pvd)); + } + } + return error_results[0]; } Path_Validation_Result x509_path_validate( @@ -716,9 +962,31 @@ Path_Validation_Restrictions::Path_Validation_Restrictions(bool require_rev, m_trusted_hashes.insert("SHA-512"); } +namespace { +CertificatePathStatusCodes find_warnings(const CertificatePathStatusCodes& all_statuses) + { + CertificatePathStatusCodes warnings; + for(const auto& status_set_i : all_statuses) + { + std::set<Certificate_Status_Code> warning_set_i; + for(const auto& code : status_set_i) + { + if(code >= Certificate_Status_Code::FIRST_WARNING_STATUS && + code < Certificate_Status_Code::FIRST_ERROR_STATUS) + { + warning_set_i.insert(code); + } + } + warnings.push_back(warning_set_i); + } + return warnings; + } +} + Path_Validation_Result::Path_Validation_Result(CertificatePathStatusCodes status, std::vector<std::shared_ptr<const X509_Certificate>>&& cert_chain) : m_all_status(status), + m_warnings(find_warnings(m_all_status)), m_cert_path(cert_chain), m_overall(PKIX::overall_status(m_all_status)) { @@ -749,6 +1017,16 @@ bool Path_Validation_Result::successful_validation() const result() == Certificate_Status_Code::VALID_CRL_CHECKED); } +bool Path_Validation_Result::no_warnings() const + { + return m_warnings.empty(); + } + +CertificatePathStatusCodes Path_Validation_Result::warnings() const + { + return m_warnings; + } + std::string Path_Validation_Result::result_string() const { return status_string(result()); diff --git a/src/lib/x509/x509path.h b/src/lib/x509/x509path.h index 6898d0679..12a924873 100644 --- a/src/lib/x509/x509path.h +++ b/src/lib/x509/x509path.h @@ -136,6 +136,11 @@ class BOTAN_PUBLIC_API(2,0) Path_Validation_Result final bool successful_validation() const; /** + * @return true iff no warnings occured during validation + */ + bool no_warnings() const; + + /** * @return overall validation result code */ Certificate_Status_Code result() const { return m_overall; } @@ -147,6 +152,11 @@ class BOTAN_PUBLIC_API(2,0) Path_Validation_Result final { return m_all_status; } /** + * @return the subset of status codes that are warnings + */ + CertificatePathStatusCodes warnings() const; + + /** * @return string representation of the validation result */ std::string result_string() const; @@ -173,6 +183,7 @@ class BOTAN_PUBLIC_API(2,0) Path_Validation_Result final private: CertificatePathStatusCodes m_all_status; + CertificatePathStatusCodes m_warnings; std::vector<std::shared_ptr<const X509_Certificate>> m_cert_path; Certificate_Status_Code m_overall; }; @@ -274,6 +285,13 @@ Path_Validation_Result BOTAN_PUBLIC_API(2,0) x509_path_validate( */ namespace PKIX { +Certificate_Status_Code +build_all_certificate_paths(std::vector<std::vector<std::shared_ptr<const X509_Certificate>>>& cert_paths, + const std::vector<Certificate_Store*>& trusted_certstores, + const std::shared_ptr<const X509_Certificate>& end_entity, + const std::vector<std::shared_ptr<const X509_Certificate>>& end_entity_extra); + + /** * Build certificate path * @param cert_path_out output parameter, cert_path will be appended to this vector |