Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions src/cli/perf_x509.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@
#include <botan/ber_dec.h>
#include <botan/bigint.h>
#include <botan/der_enc.h>
#include <botan/dns_name.h>
#include <botan/email.h>
#include <botan/pk_algs.h>
#include <botan/pk_keys.h>
#include <botan/rng.h>
#include <botan/x509_builder.h>
#include <botan/x509_ca.h>
#include <botan/x509_ext.h>
#include <botan/x509self.h>
#endif

namespace Botan_CLI {
Expand All @@ -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{
Expand Down
2 changes: 1 addition & 1 deletion src/lib/utils/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
* <dt>X.509<dd>
* 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
* <dt>eXtendable Output Functions<dd>
* @ref Ascon_XOF128 "Ascon-XOF128", @ref SHAKE_XOF "SHAKE"
Expand Down
1 change: 1 addition & 0 deletions src/lib/x509/info.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/lib/x509/pkix_enums.h
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,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; }

Expand Down
304 changes: 304 additions & 0 deletions src/lib/x509/x509_builder.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
/*
* (C) 2025 Jack Lloyd
*
* Botan is released under the Simplified BSD License (see license.txt)
*/

#include <botan/x509_builder.h>

#include <botan/assert.h>
#include <botan/dns_name.h>
#include <botan/email.h>
#include <botan/ipv4_address.h>
#include <botan/ipv6_address.h>
#include <botan/pubkey.h>
#include <botan/uri.h>
#include <botan/x509_ca.h>
#include <botan/x509_ext.h>
#include <botan/internal/fmt.h>

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));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if I manually construct Cert_Extension::Subject_Alternative_Name and add it as critical, doesn't this always make it non-critical?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same should be true for Extended_Key_Usage below, though I don't know if that is ever "critical" in real-world use


extensions.add_new(
std::make_unique<Cert_Extension::Basic_Constraints>(m_is_ca_request, m_path_limit.value_or(0)), true);
Comment thread
randombit marked this conversation as resolved.

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<Cert_Extension::Key_Usage>(usage), true);
}

if(!m_ext_usage.empty()) {
extensions.add_new(std::make_unique<Cert_Extension::Extended_Key_Usage>(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<Certificate_Extension> 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<size_t> 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<Certificate_Extension> 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
Comment thread
randombit marked this conversation as resolved.
*/
if(const auto* ext = extensions.get_extension_object_as<Cert_Extension::Subject_Alternative_Name>()) {
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<Cert_Extension::Subject_Alternative_Name>(subject_alt);
}

X509_DN m_subject_dn;
Extensions m_extensions;
Key_Constraints m_usage;
std::vector<EmailAddress> m_email;
std::vector<DNSName> m_dns;
std::vector<URI> m_uri;
std::vector<std::string> m_xmpp;
std::vector<IPv4Address> m_ipv4;
std::vector<IPv6Address> m_ipv6;
std::vector<OID> m_ext_usage;
bool m_is_ca_request = false;
std::optional<size_t> m_path_limit;
};

CertificateParametersBuilder::CertificateParametersBuilder() :
m_state(std::make_unique<CertificateParametersBuilder::State>()) {}

CertificateParametersBuilder::CertificateParametersBuilder(CertificateParametersBuilder&& other) noexcept = default;

CertificateParametersBuilder::~CertificateParametersBuilder() = default;

X509_Certificate CertificateParametersBuilder::into_self_signed_cert(std::chrono::system_clock::time_point not_before,

This comment was marked as resolved.

std::chrono::system_clock::time_point not_after,
Comment on lines +178 to +179

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is slightly annoying, X509_CA::sign_request takes an X509_Time, this takes a time point. Would be nice to use only one

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add a second version of sign_request taking time points. It's X509_Time there mostly because that API predates adoption of C++11 and std::chrono.... but it's generally more useful for an application to specify time as a chrono type vs our ASN.1 type

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fine to leave it as is, it just means one 3 line conversion function for the ffi.
I already have one anyway for uint64_t -> X509_Time, so adding one for uint64_t -> time_point -> X509_Time isn't a big deal

const Private_Key& key,
RandomNumberGenerator& rng,
std::optional<std::string_view> hash_fn,
std::optional<std::string_view> 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<uint8_t> pub_key = key.subject_public_key();
Comment thread
randombit marked this conversation as resolved.
auto skid = std::make_unique<Cert_Extension::Subject_Key_ID>(pub_key, signer.hash_function());

extensions.add_new(std::make_unique<Cert_Extension::Authority_Key_ID>(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<std::string_view> hash_fn,
std::optional<std::string_view> padding,
std::optional<std::string_view> 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<Certificate_Extension> extn,
bool is_critical) {
m_state->add_extension(std::move(extn), is_critical);
return (*this);
}

CertificateParametersBuilder& CertificateParametersBuilder::set_as_ca_certificate(std::optional<size_t> path_limit) {
m_state->set_as_ca_certificate(path_limit);
return (*this);
}

} // namespace Botan
Loading
Loading