• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

randombit / botan / 13021208024

28 Jan 2025 11:07PM UTC coverage: 91.241% (-0.02%) from 91.258%
13021208024

push

github

web-flow
Merge pull request #4604 from randombit/jack/support-minimal-curves

Avoid requiring legacy_ec_group in order to run the tests

94137 of 103174 relevant lines covered (91.24%)

11291073.45 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.11
/src/tests/unit_x509.cpp
1
/*
2
* (C) 2009,2019 Jack Lloyd
3
* (C) 2016 René Korthaus, Rohde & Schwarz Cybersecurity
4
*
5
* Botan is released under the Simplified BSD License (see license.txt)
6
*/
7

8
#include "tests.h"
9

10
#if defined(BOTAN_HAS_X509_CERTIFICATES)
11
   #include <botan/ber_dec.h>
12
   #include <botan/der_enc.h>
13
   #include <botan/pk_algs.h>
14
   #include <botan/pkcs10.h>
15
   #include <botan/pkcs8.h>
16
   #include <botan/x509_ca.h>
17
   #include <botan/x509_ext.h>
18
   #include <botan/x509path.h>
19
   #include <botan/x509self.h>
20
   #include <botan/internal/calendar.h>
21

22
   #if defined(BOTAN_HAS_ECC_GROUP)
23
      #include <botan/ec_group.h>
24
   #endif
25
#endif
26

27
namespace Botan_Tests {
28

29
namespace {
30

31
#if defined(BOTAN_HAS_X509_CERTIFICATES)
32

33
Botan::X509_Time from_date(const int y, const int m, const int d) {
346✔
34
   const size_t this_year = Botan::calendar_point(std::chrono::system_clock::now()).year();
346✔
35

36
   Botan::calendar_point t(static_cast<uint32_t>(this_year + y), m, d, 0, 0, 0);
346✔
37
   return Botan::X509_Time(t.to_std_timepoint());
346✔
38
}
39

40
/* Return some option sets */
41
Botan::X509_Cert_Options ca_opts(const std::string& sig_padding = "") {
121✔
42
   Botan::X509_Cert_Options opts("Test CA/US/Botan Project/Testing");
121✔
43

44
   opts.uri = "https://botan.randombit.net";
121✔
45
   opts.dns = "botan.randombit.net";
121✔
46
   opts.email = "testing@randombit.net";
121✔
47
   opts.set_padding_scheme(sig_padding);
121✔
48

49
   opts.CA_key(1);
121✔
50

51
   return opts;
121✔
52
}
×
53

54
Botan::X509_Cert_Options req_opts1(const std::string& algo, const std::string& sig_padding = "") {
39✔
55
   Botan::X509_Cert_Options opts("Test User 1/US/Botan Project/Testing");
39✔
56

57
   opts.uri = "https://botan.randombit.net";
39✔
58
   opts.dns = "botan.randombit.net";
39✔
59
   opts.email = "testing@randombit.net";
39✔
60
   opts.set_padding_scheme(sig_padding);
39✔
61

62
   opts.not_before("160101200000Z");
39✔
63
   opts.not_after("300101200000Z");
39✔
64

65
   opts.challenge = "zoom";
39✔
66

67
   if(algo == "RSA") {
39✔
68
      opts.constraints = Botan::Key_Constraints::KeyEncipherment;
9✔
69
   } else if(algo == "DSA" || algo == "ECDSA" || algo == "ECGDSA" || algo == "ECKCDSA") {
30✔
70
      opts.constraints = Botan::Key_Constraints::DigitalSignature;
12✔
71
   }
72

73
   return opts;
39✔
74
}
×
75

76
Botan::X509_Cert_Options req_opts2(const std::string& sig_padding = "") {
12✔
77
   Botan::X509_Cert_Options opts("Test User 2/US/Botan Project/Testing");
12✔
78

79
   opts.uri = "https://botan.randombit.net";
12✔
80
   opts.dns = "botan.randombit.net";
12✔
81
   opts.email = "testing@randombit.net";
12✔
82
   opts.set_padding_scheme(sig_padding);
12✔
83

84
   opts.add_ex_constraint("PKIX.EmailProtection");
12✔
85

86
   return opts;
12✔
87
}
×
88

89
Botan::X509_Cert_Options req_opts3(const std::string& sig_padding = "") {
60✔
90
   Botan::X509_Cert_Options opts("Test User 2/US/Botan Project/Testing");
60✔
91

92
   opts.uri = "https://botan.randombit.net";
60✔
93
   opts.dns = "botan.randombit.net";
60✔
94
   opts.email = "testing@randombit.net";
60✔
95
   opts.set_padding_scheme(sig_padding);
60✔
96

97
   opts.more_org_units.push_back("IT");
120✔
98
   opts.more_org_units.push_back("Security");
120✔
99
   opts.more_dns.push_back("www.botan.randombit.net");
120✔
100

101
   return opts;
60✔
102
}
×
103

104
std::unique_ptr<Botan::Private_Key> make_a_private_key(const std::string& algo, Botan::RandomNumberGenerator& rng) {
105✔
105
   const std::string params = [&] {
210✔
106
      // Here we override defaults as needed
107
      if(algo == "RSA") {
105✔
108
         return "1024";
109
      }
110
      if(algo == "GOST-34.10") {
89✔
111
         if(Botan::EC_Group::supports_named_group("gost_256A")) {
8✔
112
            return "gost_256A";
113
         } else {
114
            return "secp256r1";
×
115
         }
116
      }
117
      if(algo == "ECKCDSA" || algo == "ECGDSA") {
81✔
118
         return "brainpool256r1";
119
      }
120
      if(algo == "HSS-LMS") {
65✔
121
         return "SHA-256,HW(5,4),HW(5,4)";
122
      }
123
      if(algo == "SLH-DSA") {
57✔
124
         return "SLH-DSA-SHA2-128f";
2✔
125
      }
126
      return "";  // default "" means choose acceptable algo-specific params
127
   }();
105✔
128

129
   return Botan::create_private_key(algo, rng, params);
105✔
130
}
105✔
131

132
Test::Result test_cert_status_strings() {
1✔
133
   Test::Result result("Certificate_Status_Code to_string");
1✔
134

135
   std::set<std::string> seen;
1✔
136

137
   result.test_eq("Same string",
1✔
138
                  Botan::to_string(Botan::Certificate_Status_Code::OK),
139
                  Botan::to_string(Botan::Certificate_Status_Code::VERIFIED));
140

141
   const Botan::Certificate_Status_Code codes[]{
1✔
142
      Botan::Certificate_Status_Code::OCSP_RESPONSE_GOOD,
143
      Botan::Certificate_Status_Code::OCSP_SIGNATURE_OK,
144
      Botan::Certificate_Status_Code::VALID_CRL_CHECKED,
145
      Botan::Certificate_Status_Code::OCSP_NO_HTTP,
146

147
      Botan::Certificate_Status_Code::CERT_SERIAL_NEGATIVE,
148
      Botan::Certificate_Status_Code::DN_TOO_LONG,
149

150
      Botan::Certificate_Status_Code::SIGNATURE_METHOD_TOO_WEAK,
151
      Botan::Certificate_Status_Code::NO_MATCHING_CRLDP,
152
      Botan::Certificate_Status_Code::UNTRUSTED_HASH,
153
      Botan::Certificate_Status_Code::NO_REVOCATION_DATA,
154
      Botan::Certificate_Status_Code::CERT_NOT_YET_VALID,
155
      Botan::Certificate_Status_Code::CERT_HAS_EXPIRED,
156
      Botan::Certificate_Status_Code::OCSP_NOT_YET_VALID,
157
      Botan::Certificate_Status_Code::OCSP_HAS_EXPIRED,
158
      Botan::Certificate_Status_Code::CRL_NOT_YET_VALID,
159
      Botan::Certificate_Status_Code::CRL_HAS_EXPIRED,
160
      Botan::Certificate_Status_Code::CERT_ISSUER_NOT_FOUND,
161
      Botan::Certificate_Status_Code::CANNOT_ESTABLISH_TRUST,
162
      Botan::Certificate_Status_Code::CERT_CHAIN_LOOP,
163
      Botan::Certificate_Status_Code::CHAIN_LACKS_TRUST_ROOT,
164
      Botan::Certificate_Status_Code::CHAIN_NAME_MISMATCH,
165
      Botan::Certificate_Status_Code::POLICY_ERROR,
166
      Botan::Certificate_Status_Code::DUPLICATE_CERT_POLICY,
167
      Botan::Certificate_Status_Code::INVALID_USAGE,
168
      Botan::Certificate_Status_Code::CERT_CHAIN_TOO_LONG,
169
      Botan::Certificate_Status_Code::CA_CERT_NOT_FOR_CERT_ISSUER,
170
      Botan::Certificate_Status_Code::NAME_CONSTRAINT_ERROR,
171
      Botan::Certificate_Status_Code::CA_CERT_NOT_FOR_CRL_ISSUER,
172
      Botan::Certificate_Status_Code::OCSP_CERT_NOT_LISTED,
173
      Botan::Certificate_Status_Code::OCSP_BAD_STATUS,
174
      Botan::Certificate_Status_Code::CERT_NAME_NOMATCH,
175
      Botan::Certificate_Status_Code::UNKNOWN_CRITICAL_EXTENSION,
176
      Botan::Certificate_Status_Code::DUPLICATE_CERT_EXTENSION,
177
      Botan::Certificate_Status_Code::EXT_IN_V1_V2_CERT,
178
      Botan::Certificate_Status_Code::OCSP_SIGNATURE_ERROR,
179
      Botan::Certificate_Status_Code::OCSP_ISSUER_NOT_FOUND,
180
      Botan::Certificate_Status_Code::OCSP_RESPONSE_MISSING_KEYUSAGE,
181
      Botan::Certificate_Status_Code::OCSP_RESPONSE_INVALID,
182
      Botan::Certificate_Status_Code::CERT_IS_REVOKED,
183
      Botan::Certificate_Status_Code::CRL_BAD_SIGNATURE,
184
      Botan::Certificate_Status_Code::SIGNATURE_ERROR,
185
      Botan::Certificate_Status_Code::CERT_PUBKEY_INVALID,
186
      Botan::Certificate_Status_Code::SIGNATURE_ALGO_UNKNOWN,
187
      Botan::Certificate_Status_Code::SIGNATURE_ALGO_BAD_PARAMS,
188
   };
189

190
   for(const auto code : codes) {
45✔
191
      const std::string s = Botan::to_string(code);
44✔
192
      result.confirm("String is long enough to be informative", s.size() > 12);
88✔
193
      result.test_eq("No duplicates", seen.count(s), 0);
44✔
194
      seen.insert(s);
44✔
195
   }
44✔
196

197
   return result;
1✔
198
}
1✔
199

200
Test::Result test_x509_extension() {
1✔
201
   Test::Result result("X509 Extensions API");
1✔
202

203
   Botan::Extensions extn;
1✔
204

205
   const auto oid_bc = Botan::OID::from_string("X509v3.BasicConstraints");
1✔
206
   const auto oid_skid = Botan::OID::from_string("X509v3.SubjectKeyIdentifier");
1✔
207

208
   extn.add(std::make_unique<Botan::Cert_Extension::Basic_Constraints>(true), true);
2✔
209

210
   result.confirm("Basic constraints is set", extn.extension_set(oid_bc));
2✔
211
   result.confirm("Basic constraints is critical", extn.critical_extension_set(oid_bc));
2✔
212
   result.confirm("SKID is not set", !extn.extension_set(oid_skid));
2✔
213
   result.confirm("SKID is not critical", !extn.critical_extension_set(oid_skid));
2✔
214

215
   result.test_eq("Extension::get_extension_bits", extn.get_extension_bits(oid_bc), "30060101FF020100");
2✔
216

217
   result.test_throws("Extension::get_extension_bits throws if not set", [&]() { extn.get_extension_bits(oid_skid); });
3✔
218

219
   result.test_throws("Extension::add throws on second add",
2✔
220
                      [&]() { extn.add(std::make_unique<Botan::Cert_Extension::Basic_Constraints>(false), false); });
2✔
221

222
   result.test_eq("Extension::get_extension_bits", extn.get_extension_bits(oid_bc), "30060101FF020100");
2✔
223

224
   result.confirm("Returns false since extension already existed",
1✔
225
                  !extn.add_new(std::make_unique<Botan::Cert_Extension::Basic_Constraints>(false), false));
2✔
226

227
   result.confirm("Basic constraints is still critical", extn.critical_extension_set(oid_bc));
2✔
228

229
   extn.replace(std::make_unique<Botan::Cert_Extension::Basic_Constraints>(false), false);
2✔
230
   result.confirm("Replaced basic constraints is not critical", !extn.critical_extension_set(oid_bc));
2✔
231
   result.test_eq("Extension::get_extension_bits", extn.get_extension_bits(oid_bc), "3000");
2✔
232

233
   result.confirm("Delete returns false if extn not set", !extn.remove(oid_skid));
2✔
234
   result.confirm("Delete returns true if extn was set", extn.remove(oid_bc));
2✔
235
   result.confirm("Basic constraints is not set", !extn.extension_set(oid_bc));
2✔
236
   result.confirm("Basic constraints is not critical", !extn.critical_extension_set(oid_bc));
2✔
237

238
   return result;
2✔
239
}
2✔
240

241
Test::Result test_x509_dates() {
1✔
242
   Test::Result result("X509 Time");
1✔
243

244
   Botan::X509_Time time;
1✔
245
   result.confirm("unset time not set", !time.time_is_set());
2✔
246
   time = Botan::X509_Time("080201182200Z", Botan::ASN1_Type::UtcTime);
1✔
247
   result.confirm("time set after construction", time.time_is_set());
2✔
248
   result.test_eq("time readable_string", time.readable_string(), "2008/02/01 18:22:00 UTC");
2✔
249

250
   time = Botan::X509_Time("200305100350Z", Botan::ASN1_Type::UtcTime);
1✔
251
   result.test_eq("UTC_TIME readable_string", time.readable_string(), "2020/03/05 10:03:50 UTC");
2✔
252

253
   time = Botan::X509_Time("200305100350Z");
1✔
254
   result.test_eq(
3✔
255
      "UTC_OR_GENERALIZED_TIME from UTC_TIME readable_string", time.readable_string(), "2020/03/05 10:03:50 UTC");
2✔
256

257
   time = Botan::X509_Time("20200305100350Z");
1✔
258
   result.test_eq("UTC_OR_GENERALIZED_TIME from GENERALIZED_TIME readable_string",
3✔
259
                  time.readable_string(),
2✔
260
                  "2020/03/05 10:03:50 UTC");
261

262
   time = Botan::X509_Time("20200305100350Z", Botan::ASN1_Type::GeneralizedTime);
1✔
263
   result.test_eq("GENERALIZED_TIME readable_string", time.readable_string(), "2020/03/05 10:03:50 UTC");
2✔
264

265
   // Dates that are valid per X.500 but rejected as unsupported
266
   const std::string valid_but_unsup[]{
1✔
267
      "0802010000-0000",
268
      "0802011724+0000",
269
      "0406142334-0500",
270
      "9906142334+0500",
271
      "0006142334-0530",
272
      "0006142334+0530",
273

274
      "080201000000-0000",
275
      "080201172412+0000",
276
      "040614233433-0500",
277
      "990614233444+0500",
278
      "000614233455-0530",
279
      "000614233455+0530",
280
   };
13✔
281

282
   // valid length 13
283
   const std::string valid_utc[]{
1✔
284
      "080201000000Z",
285
      "080201172412Z",
286
      "040614233433Z",
287
      "990614233444Z",
288
      "000614233455Z",
289
   };
6✔
290

291
   const std::string invalid_utc[]{
1✔
292
      "",
293
      " ",
294
      "2008`02-01",
295
      "9999-02-01",
296
      "2000-02-01 17",
297
      "999921",
298

299
      // No seconds
300
      "0802010000Z",
301
      "0802011724Z",
302
      "0406142334Z",
303
      "9906142334Z",
304
      "0006142334Z",
305

306
      // valid length 13 -> range check
307
      "080201000061Z",  // seconds too big (61)
308
      "080201000060Z",  // seconds too big (60, leap seconds not covered by the standard)
309
      "0802010000-1Z",  // seconds too small (-1)
310
      "080201006000Z",  // minutes too big (60)
311
      "080201240000Z",  // hours too big (24:00)
312

313
      // valid length 13 -> invalid numbers
314
      "08020123112 Z",
315
      "08020123112!Z",
316
      "08020123112,Z",
317
      "08020123112\nZ",
318
      "080201232 33Z",
319
      "080201232!33Z",
320
      "080201232,33Z",
321
      "080201232\n33Z",
322
      "0802012 3344Z",
323
      "0802012!3344Z",
324
      "0802012,3344Z",
325
      "08022\n334455Z",
326
      "08022 334455Z",
327
      "08022!334455Z",
328
      "08022,334455Z",
329
      "08022\n334455Z",
330
      "082 33445511Z",
331
      "082!33445511Z",
332
      "082,33445511Z",
333
      "082\n33445511Z",
334
      "2 2211221122Z",
335
      "2!2211221122Z",
336
      "2,2211221122Z",
337
      "2\n2211221122Z",
338

339
      // wrong time zone
340
      "080201000000",
341
      "080201000000z",
342

343
      // Fractional seconds
344
      "170217180154.001Z",
345

346
      // Timezone offset
347
      "170217180154+0100",
348

349
      // Extra digits
350
      "17021718015400Z",
351

352
      // Non-digits
353
      "17021718015aZ",
354

355
      // Trailing garbage
356
      "170217180154Zlongtrailinggarbage",
357

358
      // Swapped type
359
      "20170217180154Z",
360
   };
49✔
361

362
   // valid length 15
363
   const std::string valid_generalized_time[]{
1✔
364
      "20000305100350Z",
365
   };
2✔
366

367
   const std::string invalid_generalized[]{
1✔
368
      // No trailing Z
369
      "20000305100350",
370

371
      // No seconds
372
      "200003051003Z",
373

374
      // Fractional seconds
375
      "20000305100350.001Z",
376

377
      // Timezone offset
378
      "20170217180154+0100",
379

380
      // Extra digits
381
      "2017021718015400Z",
382

383
      // Non-digits
384
      "2017021718015aZ",
385

386
      // Trailing garbage
387
      "20170217180154Zlongtrailinggarbage",
388

389
      // Swapped type
390
      "170217180154Z",
391
   };
9✔
392

393
   for(const auto& v : valid_but_unsup) {
13✔
394
      result.test_throws("valid but unsupported", [v]() { Botan::X509_Time t(v, Botan::ASN1_Type::UtcTime); });
108✔
395
   }
396

397
   for(const auto& v : valid_utc) {
6✔
398
      Botan::X509_Time t(v, Botan::ASN1_Type::UtcTime);
5✔
399
   }
5✔
400

401
   for(const auto& v : valid_generalized_time) {
2✔
402
      Botan::X509_Time t(v, Botan::ASN1_Type::GeneralizedTime);
1✔
403
   }
1✔
404

405
   for(const auto& v : invalid_utc) {
49✔
406
      result.test_throws("invalid", [v]() { Botan::X509_Time t(v, Botan::ASN1_Type::UtcTime); });
432✔
407
   }
408

409
   for(const auto& v : invalid_generalized) {
9✔
410
      result.test_throws("invalid", [v]() { Botan::X509_Time t(v, Botan::ASN1_Type::GeneralizedTime); });
72✔
411
   }
412

413
   return result;
1✔
414
}
80✔
415

416
Test::Result test_x509_encode_authority_info_access_extension() {
1✔
417
   Test::Result result("X509 with encoded PKIX.AuthorityInformationAccess extension");
1✔
418

419
   #if defined(BOTAN_HAS_RSA)
420
   auto rng = Test::new_rng(__func__);
1✔
421

422
   const std::string sig_algo{"RSA"};
1✔
423
   const std::string hash_fn{"SHA-256"};
1✔
424
   const std::string padding_method{"EMSA3(SHA-256)"};
1✔
425

426
   // CA Issuer information
427
   const std::vector<std::string> ca_issuers = {
1✔
428
      "http://www.d-trust.net/cgi-bin/Bdrive_Test_CA_1-2_2017.crt",
429
      "ldap://directory.d-trust.net/CN=Bdrive%20Test%20CA%201-2%202017,O=Bundesdruckerei%20GmbH,C=DE?cACertificate?base?"};
1✔
430

431
   // OCSP
432
   const std::string_view ocsp_uri{"http://staging.ocsp.d-trust.net"};
1✔
433

434
   // create a CA
435
   auto ca_key = make_a_private_key(sig_algo, *rng);
1✔
436
   result.require("CA key", ca_key != nullptr);
1✔
437
   const auto ca_cert = Botan::X509::create_self_signed_cert(ca_opts(), *ca_key, hash_fn, *rng);
1✔
438
   Botan::X509_CA ca(ca_cert, *ca_key, hash_fn, padding_method, *rng);
1✔
439

440
   // create a certificate with only caIssuer information
441
   auto key = make_a_private_key(sig_algo, *rng);
1✔
442

443
   Botan::X509_Cert_Options opts1 = req_opts1(sig_algo);
1✔
444
   opts1.extensions.add(std::make_unique<Botan::Cert_Extension::Authority_Information_Access>("", ca_issuers));
2✔
445

446
   Botan::PKCS10_Request req = Botan::X509::create_cert_req(opts1, *key, hash_fn, *rng);
1✔
447

448
   Botan::X509_Certificate cert = ca.sign_request(req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01));
1✔
449

450
   if(!result.test_eq("number of ca_issuers URIs", cert.ca_issuers().size(), 2)) {
1✔
451
      return result;
452
   }
453

454
   for(const auto& ca_issuer : cert.ca_issuers()) {
3✔
455
      result.confirm("CA issuer URI present in certificate",
4✔
456
                     std::ranges::find(ca_issuers, ca_issuer) != ca_issuers.end());
4✔
457
   }
1✔
458

459
   result.confirm("no OCSP url available", cert.ocsp_responder().empty());
2✔
460

461
   // create a certificate with only OCSP URI information
462
   Botan::X509_Cert_Options opts2 = req_opts1(sig_algo);
1✔
463
   opts2.extensions.add(std::make_unique<Botan::Cert_Extension::Authority_Information_Access>(ocsp_uri));
2✔
464

465
   req = Botan::X509::create_cert_req(opts2, *key, hash_fn, *rng);
1✔
466

467
   cert = ca.sign_request(req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01));
2✔
468

469
   result.confirm("OCSP URI available", !cert.ocsp_responder().empty());
2✔
470
   result.confirm("no CA Issuer URI available", cert.ca_issuers().empty());
2✔
471
   result.test_eq("OCSP responder URI matches", cert.ocsp_responder(), std::string(ocsp_uri));
3✔
472

473
   // create a certificate with OCSP URI and CA Issuer information
474
   Botan::X509_Cert_Options opts3 = req_opts1(sig_algo);
1✔
475
   opts3.extensions.add(std::make_unique<Botan::Cert_Extension::Authority_Information_Access>(ocsp_uri, ca_issuers));
2✔
476

477
   req = Botan::X509::create_cert_req(opts3, *key, hash_fn, *rng);
1✔
478

479
   cert = ca.sign_request(req, *rng, from_date(-1, 01, 01), from_date(2, 01, 01));
2✔
480

481
   result.confirm("OCSP URI available", !cert.ocsp_responder().empty());
2✔
482
   result.confirm("CA Issuer URI available", !cert.ca_issuers().empty());
2✔
483
   #endif
484

485
   return result;
1✔
486
}
4✔
487

488
   #if defined(BOTAN_TARGET_OS_HAS_FILESYSTEM)
489

490
Test::Result test_crl_dn_name() {
1✔
491
   Test::Result result("CRL DN name");
1✔
492

493
      // See GH #1252
494

495
      #if defined(BOTAN_HAS_RSA) && defined(BOTAN_HAS_EMSA_PKCS1)
496
   auto rng = Test::new_rng(__func__);
1✔
497

498
   const Botan::OID dc_oid("0.9.2342.19200300.100.1.25");
1✔
499

500
   Botan::X509_Certificate cert(Test::data_file("x509/misc/opcuactt_ca.der"));
2✔
501

502
   Botan::DataSource_Stream key_input(Test::data_file("x509/misc/opcuactt_ca.pem"));
2✔
503
   auto key = Botan::PKCS8::load_key(key_input);
1✔
504
   Botan::X509_CA ca(cert, *key, "SHA-256", *rng);
1✔
505

506
   Botan::X509_CRL crl = ca.new_crl(*rng);
1✔
507

508
   result.confirm("matches issuer cert", crl.issuer_dn() == cert.subject_dn());
2✔
509

510
   result.confirm("contains DC component", crl.issuer_dn().get_attributes().count(dc_oid) == 1);
3✔
511
      #endif
512

513
   return result;
2✔
514
}
3✔
515

516
Test::Result test_rdn_multielement_set_name() {
1✔
517
   Test::Result result("DN with multiple elements in RDN");
1✔
518

519
   // GH #2611
520

521
   Botan::X509_Certificate cert(Test::data_file("x509/misc/rdn_set.crt"));
2✔
522

523
   result.confirm("issuer DN contains expected name components", cert.issuer_dn().get_attributes().size() == 4);
2✔
524
   result.confirm("subject DN contains expected name components", cert.subject_dn().get_attributes().size() == 4);
2✔
525

526
   return result;
1✔
527
}
1✔
528

529
Test::Result test_rsa_oaep() {
1✔
530
   Test::Result result("RSA OAEP decoding");
1✔
531

532
      #if defined(BOTAN_HAS_RSA)
533
   Botan::X509_Certificate cert(Test::data_file("x509/misc/rsa_oaep.pem"));
2✔
534

535
   auto public_key = cert.subject_public_key();
1✔
536
   result.test_not_null("Decoding RSA-OAEP worked", public_key.get());
1✔
537
   const auto& pk_info = cert.subject_public_key_algo();
1✔
538

539
   result.test_eq("RSA-OAEP OID", pk_info.oid().to_string(), Botan::OID::from_string("RSA/OAEP").to_string());
2✔
540
      #endif
541

542
   return result;
2✔
543
}
1✔
544

545
Test::Result test_x509_decode_list() {
1✔
546
   Test::Result result("X509_Certificate list decode");
1✔
547

548
   Botan::DataSource_Stream input(Test::data_file("x509/misc/cert_seq.der"), true);
2✔
549

550
   Botan::BER_Decoder dec(input);
1✔
551
   std::vector<Botan::X509_Certificate> certs;
1✔
552
   dec.decode_list(certs);
1✔
553

554
   result.test_eq("Expected number of certs in list", certs.size(), 2);
1✔
555

556
   result.test_eq("Expected cert 1 CN", certs[0].subject_dn().get_first_attribute("CN"), "CA1-PP.01.02");
2✔
557
   result.test_eq("Expected cert 2 CN", certs[1].subject_dn().get_first_attribute("CN"), "User1-PP.01.02");
2✔
558

559
   return result;
1✔
560
}
1✔
561

562
Test::Result test_x509_utf8() {
1✔
563
   Test::Result result("X509 with UTF-8 encoded fields");
1✔
564

565
   try {
1✔
566
      Botan::X509_Certificate utf8_cert(Test::data_file("x509/misc/contains_utf8string.pem"));
2✔
567

568
      // UTF-8 encoded fields of test certificate (contains cyrillic letters)
569
      const std::string organization =
1✔
570
         "\xD0\x9C\xD0\xBE\xD1\x8F\x20\xD0\xBA\xD0\xBE\xD0"
571
         "\xBC\xD0\xBF\xD0\xB0\xD0\xBD\xD0\xB8\xD1\x8F";
1✔
572
      const std::string organization_unit =
1✔
573
         "\xD0\x9C\xD0\xBE\xD1\x91\x20\xD0\xBF\xD0\xBE\xD0\xB4\xD1\x80\xD0\xB0"
574
         "\xD0\xB7\xD0\xB4\xD0\xB5\xD0\xBB\xD0\xB5\xD0\xBD\xD0\xB8\xD0\xB5";
1✔
575
      const std::string common_name =
1✔
576
         "\xD0\x9E\xD0\xBF\xD0\xB8\xD1\x81\xD0\xB0\xD0\xBD\xD0\xB8"
577
         "\xD0\xB5\x20\xD1\x81\xD0\xB0\xD0\xB9\xD1\x82\xD0\xB0";
1✔
578
      const std::string location = "\xD0\x9C\xD0\xBE\xD1\x81\xD0\xBA\xD0\xB2\xD0\xB0";
1✔
579

580
      const Botan::X509_DN& issuer_dn = utf8_cert.issuer_dn();
1✔
581

582
      result.test_eq("O", issuer_dn.get_first_attribute("O"), organization);
2✔
583
      result.test_eq("OU", issuer_dn.get_first_attribute("OU"), organization_unit);
2✔
584
      result.test_eq("CN", issuer_dn.get_first_attribute("CN"), common_name);
2✔
585
      result.test_eq("L", issuer_dn.get_first_attribute("L"), location);
2✔
586
   } catch(const Botan::Decoding_Error& ex) {
1✔
587
      result.test_failure(ex.what());
×
588
   }
×
589

590
   return result;
1✔
591
}
×
592

593
Test::Result test_x509_bmpstring() {
1✔
594
   Test::Result result("X509 with UCS-2 (BMPString) encoded fields");
1✔
595

596
   try {
1✔
597
      Botan::X509_Certificate ucs2_cert(Test::data_file("x509/misc/contains_bmpstring.pem"));
2✔
598

599
      // UTF-8 encoded fields of test certificate (contains cyrillic and greek letters)
600
      const std::string organization = "\x6E\x65\xCF\x87\xCF\xB5\x6E\x69\xCF\x89";
1✔
601
      const std::string common_name =
1✔
602
         "\xC3\xA8\x6E\xC7\x9D\xD0\xAF\x20\xD0\x9C\xC7\x9D\xD0\xB9\xD0\xB7\xD1\x8D\xD0\xBB";
1✔
603

604
      // UTF-8 encoded fields of test certificate (contains only ASCII characters)
605
      const std::string location = "Berlin";
1✔
606

607
      const Botan::X509_DN& issuer_dn = ucs2_cert.issuer_dn();
1✔
608

609
      result.test_eq("O", issuer_dn.get_first_attribute("O"), organization);
2✔
610
      result.test_eq("CN", issuer_dn.get_first_attribute("CN"), common_name);
2✔
611
      result.test_eq("L", issuer_dn.get_first_attribute("L"), location);
2✔
612
   } catch(const Botan::Decoding_Error& ex) {
1✔
613
      result.test_failure(ex.what());
×
614
   }
×
615

616
   return result;
1✔
617
}
×
618

619
Test::Result test_x509_teletex() {
1✔
620
   Test::Result result("X509 with TeletexString encoded fields");
1✔
621

622
   try {
1✔
623
      Botan::X509_Certificate teletex_cert(Test::data_file("x509/misc/teletex_dn.der"));
2✔
624

625
      const Botan::X509_DN& issuer_dn = teletex_cert.issuer_dn();
1✔
626

627
      const std::string common_name = "neam Gesellschaft f\xc3\xbcr Kommunikationsl\xc3\xb6sungen mbH";
1✔
628

629
      result.test_eq("O", issuer_dn.get_first_attribute("O"), "neam CA");
2✔
630
      result.test_eq("CN", issuer_dn.get_first_attribute("CN"), common_name);
2✔
631
   } catch(const Botan::Decoding_Error& ex) {
1✔
632
      result.test_failure(ex.what());
×
633
   }
×
634

635
   return result;
1✔
636
}
×
637

638
Test::Result test_x509_authority_info_access_extension() {
1✔
639
   Test::Result result("X509 with PKIX.AuthorityInformationAccess extension");
1✔
640

641
   // contains no AIA extension
642
   Botan::X509_Certificate no_aia_cert(Test::data_file("x509/misc/contains_utf8string.pem"));
2✔
643

644
   result.test_eq("number of ca_issuers URLs", no_aia_cert.ca_issuers().size(), 0);
2✔
645
   result.test_eq("CA issuer URL matches", no_aia_cert.ocsp_responder(), "");
2✔
646

647
   // contains AIA extension with 1 CA issuer URL and 1 OCSP responder
648
   Botan::X509_Certificate aia_cert(Test::data_file("x509/misc/contains_authority_info_access.pem"));
2✔
649

650
   const auto ca_issuers = aia_cert.ca_issuers();
1✔
651

652
   result.test_eq("number of ca_issuers URLs", ca_issuers.size(), 1);
1✔
653
   if(result.tests_failed()) {
1✔
654
      return result;
655
   }
656

657
   result.test_eq("CA issuer URL matches", ca_issuers[0], "http://gp.symcb.com/gp.crt");
2✔
658
   result.test_eq("OCSP responder URL matches", aia_cert.ocsp_responder(), "http://gp.symcd.com");
2✔
659

660
   // contains AIA extension with 2 CA issuer URL and 1 OCSP responder
661
   Botan::X509_Certificate aia_cert_2ca(
1✔
662
      Test::data_file("x509/misc/contains_authority_info_access_with_two_ca_issuers.pem"));
2✔
663

664
   const auto ca_issuers2 = aia_cert_2ca.ca_issuers();
1✔
665

666
   result.test_eq("number of ca_issuers URLs", ca_issuers2.size(), 2);
1✔
667
   if(result.tests_failed()) {
1✔
668
      return result;
669
   }
670

671
   result.test_eq(
2✔
672
      "CA issuer URL matches", ca_issuers2[0], "http://www.d-trust.net/cgi-bin/Bdrive_Test_CA_1-2_2017.crt");
1✔
673
   result.test_eq(
2✔
674
      "CA issuer URL matches",
675
      ca_issuers2[1],
1✔
676
      "ldap://directory.d-trust.net/CN=Bdrive%20Test%20CA%201-2%202017,O=Bundesdruckerei%20GmbH,C=DE?cACertificate?base?");
677
   result.test_eq("OCSP responder URL matches", aia_cert_2ca.ocsp_responder(), "http://staging.ocsp.d-trust.net");
2✔
678

679
   return result;
1✔
680
}
1✔
681

682
Test::Result test_parse_rsa_pss_cert() {
1✔
683
   Test::Result result("X509 RSA-PSS certificate");
1✔
684

685
   // See https://github.com/randombit/botan/issues/3019 for background
686

687
   try {
1✔
688
      Botan::X509_Certificate rsa_pss(Test::data_file("x509/misc/rsa_pss.pem"));
2✔
689
      result.test_success("Was able to parse RSA-PSS certificate signed with ECDSA");
1✔
690
   } catch(Botan::Exception& e) {
1✔
691
      result.test_failure("Parsing failed", e.what());
×
692
   }
×
693

694
   return result;
1✔
695
}
×
696

697
Test::Result test_verify_gost2012_cert() {
1✔
698
   Test::Result result("X509 GOST-2012 certificates");
1✔
699

700
      #if defined(BOTAN_HAS_GOST_34_10_2012) && defined(BOTAN_HAS_STREEBOG)
701
   try {
1✔
702
      if(Botan::EC_Group::supports_named_group("gost_256A")) {
1✔
703
         Botan::X509_Certificate root_cert(Test::data_file("x509/gost/gost_root.pem"));
2✔
704
         Botan::X509_Certificate root_int(Test::data_file("x509/gost/gost_int.pem"));
2✔
705

706
         Botan::Certificate_Store_In_Memory trusted;
1✔
707
         trusted.add_certificate(root_cert);
1✔
708

709
         const Botan::Path_Validation_Restrictions restrictions(false, 128, false, {"Streebog-256"});
2✔
710
         const Botan::Path_Validation_Result validation_result =
1✔
711
            Botan::x509_path_validate(root_int, restrictions, trusted);
1✔
712

713
         result.confirm("GOST certificate validates", validation_result.successful_validation());
2✔
714
      }
1✔
715
   } catch(const Botan::Decoding_Error& e) {
×
716
      result.test_failure(e.what());
×
717
   }
×
718
      #endif
719

720
   return result;
1✔
721
}
×
722

723
      /*
724
 * @brief checks the configurability of the EMSA4(RSA-PSS) signature scheme
725
 *
726
 * For the other algorithms than RSA, only one padding is supported right now.
727
 */
728
      #if defined(BOTAN_HAS_EMSA_PKCS1) && defined(BOTAN_HAS_EMSA_PSSR) && defined(BOTAN_HAS_RSA)
729
Test::Result test_padding_config() {
1✔
730
   // Throughout the test, some synonyms for EMSA4 are used, e.g. PSSR, EMSA-PSS
731
   Test::Result test_result("X509 Padding Config");
1✔
732

733
   auto rng = Test::new_rng(__func__);
1✔
734

735
   Botan::DataSource_Stream key_stream(Test::data_file("x509/misc/rsa_key.pem"));
2✔
736
   auto sk = Botan::PKCS8::load_key(key_stream);
1✔
737

738
   // Create X509 CA certificate; EMSA3 is used for signing by default
739
   Botan::X509_Cert_Options opt("TESTCA");
1✔
740
   opt.CA_key();
1✔
741

742
   Botan::X509_Certificate ca_cert_def = Botan::X509::create_self_signed_cert(opt, (*sk), "SHA-512", *rng);
1✔
743
   test_result.test_eq("CA certificate signature algorithm (default)",
3✔
744
                       ca_cert_def.signature_algorithm().oid().to_formatted_string(),
2✔
745
                       "RSA/PKCS1v15(SHA-512)");
746

747
   // Create X509 CA certificate; RSA-PSS is explicitly set
748
   opt.set_padding_scheme("PSSR");
1✔
749
   Botan::X509_Certificate ca_cert_exp = Botan::X509::create_self_signed_cert(opt, (*sk), "SHA-512", *rng);
1✔
750
   test_result.test_eq("CA certificate signature algorithm (explicit)",
3✔
751
                       ca_cert_exp.signature_algorithm().oid().to_formatted_string(),
2✔
752
                       "RSA/PSS");
753

754
         #if defined(BOTAN_HAS_EMSA_X931)
755
   // Try to set a padding scheme that is not supported for signing with the given key type
756
   opt.set_padding_scheme("EMSA2");
1✔
757
   try {
1✔
758
      Botan::X509_Certificate ca_cert_wrong = Botan::X509::create_self_signed_cert(opt, (*sk), "SHA-512", *rng);
1✔
759
      test_result.test_failure("Could build CA cert with invalid encoding scheme X9.31 for key type " +
×
760
                               sk->algo_name());
×
761
   } catch(const Botan::Invalid_Argument& e) {
1✔
762
      test_result.test_eq("Build CA certificate with invalid encoding scheme X9.31 for key type " + sk->algo_name(),
3✔
763
                          e.what(),
1✔
764
                          "Signatures using RSA/X9.31(SHA-512) are not supported");
765
   }
1✔
766
         #endif
767

768
   test_result.test_eq("CA certificate signature algorithm (explicit)",
3✔
769
                       ca_cert_exp.signature_algorithm().oid().to_formatted_string(),
2✔
770
                       "RSA/PSS");
771

772
   const Botan::X509_Time not_before = from_date(-1, 1, 1);
1✔
773
   const Botan::X509_Time not_after = from_date(2, 1, 2);
1✔
774

775
   // Prepare a signing request for the end certificate
776
   Botan::X509_Cert_Options req_opt("endpoint");
1✔
777
   req_opt.set_padding_scheme("EMSA4(SHA-512,MGF1,64)");
1✔
778
   Botan::PKCS10_Request end_req = Botan::X509::create_cert_req(req_opt, (*sk), "SHA-512", *rng);
1✔
779
   test_result.test_eq(
3✔
780
      "Certificate request signature algorithm", end_req.signature_algorithm().oid().to_formatted_string(), "RSA/PSS");
2✔
781

782
   // Create X509 CA object: will fail as the chosen hash functions differ
783
   try {
1✔
784
      Botan::X509_CA ca_fail(ca_cert_exp, (*sk), "SHA-512", "EMSA4(SHA-256)", *rng);
1✔
785
      test_result.test_failure("Configured conflicting hash functions for CA");
×
786
   } catch(const Botan::Invalid_Argument& e) {
1✔
787
      test_result.test_eq(
1✔
788
         "Configured conflicting hash functions for CA",
789
         e.what(),
1✔
790
         "Specified hash function SHA-512 is incompatible with RSA chose hash function SHA-256 with user specified padding EMSA4(SHA-256)");
791
   }
1✔
792

793
   // Create X509 CA object: its signer will use the padding scheme from the CA certificate, i.e. EMSA3
794
   Botan::X509_CA ca_def(ca_cert_def, (*sk), "SHA-512", *rng);
1✔
795
   Botan::X509_Certificate end_cert_emsa3 = ca_def.sign_request(end_req, *rng, not_before, not_after);
1✔
796
   test_result.test_eq("End certificate signature algorithm",
3✔
797
                       end_cert_emsa3.signature_algorithm().oid().to_formatted_string(),
2✔
798
                       "RSA/PKCS1v15(SHA-512)");
799

800
   // Create X509 CA object: its signer will use the explicitly configured padding scheme, which is different from the CA certificate's scheme
801
   Botan::X509_CA ca_diff(ca_cert_def, (*sk), "SHA-512", "EMSA-PSS", *rng);
1✔
802
   Botan::X509_Certificate end_cert_diff_emsa4 = ca_diff.sign_request(end_req, *rng, not_before, not_after);
1✔
803
   test_result.test_eq("End certificate signature algorithm",
4✔
804
                       end_cert_diff_emsa4.signature_algorithm().oid().to_formatted_string(),
2✔
805
                       "RSA/PSS");
806

807
   // Create X509 CA object: its signer will use the explicitly configured padding scheme, which is identical to the CA certificate's scheme
808
   Botan::X509_CA ca_exp(ca_cert_exp, (*sk), "SHA-512", "EMSA4(SHA-512,MGF1,64)", *rng);
1✔
809
   Botan::X509_Certificate end_cert_emsa4 = ca_exp.sign_request(end_req, *rng, not_before, not_after);
1✔
810
   test_result.test_eq("End certificate signature algorithm",
3✔
811
                       end_cert_emsa4.signature_algorithm().oid().to_formatted_string(),
2✔
812
                       "RSA/PSS");
813

814
   // Check CRL signature algorithm
815
   Botan::X509_CRL crl = ca_exp.new_crl(*rng);
1✔
816
   test_result.test_eq("CRL signature algorithm", crl.signature_algorithm().oid().to_formatted_string(), "RSA/PSS");
2✔
817

818
   // sanity check for verification, the heavy lifting is done in the other unit tests
819
   const Botan::Certificate_Store_In_Memory trusted(ca_exp.ca_certificate());
1✔
820
   const Botan::Path_Validation_Restrictions restrictions(false, 80);
2✔
821
   const Botan::Path_Validation_Result validation_result =
1✔
822
      Botan::x509_path_validate(end_cert_emsa4, restrictions, trusted);
1✔
823
   test_result.confirm("EMSA4-signed certificate validates", validation_result.successful_validation());
2✔
824

825
   return test_result;
2✔
826
}
3✔
827
      #endif
828

829
   #endif
830

831
Test::Result test_pkcs10_ext(const Botan::Private_Key& key,
12✔
832
                             const std::string& sig_padding,
833
                             const std::string& hash_fn,
834
                             Botan::RandomNumberGenerator& rng) {
835
   Test::Result result("PKCS10 extensions");
12✔
836

837
   Botan::X509_Cert_Options opts;
12✔
838

839
   opts.dns = "main.example.org";
12✔
840
   opts.more_dns.push_back("more1.example.org");
24✔
841
   opts.more_dns.push_back("more2.example.org");
24✔
842

843
   opts.padding_scheme = sig_padding;
12✔
844

845
   Botan::AlternativeName alt_name;
12✔
846
   alt_name.add_attribute("DNS", "bonus.example.org");
12✔
847

848
   Botan::X509_DN alt_dn;
12✔
849
   alt_dn.add_attribute("X520.CommonName", "alt_cn");
12✔
850
   alt_dn.add_attribute("X520.Organization", "testing");
12✔
851
   alt_name.add_dn(alt_dn);
12✔
852

853
   opts.extensions.add(std::make_unique<Botan::Cert_Extension::Subject_Alternative_Name>(alt_name));
24✔
854

855
   const auto req = Botan::X509::create_cert_req(opts, key, hash_fn, rng);
12✔
856

857
   const auto alt_dns_names = req.subject_alt_name().get_attribute("DNS");
12✔
858

859
   result.test_eq("Expected number of DNS names", alt_dns_names.size(), 4);
12✔
860

861
   if(alt_dns_names.size() == 4) {
12✔
862
      result.test_eq("Expected DNS name 1", alt_dns_names.at(0), "bonus.example.org");
36✔
863
      result.test_eq("Expected DNS name 2", alt_dns_names.at(1), "main.example.org");
36✔
864
      result.test_eq("Expected DNS name 3", alt_dns_names.at(2), "more1.example.org");
36✔
865
      result.test_eq("Expected DNS name 3", alt_dns_names.at(3), "more2.example.org");
36✔
866
   }
867

868
   result.test_eq("Expected number of alt DNs", req.subject_alt_name().directory_names().size(), 1);
12✔
869
   result.confirm("Alt DN is correct", *req.subject_alt_name().directory_names().begin() == alt_dn);
24✔
870

871
   return result;
12✔
872
}
12✔
873

874
Test::Result test_x509_cert(const Botan::Private_Key& ca_key,
12✔
875
                            const std::string& sig_algo,
876
                            const std::string& sig_padding,
877
                            const std::string& hash_fn,
878
                            Botan::RandomNumberGenerator& rng) {
879
   Test::Result result("X509 Unit");
12✔
880

881
   /* Create the self-signed cert */
882
   const auto ca_cert = Botan::X509::create_self_signed_cert(ca_opts(sig_padding), ca_key, hash_fn, rng);
12✔
883

884
   {
12✔
885
      result.confirm("ca key usage cert", ca_cert.constraints().includes(Botan::Key_Constraints::KeyCertSign));
24✔
886
      result.confirm("ca key usage crl", ca_cert.constraints().includes(Botan::Key_Constraints::CrlSign));
24✔
887
   }
888

889
   /* Create user #1's key and cert request */
890
   auto user1_key = make_a_private_key(sig_algo, rng);
12✔
891

892
   Botan::PKCS10_Request user1_req =
12✔
893
      Botan::X509::create_cert_req(req_opts1(sig_algo, sig_padding), *user1_key, hash_fn, rng);
12✔
894

895
   result.test_eq("PKCS10 challenge password parsed", user1_req.challenge_password(), "zoom");
24✔
896

897
   /* Create user #2's key and cert request */
898
   auto user2_key = make_a_private_key(sig_algo, rng);
12✔
899

900
   Botan::PKCS10_Request user2_req = Botan::X509::create_cert_req(req_opts2(sig_padding), *user2_key, hash_fn, rng);
12✔
901

902
   // /* Create user #3's key and cert request */
903
   auto user3_key = make_a_private_key(sig_algo, rng);
12✔
904

905
   Botan::PKCS10_Request user3_req = Botan::X509::create_cert_req(req_opts3(sig_padding), *user3_key, hash_fn, rng);
12✔
906

907
   /* Create the CA object */
908
   Botan::X509_CA ca(ca_cert, ca_key, hash_fn, sig_padding, rng);
12✔
909

910
   const BigInt user1_serial = 99;
12✔
911

912
   /* Sign the requests to create the certs */
913
   Botan::X509_Certificate user1_cert =
12✔
914
      ca.sign_request(user1_req, rng, user1_serial, from_date(-1, 01, 01), from_date(2, 01, 01));
12✔
915

916
   result.test_eq("User1 serial size matches expected", user1_cert.serial_number().size(), 1);
12✔
917
   result.test_eq("User1 serial matches expected", user1_cert.serial_number().at(0), size_t(99));
12✔
918

919
   Botan::X509_Certificate user2_cert = ca.sign_request(user2_req, rng, from_date(-1, 01, 01), from_date(2, 01, 01));
12✔
920

921
   Botan::X509_Certificate user3_cert = ca.sign_request(user3_req, rng, from_date(-1, 01, 01), from_date(2, 01, 01));
12✔
922

923
   // user#1 creates a self-signed cert on the side
924
   const auto user1_ss_cert =
12✔
925
      Botan::X509::create_self_signed_cert(req_opts1(sig_algo, sig_padding), *user1_key, hash_fn, rng);
12✔
926

927
   {
12✔
928
      auto constraints = req_opts1(sig_algo).constraints;
12✔
929
      result.confirm("user1 key usage", user1_cert.constraints().includes(constraints));
24✔
930
   }
931

932
   /* Copy, assign and compare */
933
   Botan::X509_Certificate user1_cert_copy(user1_cert);
12✔
934
   result.test_eq("certificate copy", user1_cert == user1_cert_copy, true);
12✔
935

936
   user1_cert_copy = user2_cert;
12✔
937
   result.test_eq("certificate assignment", user2_cert == user1_cert_copy, true);
12✔
938

939
   Botan::X509_Certificate user1_cert_differ =
12✔
940
      ca.sign_request(user1_req, rng, from_date(-1, 01, 01), from_date(2, 01, 01));
12✔
941

942
   result.test_eq("certificate differs", user1_cert == user1_cert_differ, false);
12✔
943

944
   /* Get cert data */
945
   result.test_eq("x509 version", user1_cert.x509_version(), size_t(3));
12✔
946

947
   const Botan::X509_DN& user1_issuer_dn = user1_cert.issuer_dn();
12✔
948
   result.test_eq("issuer info CN", user1_issuer_dn.get_first_attribute("CN"), ca_opts().common_name);
24✔
949
   result.test_eq("issuer info Country", user1_issuer_dn.get_first_attribute("C"), ca_opts().country);
24✔
950
   result.test_eq("issuer info Orga", user1_issuer_dn.get_first_attribute("O"), ca_opts().organization);
24✔
951
   result.test_eq("issuer info OrgaUnit", user1_issuer_dn.get_first_attribute("OU"), ca_opts().org_unit);
24✔
952

953
   const Botan::X509_DN& user3_subject_dn = user3_cert.subject_dn();
12✔
954
   result.test_eq("subject OrgaUnit count",
12✔
955
                  user3_subject_dn.get_attribute("OU").size(),
24✔
956
                  req_opts3(sig_algo).more_org_units.size() + 1);
24✔
957
   result.test_eq(
12✔
958
      "subject OrgaUnit #2", user3_subject_dn.get_attribute("OU").at(1), req_opts3(sig_algo).more_org_units.at(0));
48✔
959

960
   const Botan::AlternativeName& user1_altname = user1_cert.subject_alt_name();
12✔
961
   result.test_eq("subject alt email", user1_altname.get_first_attribute("RFC822"), "testing@randombit.net");
24✔
962
   result.test_eq("subject alt dns", user1_altname.get_first_attribute("DNS"), "botan.randombit.net");
24✔
963
   result.test_eq("subject alt uri", user1_altname.get_first_attribute("URI"), "https://botan.randombit.net");
24✔
964

965
   const Botan::AlternativeName& user3_altname = user3_cert.subject_alt_name();
12✔
966
   result.test_eq(
12✔
967
      "subject alt dns count", user3_altname.get_attribute("DNS").size(), req_opts3(sig_algo).more_dns.size() + 1);
24✔
968
   result.test_eq("subject alt dns #2", user3_altname.get_attribute("DNS").at(1), req_opts3(sig_algo).more_dns.at(0));
48✔
969

970
   const Botan::X509_CRL crl1 = ca.new_crl(rng);
12✔
971

972
   /* Verify the certs */
973
   Botan::Path_Validation_Restrictions restrictions(false, 80);
24✔
974
   Botan::Certificate_Store_In_Memory store;
12✔
975

976
   // First try with an empty store
977
   Botan::Path_Validation_Result result_no_issuer = Botan::x509_path_validate(user1_cert, restrictions, store);
12✔
978
   result.test_eq("user 1 issuer not found",
36✔
979
                  result_no_issuer.result_string(),
24✔
980
                  Botan::Path_Validation_Result::status_string(Botan::Certificate_Status_Code::CERT_ISSUER_NOT_FOUND));
981

982
   store.add_certificate(ca.ca_certificate());
12✔
983

984
   Botan::Path_Validation_Result result_u1 = Botan::x509_path_validate(user1_cert, restrictions, store);
12✔
985
   if(!result.confirm("user 1 validates", result_u1.successful_validation())) {
24✔
986
      result.test_note("user 1 validation result was " + result_u1.result_string());
×
987
   }
988

989
   Botan::Path_Validation_Result result_u2 = Botan::x509_path_validate(user2_cert, restrictions, store);
12✔
990
   if(!result.confirm("user 2 validates", result_u2.successful_validation())) {
24✔
991
      result.test_note("user 2 validation result was " + result_u2.result_string());
×
992
   }
993

994
   Botan::Path_Validation_Result result_self_signed = Botan::x509_path_validate(user1_ss_cert, restrictions, store);
12✔
995
   result.test_eq("user 1 issuer not found",
36✔
996
                  result_no_issuer.result_string(),
24✔
997
                  Botan::Path_Validation_Result::status_string(Botan::Certificate_Status_Code::CERT_ISSUER_NOT_FOUND));
998
   store.add_crl(crl1);
12✔
999

1000
   std::vector<Botan::CRL_Entry> revoked;
12✔
1001
   revoked.push_back(Botan::CRL_Entry(user1_cert, Botan::CRL_Code::CessationOfOperation));
24✔
1002
   revoked.push_back(user2_cert);
24✔
1003

1004
   const Botan::X509_CRL crl2 = ca.update_crl(crl1, revoked, rng);
12✔
1005

1006
   store.add_crl(crl2);
12✔
1007

1008
   const std::string revoked_str =
12✔
1009
      Botan::Path_Validation_Result::status_string(Botan::Certificate_Status_Code::CERT_IS_REVOKED);
12✔
1010

1011
   result_u1 = Botan::x509_path_validate(user1_cert, restrictions, store);
12✔
1012
   result.test_eq("user 1 revoked", result_u1.result_string(), revoked_str);
24✔
1013

1014
   result_u2 = Botan::x509_path_validate(user2_cert, restrictions, store);
12✔
1015
   result.test_eq("user 1 revoked", result_u2.result_string(), revoked_str);
24✔
1016

1017
   revoked.clear();
12✔
1018
   revoked.push_back(Botan::CRL_Entry(user1_cert, Botan::CRL_Code::RemoveFromCrl));
24✔
1019
   Botan::X509_CRL crl3 = ca.update_crl(crl2, revoked, rng);
12✔
1020

1021
   store.add_crl(crl3);
12✔
1022

1023
   result_u1 = Botan::x509_path_validate(user1_cert, restrictions, store);
12✔
1024
   if(!result.confirm("user 1 validates", result_u1.successful_validation())) {
24✔
1025
      result.test_note("user 1 validation result was " + result_u1.result_string());
×
1026
   }
1027

1028
   result_u2 = Botan::x509_path_validate(user2_cert, restrictions, store);
12✔
1029
   result.test_eq("user 2 still revoked", result_u2.result_string(), revoked_str);
24✔
1030

1031
   return result;
12✔
1032
}
60✔
1033

1034
Test::Result test_usage(const Botan::Private_Key& ca_key,
12✔
1035
                        const std::string& sig_algo,
1036
                        const std::string& hash_fn,
1037
                        Botan::RandomNumberGenerator& rng) {
1038
   using Botan::Key_Constraints;
12✔
1039
   using Botan::Usage_Type;
12✔
1040

1041
   Test::Result result("X509 Usage");
12✔
1042

1043
   /* Create the self-signed cert */
1044
   const Botan::X509_Certificate ca_cert = Botan::X509::create_self_signed_cert(ca_opts(), ca_key, hash_fn, rng);
12✔
1045

1046
   /* Create the CA object */
1047
   const Botan::X509_CA ca(ca_cert, ca_key, hash_fn, rng);
12✔
1048

1049
   auto user1_key = make_a_private_key(sig_algo, rng);
12✔
1050

1051
   Botan::X509_Cert_Options opts("Test User 1/US/Botan Project/Testing");
12✔
1052
   opts.constraints = Key_Constraints::DigitalSignature;
12✔
1053

1054
   const Botan::PKCS10_Request user1_req = Botan::X509::create_cert_req(opts, *user1_key, hash_fn, rng);
12✔
1055

1056
   const Botan::X509_Certificate user1_cert =
12✔
1057
      ca.sign_request(user1_req, rng, from_date(-1, 01, 01), from_date(2, 01, 01));
12✔
1058

1059
   // cert only allows digitalSignature, but we check for both digitalSignature and cRLSign
1060
   result.test_eq(
12✔
1061
      "key usage cRLSign not allowed",
1062
      user1_cert.allowed_usage(Key_Constraints(Key_Constraints::DigitalSignature | Key_Constraints::CrlSign)),
12✔
1063
      false);
1064
   result.test_eq("encryption is not allowed", user1_cert.allowed_usage(Usage_Type::ENCRYPTION), false);
12✔
1065

1066
   // cert only allows digitalSignature, so checking for only that should be ok
1067
   result.confirm("key usage digitalSignature allowed", user1_cert.allowed_usage(Key_Constraints::DigitalSignature));
24✔
1068

1069
   opts.constraints = Key_Constraints(Key_Constraints::DigitalSignature | Key_Constraints::CrlSign);
12✔
1070

1071
   const Botan::PKCS10_Request mult_usage_req = Botan::X509::create_cert_req(opts, *user1_key, hash_fn, rng);
12✔
1072

1073
   const Botan::X509_Certificate mult_usage_cert =
12✔
1074
      ca.sign_request(mult_usage_req, rng, from_date(-1, 01, 01), from_date(2, 01, 01));
12✔
1075

1076
   // cert allows multiple usages, so each one of them as well as both together should be allowed
1077
   result.confirm("key usage multiple digitalSignature allowed",
24✔
1078
                  mult_usage_cert.allowed_usage(Key_Constraints::DigitalSignature));
12✔
1079
   result.confirm("key usage multiple cRLSign allowed", mult_usage_cert.allowed_usage(Key_Constraints::CrlSign));
24✔
1080
   result.confirm(
24✔
1081
      "key usage multiple digitalSignature and cRLSign allowed",
1082
      mult_usage_cert.allowed_usage(Key_Constraints(Key_Constraints::DigitalSignature | Key_Constraints::CrlSign)));
12✔
1083
   result.test_eq("encryption is not allowed", mult_usage_cert.allowed_usage(Usage_Type::ENCRYPTION), false);
12✔
1084

1085
   opts.constraints = Key_Constraints();
12✔
1086

1087
   const Botan::PKCS10_Request no_usage_req = Botan::X509::create_cert_req(opts, *user1_key, hash_fn, rng);
12✔
1088

1089
   const Botan::X509_Certificate no_usage_cert =
12✔
1090
      ca.sign_request(no_usage_req, rng, from_date(-1, 01, 01), from_date(2, 01, 01));
12✔
1091

1092
   // cert allows every usage
1093
   result.confirm("key usage digitalSignature allowed", no_usage_cert.allowed_usage(Key_Constraints::DigitalSignature));
24✔
1094
   result.confirm("key usage cRLSign allowed", no_usage_cert.allowed_usage(Key_Constraints::CrlSign));
24✔
1095
   result.confirm("key usage encryption allowed", no_usage_cert.allowed_usage(Usage_Type::ENCRYPTION));
24✔
1096

1097
   if(sig_algo == "RSA") {
12✔
1098
      // cert allows data encryption
1099
      opts.constraints = Key_Constraints(Key_Constraints::KeyEncipherment | Key_Constraints::DataEncipherment);
1✔
1100

1101
      const Botan::PKCS10_Request enc_req = Botan::X509::create_cert_req(opts, *user1_key, hash_fn, rng);
1✔
1102

1103
      const Botan::X509_Certificate enc_cert =
1✔
1104
         ca.sign_request(enc_req, rng, from_date(-1, 01, 01), from_date(2, 01, 01));
1✔
1105

1106
      result.confirm("cert allows encryption", enc_cert.allowed_usage(Usage_Type::ENCRYPTION));
2✔
1107
      result.confirm("cert does not allow TLS client auth", !enc_cert.allowed_usage(Usage_Type::TLS_CLIENT_AUTH));
2✔
1108
   }
1✔
1109

1110
   return result;
12✔
1111
}
24✔
1112

1113
Test::Result test_self_issued(const Botan::Private_Key& ca_key,
12✔
1114
                              const std::string& sig_algo,
1115
                              const std::string& sig_padding,
1116
                              const std::string& hash_fn,
1117
                              Botan::RandomNumberGenerator& rng) {
1118
   using Botan::Key_Constraints;
12✔
1119

1120
   Test::Result result("X509 Self Issued");
12✔
1121

1122
   // create the self-signed cert
1123
   const Botan::X509_Certificate ca_cert =
12✔
1124
      Botan::X509::create_self_signed_cert(ca_opts(sig_padding), ca_key, hash_fn, rng);
12✔
1125

1126
   /* Create the CA object */
1127
   const Botan::X509_CA ca(ca_cert, ca_key, hash_fn, sig_padding, rng);
12✔
1128

1129
   auto user_key = make_a_private_key(sig_algo, rng);
12✔
1130

1131
   // create a self-issued certificate, that is, a certificate with subject dn == issuer dn,
1132
   // but signed by a CA, not signed by it's own private key
1133
   Botan::X509_Cert_Options opts = ca_opts();
12✔
1134
   opts.constraints = Key_Constraints::DigitalSignature;
12✔
1135
   opts.set_padding_scheme(sig_padding);
12✔
1136

1137
   const Botan::PKCS10_Request self_issued_req = Botan::X509::create_cert_req(opts, *user_key, hash_fn, rng);
12✔
1138

1139
   const Botan::X509_Certificate self_issued_cert =
12✔
1140
      ca.sign_request(self_issued_req, rng, from_date(-1, 01, 01), from_date(2, 01, 01));
12✔
1141

1142
   // check that this chain can can be verified successfully
1143
   const Botan::Certificate_Store_In_Memory trusted(ca.ca_certificate());
12✔
1144

1145
   const Botan::Path_Validation_Restrictions restrictions(false, 80);
24✔
1146

1147
   const Botan::Path_Validation_Result validation_result =
12✔
1148
      Botan::x509_path_validate(self_issued_cert, restrictions, trusted);
12✔
1149

1150
   result.confirm("chain with self-issued cert validates", validation_result.successful_validation());
24✔
1151

1152
   return result;
12✔
1153
}
24✔
1154

1155
Test::Result test_x509_uninit() {
1✔
1156
   Test::Result result("X509 object uninitialized access");
1✔
1157

1158
   Botan::X509_Certificate cert;
1✔
1159
   result.test_throws("uninitialized cert access causes exception", "X509_Certificate uninitialized", [&cert]() {
2✔
1160
      cert.x509_version();
1✔
1161
   });
1162

1163
   Botan::X509_CRL crl;
1✔
1164
   result.test_throws(
2✔
1165
      "uninitialized crl access causes exception", "X509_CRL uninitialized", [&crl]() { crl.crl_number(); });
2✔
1166

1167
   return result;
1✔
1168
}
1✔
1169

1170
Test::Result test_valid_constraints(const Botan::Private_Key& key, const std::string& pk_algo) {
19✔
1171
   using Botan::Key_Constraints;
19✔
1172

1173
   Test::Result result("X509 Valid Constraints " + pk_algo);
19✔
1174

1175
   result.confirm("empty constraints always acceptable", Key_Constraints().compatible_with(key));
38✔
1176

1177
   // Now check some typical usage scenarios for the given key type
1178
   // Taken from RFC 5280, sec. 4.2.1.3
1179
   // ALL constraints are not typical at all, but we use them for a negative test
1180
   const auto all = Key_Constraints(
19✔
1181
      Key_Constraints::DigitalSignature | Key_Constraints::NonRepudiation | Key_Constraints::KeyEncipherment |
1182
      Key_Constraints::DataEncipherment | Key_Constraints::KeyAgreement | Key_Constraints::KeyCertSign |
1183
      Key_Constraints::CrlSign | Key_Constraints::EncipherOnly | Key_Constraints::DecipherOnly);
19✔
1184

1185
   const auto ca = Key_Constraints(Key_Constraints::KeyCertSign);
19✔
1186
   const auto sign_data = Key_Constraints(Key_Constraints::DigitalSignature);
19✔
1187
   const auto non_repudiation = Key_Constraints(Key_Constraints::NonRepudiation | Key_Constraints::DigitalSignature);
19✔
1188
   const auto key_encipherment = Key_Constraints(Key_Constraints::KeyEncipherment);
19✔
1189
   const auto data_encipherment = Key_Constraints(Key_Constraints::DataEncipherment);
19✔
1190
   const auto key_agreement = Key_Constraints(Key_Constraints::KeyAgreement);
19✔
1191
   const auto key_agreement_encipher_only =
19✔
1192
      Key_Constraints(Key_Constraints::KeyAgreement | Key_Constraints::EncipherOnly);
19✔
1193
   const auto key_agreement_decipher_only =
19✔
1194
      Key_Constraints(Key_Constraints::KeyAgreement | Key_Constraints::DecipherOnly);
19✔
1195
   const auto crl_sign = Key_Constraints(Key_Constraints::CrlSign);
19✔
1196
   const auto sign_everything =
19✔
1197
      Key_Constraints(Key_Constraints::DigitalSignature | Key_Constraints::KeyCertSign | Key_Constraints::CrlSign);
19✔
1198

1199
   if(pk_algo == "DH" || pk_algo == "ECDH") {
19✔
1200
      // DH and ECDH only for key agreement
1201
      result.test_eq("all constraints not permitted", all.compatible_with(key), false);
2✔
1202
      result.test_eq("cert sign not permitted", ca.compatible_with(key), false);
2✔
1203
      result.test_eq("signature not permitted", sign_data.compatible_with(key), false);
2✔
1204
      result.test_eq("non repudiation not permitted", non_repudiation.compatible_with(key), false);
2✔
1205
      result.test_eq("key encipherment not permitted", key_encipherment.compatible_with(key), false);
2✔
1206
      result.test_eq("data encipherment not permitted", data_encipherment.compatible_with(key), false);
2✔
1207
      result.test_eq("usage acceptable", key_agreement.compatible_with(key), true);
2✔
1208
      result.test_eq("usage acceptable", key_agreement_encipher_only.compatible_with(key), true);
2✔
1209
      result.test_eq("usage acceptable", key_agreement_decipher_only.compatible_with(key), true);
2✔
1210
      result.test_eq("crl sign not permitted", crl_sign.compatible_with(key), false);
2✔
1211
      result.test_eq("sign", sign_everything.compatible_with(key), false);
4✔
1212
   } else if(pk_algo == "Kyber" || pk_algo == "FrodoKEM" || pk_algo == "ML-KEM" || pk_algo == "ClassicMcEliece") {
17✔
1213
      // KEMs can encrypt and agree
1214
      result.test_eq("all constraints not permitted", all.compatible_with(key), false);
4✔
1215
      result.test_eq("cert sign not permitted", ca.compatible_with(key), false);
4✔
1216
      result.test_eq("signature not permitted", sign_data.compatible_with(key), false);
4✔
1217
      result.test_eq("non repudiation not permitted", non_repudiation.compatible_with(key), false);
4✔
1218
      result.test_eq("crl sign not permitted", crl_sign.compatible_with(key), false);
4✔
1219
      result.test_eq("sign", sign_everything.compatible_with(key), false);
4✔
1220
      result.test_eq("key agreement not permitted", key_agreement.compatible_with(key), false);
4✔
1221
      result.test_eq("usage acceptable", data_encipherment.compatible_with(key), false);
4✔
1222
      result.test_eq("usage acceptable", key_encipherment.compatible_with(key), true);
8✔
1223
   } else if(pk_algo == "RSA") {
13✔
1224
      // RSA can do everything except key agreement
1225
      result.test_eq("all constraints not permitted", all.compatible_with(key), false);
1✔
1226

1227
      result.test_eq("usage acceptable", ca.compatible_with(key), true);
1✔
1228
      result.test_eq("usage acceptable", sign_data.compatible_with(key), true);
1✔
1229
      result.test_eq("usage acceptable", non_repudiation.compatible_with(key), true);
1✔
1230
      result.test_eq("usage acceptable", key_encipherment.compatible_with(key), true);
1✔
1231
      result.test_eq("usage acceptable", data_encipherment.compatible_with(key), true);
1✔
1232
      result.test_eq("key agreement not permitted", key_agreement.compatible_with(key), false);
1✔
1233
      result.test_eq("key agreement", key_agreement_encipher_only.compatible_with(key), false);
1✔
1234
      result.test_eq("key agreement", key_agreement_decipher_only.compatible_with(key), false);
1✔
1235
      result.test_eq("usage acceptable", crl_sign.compatible_with(key), true);
1✔
1236
      result.test_eq("usage acceptable", sign_everything.compatible_with(key), true);
2✔
1237
   } else if(pk_algo == "ElGamal") {
12✔
1238
      // only ElGamal encryption is currently implemented
1239
      result.test_eq("all constraints not permitted", all.compatible_with(key), false);
1✔
1240
      result.test_eq("cert sign not permitted", ca.compatible_with(key), false);
1✔
1241
      result.test_eq("data encipherment permitted", data_encipherment.compatible_with(key), true);
1✔
1242
      result.test_eq("key encipherment permitted", key_encipherment.compatible_with(key), true);
1✔
1243
      result.test_eq("key agreement not permitted", key_agreement.compatible_with(key), false);
1✔
1244
      result.test_eq("key agreement", key_agreement_encipher_only.compatible_with(key), false);
1✔
1245
      result.test_eq("key agreement", key_agreement_decipher_only.compatible_with(key), false);
1✔
1246
      result.test_eq("crl sign not permitted", crl_sign.compatible_with(key), false);
1✔
1247
      result.test_eq("sign", sign_everything.compatible_with(key), false);
2✔
1248
   } else if(pk_algo == "DSA" || pk_algo == "ECDSA" || pk_algo == "ECGDSA" || pk_algo == "ECKCDSA" ||
10✔
1249
             pk_algo == "GOST-34.10" || pk_algo == "Dilithium" || pk_algo == "ML-DSA" || pk_algo == "SLH-DSA" ||
18✔
1250
             pk_algo == "HSS-LMS") {
3✔
1251
      // these are signature algorithms only
1252
      result.test_eq("all constraints not permitted", all.compatible_with(key), false);
9✔
1253

1254
      result.test_eq("ca allowed", ca.compatible_with(key), true);
9✔
1255
      result.test_eq("sign allowed", sign_data.compatible_with(key), true);
9✔
1256
      result.test_eq("non-repudiation allowed", non_repudiation.compatible_with(key), true);
9✔
1257
      result.test_eq("key encipherment not permitted", key_encipherment.compatible_with(key), false);
9✔
1258
      result.test_eq("data encipherment not permitted", data_encipherment.compatible_with(key), false);
9✔
1259
      result.test_eq("key agreement not permitted", key_agreement.compatible_with(key), false);
9✔
1260
      result.test_eq("key agreement", key_agreement_encipher_only.compatible_with(key), false);
9✔
1261
      result.test_eq("key agreement", key_agreement_decipher_only.compatible_with(key), false);
9✔
1262
      result.test_eq("crl sign allowed", crl_sign.compatible_with(key), true);
9✔
1263
      result.test_eq("sign allowed", sign_everything.compatible_with(key), true);
18✔
1264
   }
1265

1266
   return result;
19✔
1267
}
×
1268

1269
/**
1270
 * @brief X.509v3 extension that encodes a given string
1271
 */
1272
class String_Extension final : public Botan::Certificate_Extension {
24✔
1273
   public:
1274
      String_Extension() = default;
24✔
1275

1276
      explicit String_Extension(const std::string& val) : m_contents(val) {}
12✔
1277

1278
      std::string value() const { return m_contents; }
48✔
1279

1280
      std::unique_ptr<Certificate_Extension> copy() const override {
×
1281
         return std::make_unique<String_Extension>(m_contents);
×
1282
      }
1283

1284
      Botan::OID oid_of() const override { return Botan::OID("1.2.3.4.5.6.7.8.9.1"); }
24✔
1285

1286
      bool should_encode() const override { return true; }
24✔
1287

1288
      std::string oid_name() const override { return "String Extension"; }
×
1289

1290
      std::vector<uint8_t> encode_inner() const override {
12✔
1291
         std::vector<uint8_t> bits;
12✔
1292
         Botan::DER_Encoder(bits).encode(Botan::ASN1_String(m_contents, Botan::ASN1_Type::Utf8String));
12✔
1293
         return bits;
12✔
1294
      }
×
1295

1296
      void decode_inner(const std::vector<uint8_t>& in) override {
24✔
1297
         Botan::ASN1_String str;
24✔
1298
         Botan::BER_Decoder(in).decode(str, Botan::ASN1_Type::Utf8String).verify_end();
24✔
1299
         m_contents = str.value();
24✔
1300
      }
24✔
1301

1302
   private:
1303
      std::string m_contents;
1304
};
1305

1306
Test::Result test_custom_dn_attr(const Botan::Private_Key& ca_key,
12✔
1307
                                 const std::string& sig_algo,
1308
                                 const std::string& sig_padding,
1309
                                 const std::string& hash_fn,
1310
                                 Botan::RandomNumberGenerator& rng) {
1311
   Test::Result result("X509 Custom DN");
12✔
1312

1313
   /* Create the self-signed cert */
1314
   Botan::X509_Certificate ca_cert = Botan::X509::create_self_signed_cert(ca_opts(sig_padding), ca_key, hash_fn, rng);
12✔
1315

1316
   /* Create the CA object */
1317
   Botan::X509_CA ca(ca_cert, ca_key, hash_fn, sig_padding, rng);
12✔
1318

1319
   auto user_key = make_a_private_key(sig_algo, rng);
12✔
1320

1321
   Botan::X509_DN subject_dn;
12✔
1322

1323
   const Botan::OID attr1(Botan::OID("1.3.6.1.4.1.25258.9.1.1"));
12✔
1324
   const Botan::OID attr2(Botan::OID("1.3.6.1.4.1.25258.9.1.2"));
12✔
1325
   const Botan::ASN1_String val1("Custom Attr 1", Botan::ASN1_Type::PrintableString);
12✔
1326
   const Botan::ASN1_String val2("12345", Botan::ASN1_Type::Utf8String);
12✔
1327

1328
   subject_dn.add_attribute(attr1, val1);
12✔
1329
   subject_dn.add_attribute(attr2, val2);
12✔
1330

1331
   Botan::Extensions extensions;
12✔
1332

1333
   Botan::PKCS10_Request req =
12✔
1334
      Botan::PKCS10_Request::create(*user_key, subject_dn, extensions, hash_fn, rng, sig_padding);
12✔
1335

1336
   const Botan::X509_DN& req_dn = req.subject_dn();
12✔
1337

1338
   result.test_eq("Expected number of DN entries", req_dn.dn_info().size(), 2);
12✔
1339

1340
   Botan::ASN1_String req_val1 = req_dn.get_first_attribute(attr1);
12✔
1341
   Botan::ASN1_String req_val2 = req_dn.get_first_attribute(attr2);
12✔
1342
   result.confirm("Attr1 matches encoded", req_val1 == val1);
24✔
1343
   result.confirm("Attr2 matches encoded", req_val2 == val2);
24✔
1344
   result.confirm("Attr1 tag matches encoded", req_val1.tagging() == val1.tagging());
24✔
1345
   result.confirm("Attr2 tag matches encoded", req_val2.tagging() == val2.tagging());
24✔
1346

1347
   Botan::X509_Time not_before("100301123001Z", Botan::ASN1_Type::UtcTime);
12✔
1348
   Botan::X509_Time not_after("300301123001Z", Botan::ASN1_Type::UtcTime);
12✔
1349

1350
   auto cert = ca.sign_request(req, rng, not_before, not_after);
12✔
1351

1352
   const Botan::X509_DN& cert_dn = cert.subject_dn();
12✔
1353

1354
   result.test_eq("Expected number of DN entries", cert_dn.dn_info().size(), 2);
12✔
1355

1356
   Botan::ASN1_String cert_val1 = cert_dn.get_first_attribute(attr1);
12✔
1357
   Botan::ASN1_String cert_val2 = cert_dn.get_first_attribute(attr2);
12✔
1358
   result.confirm("Attr1 matches encoded", cert_val1 == val1);
24✔
1359
   result.confirm("Attr2 matches encoded", cert_val2 == val2);
24✔
1360
   result.confirm("Attr1 tag matches encoded", cert_val1.tagging() == val1.tagging());
24✔
1361
   result.confirm("Attr2 tag matches encoded", cert_val2.tagging() == val2.tagging());
24✔
1362

1363
   return result;
12✔
1364
}
36✔
1365

1366
Test::Result test_x509_extensions(const Botan::Private_Key& ca_key,
12✔
1367
                                  const std::string& sig_algo,
1368
                                  const std::string& sig_padding,
1369
                                  const std::string& hash_fn,
1370
                                  Botan::RandomNumberGenerator& rng) {
1371
   using Botan::Key_Constraints;
12✔
1372

1373
   Test::Result result("X509 Extensions");
12✔
1374

1375
   /* Create the self-signed cert */
1376
   Botan::X509_Certificate ca_cert = Botan::X509::create_self_signed_cert(ca_opts(sig_padding), ca_key, hash_fn, rng);
12✔
1377

1378
   /* Create the CA object */
1379
   Botan::X509_CA ca(ca_cert, ca_key, hash_fn, sig_padding, rng);
12✔
1380

1381
   /* Prepare CDP extension */
1382
   std::vector<std::string> cdp_urls = {
12✔
1383
      "http://example.com/crl1.pem",
1384
      "ldap://ldap.example.com/cn=crl1,dc=example,dc=com?certificateRevocationList;binary"};
12✔
1385

1386
   std::vector<Botan::Cert_Extension::CRL_Distribution_Points::Distribution_Point> dps;
12✔
1387

1388
   for(const auto& uri : cdp_urls) {
36✔
1389
      Botan::AlternativeName cdp_alt_name;
24✔
1390
      cdp_alt_name.add_uri(uri);
24✔
1391
      Botan::Cert_Extension::CRL_Distribution_Points::Distribution_Point dp(cdp_alt_name);
24✔
1392

1393
      dps.emplace_back(dp);
24✔
1394
   }
24✔
1395

1396
   auto user_key = make_a_private_key(sig_algo, rng);
12✔
1397

1398
   Botan::X509_Cert_Options opts("Test User 1/US/Botan Project/Testing");
12✔
1399
   opts.constraints = Key_Constraints::DigitalSignature;
12✔
1400

1401
   // include a custom extension in the request
1402
   Botan::Extensions req_extensions;
12✔
1403
   const Botan::OID oid("1.2.3.4.5.6.7.8.9.1");
12✔
1404
   const Botan::OID ku_oid = Botan::OID::from_string("X509v3.KeyUsage");
12✔
1405
   req_extensions.add(std::make_unique<String_Extension>("AAAAAAAAAAAAAABCDEF"), false);
24✔
1406
   req_extensions.add(std::make_unique<Botan::Cert_Extension::CRL_Distribution_Points>(dps));
24✔
1407
   opts.extensions = req_extensions;
12✔
1408
   opts.set_padding_scheme(sig_padding);
12✔
1409

1410
   /* Create a self-signed certificate */
1411
   const Botan::X509_Certificate self_signed_cert = Botan::X509::create_self_signed_cert(opts, *user_key, hash_fn, rng);
12✔
1412

1413
   result.confirm("Extensions::extension_set true for Key_Usage",
24✔
1414
                  self_signed_cert.v3_extensions().extension_set(ku_oid));
12✔
1415

1416
   // check if known Key_Usage extension is present in self-signed cert
1417
   auto key_usage_ext = self_signed_cert.v3_extensions().get(ku_oid);
12✔
1418
   if(result.confirm("Key_Usage extension present in self-signed certificate", key_usage_ext != nullptr)) {
24✔
1419
      result.confirm(
24✔
1420
         "Key_Usage extension value matches in self-signed certificate",
1421
         dynamic_cast<Botan::Cert_Extension::Key_Usage&>(*key_usage_ext).get_constraints() == opts.constraints);
12✔
1422
   }
1423

1424
   // check if custom extension is present in self-signed cert
1425
   auto string_ext = self_signed_cert.v3_extensions().get_raw<String_Extension>(oid);
12✔
1426
   if(result.confirm("Custom extension present in self-signed certificate", string_ext != nullptr)) {
24✔
1427
      result.test_eq(
48✔
1428
         "Custom extension value matches in self-signed certificate", string_ext->value(), "AAAAAAAAAAAAAABCDEF");
36✔
1429
   }
1430

1431
   // check if CDPs are present in the self-signed cert
1432
   auto cert_cdps =
12✔
1433
      self_signed_cert.v3_extensions().get_extension_object_as<Botan::Cert_Extension::CRL_Distribution_Points>();
24✔
1434

1435
   if(result.confirm("CRL Distribution Points extension present in self-signed certificate",
24✔
1436
                     !cert_cdps->crl_distribution_urls().empty())) {
12✔
1437
      for(const auto& cdp : cert_cdps->distribution_points()) {
36✔
1438
         result.confirm("CDP URI present in self-signed certificate",
48✔
1439
                        std::ranges::find(cdp_urls, cdp.point().get_first_attribute("URI")) != cdp_urls.end());
72✔
1440
      }
1441
   }
1442

1443
   const Botan::PKCS10_Request user_req = Botan::X509::create_cert_req(opts, *user_key, hash_fn, rng);
12✔
1444

1445
   /* Create a CA-signed certificate */
1446
   const Botan::X509_Certificate ca_signed_cert =
12✔
1447
      ca.sign_request(user_req, rng, from_date(-1, 01, 01), from_date(2, 01, 01));
12✔
1448

1449
   // check if known Key_Usage extension is present in CA-signed cert
1450
   result.confirm("Extensions::extension_set true for Key_Usage", ca_signed_cert.v3_extensions().extension_set(ku_oid));
24✔
1451

1452
   key_usage_ext = ca_signed_cert.v3_extensions().get(ku_oid);
24✔
1453
   if(result.confirm("Key_Usage extension present in CA-signed certificate", key_usage_ext != nullptr)) {
24✔
1454
      auto constraints = dynamic_cast<Botan::Cert_Extension::Key_Usage&>(*key_usage_ext).get_constraints();
12✔
1455
      result.confirm("Key_Usage extension value matches in user certificate",
24✔
1456
                     constraints == Botan::Key_Constraints::DigitalSignature);
12✔
1457
   }
1458

1459
   // check if custom extension is present in CA-signed cert
1460
   result.confirm("Extensions::extension_set true for String_Extension",
24✔
1461
                  ca_signed_cert.v3_extensions().extension_set(oid));
12✔
1462
   string_ext = ca_signed_cert.v3_extensions().get_raw<String_Extension>(oid);
24✔
1463
   if(result.confirm("Custom extension present in CA-signed certificate", string_ext != nullptr)) {
24✔
1464
      result.test_eq(
48✔
1465
         "Custom extension value matches in CA-signed certificate", string_ext->value(), "AAAAAAAAAAAAAABCDEF");
36✔
1466
   }
1467

1468
   // check if CDPs are present in the CA-signed cert
1469
   cert_cdps = ca_signed_cert.v3_extensions().get_extension_object_as<Botan::Cert_Extension::CRL_Distribution_Points>();
24✔
1470

1471
   if(result.confirm("CRL Distribution Points extension present in self-signed certificate",
24✔
1472
                     !cert_cdps->crl_distribution_urls().empty())) {
12✔
1473
      for(const auto& cdp : cert_cdps->distribution_points()) {
36✔
1474
         result.confirm("CDP URI present in self-signed certificate",
48✔
1475
                        std::ranges::find(cdp_urls, cdp.point().get_first_attribute("URI")) != cdp_urls.end());
72✔
1476
      }
1477
   }
1478

1479
   return result;
12✔
1480
}
60✔
1481

1482
Test::Result test_hashes(const Botan::Private_Key& key, const std::string& hash_fn, Botan::RandomNumberGenerator& rng) {
12✔
1483
   Test::Result result("X509 Hashes");
12✔
1484

1485
   struct TestData {
12✔
1486
         const std::string issuer, subject, issuer_hash, subject_hash;
1487
   } const cases[]{{"",
12✔
1488
                    "",
1489
                    "E4F60D0AA6D7F3D3B6A6494B1C861B99F649C6F9EC51ABAF201B20F297327C95",
1490
                    "E4F60D0AA6D7F3D3B6A6494B1C861B99F649C6F9EC51ABAF201B20F297327C95"},
1491
                   {"a",
1492
                    "b",
1493
                    "BC2E013472F39AC579964880E422737C82BA812CB8BC2FD17E013060D71E6E19",
1494
                    "5E31CFAA3FAFB1A5BA296A0D2BAB9CA44D7936E9BF0BBC54637D0C53DBC4A432"},
1495
                   {"A",
1496
                    "B",
1497
                    "4B3206201C4BC9B6CD6C36532A97687DF9238155D99ADB60C66BF2B2220643D8",
1498
                    "FFF635A52A16618B4A0E9CD26B5E5A2FA573D343C051E6DE8B0811B1ACC89B86"},
1499
                   {
1500
                      "Test Issuer/US/Botan Project/Testing",
1501
                      "Test Subject/US/Botan Project/Testing",
1502
                      "ACB4F373004A56A983A23EB8F60FA4706312B5DB90FD978574FE7ACC84E093A5",
1503
                      "87039231C2205B74B6F1F3830A66272C0B41F71894B03AC3150221766D95267B",
1504
                   },
1505
                   {
1506
                      "Test Subject/US/Botan Project/Testing",
1507
                      "Test Issuer/US/Botan Project/Testing",
1508
                      "87039231C2205B74B6F1F3830A66272C0B41F71894B03AC3150221766D95267B",
1509
                      "ACB4F373004A56A983A23EB8F60FA4706312B5DB90FD978574FE7ACC84E093A5",
1510
                   }};
72✔
1511

1512
   for(const auto& a : cases) {
72✔
1513
      Botan::X509_Cert_Options opts{a.issuer};
60✔
1514
      opts.CA_key();
60✔
1515

1516
      const Botan::X509_Certificate issuer_cert = Botan::X509::create_self_signed_cert(opts, key, hash_fn, rng);
60✔
1517

1518
      result.test_eq(a.issuer, Botan::hex_encode(issuer_cert.raw_issuer_dn_sha256()), a.issuer_hash);
120✔
1519
      result.test_eq(a.issuer, Botan::hex_encode(issuer_cert.raw_subject_dn_sha256()), a.issuer_hash);
120✔
1520

1521
      const Botan::X509_CA ca(issuer_cert, key, hash_fn, rng);
60✔
1522
      const Botan::PKCS10_Request req =
60✔
1523
         Botan::X509::create_cert_req(Botan::X509_Cert_Options(a.subject), key, hash_fn, rng);
60✔
1524
      const Botan::X509_Certificate subject_cert =
60✔
1525
         ca.sign_request(req, rng, from_date(-1, 01, 01), from_date(2, 01, 01));
60✔
1526

1527
      result.test_eq(a.subject, Botan::hex_encode(subject_cert.raw_issuer_dn_sha256()), a.issuer_hash);
120✔
1528
      result.test_eq(a.subject, Botan::hex_encode(subject_cert.raw_subject_dn_sha256()), a.subject_hash);
120✔
1529
   }
60✔
1530
   return result;
12✔
1531
}
72✔
1532

1533
   #if defined(BOTAN_TARGET_OS_HAS_FILESYSTEM)
1534

1535
Test::Result test_x509_tn_auth_list_extension_decode() {
1✔
1536
   /* cert with TNAuthList extension data was generated by asn1parse cfg:
1537

1538
      asn1=SEQUENCE:tn_auth_list
1539

1540
      [tn_auth_list]
1541
      spc=EXP:0,IA5:1001
1542
      range=EXP:1,SEQUENCE:TelephoneNumberRange
1543
      one=EXP:2,IA5:333
1544

1545
      [TelephoneNumberRange]
1546
      start1=IA5:111
1547
      count1=INT:128
1548
      start2=IA5:222
1549
      count2=INT:256
1550
    */
1551
   const std::string filename("TNAuthList.pem");
1✔
1552
   Test::Result result("X509 TNAuthList decode");
1✔
1553
   result.start_timer();
1✔
1554

1555
   Botan::X509_Certificate cert(Test::data_file("x509/x509test/" + filename));
2✔
1556

1557
   using Botan::Cert_Extension::TNAuthList;
1✔
1558

1559
   auto tn_auth_list = cert.v3_extensions().get_extension_object_as<TNAuthList>();
2✔
1560

1561
   auto& tn_entries = tn_auth_list->entries();
1✔
1562

1563
   result.confirm("cert has TNAuthList extension", tn_auth_list != nullptr, true);
2✔
1564

1565
   result.test_throws("wrong telephone_number_range() accessor for spc",
2✔
1566
                      [&tn_entries] { tn_entries[0].telephone_number_range(); });
2✔
1567
   result.test_throws("wrong telephone_number() accessor for range",
2✔
1568
                      [&tn_entries] { tn_entries[1].telephone_number(); });
2✔
1569
   result.test_throws("wrong service_provider_code() accessor for one",
2✔
1570
                      [&tn_entries] { tn_entries[2].service_provider_code(); });
2✔
1571

1572
   result.test_eq("spc entry type", tn_entries[0].type() == TNAuthList::Entry::ServiceProviderCode, true);
1✔
1573
   result.test_eq("spc entry data", tn_entries[0].service_provider_code(), "1001");
2✔
1574

1575
   result.test_eq("range entry type", tn_entries[1].type() == TNAuthList::Entry::TelephoneNumberRange, true);
1✔
1576
   auto& range = tn_entries[1].telephone_number_range();
1✔
1577
   result.test_eq("range entries count", range.size(), 2);
1✔
1578
   result.test_eq("range entry 0 start data", range[0].start.value(), "111");
2✔
1579
   result.test_eq("range entry 0 count data", range[0].count, 128);
1✔
1580
   result.test_eq("range entry 1 start data", range[1].start.value(), "222");
2✔
1581
   result.test_eq("range entry 1 count data", range[1].count, 256);
1✔
1582

1583
   result.test_eq("one entry type", tn_entries[2].type() == TNAuthList::Entry::TelephoneNumber, true);
1✔
1584
   result.test_eq("one entry data", tn_entries[2].telephone_number(), "333");
2✔
1585

1586
   result.end_timer();
1✔
1587
   return result;
2✔
1588
}
1✔
1589

1590
   #endif
1591

1592
std::vector<std::string> get_sig_paddings(const std::string& sig_algo, const std::string& hash) {
12✔
1593
   if(sig_algo == "RSA") {
12✔
1594
      return {"EMSA3(" + hash + ")", "EMSA4(" + hash + ")"};
5✔
1595
   } else if(sig_algo == "DSA" || sig_algo == "ECDSA" || sig_algo == "ECGDSA" || sig_algo == "ECKCDSA" ||
11✔
1596
             sig_algo == "GOST-34.10") {
7✔
1597
      return {hash};
10✔
1598
   } else if(sig_algo == "Ed25519" || sig_algo == "Ed448") {
6✔
1599
      return {"Pure"};
2✔
1600
   } else if(sig_algo == "Dilithium" || sig_algo == "ML-DSA") {
4✔
1601
      return {"Randomized"};
2✔
1602
   } else if(sig_algo == "HSS-LMS") {
2✔
1603
      return {""};
1✔
1604
   } else {
1605
      return {};
1✔
1606
   }
1607
}
7✔
1608

1609
class X509_Cert_Unit_Tests final : public Test {
×
1610
   public:
1611
      std::vector<Test::Result> run() override {
1✔
1612
         std::vector<Test::Result> results;
1✔
1613

1614
         auto& rng = this->rng();
1✔
1615

1616
         const std::string sig_algos[]{"RSA",
1✔
1617
                                       "DSA",
1618
                                       "ECDSA",
1619
                                       "ECGDSA",
1620
                                       "ECKCDSA",
1621
                                       "GOST-34.10",
1622
                                       "Ed25519",
1623
                                       "Ed448",
1624
                                       "Dilithium",
1625
                                       "ML-DSA",
1626
                                       "SLH-DSA",
1627
                                       "HSS-LMS"};
13✔
1628

1629
         for(const std::string& algo : sig_algos) {
13✔
1630
   #if !defined(BOTAN_HAS_EMSA_PKCS1)
1631
            if(algo == "RSA")
1632
               continue;
1633
   #endif
1634

1635
            std::string hash = "SHA-256";
12✔
1636

1637
            if(algo == "Ed25519") {
12✔
1638
               hash = "SHA-512";
1✔
1639
            }
1640
            if(algo == "Ed448") {
12✔
1641
               hash = "SHAKE-256(912)";
1✔
1642
            }
1643
            if(algo == "Dilithium" || algo == "ML-DSA") {
12✔
1644
               hash = "SHAKE-256(512)";
2✔
1645
            }
1646

1647
            auto key = make_a_private_key(algo, rng);
12✔
1648

1649
            if(key == nullptr) {
12✔
1650
               continue;
×
1651
            }
1652

1653
            results.push_back(test_hashes(*key, hash, rng));
24✔
1654
            results.push_back(test_valid_constraints(*key, algo));
24✔
1655

1656
            Test::Result usage_result("X509 Usage");
12✔
1657
            try {
12✔
1658
               usage_result.merge(test_usage(*key, algo, hash, rng));
12✔
1659
            } catch(std::exception& e) {
×
1660
               usage_result.test_failure("test_usage " + algo, e.what());
×
1661
            }
×
1662
            results.push_back(usage_result);
12✔
1663

1664
            for(const auto& padding_scheme : get_sig_paddings(algo, hash)) {
24✔
1665
               Test::Result cert_result("X509 Unit");
12✔
1666

1667
               try {
12✔
1668
                  cert_result.merge(test_x509_cert(*key, algo, padding_scheme, hash, rng));
12✔
1669
               } catch(std::exception& e) {
×
1670
                  cert_result.test_failure("test_x509_cert " + algo, e.what());
×
1671
               }
×
1672
               results.push_back(cert_result);
12✔
1673

1674
               Test::Result pkcs10_result("PKCS10 extensions");
12✔
1675
               try {
12✔
1676
                  pkcs10_result.merge(test_pkcs10_ext(*key, padding_scheme, hash, rng));
12✔
1677
               } catch(std::exception& e) {
×
1678
                  pkcs10_result.test_failure("test_pkcs10_ext " + algo, e.what());
×
1679
               }
×
1680
               results.push_back(pkcs10_result);
12✔
1681

1682
               Test::Result self_issued_result("X509 Self Issued");
12✔
1683
               try {
12✔
1684
                  self_issued_result.merge(test_self_issued(*key, algo, padding_scheme, hash, rng));
12✔
1685
               } catch(std::exception& e) {
×
1686
                  self_issued_result.test_failure("test_self_issued " + algo, e.what());
×
1687
               }
×
1688
               results.push_back(self_issued_result);
12✔
1689

1690
               Test::Result extensions_result("X509 Extensions");
12✔
1691
               try {
12✔
1692
                  extensions_result.merge(test_x509_extensions(*key, algo, padding_scheme, hash, rng));
12✔
1693
               } catch(std::exception& e) {
×
1694
                  extensions_result.test_failure("test_extensions " + algo, e.what());
×
1695
               }
×
1696
               results.push_back(extensions_result);
12✔
1697

1698
               Test::Result custom_dn_result("X509 Custom DN");
12✔
1699
               try {
12✔
1700
                  custom_dn_result.merge(test_custom_dn_attr(*key, algo, padding_scheme, hash, rng));
12✔
1701
               } catch(std::exception& e) {
×
1702
                  custom_dn_result.test_failure("test_custom_dn_attr " + algo, e.what());
×
1703
               }
×
1704
               results.push_back(custom_dn_result);
12✔
1705
            }
24✔
1706
         }
24✔
1707

1708
         /*
1709
         These are algos which cannot sign but can be included in certs
1710
         */
1711
         const std::vector<std::string> enc_algos = {
1✔
1712
            "DH", "ECDH", "ElGamal", "Kyber", "ML-KEM", "FrodoKEM", "ClassicMcEliece"};
1✔
1713

1714
         for(const std::string& algo : enc_algos) {
8✔
1715
            auto key = make_a_private_key(algo, rng);
7✔
1716

1717
            if(key) {
7✔
1718
               results.push_back(test_valid_constraints(*key, algo));
14✔
1719
            }
1720
         }
7✔
1721

1722
   #if defined(BOTAN_TARGET_OS_HAS_FILESYSTEM) && defined(BOTAN_HAS_EMSA_PKCS1) && defined(BOTAN_HAS_EMSA_PSSR) && \
1723
      defined(BOTAN_HAS_RSA)
1724
         Test::Result pad_config_result("X509 Padding Config");
1✔
1725
         try {
1✔
1726
            pad_config_result.merge(test_padding_config());
1✔
1727
         } catch(const std::exception& e) {
×
1728
            pad_config_result.test_failure("test_padding_config", e.what());
×
1729
         }
×
1730
         results.push_back(pad_config_result);
1✔
1731
   #endif
1732

1733
   #if defined(BOTAN_TARGET_OS_HAS_FILESYSTEM)
1734
         results.push_back(test_x509_utf8());
2✔
1735
         results.push_back(test_x509_bmpstring());
2✔
1736
         results.push_back(test_x509_teletex());
2✔
1737
         results.push_back(test_crl_dn_name());
2✔
1738
         results.push_back(test_rdn_multielement_set_name());
2✔
1739
         results.push_back(test_x509_decode_list());
2✔
1740
         results.push_back(test_rsa_oaep());
2✔
1741
         results.push_back(test_x509_authority_info_access_extension());
2✔
1742
         results.push_back(test_verify_gost2012_cert());
2✔
1743
         results.push_back(test_parse_rsa_pss_cert());
2✔
1744
         results.push_back(test_x509_tn_auth_list_extension_decode());
2✔
1745
   #endif
1746

1747
         results.push_back(test_x509_encode_authority_info_access_extension());
2✔
1748
         results.push_back(test_x509_extension());
2✔
1749
         results.push_back(test_x509_dates());
2✔
1750
         results.push_back(test_cert_status_strings());
2✔
1751
         results.push_back(test_x509_uninit());
2✔
1752

1753
         return results;
1✔
1754
      }
13✔
1755
};
1756

1757
BOTAN_REGISTER_TEST("x509", "x509_unit", X509_Cert_Unit_Tests);
1758

1759
#endif
1760

1761
}  // namespace
1762

1763
}  // namespace Botan_Tests
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc