From cdba9e8eb53181ad8f826986025545fccca06e24 Mon Sep 17 00:00:00 2001 From: Amos Treiber Date: Mon, 12 Jan 2026 14:26:07 +0100 Subject: [PATCH] X509: Support multiple OCSP responders in AIA Before, only a single OCSP responder URI was parsed and exposed via AIA extension and Botan::X509_Certificate. These now handle multiple OCSP responder URIs as per RFC 5280. Add corresponding test cases. Deprecate the single OCSP responder constructors and accessors. Note that the support for multiple OCSP responders is not propagated to `Botan::OCSP` methods yet. --- src/lib/ffi/ffi_cert.cpp | 10 +--- src/lib/x509/x509_ext.cpp | 12 ++-- src/lib/x509/x509_ext.h | 22 ++++++-- src/lib/x509/x509cert.cpp | 21 +++++-- src/lib/x509/x509cert.h | 7 ++- .../contains_multiple_ocsp_responders.pem | 55 +++++++++++++++++++ src/tests/unit_x509.cpp | 36 ++++++++++++ 7 files changed, 137 insertions(+), 26 deletions(-) create mode 100644 src/tests/data/x509/misc/contains_multiple_ocsp_responders.pem diff --git a/src/lib/ffi/ffi_cert.cpp b/src/lib/ffi/ffi_cert.cpp index 5cd2e26cb01..f2f9f5dc385 100644 --- a/src/lib/ffi/ffi_cert.cpp +++ b/src/lib/ffi/ffi_cert.cpp @@ -372,20 +372,12 @@ int botan_x509_cert_view_string_values(botan_x509_cert_t cert, return BOTAN_FFI_ERROR_OUT_OF_RANGE; }; - auto wrap_or_empty = [](std::string str) -> std::vector { - if(str.empty()) { - return {}; - } else { - return {std::move(str)}; - } - }; - return BOTAN_FFI_VISIT(cert, [=](const Botan::X509_Certificate& c) -> int { switch(value_type) { case BOTAN_X509_CRL_DISTRIBUTION_URLS: return enumerate_crl_distribution_points(c, index); case BOTAN_X509_OCSP_RESPONDER_URLS: - return enumerate(wrap_or_empty(c.ocsp_responder()), index); + return enumerate(c.ocsp_responders(), index); case BOTAN_X509_CA_ISSUERS_URLS: return enumerate(c.ca_issuers(), index); case BOTAN_X509_PEM_ENCODING: diff --git a/src/lib/x509/x509_ext.cpp b/src/lib/x509/x509_ext.cpp index cea8d51270b..89aa22bf1d6 100644 --- a/src/lib/x509/x509_ext.cpp +++ b/src/lib/x509/x509_ext.cpp @@ -632,9 +632,9 @@ std::vector Authority_Information_Access::encode_inner() const { DER_Encoder der(output); der.start_sequence(); - // OCSP - if(!m_ocsp_responder.empty()) { - const ASN1_String url(m_ocsp_responder, ASN1_Type::Ia5String); + // OCSP Responders + for(const auto& ocsp_responder : m_ocsp_responders) { + const ASN1_String url(ocsp_responder, ASN1_Type::Ia5String); der.start_sequence() .encode(OID::from_string("PKIX.OCSP")) .add_object(ASN1_Type(6), ASN1_Class::ContextSpecific, url.value()) @@ -642,8 +642,8 @@ std::vector Authority_Information_Access::encode_inner() const { } // CA Issuers - for(const auto& ca_isser : m_ca_issuers) { - const ASN1_String asn1_ca_issuer(ca_isser, ASN1_Type::Ia5String); + for(const auto& ca_issuer : m_ca_issuers) { + const ASN1_String asn1_ca_issuer(ca_issuer, ASN1_Type::Ia5String); der.start_sequence() .encode(OID::from_string("PKIX.CertificateAuthorityIssuers")) .add_object(ASN1_Type(6), ASN1_Class::ContextSpecific, asn1_ca_issuer.value()) @@ -668,7 +668,7 @@ void Authority_Information_Access::decode_inner(const std::vector& in) const BER_Object name = info.get_next_object(); if(name.is_a(6, ASN1_Class::ContextSpecific)) { - m_ocsp_responder = ASN1::to_string(name); + m_ocsp_responders.push_back(ASN1::to_string(name)); } } if(oid == OID::from_string("PKIX.CertificateAuthorityIssuers")) { diff --git a/src/lib/x509/x509_ext.h b/src/lib/x509/x509_ext.h index 8a930944887..29930e5eaeb 100644 --- a/src/lib/x509/x509_ext.h +++ b/src/lib/x509/x509_ext.h @@ -322,16 +322,28 @@ class BOTAN_PUBLIC_API(2, 0) Certificate_Policies final : public Certificate_Ext class BOTAN_PUBLIC_API(2, 0) Authority_Information_Access final : public Certificate_Extension { public: std::unique_ptr copy() const override { - return std::make_unique(m_ocsp_responder, m_ca_issuers); + return std::make_unique(m_ocsp_responders, m_ca_issuers); } Authority_Information_Access() = default; + BOTAN_DEPRECATED("Use constructor with list of OCSP responders") explicit Authority_Information_Access(std::string_view ocsp, const std::vector& ca_issuers = std::vector()) : - m_ocsp_responder(ocsp), m_ca_issuers(ca_issuers) {} + m_ocsp_responders{std::string(ocsp)}, m_ca_issuers(ca_issuers) {} - std::string ocsp_responder() const { return m_ocsp_responder; } + explicit Authority_Information_Access(std::vector ocsp_responders, + std::vector ca_issuers = std::vector()) : + m_ocsp_responders(std::move(ocsp_responders)), m_ca_issuers(std::move(ca_issuers)) {} + + BOTAN_DEPRECATED("Use ocsp_responders") std::string ocsp_responder() const { + if(m_ocsp_responders.empty()) { + return {}; + } + return m_ocsp_responders[0]; + } + + const std::vector& ocsp_responders() const { return m_ocsp_responders; } static OID static_oid() { return OID({1, 3, 6, 1, 5, 5, 7, 1, 1}); } @@ -342,12 +354,12 @@ class BOTAN_PUBLIC_API(2, 0) Authority_Information_Access final : public Certifi private: std::string oid_name() const override { return "PKIX.AuthorityInformationAccess"; } - bool should_encode() const override { return (!m_ocsp_responder.empty() || !m_ca_issuers.empty()); } + bool should_encode() const override { return (!m_ocsp_responders.empty() || !m_ca_issuers.empty()); } std::vector encode_inner() const override; void decode_inner(const std::vector& in) override; - std::string m_ocsp_responder; + std::vector m_ocsp_responders; std::vector m_ca_issuers; }; diff --git a/src/lib/x509/x509cert.cpp b/src/lib/x509/x509cert.cpp index e1c35d1eda4..135a75d5290 100644 --- a/src/lib/x509/x509cert.cpp +++ b/src/lib/x509/x509cert.cpp @@ -48,7 +48,7 @@ struct X509_Certificate_Data { std::vector m_cert_policies; std::vector m_crl_distribution_points; - std::string m_ocsp_responder; + std::vector m_ocsp_responders; std::vector m_ca_issuers; std::vector m_issuer_dn_bits_sha256; @@ -265,7 +265,7 @@ std::unique_ptr parse_x509_cert_body(const X509_Object& o } if(const auto* ext = data->m_v3_extensions.get_extension_object_as()) { - data->m_ocsp_responder = ext->ocsp_responder(); + data->m_ocsp_responders = ext->ocsp_responders(); data->m_ca_issuers = ext->ca_issuers(); } @@ -538,7 +538,14 @@ bool X509_Certificate::is_critical(std::string_view ex_name) const { } std::string X509_Certificate::ocsp_responder() const { - return data().m_ocsp_responder; + if(data().m_ocsp_responders.empty()) { + return {}; + } + return data().m_ocsp_responders[0]; +} + +const std::vector& X509_Certificate::ocsp_responders() const { + return data().m_ocsp_responders; } std::vector X509_Certificate::ca_issuers() const { @@ -798,8 +805,12 @@ std::string X509_Certificate::to_string() const { } } - if(!ocsp_responder().empty()) { - out << "OCSP responder " << ocsp_responder() << "\n"; + const auto& ocsp_responders = this->ocsp_responders(); + if(!ocsp_responders.empty()) { + out << "OCSP Responders:\n"; + for(const auto& ocsp_responder : ocsp_responders) { + out << " URI: " << ocsp_responder << "\n"; + } } const std::vector ca_issuers = this->ca_issuers(); diff --git a/src/lib/x509/x509cert.h b/src/lib/x509/x509cert.h index 9513a1c90e5..25c7dbf877d 100644 --- a/src/lib/x509/x509cert.h +++ b/src/lib/x509/x509cert.h @@ -330,7 +330,12 @@ class BOTAN_PUBLIC_API(2, 0) X509_Certificate : public X509_Object { /** * Return the listed address of an OCSP responder, or empty if not set */ - std::string ocsp_responder() const; + BOTAN_DEPRECATED("Use ocsp_responders") std::string ocsp_responder() const; + + /** + * Return the listed addresses of OCSP responders, or empty if not set + */ + const std::vector& ocsp_responders() const; /** * Return the listed addresses of ca issuers, or empty if not set diff --git a/src/tests/data/x509/misc/contains_multiple_ocsp_responders.pem b/src/tests/data/x509/misc/contains_multiple_ocsp_responders.pem new file mode 100644 index 00000000000..ce769d3e43e --- /dev/null +++ b/src/tests/data/x509/misc/contains_multiple_ocsp_responders.pem @@ -0,0 +1,55 @@ +-----BEGIN CERTIFICATE----- +MIIFvzCCA6egAwIBAgIUbNlFYpSi5yeivRmg+5P61FgbKucwDQYJKoZIhvcNAQEL +BQAwLjEsMCoGA1UEAwwjVGVzdCBDZXJ0aWZpY2F0ZSB3aXRoIE11bHRpcGxlIE9D +U1AwHhcNMjYwMTEyMTMxNTE0WhcNMzYwMTEwMTMxNTE0WjAuMSwwKgYDVQQDDCNU +ZXN0IENlcnRpZmljYXRlIHdpdGggTXVsdGlwbGUgT0NTUDCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAJ1P8yscKZ5lKggYIxMjRrID52eXuo1K1i5MyllF +qljU5cKrsCHXQRNSS1qIc2k3mYwTUerGT7a4uLG/Z6WHA5xgt6Q4ivSmKvcm56Ta +fiSEgHBxG5BrgemUciCbjNSJTX2N+8urrCbLYkuS6+DLfUdTPy7RGEijBJZ4+/rc +7FNiU6WA0O4X5vm6lUBhCKnqE4Yq15bi40fTWW65e7O1mrtyjT7tcfGm4Dla5UCi +oTQ2xedubjP2PPLgjw1yHYNX5y+TRN/A7pZDRv0/irK7AaTjIc69JY3u3kDmVeB1 +fjqSveaCrhjc6ZdEDYSnE3YxSoqrbwU513ArwH9ujtuaiDtj1eIcYOGLoh076j7S +/1iugUhTsDeveHU1fG9idiiktNyRUWRCzMvL97u0og+mQRus4BrsZhukj6VzjOmG +xXLjq/S6mHMG7N2Rk0NyOVomzV+DqfhxT2u53beww8ISJxAFZZOEx1ys9FRJNoc5 +BewbpAi3nRtxE2BPIzL89kDmjDsXuJTipVatN0LlI1KOwXLfjt1fAKIl88cDx0xW +Y5sqiI5vX/vh5doveRzwwyLrCyiPnVzcFL0v9sIFZnFsc0F8FSfnf5yXOX0j2omC +Ymll9FJUeU7tzuiiDNjdz5iOOcLXAYlchcDPGiuOTwJTXLJ5QecGOfXfSJRE5vnk +jgCbAgMBAAGjgdQwgdEwHQYDVR0OBBYEFOI4tY7OLK/QNpmDBjE7lnO6oNG5MB8G +A1UdIwQYMBaAFOI4tY7OLK/QNpmDBjE7lnO6oNG5MAwGA1UdEwQFMAMBAf8wgYAG +CCsGAQUFBwEBBHQwcjAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AxLmV4YW1wbGUu +Y29tMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcDIuZXhhbXBsZS5jb20wJAYIKwYB +BQUHMAGGGGh0dHA6Ly9vY3NwMy5leGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAAOC +AgEARhrXE1DcGt7UJtH1j0zxMEgfKzgbnolLVgaWMzZ//1sOH6Qk8P2mfOyjm+l3 +4pH6bz/h8+D8TglOKDlcdniwOHXsuP2w4CG9LfQS3YprKH4ltGaQCQ5cL1OMWWq4 +MlQ80tHe43kSbJ6eQF+EZr5WGxEwA8hcAaQ08F6QEiPN1h0zOpTfIa4zU0ExUYGm +dCs1ST0RNQuCkEogKnyl6iFzd4AzshHkmzeqdSjvewsbO+UDwSsy/R5pYfzX2IJE +zg8J77q5fy43WJrcubT4uu2fsUKjHMxjpgdYrkIlLtNByviBj6px/4JK2wrczPJ0 ++hPOa5boE7P+qpimklmBHnOXDo5BTpRCZXwvHQHKB1JHPmLAGLibF4ljzJ32H44h +DUIV/z1P2Jx2duxh6CIgfOJAZBSln2blpHSpuSemt2j1orM+tT+DWuIz+H3uzKm6 +O2TmYGi5R0+wvV8a1ux4HsUMYU9yW+g88LVYRzWWw89g8aRbkVQ6OAAvYgMtJj7d +VDnoCzt9VOU6269w4N2jmQ1IYa3sP60JbdpW+pvfeN35WARmvwcPEBrQYiLl92Fu +e/P6h6L4nMkVS952DJCCDdBceDIh2CPD8t0/XHZqdLRYCaeiOVe+zNYTe6TY1gcx +Soy/P3vJc7UfvNtl0PZx3Fs5UCb5O0RhJXdOn58l8lScCWo= +-----END CERTIFICATE----- + + +openssl req -new -x509 -days 3650 -nodes \ + -newkey rsa:4096 \ + -keyout multi_ocsp_key.pem \ + -out multi_ocsp_cert.pem \ + -config multi_ocsp.cnf + +[ req ] +default_bits = 4096 +distinguished_name = req_distinguished_name +x509_extensions = v3_ca +prompt = no + +[ req_distinguished_name ] +CN = Test Certificate with Multiple OCSP + +[ v3_ca ] +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always,issuer +basicConstraints = CA:TRUE +authorityInfoAccess = OCSP;URI:http://ocsp1.example.com,OCSP;URI:http://ocsp2.example.com,OCSP;URI:http://ocsp3.example.com diff --git a/src/tests/unit_x509.cpp b/src/tests/unit_x509.cpp index ff4e3ef2a9e..73bb70e4e53 100644 --- a/src/tests/unit_x509.cpp +++ b/src/tests/unit_x509.cpp @@ -490,6 +490,31 @@ Test::Result test_x509_encode_authority_info_access_extension() { result.test_is_true("OCSP URI available", !cert.ocsp_responder().empty()); result.test_is_true("CA Issuer URI available", !cert.ca_issuers().empty()); + + // create a certificate with multiple OCSP URIs + Botan::X509_Cert_Options opts_multi_ocsp = req_opts1(sig_algo); + const std::vector ocsp_uris = {"http://ocsp.example.com", "http://backup-ocsp.example.com"}; + opts_multi_ocsp.extensions.add(std::make_unique(ocsp_uris)); + + req = Botan::X509::create_cert_req(opts_multi_ocsp, *key, hash_fn, *rng); + + cert = ca.sign_request(req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); + + const auto* aia_ext = + cert.v3_extensions().get_extension_object_as(); + result.test_is_true("AIA extension present", aia_ext != nullptr); + + const auto ocsp_responders = aia_ext->ocsp_responders(); + result.test_sz_eq("number of OCSP responder URIs", ocsp_responders.size(), 2); + result.test_str_eq("First OCSP responder URI matches", ocsp_responders[0], "http://ocsp.example.com"); + result.test_str_eq("Second OCSP responder URI matches", ocsp_responders[1], "http://backup-ocsp.example.com"); + + const auto cert_ocsp_responders = cert.ocsp_responders(); + result.test_sz_eq("Certificate: number of OCSP responder URIs", cert_ocsp_responders.size(), 2); + result.test_str_eq( + "Certificate: First OCSP responder URI matches", cert_ocsp_responders[0], "http://ocsp.example.com"); + result.test_str_eq( + "Certificate: Second OCSP responder URI matches", cert_ocsp_responders[1], "http://backup-ocsp.example.com"); #endif return result; @@ -686,6 +711,17 @@ Test::Result test_x509_authority_info_access_extension() { "ldap://directory.d-trust.net/CN=Bdrive%20Test%20CA%201-2%202017,O=Bundesdruckerei%20GmbH,C=DE?cACertificate?base?"); result.test_str_eq("OCSP responder URL matches", aia_cert_2ca.ocsp_responder(), "http://staging.ocsp.d-trust.net"); + // contains AIA extension with multiple OCSP responders + const Botan::X509_Certificate aia_cert_multi_ocsp( + Test::data_file("x509/misc/contains_multiple_ocsp_responders.pem")); + + const auto& ocsp_responders_multi = aia_cert_multi_ocsp.ocsp_responders(); + result.test_sz_eq("number of OCSP responders", ocsp_responders_multi.size(), 3); + result.test_str_eq("First OCSP responder URL matches", ocsp_responders_multi[0], "http://ocsp1.example.com"); + result.test_str_eq("Second OCSP responder URL matches", ocsp_responders_multi[1], "http://ocsp2.example.com"); + result.test_str_eq("Third OCSP responder URL matches", ocsp_responders_multi[2], "http://ocsp3.example.com"); + result.test_is_true("no CA Issuer URI available", aia_cert_multi_ocsp.ca_issuers().empty()); + return result; }