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

randombit / botan / 24872040145

24 Apr 2026 01:10AM UTC coverage: 89.401% (-0.07%) from 89.474%
24872040145

push

github

web-flow
Merge pull request #5539 from randombit/jack/cert-cache

Refactor and optimize Windows certificate store

106671 of 119318 relevant lines covered (89.4%)

11479699.69 hits per line

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

93.33
/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/pkix_types.h>
13
   #include <botan/x509cert.h>
14
   #include <botan/x509path.h>
15
   #include <botan/internal/calendar.h>
16
#endif
17

18
namespace Botan_Tests {
19

20
namespace {
21

22
#if defined(BOTAN_HAS_X509_CERTIFICATES) && defined(BOTAN_HAS_RSA) && defined(BOTAN_HAS_EMSA_PKCS1) && \
23
   defined(BOTAN_TARGET_OS_HAS_FILESYSTEM)
24

25
class Name_Constraint_Tests final : public Test {
1✔
26
   public:
27
      std::vector<Test::Result> run() override {
1✔
28
         const std::vector<std::tuple<std::string, std::string, std::string, std::string>> test_cases = {
1✔
29
            std::make_tuple("Root_Email_Name_Constraint.crt",
1✔
30
                            "Invalid_Email_Name_Constraint.crt",
31
                            "",
32
                            "Certificate does not pass name constraint"),
33
            std::make_tuple("Root_DN_Name_Constraint.crt",
1✔
34
                            "Invalid_DN_Name_Constraint.crt",
35
                            "",
36
                            "Certificate does not pass name constraint"),
37
            std::make_tuple("Root_DN_Name_Constraint.crt", "Valid_DN_Name_Constraint.crt", "", "Verified"),
1✔
38
            std::make_tuple(
1✔
39
               "Root_DNS_Name_Constraint.crt", "Valid_DNS_Name_Constraint.crt", "aexample.com", "Verified"),
40
            std::make_tuple("Root_IP_Name_Constraint.crt", "Valid_IP_Name_Constraint.crt", "", "Verified"),
1✔
41
            std::make_tuple("Root_IP_Name_Constraint.crt",
1✔
42
                            "Invalid_IP_Name_Constraint.crt",
43
                            "",
44
                            "Certificate does not pass name constraint"),
45
         };
8✔
46
         std::vector<Test::Result> results;
1✔
47
         const Botan::Path_Validation_Restrictions restrictions(false, 80);
2✔
48

49
         const std::chrono::system_clock::time_point validation_time =
1✔
50
            Botan::calendar_point(2016, 10, 21, 4, 20, 0).to_std_timepoint();
1✔
51

52
         for(const auto& t : test_cases) {
7✔
53
            const Botan::X509_Certificate root(Test::data_file("x509/name_constraint/" + std::get<0>(t)));
12✔
54
            const Botan::X509_Certificate sub(Test::data_file("x509/name_constraint/" + std::get<1>(t)));
12✔
55
            Botan::Certificate_Store_In_Memory trusted;
6✔
56
            Test::Result result("X509v3 Name Constraints: " + std::get<1>(t));
6✔
57

58
            trusted.add_certificate(root);
6✔
59
            Botan::Path_Validation_Result path_result = Botan::x509_path_validate(
6✔
60
               sub, restrictions, trusted, std::get<2>(t), Botan::Usage_Type::TLS_SERVER_AUTH, validation_time);
6✔
61

62
            if(path_result.successful_validation() && path_result.trust_root() != root) {
6✔
63
               path_result = Botan::Path_Validation_Result(Botan::Certificate_Status_Code::CANNOT_ESTABLISH_TRUST);
×
64
            }
65

66
            result.test_str_eq("validation result", path_result.result_string(), std::get<3>(t));
6✔
67
            results.emplace_back(result);
6✔
68
         }
6✔
69

70
         return results;
1✔
71
      }
2✔
72
};
73

74
BOTAN_REGISTER_TEST("x509", "x509_path_name_constraint", Name_Constraint_Tests);
75

76
// Verify that DNS constraints are case-insensitive also when falling back to the CN
77
class Name_Constraint_Excluded_CN_Case_Test final : public Test {
1✔
78
   public:
79
      std::vector<Test::Result> run() override {
1✔
80
         Test::Result result("X509v3 Name Constraints: excluded DNS with mixed-case CN and no SAN");
1✔
81

82
         const Botan::X509_Certificate root(
1✔
83
            Test::data_file("x509/name_constraint/Root_DNS_Excluded_Mixed_Case_CN.crt"));
2✔
84
         const Botan::X509_Certificate leaf(
1✔
85
            Test::data_file("x509/name_constraint/Invalid_DNS_Excluded_Mixed_Case_CN.crt"));
2✔
86

87
         Botan::Certificate_Store_In_Memory trusted;
1✔
88
         trusted.add_certificate(root);
1✔
89

90
         const Botan::Path_Validation_Restrictions restrictions(false, 80);
2✔
91
         const auto validation_time = Botan::calendar_point(2026, 6, 1, 0, 0, 0).to_std_timepoint();
1✔
92

93
         const auto path_result = Botan::x509_path_validate(
1✔
94
            leaf, restrictions, trusted, "" /* hostname */, Botan::Usage_Type::UNSPECIFIED, validation_time);
1✔
95

96
         result.test_str_eq(
1✔
97
            "validation result", path_result.result_string(), "Certificate does not pass name constraint");
1✔
98

99
         return {result};
3✔
100
      }
2✔
101
};
102

103
BOTAN_REGISTER_TEST("x509", "x509_name_constraint_excluded_cn_case", Name_Constraint_Excluded_CN_Case_Test);
104

105
class Name_Constraint_IPv6_Chain_Tests final : public Test {
1✔
106
   public:
107
      std::vector<Test::Result> run() override {
1✔
108
         struct Case {
1✔
109
               std::string label;
110
               std::string dir;
111
               std::vector<std::string> intermediates;
112
               std::string leaf;
113
               bool accept;
114
         };
115

116
         const std::vector<Case> cases = {
1✔
117
            // IPv6 permittedSubtree 2001:db8::/32
118
            {"IPv6 permit: SAN inside subtree", "permitted", {}, "leaf_valid.pem", true},
1✔
119
            {"IPv6 permit: SAN outside subtree", "permitted", {}, "leaf_invalid.pem", false},
120

121
            // IPv6 excludedSubtree 2001:db8::/32
122
            {"IPv6 exclude: SAN outside subtree", "excluded", {}, "leaf_valid.pem", true},
123
            {"IPv6 exclude: SAN inside subtree", "excluded", {}, "leaf_invalid.pem", false},
124

125
            // Root permits only IPv4 10.0.0.0/8, so an IPv6 SAN must be rejected
126
            // because iPAddress is a single GeneralName form (RFC 5280 4.2.1.10).
127
            {"IPv4-only permit: IPv4 SAN", "cross_v4only", {}, "leaf_valid.pem", true},
128
            {"IPv4-only permit: IPv6 SAN", "cross_v4only", {}, "leaf_invalid.pem", false},
129

130
            // Similar to previous - root permits only IPv6 2001:db8::/32 so IPv4 must be rejected
131
            {"IPv6-only permit: IPv6 SAN", "cross_v6only", {}, "leaf_valid.pem", true},
132
            {"IPv6-only permit: IPv4 SAN", "cross_v6only", {}, "leaf_invalid.pem", false},
133

134
            // Constraints across multiple issuers
135
            // - root permits {10/8, 2001:db8::/32}
136
            // - intermediate narrows permits to {10.1/16, 2001:db8:cafe::/48} and excludes
137
            //   {10.1.99/24, 2001:db8:cafe:bad::/64}.
138
            //
139
            // Every leaf has one IPv4 and one IPv6 SAN
140
            {"Mixed v4+v6: all SANs in range", "mixed_multi", {"int.pem"}, "leaf_valid.pem", true},
141
            {"Mixed v4+v6: IPv4 outside int permit", "mixed_multi", {"int.pem"}, "leaf_invalid_int_v4.pem", false},
142
            {"Mixed v4+v6: IPv6 outside int permit", "mixed_multi", {"int.pem"}, "leaf_invalid_int_v6.pem", false},
143
            {"Mixed v4+v6: IPv4 hits int exclude", "mixed_multi", {"int.pem"}, "leaf_invalid_excl_v4.pem", false},
144
            {"Mixed v4+v6: IPv6 hits int exclude", "mixed_multi", {"int.pem"}, "leaf_invalid_excl_v6.pem", false},
145
            {"Mixed v4+v6: IPv4 outside root permit", "mixed_multi", {"int.pem"}, "leaf_invalid_root_v4.pem", false},
146
            {"Mixed v4+v6: IPv6 outside root permit", "mixed_multi", {"int.pem"}, "leaf_invalid_root_v6.pem", false},
147

148
            // Here the root excludes IPv4 10.0.0.0/8 and the leaf certs have IPv6 SAN; the
149
            // invalid leaf has a IPv4-mapped IPv6 address matching 10.0.0.0/8 while the valid leaf
150
            // has some other IPv6 address which is not excluded
151
            {"v4 exclude: mapped-v6 SAN inside v4 excl", "v4_exclude_mapped", {}, "leaf_invalid.pem", false},
152
            {"v4 exclude: mapped-v6 SAN outside v4 excl", "v4_exclude_mapped", {}, "leaf_valid.pem", true},
153

154
            // Here the root permits only IPv4 10.0.0.0/8. The invalid leaf has an IPv6 address in the SAN
155
            // which should be rejected as not being in the range. The 'valid' leaf is a questionable case: it
156
            // has an IPv6 SAN which is an IPv4-mapped IPv6 address inside 10.0.0.0/8. Arguably it really is
157
            // valid; RFC 5280 is silent on the issue. But lacking a clear consensus, it is rejected for now.
158
            {"v4 permit: mapped-v6 SAN inside v4 permit", "v4_permit_mapped", {}, "leaf_valid.pem", false},
159
            {"v4 permit: mapped-v6 SAN outside v4 permit", "v4_permit_mapped", {}, "leaf_invalid.pem", false},
160
         };
20✔
161

162
         const Botan::Path_Validation_Restrictions restrictions(false, 80);
2✔
163

164
         const auto validation_time = Botan::calendar_point(2027, 4, 22, 20, 0, 0).to_std_timepoint();
1✔
165

166
         std::vector<Test::Result> results;
1✔
167
         for(const auto& c : cases) {
20✔
168
            const std::string base = "x509/name_constraint_ipv6/" + c.dir + "/";
38✔
169
            const Botan::X509_Certificate root(Test::data_file(base + "root.pem"));
38✔
170
            const Botan::X509_Certificate leaf(Test::data_file(base + c.leaf));
38✔
171

172
            std::vector<Botan::X509_Certificate> chain{leaf};
38✔
173
            for(const auto& intermediate_file : c.intermediates) {
26✔
174
               chain.emplace_back(Test::data_file(base + intermediate_file));
14✔
175
            }
176

177
            Botan::Certificate_Store_In_Memory trusted;
19✔
178
            trusted.add_certificate(root);
19✔
179

180
            const auto pv = Botan::x509_path_validate(
19✔
181
               chain, restrictions, trusted, "" /* hostname */, Botan::Usage_Type::UNSPECIFIED, validation_time);
19✔
182

183
            Test::Result result("X509v3 Name Constraints (IPv6 chains): " + c.label);
19✔
184

185
            const std::string expected = c.accept ? "Verified" : "Certificate does not pass name constraint";
32✔
186
            result.test_str_eq("path validation result", pv.result_string(), expected);
19✔
187
            results.emplace_back(std::move(result));
19✔
188
         }
19✔
189

190
         return results;
1✔
191
      }
22✔
192
};
193

194
BOTAN_REGISTER_TEST("x509", "x509_name_constraint_ipv6_chains", Name_Constraint_IPv6_Chain_Tests);
195

196
/*
197
* Validate that GeneralName iPAddress decoding rejects masks that are not a
198
* contiguous CIDR prefix. Drives the decoder with hand-rolled BER for a
199
* single [7] IMPLICIT OCTET STRING carrying {net || mask}.
200
*/
201
class Name_Constraint_IP_Mask_Tests final : public Text_Based_Test {
×
202
   public:
203
      Name_Constraint_IP_Mask_Tests() : Text_Based_Test("x509/general_name_ip.vec", "Address,Netmask") {}
2✔
204

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

208
         const auto address = vars.get_req_bin("Address");
19✔
209
         const auto netmask = vars.get_req_bin("Netmask");
19✔
210

211
         const auto der = encode_address(address, netmask);
19✔
212

213
         Botan::BER_Decoder decoder(der, Botan::BER_Decoder::Limits::DER());
19✔
214
         Botan::GeneralName gn;
19✔
215

216
         if(header == "Valid") {
19✔
217
            try {
12✔
218
               gn.decode_from(decoder);
12✔
219
               result.test_success("Accepted valid GeneralName IP encoding");
12✔
220
            } catch(Botan::Decoding_Error&) {
×
221
               result.test_failure("Rejected valid GeneralName IP encoding");
×
222
            }
×
223
         } else {
224
            try {
7✔
225
               gn.decode_from(decoder);
7✔
226
               result.test_failure("Accepted invalid GeneralName IP encoding");
×
227
            } catch(Botan::Decoding_Error&) {
7✔
228
               result.test_success("Rejected invalid GeneralName IP encoding");
7✔
229
            }
7✔
230
         }
231

232
         return result;
38✔
233
      }
19✔
234

235
   private:
236
      static std::vector<uint8_t> encode_address(std::span<const uint8_t> address, std::span<const uint8_t> netmask) {
19✔
237
         std::vector<uint8_t> der;
19✔
238
         // [7] IMPLICIT OCTET STRING, primitive, context-specific.
239
         der.push_back(0x87);
19✔
240
         // Short for length is sufficient here
241
         der.push_back(static_cast<uint8_t>(address.size() + netmask.size()));
19✔
242
         der.insert(der.end(), address.begin(), address.end());
19✔
243
         der.insert(der.end(), netmask.begin(), netmask.end());
19✔
244
         return der;
19✔
245
      }
×
246
};
247

248
BOTAN_REGISTER_TEST("x509", "x509_name_constraint_ip_mask", Name_Constraint_IP_Mask_Tests);
249

250
#endif
251

252
}  // namespace
253

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