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

randombit / botan / 27324610210

10 Jun 2026 06:04PM UTC coverage: 89.378% (+0.01%) from 89.367%
27324610210

push

github

web-flow
Merge pull request #5660 from randombit/jack/x509-dfs-order

Fix order of X.509 DFS iteration

110896 of 124075 relevant lines covered (89.38%)

11140723.17 hits per line

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

96.57
/src/tests/test_x509_dn.cpp
1
/*
2
* (C) 2017 Jack Lloyd
3
*
4
* Botan is released under the Simplified BSD License (see license.txt)
5
*/
6

7
#include "tests.h"
8

9
#if defined(BOTAN_HAS_X509_CERTIFICATES)
10
   #include <botan/ber_dec.h>
11
   #include <botan/hex.h>
12
   #include <botan/pkix_types.h>
13
   #include <botan/internal/charset.h>
14
   #include <algorithm>
15
   #include <sstream>
16
#endif
17

18
namespace Botan_Tests {
19

20
namespace {
21

22
#if defined(BOTAN_HAS_X509_CERTIFICATES)
23
class X509_DN_Comparisons_Tests final : public Text_Based_Test {
×
24
   public:
25
      X509_DN_Comparisons_Tests() : Text_Based_Test("x509_dn.vec", "DN1,DN2") {}
2✔
26

27
      Test::Result run_one_test(const std::string& type, const VarMap& vars) override {
10✔
28
         const std::vector<uint8_t> dn_bits1 = vars.get_req_bin("DN1");
10✔
29
         const std::vector<uint8_t> dn_bits2 = vars.get_req_bin("DN2");
10✔
30

31
         const bool dn_same = (type == "Equal");
10✔
32

33
         Test::Result result("X509_DN comparisons");
10✔
34
         try {
10✔
35
            Botan::X509_DN dn1;
10✔
36
            Botan::BER_Decoder bd1(dn_bits1);
10✔
37
            dn1.decode_from(bd1);
10✔
38

39
            Botan::X509_DN dn2;
10✔
40
            Botan::BER_Decoder bd2(dn_bits2);
10✔
41
            dn2.decode_from(bd2);
10✔
42

43
            const bool compared_same = (dn1 == dn2);
10✔
44
            result.test_bool_eq("Comparison matches expected", dn_same, compared_same);
10✔
45

46
            const bool lt1 = (dn1 < dn2);
10✔
47
            const bool lt2 = (dn2 < dn1);
10✔
48

49
            if(dn_same) {
10✔
50
               result.test_is_false("same means neither is less than", lt1);
6✔
51
               result.test_is_false("same means neither is less than", lt2);
6✔
52
            } else {
53
               result.test_is_true("different means one is less than", lt1 || lt2);
4✔
54
               result.test_is_false("different means only one is less than", lt1 && lt2);
4✔
55
            }
56
         } catch(Botan::Exception& e) {
10✔
57
            result.test_failure(e.what());
×
58
         }
×
59

60
         return result;
10✔
61
      }
10✔
62
};
63

64
BOTAN_REGISTER_TEST("x509", "x509_dn_cmp", X509_DN_Comparisons_Tests);
65

66
class X509_DN_Valid_String_Tests final : public Text_Based_Test {
×
67
   public:
68
      X509_DN_Valid_String_Tests() : Text_Based_Test("x509/x509_dn_valid.vec", "Input,DER", "Output") {}
2✔
69

70
      Test::Result run_one_test(const std::string& /*header*/, const VarMap& vars) override {
19✔
71
         Test::Result result("X509_DN valid string encoding");
19✔
72

73
         const std::string input = vars.get_req_str("Input");
19✔
74
         const std::vector<uint8_t> expected_der = vars.get_req_bin("DER");
19✔
75
         const std::string expected_print = vars.get_opt_str("Output", input);
19✔
76

77
         const auto parsed = Botan::X509_DN::parse(input);
19✔
78
         if(!result.test_is_true("X509_DN::parse accepts valid input", parsed.has_value())) {
19✔
79
            return result;
80
         }
81

82
         // The parsed DN must encode to exactly the expected bytes ...
83
         result.test_bin_eq("DER encoding", parsed->DER_encode(), expected_der);
19✔
84

85
         // ... and that DER must decode back to an equal DN
86
         Botan::X509_DN decoded;
19✔
87
         Botan::BER_Decoder ber(expected_der);
19✔
88
         decoded.decode_from(ber);
19✔
89
         ber.verify_end();
19✔
90
         result.test_is_true("DER decodes to equal DN", *parsed == decoded);
19✔
91

92
         // to_string reproduces the input exactly, unless it's not canonical
93
         result.test_str_eq("string formatting", parsed->to_string(), expected_print);
19✔
94

95
         // to_string of the parsed DN and the decoded-from-DER DN should be the same
96
         result.test_str_eq("string formatting", parsed->to_string(), decoded.to_string());
19✔
97

98
         return result;
19✔
99
      }
38✔
100
};
101

102
BOTAN_REGISTER_TEST("x509", "x509_dn_valid", X509_DN_Valid_String_Tests);
103

104
class X509_DN_Invalid_String_Tests final : public Text_Based_Test {
×
105
   public:
106
      X509_DN_Invalid_String_Tests() : Text_Based_Test("x509/x509_dn_invalid.vec", "Input") {}
2✔
107

108
      Test::Result run_one_test(const std::string& /*header*/, const VarMap& vars) override {
23✔
109
         Test::Result result("X509_DN invalid string rejection");
23✔
110

111
         const std::string input = vars.get_req_str("Input");
23✔
112

113
         result.test_is_false("parse rejects malformed input", Botan::X509_DN::parse(input).has_value());
23✔
114

115
         // Stream extraction must signal the same failure via the failbit
116
         std::istringstream iss(input);
23✔
117
         Botan::X509_DN dn;
23✔
118
         iss >> dn;
23✔
119
         result.test_is_true("stream extraction sets failbit", iss.fail());
23✔
120

121
         return result;
46✔
122
      }
23✔
123
};
124

125
BOTAN_REGISTER_TEST("x509", "x509_dn_invalid", X509_DN_Invalid_String_Tests);
126

127
class X509_DN_String_Tests final : public Test {
1✔
128
   public:
129
      std::vector<Test::Result> run() override {
1✔
130
         std::vector<Test::Result> results;
1✔
131
         results.push_back(test_single_ava_round_trip());
2✔
132
         results.push_back(test_multi_ava_rdn_emits_plus());
2✔
133
         results.push_back(test_multi_ava_rdn_round_trip());
2✔
134
         results.push_back(test_parse_multi_ava_rdn());
2✔
135
         results.push_back(test_mixed_single_and_multi_ava_round_trip());
2✔
136
         results.push_back(test_quoted_plus_in_value_not_split());
2✔
137
         results.push_back(test_parse_rejects_trailing_separator_with_whitespace());
2✔
138
         results.push_back(test_decode_failure_leaves_dn_unchanged());
2✔
139
         results.push_back(test_value_escaping_round_trips());
2✔
140
         return results;
1✔
141
      }
×
142

143
   private:
144
      static Botan::X509_DN parse(std::string_view s) {
11✔
145
         Botan::X509_DN dn;
11✔
146
         std::istringstream iss{std::string(s)};
22✔
147
         iss >> dn;
11✔
148
         return dn;
11✔
149
      }
11✔
150

151
      static std::string format(const Botan::X509_DN& dn) {
17✔
152
         std::ostringstream oss;
17✔
153
         oss << dn;
17✔
154
         return oss.str();
34✔
155
      }
17✔
156

157
      static Test::Result test_single_ava_round_trip() {
1✔
158
         Test::Result result("X509_DN string round-trip (single-AVA RDNs)");
1✔
159
         Botan::X509_DN dn;
1✔
160
         dn.add_attribute("X520.CommonName", "Alice");
1✔
161
         dn.add_attribute("X520.Organization", "Example");
1✔
162

163
         const std::string s = format(dn);
1✔
164
         result.test_str_eq("expected serialization", s, R"(CN="Alice",O="Example")");
1✔
165

166
         const Botan::X509_DN parsed = parse(s);
1✔
167
         result.test_sz_eq("two RDNs", parsed.count(), size_t(2));
1✔
168
         result.test_is_true("parses back to equal DN", parsed == dn);
1✔
169
         return result;
1✔
170
      }
1✔
171

172
      static Test::Result test_multi_ava_rdn_emits_plus() {
1✔
173
         Test::Result result("X509_DN string output uses '+' within RDN");
1✔
174
         Botan::X509_DN dn;
1✔
175
         dn.add_rdn({{Botan::OID::from_string("X520.CommonName"), Botan::ASN1_String("Alice")},
5✔
176
                     {Botan::OID::from_string("X520.Organization"), Botan::ASN1_String("Example")}});
2✔
177

178
         const std::string s = format(dn);
1✔
179
         result.test_str_eq("multi-AVA RDN uses '+' separator", s, R"(CN="Alice"+O="Example")");
1✔
180
         return result;
2✔
181
      }
4✔
182

183
      static Test::Result test_multi_ava_rdn_round_trip() {
1✔
184
         Test::Result result("X509_DN string round-trip (multi-AVA RDN)");
1✔
185
         Botan::X509_DN dn;
1✔
186
         dn.add_rdn({{Botan::OID::from_string("X520.CommonName"), Botan::ASN1_String("Alice")},
5✔
187
                     {Botan::OID::from_string("X520.Organization"), Botan::ASN1_String("Example")}});
2✔
188

189
         const std::string s = format(dn);
1✔
190
         const Botan::X509_DN parsed = parse(s);
1✔
191

192
         result.test_sz_eq("one RDN", parsed.count(), size_t(1));
1✔
193
         result.test_sz_eq("two AVAs in RDN", parsed.rdns().at(0).size(), size_t(2));
1✔
194
         result.test_is_true("parses back to equal DN", parsed == dn);
1✔
195
         result.test_str_eq("re-emits identical string", format(parsed), s);
1✔
196
         return result;
1✔
197
      }
4✔
198

199
      static Test::Result test_parse_multi_ava_rdn() {
1✔
200
         Test::Result result("X509_DN parses '+'-separated AVAs into one RDN");
1✔
201
         const Botan::X509_DN parsed = parse(R"(CN="Alice"+O="Example")");
1✔
202
         result.test_sz_eq("one RDN", parsed.count(), size_t(1));
1✔
203
         result.test_sz_eq("two AVAs in that RDN", parsed.rdns().at(0).size(), size_t(2));
1✔
204

205
         // ',' continues to act as the RDN separator.
206
         const Botan::X509_DN comma = parse(R"(CN="Alice",O="Example")");
1✔
207
         result.test_sz_eq("',' yields two RDNs", comma.count(), size_t(2));
1✔
208
         result.test_sz_eq("each RDN has one AVA", comma.rdns().at(0).size(), size_t(1));
1✔
209
         result.test_is_false("two distinct groupings", parsed == comma);
1✔
210
         return result;
1✔
211
      }
1✔
212

213
      static Test::Result test_mixed_single_and_multi_ava_round_trip() {
1✔
214
         Test::Result result("X509_DN string round-trip (mixed RDNs)");
1✔
215
         Botan::X509_DN dn;
1✔
216
         dn.add_attribute("X520.Country", "US");
1✔
217
         dn.add_rdn({{Botan::OID::from_string("X520.CommonName"), Botan::ASN1_String("Alice")},
5✔
218
                     {Botan::OID::from_string("X520.Organization"), Botan::ASN1_String("Example")}});
2✔
219
         dn.add_attribute("X520.OrganizationalUnit", "Eng");
1✔
220

221
         const std::string s = format(dn);
1✔
222
         result.test_str_eq("mixed RDN format", s, R"(C="US",CN="Alice"+O="Example",OU="Eng")");
1✔
223

224
         const Botan::X509_DN parsed = parse(s);
1✔
225
         result.test_sz_eq("three RDNs", parsed.count(), size_t(3));
1✔
226
         result.test_sz_eq("first is single AVA", parsed.rdns().at(0).size(), size_t(1));
1✔
227
         result.test_sz_eq("second is multi-AVA", parsed.rdns().at(1).size(), size_t(2));
1✔
228
         result.test_sz_eq("third is single AVA", parsed.rdns().at(2).size(), size_t(1));
1✔
229
         result.test_is_true("round-trips equal", parsed == dn);
1✔
230
         return result;
1✔
231
      }
4✔
232

233
      static Test::Result test_quoted_plus_in_value_not_split() {
1✔
234
         Test::Result result("X509_DN parser treats '+' inside quotes as data");
1✔
235
         const Botan::X509_DN parsed = parse(R"(CN="A+B")");
1✔
236
         result.test_sz_eq("one RDN", parsed.count(), size_t(1));
1✔
237
         result.test_sz_eq("one AVA", parsed.rdns().at(0).size(), size_t(1));
1✔
238
         result.test_str_eq("value preserved", parsed.get_first_attribute("CN"), "A+B");
1✔
239
         return result;
1✔
240
      }
1✔
241

242
      static Test::Result test_parse_rejects_trailing_separator_with_whitespace() {
1✔
243
         Test::Result result("X509_DN parser rejects trailing separators");
1✔
244

245
         // The test vector harness strips trailing whitespace from the input, so
246
         // the whitespace-after-separator forms are checked here directly.
247
         for(const auto* input : {"CN=A,   ", "CN=A+   ", "CN=A, \t"}) {
4✔
248
            result.test_is_false(std::string("rejects '") + input + "'", Botan::X509_DN::parse(input).has_value());
12✔
249
         }
250

251
         // A separator with a following AVA is still accepted
252
         result.test_is_true("accepts CN=A, O=B", Botan::X509_DN::parse("CN=A, O=B").has_value());
1✔
253
         return result;
1✔
254
      }
×
255

256
      static Test::Result test_decode_failure_leaves_dn_unchanged() {
1✔
257
         Test::Result result("X509_DN decode failure leaves DN unchanged");
1✔
258

259
         Botan::X509_DN dn;
1✔
260
         dn.add_attribute("X520.CommonName", "Original");
1✔
261
         const Botan::X509_DN original = dn;
1✔
262

263
         const auto invalid_dn = Botan::hex_decode("3010310C300A06035504030C034261643100");
1✔
264
         result.test_throws("invalid empty RDN rejected", [&] {
1✔
265
            Botan::BER_Decoder bd(invalid_dn);
1✔
266
            dn.decode_from(bd);
1✔
267
         });
1✔
268

269
         result.test_str_eq("string form unchanged", format(dn), format(original));
1✔
270
         result.test_is_true("DN comparison unchanged", dn == original);
1✔
271
         return result;
1✔
272
      }
1✔
273

274
      static Test::Result test_value_escaping_round_trips() {
1✔
275
         Test::Result result("X509_DN value escaping and round-trip");
1✔
276

277
         auto has_raw_control_byte = [](std::string_view s) -> bool {
6✔
278
            return std::any_of(s.begin(), s.end(), [](char c) { return Botan::is_ascii_control_char(c); });
272✔
279
         };
280

281
         auto check_cn = [&](const std::string& label, std::string_view value) -> std::string {
6✔
282
            // Render a DN with CN=value and check the invariants that hold for any
283
            // value: the rendering has no raw C0/DEL control byte, and it parses back
284
            // to the exact value and re-renders identically. Returns the rendering.
285
            Botan::X509_DN dn;
5✔
286
            dn.add_attribute("X520.CommonName", value);
5✔
287
            const std::string s = format(dn);
5✔
288

289
            result.test_is_false(label + ": no raw control byte", has_raw_control_byte(s));
5✔
290
            const Botan::X509_DN parsed = parse(s);
5✔
291
            result.test_str_eq(label + ": value preserved", parsed.get_first_attribute("CN"), value);
10✔
292
            result.test_str_eq(label + ": re-emits identically", format(parsed), s);
10✔
293
            return s;
5✔
294
         };
5✔
295

296
         const std::string all_ascii = []() {
3✔
297
            std::string s;
1✔
298
            for(uint8_t b = 0x01; b <= 0x7F; ++b) {
128✔
299
               s.push_back(static_cast<char>(b));
127✔
300
            }
301
            return s;
1✔
302
         }();
1✔
303

304
         const std::vector<std::pair<std::string, std::string>> cases = {
1✔
305
            {"embedded newline", "This\nThat"},
306
            {"terminal escape", "ACME\x1b[2J\x1b[31mTRUSTED"},
307
            {"all ASCII bytes", all_ascii},
308
            {"embedded NUL", std::string("a\0b", 3)},
1✔
309
         };
6✔
310
         for(const auto& [label, value] : cases) {
5✔
311
            check_cn(label, value);
8✔
312
         }
313

314
         // Normal UTF-8 is unmodified
315
         const std::string utf8 = check_cn("printable UTF-8", "Fräulein");
1✔
316
         result.test_is_true("UTF-8 not escaped", utf8.find('\\') == std::string::npos);
1✔
317

318
         return result;
1✔
319
      }
3✔
320
};
321

322
BOTAN_REGISTER_TEST("x509", "x509_dn_string", X509_DN_String_Tests);
323
#endif
324

325
}  // namespace
326

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