Skip to content

Commit 7665398

Browse files
committed
Add CertificateParametersBuilder as a replacement for X509_Cert_Options
1 parent a72cc07 commit 7665398

File tree

10 files changed

+703
-318
lines changed

10 files changed

+703
-318
lines changed

src/cli/perf_x509.cpp

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
#include <botan/bigint.h>
1717
#include <botan/der_enc.h>
1818
#include <botan/pk_algs.h>
19+
#include <botan/x509_builder.h>
1920
#include <botan/x509_ca.h>
2021
#include <botan/x509_ext.h>
21-
#include <botan/x509self.h>
2222
#endif
2323

2424
namespace Botan_CLI {
@@ -40,14 +40,23 @@ class PerfTest_ASN1_Parsing final : public PerfTest {
4040
}
4141

4242
static CA create_ca(Botan::RandomNumberGenerator& rng) {
43-
auto root_cert_options = Botan::X509_Cert_Options("Benchmark Root/DE/RS/CS");
44-
root_cert_options.dns = "unobtainium.example.com";
45-
root_cert_options.email = "[email protected]";
46-
root_cert_options.is_CA = true;
47-
4843
auto root_key = create_private_key(rng);
4944
BOTAN_ASSERT_NONNULL(root_key);
50-
auto root_cert = Botan::X509::create_self_signed_cert(root_cert_options, *root_key, get_hash_function(), rng);
45+
46+
Botan::CertificateParametersBuilder root_cert_params;
47+
root_cert_params.add_common_name("Benchmark Root")
48+
.add_country("DE")
49+
.add_organization("RS")
50+
.add_organizational_unit("CS")
51+
.add_dns("unobtainium.example.com")
52+
.add_email("[email protected]")
53+
.set_as_ca_certificate();
54+
55+
const auto not_before = std::chrono::system_clock::now();
56+
const auto not_after = not_before + std::chrono::seconds(86400);
57+
58+
auto root_cert =
59+
root_cert_params.into_self_signed_cert(not_before, not_after, *root_key, rng, get_hash_function());
5160
auto ca = Botan::X509_CA(root_cert, *root_key, get_hash_function(), rng);
5261

5362
return CA{

src/lib/utils/types.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ namespace Botan {
8686
* TLS::Client, TLS::Server, TLS::Policy, TLS::Protocol_Version, TLS::Callbacks, TLS::Ciphersuite,
8787
* TLS::Session, TLS::Session_Summary, TLS::Session_Manager, Credentials_Manager
8888
* <dt>X.509<dd>
89-
* X509_Certificate, X509_CRL, X509_CA, Certificate_Extension, PKCS10_Request, X509_Cert_Options,
89+
* X509_Certificate, X509_CRL, X509_CA, Certificate_Extension, PKCS10_Request, CertificateParametersBuilder,
9090
* Certificate_Store, Certificate_Store_In_SQL, Certificate_Store_In_SQLite
9191
* <dt>eXtendable Output Functions<dd>
9292
* @ref Ascon_XOF128 "Ascon-XOF128", @ref SHAKE_XOF "SHAKE"

src/lib/x509/info.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<defines>
22
X509_CERTIFICATES -> 20201106
3-
X509 -> 20201106
3+
X509 -> 20250716
44
OCSP -> 20201106
55
</defines>
66

@@ -22,6 +22,7 @@ ocsp.h
2222
pkcs10.h
2323
pkix_enums.h
2424
pkix_types.h
25+
x509_builder.h
2526
x509_ca.h
2627
x509_crl.h
2728
x509_ext.h

src/lib/x509/pkix_enums.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@ class BOTAN_PUBLIC_API(3, 0) Key_Constraints final {
167167

168168
void operator|=(Key_Constraints::Bits other) { m_value |= other; }
169169

170+
void operator|=(Key_Constraints other) { m_value |= other.m_value; }
171+
170172
// Return true if all bits in mask are set
171173
bool includes(Key_Constraints::Bits other) const { return (m_value & other) == other; }
172174

src/lib/x509/x509_builder.cpp

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
/*
2+
* (C) 2025 Jack Lloyd
3+
*
4+
* Botan is released under the Simplified BSD License (see license.txt)
5+
*/
6+
7+
#include <botan/x509_builder.h>
8+
9+
#include <botan/assert.h>
10+
#include <botan/pubkey.h>
11+
#include <botan/x509_ca.h>
12+
#include <botan/x509_ext.h>
13+
#include <botan/internal/fmt.h>
14+
15+
namespace Botan {
16+
17+
class CertificateParametersBuilder::State final {
18+
public:
19+
const X509_DN& subject_dn() const { return m_subject_dn; }
20+
21+
Extensions finalize_extensions(const Public_Key& key) const {
22+
auto extensions = m_extensions;
23+
24+
extensions.replace(setup_alt_name(extensions));
25+
26+
extensions.add_new(
27+
std::make_unique<Cert_Extension::Basic_Constraints>(m_is_ca_request, m_path_limit.value_or(0)), true);
28+
29+
const Key_Constraints usage = this->usage();
30+
31+
if(!usage.empty()) {
32+
if(!usage.compatible_with(key)) {
33+
throw Invalid_Argument("The requested key usage is incompatible with the algorithm");
34+
}
35+
36+
extensions.add_new(std::make_unique<Cert_Extension::Key_Usage>(usage), true);
37+
}
38+
39+
if(!m_ext_usage.empty()) {
40+
extensions.add_new(std::make_unique<Cert_Extension::Extended_Key_Usage>(m_ext_usage));
41+
}
42+
43+
return extensions;
44+
}
45+
46+
Key_Constraints usage() const {
47+
if(m_is_ca_request) {
48+
return Key_Constraints::ca_constraints();
49+
} else {
50+
return m_usage;
51+
}
52+
}
53+
54+
void add_common_name(std::string_view cn) { add_subject_dn("common_name", "X520.CommonName", cn); }
55+
56+
void add_country(std::string_view country) { add_subject_dn("country", "X520.Country", country); }
57+
58+
void add_state(std::string_view state) { add_subject_dn("state", "X520.State", state); }
59+
60+
void add_locality(std::string_view locality) { add_subject_dn("locality", "X520.Locality", locality); }
61+
62+
void add_serial_number(std::string_view sn) { add_subject_dn("serial_number", "X520.SerialNumber", sn); }
63+
64+
void add_organization(std::string_view org) { add_subject_dn("organization", "X520.Organization", org); }
65+
66+
void add_organizational_unit(std::string_view org_unit) {
67+
add_subject_dn("organizational_unit", "X520.OrganizationalUnit", org_unit);
68+
}
69+
70+
void add_extension(std::unique_ptr<Certificate_Extension> extn, bool is_critical) {
71+
if(!m_extensions.add_new(std::move(extn), is_critical)) {
72+
throw Invalid_Argument("CertificateParametersBuilder::add_extension: cannot add same extension twice");
73+
}
74+
}
75+
76+
void add_email(std::string_view email) { m_email.emplace_back(email); }
77+
78+
void add_dns(std::string_view dns) { m_dns.emplace_back(dns); }
79+
80+
void add_uri(std::string_view uri) { m_uri.emplace_back(uri); }
81+
82+
void add_xmpp(std::string_view xmpp) { m_xmpp.emplace_back(xmpp); }
83+
84+
void add_ipv4(uint32_t ipv4) { m_ipv4.push_back(ipv4); }
85+
86+
void add_allowed_usage(Key_Constraints usage) { m_usage |= usage; }
87+
88+
void add_allowed_extended_usage(const OID& usage) { m_ext_usage.push_back(usage); }
89+
90+
void set_as_ca_certificate(std::optional<size_t> path_limit) {
91+
if(m_is_ca_request) {
92+
throw Invalid_State("CertificateParametersBuilder::set_as_ca_certificate cannot be called twice");
93+
} else {
94+
m_is_ca_request = true;
95+
m_path_limit = path_limit;
96+
}
97+
}
98+
99+
private:
100+
void add_subject_dn(std::string_view fn_suffix, std::string_view attr, std::string_view value) {
101+
const auto oid = OID::from_string(attr);
102+
const size_t ub = X509_DN::lookup_ub(oid);
103+
104+
if(value.empty()) {
105+
throw Invalid_Argument(fmt("CertificateParametersBuilder::add_{}: empty name is prohibited", fn_suffix));
106+
}
107+
108+
if(value.size() > ub) {
109+
throw Invalid_Argument(
110+
fmt("CertificateParametersBuilder::add_{}: name exceeds maximum allowed length ({}) for this type",
111+
fn_suffix,
112+
ub));
113+
}
114+
m_subject_dn.add_attribute(oid, value);
115+
}
116+
117+
std::unique_ptr<Certificate_Extension> setup_alt_name(const Extensions& extensions) const {
118+
AlternativeName subject_alt;
119+
120+
/*
121+
If the extension was already created in extensions we need to merge the
122+
values provided with the extension value
123+
*/
124+
if(const auto* ext = extensions.get_extension_object_as<Cert_Extension::Subject_Alternative_Name>()) {
125+
subject_alt = ext->get_alt_name();
126+
}
127+
128+
for(const auto& dns : m_dns) {
129+
subject_alt.add_dns(dns);
130+
}
131+
for(const auto& uri : m_uri) {
132+
subject_alt.add_uri(uri);
133+
}
134+
for(const auto& email : m_email) {
135+
subject_alt.add_email(email);
136+
}
137+
for(const auto& xmpp : m_xmpp) {
138+
subject_alt.add_other_name(OID::from_string("PKIX.XMPPAddr"), ASN1_String(xmpp, ASN1_Type::Utf8String));
139+
}
140+
for(const uint32_t ipv4 : m_ipv4) {
141+
subject_alt.add_ipv4_address(ipv4);
142+
}
143+
144+
return std::make_unique<Cert_Extension::Subject_Alternative_Name>(subject_alt);
145+
}
146+
147+
X509_DN m_subject_dn;
148+
Extensions m_extensions;
149+
Key_Constraints m_usage;
150+
std::vector<std::string> m_email;
151+
std::vector<std::string> m_dns;
152+
std::vector<std::string> m_uri;
153+
std::vector<std::string> m_xmpp;
154+
std::vector<uint32_t> m_ipv4;
155+
std::vector<OID> m_ext_usage;
156+
bool m_is_ca_request = false;
157+
std::optional<size_t> m_path_limit;
158+
};
159+
160+
CertificateParametersBuilder::CertificateParametersBuilder() :
161+
m_state(std::make_unique<CertificateParametersBuilder::State>()) {}
162+
163+
CertificateParametersBuilder::CertificateParametersBuilder(CertificateParametersBuilder&& other) noexcept = default;
164+
165+
CertificateParametersBuilder::~CertificateParametersBuilder() = default;
166+
167+
X509_Certificate CertificateParametersBuilder::into_self_signed_cert(std::chrono::system_clock::time_point not_before,
168+
std::chrono::system_clock::time_point not_after,
169+
const Private_Key& key,
170+
RandomNumberGenerator& rng,
171+
std::optional<std::string_view> hash_fn,
172+
std::optional<std::string_view> padding) const {
173+
auto signer_p = X509_Object::choose_sig_format(key, rng, hash_fn.value_or(""), padding.value_or(""));
174+
auto& signer = *signer_p;
175+
176+
const AlgorithmIdentifier sig_algo = signer.algorithm_identifier();
177+
BOTAN_ASSERT_NOMSG(sig_algo.oid().has_value());
178+
179+
Extensions extensions = m_state->finalize_extensions(key);
180+
181+
const std::vector<uint8_t> pub_key = key.subject_public_key();
182+
auto skid = std::make_unique<Cert_Extension::Subject_Key_ID>(pub_key, signer.hash_function());
183+
184+
extensions.add_new(std::make_unique<Cert_Extension::Authority_Key_ID>(skid->get_key_id()));
185+
extensions.add_new(std::move(skid));
186+
187+
const auto& subject_dn = m_state->subject_dn();
188+
189+
return X509_CA::make_cert(
190+
signer, rng, sig_algo, pub_key, X509_Time(not_before), X509_Time(not_after), subject_dn, subject_dn, extensions);
191+
}
192+
193+
PKCS10_Request CertificateParametersBuilder::into_pkcs10_request(
194+
const Private_Key& key,
195+
RandomNumberGenerator& rng,
196+
std::optional<std::string_view> hash_fn,
197+
std::optional<std::string_view> padding,
198+
std::optional<std::string_view> challenge_password) const {
199+
const auto& subject_dn = m_state->subject_dn();
200+
201+
const auto extensions = m_state->finalize_extensions(key);
202+
203+
return PKCS10_Request::create(
204+
key, subject_dn, extensions, hash_fn.value_or(""), rng, padding.value_or(""), challenge_password.value_or(""));
205+
}
206+
207+
CertificateParametersBuilder& CertificateParametersBuilder::add_common_name(std::string_view cn) {
208+
m_state->add_common_name(cn);
209+
return (*this);
210+
}
211+
212+
CertificateParametersBuilder& CertificateParametersBuilder::add_country(std::string_view country) {
213+
m_state->add_country(country);
214+
return (*this);
215+
}
216+
217+
CertificateParametersBuilder& CertificateParametersBuilder::add_organization(std::string_view org) {
218+
m_state->add_organization(org);
219+
return (*this);
220+
}
221+
222+
CertificateParametersBuilder& CertificateParametersBuilder::add_organizational_unit(std::string_view org_unit) {
223+
m_state->add_organizational_unit(org_unit);
224+
return (*this);
225+
}
226+
227+
CertificateParametersBuilder& CertificateParametersBuilder::add_locality(std::string_view locality) {
228+
m_state->add_locality(locality);
229+
return (*this);
230+
}
231+
232+
CertificateParametersBuilder& CertificateParametersBuilder::add_state(std::string_view state) {
233+
m_state->add_state(state);
234+
return (*this);
235+
}
236+
237+
CertificateParametersBuilder& CertificateParametersBuilder::add_serial_number(std::string_view serial) {
238+
m_state->add_serial_number(serial);
239+
return (*this);
240+
}
241+
242+
CertificateParametersBuilder& CertificateParametersBuilder::add_email(std::string_view email) {
243+
m_state->add_email(email);
244+
return (*this);
245+
}
246+
247+
CertificateParametersBuilder& CertificateParametersBuilder::add_uri(std::string_view uri) {
248+
m_state->add_uri(uri);
249+
return (*this);
250+
}
251+
252+
CertificateParametersBuilder& CertificateParametersBuilder::add_dns(std::string_view dns) {
253+
m_state->add_dns(dns);
254+
return (*this);
255+
}
256+
257+
CertificateParametersBuilder& CertificateParametersBuilder::add_ipv4(uint32_t ipv4) {
258+
m_state->add_ipv4(ipv4);
259+
return (*this);
260+
}
261+
262+
CertificateParametersBuilder& CertificateParametersBuilder::add_xmpp(std::string_view xmpp) {
263+
m_state->add_xmpp(xmpp);
264+
return (*this);
265+
}
266+
267+
CertificateParametersBuilder& CertificateParametersBuilder::add_allowed_usage(Key_Constraints kc) {
268+
m_state->add_allowed_usage(kc);
269+
return (*this);
270+
}
271+
272+
CertificateParametersBuilder& CertificateParametersBuilder::add_allowed_extended_usage(const OID& usage) {
273+
m_state->add_allowed_extended_usage(usage);
274+
return (*this);
275+
}
276+
277+
CertificateParametersBuilder& CertificateParametersBuilder::add_extension(std::unique_ptr<Certificate_Extension> extn,
278+
bool is_critical) {
279+
m_state->add_extension(std::move(extn), is_critical);
280+
return (*this);
281+
}
282+
283+
CertificateParametersBuilder& CertificateParametersBuilder::set_as_ca_certificate(std::optional<size_t> path_limit) {
284+
m_state->set_as_ca_certificate(path_limit);
285+
return (*this);
286+
}
287+
288+
} // namespace Botan

0 commit comments

Comments
 (0)