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

randombit / botan / 13043245251

30 Jan 2025 12:38AM UTC coverage: 91.229% (-0.003%) from 91.232%
13043245251

Pull #4613

github

web-flow
Merge 210366a42 into 37c5007d3
Pull Request #4613: Fix some errors caught by test_all_configs.py

94115 of 103164 relevant lines covered (91.23%)

11332427.14 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 defined(BOTAN_HAS_ECC_GROUP)
112
         if(Botan::EC_Group::supports_named_group("gost_256A")) {
8✔
113
            return "gost_256A";
114
         }
115
   #endif
116
         return "secp256r1";
×
117
      }
118
      if(algo == "ECKCDSA" || algo == "ECGDSA") {
81✔
119
         return "brainpool256r1";
120
      }
121
      if(algo == "HSS-LMS") {
65✔
122
         return "SHA-256,HW(5,4),HW(5,4)";
123
      }
124
      if(algo == "SLH-DSA") {
57✔
125
         return "SLH-DSA-SHA2-128f";
2✔
126
      }
127
      return "";  // default "" means choose acceptable algo-specific params
128
   }();
105✔
129

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

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

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

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

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

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

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

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

198
   return result;
1✔
199
}
1✔
200

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

204
   Botan::Extensions extn;
1✔
205

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

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

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

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

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

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

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

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

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

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

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

239
   return result;
2✔
240
}
2✔
241

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

414
   return result;
1✔
415
}
80✔
416

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

486
   return result;
1✔
487
}
4✔
488

489
   #if defined(BOTAN_TARGET_OS_HAS_FILESYSTEM)
490

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

494
      // See GH #1252
495

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

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

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

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

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

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

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

514
   return result;
2✔
515
}
3✔
516

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

520
   // GH #2611
521

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

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

527
   return result;
1✔
528
}
1✔
529

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

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

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

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

543
   return result;
2✔
544
}
1✔
545

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

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

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

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

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

560
   return result;
1✔
561
}
1✔
562

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

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

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

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

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

591
   return result;
1✔
592
}
×
593

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

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

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

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

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

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

617
   return result;
1✔
618
}
×
619

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

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

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

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

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

636
   return result;
1✔
637
}
×
638

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

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

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

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

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

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

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

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

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

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

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

680
   return result;
1✔
681
}
1✔
682

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

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

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

695
   return result;
1✔
696
}
×
697

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

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

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

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

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

721
   return result;
1✔
722
}
×
723

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

830
   #endif
831

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

838
   Botan::X509_Cert_Options opts;
12✔
839

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

844
   opts.padding_scheme = sig_padding;
12✔
845

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

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

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

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

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

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

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

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

872
   return result;
12✔
873
}
12✔
874

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

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

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

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

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

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

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

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

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

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

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

911
   const BigInt user1_serial = 99;
12✔
912

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1007
   store.add_crl(crl2);
12✔
1008

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

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

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

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

1022
   store.add_crl(crl3);
12✔
1023

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

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

1032
   return result;
12✔
1033
}
60✔
1034

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1111
   return result;
12✔
1112
}
24✔
1113

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

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

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

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

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

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

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

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

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

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

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

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

1153
   return result;
12✔
1154
}
24✔
1155

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

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

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

1168
   return result;
1✔
1169
}
1✔
1170

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

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

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

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

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

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

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

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

1267
   return result;
19✔
1268
}
×
1269

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1322
   Botan::X509_DN subject_dn;
12✔
1323

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

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

1332
   Botan::Extensions extensions;
12✔
1333

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

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

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

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

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

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

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

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

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

1364
   return result;
12✔
1365
}
36✔
1366

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1480
   return result;
12✔
1481
}
60✔
1482

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

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

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

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

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

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

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

1534
   #if defined(BOTAN_TARGET_OS_HAS_FILESYSTEM)
1535

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

1539
      asn1=SEQUENCE:tn_auth_list
1540

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

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

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

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

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

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

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

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

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

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

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

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

1591
   #endif
1592

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1760
#endif
1761

1762
}  // namespace
1763

1764
}  // 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