From 44015c5d95f641277f574bf003a83d1d2f78c713 Mon Sep 17 00:00:00 2001 From: Jack Lloyd Date: Wed, 16 Jul 2025 04:54:05 -0400 Subject: [PATCH 1/3] Add CertificateParametersBuilder as a replacement for X509_Cert_Options --- src/cli/perf_x509.cpp | 25 ++- src/lib/utils/types.h | 2 +- src/lib/x509/info.txt | 1 + src/lib/x509/pkix_enums.h | 2 + src/lib/x509/x509_builder.cpp | 304 +++++++++++++++++++++++++++ src/lib/x509/x509_builder.h | 195 ++++++++++++++++++ src/lib/x509/x509opt.cpp | 91 ++++++++ src/lib/x509/x509self.cpp | 133 ++---------- src/lib/x509/x509self.h | 7 + src/tests/test_x509_rpki.cpp | 376 +++++++++++++++------------------- 10 files changed, 795 insertions(+), 341 deletions(-) create mode 100644 src/lib/x509/x509_builder.cpp create mode 100644 src/lib/x509/x509_builder.h diff --git a/src/cli/perf_x509.cpp b/src/cli/perf_x509.cpp index 7656de98e95..17491cf35ef 100644 --- a/src/cli/perf_x509.cpp +++ b/src/cli/perf_x509.cpp @@ -15,12 +15,14 @@ #include #include #include + #include + #include #include #include #include + #include #include #include - #include #endif namespace Botan_CLI { @@ -44,14 +46,23 @@ class PerfTest_ASN1_Parsing final : public PerfTest { } static CA create_ca(Botan::RandomNumberGenerator& rng) { - auto root_cert_options = Botan::X509_Cert_Options("Benchmark Root/DE/RS/CS"); - root_cert_options.dns = "unobtainium.example.com"; - root_cert_options.email = "idont@exist.com"; - root_cert_options.is_CA = true; - auto root_key = create_private_key(rng); BOTAN_ASSERT_NONNULL(root_key); - auto root_cert = Botan::X509::create_self_signed_cert(root_cert_options, *root_key, get_hash_function(), rng); + + Botan::CertificateParametersBuilder root_cert_params; + root_cert_params.add_common_name("Benchmark Root") + .add_country("DE") + .add_organization("RS") + .add_organizational_unit("CS") + .add_dns(Botan::DNSName::from_string("unobtainium.example.com").value()) + .add_email(Botan::EmailAddress::from_string("idont@exist.com").value()) + .set_as_ca_certificate(); + + const auto not_before = std::chrono::system_clock::now(); + const auto not_after = not_before + std::chrono::seconds(86400); + + auto root_cert = + root_cert_params.into_self_signed_cert(not_before, not_after, *root_key, rng, get_hash_function()); auto ca = Botan::X509_CA(root_cert, *root_key, get_hash_function(), rng); return CA{ diff --git a/src/lib/utils/types.h b/src/lib/utils/types.h index b76399e21ce..9872d823625 100644 --- a/src/lib/utils/types.h +++ b/src/lib/utils/types.h @@ -86,7 +86,7 @@ namespace Botan { * TLS::Client, TLS::Server, TLS::Policy, TLS::Protocol_Version, TLS::Callbacks, TLS::Ciphersuite, * TLS::Session, TLS::Session_Summary, TLS::Session_Manager, Credentials_Manager *
X.509
-* X509_Certificate, X509_CRL, X509_CA, Certificate_Extension, PKCS10_Request, X509_Cert_Options, +* X509_Certificate, X509_CRL, X509_CA, Certificate_Extension, PKCS10_Request, CertificateParametersBuilder, * Certificate_Store, Certificate_Store_In_SQL, Certificate_Store_In_SQLite *
eXtendable Output Functions
* @ref Ascon_XOF128 "Ascon-XOF128", @ref SHAKE_XOF "SHAKE" diff --git a/src/lib/x509/info.txt b/src/lib/x509/info.txt index 0cc33ae80ff..f937764771d 100644 --- a/src/lib/x509/info.txt +++ b/src/lib/x509/info.txt @@ -26,6 +26,7 @@ ocsp.h pkcs10.h pkix_enums.h pkix_types.h +x509_builder.h x509_ca.h x509_crl.h x509_ext.h diff --git a/src/lib/x509/pkix_enums.h b/src/lib/x509/pkix_enums.h index fae0f5ebdac..942b018047b 100644 --- a/src/lib/x509/pkix_enums.h +++ b/src/lib/x509/pkix_enums.h @@ -182,6 +182,8 @@ class BOTAN_PUBLIC_API(3, 0) Key_Constraints final { void operator|=(Key_Constraints::Bits other) { m_value |= other; } + void operator|=(Key_Constraints other) { m_value |= other.m_value; } + // Return true if all bits in mask are set bool includes(Key_Constraints::Bits other) const { return (m_value & other) == other; } diff --git a/src/lib/x509/x509_builder.cpp b/src/lib/x509/x509_builder.cpp new file mode 100644 index 00000000000..6c06c3b1239 --- /dev/null +++ b/src/lib/x509/x509_builder.cpp @@ -0,0 +1,304 @@ +/* +* (C) 2025 Jack Lloyd +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Botan { + +class CertificateParametersBuilder::State final { + public: + const X509_DN& subject_dn() const { return m_subject_dn; } + + Extensions finalize_extensions(const Public_Key& key) const { + auto extensions = m_extensions; + + extensions.replace(setup_alt_name(extensions)); + + extensions.add_new( + std::make_unique(m_is_ca_request, m_path_limit.value_or(0)), true); + + const Key_Constraints usage = this->usage(); + + if(!usage.empty()) { + if(!usage.compatible_with(key)) { + throw Invalid_Argument("The requested key usage is incompatible with the algorithm"); + } + + extensions.add_new(std::make_unique(usage), true); + } + + if(!m_ext_usage.empty()) { + extensions.add_new(std::make_unique(m_ext_usage)); + } + + return extensions; + } + + Key_Constraints usage() const { + if(m_is_ca_request) { + return Key_Constraints::ca_constraints(); + } else { + return m_usage; + } + } + + void add_common_name(std::string_view cn) { add_subject_dn("common_name", "X520.CommonName", cn); } + + void add_country(std::string_view country) { add_subject_dn("country", "X520.Country", country); } + + void add_state(std::string_view state) { add_subject_dn("state", "X520.State", state); } + + void add_locality(std::string_view locality) { add_subject_dn("locality", "X520.Locality", locality); } + + void add_serial_number(std::string_view sn) { add_subject_dn("serial_number", "X520.SerialNumber", sn); } + + void add_organization(std::string_view org) { add_subject_dn("organization", "X520.Organization", org); } + + void add_organizational_unit(std::string_view org_unit) { + add_subject_dn("organizational_unit", "X520.OrganizationalUnit", org_unit); + } + + void add_extension(std::unique_ptr extn, bool is_critical) { + if(!m_extensions.add_new(std::move(extn), is_critical)) { + throw Invalid_Argument("CertificateParametersBuilder::add_extension: cannot add same extension twice"); + } + } + + void add_email(const EmailAddress& email) { m_email.push_back(email); } + + void add_dns(const DNSName& dns) { m_dns.push_back(dns); } + + void add_uri(const URI& uri) { m_uri.push_back(uri); } + + void add_xmpp(std::string_view xmpp) { m_xmpp.emplace_back(xmpp); } + + void add_ipv4(const IPv4Address& ipv4) { m_ipv4.push_back(ipv4); } + + void add_ipv6(const IPv6Address& ipv6) { m_ipv6.push_back(ipv6); } + + void add_allowed_usage(Key_Constraints usage) { m_usage |= usage; } + + void add_allowed_extended_usage(const OID& usage) { m_ext_usage.push_back(usage); } + + void set_as_ca_certificate(std::optional path_limit) { + if(m_is_ca_request) { + throw Invalid_State("CertificateParametersBuilder::set_as_ca_certificate cannot be called twice"); + } else { + m_is_ca_request = true; + m_path_limit = path_limit; + } + } + + private: + void add_subject_dn(std::string_view fn_suffix, std::string_view attr, std::string_view value) { + const auto oid = OID::from_string(attr); + const size_t ub = X509_DN::lookup_ub(oid); + + if(value.empty()) { + throw Invalid_Argument(fmt("CertificateParametersBuilder::add_{}: empty name is prohibited", fn_suffix)); + } + + if(value.size() > ub) { + throw Invalid_Argument( + fmt("CertificateParametersBuilder::add_{}: name exceeds maximum allowed length ({}) for this type", + fn_suffix, + ub)); + } + m_subject_dn.add_attribute(oid, value); + } + + std::unique_ptr setup_alt_name(const Extensions& extensions) const { + AlternativeName subject_alt; + + /* + If the extension was already created in extensions we need to merge the + values provided with the extension value + */ + if(const auto* ext = extensions.get_extension_object_as()) { + subject_alt = ext->get_alt_name(); + } + + for(const auto& dns : m_dns) { + subject_alt.add_dns(dns.name()); + } + for(const auto& uri : m_uri) { + subject_alt.add_uri(uri.original_input()); + } + for(const auto& email : m_email) { + subject_alt.add_email(email.to_string()); + } + for(const auto& xmpp : m_xmpp) { + subject_alt.add_other_name(OID::from_string("PKIX.XMPPAddr"), ASN1_String(xmpp, ASN1_Type::Utf8String)); + } + for(const auto& ipv4 : m_ipv4) { + subject_alt.add_ipv4_address(ipv4); + } + for(const auto& ipv6 : m_ipv6) { + subject_alt.add_ipv6_address(ipv6); + } + + return std::make_unique(subject_alt); + } + + X509_DN m_subject_dn; + Extensions m_extensions; + Key_Constraints m_usage; + std::vector m_email; + std::vector m_dns; + std::vector m_uri; + std::vector m_xmpp; + std::vector m_ipv4; + std::vector m_ipv6; + std::vector m_ext_usage; + bool m_is_ca_request = false; + std::optional m_path_limit; +}; + +CertificateParametersBuilder::CertificateParametersBuilder() : + m_state(std::make_unique()) {} + +CertificateParametersBuilder::CertificateParametersBuilder(CertificateParametersBuilder&& other) noexcept = default; + +CertificateParametersBuilder::~CertificateParametersBuilder() = default; + +X509_Certificate CertificateParametersBuilder::into_self_signed_cert(std::chrono::system_clock::time_point not_before, + std::chrono::system_clock::time_point not_after, + const Private_Key& key, + RandomNumberGenerator& rng, + std::optional hash_fn, + std::optional padding) const { + auto signer_p = X509_Object::choose_sig_format(key, rng, hash_fn.value_or(""), padding.value_or("")); + auto& signer = *signer_p; + + const AlgorithmIdentifier sig_algo = signer.algorithm_identifier(); + BOTAN_ASSERT_NOMSG(sig_algo.oid().has_value()); + + Extensions extensions = m_state->finalize_extensions(key); + + const std::vector pub_key = key.subject_public_key(); + auto skid = std::make_unique(pub_key, signer.hash_function()); + + extensions.add_new(std::make_unique(skid->get_key_id())); + extensions.add_new(std::move(skid)); + + const auto& subject_dn = m_state->subject_dn(); + + return X509_CA::make_cert( + signer, rng, sig_algo, pub_key, X509_Time(not_before), X509_Time(not_after), subject_dn, subject_dn, extensions); +} + +PKCS10_Request CertificateParametersBuilder::into_pkcs10_request( + const Private_Key& key, + RandomNumberGenerator& rng, + std::optional hash_fn, + std::optional padding, + std::optional challenge_password) const { + const auto& subject_dn = m_state->subject_dn(); + + const auto extensions = m_state->finalize_extensions(key); + + return PKCS10_Request::create( + key, subject_dn, extensions, hash_fn.value_or(""), rng, padding.value_or(""), challenge_password.value_or("")); +} + +CertificateParametersBuilder& CertificateParametersBuilder::add_common_name(std::string_view cn) { + m_state->add_common_name(cn); + return (*this); +} + +CertificateParametersBuilder& CertificateParametersBuilder::add_country(std::string_view country) { + m_state->add_country(country); + return (*this); +} + +CertificateParametersBuilder& CertificateParametersBuilder::add_organization(std::string_view org) { + m_state->add_organization(org); + return (*this); +} + +CertificateParametersBuilder& CertificateParametersBuilder::add_organizational_unit(std::string_view org_unit) { + m_state->add_organizational_unit(org_unit); + return (*this); +} + +CertificateParametersBuilder& CertificateParametersBuilder::add_locality(std::string_view locality) { + m_state->add_locality(locality); + return (*this); +} + +CertificateParametersBuilder& CertificateParametersBuilder::add_state(std::string_view state) { + m_state->add_state(state); + return (*this); +} + +CertificateParametersBuilder& CertificateParametersBuilder::add_serial_number(std::string_view serial) { + m_state->add_serial_number(serial); + return (*this); +} + +CertificateParametersBuilder& CertificateParametersBuilder::add_email(const EmailAddress& email) { + m_state->add_email(email); + return (*this); +} + +CertificateParametersBuilder& CertificateParametersBuilder::add_uri(const URI& uri) { + m_state->add_uri(uri); + return (*this); +} + +CertificateParametersBuilder& CertificateParametersBuilder::add_dns(const DNSName& dns) { + m_state->add_dns(dns); + return (*this); +} + +CertificateParametersBuilder& CertificateParametersBuilder::add_ipv4(const IPv4Address& ipv4) { + m_state->add_ipv4(ipv4); + return (*this); +} + +CertificateParametersBuilder& CertificateParametersBuilder::add_ipv6(const IPv6Address& ipv6) { + m_state->add_ipv6(ipv6); + return (*this); +} + +CertificateParametersBuilder& CertificateParametersBuilder::add_xmpp(std::string_view xmpp) { + m_state->add_xmpp(xmpp); + return (*this); +} + +CertificateParametersBuilder& CertificateParametersBuilder::add_allowed_usage(Key_Constraints kc) { + m_state->add_allowed_usage(kc); + return (*this); +} + +CertificateParametersBuilder& CertificateParametersBuilder::add_allowed_extended_usage(const OID& usage) { + m_state->add_allowed_extended_usage(usage); + return (*this); +} + +CertificateParametersBuilder& CertificateParametersBuilder::add_extension(std::unique_ptr extn, + bool is_critical) { + m_state->add_extension(std::move(extn), is_critical); + return (*this); +} + +CertificateParametersBuilder& CertificateParametersBuilder::set_as_ca_certificate(std::optional path_limit) { + m_state->set_as_ca_certificate(path_limit); + return (*this); +} + +} // namespace Botan diff --git a/src/lib/x509/x509_builder.h b/src/lib/x509/x509_builder.h new file mode 100644 index 00000000000..57f809de3c6 --- /dev/null +++ b/src/lib/x509/x509_builder.h @@ -0,0 +1,195 @@ +/* +* (C) 2025 Jack Lloyd +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#ifndef BOTAN_X509_CERT_PARAM_BUILDER_H_ +#define BOTAN_X509_CERT_PARAM_BUILDER_H_ + +#include +#include +#include +#include +#include +#include +#include + +namespace Botan { + +class Certificate_Extension; +class DNSName; +class EmailAddress; +class IPv4Address; +class IPv6Address; +class OID; +class Private_Key; +class RandomNumberGenerator; +class URI; + +/** +* Certificate Metadata Builder +* +* Allows incrementally forming metadata included in a certificate or PKCS10 request. +* +* The various add_ methods each return a reference to this, allowing for convenient method chaining. +*/ +class BOTAN_PUBLIC_API(3, 9) CertificateParametersBuilder final { + public: + CertificateParametersBuilder(); + + CertificateParametersBuilder(const CertificateParametersBuilder& other) = delete; + CertificateParametersBuilder& operator=(const CertificateParametersBuilder& other) = delete; + + CertificateParametersBuilder(CertificateParametersBuilder&& other) noexcept; + CertificateParametersBuilder& operator=(CertificateParametersBuilder&& other) = delete; + ~CertificateParametersBuilder(); + + /** + * Create a self-signed X.509 certificate from these parameters. + * + * @param not_before the initial validity time of the generated certificate + * @param not_after the final validity time of the generated certificate + * @param key the private key + * @param rng the rng to use + * @param hash_fn the hash function to use; if unset a reasonable default is used + * @param padding optional padding scheme (for specifying RSA-PSS vs RSA-PKCS1, + * otherwise not necessary) + * + * @return newly created self-signed certificate + */ + X509_Certificate into_self_signed_cert(std::chrono::system_clock::time_point not_before, + std::chrono::system_clock::time_point not_after, + const Private_Key& key, + RandomNumberGenerator& rng, + std::optional hash_fn = {}, + std::optional padding = {}) const; + + /** + * Create a PKCS10 request + * + * @param key the private key + * @param rng the rng to use + * @param hash_fn the hash function to use; if unset a reasonable default is used + * @param padding optional padding scheme (for specifying RSA-PSS vs RSA-PKCS1, + * otherwise not necessary) + * @param challenge_password an optional challenge password included in the PKCS10 request + * (originally intended as a preshared key to allow authenticating a + * later revocation request, but now obsolete in most contexts) + * + * @return newly created self-signed certificate + */ + PKCS10_Request into_pkcs10_request(const Private_Key& key, + RandomNumberGenerator& rng, + std::optional hash_fn = {}, + std::optional padding = {}, + std::optional challenge_password = {}) const; + + /** + * Add an additional common name to the certificate metadata + */ + CertificateParametersBuilder& add_common_name(std::string_view cn); + + /** + * Add an additional country name to the certificate metadata + */ + CertificateParametersBuilder& add_country(std::string_view country); + + /** + * Add an additional organization name to the certificate metadata + */ + CertificateParametersBuilder& add_organization(std::string_view org); + + /** + * Add an additional organization unit name to the certificate metadata + */ + CertificateParametersBuilder& add_organizational_unit(std::string_view org_unit); + + /** + * Add an additional locality name to the certificate metadata + */ + CertificateParametersBuilder& add_locality(std::string_view locality); + + /** + * Add an additional state name to the certificate metadata + */ + CertificateParametersBuilder& add_state(std::string_view state); + + /** + * Add an additional serial number to the certificate metadata + * + * Note this is the X.520 serial number included in the DN, and has nothing + * to do with the serial number of the issued certificate. + */ + CertificateParametersBuilder& add_serial_number(std::string_view serial); + + /** + * Add an additional email address to the certificate metadata + */ + CertificateParametersBuilder& add_email(const EmailAddress& email); + + /** + * Add an additional URI to the certificate metadata + */ + CertificateParametersBuilder& add_uri(const URI& uri); + + /** + * Add an additional DNS name to the certificate metadata + */ + CertificateParametersBuilder& add_dns(const DNSName& dns); + + /** + * Add an additional IPv4 address to the certificate metadata + */ + CertificateParametersBuilder& add_ipv4(const IPv4Address& ipv4); + + /** + * Add an additional IPv6 address to the certificate metadata + */ + CertificateParametersBuilder& add_ipv6(const IPv6Address& ipv6); + + /** + * Add a XMPP name to the certificate metadata + */ + CertificateParametersBuilder& add_xmpp(std::string_view xmpp); + + /** + * Add another allowed usage to the KeyUsage extension + * + * This is cummulative; all usages indicated are ORed together + */ + CertificateParametersBuilder& add_allowed_usage(Key_Constraints kc); + + /** + * Add another allowed usage to the ExtendedKeyUsage extension + */ + CertificateParametersBuilder& add_allowed_extended_usage(const OID& usage); + + /** + * Add an arbitrary certificate extension + * + * An extension can be added at most once. If the same extension is added a second + * time (even with identical contents) an exception will be thrown. + */ + CertificateParametersBuilder& add_extension(std::unique_ptr extn, + bool is_critical = false); + + /** + * Set the parameters as being for a CA certificate + * + * If the path limit is specified then the BasicConstraints extension will include + * that value as the maximum chain length issuable by the resulting cert. + * + * It is not allowed to call set_as_ca_certificate more than once; further calls will + * result in an Invalid_State exception. + */ + CertificateParametersBuilder& set_as_ca_certificate(std::optional path_limit = {}); + + private: + class State; + std::unique_ptr m_state; +}; + +} // namespace Botan + +#endif diff --git a/src/lib/x509/x509opt.cpp b/src/lib/x509/x509opt.cpp index f045c5cfd22..1e7e74debf3 100644 --- a/src/lib/x509/x509opt.cpp +++ b/src/lib/x509/x509opt.cpp @@ -7,6 +7,11 @@ #include +#include +#include +#include +#include +#include #include #include @@ -92,4 +97,90 @@ X509_Cert_Options::X509_Cert_Options(std::string_view initial_opts, uint32_t exp } } +CertificateParametersBuilder X509_Cert_Options::into_builder() const { + auto builder = CertificateParametersBuilder(); + if(!this->common_name.empty()) { + builder.add_common_name(this->common_name); + } + if(!this->country.empty()) { + builder.add_country(this->country); + } + if(!this->organization.empty()) { + builder.add_organization(this->organization); + } + if(!this->org_unit.empty()) { + builder.add_organizational_unit(this->org_unit); + } + for(const auto& ou : this->more_org_units) { + if(!ou.empty()) { + builder.add_organizational_unit(ou); + } + } + if(!this->locality.empty()) { + builder.add_locality(this->locality); + } + if(!this->state.empty()) { + builder.add_state(this->state); + } + if(!this->serial_number.empty()) { + builder.add_serial_number(this->serial_number); + } + if(!this->email.empty()) { + if(auto parsed = EmailAddress::from_string(this->email)) { + builder.add_email(*parsed); + } else { + throw Invalid_Argument(fmt("Invalid email address '{}'", this->email)); + } + } + if(!this->uri.empty()) { + if(auto parsed = URI::parse(this->uri)) { + builder.add_uri(*parsed); + } else { + throw Invalid_Argument(fmt("Invalid URI '{}'", this->uri)); + } + } + if(!this->ip.empty()) { + if(auto ipv4 = IPv4Address::from_string(this->ip)) { + builder.add_ipv4(*ipv4); + } else { + throw Invalid_Argument(fmt("Invalid IPv4 address '{}'", this->ip)); + } + } + + auto add_dns = [&](std::string_view nm) { + if(auto parsed = DNSName::from_san_string(nm)) { + builder.add_dns(*parsed); + } else { + throw Invalid_Argument(fmt("Invalid DNS name '{}'", nm)); + } + }; + + if(!this->dns.empty()) { + add_dns(this->dns); + } + for(const auto& nm : this->more_dns) { + if(!nm.empty()) { + add_dns(nm); + } + } + if(!this->xmpp.empty()) { + builder.add_xmpp(this->xmpp); + } + if(this->is_CA) { + builder.set_as_ca_certificate(this->path_limit); + } + if(!this->constraints.empty()) { + builder.add_allowed_usage(this->constraints); + } + for(const OID& usage : this->ex_constraints) { + builder.add_allowed_extended_usage(usage); + } + + for(auto& [extn, is_critical] : this->extensions.extensions()) { + builder.add_extension(std::move(extn), is_critical); + } + + return builder; +} + } // namespace Botan diff --git a/src/lib/x509/x509self.cpp b/src/lib/x509/x509self.cpp index aac7268e48d..7533a3c15d1 100644 --- a/src/lib/x509/x509self.cpp +++ b/src/lib/x509/x509self.cpp @@ -7,73 +7,7 @@ #include -#include -#include -#include -#include -#include -#include - -namespace Botan { - -namespace { - -/* -* Load information from the X509_Cert_Options -*/ -X509_DN load_dn_info(const X509_Cert_Options& opts) { - X509_DN subject_dn; - - subject_dn.add_attribute("X520.CommonName", opts.common_name); - subject_dn.add_attribute("X520.Country", opts.country); - subject_dn.add_attribute("X520.State", opts.state); - subject_dn.add_attribute("X520.Locality", opts.locality); - subject_dn.add_attribute("X520.Organization", opts.organization); - subject_dn.add_attribute("X520.OrganizationalUnit", opts.org_unit); - subject_dn.add_attribute("X520.SerialNumber", opts.serial_number); - - for(const auto& extra_ou : opts.more_org_units) { - subject_dn.add_attribute("X520.OrganizationalUnit", extra_ou); - } - - return subject_dn; -} - -auto create_alt_name_ext(const X509_Cert_Options& opts, const Extensions& extensions) { - AlternativeName subject_alt; - - /* - If the extension was already created in opts.extension we need to - merge the values provided in opts with the values set in the extension. - */ - if(const auto* ext = extensions.get_extension_object_as()) { - subject_alt = ext->get_alt_name(); - } - - subject_alt.add_dns(opts.dns); - for(const auto& nm : opts.more_dns) { - subject_alt.add_dns(nm); - } - subject_alt.add_uri(opts.uri); - subject_alt.add_email(opts.email); - if(!opts.ip.empty()) { - if(auto ipv4 = IPv4Address::from_string(opts.ip)) { - subject_alt.add_ipv4_address(*ipv4); - } else { - throw Invalid_Argument(fmt("Invalid IPv4 address '{}'", opts.ip)); - } - } - - if(!opts.xmpp.empty()) { - subject_alt.add_other_name(OID::from_string("PKIX.XMPPAddr"), ASN1_String(opts.xmpp, ASN1_Type::Utf8String)); - } - - return std::make_unique(subject_alt); -} - -} // namespace - -namespace X509 { +namespace Botan::X509 { /* * Create a new self-signed X.509 certificate @@ -82,37 +16,13 @@ X509_Certificate create_self_signed_cert(const X509_Cert_Options& opts, const Private_Key& key, std::string_view hash_fn, RandomNumberGenerator& rng) { - const std::vector pub_key = X509::BER_encode(key); - auto signer = X509_Object::choose_sig_format(key, rng, hash_fn, opts.padding_scheme); - const AlgorithmIdentifier sig_algo = signer->algorithm_identifier(); - BOTAN_ASSERT_NOMSG(sig_algo.oid().has_value()); + auto not_before = opts.start.to_std_timepoint(); + auto not_after = opts.end.to_std_timepoint(); - const auto subject_dn = load_dn_info(opts); + const std::optional padding = + (opts.padding_scheme.empty()) ? std::nullopt : std::optional(opts.padding_scheme); - Extensions extensions = opts.extensions; - - const auto constraints = opts.is_CA ? Key_Constraints::ca_constraints() : opts.constraints; - - if(!constraints.compatible_with(key)) { - throw Invalid_Argument("The requested key constraints are incompatible with the algorithm"); - } - - extensions.add_new(std::make_unique(opts.is_CA, opts.path_limit), true); - - if(!constraints.empty()) { - extensions.add_new(std::make_unique(constraints), true); - } - - auto skid = std::make_unique(key); - - extensions.add_new(std::make_unique(skid->get_key_id())); - extensions.add_new(std::move(skid)); - - extensions.replace(create_alt_name_ext(opts, extensions)); - - extensions.add_new(std::make_unique(opts.ex_constraints)); - - return X509_CA::make_cert(*signer, rng, sig_algo, pub_key, opts.start, opts.end, subject_dn, subject_dn, extensions); + return opts.into_builder().into_self_signed_cert(not_before, not_after, key, rng, hash_fn, padding); } /* @@ -122,31 +32,14 @@ PKCS10_Request create_cert_req(const X509_Cert_Options& opts, const Private_Key& key, std::string_view hash_fn, RandomNumberGenerator& rng) { - const auto subject_dn = load_dn_info(opts); - - const auto constraints = opts.is_CA ? Key_Constraints::ca_constraints() : opts.constraints; - - if(!constraints.compatible_with(key)) { - throw Invalid_Argument("The requested key constraints are incompatible with the algorithm"); - } - - Extensions extensions = opts.extensions; + const std::optional challenge_password = + (opts.challenge.empty()) ? std::nullopt : std::optional(opts.challenge); + const std::optional padding = + (opts.padding_scheme.empty()) ? std::nullopt : std::optional(opts.padding_scheme); - extensions.add_new(std::make_unique(opts.is_CA, opts.path_limit)); + // opts.start and opts.end are ignored here - if(!constraints.empty()) { - extensions.add_new(std::make_unique(constraints)); - } - - extensions.replace(create_alt_name_ext(opts, extensions)); - - if(!opts.ex_constraints.empty()) { - extensions.add_new(std::make_unique(opts.ex_constraints)); - } - - return PKCS10_Request::create(key, subject_dn, extensions, hash_fn, rng, opts.padding_scheme, opts.challenge); + return opts.into_builder().into_pkcs10_request(key, rng, hash_fn, padding, challenge_password); } -} // namespace X509 - -} // namespace Botan +} // namespace Botan::X509 diff --git a/src/lib/x509/x509self.h b/src/lib/x509/x509self.h index 0754956c147..d1e8b065e57 100644 --- a/src/lib/x509/x509self.h +++ b/src/lib/x509/x509self.h @@ -12,13 +12,18 @@ #include #include #include +#include #include +#include +#include namespace Botan { class RandomNumberGenerator; class Private_Key; +// Older interface for creating PKCS10 requests and self-signed certificates follows + /** * Options for X.509 certificates. */ @@ -179,6 +184,8 @@ class BOTAN_PUBLIC_API(2, 0) X509_Cert_Options final { */ void add_ex_constraint(std::string_view name); + CertificateParametersBuilder into_builder() const; + /** * Construct a new options object * @param opts define the common name of this object. An example for this diff --git a/src/tests/test_x509_rpki.cpp b/src/tests/test_x509_rpki.cpp index 80dfd43245c..79383ea9a56 100644 --- a/src/tests/test_x509_rpki.cpp +++ b/src/tests/test_x509_rpki.cpp @@ -10,14 +10,18 @@ #if defined(BOTAN_HAS_X509_CERTIFICATES) #include #include + #include #include #include #include + #include #include #include #include - #include #include + #if defined(BOTAN_HAS_ECC_GROUP) + #include + #endif #endif namespace Botan_Tests { @@ -42,96 +46,109 @@ Botan::X509_Time from_date(const int y, const int m, const int d) { return Botan::X509_Time(t.to_std_timepoint()); } -Botan::X509_Cert_Options ca_opts(const std::string& sig_padding = "") { - Botan::X509_Cert_Options opts("Test CA/US/Botan Project/Testing"); - - opts.uri = "https://botan.randombit.net"; - opts.dns = "botan.randombit.net"; - opts.email = "testing@randombit.net"; - opts.set_padding_scheme(sig_padding); +std::unique_ptr generate_key(const std::string& algo, Botan::RandomNumberGenerator& rng) { + std::string params; + if(algo == "ECDSA") { + params = "secp256r1"; - opts.CA_key(1); + #if defined(BOTAN_HAS_ECC_GROUP) + if(Botan::EC_Group::supports_named_group("secp192r1")) { + params = "secp192r1"; + } + #endif + } else if(algo == "Ed25519") { + params = ""; + } else if(algo == "RSA") { + params = "1024"; + } - return opts; + return Botan::create_private_key(algo, rng, params); } -Botan::X509_Cert_Options req_opts(const std::string& algo, const std::string& sig_padding = "") { - Botan::X509_Cert_Options opts("Test User 1/US/Botan Project/Testing"); +Botan::CertificateParametersBuilder ca_params() { + Botan::CertificateParametersBuilder builder; - opts.uri = "https://botan.randombit.net"; - opts.dns = "botan.randombit.net"; - opts.email = "testing@randombit.net"; - opts.set_padding_scheme(sig_padding); + builder.add_common_name("RPKI Test CA") + .add_country("US") + .add_organization("Botan") + .add_dns(Botan::DNSName::from_string("botan.randombit.net").value()) + .set_as_ca_certificate(1); - opts.not_before("160101200000Z"); - opts.not_after("300101200000Z"); + return builder; +} - opts.challenge = "zoom"; +Botan::CertificateParametersBuilder user_params() { + Botan::CertificateParametersBuilder builder; - if(algo == "RSA") { - opts.constraints = Botan::Key_Constraints::KeyEncipherment; - } else if(algo == "DSA" || algo == "ECDSA" || algo == "ECGDSA" || algo == "ECKCDSA") { - opts.constraints = Botan::Key_Constraints::DigitalSignature; - } + builder.add_common_name("RPKI Test User") + .add_country("US") + .add_organization("Botan") + .add_allowed_usage(Botan::Key_Constraints::DigitalSignature); - return opts; + return builder; } -std::tuple> -get_sig_algo_padding_and_generate_key(Botan::RandomNumberGenerator& rng) { - const auto generate_key = [&](const std::string& alg_name, const std::string& params = "") { - auto key = Botan::create_private_key(alg_name, rng, params); - if(!key) { - throw Test_Error("Could not generate key for algorithm " + alg_name); - } - return key; - }; - - #if defined(BOTAN_HAS_ECDSA) && defined(BOTAN_HAS_SHA2_32) - // Try to find a group that is supported in this build - const auto group_name = Test::supported_ec_group_name(); - if(group_name) { - return {"ECDSA", "", "SHA-256", generate_key("ECDSA", *group_name)}; - } +std::pair get_sig_algo() { + #if defined(BOTAN_HAS_ECDSA) + const std::string sig_algo{"ECDSA"}; + const std::string hash_fn{"SHA-256"}; + #elif defined(BOTAN_HAS_ED25519) + const std::string sig_algo{"Ed25519"}; + const std::string hash_fn{"SHA-512"}; + #elif defined(BOTAN_HAS_RSA) + const std::string sig_algo{"RSA"}; + const std::string hash_fn{"SHA-256"}; #endif - #if defined(BOTAN_HAS_ED25519) - return {"Ed25519", "", "SHA-512", generate_key("Ed25519")}; - #elif defined(BOTAN_HAS_RSA) && defined(BOTAN_HAS_EMSA_PKCS1) - return {"RSA", "PKCS1v15(SHA-256)", "SHA-256", generate_key("RSA", "1536")}; - #else - throw Test_Error("No suitable signature algorithm available in this build"); - #endif + return std::make_pair(sig_algo, hash_fn); } -Botan::X509_Certificate make_self_signed(std::unique_ptr& rng, - const Botan::X509_Cert_Options& opts = std::move(ca_opts())) { - auto [sig_algo, padding_method, hash_fn, key] = get_sig_algo_padding_and_generate_key(*rng); - return Botan::X509::create_self_signed_cert(opts, *key, hash_fn, *rng); +Botan::X509_Certificate make_self_signed(Botan::RandomNumberGenerator& rng, + const Botan::CertificateParametersBuilder& params) { + auto [sig_algo, hash_fn] = get_sig_algo(); + auto key = generate_key(sig_algo, rng); + + const auto now = std::chrono::system_clock::now(); + const auto not_before = now - std::chrono::seconds(180); + const auto not_after = now + std::chrono::seconds(86400); + + return params.into_self_signed_cert(not_before, not_after, *key, rng, hash_fn); } -CA_Creation_Result make_ca(std::unique_ptr& rng, - const Botan::X509_Cert_Options& opts = std::move(ca_opts())) { - auto [sig_algo, padding_method, hash_fn, ca_key] = get_sig_algo_padding_and_generate_key(*rng); - const auto ca_cert = Botan::X509::create_self_signed_cert(opts, *ca_key, hash_fn, *rng); - Botan::X509_CA ca(ca_cert, *ca_key, hash_fn, padding_method, *rng); - auto sub_key = ca_key->generate_another(*rng); +CA_Creation_Result make_ca(Botan::RandomNumberGenerator& rng, const Botan::CertificateParametersBuilder& params) { + auto [sig_algo, hash_fn] = get_sig_algo(); + auto ca_key = generate_key(sig_algo, rng); + + const auto now = std::chrono::system_clock::now(); + const auto not_before = now - std::chrono::seconds(180); + const auto not_after = now + std::chrono::seconds(86400); + + const auto ca_cert = params.into_self_signed_cert(not_before, not_after, *ca_key, rng, hash_fn); + + Botan::X509_CA ca(ca_cert, *ca_key, hash_fn, rng); + auto sub_key = generate_key(sig_algo, rng); return CA_Creation_Result{ca_cert, std::move(ca), std::move(sub_key), sig_algo, hash_fn}; } -std::pair make_and_sign_ca( - std::unique_ptr ext, - Botan::X509_CA& parent_ca, - std::unique_ptr& rng) { - auto [sig_algo, padding_method, hash_fn, key] = get_sig_algo_padding_and_generate_key(*rng); +CA_Creation_Result make_ca(Botan::RandomNumberGenerator& rng) { + return make_ca(rng, ca_params()); +} + +std::pair make_and_sign_ca(std::unique_ptr ext, + Botan::X509_CA& parent_ca, + Botan::RandomNumberGenerator& rng) { + auto [sig_algo, hash_fn] = get_sig_algo(); + + auto params = ca_params(); + params.add_extension(std::move(ext)); - Botan::X509_Cert_Options opts = ca_opts(); - opts.extensions.add(std::move(ext)); + std::unique_ptr key = generate_key(sig_algo, rng); - const Botan::PKCS10_Request req = Botan::X509::create_cert_req(opts, *key, hash_fn, *rng); - Botan::X509_Certificate cert = parent_ca.sign_request(req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); - Botan::X509_CA ca(cert, *key, hash_fn, padding_method, *rng); + Botan::PKCS10_Request req = params.into_pkcs10_request(*key, rng, hash_fn); + + Botan::X509_Certificate cert = parent_ca.sign_request(req, rng, from_date(-1, 01, 01), from_date(2, 01, 01)); + Botan::X509_CA ca(cert, *key, hash_fn, rng); return std::make_pair(std::move(cert), std::move(ca)); } @@ -425,10 +442,7 @@ Test::Result test_x509_ip_addr_blocks_rfc3779_example() { blocks_1->add_address({10, 3, 0, 0}, {10, 3, 255, 255}, 1); blocks_1->inherit(); - Botan::X509_Cert_Options opts_1 = ca_opts(); - opts_1.extensions.add(std::move(blocks_1)); - - auto cert_1 = make_self_signed(rng, opts_1); + auto cert_1 = make_self_signed(*rng, ca_params().add_extension(std::move(blocks_1))); auto bits_1 = cert_1.v3_extensions().get_extension_bits(IPAddressBlocks::static_oid()); @@ -472,10 +486,7 @@ Test::Result test_x509_ip_addr_blocks_rfc3779_example() { blocks_2->add_address({172, 16, 0, 0}, {172, 31, 255, 255}, 1); blocks_2->inherit(2); - Botan::X509_Cert_Options opts_2 = ca_opts(); - opts_2.extensions.add(std::move(blocks_2)); - - auto cert_2 = make_self_signed(rng, opts_2); + auto cert_2 = make_self_signed(*rng, ca_params().add_extension(std::move(blocks_2))); auto bits_2 = cert_2.v3_extensions().get_extension_bits(IPAddressBlocks::static_oid()); @@ -539,10 +550,7 @@ Test::Result test_x509_ip_addr_blocks_encode_builder() { blocks->add_address({123, 123, 2, 1}); - Botan::X509_Cert_Options opts = ca_opts(); - opts.extensions.add(std::move(blocks)); - - auto cert = make_self_signed(rng, opts); + auto cert = make_self_signed(*rng, ca_params().add_extension(std::move(blocks))); auto bits = cert.v3_extensions().get_extension_bits(IPAddressBlocks::static_oid()); // hand validated with https://lapo.it/asn1js/ @@ -576,7 +584,7 @@ Test::Result test_x509_ip_addr_blocks_extension_encode_ctor() { auto rng = Test::new_rng(__func__); - auto [ca_cert, ca, sub_key, sig_algo, hash_fn] = make_ca(rng); + auto [ca_cert, ca, sub_key, sig_algo, hash_fn] = make_ca(*rng); for(size_t i = 0; i < 64; i++) { const bool push_ipv4_ranges = bit_set<0>(i); @@ -586,8 +594,6 @@ Test::Result test_x509_ip_addr_blocks_extension_encode_ctor() { const bool push_ipv4_family = bit_set<4>(i); const bool push_ipv6_family = bit_set<5>(i); - Botan::X509_Cert_Options opts = req_opts(sig_algo); - std::vector a = {123, 123, 2, 1}; auto ipv4_1 = IPAddressBlocks::IPAddress(a); a = {255, 255, 255, 255}; @@ -672,10 +678,8 @@ Test::Result test_x509_ip_addr_blocks_extension_encode_ctor() { std::unique_ptr blocks = std::make_unique(addr_blocks); - opts.extensions.add(std::move(blocks)); - - const Botan::PKCS10_Request req = Botan::X509::create_cert_req(opts, *sub_key, hash_fn, *rng); - const Botan::X509_Certificate cert = ca.sign_request(req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); + auto req = user_params().add_extension(std::move(blocks)).into_pkcs10_request(*sub_key, *rng, hash_fn); + Botan::X509_Certificate cert = ca.sign_request(req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); { const auto* ip_blocks = cert.v3_extensions().get_extension_object_as(); result.test_is_true("cert has IPAddrBlocks extension", ip_blocks != nullptr); @@ -753,7 +757,7 @@ Test::Result test_x509_ip_addr_blocks_extension_encode_edge_cases_ctor() { std::vector edge_values = {0, 2, 4, 8, 16, 32, 64, 128, 1, 3, 7, 15, 31, 63, 127, 255, 12, 46, 123, 160, 234}; - auto [ca_cert, ca, sub_key, sig_algo, hash_fn] = make_ca(rng); + auto [ca_cert, ca, sub_key, sig_algo, hash_fn] = make_ca(*rng); for(size_t i = 0; i < edge_values.size(); i++) { for(size_t j = 0; j < 4; j++) { @@ -767,8 +771,6 @@ Test::Result test_x509_ip_addr_blocks_extension_encode_edge_cases_ctor() { break; } - Botan::X509_Cert_Options opts = req_opts(sig_algo); - std::vector min_bytes(16, 0x00); std::vector max_bytes(16, 0xFF); @@ -796,11 +798,8 @@ Test::Result test_x509_ip_addr_blocks_extension_encode_edge_cases_ctor() { std::unique_ptr blocks = std::make_unique(addr_blocks); - opts.extensions.add(std::move(blocks)); - - const Botan::PKCS10_Request req = Botan::X509::create_cert_req(opts, *sub_key, hash_fn, *rng); - const Botan::X509_Certificate cert = - ca.sign_request(req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); + auto req = user_params().add_extension(std::move(blocks)).into_pkcs10_request(*sub_key, *rng, hash_fn); + Botan::X509_Certificate cert = ca.sign_request(req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); { const auto* ip_blocks = cert.v3_extensions().get_extension_object_as(); result.test_not_null("cert has IPAddrBlocks extension", ip_blocks); @@ -829,8 +828,7 @@ Test::Result test_x509_ip_addr_blocks_range_merge() { auto rng = Test::new_rng(__func__); - auto [ca_cert, ca, sub_key, sig_algo, hash_fn] = make_ca(rng); - Botan::X509_Cert_Options opts = req_opts(sig_algo); + auto [ca_cert, ca, sub_key, sig_algo, hash_fn] = make_ca(*rng); const std::vector>> addresses = { {{11, 0, 0, 0}, {{11, 0, 0, 0}}}, @@ -860,10 +858,8 @@ Test::Result test_x509_ip_addr_blocks_range_merge() { std::unique_ptr blocks = std::make_unique(addr_blocks); - opts.extensions.add(std::move(blocks)); - - const Botan::PKCS10_Request req = Botan::X509::create_cert_req(opts, *sub_key, hash_fn, *rng); - const Botan::X509_Certificate cert = ca.sign_request(req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); + auto req = user_params().add_extension(std::move(blocks)).into_pkcs10_request(*sub_key, *rng, hash_fn); + Botan::X509_Certificate cert = ca.sign_request(req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); { const auto* ip_blocks = cert.v3_extensions().get_extension_object_as(); result.test_not_null("cert has IPAddrBlocks extension", ip_blocks); @@ -892,8 +888,7 @@ Test::Result test_x509_ip_addr_blocks_family_merge() { auto rng = Test::new_rng(__func__); - auto [ca_cert, ca, sub_key, sig_algo, hash_fn] = make_ca(rng); - Botan::X509_Cert_Options opts = req_opts(sig_algo); + auto [ca_cert, ca, sub_key, sig_algo, hash_fn] = make_ca(*rng); std::vector addr_blocks; @@ -947,10 +942,8 @@ Test::Result test_x509_ip_addr_blocks_family_merge() { std::unique_ptr blocks = std::make_unique(addr_blocks); - opts.extensions.add(std::move(blocks)); - - const Botan::PKCS10_Request req = Botan::X509::create_cert_req(opts, *sub_key, hash_fn, *rng); - const Botan::X509_Certificate cert = ca.sign_request(req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); + auto req = user_params().add_extension(std::move(blocks)).into_pkcs10_request(*sub_key, *rng, hash_fn); + Botan::X509_Certificate cert = ca.sign_request(req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); const auto* ip_blocks = cert.v3_extensions().get_extension_object_as(); result.test_not_null("cert has IPAddrBlocks extension", ip_blocks); @@ -1115,12 +1108,9 @@ Test::Result test_x509_ip_addr_blocks_path_validation_success_builder() { {0x00, 0x00, 0x00, 0xAB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, {0x0D, 0x00, 0x00, 0xAB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); - Botan::X509_Cert_Options root_opts = ca_opts(); - root_opts.extensions.add(std::move(root_blocks)); - auto [root_cert, root_ca, sub_key, sig_algo, hash_fn] = make_ca(rng, root_opts); - Botan::X509_Cert_Options sub_opts = req_opts(sig_algo); - sub_opts.extensions.add(std::move(sub_blocks)); - auto [inherit_cert, inherit_ca] = make_and_sign_ca(std::move(inherit_blocks), root_ca, rng); + auto [root_cert, root_ca, sub_key, sig_algo, hash_fn] = + make_ca(*rng, ca_params().add_extension(std::move(root_blocks))); + auto [inherit_cert, inherit_ca] = make_and_sign_ca(std::move(inherit_blocks), root_ca, *rng); Botan::Certificate_Store_In_Memory trusted; trusted.add_certificate(root_cert); @@ -1146,11 +1136,11 @@ Test::Result test_x509_ip_addr_blocks_path_validation_success_builder() { dyn_blocks->inherit(); } - auto [dyn_cert, dyn_ca] = make_and_sign_ca(std::move(dyn_blocks), inherit_ca, rng); + auto [dyn_cert, dyn_ca] = make_and_sign_ca(std::move(dyn_blocks), inherit_ca, *rng); - const Botan::PKCS10_Request sub_req = Botan::X509::create_cert_req(sub_opts, *sub_key, hash_fn, *rng); - const Botan::X509_Certificate sub_cert = - dyn_ca.sign_request(sub_req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); + const auto sub_req = user_params().add_extension(sub_blocks->copy()).into_pkcs10_request(*sub_key, *rng, hash_fn); + + const auto sub_cert = dyn_ca.sign_request(sub_req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); const Botan::Path_Validation_Restrictions restrictions(false, 80); const std::vector certs = {sub_cert, dyn_cert, inherit_cert}; @@ -1280,12 +1270,9 @@ Test::Result test_x509_ip_addr_blocks_path_validation_success_ctor() { auto sub_addr_blocks = {sub_ipv4_family, sub_ipv6_family}; std::unique_ptr sub_blocks = std::make_unique(sub_addr_blocks); - Botan::X509_Cert_Options root_opts = ca_opts(); - root_opts.extensions.add(std::move(root_blocks)); - auto [root_cert, root_ca, sub_key, sig_algo, hash_fn] = make_ca(rng, root_opts); - Botan::X509_Cert_Options sub_opts = req_opts(sig_algo); - sub_opts.extensions.add(std::move(sub_blocks)); - auto [inherit_cert, inherit_ca] = make_and_sign_ca(std::move(inherit_blocks), root_ca, rng); + auto [root_cert, root_ca, sub_key, sig_algo, hash_fn] = + make_ca(*rng, ca_params().add_extension(std::move(root_blocks))); + auto [inherit_cert, inherit_ca] = make_and_sign_ca(std::move(inherit_blocks), root_ca, *rng); Botan::Certificate_Store_In_Memory trusted; trusted.add_certificate(root_cert); @@ -1305,11 +1292,10 @@ Test::Result test_x509_ip_addr_blocks_path_validation_success_ctor() { auto dyn_addr_blocks = {dyn_ipv4_family, dyn_ipv6_family}; std::unique_ptr dyn_blocks = std::make_unique(dyn_addr_blocks); - auto [dyn_cert, dyn_ca] = make_and_sign_ca(std::move(dyn_blocks), inherit_ca, rng); + auto [dyn_cert, dyn_ca] = make_and_sign_ca(std::move(dyn_blocks), inherit_ca, *rng); - const Botan::PKCS10_Request sub_req = Botan::X509::create_cert_req(sub_opts, *sub_key, hash_fn, *rng); - const Botan::X509_Certificate sub_cert = - dyn_ca.sign_request(sub_req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); + const auto sub_req = user_params().add_extension(sub_blocks->copy()).into_pkcs10_request(*sub_key, *rng, hash_fn); + const auto sub_cert = dyn_ca.sign_request(sub_req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); const Botan::Path_Validation_Restrictions restrictions(false, 80); const std::vector certs = {sub_cert, dyn_cert, inherit_cert}; @@ -1346,11 +1332,11 @@ Test::Result test_x509_ip_addr_blocks_path_validation_failure_builder() { root_blocks->inherit(42); } - Botan::X509_Cert_Options root_opts = ca_opts(); + auto root_params = ca_params(); if(!nullptr_extensions) { - root_opts.extensions.add(std::move(root_blocks)); + root_params.add_extension(std::move(root_blocks)); } - auto [root_cert, root_ca, sub_key, sig_algo, hash_fn] = make_ca(rng, root_opts); + auto [root_cert, root_ca, sub_key, sig_algo, hash_fn] = make_ca(*rng, root_params); // Issuer Cert std::unique_ptr iss_blocks = std::make_unique(); @@ -1364,7 +1350,7 @@ Test::Result test_x509_ip_addr_blocks_path_validation_failure_builder() { iss_blocks->inherit(42); } - auto [iss_cert, iss_ca] = make_and_sign_ca(std::move(iss_blocks), root_ca, rng); + auto [iss_cert, iss_ca] = make_and_sign_ca(std::move(iss_blocks), root_ca, *rng); // Subject cert std::unique_ptr sub_blocks = std::make_unique(); @@ -1385,11 +1371,8 @@ Test::Result test_x509_ip_addr_blocks_path_validation_failure_builder() { sub_blocks->inherit(safi); } - Botan::X509_Cert_Options sub_opts = req_opts(sig_algo); - sub_opts.extensions.add(std::move(sub_blocks)); - - const Botan::PKCS10_Request sub_req = Botan::X509::create_cert_req(sub_opts, *sub_key, hash_fn, *rng); - const Botan::X509_Certificate sub_cert = + auto sub_req = user_params().add_extension(std::move(sub_blocks)).into_pkcs10_request(*sub_key, *rng, hash_fn); + Botan::X509_Certificate sub_cert = iss_ca.sign_request(sub_req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); const Botan::Path_Validation_Restrictions restrictions(false, 80); @@ -1436,11 +1419,11 @@ Test::Result test_x509_ip_addr_blocks_path_validation_failure_ctor() { auto root_addr_blocks = {root_family}; std::unique_ptr root_blocks = std::make_unique(root_addr_blocks); - Botan::X509_Cert_Options root_opts = ca_opts(); + auto root_params = ca_params(); if(!nullptr_extensions) { - root_opts.extensions.add(std::move(root_blocks)); + root_params.add_extension(std::move(root_blocks)); } - auto [root_cert, root_ca, sub_key, sig_algo, hash_fn] = make_ca(rng, root_opts); + auto [root_cert, root_ca, sub_key, sig_algo, hash_fn] = make_ca(*rng, root_params); // Issuer Cert a = {122, 0, 0, 255}; @@ -1459,7 +1442,7 @@ Test::Result test_x509_ip_addr_blocks_path_validation_failure_ctor() { auto iss_family = IPAddressBlocks::IPAddressFamily(iss_choice, 42); auto iss_addr_blocks = {iss_family}; std::unique_ptr iss_blocks = std::make_unique(iss_addr_blocks); - auto [iss_cert, iss_ca] = make_and_sign_ca(std::move(iss_blocks), root_ca, rng); + auto [iss_cert, iss_ca] = make_and_sign_ca(std::move(iss_blocks), root_ca, *rng); // Subject cert if(too_small_subrange) { @@ -1488,11 +1471,8 @@ Test::Result test_x509_ip_addr_blocks_path_validation_failure_ctor() { auto sub_addr_blocks = {sub_family}; std::unique_ptr sub_blocks = std::make_unique(sub_addr_blocks); - Botan::X509_Cert_Options sub_opts = req_opts(sig_algo); - sub_opts.extensions.add(std::move(sub_blocks)); - - const Botan::PKCS10_Request sub_req = Botan::X509::create_cert_req(sub_opts, *sub_key, hash_fn, *rng); - const Botan::X509_Certificate sub_cert = + auto sub_req = user_params().add_extension(std::move(sub_blocks)).into_pkcs10_request(*sub_key, *rng, hash_fn); + Botan::X509_Certificate sub_cert = iss_ca.sign_request(sub_req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); const Botan::Path_Validation_Restrictions restrictions(false, 80); @@ -1523,10 +1503,7 @@ Test::Result test_x509_as_blocks_rfc3779_example() { blocks->add_asnum(5001); blocks->inherit_rdi(); - Botan::X509_Cert_Options opts = ca_opts(); - opts.extensions.add(std::move(blocks)); - - auto cert = make_self_signed(rng, opts); + auto cert = make_self_signed(*rng, ca_params().add_extension(std::move(blocks))); auto bits = cert.v3_extensions().get_extension_bits(ASBlocks::static_oid()); result.test_bin_eq( @@ -1560,10 +1537,7 @@ Test::Result test_x509_as_blocks_encode_builder() { // this overwrites the previous two blocks->restrict_asnum(); - Botan::X509_Cert_Options opts = ca_opts(); - opts.extensions.add(std::move(blocks)); - - auto cert = make_self_signed(rng, opts); + auto cert = make_self_signed(*rng, ca_params().add_extension(std::move(blocks))); auto bits = cert.v3_extensions().get_extension_bits(ASBlocks::static_oid()); result.test_bin_eq("extension is encoded as specified", bits, "3011A0023000A10B300930070201090202012D"); @@ -1580,7 +1554,7 @@ Test::Result test_x509_as_blocks_extension_encode_ctor() { auto rng = Test::new_rng(__func__); - auto [ca_cert, ca, sub_key, sig_algo, hash_fn] = make_ca(rng); + auto [ca_cert, ca, sub_key, sig_algo, hash_fn] = make_ca(*rng); for(size_t i = 0; i < 16; i++) { const bool push_asnum = bit_set<0>(i); @@ -1621,11 +1595,8 @@ Test::Result test_x509_as_blocks_extension_encode_ctor() { std::unique_ptr blocks = std::make_unique(ident); - Botan::X509_Cert_Options opts = req_opts(sig_algo); - opts.extensions.add(std::move(blocks)); - - const Botan::PKCS10_Request req = Botan::X509::create_cert_req(opts, *sub_key, hash_fn, *rng); - const Botan::X509_Certificate cert = ca.sign_request(req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); + auto req = user_params().add_extension(std::move(blocks)).into_pkcs10_request(*sub_key, *rng, hash_fn); + Botan::X509_Certificate cert = ca.sign_request(req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); { const auto* as_blocks = cert.v3_extensions().get_extension_object_as(); @@ -1679,8 +1650,7 @@ Test::Result test_x509_as_blocks_range_merge() { auto rng = Test::new_rng(__func__); - auto [ca_cert, ca, sub_key, sig_algo, hash_fn] = make_ca(rng); - Botan::X509_Cert_Options opts = req_opts(sig_algo); + auto [ca_cert, ca, sub_key, sig_algo, hash_fn] = make_ca(*rng); const std::vector> ranges = { {2005, 37005}, @@ -1704,10 +1674,8 @@ Test::Result test_x509_as_blocks_range_merge() { std::unique_ptr blocks = std::make_unique(ident); - opts.extensions.add(std::move(blocks)); - - const Botan::PKCS10_Request req = Botan::X509::create_cert_req(opts, *sub_key, hash_fn, *rng); - const Botan::X509_Certificate cert = ca.sign_request(req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); + const auto req = user_params().add_extension(std::move(blocks)).into_pkcs10_request(*sub_key, *rng, hash_fn); + Botan::X509_Certificate cert = ca.sign_request(req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); { const auto* as_blocks = cert.v3_extensions().get_extension_object_as(); result.test_is_true("cert has ASBlock extension", as_blocks != nullptr); @@ -1772,12 +1740,9 @@ Test::Result test_x509_as_blocks_path_validation_success_builder() { sub_blocks->add_rdi(1567); sub_blocks->add_rdi(33100, 40000); - Botan::X509_Cert_Options root_opts = ca_opts(); - root_opts.extensions.add(std::move(root_blocks)); - auto [root_cert, root_ca, sub_key, sig_algo, hash_fn] = make_ca(rng, root_opts); - Botan::X509_Cert_Options sub_opts = req_opts(sig_algo); - sub_opts.extensions.add(std::move(sub_blocks)); - auto [inherit_cert, inherit_ca] = make_and_sign_ca(std::move(inherit_blocks), root_ca, rng); + auto [root_cert, root_ca, sub_key, sig_algo, hash_fn] = + make_ca(*rng, ca_params().add_extension(std::move(root_blocks))); + auto [inherit_cert, inherit_ca] = make_and_sign_ca(std::move(inherit_blocks), root_ca, *rng); Botan::Certificate_Store_In_Memory trusted; trusted.add_certificate(root_cert); @@ -1802,10 +1767,10 @@ Test::Result test_x509_as_blocks_path_validation_success_builder() { dyn_blocks->inherit_rdi(); } - auto [dyn_cert, dyn_ca] = make_and_sign_ca(std::move(dyn_blocks), inherit_ca, rng); + auto [dyn_cert, dyn_ca] = make_and_sign_ca(std::move(dyn_blocks), inherit_ca, *rng); - const Botan::PKCS10_Request sub_req = Botan::X509::create_cert_req(sub_opts, *sub_key, hash_fn, *rng); - const Botan::X509_Certificate sub_cert = + const auto sub_req = user_params().add_extension(sub_blocks->copy()).into_pkcs10_request(*sub_key, *rng, hash_fn); + Botan::X509_Certificate sub_cert = dyn_ca.sign_request(sub_req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); const Botan::Path_Validation_Restrictions restrictions(false, 80); @@ -1911,12 +1876,9 @@ Test::Result test_x509_as_blocks_path_validation_success_ctor() { const ASBlocks::ASIdentifiers sub_ident = ASBlocks::ASIdentifiers(sub_asnum, sub_rdi); std::unique_ptr sub_blocks = std::make_unique(sub_ident); - Botan::X509_Cert_Options root_opts = ca_opts(); - root_opts.extensions.add(std::move(root_blocks)); - auto [root_cert, root_ca, sub_key, sig_algo, hash_fn] = make_ca(rng, root_opts); - Botan::X509_Cert_Options sub_opts = req_opts(sig_algo); - sub_opts.extensions.add(std::move(sub_blocks)); - auto [inherit_cert, inherit_ca] = make_and_sign_ca(std::move(inherit_blocks), root_ca, rng); + auto [root_cert, root_ca, sub_key, sig_algo, hash_fn] = + make_ca(*rng, ca_params().add_extension(std::move(root_blocks))); + auto [inherit_cert, inherit_ca] = make_and_sign_ca(std::move(inherit_blocks), root_ca, *rng); Botan::Certificate_Store_In_Memory trusted; trusted.add_certificate(root_cert); @@ -1932,10 +1894,10 @@ Test::Result test_x509_as_blocks_path_validation_success_ctor() { const ASBlocks::ASIdentifiers dyn_ident = ASBlocks::ASIdentifiers(dyn_asnum, dyn_rdi); std::unique_ptr dyn_blocks = std::make_unique(dyn_ident); - auto [dyn_cert, dyn_ca] = make_and_sign_ca(std::move(dyn_blocks), inherit_ca, rng); + auto [dyn_cert, dyn_ca] = make_and_sign_ca(std::move(dyn_blocks), inherit_ca, *rng); - const Botan::PKCS10_Request sub_req = Botan::X509::create_cert_req(sub_opts, *sub_key, hash_fn, *rng); - const Botan::X509_Certificate sub_cert = + const auto sub_req = user_params().add_extension(sub_blocks->copy()).into_pkcs10_request(*sub_key, *rng, hash_fn); + Botan::X509_Certificate sub_cert = dyn_ca.sign_request(sub_req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); const Botan::Path_Validation_Restrictions restrictions(false, 80); @@ -1969,13 +1931,9 @@ Test::Result test_x509_as_blocks_path_validation_extension_not_present_builder() sub_blocks->add_rdi(33100, 40000); // create a root ca that does not have any extension - const Botan::X509_Cert_Options root_opts = ca_opts(); - auto [root_cert, root_ca, sub_key, sig_algo, hash_fn] = make_ca(rng, root_opts); - Botan::X509_Cert_Options sub_opts = req_opts(sig_algo); - sub_opts.extensions.add(std::move(sub_blocks)); - const Botan::PKCS10_Request sub_req = Botan::X509::create_cert_req(sub_opts, *sub_key, hash_fn, *rng); - const Botan::X509_Certificate sub_cert = - root_ca.sign_request(sub_req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); + auto [root_cert, root_ca, sub_key, sig_algo, hash_fn] = make_ca(*rng, ca_params()); + auto sub_req = user_params().add_extension(std::move(sub_blocks)).into_pkcs10_request(*sub_key, *rng, hash_fn); + Botan::X509_Certificate sub_cert = root_ca.sign_request(sub_req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); Botan::Certificate_Store_In_Memory trusted; trusted.add_certificate(root_cert); @@ -2028,13 +1986,9 @@ Test::Result test_x509_as_blocks_path_validation_extension_not_present_ctor() { std::unique_ptr sub_blocks = std::make_unique(sub_ident); // create a root ca that does not have any extension - const Botan::X509_Cert_Options root_opts = ca_opts(); - auto [root_cert, root_ca, sub_key, sig_algo, hash_fn] = make_ca(rng, root_opts); - Botan::X509_Cert_Options sub_opts = req_opts(sig_algo); - sub_opts.extensions.add(std::move(sub_blocks)); - const Botan::PKCS10_Request sub_req = Botan::X509::create_cert_req(sub_opts, *sub_key, hash_fn, *rng); - const Botan::X509_Certificate sub_cert = - root_ca.sign_request(sub_req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); + auto [root_cert, root_ca, sub_key, sig_algo, hash_fn] = make_ca(*rng, ca_params()); + const auto sub_req = user_params().add_extension(std::move(sub_blocks)).into_pkcs10_request(*sub_key, *rng, hash_fn); + Botan::X509_Certificate sub_cert = root_ca.sign_request(sub_req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); Botan::Certificate_Store_In_Memory trusted; trusted.add_certificate(root_cert); @@ -2222,15 +2176,13 @@ Test::Result test_x509_as_blocks_path_validation_failure_builder() { sub_blocks->inherit_rdi(); } - Botan::X509_Cert_Options root_opts = ca_opts(); - root_opts.extensions.add(std::move(root_blocks)); - auto [root_cert, root_ca, sub_key, sig_algo, hash_fn] = make_ca(rng, root_opts); - auto [issu_cert, issu_ca] = make_and_sign_ca(std::move(issu_blocks), root_ca, rng); + auto [root_cert, root_ca, sub_key, sig_algo, hash_fn] = + make_ca(*rng, ca_params().add_extension(std::move(root_blocks))); + auto [issu_cert, issu_ca] = make_and_sign_ca(std::move(issu_blocks), root_ca, *rng); - Botan::X509_Cert_Options sub_opts = req_opts(sig_algo); - sub_opts.extensions.add(std::move(sub_blocks)); - const Botan::PKCS10_Request sub_req = Botan::X509::create_cert_req(sub_opts, *sub_key, hash_fn, *rng); - const Botan::X509_Certificate sub_cert = + const auto sub_req = + user_params().add_extension(std::move(sub_blocks)).into_pkcs10_request(*sub_key, *rng, hash_fn); + Botan::X509_Certificate sub_cert = issu_ca.sign_request(sub_req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); Botan::Certificate_Store_In_Memory trusted; @@ -2418,15 +2370,13 @@ Test::Result test_x509_as_blocks_path_validation_failure_ctor() { const ASBlocks::ASIdentifiers sub_ident = ASBlocks::ASIdentifiers(sub_asnum, sub_rdi); std::unique_ptr sub_blocks = std::make_unique(sub_ident); - Botan::X509_Cert_Options root_opts = ca_opts(); - root_opts.extensions.add(std::move(root_blocks)); - auto [root_cert, root_ca, sub_key, sig_algo, hash_fn] = make_ca(rng, root_opts); - auto [issu_cert, issu_ca] = make_and_sign_ca(std::move(issu_blocks), root_ca, rng); + auto [root_cert, root_ca, sub_key, sig_algo, hash_fn] = + make_ca(*rng, ca_params().add_extension(std::move(root_blocks))); + auto [issu_cert, issu_ca] = make_and_sign_ca(std::move(issu_blocks), root_ca, *rng); - Botan::X509_Cert_Options sub_opts = req_opts(sig_algo); - sub_opts.extensions.add(std::move(sub_blocks)); - const Botan::PKCS10_Request sub_req = Botan::X509::create_cert_req(sub_opts, *sub_key, hash_fn, *rng); - const Botan::X509_Certificate sub_cert = + const auto sub_req = + user_params().add_extension(std::move(sub_blocks)).into_pkcs10_request(*sub_key, *rng, hash_fn); + Botan::X509_Certificate sub_cert = issu_ca.sign_request(sub_req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01)); Botan::Certificate_Store_In_Memory trusted; From 7024b40cb7b062f2e8c61762040ee02c6c488fc4 Mon Sep 17 00:00:00 2001 From: arckoor <33837362+arckoor@users.noreply.github.com> Date: Wed, 8 Jul 2026 11:26:36 +0200 Subject: [PATCH 2/3] Fix type issues, add into_cert --- src/cli/perf_x509.cpp | 5 ++- src/lib/x509/x509_builder.cpp | 78 +++++++++++++++++++++++++++++++++-- src/lib/x509/x509_builder.h | 37 ++++++++++++++++- src/lib/x509/x509opt.cpp | 2 +- src/lib/x509/x509self.cpp | 3 +- src/tests/test_x509_rpki.cpp | 4 +- 6 files changed, 119 insertions(+), 10 deletions(-) diff --git a/src/cli/perf_x509.cpp b/src/cli/perf_x509.cpp index 17491cf35ef..75e2f89a30c 100644 --- a/src/cli/perf_x509.cpp +++ b/src/cli/perf_x509.cpp @@ -12,6 +12,7 @@ #include #if defined(BOTAN_HAS_X509) + #include #include #include #include @@ -61,8 +62,8 @@ class PerfTest_ASN1_Parsing final : public PerfTest { const auto not_before = std::chrono::system_clock::now(); const auto not_after = not_before + std::chrono::seconds(86400); - auto root_cert = - root_cert_params.into_self_signed_cert(not_before, not_after, *root_key, rng, get_hash_function()); + auto root_cert = root_cert_params.into_self_signed_cert( + not_before, not_after, *root_key, rng, std::nullopt, get_hash_function()); auto ca = Botan::X509_CA(root_cert, *root_key, get_hash_function(), rng); return CA{ diff --git a/src/lib/x509/x509_builder.cpp b/src/lib/x509/x509_builder.cpp index 6c06c3b1239..01940deb7e8 100644 --- a/src/lib/x509/x509_builder.cpp +++ b/src/lib/x509/x509_builder.cpp @@ -6,7 +6,9 @@ #include +#include #include +#include #include #include #include @@ -179,6 +181,7 @@ X509_Certificate CertificateParametersBuilder::into_self_signed_cert(std::chrono std::chrono::system_clock::time_point not_after, const Private_Key& key, RandomNumberGenerator& rng, + std::optional serial_number, std::optional hash_fn, std::optional padding) const { auto signer_p = X509_Object::choose_sig_format(key, rng, hash_fn.value_or(""), padding.value_or("")); @@ -190,15 +193,84 @@ X509_Certificate CertificateParametersBuilder::into_self_signed_cert(std::chrono Extensions extensions = m_state->finalize_extensions(key); const std::vector pub_key = key.subject_public_key(); - auto skid = std::make_unique(pub_key, signer.hash_function()); + auto skid = std::make_unique(key); extensions.add_new(std::make_unique(skid->get_key_id())); extensions.add_new(std::move(skid)); const auto& subject_dn = m_state->subject_dn(); - return X509_CA::make_cert( - signer, rng, sig_algo, pub_key, X509_Time(not_before), X509_Time(not_after), subject_dn, subject_dn, extensions); + if(serial_number.has_value()) { + return X509_CA::make_cert(signer, + rng, + serial_number.value(), + sig_algo, + pub_key, + X509_Time(not_before), + X509_Time(not_after), + subject_dn, + subject_dn, + extensions); + } else { + return X509_CA::make_cert(signer, + rng, + sig_algo, + pub_key, + X509_Time(not_before), + X509_Time(not_after), + subject_dn, + subject_dn, + extensions); + } +} + +X509_Certificate CertificateParametersBuilder::into_cert(std::chrono::system_clock::time_point not_before, + std::chrono::system_clock::time_point not_after, + const X509_Certificate& ca_cert, + const Private_Key& ca_key, + const Private_Key& key, + RandomNumberGenerator& rng, + std::optional serial_number, + std::optional hash_fn, + std::optional padding) const { + auto signer_p = X509_Object::choose_sig_format(ca_key, rng, hash_fn.value_or(""), padding.value_or("")); + auto& signer = *signer_p; + + const AlgorithmIdentifier sig_algo = signer.algorithm_identifier(); + BOTAN_ASSERT_NOMSG(sig_algo.oid().has_value()); + + Extensions extensions = m_state->finalize_extensions(key); + + const std::vector pub_key = key.subject_public_key(); + auto skid = std::make_unique(key); + + extensions.add_new(std::make_unique(ca_cert.subject_key_id())); + extensions.add_new(std::move(skid)); + + const auto& subject_dn = m_state->subject_dn(); + + if(serial_number.has_value()) { + return X509_CA::make_cert(signer, + rng, + serial_number.value(), + sig_algo, + pub_key, + X509_Time(not_before), + X509_Time(not_after), + ca_cert.subject_dn(), + subject_dn, + extensions); + } else { + return X509_CA::make_cert(signer, + rng, + sig_algo, + pub_key, + X509_Time(not_before), + X509_Time(not_after), + ca_cert.subject_dn(), + subject_dn, + extensions); + } } PKCS10_Request CertificateParametersBuilder::into_pkcs10_request( diff --git a/src/lib/x509/x509_builder.h b/src/lib/x509/x509_builder.h index 57f809de3c6..8356f9d799d 100644 --- a/src/lib/x509/x509_builder.h +++ b/src/lib/x509/x509_builder.h @@ -17,6 +17,7 @@ namespace Botan { +class BigInt; class Certificate_Extension; class DNSName; class EmailAddress; @@ -52,19 +53,53 @@ class BOTAN_PUBLIC_API(3, 9) CertificateParametersBuilder final { * @param not_after the final validity time of the generated certificate * @param key the private key * @param rng the rng to use + * @param serial_number the serial number assigned to the created certificate; + * randomly generated if unset * @param hash_fn the hash function to use; if unset a reasonable default is used * @param padding optional padding scheme (for specifying RSA-PSS vs RSA-PKCS1, * otherwise not necessary) * * @return newly created self-signed certificate + * + * TODO(C++26) Use std::optional instead */ X509_Certificate into_self_signed_cert(std::chrono::system_clock::time_point not_before, std::chrono::system_clock::time_point not_after, const Private_Key& key, RandomNumberGenerator& rng, + std::optional serial_number, std::optional hash_fn = {}, std::optional padding = {}) const; + /** + * Create a X.509 certificate signed by a certificate authority from these parameters. + * + * @param not_before the initial validity time of the generated certificate + * @param not_after the final validity time of the generated certificate + * @param ca_cert the CA certificate + * @param ca_key the CA certificate private key + * @param key the private key + * @param rng the rng to use + * @param serial_number the serial number assigned to the created certificate; + * randomly generated if unset + * @param hash_fn the hash function to use; if unset a reasonable default is used + * @param padding optional padding scheme (for specifying RSA-PSS vs RSA-PKCS1, + * otherwise not necessary) + * + * @return newly created certificate + * + * TODO(C++26) Use std::optional instead + */ + X509_Certificate into_cert(std::chrono::system_clock::time_point not_before, + std::chrono::system_clock::time_point not_after, + const X509_Certificate& ca_cert, + const Private_Key& ca_key, + const Private_Key& key, + RandomNumberGenerator& rng, + std::optional serial_number, + std::optional hash_fn, + std::optional padding) const; + /** * Create a PKCS10 request * @@ -156,7 +191,7 @@ class BOTAN_PUBLIC_API(3, 9) CertificateParametersBuilder final { /** * Add another allowed usage to the KeyUsage extension * - * This is cummulative; all usages indicated are ORed together + * This is cumulative; all usages indicated are ORed together */ CertificateParametersBuilder& add_allowed_usage(Key_Constraints kc); diff --git a/src/lib/x509/x509opt.cpp b/src/lib/x509/x509opt.cpp index 1e7e74debf3..41416446021 100644 --- a/src/lib/x509/x509opt.cpp +++ b/src/lib/x509/x509opt.cpp @@ -133,7 +133,7 @@ CertificateParametersBuilder X509_Cert_Options::into_builder() const { } } if(!this->uri.empty()) { - if(auto parsed = URI::parse(this->uri)) { + if(auto parsed = URI::from_string(this->uri)) { builder.add_uri(*parsed); } else { throw Invalid_Argument(fmt("Invalid URI '{}'", this->uri)); diff --git a/src/lib/x509/x509self.cpp b/src/lib/x509/x509self.cpp index 7533a3c15d1..8e0cd575663 100644 --- a/src/lib/x509/x509self.cpp +++ b/src/lib/x509/x509self.cpp @@ -5,6 +5,7 @@ * Botan is released under the Simplified BSD License (see license.txt) */ +#include #include namespace Botan::X509 { @@ -22,7 +23,7 @@ X509_Certificate create_self_signed_cert(const X509_Cert_Options& opts, const std::optional padding = (opts.padding_scheme.empty()) ? std::nullopt : std::optional(opts.padding_scheme); - return opts.into_builder().into_self_signed_cert(not_before, not_after, key, rng, hash_fn, padding); + return opts.into_builder().into_self_signed_cert(not_before, not_after, key, rng, std::nullopt, hash_fn, padding); } /* diff --git a/src/tests/test_x509_rpki.cpp b/src/tests/test_x509_rpki.cpp index 79383ea9a56..a2646baa23f 100644 --- a/src/tests/test_x509_rpki.cpp +++ b/src/tests/test_x509_rpki.cpp @@ -112,7 +112,7 @@ Botan::X509_Certificate make_self_signed(Botan::RandomNumberGenerator& rng, const auto not_before = now - std::chrono::seconds(180); const auto not_after = now + std::chrono::seconds(86400); - return params.into_self_signed_cert(not_before, not_after, *key, rng, hash_fn); + return params.into_self_signed_cert(not_before, not_after, *key, rng, std::nullopt, hash_fn); } CA_Creation_Result make_ca(Botan::RandomNumberGenerator& rng, const Botan::CertificateParametersBuilder& params) { @@ -123,7 +123,7 @@ CA_Creation_Result make_ca(Botan::RandomNumberGenerator& rng, const Botan::Certi const auto not_before = now - std::chrono::seconds(180); const auto not_after = now + std::chrono::seconds(86400); - const auto ca_cert = params.into_self_signed_cert(not_before, not_after, *ca_key, rng, hash_fn); + const auto ca_cert = params.into_self_signed_cert(not_before, not_after, *ca_key, rng, std::nullopt, hash_fn); Botan::X509_CA ca(ca_cert, *ca_key, hash_fn, rng); auto sub_key = generate_key(sig_algo, rng); From 03989f20562c1568eab57c620bf359cffe57d898 Mon Sep 17 00:00:00 2001 From: arckoor <33837362+arckoor@users.noreply.github.com> Date: Tue, 21 Jul 2026 13:02:31 +0200 Subject: [PATCH 3/3] Add certificate creation to FFI --- src/lib/ffi/ffi.h | 108 +++++++++++ src/lib/ffi/ffi_cert.cpp | 362 ++++++++++++++++++++++++++++++++++++- src/lib/ffi/ffi_cert.h | 4 + src/python/botan3.py | 336 ++++++++++++++++++++++++++++++++-- src/scripts/test_python.py | 32 ++++ 5 files changed, 822 insertions(+), 20 deletions(-) diff --git a/src/lib/ffi/ffi.h b/src/lib/ffi/ffi.h index 7e10fe26ba6..dcbb0873802 100644 --- a/src/lib/ffi/ffi.h +++ b/src/lib/ffi/ffi.h @@ -2704,6 +2704,114 @@ int botan_x509_cert_verify(int* validation_result, */ BOTAN_FFI_EXPORT(2, 8) const char* botan_x509_cert_validation_status(int code); +typedef struct botan_x509_cert_builder_struct* botan_x509_cert_builder_t; +typedef struct botan_x509_pkcs10_req_struct* botan_x509_pkcs10_req_t; + +BOTAN_FFI_EXPORT(3, 13) int botan_x509_cert_builder_destroy(botan_x509_cert_builder_t builder); + +BOTAN_FFI_EXPORT(3, 13) int botan_x509_cert_builder_create(botan_x509_cert_builder_t* builder_obj); + +BOTAN_FFI_EXPORT(3, 13) +int botan_x509_cert_builder_add_common_name(botan_x509_cert_builder_t builder, const char* value); + +BOTAN_FFI_EXPORT(3, 13) int botan_x509_cert_builder_add_country(botan_x509_cert_builder_t builder, const char* value); + +BOTAN_FFI_EXPORT(3, 13) +int botan_x509_cert_builder_add_organization(botan_x509_cert_builder_t builder, const char* value); + +BOTAN_FFI_EXPORT(3, 13) +int botan_x509_cert_builder_add_organizational_unit(botan_x509_cert_builder_t builder, const char* value); + +BOTAN_FFI_EXPORT(3, 13) int botan_x509_cert_builder_add_locality(botan_x509_cert_builder_t builder, const char* value); + +BOTAN_FFI_EXPORT(3, 13) int botan_x509_cert_builder_add_state(botan_x509_cert_builder_t builder, const char* value); + +BOTAN_FFI_EXPORT(3, 13) +int botan_x509_cert_builder_add_serial_number(botan_x509_cert_builder_t builder, const char* value); + +BOTAN_FFI_EXPORT(3, 13) int botan_x509_cert_builder_add_email(botan_x509_cert_builder_t builder, const char* value); + +BOTAN_FFI_EXPORT(3, 13) int botan_x509_cert_builder_add_uri(botan_x509_cert_builder_t builder, const char* value); + +BOTAN_FFI_EXPORT(3, 13) int botan_x509_cert_builder_add_dns(botan_x509_cert_builder_t builder, const char* value); + +BOTAN_FFI_EXPORT(3, 13) int botan_x509_cert_builder_add_ipv4(botan_x509_cert_builder_t builder, const char* value); + +BOTAN_FFI_EXPORT(3, 13) int botan_x509_cert_builder_add_ipv6(botan_x509_cert_builder_t builder, const char* value); + +BOTAN_FFI_EXPORT(3, 13) +int botan_x509_cert_builder_add_xmpp(botan_x509_cert_builder_t builder, const char* value); + +BOTAN_FFI_EXPORT(3, 13) +int botan_x509_cert_builder_add_allowed_usage(botan_x509_cert_builder_t builder, uint32_t usage); + +BOTAN_FFI_EXPORT(3, 13) +int botan_x509_cert_builder_add_allowed_extended_usage(botan_x509_cert_builder_t builder, botan_asn1_oid_t oid); + +BOTAN_FFI_EXPORT(3, 13) +int botan_x509_cert_builder_set_as_ca_certificate(botan_x509_cert_builder_t builder, size_t* limit); + +BOTAN_FFI_EXPORT(3, 13) +int botan_x509_cert_builder_into_self_signed_cert(botan_x509_cert_t* cert_obj, + botan_x509_cert_builder_t builder, + botan_privkey_t key, + botan_rng_t rng, + uint64_t not_before, + uint64_t not_after, + const botan_mp_t* serial_number, + const char* hash_fn, + const char* padding); + +BOTAN_FFI_EXPORT(3, 13) +int botan_x509_cert_builder_into_cert(botan_x509_cert_t* cert_obj, + botan_x509_cert_builder_t builder, + botan_x509_cert_t ca_cert, + botan_privkey_t ca_key, + botan_privkey_t key, + botan_rng_t rng, + uint64_t not_before, + uint64_t not_after, + const botan_mp_t* serial_number, + const char* hash_fn, + const char* padding); + +BOTAN_FFI_EXPORT(3, 13) +int botan_x509_cert_builder_into_pkcs10_req(botan_x509_pkcs10_req_t* req_obj, + botan_x509_cert_builder_t builder, + botan_privkey_t key, + botan_rng_t rng, + const char* hash_fn, + const char* padding, + const char* challenge_password); + +BOTAN_FFI_EXPORT(3, 13) int botan_x509_pkcs10_req_destroy(botan_x509_pkcs10_req_t req); + +BOTAN_FFI_EXPORT(3, 13) int botan_x509_pkcs10_req_load_file(botan_x509_pkcs10_req_t* req_obj, const char* req_path); + +BOTAN_FFI_EXPORT(3, 13) +int botan_x509_pkcs10_req_load(botan_x509_pkcs10_req_t* req_obj, const uint8_t req_bits[], size_t req_bits_len); + +BOTAN_FFI_EXPORT(3, 13) +int botan_x509_pkcs10_req_view_pem(botan_x509_pkcs10_req_t req, botan_view_ctx ctx, botan_view_str_fn view); + +BOTAN_FFI_EXPORT(3, 13) +int botan_x509_pkcs10_req_view_der(botan_x509_pkcs10_req_t req, botan_view_ctx ctx, botan_view_bin_fn view); + +BOTAN_FFI_EXPORT(3, 13) int botan_x509_pkcs10_req_get_public_key(botan_x509_pkcs10_req_t req, botan_pubkey_t* key); + +BOTAN_FFI_EXPORT(3, 13) int botan_x509_pkcs10_req_verify_signature(botan_x509_pkcs10_req_t req, botan_pubkey_t key); + +BOTAN_FFI_EXPORT(3, 13) +int botan_x509_pkcs10_req_sign(botan_x509_cert_t* subject_cert, + botan_x509_pkcs10_req_t subject_req, + botan_x509_cert_t issuing_cert, + botan_privkey_t issuing_key, + botan_rng_t rng, + uint64_t not_before, + uint64_t not_after, + const botan_mp_t* serial_number, + const char* hash_fn, + const char* padding); /* * X.509 CRL **************************/ diff --git a/src/lib/ffi/ffi_cert.cpp b/src/lib/ffi/ffi_cert.cpp index f0480221952..f4d33086abd 100644 --- a/src/lib/ffi/ffi_cert.cpp +++ b/src/lib/ffi/ffi_cert.cpp @@ -1,5 +1,6 @@ /* * (C) 2015,2017,2018 Jack Lloyd +* (C) 2026 Dominik Schricker * * Botan is released under the Simplified BSD License (see license.txt) */ @@ -7,6 +8,11 @@ #include #include +#include +#include +#include +#include +#include #include #include #include @@ -157,12 +163,68 @@ std::chrono::system_clock::time_point timepoint_from_timestamp(uint64_t time_sin std::string default_from_ptr(const char* value) { std::string ret; - if(value != nullptr) { + if(!Botan::any_null_pointers(value)) { ret = value; } return ret; } +std::optional optional_from_ptr(const char* value) { + if(!Botan::any_null_pointers(value)) { + return std::string(value); + } + return std::nullopt; +} + +Botan::X509_Time time_from_timestamp(uint64_t time_since_epoch) { + return Botan::X509_Time(timepoint_from_timestamp(time_since_epoch)); +} + + #if defined(BOTAN_HAS_X509_CERTIFICATES) + #define X509_CERT_BUILDER_ADD_STRING(FIELD_NAME) \ + int botan_x509_cert_builder_add_##FIELD_NAME(botan_x509_cert_builder_t builder, const char* value) { \ + if(Botan::any_null_pointers(value)) { \ + return BOTAN_FFI_ERROR_NULL_POINTER; \ + } \ + return BOTAN_FFI_VISIT(builder, [=](auto& b) { b.add_##FIELD_NAME(value); }); \ + } + #else + #define X509_CERT_BUILDER_ADD_STRING(FIELD_NAME) \ + int botan_x509_cert_builder_add_##FIELD_NAME(botan_x509_cert_builder_t builder, const char* value) { \ + if(Botan::any_null_pointers(value)) { \ + return BOTAN_FFI_ERROR_NULL_POINTER; \ + } \ + BOTAN_UNUSED(builder); \ + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; \ + } + #endif + + #if defined(BOTAN_HAS_X509_CERTIFICATES) + #define X509_CERT_BUILDER_ADD_TYPED(FIELD_NAME, TYPE_NAME) \ + int botan_x509_cert_builder_add_##FIELD_NAME(botan_x509_cert_builder_t builder, const char* value) { \ + if(Botan::any_null_pointers(value)) { \ + return BOTAN_FFI_ERROR_NULL_POINTER; \ + } \ + return BOTAN_FFI_VISIT(builder, [=](auto& b) { \ + std::optional v = TYPE_NAME::from_string(value); \ + if(!v.has_value()) { \ + return BOTAN_FFI_ERROR_BAD_PARAMETER; \ + } \ + b.add_##FIELD_NAME(v.value()); \ + return BOTAN_FFI_SUCCESS; \ + }); \ + } + #else + #define X509_CERT_BUILDER_ADD_TYPED(FIELD_NAME, TYPE_NAME) \ + int botan_x509_cert_builder_add_##FIELD_NAME(botan_x509_cert_builder_t builder, const char* value) { \ + if(Botan::any_null_pointers(value)) { \ + return BOTAN_FFI_ERROR_NULL_POINTER; \ + } \ + BOTAN_UNUSED(builder); \ + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; \ + } + #endif + } // namespace } // namespace Botan_FFI @@ -1033,6 +1095,304 @@ const char* botan_x509_cert_validation_status(int code) { #endif } +int botan_x509_cert_builder_destroy(botan_x509_cert_builder_t builder) { +#if defined(BOTAN_HAS_X509_CERTIFICATES) + return BOTAN_FFI_CHECKED_DELETE(builder); +#else + BOTAN_UNUSED(builder); + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; +#endif +} + +int botan_x509_cert_builder_create(botan_x509_cert_builder_t* builder_obj) { + if(Botan::any_null_pointers(builder_obj)) { + return BOTAN_FFI_ERROR_NULL_POINTER; + } +#if defined(BOTAN_HAS_X509_CERTIFICATES) + return ffi_guard_thunk(__func__, [=]() -> int { + auto b = std::make_unique(); + return ffi_new_object(builder_obj, std::move(b)); + }); +#else + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; +#endif +} + +X509_CERT_BUILDER_ADD_STRING(common_name) +X509_CERT_BUILDER_ADD_STRING(country) +X509_CERT_BUILDER_ADD_STRING(organization) +X509_CERT_BUILDER_ADD_STRING(organizational_unit) +X509_CERT_BUILDER_ADD_STRING(locality) +X509_CERT_BUILDER_ADD_STRING(state) +X509_CERT_BUILDER_ADD_STRING(serial_number) +X509_CERT_BUILDER_ADD_STRING(xmpp) + +X509_CERT_BUILDER_ADD_TYPED(email, Botan::EmailAddress) +X509_CERT_BUILDER_ADD_TYPED(uri, Botan::URI) +X509_CERT_BUILDER_ADD_TYPED(dns, Botan::DNSName) +X509_CERT_BUILDER_ADD_TYPED(ipv4, Botan::IPv4Address) +X509_CERT_BUILDER_ADD_TYPED(ipv6, Botan::IPv6Address) + +int botan_x509_cert_builder_add_allowed_usage(botan_x509_cert_builder_t builder, uint32_t usage) { +#if defined(BOTAN_HAS_X509_CERTIFICATES) + return BOTAN_FFI_VISIT(builder, [=](auto& b) { b.add_allowed_usage(Botan::Key_Constraints(usage)); }); +#else + BOTAN_UNUSED(builder, usage); + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; +#endif +} + +int botan_x509_cert_builder_add_allowed_extended_usage(botan_x509_cert_builder_t builder, botan_asn1_oid_t oid) { +#if defined(BOTAN_HAS_X509_CERTIFICATES) + return BOTAN_FFI_VISIT(builder, [=](auto& b) -> int { + b.add_allowed_extended_usage(safe_get(oid)); + return BOTAN_FFI_SUCCESS; + }); +#else + BOTAN_UNUSED(builder, oid); + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; +#endif +} + +int botan_x509_cert_builder_set_as_ca_certificate(botan_x509_cert_builder_t builder, size_t* limit) { +#if defined(BOTAN_HAS_X509_CERTIFICATES) + return BOTAN_FFI_VISIT(builder, [=](auto& b) { + std::optional lim; + if(limit != nullptr) { + lim = *limit; + } + b.set_as_ca_certificate(lim); + }); +#else + BOTAN_UNUSED(builder, limit); + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; +#endif +} + +int botan_x509_cert_builder_into_self_signed_cert(botan_x509_cert_t* cert_obj, + botan_x509_cert_builder_t builder, + botan_privkey_t key, + botan_rng_t rng, + uint64_t not_before, + uint64_t not_after, + const botan_mp_t* serial_number, + const char* hash_fn, + const char* padding) { + if(Botan::any_null_pointers(cert_obj)) { + return BOTAN_FFI_ERROR_NULL_POINTER; + } +#if defined(BOTAN_HAS_X509_CERTIFICATES) + return ffi_guard_thunk(__func__, [=]() -> int { + auto hash_fn_ = optional_from_ptr(hash_fn); + auto padding_ = optional_from_ptr(padding); + std::optional serial; + if(serial_number != nullptr) { + serial = safe_get(*serial_number); + } + + std::unique_ptr cert = std::make_unique( + safe_get(builder).into_self_signed_cert(timepoint_from_timestamp(not_before), + timepoint_from_timestamp(not_after), + safe_get(key), + safe_get(rng), + serial, + hash_fn_, + padding_)); + + return ffi_new_object(cert_obj, std::move(cert)); + }); +#else + BOTAN_UNUSED(builder, key, rng, not_before, not_after, hash_fn, padding); + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; +#endif +} + +int botan_x509_cert_builder_into_cert(botan_x509_cert_t* cert_obj, + botan_x509_cert_builder_t builder, + botan_x509_cert_t ca_cert, + botan_privkey_t ca_key, + botan_privkey_t key, + botan_rng_t rng, + uint64_t not_before, + uint64_t not_after, + const botan_mp_t* serial_number, + const char* hash_fn, + const char* padding) { + if(Botan::any_null_pointers(cert_obj)) { + return BOTAN_FFI_ERROR_NULL_POINTER; + } +#if defined(BOTAN_HAS_X509_CERTIFICATES) + return ffi_guard_thunk(__func__, [=]() -> int { + auto hash_fn_ = optional_from_ptr(hash_fn); + auto padding_ = optional_from_ptr(padding); + std::optional serial; + if(serial_number != nullptr) { + serial = safe_get(*serial_number); + } + + std::unique_ptr cert = + std::make_unique(safe_get(builder).into_cert(timepoint_from_timestamp(not_before), + timepoint_from_timestamp(not_after), + safe_get(ca_cert), + safe_get(ca_key), + safe_get(key), + safe_get(rng), + serial, + hash_fn_, + padding_)); + + return ffi_new_object(cert_obj, std::move(cert)); + }); +#else + BOTAN_UNUSED(builder, ca_cert, ca_key, key, rng, not_before, not_after, hash_fn, padding); + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; +#endif +} + +int botan_x509_cert_builder_into_pkcs10_req(botan_x509_pkcs10_req_t* req_obj, + botan_x509_cert_builder_t builder, + botan_privkey_t key, + botan_rng_t rng, + const char* hash_fn, + const char* padding, + const char* challenge_password) { + if(Botan::any_null_pointers(req_obj)) { + return BOTAN_FFI_ERROR_NULL_POINTER; + } +#if defined(BOTAN_HAS_X509_CERTIFICATES) + return ffi_guard_thunk(__func__, [=]() -> int { + auto req = std::make_unique( + safe_get(builder).into_pkcs10_request(safe_get(key), + safe_get(rng), + optional_from_ptr(hash_fn), + optional_from_ptr(padding), + optional_from_ptr(challenge_password))); + return ffi_new_object(req_obj, std::move(req)); + }); +#else + BOTAN_UNUSED(builder, key, rng, padding, hash_fn, challenge_password); + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; +#endif +} + +int botan_x509_pkcs10_req_destroy(botan_x509_pkcs10_req_t req) { +#if defined(BOTAN_HAS_X509_CERTIFICATES) + return BOTAN_FFI_CHECKED_DELETE(req); +#else + BOTAN_UNUSED(req); + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; +#endif +} + +int botan_x509_pkcs10_req_load_file(botan_x509_pkcs10_req_t* req_obj, const char* req_path) { + if(Botan::any_null_pointers(req_obj, req_path)) { + return BOTAN_FFI_ERROR_NULL_POINTER; + } +#if defined(BOTAN_HAS_X509_CERTIFICATES) && defined(BOTAN_TARGET_OS_HAS_FILESYSTEM) + return ffi_guard_thunk(__func__, [=]() -> int { + auto req = std::make_unique(req_path); + return ffi_new_object(req_obj, std::move(req)); + }); +#else + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; +#endif +} + +int botan_x509_pkcs10_req_load(botan_x509_pkcs10_req_t* req_obj, const uint8_t req_bits[], size_t req_bits_len) { + if(Botan::any_null_pointers(req_obj, req_bits)) { + return BOTAN_FFI_ERROR_NULL_POINTER; + } + +#if defined(BOTAN_HAS_X509_CERTIFICATES) + return ffi_guard_thunk(__func__, [=]() -> int { + Botan::DataSource_Memory bits(req_bits, req_bits_len); + auto req = std::make_unique(bits); + return ffi_new_object(req_obj, std::move(req)); + }); +#else + BOTAN_UNUSED(req_bits_len); + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; +#endif +} + +int botan_x509_pkcs10_req_view_pem(botan_x509_pkcs10_req_t req, botan_view_ctx ctx, botan_view_str_fn view) { +#if defined(BOTAN_HAS_X509_CERTIFICATES) + return BOTAN_FFI_VISIT(req, [=](const auto& r) -> int { return invoke_view_callback(view, ctx, r.PEM_encode()); }); +#else + BOTAN_UNUSED(req, ctx, view); + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; +#endif +} + +int botan_x509_pkcs10_req_view_der(botan_x509_pkcs10_req_t req, botan_view_ctx ctx, botan_view_bin_fn view) { +#if defined(BOTAN_HAS_X509_CERTIFICATES) + return BOTAN_FFI_VISIT(req, [=](const auto& r) -> int { return invoke_view_callback(view, ctx, r.BER_encode()); }); +#else + BOTAN_UNUSED(req, ctx, view); + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; +#endif +} + +int botan_x509_pkcs10_req_get_public_key(botan_x509_pkcs10_req_t req, botan_pubkey_t* key) { + if(Botan::any_null_pointers(key)) { + return BOTAN_FFI_ERROR_NULL_POINTER; + } +#if defined(BOTAN_HAS_X509_CERTIFICATES) + return BOTAN_FFI_VISIT(req, + [=](const auto& r) -> int { return ffi_new_object(key, std::move(r.subject_public_key())); }); +#else + BOTAN_UNUSED(req); + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; +#endif +} + +int botan_x509_pkcs10_req_verify_signature(botan_x509_pkcs10_req_t req, botan_pubkey_t key) { +#if defined(BOTAN_HAS_X509_CERTIFICATES) + return BOTAN_FFI_VISIT(req, [=](const auto& r) -> int { return r.check_signature(safe_get(key)) ? 1 : 0; }); +#else + BOTAN_UNUSED(req, key); + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; +#endif +} + +int botan_x509_pkcs10_req_sign(botan_x509_cert_t* subject_cert, + botan_x509_pkcs10_req_t subject_req, + botan_x509_cert_t issuing_cert, + botan_privkey_t issuing_key, + botan_rng_t rng, + uint64_t not_before, + uint64_t not_after, + const botan_mp_t* serial_number, + const char* hash_fn, + const char* padding) { + if(Botan::any_null_pointers(subject_cert)) { + return BOTAN_FFI_ERROR_NULL_POINTER; + } +#if defined(BOTAN_HAS_X509_CERTIFICATES) + return ffi_guard_thunk(__func__, [=]() -> int { + auto& rng_ = safe_get(rng); + + auto ca = Botan::X509_CA( + safe_get(issuing_cert), safe_get(issuing_key), default_from_ptr(hash_fn), default_from_ptr(padding), rng_); + + std::unique_ptr cert; + if(serial_number != nullptr) { + auto serial = safe_get(*serial_number); + cert = std::make_unique(ca.sign_request( + safe_get(subject_req), rng_, serial, time_from_timestamp(not_before), time_from_timestamp(not_after))); + } else { + cert = std::make_unique(ca.sign_request( + safe_get(subject_req), rng_, time_from_timestamp(not_before), time_from_timestamp(not_after))); + } + + return ffi_new_object(subject_cert, std::move(cert)); + }); +#else + BOTAN_UNUSED(subject_req, issuing_cert, issuing_key, rng, not_before, not_after, serial_number, hash_fn, padding); + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; +#endif +} + int botan_x509_crl_load_file(botan_x509_crl_t* crl_obj, const char* crl_path) { if(Botan::any_null_pointers(crl_obj, crl_path)) { return BOTAN_FFI_ERROR_NULL_POINTER; diff --git a/src/lib/ffi/ffi_cert.h b/src/lib/ffi/ffi_cert.h index 27bb90e9686..5b62e97a6ee 100644 --- a/src/lib/ffi/ffi_cert.h +++ b/src/lib/ffi/ffi_cert.h @@ -11,6 +11,8 @@ #if defined(BOTAN_HAS_X509_CERTIFICATES) #include + #include + #include #include #include #include @@ -24,6 +26,8 @@ BOTAN_FFI_DECLARE_STRUCT(botan_x509_cert_struct, Botan::X509_Certificate, 0x8F62 BOTAN_FFI_DECLARE_STRUCT(botan_x509_crl_struct, Botan::X509_CRL, 0x2C628910); BOTAN_FFI_DECLARE_STRUCT(botan_x509_crl_entry_struct, Botan::CRL_Entry, 0x4EAA5346); BOTAN_FFI_DECLARE_STRUCT(botan_x509_general_name_struct, Botan::GeneralName, 0x563654FD); +BOTAN_FFI_DECLARE_STRUCT(botan_x509_cert_builder_struct, Botan::CertificateParametersBuilder, 0x8A79DAB7); +BOTAN_FFI_DECLARE_STRUCT(botan_x509_pkcs10_req_struct, Botan::PKCS10_Request, 0x94C112B6); #endif } diff --git a/src/python/botan3.py b/src/python/botan3.py index 47432105178..9a05bb11fc3 100755 --- a/src/python/botan3.py +++ b/src/python/botan3.py @@ -6,7 +6,7 @@ (C) 2015 Uri Blumenthal (extensions and patches) (C) 2024 Amos Treiber - Rohde & Schwarz Cybersecurity (C) 2024,2026 René Meusel - Rohde & Schwarz Cybersecurity -(C) 2025 Dominik Schricker +(C) 2025,2026 Dominik Schricker Botan is released under the Simplified BSD License (see license.txt) @@ -556,6 +556,42 @@ def ffi_api(fn, args, allowed_errors=None): dll.botan_x509_cert_validation_status.argtypes = [c_int] dll.botan_x509_cert_validation_status.restype = c_char_p + # X509 Cert Builder + ffi_api(dll.botan_x509_cert_builder_destroy, [c_void_p]) + ffi_api(dll.botan_x509_cert_builder_create, [c_void_p]) + ffi_api(dll.botan_x509_cert_builder_add_common_name, [c_void_p, c_char_p]) + ffi_api(dll.botan_x509_cert_builder_add_country, [c_void_p, c_char_p]) + ffi_api(dll.botan_x509_cert_builder_add_organization, [c_void_p, c_char_p]) + ffi_api(dll.botan_x509_cert_builder_add_organizational_unit, [c_void_p, c_char_p]) + ffi_api(dll.botan_x509_cert_builder_add_locality, [c_void_p, c_char_p]) + ffi_api(dll.botan_x509_cert_builder_add_state, [c_void_p, c_char_p]) + ffi_api(dll.botan_x509_cert_builder_add_serial_number, [c_void_p, c_char_p]) + ffi_api(dll.botan_x509_cert_builder_add_xmpp, [c_void_p, c_char_p]) + ffi_api(dll.botan_x509_cert_builder_add_email, [c_void_p, c_char_p]) + ffi_api(dll.botan_x509_cert_builder_add_uri, [c_void_p, c_char_p]) + ffi_api(dll.botan_x509_cert_builder_add_dns, [c_void_p, c_char_p]) + ffi_api(dll.botan_x509_cert_builder_add_ipv4, [c_void_p, c_char_p]) + ffi_api(dll.botan_x509_cert_builder_add_ipv6, [c_void_p, c_char_p]) + ffi_api(dll.botan_x509_cert_builder_add_allowed_usage, [c_void_p, c_uint32]) + ffi_api(dll.botan_x509_cert_builder_add_allowed_extended_usage, [c_void_p, c_void_p]) + ffi_api(dll.botan_x509_cert_builder_set_as_ca_certificate, [c_void_p, POINTER(c_size_t)]) + ffi_api(dll.botan_x509_cert_builder_into_self_signed_cert, + [c_void_p, c_void_p, c_void_p, c_void_p, c_uint64, c_uint64, c_void_p, c_char_p, c_char_p]) + ffi_api(dll.botan_x509_cert_builder_into_cert, + [c_void_p, c_void_p, c_void_p, c_void_p, c_void_p, c_void_p, c_uint64, c_uint64, c_void_p, c_char_p, c_char_p]) + ffi_api(dll.botan_x509_cert_builder_into_pkcs10_req, + [c_void_p, c_void_p, c_void_p, c_void_p, c_char_p, c_char_p, c_char_p]) + + ffi_api(dll.botan_x509_pkcs10_req_destroy, [c_void_p]) + ffi_api(dll.botan_x509_pkcs10_req_load_file, [c_void_p, c_char_p]) + ffi_api(dll.botan_x509_pkcs10_req_load, [c_void_p, c_char_p, c_size_t]) + ffi_api(dll.botan_x509_pkcs10_req_view_pem, [c_void_p, c_void_p, VIEW_STR_CALLBACK]) + ffi_api(dll.botan_x509_pkcs10_req_view_der, [c_void_p, c_void_p, VIEW_BIN_CALLBACK]) + ffi_api(dll.botan_x509_pkcs10_req_get_public_key, [c_void_p, c_void_p]) + ffi_api(dll.botan_x509_pkcs10_req_verify_signature, [c_void_p, c_void_p]) + ffi_api(dll.botan_x509_pkcs10_req_sign, + [c_void_p, c_void_p, c_void_p, c_void_p, c_void_p, c_uint64, c_uint64, c_void_p, c_char_p, c_char_p]) + # X509 CRL ffi_api(dll.botan_x509_crl_load, [c_void_p, c_char_p, c_size_t]) ffi_api(dll.botan_x509_crl_load_file, [c_void_p, c_char_p]) @@ -2257,10 +2293,68 @@ def _load_buf_or_file(filename, buf, file_fn, buf_fn): # # X.509 certificates # +class X509KeyConstraints(IntEnum): + NO_CONSTRAINTS = 0 + DIGITAL_SIGNATURE = 1 << 15 + NON_REPUDIATION = 1 << 14 + KEY_ENCIPHERMENT = 1 << 13 + DATA_ENCIPHERMENT = 1 << 12 + KEY_AGREEMENT = 1 << 11 + KEY_CERT_SIGN = 1 << 10 + CRL_SIGN = 1 << 9 + ENCIPHER_ONLY = 1 << 8 + DECIPHER_ONLY = 1 << 7 + + @classmethod + def to_bits(cls, constraints: list[X509KeyConstraints]) -> int: + con = 0 + for constraint in constraints: + con |= constraint.value + return con + + @classmethod + def from_bits(cls, bits: int) -> list[X509KeyConstraints]: + if bits == 0: + return X509KeyConstraints.NO_CONSTRAINTS + all_constraints = [ + X509KeyConstraints.DIGITAL_SIGNATURE, + X509KeyConstraints.NON_REPUDIATION, + X509KeyConstraints.KEY_ENCIPHERMENT, + X509KeyConstraints.DATA_ENCIPHERMENT, + X509KeyConstraints.KEY_AGREEMENT, + X509KeyConstraints.KEY_CERT_SIGN, + X509KeyConstraints.CRL_SIGN, + X509KeyConstraints.ENCIPHER_ONLY, + X509KeyConstraints.DECIPHER_ONLY, + ] + + constraints = [] + for constraint in all_constraints: + if bits & constraint.value != 0: + constraints.append(constraint) + return constraints + + # TODO deprecate this in a future version + @classmethod + def from_string(cls, constraint: str) -> X509KeyConstraints: + try: + if isinstance(constraint, X509KeyConstraints): + return constraint + return cls[constraint] + except KeyError as exc: + raise BotanException("Not a valid key constraint") from exc + + @classmethod + def to_string(cls, constraint: X509KeyConstraints) -> str: + return constraint.name + + class X509Cert: # pylint: disable=invalid-name def __init__(self, filename: str | None = None, buf: bytes | None = None): - self.__obj = c_void_p(0) - self.__obj = _load_buf_or_file(filename, buf, _DLL.botan_x509_cert_load_file, _DLL.botan_x509_cert_load) + if not filename and not buf: + self.__obj = c_void_p(0) + else: + self.__obj = _load_buf_or_file(filename, buf, _DLL.botan_x509_cert_load_file, _DLL.botan_x509_cert_load) def __del__(self): _DLL.botan_x509_cert_destroy(self.__obj) @@ -2373,22 +2467,7 @@ def allowed_usage(self, usage_list: list[str]) -> bool: """Return True if the certificates Key Usage extension contains all constraints given in ``usage_list``. Also return True if the certificate doesn't have this extension. Example usage constraints are: ``"DIGITAL_SIGNATURE"``, ``"KEY_CERT_SIGN"``, ``"CRL_SIGN"``.""" - usage_values = {"NO_CONSTRAINTS": 0, - "DIGITAL_SIGNATURE": 32768, - "NON_REPUDIATION": 16384, - "KEY_ENCIPHERMENT": 8192, - "DATA_ENCIPHERMENT": 4096, - "KEY_AGREEMENT": 2048, - "KEY_CERT_SIGN": 1024, - "CRL_SIGN": 512, - "ENCIPHER_ONLY": 256, - "DECIPHER_ONLY": 128} - usage = 0 - for u in usage_list: - if u not in usage_values: - return False - usage += usage_values[u] - + usage = X509KeyConstraints.to_bits([X509KeyConstraints.from_string(x) for x in usage_list]) rc = _DLL.botan_x509_cert_allowed_usage(self.__obj, c_uint(usage)) return rc == 0 @@ -2483,6 +2562,225 @@ def is_revoked(self, crl: X509CRL) -> bool: return rc == 0 +class X509CertificateBuilder: + def __init__(self): + self.__obj = c_void_p(0) + _DLL.botan_x509_cert_builder_create(byref(self.__obj)) + + def __del__(self): + _DLL.botan_x509_cert_builder_destroy(self.__obj) + + def handle_(self): + return self.__obj + + def add_common_name(self, name: str): + """Add an additional common name to the certificate metadata""" + _DLL.botan_x509_cert_builder_add_common_name(self.__obj, _ctype_str(name)) + + def add_country(self, country: str): + """Add an additional country name to the certificate metadata""" + _DLL.botan_x509_cert_builder_add_country(self.__obj, _ctype_str(country)) + + def add_organization(self, organization: str): + """Add an additional organization name to the certificate metadata""" + _DLL.botan_x509_cert_builder_add_organization(self.__obj, _ctype_str(organization)) + + def add_organizational_unit(self, org_unit: str): + """Add an additional organization unit name to the certificate metadata""" + _DLL.botan_x509_cert_builder_add_organizational_unit(self.__obj, _ctype_str(org_unit)) + + def add_locality(self, locality: str): + """Add an additional locality name to the certificate metadata""" + _DLL.botan_x509_cert_builder_add_locality(self.__obj, _ctype_str(locality)) + + def add_state(self, state: str): + """Add an additional state name to the certificate metadata""" + _DLL.botan_x509_cert_builder_add_state(self.__obj, _ctype_str(state)) + + def add_serial_number(self, serial_number: str): + """Add an additional serial number to the certificate metadata + + Note this is the X.520 serial number included in the DN, and has nothing + to do with the serial number of the issued certificate.""" + _DLL.botan_x509_cert_builder_add_serial_number(self.__obj, _ctype_str(serial_number)) + + def add_email(self, email: str): + """Add an additional email address to the certificate metadata""" + _DLL.botan_x509_cert_builder_add_email(self.__obj, _ctype_str(email)) + + def add_uri(self, uri: str): + """Add an additional URI to the certificate metadata""" + _DLL.botan_x509_cert_builder_add_uri(self.__obj, _ctype_str(uri)) + + def add_dns(self, dns: str): + """Add an additional DNS name to the certificate metadata""" + _DLL.botan_x509_cert_builder_add_dns(self.__obj, _ctype_str(dns)) + + def add_ipv4(self, ipv4: str): + """Add an additional IPv4 address to the certificate metadata""" + _DLL.botan_x509_cert_builder_add_ipv4(self.__obj, ipv4) + + def add_ipv6(self, ipv6: str): + """Add an additional IPv6 address to the certificate metadata""" + _DLL.botan_x509_cert_builder_add_ipv6(self.__obj, ipv6) + + def add_xmpp(self, xmpp: str): + """Add a XMPP name to the certificate metadata""" + _DLL.botan_x509_cert_builder_add_xmpp(self.__obj, _ctype_str(xmpp)) + + def add_allowed_usage(self, usage_list: list[X509KeyConstraints]): + """Add another allowed usage to the KeyUsage extension + + Repeated invocations are cumulative""" + usage = X509KeyConstraints.to_bits(usage_list) + _DLL.botan_x509_cert_builder_add_allowed_usage(self.__obj, c_uint32(usage)) + + def add_allowed_extended_usage(self, oid: OID): + """Add another allowed usage to the ExtendedKeyUsage extension""" + _DLL.botan_x509_cert_builder_add_allowed_extended_usage(self.__obj, oid.handle_()) + + def set_as_ca_certificate(self, limit: int | None = None): + """Set the parameters as being for a CA certificate + + May be called at most once.""" + _DLL.botan_x509_cert_builder_set_as_ca_certificate(self.__obj, c_size_t(limit) if limit is not None else None) + + def into_self_signed_cert( + self, + key: PrivateKey, + rng: RandomNumberGenerator, + not_before: int, + not_after: int, + serial_number: MPI | None = None, + hash_fn: str | None = None, + padding: str | None = None + ) -> X509Cert: + """Create a self-signed X.509 certificate""" + cert = X509Cert() + serial = byref(serial_number.handle_()) if serial_number is not None else None + _DLL.botan_x509_cert_builder_into_self_signed_cert( + byref(cert.handle_()), + self.__obj, + key.handle_(), + rng.handle_(), + not_before, + not_after, + serial, + _ctype_str(hash_fn), + _ctype_str(padding), + ) + return cert + + def into_cert( + self, + ca_cert: X509Cert, + ca_key: PrivateKey, + key: PrivateKey, + rng: RandomNumberGenerator, + not_before: int, + not_after: int, + serial_number: MPI | None = None, + hash_fn: str | None = None, + padding: str | None = None + ) -> X509Cert: + """Create a X.509 certificate signed by a certificate authority""" + cert = X509Cert() + serial = byref(serial_number.handle_()) if serial_number is not None else None + _DLL.botan_x509_cert_builder_into_cert( + byref(cert.handle_()), + self.__obj, + ca_cert.handle_(), + ca_key.handle_(), + key.handle_(), + rng.handle_(), + not_before, + not_after, + serial, + _ctype_str(hash_fn), + _ctype_str(padding), + ) + return cert + + def into_request( + self, + key: PrivateKey, + rng: RandomNumberGenerator, + hash_fn: str | None = None, + padding: str | None = None, + challenge_password: str | None = None + ) -> PKCS10Request: + """Create a PKCS10 request""" + req = PKCS10Request() + _DLL.botan_x509_cert_builder_into_pkcs10_req( + byref(req.handle_()), + self.__obj, + key.handle_(), + rng.handle_(), + _ctype_str(hash_fn), + _ctype_str(padding), + _ctype_str(challenge_password) + ) + return req + + +class PKCS10Request: + def __init__(self, filename: str | None = None, buf: bytes | None = None): + if not filename and not buf: + self.__obj = c_void_p(0) + else: + self.__obj = _load_buf_or_file(filename, buf, _DLL.botan_x509_pkcs10_req_load_file, _DLL.botan_x509_pkcs10_req_load) + + def __del__(self): + _DLL.botan_x509_pkcs10_req_destroy(self.__obj) + + def handle_(self): + return self.__obj + + def public_key(self) -> PublicKey: + """Get the public key associated with this request""" + pub = c_void_p(0) + _DLL.botan_x509_pkcs10_req_get_public_key(self.__obj, byref(pub)) + return PublicKey(pub) + + def verify(self, key: PublicKey) -> bool: + rc = _DLL.botan_x509_pkcs10_req_verify_signature(self.__obj, key.handle_()) + return rc == 1 + + def sign( + self, + issuing_cert: X509Cert, + issuing_key: PrivateKey, + rng: RandomNumberGenerator, + not_before: int, + not_after: int, + serial_number: MPI | None = None, + hash_fn: str | None = None, + padding: str | None = None + ) -> X509Cert: + """Sign this PKCS#10 request""" + cert = X509Cert() + serial_no = byref(serial_number.handle_()) if serial_number is not None else None + _DLL.botan_x509_pkcs10_req_sign( + byref(cert.handle_()), + self.__obj, + issuing_cert.handle_(), + issuing_key.handle_(), + rng.handle_(), + not_before, + not_after, + serial_no, + _ctype_str(hash_fn), + _ctype_str(padding) + ) + return cert + + def to_pem(self) -> str: + return _call_fn_viewing_str(lambda vc, vfn: _DLL.botan_x509_pkcs10_req_view_pem(self.__obj, vc, vfn)) + + def to_der(self) -> bytes: + return _call_fn_viewing_vec(lambda vc, vfn: _DLL.botan_x509_pkcs10_req_view_der(self.__obj, vc, vfn)) + + # # X.509 Certificate revocation lists # diff --git a/src/scripts/test_python.py b/src/scripts/test_python.py index 7e85cdb087b..c744c9d2df3 100644 --- a/src/scripts/test_python.py +++ b/src/scripts/test_python.py @@ -1035,6 +1035,38 @@ def test_certs(self): self.assertFalse(int04_1.is_revoked(rootcrl)) self.assertTrue(end21.is_revoked(int21crl)) + def test_cert_creation(self): + group = "secp256r1" + now = int(time.time()) + not_before = now - 180 + not_after = now + 86400 + + rng = botan.RandomNumberGenerator() + ca_key = botan.PrivateKey.create("ECDSA", group, rng) + ca_builder = botan.X509CertificateBuilder() + ca_builder.add_common_name("Test CA") + ca_builder.add_country("US") + ca_builder.add_organization("Botan Project") + ca_builder.add_organizational_unit("Testing") + ca_builder.set_as_ca_certificate(1) + ca_cert = ca_builder.into_self_signed_cert(ca_key, rng, not_before, not_after) + + cert_key = botan.PrivateKey.create("ECDSA", group, rng) + req_builder = botan.X509CertificateBuilder() + req_builder.add_allowed_usage([botan.X509KeyConstraints.DIGITAL_SIGNATURE]) + req_builder.add_uri("https://botan.randombit.net") + for item in ["imaginary.botan.randombit.net", "botan.randombit.net", "randombit.net"]: + req_builder.add_dns(item) + req = req_builder.into_request(cert_key, rng) + self.assertTrue(req.verify(req.public_key())) + self.assertTrue(req.verify(cert_key.get_public_key())) + + cert = req.sign(ca_cert, ca_key, rng, not_before, not_after) + self.assertEqual(cert.verify(None, [ca_cert]), 0) + + cert = req_builder.into_cert(ca_cert, ca_key, cert_key, rng, not_before, not_after, botan.MPI("1")) + self.assertEqual(cert.verify(None, [ca_cert]), 0) + def test_crls(self): rng = botan.RandomNumberGenerator() now = int(time.time())