From 725468c9e0103b4ad7c819fbffbce08919d4bb78 Mon Sep 17 00:00:00 2001 From: arckoor <33837362+arckoor@users.noreply.github.com> Date: Mon, 25 Nov 2024 16:20:23 +0100 Subject: [PATCH] Add IPAddrBlock and ASIdentifiers extensions (RFC 3779) Co-authored-by: butteronarchbtw <152636161+butteronarchbtw@users.noreply.github.com> --- src/build-data/oids.txt | 2 + src/lib/asn1/oid_maps.cpp | 6 +- src/lib/x509/x509_ext.cpp | 467 +++++++++++++ src/lib/x509/x509_ext.h | 186 ++++++ src/tests/data/x509/x509test/ASNumberCert.pem | 11 + .../data/x509/x509test/ASNumberInherit.pem | 11 + src/tests/data/x509/x509test/ASNumberOnly.pem | 11 + src/tests/data/x509/x509test/ASRdiOnly.pem | 11 + .../data/x509/x509test/IPAddrBlocksAll.pem | 23 + src/tests/unit_x509.cpp | 615 ++++++++++++++++++ 10 files changed, 1342 insertions(+), 1 deletion(-) create mode 100644 src/tests/data/x509/x509test/ASNumberCert.pem create mode 100644 src/tests/data/x509/x509test/ASNumberInherit.pem create mode 100644 src/tests/data/x509/x509test/ASNumberOnly.pem create mode 100644 src/tests/data/x509/x509test/ASRdiOnly.pem create mode 100644 src/tests/data/x509/x509test/IPAddrBlocksAll.pem diff --git a/src/build-data/oids.txt b/src/build-data/oids.txt index 94edfc773f6..4ab670ef5f8 100644 --- a/src/build-data/oids.txt +++ b/src/build-data/oids.txt @@ -362,6 +362,8 @@ 2.5.29.36 = X509v3.PolicyConstraints 2.5.29.37 = X509v3.ExtendedKeyUsage 1.3.6.1.5.5.7.1.1 = PKIX.AuthorityInformationAccess +1.3.6.1.5.5.7.1.7 = PKIX.ipAddrBlocks +1.3.6.1.5.5.7.1.8 = PKIX.autonomousSysIds 1.3.6.1.5.5.7.1.26 = PKIX.TNAuthList 2.5.29.32.0 = X509v3.AnyPolicy diff --git a/src/lib/asn1/oid_maps.cpp b/src/lib/asn1/oid_maps.cpp index 56b02e1c094..6ef367fa084 100644 --- a/src/lib/asn1/oid_maps.cpp +++ b/src/lib/asn1/oid_maps.cpp @@ -1,7 +1,7 @@ /* * OID maps * -* This file was automatically generated by ./src/scripts/dev_tools/gen_oids.py on 2024-10-18 +* This file was automatically generated by ./src/scripts/dev_tools/gen_oids.py on 2024-11-25 * * All manual edits to this file will be lost. Edit the script * then regenerate this source file. @@ -237,6 +237,8 @@ std::unordered_map OID_Map::load_oid2str_map() { {"1.3.6.1.4.1.8301.3.1.2.9.0.38", "secp521r1"}, {"1.3.6.1.5.5.7.1.1", "PKIX.AuthorityInformationAccess"}, {"1.3.6.1.5.5.7.1.26", "PKIX.TNAuthList"}, + {"1.3.6.1.5.5.7.1.7", "PKIX.ipAddrBlocks"}, + {"1.3.6.1.5.5.7.1.8", "PKIX.autonomousSysIds"}, {"1.3.6.1.5.5.7.3.1", "PKIX.ServerAuth"}, {"1.3.6.1.5.5.7.3.2", "PKIX.ClientAuth"}, {"1.3.6.1.5.5.7.3.3", "PKIX.CodeSigning"}, @@ -497,6 +499,8 @@ std::unordered_map OID_Map::load_str2oid_map() { {"PKIX.TNAuthList", OID({1, 3, 6, 1, 5, 5, 7, 1, 26})}, {"PKIX.TimeStamping", OID({1, 3, 6, 1, 5, 5, 7, 3, 8})}, {"PKIX.XMPPAddr", OID({1, 3, 6, 1, 5, 5, 7, 8, 5})}, + {"PKIX.autonomousSysIds", OID({1, 3, 6, 1, 5, 5, 7, 1, 8})}, + {"PKIX.ipAddrBlocks", OID({1, 3, 6, 1, 5, 5, 7, 1, 7})}, {"RIPEMD-160", OID({1, 3, 36, 3, 2, 1})}, {"RSA", OID({1, 2, 840, 113549, 1, 1, 1})}, {"RSA/EMSA3(MD2)", OID({1, 2, 840, 113549, 1, 1, 2})}, diff --git a/src/lib/x509/x509_ext.cpp b/src/lib/x509/x509_ext.cpp index 48701336142..d6131557447 100644 --- a/src/lib/x509/x509_ext.cpp +++ b/src/lib/x509/x509_ext.cpp @@ -3,6 +3,7 @@ * (C) 1999-2010,2012 Jack Lloyd * (C) 2016 René Korthaus, Rohde & Schwarz Cybersecurity * (C) 2017 Fabian Weissberg, Rohde & Schwarz Cybersecurity +* (C) 2024 Anton Einax, Dominik Schricker * * Botan is released under the Simplified BSD License (see license.txt) */ @@ -85,6 +86,14 @@ std::unique_ptr extension_from_oid(const OID& oid) { return std::make_unique(); } + if(oid == Cert_Extension::IPAddressBlocks::static_oid()) { + return std::make_unique(); + } + + if(oid == Cert_Extension::ASBlocks::static_oid()) { + return std::make_unique(); + } + return nullptr; // unknown } @@ -847,6 +856,464 @@ void TNAuthList::decode_inner(const std::vector& in) { } } +std::vector ASBlocks::encode_inner() const { + std::vector output; + DER_Encoder(output).encode(this->m_as_identifiers); + return output; +} + +void ASBlocks::decode_inner(const std::vector& in) { + BER_Decoder(in).decode(m_as_identifiers).verify_end(); +} + +void ASBlocks::ASIdentifiers::encode_into(Botan::DER_Encoder& into) const { + into.start_sequence(); + + if(m_asnum.has_value()) { + into.start_explicit(0); + into.encode(this->m_asnum.value()); + into.end_explicit(); + } + + if(m_rdi.has_value()) { + into.start_explicit(1); + into.encode(this->m_rdi.value()); + into.end_explicit(); + } + + into.end_cons(); +} + +void ASBlocks::ASIdentifiers::decode_from(Botan::BER_Decoder& from) { + BER_Object obj = from.get_next_object(); + ASN1_Type type_tag = obj.type_tag(); + if(type_tag != ASN1_Type::Sequence) { + throw Decoding_Error(fmt("Unexpected type for ASIdentifiers {}", static_cast(type_tag))); + } + BER_Decoder obj_ber = BER_Decoder(obj); + + BER_Object elem_obj = obj_ber.get_next_object(); + uint32_t elem_type_tag = static_cast(elem_obj.type_tag()); + + // asnum, potentially followed by an rdi + if(elem_type_tag == 0) { + BER_Decoder as_obj_ber = BER_Decoder(elem_obj); + ASIdentifierChoice asnum; + as_obj_ber.decode(asnum); + m_asnum = asnum; + + BER_Object rdi_obj = obj_ber.get_next_object(); + ASN1_Type rdi_type_tag = rdi_obj.type_tag(); + if(static_cast(rdi_type_tag) == 1) { + BER_Decoder rdi_obj_ber = BER_Decoder(rdi_obj); + ASIdentifierChoice rdi; + rdi_obj_ber.decode(rdi); + m_rdi = rdi; + } else if(rdi_type_tag != ASN1_Type::NoObject) { + throw Decoding_Error(fmt("Unexpected type for ASIdentifiers rdi: {}", static_cast(rdi_type_tag))); + } + } + + // just an rdi + if(elem_type_tag == 1) { + BER_Decoder rdi_obj_ber = BER_Decoder(elem_obj); + ASIdentifierChoice rdi; + rdi_obj_ber.decode(rdi); + m_rdi = rdi; + BER_Object end = obj_ber.get_next_object(); + ASN1_Type end_type_tag = end.type_tag(); + if(end_type_tag != ASN1_Type::NoObject) { + throw Decoding_Error( + fmt("Unexpected element with type {} in ASIdentifiers", static_cast(end_type_tag))); + } + } +} + +void ASBlocks::ASIdentifierChoice::encode_into(Botan::DER_Encoder& into) const { + if(this->m_as_ranges.has_value()) { + into.start_sequence().encode_list(this->m_as_ranges.value()).end_cons(); + } else { + into.encode_null(); + } +} + +void ASBlocks::ASIdentifierChoice::decode_from(Botan::BER_Decoder& from) { + BER_Object obj = from.get_next_object(); + ASN1_Type type_tag = obj.type_tag(); + + if(type_tag == ASN1_Type::Null) { + m_as_ranges = std::nullopt; + } else if(type_tag == ASN1_Type::Sequence) { + BER_Decoder obj_ber = BER_Decoder(obj); + std::vector as_ranges; + + while(obj_ber.more_items()) { + ASIdOrRange as_id_or_range; + obj_ber.decode(as_id_or_range); + as_ranges.push_back(as_id_or_range); + } + + m_as_ranges = as_ranges; + } else { + throw Decoding_Error(fmt("Unexpected type for ASIdentifierChoice {}", static_cast(type_tag))); + } +} + +std::vector ASBlocks::ASIdOrRange::encode_asnum(asnum_t asnum) const { + std::vector bytes; + for(size_t i = 0; i < sizeof(asnum); i++) { + bytes.push_back(static_cast((asnum >> 8 * (sizeof(asnum) - i - 1)) & 0xff)); + } + + // remove leading zeros + while(bytes[0] == 0 && bytes.size() > 1) { + bytes.erase(bytes.begin()); + } + + // add one padding of zero to not get it confused as a negative number + if(((bytes[0] >> 7) & 1) == 1) { + bytes.insert(bytes.begin(), 0); + } + + return bytes; +} + +void ASBlocks::ASIdOrRange::encode_into(Botan::DER_Encoder& into) const { + if(m_min == m_max) { + into.add_object(ASN1_Type::Integer, ASN1_Class::Universal, encode_asnum(m_min)); + } else { + if(m_min >= m_max) { + throw Encoding_Error("AS range numbers must be sorted"); + } + into.start_sequence() + .add_object(ASN1_Type::Integer, ASN1_Class::Universal, encode_asnum(m_min)) + .add_object(ASN1_Type::Integer, ASN1_Class::Universal, encode_asnum(m_max)) + .end_cons(); + } +} + +void ASBlocks::ASIdOrRange::decode_from(BER_Decoder& from) { + BER_Object obj = from.get_next_object(); + ASN1_Type type_tag = obj.type_tag(); + + if(type_tag == ASN1_Type::Integer) { + std::vector bytes; + bytes.assign(obj.data().begin(), obj.data().end()); + for(size_t i = 0; i < bytes.size(); i++) { + m_min = m_min | bytes[i] << 8 * (bytes.size() - 1 - i); + } + m_max = m_min; + + } else if(type_tag == ASN1_Type::Sequence) { + BER_Decoder obj_ber = BER_Decoder(obj); + BER_Object as_min_obj = obj_ber.get_next_object(); + std::vector bytes; + bytes.assign(as_min_obj.data().begin(), as_min_obj.data().end()); + + if(bytes.empty() || bytes.size() > 4) { + throw Decoding_Error("AS Range numbers must not be empty and have a length of up to 32 bit."); + } + + for(size_t i = 0; i < bytes.size(); i++) { + m_min |= (static_cast(bytes[i]) << 8 * (bytes.size() - 1 - i)); + } + + BER_Object as_max_obj = obj_ber.get_next_object(); + bytes.assign(as_max_obj.data().begin(), as_max_obj.data().end()); + for(size_t i = 0; i < bytes.size(); i++) { + m_max |= (static_cast(bytes[i]) << 8 * (bytes.size() - 1 - i)); + } + + if(m_min > m_max) { + throw Decoding_Error("AS Range numbers must be sorted"); + } + } else { + throw Decoding_Error(fmt("Unexpected type for ASIdOrRange {}", static_cast(type_tag))); + } +} + +std::vector IPAddressBlocks::encode_inner() const { + std::vector output; + DER_Encoder(output).start_sequence().encode_list(this->m_ip_addr_blocks).end_cons(); + return output; +} + +void IPAddressBlocks::decode_inner(const std::vector& in) { + BER_Decoder(in).decode_list(m_ip_addr_blocks).verify_end(); +} + +void IPAddressBlocks::IPAddressFamily::encode_into(Botan::DER_Encoder& into) const { + into.start_sequence(); + into.add_object(ASN1_Type::OctetString, ASN1_Class::Universal, this->m_addr_family); + if(std::holds_alternative>(this->m_ip_addr_choice)) { + auto ipv4_choice = std::get>(this->m_ip_addr_choice); + into.encode(ipv4_choice); + } else { + auto ipv6_choice = std::get>(this->m_ip_addr_choice); + into.encode(ipv6_choice); + } + into.end_cons(); +} + +void IPAddressBlocks::IPAddressFamily::decode_from(Botan::BER_Decoder& from) { + BER_Object obj = from.get_next_object(); + ASN1_Type type_tag = obj.type_tag(); + if(type_tag != ASN1_Type::Sequence) { + throw Decoding_Error(fmt("Unexpected type for IPAddressFamily {}", static_cast(type_tag))); + } + BER_Decoder obj_ber = BER_Decoder(obj); + + std::vector addr_family; + obj_ber.decode(addr_family, ASN1_Type::OctetString); + m_addr_family = addr_family; + std::size_t addr_family_length = addr_family.size(); + if(2 > addr_family_length || addr_family_length > 3) { + throw Decoding_Error("AFI/SAFI too long / too short."); + } + uint16_t afi = (m_addr_family[0] << 8) | m_addr_family[1]; + if(1 > afi || afi > 2) { + throw Decoding_Error("Only AFI IPv4 and IPv6 are supported."); + } + + if(afi == 1) { + IPAddressChoice addr_choice; + obj_ber.decode(addr_choice); + m_ip_addr_choice = addr_choice; + } else { + IPAddressChoice addr_choice; + obj_ber.decode(addr_choice); + m_ip_addr_choice = addr_choice; + } +} + +template +void IPAddressBlocks::IPAddressChoice::encode_into(Botan::DER_Encoder& into) const { + if(this->m_ip_addr_ranges.has_value()) { + into.start_sequence().encode_list(this->m_ip_addr_ranges.value()).end_cons(); + } else { + into.encode_null(); + } +} + +template +void IPAddressBlocks::IPAddressChoice::decode_from(Botan::BER_Decoder& from) { + BER_Object obj = from.get_next_object(); + ASN1_Type type_tag = obj.type_tag(); + + if(type_tag == ASN1_Type::Null) { + m_ip_addr_ranges = std::nullopt; + } else if(type_tag == ASN1_Type::Sequence) { + BER_Decoder obj_ber = BER_Decoder(obj); + std::vector> ip_ranges; + + while(obj_ber.more_items()) { + IPAddressOrRange ip_address_or_range; + obj_ber.decode(ip_address_or_range); + ip_ranges.push_back(ip_address_or_range); + } + + m_ip_addr_ranges = ip_ranges; + } else { + throw Decoding_Error(fmt("Unexpected type for IPAddressChoice {}", static_cast(type_tag))); + } +} + +template +void IPAddressBlocks::IPAddressOrRange::encode_into(Botan::DER_Encoder& into) const { + std::array min = this->m_min.value(); + std::array max = this->m_max.value(); + + uint8_t zeros = 0; + uint8_t ones = 0; + + bool zeros_done = false; + bool ones_done = false; + + // count contiguos 0s/1s from the right of the min/max addresses + for(int i = static_cast(V - 1); i >= 0; i--) { + if(!zeros_done) { + uint8_t local_zeros = std::countr_zero(min[i]); + zeros += local_zeros; + zeros_done = (local_zeros != 8); + } + + if(!ones_done) { + uint8_t local_ones = std::countr_one(max[i]); + ones += local_ones; + ones_done = (local_ones != 8); + } + + if(zeros_done && ones_done) { + break; + } + } + + // the part we want to compress + uint8_t host = std::min(zeros, ones); + + // these we can outright drop + uint8_t discarded_octets = host / 8; + // in a partially used byte + uint8_t unused_bits = host % 8; + + bool octets_match = true; + for(size_t i = 0; i < static_cast(V - discarded_octets - 1); i++) { + if(min[i] != max[i]) { + octets_match = false; + break; + } + } + + // we only partially use this octet, and the used part has to match for prefix encoding + uint8_t shifted_min = (min[V - discarded_octets - 1] >> unused_bits); + uint8_t shifted_max = (max[V - discarded_octets - 1] >> unused_bits); + bool used_bits_match = (shifted_min == shifted_max); + + // both the full octets and the partially used one match + if(octets_match && used_bits_match) { + // at this point the range can be encoded as a prefix + std::vector prefix; + + prefix.push_back(unused_bits); + for(size_t i = 0; i < static_cast(V - discarded_octets); i++) { + prefix.push_back(min[i]); + } + + into.add_object(ASN1_Type::BitString, ASN1_Class::Universal, prefix); + } else { + uint8_t discarded_octets_min = zeros / 8; + uint8_t unused_bits_min = zeros % 8; + + uint8_t discarded_octets_max = ones / 8; + uint8_t unused_bits_max = ones % 8; + + // compress the min/max address by setting unused bits to 0 + max[V - discarded_octets_max - 1] >>= unused_bits_max; + max[V - discarded_octets_max - 1] <<= unused_bits_max; + + std::vector compressed_min; + std::vector compressed_max; + + // construct the address as a byte sequence of the unused bits followed by the compressed address + compressed_min.push_back(unused_bits_min); + for(size_t i = 0; i < static_cast(V - discarded_octets_min); i++) { + compressed_min.push_back(min[i]); + } + + compressed_max.push_back(unused_bits_max); + for(size_t i = 0; i < static_cast(V - discarded_octets_max); i++) { + compressed_max.push_back(max[i]); + } + + into.start_sequence() + .add_object(ASN1_Type::BitString, ASN1_Class::Universal, compressed_min) + .add_object(ASN1_Type::BitString, ASN1_Class::Universal, compressed_max) + .end_cons(); + } +} + +template +void IPAddressBlocks::IPAddressOrRange::decode_from(Botan::BER_Decoder& from) { + BER_Object obj = from.get_next_object(); + ASN1_Type type_tag = obj.type_tag(); + + // this can either be a prefix or a single address + if(type_tag == ASN1_Type::BitString) { + // construct a min and a max address from the prefix + std::vector prefix; + prefix.assign(obj.data().begin(), obj.data().end()); + uint8_t len = prefix.size(); + if(len < 1) { + throw Decoding_Error("addressPrefix must contain number of unused bits"); + } + + uint8_t unused = prefix.front(); + uint8_t discarded_octets = V - (len - 1); + + prefix.erase(prefix.begin()); + + for(int i = 0; i < discarded_octets; i++) { + prefix.push_back(0); + } + + IPAddress addr_min; + addr_min.set_from_bytes(prefix); + + for(int i = V; i >= V - discarded_octets; i--) { + prefix[i] = 0xff; + } + + // set all unused bits to 1 + for(int i = 0; i < unused; i++) { + prefix[V - discarded_octets - 1] |= (1 << i); + } + + IPAddress addr_max; + addr_max.set_from_bytes(prefix); + + m_min = addr_min; + m_max = addr_max; + } else if(type_tag == ASN1_Type::Sequence) { + // this is a range + BER_Decoder obj_ber = BER_Decoder(obj); + // address 1 (min) + std::vector address; + + BER_Object addr_min_obj = obj_ber.get_next_object(); + + // we can safely skip the unused bits here, since they would get replaced with 0 anyway + address.assign(addr_min_obj.data().begin() + 1, addr_min_obj.data().end()); + for(size_t i = 0; i < V - (addr_min_obj.length() - 1); i++) { + address.push_back(0); + } + + IPAddress addr_min; + addr_min.set_from_bytes(address); + m_min = addr_min; + + // address 2 (max) + BER_Object addr_max_obj = obj_ber.get_next_object(); + uint8_t unused_max = addr_max_obj.data().front(); + address.assign(addr_max_obj.data().begin() + 1, addr_max_obj.data().end()); + // here we need the unused bits to restore the least significant 1s correctly + for(size_t i = 0; i < V - (addr_max_obj.length() - 1); i++) { + address.push_back(255); + } + + for(int i = 0; i < unused_max; i++) { + address[addr_max_obj.length() - 2] |= (1 << i); + } + + IPAddress addr_max; + addr_max.set_from_bytes(address); + m_max = addr_max; + + bool sorted = false; + for(int i = 0; i < V; i++) { + if(m_max.value()[i] > m_min.value()[i]) { + sorted = true; + break; + } + } + if(!sorted) { + throw Decoding_Error("IP address ranges must be sorted."); + } + } else { + throw Decoding_Error(fmt("Unexpected type for IPAddressOrRange {}", static_cast(type_tag))); + } +} + +template +void IPAddressBlocks::IPAddress::set_from_bytes(std::vector v) { + if(v.size() != V) { + throw Decoding_Error("number of bytes does not match IP version used"); + } + + for(int i = 0; i < V; i++) { + m_value[i] = v[i]; + } +} + void OCSP_NoCheck::decode_inner(const std::vector& buf) { BER_Decoder(buf).verify_end(); } diff --git a/src/lib/x509/x509_ext.h b/src/lib/x509/x509_ext.h index 9087af98da3..382e7b4655f 100644 --- a/src/lib/x509/x509_ext.h +++ b/src/lib/x509/x509_ext.h @@ -1,6 +1,7 @@ /* * X.509 Certificate Extensions * (C) 1999-2007,2012 Jack Lloyd +* (C) 2024 Anton Einax, Dominik Schricker * * Botan is released under the Simplified BSD License (see license.txt) */ @@ -568,6 +569,191 @@ class BOTAN_PUBLIC_API(3, 5) TNAuthList final : public Certificate_Extension { std::vector m_tn_entries; }; +/** + * IP Adress Blocks Extension + * + * RFC 3779 X.509 Extensions for IP Addr + * +*/ +class BOTAN_PUBLIC_API(3, 6) IPAddressBlocks final : public Certificate_Extension { + public: + enum Version { + IPv4 = 4, + IPv6 = 16, + }; + + template + class IPAddress { + public: + void set_from_bytes(std::vector v); + + const std::array value() const { return m_value; } + + private: + std::array m_value; + }; + + template + class IPAddressOrRange : public ASN1_Object { + public: + void encode_into(DER_Encoder&) const override; + void decode_from(class BER_Decoder& from) override; + + IPAddress min() const { return m_min; } + + IPAddress max() const { return m_max; } + + void set_min(const IPAddress& min) { m_min = min; } + + void set_max(const IPAddress& max) { m_max = max; } + + private: + IPAddress m_min; + IPAddress m_max; + }; + + template + class IPAddressChoice : public ASN1_Object { + public: + void encode_into(DER_Encoder&) const override; + void decode_from(BER_Decoder& from) override; + + const std::optional>>& range() const { return m_ip_addr_ranges; } + + void set_range(const std::optional>>& range) { m_ip_addr_ranges = range; } + + private: + std::optional>> m_ip_addr_ranges; + }; + + class IPAddressFamily : public ASN1_Object { + public: + void encode_into(DER_Encoder&) const override; + void decode_from(BER_Decoder& from) override; + + const std::vector& addr_family() const { return m_addr_family; } + + void set_addr_family(const std::vector& addr_family) { m_addr_family = addr_family; } + + const std::variant, IPAddressChoice>& addr_choice() const { + return m_ip_addr_choice; + } + + void set_addr_choice(const std::variant, IPAddressChoice>& choice) { + m_ip_addr_choice = choice; + } + + private: + std::vector m_addr_family; + std::variant, IPAddressChoice> m_ip_addr_choice; + }; + + IPAddressBlocks() = default; + + std::unique_ptr copy() const override { return std::make_unique(*this); } + + static OID static_oid() { return OID("1.3.6.1.5.5.7.1.7"); } + + OID oid_of() const override { return static_oid(); } + + const std::vector& addr_blocks() const { return m_ip_addr_blocks; } + + void set_addr_blocks(const std::vector& addr_blocks) { m_ip_addr_blocks = addr_blocks; } + + private: + std::string oid_name() const override { return "PKIX.ipAddrBlocks"; } + + bool should_encode() const override { return true; } + + std::vector encode_inner() const override; + void decode_inner(const std::vector&) override; + + std::vector m_ip_addr_blocks; +}; + +/** + * IP Adress Blocks Extension + * + * RFC 3779 X.509 Extensions for AS ID + * +*/ +class BOTAN_PUBLIC_API(3, 6) ASBlocks final : public Certificate_Extension { + public: + typedef uint32_t asnum_t; + + class BOTAN_PUBLIC_API(3, 5) ASIdOrRange : public ASN1_Object { + public: + void encode_into(DER_Encoder&) const override; + void decode_from(class BER_Decoder& from) override; + + asnum_t min() const { return m_min; } + + asnum_t max() const { return m_max; } + + void set_min(asnum_t min) { m_min = min; } + + void set_max(asnum_t max) { m_max = max; } + + private: + asnum_t m_min = 0; + asnum_t m_max = 0; + std::vector encode_asnum(asnum_t) const; + }; + + class BOTAN_PUBLIC_API(3, 5) ASIdentifierChoice : public ASN1_Object { + public: + void encode_into(DER_Encoder&) const override; + void decode_from(class BER_Decoder& from) override; + + const std::optional>& ranges() const { return m_as_ranges; } + + void set_ranges(const std::optional>& ranges) { m_as_ranges = ranges; } + + private: + std::optional> m_as_ranges; + }; + + class BOTAN_PUBLIC_API(3, 5) ASIdentifiers : public ASN1_Object { + public: + void encode_into(DER_Encoder&) const override; + void decode_from(class BER_Decoder& from) override; + + const std::optional& asnum() const { return m_asnum; } + + const std::optional& rdi() const { return m_rdi; } + + void set_asnum(const std::optional& asnum) { m_asnum = asnum; } + + void set_rdi(const std::optional& rdi) { m_rdi = rdi; } + + private: + std::optional m_asnum; + std::optional m_rdi; + }; + + ASBlocks() = default; + + std::unique_ptr copy() const override { return std::make_unique(*this); } + + static OID static_oid() { return OID("1.3.6.1.5.5.7.1.8"); } + + OID oid_of() const override { return static_oid(); } + + const ASIdentifiers& as_identifiers() const { return m_as_identifiers; } + + void set_as_identifiers(const ASIdentifiers& as_identifiers) { m_as_identifiers = as_identifiers; } + + private: + ASIdentifiers m_as_identifiers; + + std::string oid_name() const override { return "PKIX.autonomousSysIds"; } + + bool should_encode() const override { return true; } + + std::vector encode_inner() const override; + void decode_inner(const std::vector&) override; +}; + /** * An unknown X.509 extension * Will add a failure to the path validation result, if critical diff --git a/src/tests/data/x509/x509test/ASNumberCert.pem b/src/tests/data/x509/x509test/ASNumberCert.pem new file mode 100644 index 00000000000..de0bc52ca7c --- /dev/null +++ b/src/tests/data/x509/x509test/ASNumberCert.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBqDCCAU6gAwIBAgIRALPImt78hhfZi1Y9pXp9uPkwCgYIKoZIzj0EAwIwADAe +Fw0yNDEwMjExNDQ1MTBaFw0yNTEwMjExNDQ1MTBaMAAwWTATBgcqhkjOPQIBBggq +hkjOPQMBBwNCAAQYLa6kWGm3hE1ug3BVUaui+Ui013pu/ZTeCKYU++tQEjGydJyO +UCzFDjuMZgu76+iaGWfa0PlN2pPFoIQoJduAo4GoMIGlME0GCCsGAQUFBwEIAQH/ +BD4wPKAbMBkwBwIBAAICA+cCAhOyMAoCAQACBQD/////oR0wGzAIAgIE0gICFi4C +AwCAADAKAgEAAgUA/////zAhBgNVHQ4EGgQYGz6Wu2X5h8+j64aiGIj9ts4i+J6R +lBMHMAwGA1UdEwEB/wQCMAAwIwYDVR0jBBwwGoAYGz6Wu2X5h8+j64aiGIj9ts4i ++J6RlBMHMAoGCCqGSM49BAMCA0gAMEUCIG+x6GaNAKDT2Gs9Jh7rTtAd8KAP/MCC +orUYhAug4kzQAiEApwyX0MvUoZV9fUg0AyN79OCbt0XPneyjdwYPSk3nmyI= +-----END CERTIFICATE----- diff --git a/src/tests/data/x509/x509test/ASNumberInherit.pem b/src/tests/data/x509/x509test/ASNumberInherit.pem new file mode 100644 index 00000000000..7fe72cec0ff --- /dev/null +++ b/src/tests/data/x509/x509test/ASNumberInherit.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBiTCCATCgAwIBAgIRAIxkvUFe24qH+RH0D814mEswCgYIKoZIzj0EAwIwADAe +Fw0yNDEwMjIxMDQwMTNaFw0yNTEwMjIxMDQwMTNaMAAwWTATBgcqhkjOPQIBBggq +hkjOPQMBBwNCAAS8OgRLt85kZt8M5MGKcwXyOkUXoylpsp3gKVnQukeEVUPzhYUT +t/nAC9s6tlqQx06aLo4NMpC/ZiLjfqRoh7/Co4GKMIGHMC8GCCsGAQUFBwEIAQH/ +BCAwHqACBQChGDAWMAgCAgTSAgIWLjAKAgEAAgUA/////zAhBgNVHQ4EGgQYEekx +OowtPJb0QL2dSh4YuqEhfAEZh6g6MAwGA1UdEwEB/wQCMAAwIwYDVR0jBBwwGoAY +EekxOowtPJb0QL2dSh4YuqEhfAEZh6g6MAoGCCqGSM49BAMCA0cAMEQCIG5s6rM9 +fpV76Ydij83G5dfNw8xq/PKohCQAsRc5BFP1AiANm2/BiqB6yzNO3t+1PFdjgpFu +8zYpwnxA4Q4yEvKDxg== +-----END CERTIFICATE----- diff --git a/src/tests/data/x509/x509test/ASNumberOnly.pem b/src/tests/data/x509/x509test/ASNumberOnly.pem new file mode 100644 index 00000000000..db3b4fd841e --- /dev/null +++ b/src/tests/data/x509/x509test/ASNumberOnly.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBhjCCASugAwIBAgIRAPeuZ8n5f7uJoOnwr0vpOsMwCgYIKoZIzj0EAwIwADAe +Fw0yNDEwMjIxMDM4MDFaFw0yNTEwMjIxMDM4MDFaMAAwWTATBgcqhkjOPQIBBggq +hkjOPQMBBwNCAAQs/GF1Owcxm3dS3BJtIrAPZkI2WSXet77azlDkRx9LTqjmDi53 +xRbBGTIeOQ3wOtXWH2QLFUWnBvLcGrb12OFCo4GFMIGCMCoGCCsGAQUFBwEIAQH/ +BBswGaAXMBUwBwIBAAICA+cwCgIBAAIFAP////8wIQYDVR0OBBoEGPCRR4/g4jN6 +euAgVpPhG1+vr+YBL+OmwjAMBgNVHRMBAf8EAjAAMCMGA1UdIwQcMBqAGPCRR4/g +4jN6euAgVpPhG1+vr+YBL+OmwjAKBggqhkjOPQQDAgNJADBGAiEAsucYpXQTeSoC ++DGPyrVFRS9XaimDHcDIG+VDKbRhmp8CIQDcoCqItPWoGZjF/PzW6L8CdhBzNaAP +s424AISGuExA7A== +-----END CERTIFICATE----- diff --git a/src/tests/data/x509/x509test/ASRdiOnly.pem b/src/tests/data/x509/x509test/ASRdiOnly.pem new file mode 100644 index 00000000000..20bd15e9eaa --- /dev/null +++ b/src/tests/data/x509/x509test/ASRdiOnly.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBhjCCASygAwIBAgIRANz2VZ/ccV2i/GgObA+fDU4wCgYIKoZIzj0EAwIwADAe +Fw0yNDEwMjIxMDM5MjZaFw0yNTEwMjIxMDM5MjZaMAAwWTATBgcqhkjOPQIBBggq +hkjOPQMBBwNCAATpsmi/80tOyt9nDOqJzNTVox3wOGZSEwGXeMNTR0cbytK9h+t8 +Ea3+dl6LeXvo423FZd0TNPxRrjaLYFpFjX4Ko4GGMIGDMCsGCCsGAQUFBwEIAQH/ +BBwwGqEYMBYwCAICBNICAhYuMAoCAQACBQD/////MCEGA1UdDgQaBBhMamGZIJk1 +k//8v1K14lioRkSxGj2ryhIwDAYDVR0TAQH/BAIwADAjBgNVHSMEHDAagBhMamGZ +IJk1k//8v1K14lioRkSxGj2ryhIwCgYIKoZIzj0EAwIDSAAwRQIhAMHro3vcKus9 +Id+1hnMeffZL/CWFOSTgtKjX7OMbQOK8AiBijOvIranrc1X0OwtvM2A4bJi035G9 +e8BeRHCpaJGitg== +-----END CERTIFICATE----- diff --git a/src/tests/data/x509/x509test/IPAddrBlocksAll.pem b/src/tests/data/x509/x509test/IPAddrBlocksAll.pem new file mode 100644 index 00000000000..847e7d47a94 --- /dev/null +++ b/src/tests/data/x509/x509test/IPAddrBlocksAll.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIID2TCCAsGgAwIBAgIIDCV8W/5Tqq8wDQYJKoZIhvcNAQELBQAwYTENMAsGA1UE +AxMEWk9SQjELMAkGA1UEBhMCREUxDTALBgNVBAgTBFRodXIxEzARBgNVBAoTClRV +IElsbWVuYXUxHzAdBgNVBAsTFlRlbGVtYXRpay9SZWNobmVybmV0emUwHhcNMjQx +MDAyMTIxMDM4WhcNMjcxMDAyMTIxMDM4WjBhMQ0wCwYDVQQDEwRaT1JCMQswCQYD +VQQGEwJERTENMAsGA1UECBMEVGh1cjETMBEGA1UEChMKVFUgSWxtZW5hdTEfMB0G +A1UECxMWVGVsZW1hdGlrL1JlY2huZXJuZXR6ZTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAKS8eqobCYN9/Gj41lEVvYxkBBj0tWTVKCavNRPtAPpATsbO +hGEDO0Cvt2WZZMBTjXdiCjJkdy8aHfsg4SDOK9GUlxCAR7jL1XfFeHE2Q2CWBM8J +NDk+Kx7Nxj1TBY//rTf2gPiu/CDMQPpihTH0kGUw+dR2zybjj0d3h1nQAiVWaauf +A+QZ1qcgpXpSp9r0Jds+GzCW9119oglPVMgbQGR8ExO9/gU3VS15MowZ+lonGCa7 +KSd8rO+rUbDvdZ3Gu3C00yR8Dsft4/1YqSYtdeKD87AdGu3wkx62Ia4lwarjWYwE +mmbOzEXddy/rM7eFEgCIPecxk/8eMb3MxB1Y2tsCAwEAAaOBlDCBkTCBjgYIKwYB +BQUHAQcEgYEwfzAmBAIAATAgAwQHwKgAAwMBwagwDAMDA8KoAwUAw68BAgMFAMSo +AAEwVQQCAAIwTwMKB/qAAAAAAAAAAAMGA/4gAAAAAxEAIAMAAGgpNDUEIBDFAAAA +xDAmAxEAqwEAAAAAAAAAAAAAAAAAAQMRAM0CAAAAAAAAAAAAAAAAAAIwDQYJKoZI +hvcNAQELBQADggEBAA1vysHUycl6/ij2b6pXlvei4Qni1laHGJT/8b2YW2Q3U0uc +V4WMy+nKR9/IDpFc03kZW9ihe7zbbJcoINaKq3UTfEeMcLbzDSzFFaKUANv/C2vx +sUihUo1ojle4EmmVYyYXeiZiu+46aUzuUJuWddTs4kJdNUxFkTKMmhdiGSosGKvz +wRqXj5pG1iEQZmZYDWrricVFkGuwcbAbWTtQqh+cSTt+1sKi4FwL6kkCH9kG5D+1 +/zugVwhXRgguSmqMixpNowMmiDJggzIruGGeJc3ubKuvAnRJmW4VZCXXbVDNAPXi +HDOjomC4OO17uOrWnLht/oiJ+VUhjkFtorO2RLY= +-----END CERTIFICATE----- diff --git a/src/tests/unit_x509.cpp b/src/tests/unit_x509.cpp index 50b1c2a0510..f70b025589f 100644 --- a/src/tests/unit_x509.cpp +++ b/src/tests/unit_x509.cpp @@ -1,6 +1,7 @@ /* * (C) 2009,2019 Jack Lloyd * (C) 2016 René Korthaus, Rohde & Schwarz Cybersecurity +* (C) 2024 Anton Einax, Dominik Schricker * * Botan is released under the Simplified BSD License (see license.txt) */ @@ -13,11 +14,15 @@ #include #include #include + #include #include #include #include #include #include + #include + #include + #include #endif namespace Botan_Tests { @@ -1576,6 +1581,611 @@ Test::Result test_x509_tn_auth_list_extension_decode() { return result; } +Test::Result test_x509_ip_addr_blocks_extension_decode() { + Test::Result result("X509 IP Address Block decode"); + result.start_timer(); + const std::string filename("IPAddrBlocksAll.pem"); + + Botan::X509_Certificate cert(Test::data_file("x509/x509test/" + filename)); + + using Botan::Cert_Extension::IPAddressBlocks; + + auto ip_addr_blocks = cert.v3_extensions().get_extension_object_as(); + + const auto& addr_blocks = ip_addr_blocks->addr_blocks(); + result.confirm("cert has IPAddrBlocks extension", ip_addr_blocks != nullptr, true); + result.confirm("cert has exactly one IpAddrBlock", addr_blocks.size() == 2, true); + + const auto& ipv4block = + std::get>(addr_blocks[0].addr_choice()); + const auto& ipv6block = + std::get>(addr_blocks[1].addr_choice()); + + auto& v4_blocks = ipv4block.range().value(); + + result.confirm("ipv4 block 0 min", v4_blocks[0].min().value() == std::array{192, 168, 0, 0}, true); + result.confirm("ipv4 block 0 max", v4_blocks[0].max().value() == std::array{192, 168, 127, 255}, true); + + result.confirm("ipv4 block 1 min", v4_blocks[1].min().value() == std::array{193, 168, 0, 0}, true); + result.confirm("ipv4 block 1 max", v4_blocks[1].max().value() == std::array{193, 169, 255, 255}, true); + + result.confirm("ipv4 block 2 min", v4_blocks[2].min().value() == std::array{194, 168, 0, 0}, true); + result.confirm("ipv4 block 2 max", v4_blocks[2].max().value() == std::array{195, 175, 1, 2}, true); + + result.confirm("ipv4 block 3 min", v4_blocks[3].min().value() == std::array{196, 168, 0, 1}, true); + result.confirm("ipv4 block 3 max", v4_blocks[3].max().value() == std::array{196, 168, 0, 1}, true); + + auto& v6_blocks = ipv6block.range().value(); + + result.confirm("ipv6 block 0 min", + v6_blocks[0].min().value() == + std::array{ + 0xfa, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + true); + result.confirm("ipv6 block 0 max", + v6_blocks[0].max().value() == + std::array{ + 0xfa, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + true); + + result.confirm("ipv6 block 1 min", + v6_blocks[1].min().value() == + std::array{ + 0xfe, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + true); + result.confirm("ipv6 block 1 max", + v6_blocks[1].max().value() == + std::array{ + 0xfe, 0x20, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + true); + + result.confirm("ipv6 block 2 min", + v6_blocks[2].min().value() == + std::array{ + 0x20, 0x03, 0x00, 0x00, 0x68, 0x29, 0x34, 0x35, 0x04, 0x20, 0x10, 0xc5, 0x00, 0x00, 0x00, 0xc4}, + true); + result.confirm("ipv6 block 2 max", + v6_blocks[2].max().value() == + std::array{ + 0x20, 0x03, 0x00, 0x00, 0x68, 0x29, 0x34, 0x35, 0x04, 0x20, 0x10, 0xc5, 0x00, 0x00, 0x00, 0xc4}, + true); + + result.confirm("ipv6 block 3 min", + v6_blocks[3].min().value() == + std::array{ + 0xab, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}, + true); + result.confirm("ipv6 block 3 max", + v6_blocks[3].max().value() == + std::array{ + 0xcd, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02}, + true); + + result.end_timer(); + return result; +} + +Test::Result test_x509_ip_addr_blocks_extension_encode() { + Test::Result result("X509 IP Address Block encode"); + result.start_timer(); + + using Botan::Cert_Extension::IPAddressBlocks; + + auto rng = Test::new_rng(__func__); + + const std::string sig_algo{"RSA"}; + const std::string hash_fn{"SHA-256"}; + const std::string padding_method{"EMSA3(SHA-256)"}; + + for(size_t i = 0; i < 64; i++) { + bool push_ipv4_ranges = i & 1; + bool push_ipv6_ranges = i >> 1 & 1; + bool inherit_ipv4 = i >> 2 & 1; + bool inherit_ipv6 = i >> 3 & 1; + bool push_ipv4_family = i >> 4 & 1; + bool push_ipv6_family = i >> 5 & 1; + + auto ca_key = make_a_private_key(sig_algo, *rng); + result.require("CA key", ca_key != nullptr); + const auto ca_cert = Botan::X509::create_self_signed_cert(ca_opts(), *ca_key, hash_fn, *rng); + Botan::X509_CA ca(ca_cert, *ca_key, hash_fn, padding_method, *rng); + + auto key = make_a_private_key(sig_algo, *rng); + + Botan::X509_Cert_Options opts1 = req_opts1(sig_algo); + + auto ipv4_1 = IPAddressBlocks::IPAddress(); + ipv4_1.set_from_bytes({123, 123, 2, 1}); + auto ipv4_2 = IPAddressBlocks::IPAddress(); + ipv4_2.set_from_bytes({255, 255, 255, 255}); + + // encoded as min, max + auto ipv4_range_1_min = IPAddressBlocks::IPAddress(); + ipv4_range_1_min.set_from_bytes({127, 0, 0, 1}); + auto ipv4_range_1_max = IPAddressBlocks::IPAddress(); + ipv4_range_1_max.set_from_bytes({189, 5, 7, 255}); + + // encoded as prefix + auto ipv4_range_2_min = IPAddressBlocks::IPAddress(); + ipv4_range_2_min.set_from_bytes({190, 5, 0, 0}); + auto ipv4_range_2_max = IPAddressBlocks::IPAddress(); + ipv4_range_2_max.set_from_bytes({190, 5, 127, 255}); + + auto ipv6_1 = IPAddressBlocks::IPAddress(); + ipv6_1.set_from_bytes( + {0xab, 0xcd, 0xde, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + auto ipv6_2 = IPAddressBlocks::IPAddress(); + ipv6_2.set_from_bytes( + {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}); + + // encoded as min, max + auto ipv6_range_1_min = IPAddressBlocks::IPAddress(); + ipv6_range_1_min.set_from_bytes( + {0xaf, 0x23, 0x34, 0x45, 0x67, 0x2a, 0x7d, 0xef, 0x8c, 0x00, 0x00, 0x00, 0x66, 0x00, 0x52, 0x00}); + + auto ipv6_range_1_max = IPAddressBlocks::IPAddress(); + ipv6_range_1_max.set_from_bytes( + {0xaf, 0xcd, 0xde, 0xf0, 0x00, 0x0f, 0xee, 0x00, 0xbb, 0x4a, 0x9b, 0x00, 0x00, 0x4c, 0x00, 0xcc}); + + // encoded as prefix + auto ipv6_range_2_min = IPAddressBlocks::IPAddress(); + ipv6_range_2_min.set_from_bytes( + {0xbf, 0xcd, 0xde, 0xf0, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + auto ipv6_range_2_max = IPAddressBlocks::IPAddress(); + ipv6_range_2_max.set_from_bytes( + {0xbf, 0xcd, 0xde, 0xf0, 0x00, 0x00, 0x00, 0x07, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}); + + auto ipv4_range_1 = IPAddressBlocks::IPAddressOrRange(); + ipv4_range_1.set_min(ipv4_1); + ipv4_range_1.set_max(ipv4_1); + auto ipv4_range_2 = IPAddressBlocks::IPAddressOrRange(); + ipv4_range_2.set_min(ipv4_range_1_min); + ipv4_range_2.set_max(ipv4_range_1_max); + auto ipv4_range_3 = IPAddressBlocks::IPAddressOrRange(); + ipv4_range_3.set_min(ipv4_range_2_min); + ipv4_range_3.set_max(ipv4_range_2_max); + auto ipv4_range_4 = IPAddressBlocks::IPAddressOrRange(); + ipv4_range_4.set_min(ipv4_2); + ipv4_range_4.set_max(ipv4_2); + + auto ipv6_range_1 = IPAddressBlocks::IPAddressOrRange(); + ipv6_range_1.set_min(ipv6_1); + ipv6_range_1.set_max(ipv6_1); + auto ipv6_range_2 = IPAddressBlocks::IPAddressOrRange(); + ipv6_range_2.set_min(ipv6_range_1_min); + ipv6_range_2.set_max(ipv6_range_1_max); + auto ipv6_range_3 = IPAddressBlocks::IPAddressOrRange(); + ipv6_range_3.set_min(ipv6_range_2_min); + ipv6_range_3.set_max(ipv6_range_2_max); + auto ipv6_range_4 = IPAddressBlocks::IPAddressOrRange(); + ipv6_range_4.set_min(ipv6_2); + ipv6_range_4.set_max(ipv6_2); + + std::vector> ipv4_ranges; + if(push_ipv4_ranges) { + ipv4_ranges.push_back(ipv4_range_1); + ipv4_ranges.push_back(ipv4_range_2); + ipv4_ranges.push_back(ipv4_range_3); + ipv4_ranges.push_back(ipv4_range_4); + } + + std::vector> ipv6_ranges; + if(push_ipv6_ranges) { + ipv6_ranges.push_back(ipv6_range_1); + ipv6_ranges.push_back(ipv6_range_2); + ipv6_ranges.push_back(ipv6_range_3); + ipv6_ranges.push_back(ipv6_range_4); + } + + auto ipv4_addr_choice = IPAddressBlocks::IPAddressChoice(); + if(!inherit_ipv4) { + ipv4_addr_choice.set_range(ipv4_ranges); + } + + auto ipv6_addr_choice = IPAddressBlocks::IPAddressChoice(); + if(!inherit_ipv6) { + ipv6_addr_choice.set_range(ipv6_ranges); + } + + auto ipv4_addr_family = IPAddressBlocks::IPAddressFamily(); + ipv4_addr_family.set_addr_family({0, 1}); + ipv4_addr_family.set_addr_choice(ipv4_addr_choice); + + auto ipv6_addr_family = IPAddressBlocks::IPAddressFamily(); + ipv6_addr_family.set_addr_family({0, 2}); + ipv6_addr_family.set_addr_choice(ipv6_addr_choice); + + std::vector addr_blocks; + if(push_ipv4_family) { + addr_blocks.push_back(ipv4_addr_family); + } + if(push_ipv6_family) { + addr_blocks.push_back(ipv6_addr_family); + } + + std::unique_ptr blocks = std::make_unique(); + blocks->set_addr_blocks(addr_blocks); + + opts1.extensions.add(std::move(blocks)); + + Botan::PKCS10_Request req = Botan::X509::create_cert_req(opts1, *key, hash_fn, *rng); + Botan::X509_Certificate cert = ca.sign_request(req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); + { + auto ip_blocks = cert.v3_extensions().get_extension_object_as(); + result.confirm("cert has IPAddrBlocks extension", ip_blocks != nullptr, true); + + const auto& dec_addr_blocks = ip_blocks->addr_blocks(); + if(!push_ipv4_family && !push_ipv6_family) { + result.confirm("no address family entries", dec_addr_blocks.empty(), true); + continue; + } + + if(push_ipv4_family) { + auto family = dec_addr_blocks[0]; + result.confirm("ipv4 family afi", ipv4_addr_family.addr_family() == family.addr_family(), true); + auto choice = + std::get>(family.addr_choice()); + + if(!inherit_ipv4) { + auto ranges = choice.range().value(); + if(push_ipv4_ranges) { + result.confirm("ipv4 entry 0 min", ranges[0].min().value() == ipv4_range_1.min().value(), true); + result.confirm("ipv4 entry 0 max", ranges[0].max().value() == ipv4_range_1.max().value(), true); + result.confirm("ipv4 entry 1 min", ranges[1].min().value() == ipv4_range_2.min().value(), true); + result.confirm("ipv4 entry 1 max", ranges[1].max().value() == ipv4_range_2.max().value(), true); + result.confirm("ipv4 entry 2 min", ranges[2].min().value() == ipv4_range_3.min().value(), true); + result.confirm("ipv4 entry 2 max", ranges[2].max().value() == ipv4_range_3.max().value(), true); + result.confirm("ipv4 entry 3 min", ranges[3].min().value() == ipv4_range_4.min().value(), true); + result.confirm("ipv4 entry 3 max", ranges[3].max().value() == ipv4_range_4.max().value(), true); + } else { + result.confirm("ipv4 range has no entries", ranges.empty(), true); + } + } else { + result.confirm("ipv4 family inherit", choice.range().has_value(), false); + } + } + + if(push_ipv6_family) { + auto family = dec_addr_blocks[dec_addr_blocks.size() - 1]; + result.confirm("ipv6 family afi", ipv6_addr_family.addr_family() == family.addr_family(), true); + auto choice = + std::get>(family.addr_choice()); + if(!inherit_ipv6) { + auto ranges = choice.range().value(); + if(push_ipv6_ranges) { + result.confirm("ipv6 entry 0 min", ranges[0].min().value() == ipv6_range_1.min().value(), true); + result.confirm("ipv6 entry 0 max", ranges[0].max().value() == ipv6_range_1.max().value(), true); + result.confirm("ipv6 entry 1 min", ranges[1].min().value() == ipv6_range_2.min().value(), true); + result.confirm("ipv6 entry 1 max", ranges[1].max().value() == ipv6_range_2.max().value(), true); + result.confirm("ipv6 entry 2 min", ranges[2].min().value() == ipv6_range_3.min().value(), true); + result.confirm("ipv6 entry 2 max", ranges[2].max().value() == ipv6_range_3.max().value(), true); + result.confirm("ipv6 entry 3 min", ranges[3].min().value() == ipv6_range_4.min().value(), true); + result.confirm("ipv6 entry 3 max", ranges[3].max().value() == ipv6_range_4.max().value(), true); + } else { + result.confirm("ipv6 range has no entries", ranges.empty(), true); + } + } else { + result.confirm("ipv6 family inherit", choice.range().has_value(), false); + } + } + } + } + + result.end_timer(); + return result; +} + +Test::Result test_x509_ip_addr_blocks_extension_encode_edge_cases() { + Test::Result result("X509 IP Address Block encode edge cases"); + result.start_timer(); + + using Botan::Cert_Extension::IPAddressBlocks; + + auto rng = Test::new_rng(__func__); + + const std::string sig_algo{"RSA"}; + const std::string hash_fn{"SHA-256"}; + const std::string padding_method{"EMSA3(SHA-256)"}; + + // trailing 0s, trailing 1s, and some arbitrary values + std::vector edge_values = {2, 4, 8, 16, 32, 64, 128, 0, 1, 3, 7, 15, 31, 63, 127, 255, 12, 46, 123, 234}; + + for(size_t i = 0; i < edge_values.size(); i++) { + for(size_t j = 0; j < 18; j++) { + auto ca_key = make_a_private_key(sig_algo, *rng); + result.require("CA key", ca_key != nullptr); + const auto ca_cert = Botan::X509::create_self_signed_cert(ca_opts(), *ca_key, hash_fn, *rng); + Botan::X509_CA ca(ca_cert, *ca_key, hash_fn, padding_method, *rng); + + auto key = make_a_private_key(sig_algo, *rng); + + Botan::X509_Cert_Options opts1 = req_opts1(sig_algo); + + auto address_min = IPAddressBlocks::IPAddress(); + auto address_max = IPAddressBlocks::IPAddress(); + + std::vector min_bytes = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + std::vector max_bytes = { + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}; + + min_bytes[15 - std::max(0UL, j - 2)] = edge_values[i]; + max_bytes[15 - std::min(15UL, j)] = edge_values[i]; + + address_min.set_from_bytes(min_bytes); + address_max.set_from_bytes(max_bytes); + + auto ipv6_range = IPAddressBlocks::IPAddressOrRange(); + ipv6_range.set_min(address_min); + ipv6_range.set_max(address_max); + + std::vector> ipv6_ranges; + ipv6_ranges.push_back(ipv6_range); + + auto ipv6_addr_choice = IPAddressBlocks::IPAddressChoice(); + ipv6_addr_choice.set_range(ipv6_ranges); + + auto ipv6_addr_family = IPAddressBlocks::IPAddressFamily(); + ipv6_addr_family.set_addr_family({0, 2}); + ipv6_addr_family.set_addr_choice(ipv6_addr_choice); + + std::vector addr_blocks; + addr_blocks.push_back(ipv6_addr_family); + + std::unique_ptr blocks = std::make_unique(); + blocks->set_addr_blocks(addr_blocks); + + opts1.extensions.add(std::move(blocks)); + + Botan::PKCS10_Request req = Botan::X509::create_cert_req(opts1, *key, hash_fn, *rng); + Botan::X509_Certificate cert = ca.sign_request(req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); + { + auto ip_blocks = cert.v3_extensions().get_extension_object_as(); + result.confirm("cert has IPAddrBlocks extension", ip_blocks != nullptr, true); + const auto& dec_addr_blocks = ip_blocks->addr_blocks(); + auto family = dec_addr_blocks[0]; + result.confirm("ipv6 family afi", ipv6_addr_family.addr_family() == family.addr_family(), true); + auto choice = + std::get>(family.addr_choice()); + auto ranges = choice.range().value(); + + result.confirm("ipv6 edge case min", ranges[0].min().value() == ipv6_range.min().value(), true); + result.confirm("ipv6 edge case max", ranges[0].max().value() == ipv6_range.max().value(), true); + } + } + } + result.end_timer(); + return result; +} + +Test::Result test_x509_as_blocks_extension_decode() { + Test::Result result("X509 AS Block decode"); + result.start_timer(); + using Botan::Cert_Extension::ASBlocks; + + { + const std::string filename("ASNumberCert.pem"); + Botan::X509_Certificate cert(Test::data_file("x509/x509test/" + filename)); + + auto as_blocks = cert.v3_extensions().get_extension_object_as(); + + const auto& identifier = as_blocks->as_identifiers(); + result.confirm("cert has ASBlock extension", as_blocks != nullptr, true); + + const auto& asnum = identifier.asnum().value().ranges().value(); + const auto& rdi = identifier.rdi().value().ranges().value(); + + result.confirm("asnum entry 0 min", asnum[0].min() == 0, true); + result.confirm("asnum entry 0 max", asnum[0].max() == 999, true); + + result.confirm("asnum entry 1 min", asnum[1].min() == 5042, true); + result.confirm("asnum entry 1 max", asnum[1].max() == 5042, true); + + result.confirm("asnum entry 2 min", asnum[2].min() == 0, true); + result.confirm("asnum entry 2 max", asnum[2].max() == 4294967295, true); + + result.confirm("rdi entry 0 min", rdi[0].min() == 1234, true); + result.confirm("rdi entry 0 max", rdi[0].max() == 5678, true); + + result.confirm("rdi entry 1 min", rdi[1].min() == 32768, true); + result.confirm("rdi entry 1 max", rdi[1].max() == 32768, true); + + result.confirm("rdi entry 2 min", rdi[2].min() == 0, true); + result.confirm("rdi entry 2 max", rdi[2].max() == 4294967295, true); + } + { + const std::string filename("ASNumberOnly.pem"); + Botan::X509_Certificate cert(Test::data_file("x509/x509test/" + filename)); + + auto as_blocks = cert.v3_extensions().get_extension_object_as(); + + const auto& identifier = as_blocks->as_identifiers(); + result.confirm("cert has ASBlock extension", as_blocks != nullptr, true); + + const auto& asnum = identifier.asnum().value().ranges().value(); + result.confirm("cert has no RDI entries", identifier.rdi().has_value(), false); + + result.confirm("asnum entry 0 min", asnum[0].min() == 0, true); + result.confirm("asnum entry 0 max", asnum[0].max() == 999, true); + + result.confirm("asnum entry 1 min", asnum[1].min() == 0, true); + result.confirm("asnum entry 1 max", asnum[1].max() == 4294967295, true); + } + { + const std::string filename("ASRdiOnly.pem"); + Botan::X509_Certificate cert(Test::data_file("x509/x509test/" + filename)); + + auto as_blocks = cert.v3_extensions().get_extension_object_as(); + + const auto& identifier = as_blocks->as_identifiers(); + result.confirm("cert has ASBlock extension", as_blocks != nullptr, true); + + result.confirm("cert has no ASNUM entries", identifier.asnum().has_value(), false); + const auto& rdi = identifier.rdi().value().ranges().value(); + + result.confirm("rdi entry 0 min", rdi[0].min() == 1234, true); + result.confirm("rdi entry 0 max", rdi[0].max() == 5678, true); + + result.confirm("rdi entry 1 min", rdi[1].min() == 0, true); + result.confirm("rdi entry 1 max", rdi[1].max() == 4294967295, true); + } + { + const std::string filename("ASNumberInherit.pem"); + Botan::X509_Certificate cert(Test::data_file("x509/x509test/" + filename)); + + auto as_blocks = cert.v3_extensions().get_extension_object_as(); + + const auto& identifier = as_blocks->as_identifiers(); + result.confirm("cert has ASBlock extension", as_blocks != nullptr, true); + + result.confirm("asnum has no entries", identifier.asnum().value().ranges().has_value(), false); + const auto& rdi = identifier.rdi().value().ranges().value(); + + result.confirm("rdi entry 0 min", rdi[0].min() == 1234, true); + result.confirm("rdi entry 0 max", rdi[0].max() == 5678, true); + + result.confirm("rdi entry 1 min", rdi[1].min() == 0, true); + result.confirm("rdi entry 1 max", rdi[1].max() == 4294967295, true); + } + + result.end_timer(); + return result; +} + +Test::Result test_x509_as_blocks_extension_encode() { + Test::Result result("X509 AS Number encode"); + + using Botan::Cert_Extension::ASBlocks; + + auto rng = Test::new_rng(__func__); + + const std::string sig_algo{"RSA"}; + const std::string hash_fn{"SHA-256"}; + const std::string padding_method{"EMSA3(SHA-256)"}; + + for(size_t i = 0; i < 16; i++) { + bool push_asnum = i & 1; + bool push_rdi = i >> 1 & 1; + bool include_asnum = i >> 2 & 1; + bool include_rdi = i >> 3 & 1; + + ASBlocks::ASIdOrRange asnum_id_or_range0 = ASBlocks::ASIdOrRange(); + asnum_id_or_range0.set_min(0); + asnum_id_or_range0.set_max(999); + + ASBlocks::ASIdOrRange asnum_id_or_range1 = ASBlocks::ASIdOrRange(); + asnum_id_or_range1.set_min(5042); + asnum_id_or_range1.set_max(5042); + + ASBlocks::ASIdOrRange asnum_id_or_range2 = ASBlocks::ASIdOrRange(); + asnum_id_or_range2.set_min(5043); + asnum_id_or_range2.set_max(4294967295); + + ASBlocks::ASIdOrRange rdi_id_or_range0 = ASBlocks::ASIdOrRange(); + rdi_id_or_range0.set_min(1234); + rdi_id_or_range0.set_max(5678); + + ASBlocks::ASIdOrRange rdi_id_or_range1 = ASBlocks::ASIdOrRange(); + rdi_id_or_range1.set_min(32768); + rdi_id_or_range1.set_max(32768); + + ASBlocks::ASIdOrRange rdi_id_or_range2 = ASBlocks::ASIdOrRange(); + rdi_id_or_range2.set_min(32769); + rdi_id_or_range2.set_max(4294967295); + + // both AS and RDI are pushed in incorrect order but must be sorted when encoded + std::vector as_ranges; + if(push_asnum) { + as_ranges.push_back(asnum_id_or_range0); + as_ranges.push_back(asnum_id_or_range1); + as_ranges.push_back(asnum_id_or_range2); + } + + std::vector rdi_ranges; + if(push_rdi) { + rdi_ranges.push_back(rdi_id_or_range0); + rdi_ranges.push_back(rdi_id_or_range1); + rdi_ranges.push_back(rdi_id_or_range2); + } + + ASBlocks::ASIdentifierChoice asnum = ASBlocks::ASIdentifierChoice(); + ASBlocks::ASIdentifierChoice rdi = ASBlocks::ASIdentifierChoice(); + + asnum.set_ranges(std::optional(as_ranges)); + rdi.set_ranges(std::optional(rdi_ranges)); + + ASBlocks::ASIdentifiers ident = ASBlocks::ASIdentifiers(); + + if(include_asnum) { + ident.set_asnum(std::optional(asnum)); + } + if(include_rdi) { + ident.set_rdi(std::optional(rdi)); + } + + std::unique_ptr blocks = std::make_unique(); + blocks->set_as_identifiers(ident); + + auto ca_key = make_a_private_key(sig_algo, *rng); + result.require("CA key", ca_key != nullptr); + const auto ca_cert = Botan::X509::create_self_signed_cert(ca_opts(), *ca_key, hash_fn, *rng); + Botan::X509_CA ca(ca_cert, *ca_key, hash_fn, padding_method, *rng); + auto key = make_a_private_key(sig_algo, *rng); + + Botan::X509_Cert_Options opts1 = req_opts1(sig_algo); + opts1.extensions.add(std::move(blocks)); + + Botan::PKCS10_Request req = Botan::X509::create_cert_req(opts1, *key, hash_fn, *rng); + Botan::X509_Certificate cert = ca.sign_request(req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); + + { + auto as_blocks = cert.v3_extensions().get_extension_object_as(); + result.confirm("cert has ASBlock extension", as_blocks != nullptr, true); + + const auto& identifier = as_blocks->as_identifiers(); + + if(include_asnum) { + const auto& asnum_entries = identifier.asnum().value().ranges().value(); + + if(push_asnum) { + result.confirm("asnum entry 0 min", asnum_entries[0].min() == 0, true); + result.confirm("asnum entry 0 max", asnum_entries[0].max() == 999, true); + + result.confirm("asnum entry 1 min", asnum_entries[1].min() == 5042, true); + result.confirm("asnum entry 1 max", asnum_entries[1].max() == 5042, true); + + result.confirm("asnum entry 2 min", asnum_entries[2].min() == 5043, true); + result.confirm("asnum entry 2 max", asnum_entries[2].max() == 4294967295, true); + } else { + result.confirm("asnum has no entries", asnum_entries.empty(), true); + } + } else { + result.confirm("no asnum entry", identifier.asnum().has_value(), false); + } + + if(include_rdi) { + const auto& rdi_entries = identifier.rdi().value().ranges().value(); + + if(push_rdi) { + result.confirm("rdi entry 0 min", rdi_entries[0].min() == 1234, true); + result.confirm("rdi entry 0 max", rdi_entries[0].max() == 5678, true); + + result.confirm("rdi entry 1 min", rdi_entries[1].min() == 32768, true); + result.confirm("rdi entry 1 max", rdi_entries[1].max() == 32768, true); + + result.confirm("rdi entry 2 min", rdi_entries[2].min() == 32769, true); + result.confirm("rdi entry 2 max", rdi_entries[2].max() == 4294967295, true); + } else { + result.confirm("rdi has no entries", rdi_entries.empty(), true); + } + } else { + result.confirm("rdi has no entry", identifier.rdi().has_value(), false); + } + } + } + + return result; +} + std::vector get_sig_paddings(const std::string& sig_algo, const std::string& hash) { if(sig_algo == "RSA") { return {"EMSA3(" + hash + ")", "EMSA4(" + hash + ")"}; @@ -1729,8 +2339,13 @@ class X509_Cert_Unit_Tests final : public Test { results.push_back(test_verify_gost2012_cert()); results.push_back(test_parse_rsa_pss_cert()); results.push_back(test_x509_tn_auth_list_extension_decode()); + results.push_back(test_x509_ip_addr_blocks_extension_decode()); + results.push_back(test_x509_as_blocks_extension_decode()); #endif + results.push_back(test_x509_ip_addr_blocks_extension_encode()); + results.push_back(test_x509_ip_addr_blocks_extension_encode_edge_cases()); + results.push_back(test_x509_as_blocks_extension_encode()); results.push_back(test_x509_encode_authority_info_access_extension()); results.push_back(test_x509_extension()); results.push_back(test_x509_dates());