Skip to content

Commit

Permalink
Modify name constraints internal storage to use a std::variant
Browse files Browse the repository at this point in the history
Previously name constraints flattened everything to a std::string and
then parsed it back to the desired form during matching. This is slow.
It also bounced through a std::function for each match, which has a
surprisingly high overhead.

This commit also removes some sketchy (untested, unused within
library, unclear purpose) string constructors for GeneralName and
GeneralSubtree. Technically a SemVer break but I'm pretty sure nobody
uses these.
  • Loading branch information
randombit committed May 10, 2024
1 parent 6f1d458 commit 43a4c83
Show file tree
Hide file tree
Showing 2 changed files with 139 additions and 136 deletions.
227 changes: 121 additions & 106 deletions src/lib/x509/name_constraint.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*
* X.509 Name Constraint
* (C) 2015 Kai Michaelis
* 2024 Jack Lloyd
*
* Botan is released under the Simplified BSD License (see license.txt)
*/
Expand All @@ -9,6 +10,7 @@

#include <botan/ber_dec.h>
#include <botan/x509cert.h>
#include <botan/internal/fmt.h>
#include <botan/internal/loadstor.h>
#include <botan/internal/parsing.h>
#include <functional>
Expand All @@ -18,14 +20,41 @@ namespace Botan {

class DER_Encoder;

GeneralName::GeneralName(const std::string& str) : GeneralName() {
size_t p = str.find(':');
std::string GeneralName::type() const {
switch(m_type) {
case NameType::Empty:
throw Encoding_Error("Could not convert empty NameType to string");
case NameType::RFC822:
return "RFC822";
case NameType::DNS:
return "DNS";
case NameType::URI:
return "URI";
case NameType::DN:
return "DN";
case NameType::IP:
return "IP";
}

BOTAN_ASSERT_UNREACHABLE();
}

if(p != std::string::npos) {
m_type = str.substr(0, p);
m_name = str.substr(p + 1, std::string::npos);
std::string GeneralName::name() const {
const size_t index = m_names.index();

if(index == 0) {
return std::get<0>(m_names);
} else if(index == 1) {
return std::get<1>(m_names);
} else if(index == 2) {
return std::get<2>(m_names);
} else if(index == 3) {
return std::get<3>(m_names).to_string();
} else if(index == 4) {
auto [net, mask] = std::get<4>(m_names);
return fmt("{}/{}", ipv4_to_string(net), ipv4_to_string(mask));
} else {
throw Invalid_Argument("Failed to decode Name Constraint");
BOTAN_ASSERT_UNREACHABLE();
}
}

Expand All @@ -37,29 +66,30 @@ void GeneralName::decode_from(BER_Decoder& ber) {
BER_Object obj = ber.get_next_object();

if(obj.is_a(1, ASN1_Class::ContextSpecific)) {
m_type = "RFC822";
m_name = ASN1::to_string(obj);
m_type = NameType::RFC822;
m_names.emplace<0>(ASN1::to_string(obj));
} else if(obj.is_a(2, ASN1_Class::ContextSpecific)) {
m_type = "DNS";
m_name = ASN1::to_string(obj);
m_type = NameType::DNS;
// Store it in case insensitive form so we don't have to do it
// again while matching
m_names.emplace<1>(tolower_string(ASN1::to_string(obj)));
} else if(obj.is_a(6, ASN1_Class::ContextSpecific)) {
m_type = "URI";
m_name = ASN1::to_string(obj);
m_type = NameType::URI;
m_names.emplace<2>(ASN1::to_string(obj));
} else if(obj.is_a(4, ASN1_Class::ContextSpecific | ASN1_Class::Constructed)) {
m_type = "DN";
X509_DN dn;
BER_Decoder dec(obj);
std::stringstream ss;

dn.decode_from(dec);
ss << dn;

m_name = ss.str();
m_type = NameType::DN;
m_names.emplace<3>(dn);
} else if(obj.is_a(7, ASN1_Class::ContextSpecific)) {
if(obj.length() == 8) {
m_type = "IP";
m_name =
ipv4_to_string(load_be<uint32_t>(obj.bits(), 0)) + "/" + ipv4_to_string(load_be<uint32_t>(obj.bits(), 1));
const uint32_t net = load_be<uint32_t>(obj.bits(), 0);
const uint32_t mask = load_be<uint32_t>(obj.bits(), 1);

m_type = NameType::IP;
m_names.emplace<4>(std::make_pair(net, mask));
} else if(obj.length() == 32) {
throw Decoding_Error("Unsupported IPv6 name constraint");
} else {
Expand All @@ -71,87 +101,101 @@ void GeneralName::decode_from(BER_Decoder& ber) {
}

GeneralName::MatchResult GeneralName::matches(const X509_Certificate& cert) const {
std::vector<std::string> nam;
std::function<bool(const GeneralName*, const std::string&)> match_fn;
class MatchScore final {
public:
MatchScore() : m_any(false), m_some(false), m_all(true) {}

void add(bool m) {
m_any = true;
m_some |= m;
m_all &= m;
}

MatchResult result() const {
if(!m_any) {
return MatchResult::NotFound;
} else if(m_all) {
return MatchResult::All;
} else if(m_some) {
return MatchResult::Some;
} else {
return MatchResult::None;
}
}

private:
bool m_any;
bool m_some;
bool m_all;
};

const X509_DN& dn = cert.subject_dn();
const AlternativeName& alt_name = cert.subject_alt_name();

if(type() == "DNS") {
match_fn = std::mem_fn(&GeneralName::matches_dns);
MatchScore score;

if(m_type == NameType::DNS) {
const auto& constraint = std::get<1>(m_names);

nam = alt_name.get_attribute("DNS");
const auto& alt_names = alt_name.dns();

for(const std::string& dns : alt_names) {
score.add(matches_dns(dns, constraint));
}

if(nam.empty()) {
nam = dn.get_attribute("CN");
if(alt_names.empty()) {
// Check CN instead...
for(const std::string& cn : dn.get_attribute("CN")) {
score.add(matches_dns(cn, constraint));
}
}
} else if(type() == "DN") {
match_fn = std::mem_fn(&GeneralName::matches_dn);
} else if(m_type == NameType::DN) {
const X509_DN& constraint = std::get<3>(m_names);
score.add(matches_dn(dn, constraint));

nam.push_back(dn.to_string());
for(const auto& alt_dn : alt_name.directory_names()) {
score.add(matches_dn(alt_dn, constraint));
}
} else if(m_type == NameType::IP) {
auto [net, mask] = std::get<4>(m_names);

const auto alt_dn = alt_name.dn();
if(!alt_dn.empty()) {
nam.push_back(alt_dn.to_string());
for(uint32_t ipv4 : alt_name.ipv4_address()) {
bool match = (ipv4 & mask) == net;
score.add(match);
}
} else if(type() == "IP") {
match_fn = std::mem_fn(&GeneralName::matches_ip);
nam = alt_name.get_attribute("IP");
} else {
// URI and email name constraint matching not implemented
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;
}
return score.result();
}

bool GeneralName::matches_dns(const std::string& nam) const {
if(nam.size() == name().size()) {
return tolower_string(nam) == tolower_string(name());
} else if(name().size() > nam.size()) {
//static
bool GeneralName::matches_dns(const std::string& name, const std::string& constraint) {
// constraint is assumed already tolower
if(name.size() == constraint.size()) {
return tolower_string(name) == constraint;
} else if(constraint.size() > name.size()) {
// The constraint is longer than the issued name: not possibly a match
return false;
} else // name.size() < nam.size()
{
// constr is suffix of nam
const std::string constr = name().front() == '.' ? name() : "." + name();
const std::string substr = nam.substr(nam.size() - constr.size(), constr.size());
return tolower_string(constr) == tolower_string(substr);
} else {
// constraint.size() < name.size()
// constr is suffix of constraint
const std::string constr = constraint.front() == '.' ? constraint : "." + constraint;
BOTAN_ASSERT_NOMSG(name.size() >= constr.size());
const std::string substr = name.substr(name.size() - constr.size(), constr.size());
return tolower_string(substr) == constr;
}
}

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();
//static
bool GeneralName::matches_dn(const X509_DN& name, const X509_DN& constraint) {
const auto attr = name.get_attributes();
bool ret = true;
size_t trys = 0;

for(const auto& c : my_dn.dn_info()) {
for(const auto& c : constraint.dn_info()) {
auto i = attr.equal_range(c.first);

if(i.first != i.second) {
Expand All @@ -163,40 +207,11 @@ bool GeneralName::matches_dn(const std::string& nam) const {
return trys > 0 && ret;
}

bool GeneralName::matches_ip(const std::string& nam) const {
uint32_t 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");
}

uint32_t net = string_to_ipv4(p.at(0));
uint32_t 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& str) : GeneralSubtree() {
size_t p0, p1;
const auto min = std::stoull(str, &p0, 10);
const auto max = std::stoull(str.substr(p0 + 1), &p1, 10);
GeneralName gn(str.substr(p0 + p1 + 2));

if(p0 > 0 && p1 > 0) {
m_minimum = static_cast<size_t>(min);
m_maximum = static_cast<size_t>(max);
m_base = gn;
} else {
throw Invalid_Argument("Failed to decode Name Constraint");
}
}

void GeneralSubtree::encode_into(DER_Encoder& /*to*/) const {
throw Not_Implemented("General Subtree encoding");
}
Expand Down
48 changes: 18 additions & 30 deletions src/lib/x509/pkix_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include <set>
#include <string>
#include <string_view>
#include <variant>
#include <vector>

namespace Botan {
Expand Down Expand Up @@ -245,30 +246,22 @@ class BOTAN_PUBLIC_API(2, 0) GeneralName final : public ASN1_Object {
UnknownType,
};

/**
* Creates an empty GeneralName.
*/
GeneralName() = default;

/**
* Creates a new GeneralName for its string format.
* @param str type and name, colon-separated, e.g., "DNS:google.com"
*/
GeneralName(const std::string& str);

// Encoding is not implemented
void encode_into(DER_Encoder&) const override;

void decode_from(BER_Decoder&) override;

/**
* @return Type of the name. Can be DN, DNS, IP, RFC822 or URI.
*/
const std::string& type() const { return m_type; }
std::string type() const;

/**
* @return The name as string. Format depends on type.
*/
const std::string& name() const { return m_name; }
std::string name() const;

/**
* Checks whether a given certificate (partially) matches this name.
Expand All @@ -278,12 +271,21 @@ class BOTAN_PUBLIC_API(2, 0) GeneralName final : public ASN1_Object {
MatchResult matches(const X509_Certificate& cert) const;

private:
std::string m_type;
std::string m_name;
enum class NameType : uint8_t {
Empty = 0,
RFC822 = 1,
DNS = 2,
URI = 3,
DN = 4,
IP = 5,
};

NameType m_type;
std::variant<std::string, std::string, std::string, X509_DN, std::pair<uint32_t, uint32_t>> m_names;

bool matches_dns(const std::string&) const;
bool matches_dn(const std::string&) const;
bool matches_ip(const std::string&) const;
static bool matches_dns(const std::string& name, const std::string& constraint);

static bool matches_dn(const X509_DN& name, const X509_DN& constraint);
};

std::ostream& operator<<(std::ostream& os, const GeneralName& gn);
Expand All @@ -302,20 +304,6 @@ class BOTAN_PUBLIC_API(2, 0) GeneralSubtree final : public ASN1_Object {
*/
GeneralSubtree() : m_base(), m_minimum(0), m_maximum(std::numeric_limits<std::size_t>::max()) {}

/***
* Creates a new name constraint.
* @param base name
* @param min minimum path length
* @param max maximum path length
*/
GeneralSubtree(const GeneralName& base, size_t min, size_t max) : m_base(base), m_minimum(min), m_maximum(max) {}

/**
* Creates a new name constraint for its string format.
* @param str name constraint
*/
GeneralSubtree(const std::string& str);

void encode_into(DER_Encoder&) const override;

void decode_from(BER_Decoder&) override;
Expand Down

0 comments on commit 43a4c83

Please # to comment.