diff --git a/src/lib/x509/alt_name.cpp b/src/lib/x509/alt_name.cpp index 1b262aa4bd..d835cc856b 100644 --- a/src/lib/x509/alt_name.cpp +++ b/src/lib/x509/alt_name.cpp @@ -109,6 +109,57 @@ void AlternativeName::encode_into(DER_Encoder& der) const { der.end_cons(); } +namespace { + +std::string check_and_canonicalize_dns_name(std::string_view name) { + if(name.size() > 255) { + throw Decoding_Error("DNS SAN exceeds maximum allowed length"); + } + + if(name.empty()) { + throw Decoding_Error("DNS SAN cannot be empty"); + } + + if(name.starts_with(".")) { + throw Decoding_Error("DNS SAN cannot start with a dot"); + } + + /* + * Table mapping uppercase to lowercase and only including values for valid DNS names + * namely A-Z, a-z, 0-9, hypen, and dot, plus '*' for wildcarding. + */ + // clang-format off + constexpr uint8_t DNS_CHAR_MAPPING[128] = { + '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', + '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', + '\0', '\0', '\0', '\0', '*', '\0', '\0', '-', '.', '\0', '0', '1', '2', '3', '4', '5', '6', '7', '8', + '9', '\0', '\0', '\0', '\0', '\0', '\0', '\0', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', + 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '\0', '\0', '\0', '\0', + '\0', '\0', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', + 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '\0', '\0', '\0', '\0', '\0', + }; + // clang-format on + + std::string canon; + canon.reserve(name.size()); + + for(char c : name) { + const uint8_t cu = static_cast(c); + if(cu >= 128) { + throw Decoding_Error("DNS name must not contain any extended ASCII code points"); + } + const uint8_t mapped = DNS_CHAR_MAPPING[cu]; + if(mapped == 0) { + throw Decoding_Error("Name in DNS SAN includes invalid character"); + } + canon.push_back(static_cast(mapped)); + } + + return canon; +} + +} // namespace + void AlternativeName::decode_from(BER_Decoder& source) { BER_Decoder names = source.start_sequence(); @@ -140,7 +191,7 @@ void AlternativeName::decode_from(BER_Decoder& source) { } else if(obj.is_a(1, ASN1_Class::ContextSpecific)) { add_email(ASN1::to_string(obj)); } else if(obj.is_a(2, ASN1_Class::ContextSpecific)) { - add_dns(ASN1::to_string(obj)); + m_dns.insert(check_and_canonicalize_dns_name(ASN1::to_string(obj))); } else if(obj.is_a(4, ASN1_Class::ContextSpecific | ASN1_Class::Constructed)) { BER_Decoder dec(obj); X509_DN dn; diff --git a/src/lib/x509/x509cert.cpp b/src/lib/x509/x509cert.cpp index b964c0c4ec..26b9e9c888 100644 --- a/src/lib/x509/x509cert.cpp +++ b/src/lib/x509/x509cert.cpp @@ -181,6 +181,20 @@ std::unique_ptr parse_x509_cert_body(const X509_Object& o if(auto ext = data->m_v3_extensions.get_extension_object_as()) { data->m_extended_key_usage = ext->object_identifiers(); + /* + RFC 5280 section 4.2.1.12 + + "This extension indicates one or more purposes ..." + + "If the extension is present, then the certificate MUST only be + used for one of the purposes indicated." + + Thus we reject an EKU extension which is empty, since this indicates + the certificate cannot be used for any purpose. + */ + if(data->m_extended_key_usage.empty()) { + throw Decoding_Error("Certificate has invalid empty EKU extension"); + } } if(auto ext = data->m_v3_extensions.get_extension_object_as()) { diff --git a/src/scripts/ci/setup_gh_actions.sh b/src/scripts/ci/setup_gh_actions.sh index 089bf36071..7d8e074c09 100755 --- a/src/scripts/ci/setup_gh_actions.sh +++ b/src/scripts/ci/setup_gh_actions.sh @@ -156,7 +156,7 @@ if type -p "apt-get"; then elif [ "$TARGET" = "limbo" ]; then sudo apt-get -qq install python3-dateutil - wget -nv https://raw.githubusercontent.com/C2SP/x509-limbo/bd88042508ccfde351b2fee293aebda8971fbebb/limbo.json -O "${SCRIPT_LOCATION}/../../../limbo.json" + wget -nv https://raw.githubusercontent.com/C2SP/x509-limbo/f98aa03f45d108ae4e1bc5a61ec4bd0b8d137559/limbo.json -O "${SCRIPT_LOCATION}/../../../limbo.json" elif [ "$TARGET" = "coverage" ] || [ "$TARGET" = "sanitizer" ]; then if [ "$TARGET" = "coverage" ]; then