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

randombit / botan / 27930394332

22 Jun 2026 02:29AM UTC coverage: 89.361% (-2.3%) from 91.664%
27930394332

push

github

randombit
Escape control chars in X509_Certificate::to_string

111792 of 125101 relevant lines covered (89.36%)

10818223.53 hits per line

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

92.99
/src/tests/test_name_constraint.cpp
1
/*
2
* (C) 2015,2016 Kai Michaelis
3
*     2026 Jack Lloyd
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/data_src.h>
13
   #include <botan/pkix_types.h>
14
   #include <botan/x509cert.h>
15
   #include <botan/x509path.h>
16
   #include <botan/internal/calendar.h>
17
   #include <botan/internal/x509_utils.h>
18
   #include <algorithm>
19
   #include <fstream>
20
#endif
21

22
namespace Botan_Tests {
23

24
namespace {
25

26
#if defined(BOTAN_HAS_X509_CERTIFICATES) && defined(BOTAN_HAS_ECDSA) && defined(BOTAN_HAS_SHA2_32) && \
27
   defined(BOTAN_TARGET_OS_HAS_FILESYSTEM)
28

29
class Name_Constraint_Validation_Tests final : public Test {
1✔
30
   public:
31
      std::vector<Test::Result> run() override {
1✔
32
         std::vector<Test::Result> results;
1✔
33

34
         /*
35
         * Each test is a single PEM file containing the chain leaf-first (leaf,
36
         * intermediates...), the trust anchor is shared as root.pem, and expected.txt
37
         * maps test-name to the Path_Validation_Result::result_string() output.
38
         */
39
         const std::string base = "x509/name_constraints/";
1✔
40
         const auto expected = read_manifest(Test::data_file(base + "expected.txt"));
2✔
41

42
         const Botan::X509_Certificate trust_anchor(Test::data_file(base + "root.pem"));
2✔
43

44
         const auto when = Botan::calendar_point(2027, 1, 1, 0, 0, 0).to_std_timepoint();
1✔
45

46
         const Botan::Path_Validation_Restrictions restrictions(false, 128);
1✔
47

48
         for(const auto& [name, expected_result] : expected) {
300✔
49
            Test::Result result("Name constraints test " + name);
299✔
50

51
            Botan::Certificate_Store_In_Memory store;
299✔
52
            store.add_certificate(trust_anchor);
299✔
53

54
            const auto chain = load_chain(Test::data_file(base + name + ".pem"));
897✔
55
            if(chain.empty()) {
299✔
56
               result.test_str_eq("validation result", "Certificate failed to decode", expected_result);
3✔
57
               results.emplace_back(std::move(result));
3✔
58
               continue;
3✔
59
            }
60

61
            const std::string hostname;
296✔
62

63
            const auto pv =
296✔
64
               Botan::x509_path_validate(chain, restrictions, store, hostname, Botan::Usage_Type::UNSPECIFIED, when);
296✔
65

66
            result.test_str_eq("validation result", pv.result_string(), expected_result);
296✔
67
            results.emplace_back(std::move(result));
296✔
68
         }
299✔
69

70
         return results;
2✔
71
      }
1✔
72

73
   private:
74
      // Read all certificates from a PEM bundle in file order (leaf first).
75
      static std::vector<Botan::X509_Certificate> load_chain(const std::string& filename) {
299✔
76
         Botan::DataSource_Stream in(filename);
299✔
77
         std::vector<Botan::X509_Certificate> certs;
299✔
78
         while(!in.end_of_data()) {
921✔
79
            try {
921✔
80
               certs.emplace_back(in);
921✔
81
            } catch(const Botan::Decoding_Error&) {
299✔
82
               break;
299✔
83
            }
299✔
84
         }
85
         return certs;
299✔
86
      }
299✔
87

88
      // Parse `<chain-name>:<result>` lines; ignore blanks and `#` comments.
89
      static std::vector<std::pair<std::string, std::string>> read_manifest(const std::string& path) {
1✔
90
         std::vector<std::pair<std::string, std::string>> out;
1✔
91
         std::ifstream in(path);
1✔
92
         std::string line;
1✔
93
         while(std::getline(in, line)) {
305✔
94
            if(line.empty() || line.front() == '#') {
304✔
95
               continue;
5✔
96
            }
97
            const auto colon = line.find(':');
299✔
98
            if(colon == std::string::npos) {
299✔
99
               continue;
×
100
            }
101
            out.emplace_back(line.substr(0, colon), line.substr(colon + 1));
598✔
102
         }
103
         return out;
2✔
104
      }
1✔
105
};
106

107
BOTAN_REGISTER_TEST("x509", "x509_name_constraints", Name_Constraint_Validation_Tests);
108

109
/*
110
* Validate that GeneralName iPAddress decoding rejects masks that are not a
111
* contiguous CIDR prefix. Drives the decoder with hand-rolled BER for a
112
* single [7] IMPLICIT OCTET STRING carrying {net || mask}.
113
*/
114
class Name_Constraint_IP_Mask_Tests final : public Text_Based_Test {
×
115
   public:
116
      Name_Constraint_IP_Mask_Tests() : Text_Based_Test("x509/general_name_ip.vec", "Address,Netmask") {}
2✔
117

118
      Test::Result run_one_test(const std::string& header, const VarMap& vars) override {
19✔
119
         Test::Result result("GeneralName iPAddress mask validation");
19✔
120

121
         const auto address = vars.get_req_bin("Address");
19✔
122
         const auto netmask = vars.get_req_bin("Netmask");
19✔
123

124
         const auto der = encode_address(address, netmask);
19✔
125

126
         Botan::BER_Decoder decoder(der, Botan::BER_Decoder::Limits::DER());
19✔
127
         Botan::GeneralName gn;
19✔
128

129
         if(header == "Valid") {
19✔
130
            try {
12✔
131
               gn.decode_from(decoder);
12✔
132
               result.test_success("Accepted valid GeneralName IP encoding");
12✔
133
            } catch(Botan::Decoding_Error&) {
×
134
               result.test_failure("Rejected valid GeneralName IP encoding");
×
135
            }
×
136
         } else {
137
            try {
7✔
138
               gn.decode_from(decoder);
7✔
139
               result.test_failure("Accepted invalid GeneralName IP encoding");
×
140
            } catch(Botan::Decoding_Error&) {
7✔
141
               result.test_success("Rejected invalid GeneralName IP encoding");
7✔
142
            }
7✔
143
         }
144

145
         return result;
38✔
146
      }
19✔
147

148
   private:
149
      static std::vector<uint8_t> encode_address(std::span<const uint8_t> address, std::span<const uint8_t> netmask) {
19✔
150
         std::vector<uint8_t> der;
19✔
151
         // [7] IMPLICIT OCTET STRING, primitive, context-specific.
152
         der.push_back(0x87);
19✔
153
         // Short for length is sufficient here
154
         der.push_back(static_cast<uint8_t>(address.size() + netmask.size()));
19✔
155
         der.insert(der.end(), address.begin(), address.end());
19✔
156
         der.insert(der.end(), netmask.begin(), netmask.end());
19✔
157
         return der;
19✔
158
      }
×
159
};
160

161
BOTAN_REGISTER_TEST("x509", "x509_name_constraint_ip_mask", Name_Constraint_IP_Mask_Tests);
162

163
/*
164
* Strict validation at the constraint-factory boundary: malformed
165
* inputs throw, valid inputs are canonicalized (lowercase host,
166
* preserve email local-part case).
167
*/
168
class Name_Constraint_Factory_Validation_Tests final : public Test {
1✔
169
   private:
170
      using FactoryFn = Botan::GeneralName (*)(std::string_view);
171

172
      static void check_valid(Test::Result& result,
21✔
173
                              const std::string& label,
174
                              FactoryFn make,
175
                              std::string_view input,
176
                              std::string_view expected_name) {
177
         try {
21✔
178
            const auto gn = make(input);
21✔
179
            result.test_str_eq(label + " canonical: " + std::string(input), gn.name(), expected_name);
63✔
180
         } catch(const std::exception& e) {
21✔
181
            result.test_failure(label + " rejected valid '" + std::string(input) + "': " + e.what());
×
182
         }
×
183
      }
21✔
184

185
      static void check_invalid(Test::Result& result,
42✔
186
                                const std::string& label,
187
                                FactoryFn make,
188
                                std::string_view input) {
189
         try {
42✔
190
            (void)make(input);
42✔
191
            result.test_failure(label + " accepted invalid '" + std::string(input) + "'");
×
192
         } catch(const Botan::Invalid_Argument&) {
42✔
193
            result.test_success(label + " rejected '" + std::string(input) + "'");
168✔
194
         }
42✔
195
      }
42✔
196

197
      static Test::Result test_dns() {
1✔
198
         Test::Result result("X509v3 Name Constraints: DNS factory validation");
1✔
199
         const auto m = &Botan::GeneralName::dns;
1✔
200
         check_valid(result, "DNS", m, "example.com", "example.com");
1✔
201
         check_valid(result, "DNS", m, "EXAMPLE.com", "example.com");
1✔
202
         check_valid(result, "DNS", m, "host", "host");
1✔
203
         check_valid(result, "DNS", m, ".example.com", ".example.com");
1✔
204

205
         const auto rejected = {"",
1✔
206
                                ".",
207
                                "..example.com",
208
                                "example..com",
209
                                "example.com.",
210
                                "*.example.com",
211
                                "host name",
212
                                " example.com",
213
                                "example.com ",
214
                                "_acme-challenge.example.com"};
1✔
215

216
         for(const auto& bad : rejected) {
11✔
217
            check_invalid(result, "DNS", m, bad);
20✔
218
         }
219
         return result;
1✔
220
      }
×
221

222
      static Test::Result test_uri() {
1✔
223
         Test::Result result("X509v3 Name Constraints: URI factory validation");
1✔
224
         const auto m = &Botan::GeneralName::uri;
1✔
225
         check_valid(result, "URI", m, "example.com", "example.com");
1✔
226
         check_valid(result, "URI", m, ".example.com", ".example.com");
1✔
227
         check_valid(result, "URI", m, "EXAMPLE.com", "example.com");
1✔
228
         // RFC 5280 4.2.1.10: "The constraint MUST be specified as a
229
         // fully qualified domain name". Single-label hosts and full
230
         // URIs are not constraint-shaped; both are rejected.
231
         for(const auto& bad : {"",
21✔
232
                                ".",
233
                                "localhost",
234
                                ".localhost",
235
                                "https://example.com",
236
                                "https://example.com/path",
237
                                "example.com:443",
238
                                "*.example.com",
239
                                "example.com.",
240
                                "..example.com"}) {
11✔
241
            check_invalid(result, "URI", m, bad);
20✔
242
         }
243
         return result;
1✔
244
      }
×
245

246
      static Test::Result test_uri_san_value() {
1✔
247
         Test::Result result("X509v3 Name Constraints: URI SAN value factory validation");
1✔
248
         const auto m = &Botan::GeneralName::_uri_san_value;
1✔
249
         check_valid(result, "URI SAN", m, "https://example.com", "https://example.com");
1✔
250
         check_valid(result, "URI SAN", m, "https://example.com/path?q=1#frag", "https://example.com/path?q=1#frag");
1✔
251
         check_valid(result, "URI SAN", m, "HTTPS://Example.COM/", "HTTPS://Example.COM/");
1✔
252
         check_valid(result, "URI SAN", m, "https://localhost/", "https://localhost/");
1✔
253
         check_valid(result, "URI SAN", m, "mailto:root@example.com", "mailto:root@example.com");
1✔
254
         // Inputs URI::parse rejects (RFC 3986 syntax violations,
255
         // constraint-shape values that aren't URIs).
256
         for(const auto& bad : {"",
17✔
257
                                "example.com",
258
                                ".example.com",
259
                                "not a uri",
260
                                "://no.scheme/",
261
                                "https://example.com/has space",
262
                                "https://user@bad@example.com/",
263
                                "https://example.com/%G0"}) {
9✔
264
            check_invalid(result, "URI SAN", m, bad);
16✔
265
         }
266
         return result;
1✔
267
      }
×
268

269
      static Test::Result test_dns_san_value() {
1✔
270
         Test::Result result("X509v3 Name Constraints: DNS SAN value factory validation");
1✔
271
         const auto m = &Botan::GeneralName::_dns_san_value;
1✔
272

273
         check_valid(result, "DNS SAN", m, "example.com", "example.com");
1✔
274
         check_valid(result, "DNS SAN", m, "EXAMPLE.com", "example.com");
1✔
275
         check_valid(result, "DNS SAN", m, "*.example.com", "*.example.com");
1✔
276
         check_valid(result, "DNS SAN", m, "foo*.example.com", "foo*.example.com");
1✔
277
         check_valid(result, "DNS SAN", m, "*bar.example.com", "*bar.example.com");
1✔
278

279
         for(const auto& bad : {"", ".", "..example.com", "*.*.example.com", "foo.*.example.com", "host name"}) {
7✔
280
            check_invalid(result, "DNS SAN", m, bad);
12✔
281
         }
282
         return result;
1✔
283
      }
×
284

285
      static Test::Result test_email() {
1✔
286
         Test::Result result("X509v3 Name Constraints: email factory validation");
1✔
287
         const auto m = &Botan::GeneralName::email;
1✔
288
         // Mailbox form: local-part case-preserved, host lowercased (RFC 5280 7.5).
289
         check_valid(result, "Email", m, "Alice@Example.COM", "Alice@example.com");
1✔
290
         check_valid(result, "Email", m, "user@example.com", "user@example.com");
1✔
291
         // Host form: bare DNS name.
292
         check_valid(result, "Email", m, "example.com", "example.com");
1✔
293
         // Subtree form: leading dot is preserved.
294
         check_valid(result, "Email", m, ".example.com", ".example.com");
1✔
295
         for(const auto& bad : {"",
17✔
296
                                "@example.com",
297
                                "user@",
298
                                "a@b@c",
299
                                ".",
300
                                "user@example..com",
301
                                "user@.example.com",
302
                                "user@*.example.com"}) {
9✔
303
            check_invalid(result, "Email", m, bad);
16✔
304
         }
305
         return result;
1✔
306
      }
×
307

308
   public:
309
      std::vector<Test::Result> run() override {
1✔
310
         return {test_dns(), test_uri(), test_email(), test_uri_san_value(), test_dns_san_value()};
6✔
311
      }
1✔
312
};
313

314
BOTAN_REGISTER_TEST("x509", "x509_name_constraint_factory_validation", Name_Constraint_Factory_Validation_Tests);
315

316
class Wildcard_Excluded_Subtree_Containment_Tests final : public Test {
1✔
317
   public:
318
      std::vector<Test::Result> run() override {
1✔
319
         Test::Result result("X509v3 Name Constraints: wildcard SAN vs excluded DNS subtree");
1✔
320

321
         struct Case {
28✔
322
               std::string pattern;     // SAN wildcard
323
               std::string constraint;  // excluded DNS constraint value
324
               bool expect_intersect;
325
         };
326

327
         const std::vector<Case> cases = {
1✔
328
            // SAN of *.com can expand to evil.com.
329
            {"*.com", "evil.com", true},
1✔
330
            // Leading-dot subtree: *.com can expand to <anything>.com.
331
            {"*.com", ".com", true},
332
            // Wildcard whose tail equals the constraint: every expansion
333
            // is in the subtree.
334
            {"*.example.com", "example.com", true},
335
            {"*.example.com", ".example.com", true},
336
            // Wildcard with extra labels under the constraint: every
337
            // expansion is in the subtree.
338
            {"*.foo.example.com", "example.com", true},
339
            // Partial wildcards in the leftmost label that absorb the
340
            // missing labels of the constraint base.
341
            {"foo*.example.com", "example.com", true},
342
            {"*bar.example.com", "example.com", true},
343
            // Non-overlapping suffixes: no expansion in subtree.
344
            {"*.example.com", "evil.com", false},
345
            {"*.example.com", ".other.com", false},
346
            // Wildcard tail shorter than constraint, and leftover prefix
347
            // contains a dot - can't be produced by a single-label wildcard.
348
            {"*.com", "evil.example.com", false},
349
            // Single-label wildcards only match single-label hosts.
350
            {"*", "evil.com", false},
351
            {"*", "com", true},
352
            {"foo*", "foobar", true},
353
            // Leading-dot subtree excludes the apex; single-label wildcard
354
            // can't reach into it.
355
            {"*", ".com", false},
356
         };
15✔
357

358
         for(const auto& c : cases) {
15✔
359
            const bool got = Botan::wildcard_intersects_excluded_dns_subtree(c.pattern, c.constraint);
14✔
360
            result.test_bool_eq(c.pattern + " vs " + c.constraint, got, c.expect_intersect);
42✔
361
         }
362

363
         return {result};
3✔
364
      }
4✔
365
};
366

367
BOTAN_REGISTER_TEST("x509",
368
                    "x509_name_constraint_wildcard_excluded_containment",
369
                    Wildcard_Excluded_Subtree_Containment_Tests);
370

371
class SmtpUTF8Mailbox_Constraint_Match_Tests final : public Test {
1✔
372
   public:
373
      std::vector<Test::Result> run() override {
1✔
374
         Test::Result result("X509v3 Name Constraints: rfc822Name matches SmtpUTF8Mailbox");
1✔
375

376
         // RFC 9598 Section 6: rfc822Name constraints extend to SmtpUTF8Mailbox
377
         // SAN entries. The constraint's local-part (if any) is
378
         // ignored; comparison is on the domain part.
379

380
         const auto host_constraint = Botan::GeneralName::email("example.com");
1✔
381
         const auto subtree_constraint = Botan::GeneralName::email(".example.com");
1✔
382
         const auto mailbox_constraint = Botan::GeneralName::email("alice@example.com");
1✔
383

384
         const auto mailbox = [](std::string_view s) { return Botan::SmtpUtf8Mailbox::from_string(s).value(); };
30✔
385

386
         // Host constraint: domain must match exactly.
387
         result.test_is_true("host constraint matches identical domain",
2✔
388
                             host_constraint.matches_email(mailbox("user@example.com")));
1✔
389
         result.test_is_false("host constraint rejects subdomain",
2✔
390
                              host_constraint.matches_email(mailbox("user@sub.example.com")));
1✔
391
         result.test_is_false("host constraint rejects unrelated domain",
2✔
392
                              host_constraint.matches_email(mailbox("user@evil.com")));
1✔
393

394
         // Subtree constraint (leading dot): proper subdomains match,
395
         // base does not.
396
         result.test_is_true("subtree constraint matches subdomain",
2✔
397
                             subtree_constraint.matches_email(mailbox("user@sub.example.com")));
1✔
398
         result.test_is_false("subtree constraint rejects apex",
2✔
399
                              subtree_constraint.matches_email(mailbox("user@example.com")));
1✔
400
         result.test_is_false("subtree constraint rejects unrelated domain",
2✔
401
                              subtree_constraint.matches_email(mailbox("user@evil.com")));
1✔
402

403
         // RFC 9549 deprecates mailbox-form rfc822Name constraints for
404
         // SmtpUTF8Mailbox matching: such constraints must not match.
405
         result.test_is_false("mailbox constraint does not apply to SmtpUTF8Mailbox",
2✔
406
                              mailbox_constraint.matches_email(mailbox("alice@example.com")));
1✔
407

408
         // The reviewer's bypass: a CA permitted to ".example.com" issues
409
         // a leaf with SmtpUTF8Mailbox "alice@evil.com". Before the fix
410
         // this would slip through; after, the matcher correctly reports
411
         // no match, and is_excluded/is_permitted will reject the chain.
412
         result.test_is_false("bypass closed: ASCII evil.com against .example.com",
2✔
413
                              subtree_constraint.matches_email(mailbox("alice@evil.com")));
1✔
414
         result.test_is_false("bypass closed: UTF-8 local part doesn't change the answer",
2✔
415
                              subtree_constraint.matches_email(mailbox("\xCE\xB4\xCE\xBF\xCE\xBA\xCE\xB9@evil.com")));
1✔
416

417
         // Non-email constraints don't match regardless of mailbox.
418
         const auto dns_constraint = Botan::GeneralName::dns("example.com");
1✔
419
         result.test_is_false("DNS constraint doesn't match SmtpUTF8Mailbox",
2✔
420
                              dns_constraint.matches_email(mailbox("user@example.com")));
1✔
421

422
         // SmtpUtf8Mailbox::from_string rejects the malformed shapes
423
         // we previously had to guard against in the matcher.
424
         for(const auto& bad : {"",
23✔
425
                                "no-at-sign.example.com",
426
                                "@example.com",
427
                                "alice@",
428
                                "a@b@c",
429
                                "alice@.example.com",
430
                                "alice@example..com",
431
                                "alice..bob@example.com",
432
                                ".alice@example.com",
433
                                // RFC 9598 Section 3: non-ASCII domain labels
434
                                // MUST be in A-label form on the wire.
435
                                // Raw UTF-8 in the domain is rejected.
436
                                "alice@\xD0\xBF\xD1\x80\xD0\xB8\xD0\xBC\xD0\xB5\xD1\x80.\xD1\x80\xD1\x84",
437
                                // Invalid UTF-8 anywhere in the input.
438
                                "alice@\xC0\xC0.com"}) {
12✔
439
            result.test_is_false("SmtpUtf8Mailbox rejects malformed: " + std::string(bad),
22✔
440
                                 Botan::SmtpUtf8Mailbox::from_string(bad).has_value());
22✔
441
         }
442

443
         // ASCII and UTF-8-local-part mailboxes both parse.
444
         result.test_is_true("ASCII mailbox parses",
2✔
445
                             Botan::SmtpUtf8Mailbox::from_string("alice@example.com").has_value());
1✔
446
         result.test_is_true(
2✔
447
            "UTF-8 local part parses",
448
            Botan::SmtpUtf8Mailbox::from_string("\xCE\xB4\xCE\xBF\xCE\xBA\xCE\xB9@example.com").has_value());
1✔
449
         // A-label encoded IDN domain parses (RFC 9598 Section 3 mandates this
450
         // form for any label containing non-ASCII characters).
451
         result.test_is_true("A-label IDN domain parses",
2✔
452
                             Botan::SmtpUtf8Mailbox::from_string("alice@xn--e1afmkfd.xn--p1ai").has_value());
1✔
453

454
         return {result};
3✔
455
      }
5✔
456
};
457

458
BOTAN_REGISTER_TEST("x509", "x509_name_constraint_smtp_utf8_match", SmtpUTF8Mailbox_Constraint_Match_Tests);
459

460
#endif
461

462
}  // namespace
463

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