diff options
author | Jack Lloyd <[email protected]> | 2016-03-16 01:27:29 -0400 |
---|---|---|
committer | Jack Lloyd <[email protected]> | 2016-03-16 01:27:29 -0400 |
commit | eba8e2e0f1baf64637acda3f049fa14f79283201 (patch) | |
tree | a9f5311413629259f8169b80eef87312c8760ee2 /src/lib/cert/x509 | |
parent | 93966abb3c51a77edf867abe7d7388ec542411bb (diff) | |
parent | efe8e7d46683ceab23889fda7fcbc68303f23d62 (diff) |
Merge GH #454 X.509 name constraints
Diffstat (limited to 'src/lib/cert/x509')
-rw-r--r-- | src/lib/cert/x509/cert_status.h | 1 | ||||
-rw-r--r-- | src/lib/cert/x509/name_constraint.cpp | 273 | ||||
-rw-r--r-- | src/lib/cert/x509/name_constraint.h | 132 | ||||
-rw-r--r-- | src/lib/cert/x509/x509_ext.cpp | 70 | ||||
-rw-r--r-- | src/lib/cert/x509/x509_ext.h | 23 | ||||
-rw-r--r-- | src/lib/cert/x509/x509cert.cpp | 55 | ||||
-rw-r--r-- | src/lib/cert/x509/x509cert.h | 23 | ||||
-rw-r--r-- | src/lib/cert/x509/x509path.cpp | 60 |
8 files changed, 630 insertions, 7 deletions
diff --git a/src/lib/cert/x509/cert_status.h b/src/lib/cert/x509/cert_status.h index 5028af384..6e8635237 100644 --- a/src/lib/cert/x509/cert_status.h +++ b/src/lib/cert/x509/cert_status.h @@ -38,6 +38,7 @@ enum class Certificate_Status_Code { INVALID_USAGE, CERT_CHAIN_TOO_LONG, CA_CERT_NOT_FOR_CERT_ISSUER, + NAME_CONSTRAINT_ERROR, // Revocation errors CA_CERT_NOT_FOR_CRL_ISSUER, diff --git a/src/lib/cert/x509/name_constraint.cpp b/src/lib/cert/x509/name_constraint.cpp new file mode 100644 index 000000000..a1ed19856 --- /dev/null +++ b/src/lib/cert/x509/name_constraint.cpp @@ -0,0 +1,273 @@ +/* +* X.509 Name Constraint +* (C) 2015 Kai Michaelis +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#include <botan/name_constraint.h> +#include <botan/ber_dec.h> +#include <botan/der_enc.h> +#include <botan/charset.h> +#include <botan/loadstor.h> +#include <botan/x509_dn.h> +#include <botan/x509cert.h> +#include <sstream> + +namespace Botan { + +GeneralName::GeneralName(const std::string& v) : GeneralName() + { + size_t p = v.find(':'); + + if(p != std::string::npos) + { + m_type = v.substr(0,p); + m_name = v.substr(p + 1,std::string::npos); + } + else + { + throw Invalid_Argument("Failed to decode Name Constraint"); + } + } + +void GeneralName::encode_into(class DER_Encoder&) const + { + throw Exception("General Name encoding not implemented"); + } + +void GeneralName::decode_from(class BER_Decoder& ber) + { + BER_Object obj = ber.get_next_object(); + if((obj.class_tag != CONTEXT_SPECIFIC) && + (obj.class_tag != (CONTEXT_SPECIFIC | CONSTRUCTED))) + throw Decoding_Error("Invalid class tag while decoding GeneralName"); + + const ASN1_Tag tag = obj.type_tag; + + if(tag == 1 || tag == 2 || tag == 6) + { + m_name = Charset::transcode(ASN1::to_string(obj),LATIN1_CHARSET,LOCAL_CHARSET); + + if(tag == 1) + { + m_type = "RFC822"; + } + else if(tag == 2) + { + m_type = "DNS"; + } + else if(tag == 6) + { + m_type = "URI"; + } + } + else if(tag == 4) + { + X509_DN dn; + std::multimap<std::string, std::string> nam; + BER_Decoder dec(obj.value); + std::stringstream ss; + + dn.decode_from(dec); + ss << dn; + + m_name = ss.str(); + m_type = "DN"; + } + else if(tag == 7) + { + if(obj.value.size() == 8) + { + const std::vector<byte> ip(obj.value.begin(),obj.value.begin() + 4); + const std::vector<byte> net(obj.value.begin() + 4,obj.value.end()); + m_type = "IP"; + m_name = ipv4_to_string(load_be<u32bit>(ip.data(),0)) + "/" + ipv4_to_string(load_be<u32bit>(net.data(),0)); + } + else if(obj.value.size() == 32) + { + throw Decoding_Error("Unsupported IPv6 name constraint"); + } + else + { + throw Decoding_Error("Invalid IP name constraint size " + + std::to_string(obj.value.size())); + } + } + else + { + throw Decoding_Error("Found unknown GeneralName type"); + } + } + +GeneralName::MatchResult GeneralName::matches(const X509_Certificate& cert) const + { + std::vector<std::string> nam; + std::function<bool(const GeneralName*,const std::string&)> match_fn; + + if(type() == "DNS") + { + match_fn = std::mem_fn(&GeneralName::matches_dns); + nam = cert.subject_info("DNS"); + + if(nam.empty()) + { + nam = cert.subject_info("CN"); + } + } + else if(type() == "DN") + { + match_fn = std::mem_fn(&GeneralName::matches_dn); + + std::stringstream ss; + ss << cert.subject_dn(); + nam.push_back(ss.str()); + } + else if(type() == "IP") + { + match_fn = std::mem_fn(&GeneralName::matches_ip); + nam = cert.subject_info("IP"); + } + else + { + return MatchResult::UnknownType; + } + + if(nam.empty()) + { + return MatchResult::NotFound; + } + + bool some = false; + bool all = true; + + for(const std::string& n: nam) + { + bool m = match_fn(this,n); + + some |= m; + all &= m; + } + + if(all) + { + return MatchResult::All; + } + else if(some) + { + return MatchResult::Some; + } + else + { + return MatchResult::None; + } + } + +bool GeneralName::matches_dns(const std::string& nam) const + { + if(nam.size() == name().size()) + { + return nam == name(); + } + else if(name().size() > nam.size()) + { + return false; + } + else // name.size() < nam.size() + { + std::string constr = name().front() == '.' ? name() : "." + name(); + // constr is suffix of nam + return constr == nam.substr(nam.size() - constr.size(),constr.size()); + } + } + +bool GeneralName::matches_dn(const std::string& nam) const + { + std::stringstream ss(nam); + std::stringstream tt(name()); + X509_DN nam_dn, my_dn; + + ss >> nam_dn; + tt >> my_dn; + + auto attr = nam_dn.get_attributes(); + bool ret = true; + int trys = 0; + + for(const std::pair<OID,std::string>& c: my_dn.get_attributes()) + { + auto i = attr.equal_range(c.first); + + if(i.first != i.second) + { + trys += 1; + ret &= i.first->second == c.second; + } + } + + return trys > 0 && ret; + } + +bool GeneralName::matches_ip(const std::string& nam) const + { + u32bit ip = string_to_ipv4(nam); + std::vector<std::string> p = split_on(name(),'/'); + + if(p.size() != 2) + throw Decoding_Error("failed to parse IPv4 address"); + + u32bit net = string_to_ipv4(p.at(0)); + u32bit mask = string_to_ipv4(p.at(1)); + + return (ip & mask) == net; + } + +std::ostream& operator<<(std::ostream& os, const GeneralName& gn) + { + os << gn.type() << ":" << gn.name(); + return os; + } + +GeneralSubtree::GeneralSubtree(const std::string& v) : GeneralSubtree() + { + size_t p0, p1; + size_t min = std::stoull(v, &p0, 10); + size_t max = std::stoull(v.substr(p0 + 1), &p1, 10); + GeneralName gn(v.substr(p0 + p1 + 2)); + + if(p0 > 0 && p1 > 0) + { + m_minimum = min; + m_maximum = max; + m_base = gn; + } + else + { + throw Invalid_Argument("Failed to decode Name Constraint"); + } + } + +void GeneralSubtree::encode_into(class DER_Encoder&) const + { + throw std::runtime_error("General Subtree encoding not implemented"); + } + +void GeneralSubtree::decode_from(class BER_Decoder& ber) + { + ber.start_cons(SEQUENCE) + .decode(m_base) + .decode_optional(m_minimum,ASN1_Tag(0),CONTEXT_SPECIFIC,size_t(0)) + .end_cons(); + + if(m_minimum != 0) + throw Decoding_Error("GeneralSubtree minimum must be 0"); + + m_maximum = std::numeric_limits<std::size_t>::max(); + } + +std::ostream& operator<<(std::ostream& os, const GeneralSubtree& gs) + { + os << gs.minimum() << "," << gs.maximum() << "," << gs.base(); + return os; + } +} diff --git a/src/lib/cert/x509/name_constraint.h b/src/lib/cert/x509/name_constraint.h new file mode 100644 index 000000000..345e64ff5 --- /dev/null +++ b/src/lib/cert/x509/name_constraint.h @@ -0,0 +1,132 @@ +/* +* X.509 Name Constraint +* (C) 2015 Kai Michaelis +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#ifndef BOTAN_NAME_CONSTRAINT_H__ +#define BOTAN_NAME_CONSTRAINT_H__ + +#include <botan/asn1_obj.h> +#include <ostream> + +namespace Botan { + + class X509_Certificate; + + /** + * @brief X.509 GeneralName Type + * + * Handles parsing GeneralName types in their BER and canonical string + * encoding. Allows matching GeneralNames against each other using + * the rules laid out in the X.509 4.2.1.10 (Name Contraints). + */ + class BOTAN_DLL GeneralName : public ASN1_Object + { + public: + enum MatchResult : int + { + All, + Some, + None, + NotFound, + UnknownType, + }; + + GeneralName() : m_type(), m_name() {} + + /// Constructs a new GeneralName for its string format. + GeneralName(const std::string& s); + + void encode_into(class DER_Encoder&) const override; + void decode_from(class BER_Decoder&) override; + + /// Type of the name. Can be DN, DNS, IP, RFC822, URI. + const std::string& type() const { return m_type; } + + /// The name as string. Format depends on type. + const std::string& name() const { return m_name; } + + /// Checks whenever a given certificate (partially) matches this name. + MatchResult matches(const X509_Certificate&) const; + + private: + std::string m_type; + std::string m_name; + + bool matches_dns(const std::string&) const; + bool matches_dn(const std::string&) const; + bool matches_ip(const std::string&) const; + }; + + std::ostream& operator<<(std::ostream& os, const GeneralName& gn); + + /** + * @brief A single Name Constraints + * + * THe Name Constraint extension adds a minimum and maximum path + * length to a GeneralName to form a constraint. The length limits + * are currently unused. + */ + class BOTAN_DLL GeneralSubtree : public ASN1_Object + { + public: + GeneralSubtree() : m_base(), m_minimum(0), m_maximum(std::numeric_limits<std::size_t>::max()) + {} + + /// Constructs a new Name Constraint + GeneralSubtree(GeneralName b,size_t min,size_t max) + : m_base(b), m_minimum(min), m_maximum(max) + {} + + /// Constructs a new GeneralSubtree for its string format. + GeneralSubtree(const std::string&); + + void encode_into(class DER_Encoder&) const override; + void decode_from(class BER_Decoder&) override; + + /// Name + GeneralName base() const { return m_base; } + + // Minimum path length + size_t minimum() const { return m_minimum; } + + // Maximum path length + size_t maximum() const { return m_maximum; } + + private: + GeneralName m_base; + size_t m_minimum; + size_t m_maximum; + }; + + std::ostream& operator<<(std::ostream& os, const GeneralSubtree& gs); + + /** + * @brief Name Constraints + * + * Wraps the Name Constraints associated with a certificate. + */ + class BOTAN_DLL NameConstraints + { + public: + NameConstraints() : m_permitted_subtrees(), m_excluded_subtrees() {} + + NameConstraints(std::vector<GeneralSubtree>&& ps, std::vector<GeneralSubtree>&& es) + : m_permitted_subtrees(ps), m_excluded_subtrees(es) + {} + + /// Permitted names + const std::vector<GeneralSubtree>& permitted() const { return m_permitted_subtrees; } + + /// Excluded names + const std::vector<GeneralSubtree>& excluded() const { return m_excluded_subtrees; } + + private: + std::vector<GeneralSubtree> m_permitted_subtrees; + std::vector<GeneralSubtree> m_excluded_subtrees; + }; +} + +#endif diff --git a/src/lib/cert/x509/x509_ext.cpp b/src/lib/cert/x509/x509_ext.cpp index f3a9a7f1c..47fd909eb 100644 --- a/src/lib/cert/x509/x509_ext.cpp +++ b/src/lib/cert/x509/x509_ext.cpp @@ -13,6 +13,7 @@ #include <botan/charset.h> #include <botan/internal/bit_ops.h> #include <algorithm> +#include <sstream> namespace Botan { @@ -32,6 +33,7 @@ Certificate_Extension* Extensions::get_extension(const OID& oid) X509_EXTENSION("X509v3.ExtendedKeyUsage", Extended_Key_Usage); X509_EXTENSION("X509v3.IssuerAlternativeName", Issuer_Alternative_Name); X509_EXTENSION("X509v3.SubjectAlternativeName", Subject_Alternative_Name); + X509_EXTENSION("X509v3.NameConstraints", Name_Constraints); X509_EXTENSION("X509v3.CertificatePolicies", Certificate_Policies); X509_EXTENSION("X509v3.CRLDistributionPoints", CRL_Distribution_Points); X509_EXTENSION("PKIX.AuthorityInformationAccess", Authority_Information_Access); @@ -166,7 +168,10 @@ void Extensions::contents_to(Data_Store& subject_info, Data_Store& issuer_info) const { for(size_t i = 0; i != m_extensions.size(); ++i) + { m_extensions[i].first->contents_to(subject_info, issuer_info); + subject_info.add(m_extensions[i].first->oid_name() + ".is_critical", (m_extensions[i].second ? 1 : 0)); + } } @@ -430,6 +435,71 @@ void Extended_Key_Usage::contents_to(Data_Store& subject, Data_Store&) const subject.add("X509v3.ExtendedKeyUsage", m_oids[i].as_string()); } +/* +* Encode the extension +*/ +std::vector<byte> Name_Constraints::encode_inner() const + { + throw std::runtime_error("Name_Constraints encoding not implemented"); + } + + +/* +* Decode the extension +*/ +void Name_Constraints::decode_inner(const std::vector<byte>& in) + { + std::vector<GeneralSubtree> permit, exclude; + BER_Decoder ber(in); + BER_Decoder ext = ber.start_cons(SEQUENCE); + BER_Object per = ext.get_next_object(); + + ext.push_back(per); + if(per.type_tag == 0 && per.class_tag == ASN1_Tag(CONSTRUCTED | CONTEXT_SPECIFIC)) + { + ext.decode_list(permit,ASN1_Tag(0),ASN1_Tag(CONSTRUCTED | CONTEXT_SPECIFIC)); + if(permit.empty()) + throw Encoding_Error("Empty Name Contraint list"); + } + + BER_Object exc = ext.get_next_object(); + ext.push_back(exc); + if(per.type_tag == 1 && per.class_tag == ASN1_Tag(CONSTRUCTED | CONTEXT_SPECIFIC)) + { + ext.decode_list(exclude,ASN1_Tag(1),ASN1_Tag(CONSTRUCTED | CONTEXT_SPECIFIC)); + if(exclude.empty()) + throw Encoding_Error("Empty Name Contraint list"); + } + + ext.end_cons(); + + if(permit.empty() && exclude.empty()) + throw Encoding_Error("Empty Name Contraint extension"); + + m_name_constraints = NameConstraints(std::move(permit),std::move(exclude)); + } + +/* +* Return a textual representation +*/ +void Name_Constraints::contents_to(Data_Store& subject, Data_Store&) const + { + std::stringstream ss; + + for(const GeneralSubtree& gs: m_name_constraints.permitted()) + { + ss << gs; + subject.add("X509v3.NameConstraints.permitted", ss.str()); + ss.str(std::string()); + } + for(const GeneralSubtree& gs: m_name_constraints.excluded()) + { + ss << gs; + subject.add("X509v3.NameConstraints.excluded", ss.str()); + ss.str(std::string()); + } + } + namespace { /* diff --git a/src/lib/cert/x509/x509_ext.h b/src/lib/cert/x509/x509_ext.h index 8d2dcb52b..ac456b998 100644 --- a/src/lib/cert/x509/x509_ext.h +++ b/src/lib/cert/x509/x509_ext.h @@ -260,6 +260,29 @@ class BOTAN_DLL Extended_Key_Usage final : public Certificate_Extension }; /** +* Name Constraints +*/ +class BOTAN_DLL Name_Constraints : public Certificate_Extension + { + public: + Name_Constraints* copy() const override + { return new Name_Constraints(m_name_constraints); } + + Name_Constraints() {} + Name_Constraints(const NameConstraints &nc) : m_name_constraints(nc) {} + private: + std::string oid_name() const override + { return "X509v3.NameConstraints"; } + + bool should_encode() const override { return true; } + std::vector<byte> encode_inner() const override; + void decode_inner(const std::vector<byte>&) override; + void contents_to(Data_Store&, Data_Store&) const override; + + NameConstraints m_name_constraints; + }; + +/** * Certificate Policies Extension */ class BOTAN_DLL Certificate_Policies final : public Certificate_Extension diff --git a/src/lib/cert/x509/x509cert.cpp b/src/lib/cert/x509/x509cert.cpp index 8d6d9a70a..959cddb53 100644 --- a/src/lib/cert/x509/x509cert.cpp +++ b/src/lib/cert/x509/x509cert.cpp @@ -280,6 +280,14 @@ u32bit X509_Certificate::path_limit() const } /* +* Return if a certificate extension is marked critical +*/ +bool X509_Certificate::is_critical(const std::string& ex_name) const + { + return !!m_subject.get1_u32bit(ex_name + ".is_critical",0); + } + +/* * Return the key usage constraints */ Key_Constraints X509_Certificate::constraints() const @@ -297,6 +305,26 @@ std::vector<std::string> X509_Certificate::ex_constraints() const } /* +* Return the name constraints +*/ +NameConstraints X509_Certificate::name_constraints() const + { + std::vector<GeneralSubtree> permit, exclude; + + for(const std::string& v: m_subject.get("X509v3.NameConstraints.permitted")) + { + permit.push_back(GeneralSubtree(v)); + } + + for(const std::string& v: m_subject.get("X509v3.NameConstraints.excluded")) + { + exclude.push_back(GeneralSubtree(v)); + } + + return NameConstraints(std::move(permit),std::move(exclude)); + } + +/* * Return the list of certificate policies */ std::vector<std::string> X509_Certificate::policies() const @@ -514,6 +542,33 @@ std::string X509_Certificate::to_string() const out << " " << ex_constraints[i] << "\n"; } + NameConstraints name_constraints = this->name_constraints(); + if(!name_constraints.permitted().empty() || + !name_constraints.excluded().empty()) + { + out << "Name Constraints:\n"; + + if(!name_constraints.permitted().empty()) + { + out << " Permit"; + for(auto st: name_constraints.permitted()) + { + out << " " << st.base(); + } + out << "\n"; + } + + if(!name_constraints.excluded().empty()) + { + out << " Exclude"; + for(auto st: name_constraints.excluded()) + { + out << " " << st.base(); + } + out << "\n"; + } + } + if(!ocsp_responder().empty()) out << "OCSP responder " << ocsp_responder() << "\n"; if(!crl_distribution_point().empty()) diff --git a/src/lib/cert/x509/x509cert.h b/src/lib/cert/x509/x509cert.h index 32f2bba9f..54d82b1b4 100644 --- a/src/lib/cert/x509/x509cert.h +++ b/src/lib/cert/x509/x509cert.h @@ -14,6 +14,7 @@ #include <botan/asn1_alt_name.h> #include <botan/datastor.h> #include <botan/key_constraint.h> +#include <botan/name_constraint.h> #include <map> namespace Botan { @@ -64,9 +65,9 @@ class BOTAN_DLL X509_Certificate final : public X509_Object * "X509.Certificate.start", "X509.Certificate.end", * "X509.Certificate.v2.key_id", "X509.Certificate.public_key", * "X509v3.BasicConstraints.path_constraint", - * "X509v3.BasicConstraints.is_ca", "X509v3.ExtendedKeyUsage", - * "X509v3.CertificatePolicies", "X509v3.SubjectKeyIdentifier" or - * "X509.Certificate.serial". + * "X509v3.BasicConstraints.is_ca", "X509v3.NameConstraints", + * "X509v3.ExtendedKeyUsage", "X509v3.CertificatePolicies", + * "X509v3.SubjectKeyIdentifier" or "X509.Certificate.serial". * @return value(s) of the specified parameter */ std::vector<std::string> subject_info(const std::string& name) const; @@ -156,6 +157,12 @@ class BOTAN_DLL X509_Certificate final : public X509_Object u32bit path_limit() const; /** + * Check whenever a given X509 Extension is marked critical in this + * certificate. + */ + bool is_critical(const std::string& ex_name) const; + + /** * Get the key constraints as defined in the KeyUsage extension of this * certificate. * @return key constraints @@ -164,13 +171,19 @@ class BOTAN_DLL X509_Certificate final : public X509_Object /** * Get the key constraints as defined in the ExtendedKeyUsage - * extension of this - * certificate. + * extension of this certificate. * @return key constraints */ std::vector<std::string> ex_constraints() const; /** + * Get the name constraints as defined in the NameConstraints + * extension of this certificate. + * @return name constraints + */ + NameConstraints name_constraints() const; + + /** * Get the policies as defined in the CertificatePolicies extension * of this certificate. * @return certificate policies diff --git a/src/lib/cert/x509/x509path.cpp b/src/lib/cert/x509/x509path.cpp index 71c025280..dd9df6f51 100644 --- a/src/lib/cert/x509/x509path.cpp +++ b/src/lib/cert/x509/x509path.cpp @@ -16,8 +16,6 @@ #include <vector> #include <set> -#include <iostream> - namespace Botan { namespace { @@ -143,6 +141,62 @@ check_chain(const std::vector<X509_Certificate>& cert_path, if(!trusted_hashes.count(subject.hash_used_for_signature())) status.insert(Certificate_Status_Code::UNTRUSTED_HASH); } + + const NameConstraints& name_constr = issuer.name_constraints(); + + if(!name_constr.permitted().empty() || !name_constr.excluded().empty()) + { + if(!issuer.is_CA_cert() || !issuer.is_critical("X509v3.NameConstraints")) + cert_status.at(i).insert(Certificate_Status_Code::NAME_CONSTRAINT_ERROR); + + // Check that all subordinate certs pass the name constraint + for(size_t j = 0; j <= i; ++j) + { + if(i == j && at_self_signed_root) + continue; + + bool permitted = name_constr.permitted().empty(); + bool failed = false; + + for(auto c: name_constr.permitted()) + { + switch(c.base().matches(cert_path.at(j))) + { + case GeneralName::MatchResult::NotFound: + case GeneralName::MatchResult::All: + permitted = true; + break; + case GeneralName::MatchResult::UnknownType: + failed = issuer.is_critical("X509v3.NameConstraints"); + permitted = true; + break; + default: + break; + } + } + + for(auto c: name_constr.excluded()) + { + switch(c.base().matches(cert_path.at(j))) + { + case GeneralName::MatchResult::All: + case GeneralName::MatchResult::Some: + failed = true; + break; + case GeneralName::MatchResult::UnknownType: + failed = issuer.is_critical("X509v3.NameConstraints"); + break; + default: + break; + } + } + + if(failed || !permitted) + { + cert_status.at(j).insert(Certificate_Status_Code::NAME_CONSTRAINT_ERROR); + } + } + } } for(size_t i = 0; i != cert_path.size() - 1; ++i) @@ -416,6 +470,8 @@ const char* Path_Validation_Result::status_string(Certificate_Status_Code code) return "OCSP bad status"; case Certificate_Status_Code::CERT_NAME_NOMATCH: return "Certificate does not match provided name"; + case Certificate_Status_Code::NAME_CONSTRAINT_ERROR: + return "Certificate does not pass name constraint"; case Certificate_Status_Code::CERT_IS_REVOKED: return "Certificate is revoked"; |