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

randombit / botan / 27253840577

08 Jun 2026 09:36PM UTC coverage: 89.367% (+0.01%) from 89.356%
27253840577

push

github

web-flow
Merge pull request #5657 from randombit/jack/ctrl-escaping

Escape control characters in codec error reporting and DN printing

110761 of 123940 relevant lines covered (89.37%)

11052282.15 hits per line

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

97.58
/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_String_Tests final : public Test {
1✔
67
   public:
68
      std::vector<Test::Result> run() override {
1✔
69
         std::vector<Test::Result> results;
1✔
70
         results.push_back(test_single_ava_round_trip());
2✔
71
         results.push_back(test_multi_ava_rdn_emits_plus());
2✔
72
         results.push_back(test_multi_ava_rdn_round_trip());
2✔
73
         results.push_back(test_parse_multi_ava_rdn());
2✔
74
         results.push_back(test_mixed_single_and_multi_ava_round_trip());
2✔
75
         results.push_back(test_quoted_plus_in_value_not_split());
2✔
76
         results.push_back(test_decode_failure_leaves_dn_unchanged());
2✔
77
         results.push_back(test_value_escaping_round_trips());
2✔
78
         return results;
1✔
79
      }
×
80

81
   private:
82
      static Botan::X509_DN parse(std::string_view s) {
11✔
83
         Botan::X509_DN dn;
11✔
84
         std::istringstream iss{std::string(s)};
22✔
85
         iss >> dn;
11✔
86
         return dn;
11✔
87
      }
11✔
88

89
      static std::string format(const Botan::X509_DN& dn) {
17✔
90
         std::ostringstream oss;
17✔
91
         oss << dn;
17✔
92
         return oss.str();
34✔
93
      }
17✔
94

95
      static Test::Result test_single_ava_round_trip() {
1✔
96
         Test::Result result("X509_DN string round-trip (single-AVA RDNs)");
1✔
97
         Botan::X509_DN dn;
1✔
98
         dn.add_attribute("X520.CommonName", "Alice");
1✔
99
         dn.add_attribute("X520.Organization", "Example");
1✔
100

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

104
         const Botan::X509_DN parsed = parse(s);
1✔
105
         result.test_sz_eq("two RDNs", parsed.count(), size_t(2));
1✔
106
         result.test_is_true("parses back to equal DN", parsed == dn);
1✔
107
         return result;
1✔
108
      }
1✔
109

110
      static Test::Result test_multi_ava_rdn_emits_plus() {
1✔
111
         Test::Result result("X509_DN string output uses '+' within RDN");
1✔
112
         Botan::X509_DN dn;
1✔
113
         dn.add_rdn({{Botan::OID::from_string("X520.CommonName"), Botan::ASN1_String("Alice")},
5✔
114
                     {Botan::OID::from_string("X520.Organization"), Botan::ASN1_String("Example")}});
2✔
115

116
         const std::string s = format(dn);
1✔
117
         result.test_str_eq("multi-AVA RDN uses '+' separator", s, R"(CN="Alice"+O="Example")");
1✔
118
         return result;
2✔
119
      }
4✔
120

121
      static Test::Result test_multi_ava_rdn_round_trip() {
1✔
122
         Test::Result result("X509_DN string round-trip (multi-AVA RDN)");
1✔
123
         Botan::X509_DN dn;
1✔
124
         dn.add_rdn({{Botan::OID::from_string("X520.CommonName"), Botan::ASN1_String("Alice")},
5✔
125
                     {Botan::OID::from_string("X520.Organization"), Botan::ASN1_String("Example")}});
2✔
126

127
         const std::string s = format(dn);
1✔
128
         const Botan::X509_DN parsed = parse(s);
1✔
129

130
         result.test_sz_eq("one RDN", parsed.count(), size_t(1));
1✔
131
         result.test_sz_eq("two AVAs in RDN", parsed.rdns().at(0).size(), size_t(2));
1✔
132
         result.test_is_true("parses back to equal DN", parsed == dn);
1✔
133
         result.test_str_eq("re-emits identical string", format(parsed), s);
1✔
134
         return result;
1✔
135
      }
4✔
136

137
      static Test::Result test_parse_multi_ava_rdn() {
1✔
138
         Test::Result result("X509_DN parses '+'-separated AVAs into one RDN");
1✔
139
         const Botan::X509_DN parsed = parse(R"(CN="Alice"+O="Example")");
1✔
140
         result.test_sz_eq("one RDN", parsed.count(), size_t(1));
1✔
141
         result.test_sz_eq("two AVAs in that RDN", parsed.rdns().at(0).size(), size_t(2));
1✔
142

143
         // ',' continues to act as the RDN separator.
144
         const Botan::X509_DN comma = parse(R"(CN="Alice",O="Example")");
1✔
145
         result.test_sz_eq("',' yields two RDNs", comma.count(), size_t(2));
1✔
146
         result.test_sz_eq("each RDN has one AVA", comma.rdns().at(0).size(), size_t(1));
1✔
147
         result.test_is_false("two distinct groupings", parsed == comma);
1✔
148
         return result;
1✔
149
      }
1✔
150

151
      static Test::Result test_mixed_single_and_multi_ava_round_trip() {
1✔
152
         Test::Result result("X509_DN string round-trip (mixed RDNs)");
1✔
153
         Botan::X509_DN dn;
1✔
154
         dn.add_attribute("X520.Country", "US");
1✔
155
         dn.add_rdn({{Botan::OID::from_string("X520.CommonName"), Botan::ASN1_String("Alice")},
5✔
156
                     {Botan::OID::from_string("X520.Organization"), Botan::ASN1_String("Example")}});
2✔
157
         dn.add_attribute("X520.OrganizationalUnit", "Eng");
1✔
158

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

162
         const Botan::X509_DN parsed = parse(s);
1✔
163
         result.test_sz_eq("three RDNs", parsed.count(), size_t(3));
1✔
164
         result.test_sz_eq("first is single AVA", parsed.rdns().at(0).size(), size_t(1));
1✔
165
         result.test_sz_eq("second is multi-AVA", parsed.rdns().at(1).size(), size_t(2));
1✔
166
         result.test_sz_eq("third is single AVA", parsed.rdns().at(2).size(), size_t(1));
1✔
167
         result.test_is_true("round-trips equal", parsed == dn);
1✔
168
         return result;
1✔
169
      }
4✔
170

171
      static Test::Result test_quoted_plus_in_value_not_split() {
1✔
172
         Test::Result result("X509_DN parser treats '+' inside quotes as data");
1✔
173
         const Botan::X509_DN parsed = parse(R"(CN="A+B")");
1✔
174
         result.test_sz_eq("one RDN", parsed.count(), size_t(1));
1✔
175
         result.test_sz_eq("one AVA", parsed.rdns().at(0).size(), size_t(1));
1✔
176
         result.test_str_eq("value preserved", parsed.get_first_attribute("CN"), "A+B");
1✔
177
         return result;
1✔
178
      }
1✔
179

180
      static Test::Result test_decode_failure_leaves_dn_unchanged() {
1✔
181
         Test::Result result("X509_DN decode failure leaves DN unchanged");
1✔
182

183
         Botan::X509_DN dn;
1✔
184
         dn.add_attribute("X520.CommonName", "Original");
1✔
185
         const Botan::X509_DN original = dn;
1✔
186

187
         const auto invalid_dn = Botan::hex_decode("3010310C300A06035504030C034261643100");
1✔
188
         result.test_throws("invalid empty RDN rejected", [&] {
1✔
189
            Botan::BER_Decoder bd(invalid_dn);
1✔
190
            dn.decode_from(bd);
1✔
191
         });
1✔
192

193
         result.test_str_eq("string form unchanged", format(dn), format(original));
1✔
194
         result.test_is_true("DN comparison unchanged", dn == original);
1✔
195
         return result;
1✔
196
      }
1✔
197

198
      static Test::Result test_value_escaping_round_trips() {
1✔
199
         Test::Result result("X509_DN value escaping and round-trip");
1✔
200

201
         auto has_raw_control_byte = [](std::string_view s) -> bool {
6✔
202
            return std::any_of(s.begin(), s.end(), [](char c) { return Botan::is_ascii_control_char(c); });
272✔
203
         };
204

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

213
            result.test_is_false(label + ": no raw control byte", has_raw_control_byte(s));
5✔
214
            const Botan::X509_DN parsed = parse(s);
5✔
215
            result.test_str_eq(label + ": value preserved", parsed.get_first_attribute("CN"), value);
10✔
216
            result.test_str_eq(label + ": re-emits identically", format(parsed), s);
10✔
217
            return s;
5✔
218
         };
5✔
219

220
         const std::string all_ascii = []() {
3✔
221
            std::string s;
1✔
222
            for(uint8_t b = 0x01; b <= 0x7F; ++b) {
128✔
223
               s.push_back(static_cast<char>(b));
127✔
224
            }
225
            return s;
1✔
226
         }();
1✔
227

228
         const std::vector<std::pair<std::string, std::string>> cases = {
1✔
229
            {"embedded newline", "This\nThat"},
230
            {"terminal escape", "ACME\x1b[2J\x1b[31mTRUSTED"},
231
            {"all ASCII bytes", all_ascii},
232
            {"embedded NUL", std::string("a\0b", 3)},
1✔
233
         };
6✔
234
         for(const auto& [label, value] : cases) {
5✔
235
            check_cn(label, value);
8✔
236
         }
237

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

242
         return result;
1✔
243
      }
3✔
244
};
245

246
BOTAN_REGISTER_TEST("x509", "x509_dn_string", X509_DN_String_Tests);
247
#endif
248

249
}  // namespace
250

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