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

randombit / botan / 29630228535

18 Jul 2026 02:46AM UTC coverage: 89.407% (-2.3%) from 91.66%
29630228535

push

github

web-flow
Merge pull request #5740 from randombit/jack/x509-serial-number

Add an explicit type for X509 certificate serial numbers

114089 of 127606 relevant lines covered (89.41%)

10801388.62 hits per line

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

97.18
/src/tests/test_ocsp.cpp
1
/*
2
* (C) 2016 Jack Lloyd
3
* (C) 2022 René Meusel, 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_OCSP)
11
   #include "test_arb_eq.h"
12
   #include <botan/ber_dec.h>
13
   #include <botan/certstor.h>
14
   #include <botan/der_enc.h>
15
   #include <botan/ocsp.h>
16
   #include <botan/x509path.h>
17
   #include <botan/internal/calendar.h>
18
#endif
19

20
namespace Botan_Tests {
21

22
namespace {
23

24
#if defined(BOTAN_HAS_OCSP) && defined(BOTAN_HAS_RSA) && defined(BOTAN_HAS_EMSA_PKCS1) && \
25
   defined(BOTAN_TARGET_OS_HAS_FILESYSTEM)
26

27
class OCSP_Tests final : public Test {
1✔
28
   private:
29
      static Botan::X509_Certificate load_test_X509_cert(const std::string& path) {
33✔
30
         return Botan::X509_Certificate(Test::data_file(path));
66✔
31
      }
32

33
      static Botan::OCSP::Response load_test_OCSP_resp(const std::string& path) {
12✔
34
         return Botan::OCSP::Response(Test::read_binary_data_file(path));
24✔
35
      }
36

37
      static Test::Result test_certid_serial_sign() {
1✔
38
         Test::Result result("OCSP CertID serial matching respects sign");
1✔
39

40
         const std::string base = "x509/serial_numbers/";
1✔
41
         const auto ca = load_test_X509_cert(base + "ca.pem");
1✔
42
         const auto pos = load_test_X509_cert(base + "pos255.pem");
1✔
43
         const auto neg = load_test_X509_cert(base + "neg255.pem");
1✔
44

45
         const Botan::OCSP::CertID neg_id(ca, neg.serial());
1✔
46
         result.test_is_true("CertID matches its own cert", neg_id.is_id_for(ca, neg));
1✔
47
         result.test_is_false("CertID does not match same-magnitude positive serial", neg_id.is_id_for(ca, pos));
1✔
48

49
         const Botan::OCSP::CertID pos_id(ca, pos.serial());
1✔
50
         result.test_is_true("positive CertID matches its own cert", pos_id.is_id_for(ca, pos));
1✔
51
         result.test_is_false("positive CertID does not match negative serial", pos_id.is_id_for(ca, neg));
1✔
52

53
         // The sign survives an encode/decode round trip
54
         std::vector<uint8_t> der;
1✔
55
         Botan::DER_Encoder enc(der);
1✔
56
         neg_id.encode_into(enc);
1✔
57
         Botan::OCSP::CertID neg_id_rt;
1✔
58
         Botan::BER_Decoder dec(der);
1✔
59
         neg_id_rt.decode_from(dec);
1✔
60
         result.test_is_true("round-tripped CertID matches its own cert", neg_id_rt.is_id_for(ca, neg));
1✔
61
         result.test_is_false("round-tripped CertID does not match positive serial", neg_id_rt.is_id_for(ca, pos));
1✔
62

63
         return result;
2✔
64
      }
1✔
65

66
      static Test::Result test_response_parsing() {
1✔
67
         Test::Result result("OCSP response parsing");
1✔
68

69
         // Simple parsing tests
70
         const std::vector<std::string> ocsp_input_paths = {
1✔
71
            "x509/ocsp/resp1.der", "x509/ocsp/resp2.der", "x509/ocsp/resp3.der"};
1✔
72

73
         for(const std::string& ocsp_input_path : ocsp_input_paths) {
4✔
74
            try {
3✔
75
               const Botan::OCSP::Response resp(Test::read_binary_data_file(ocsp_input_path));
6✔
76
               result.test_enum_eq(
6✔
77
                  "parsing was successful", resp.status(), Botan::OCSP::Response_Status_Code::Successful);
3✔
78
               result.test_success("Parsed input " + ocsp_input_path);
3✔
79
            } catch(Botan::Exception& e) {
3✔
80
               result.test_failure("Parsing failed", e.what());
×
81
            }
×
82
         }
83

84
         try {
1✔
85
            // Contrary to RFC 6960 this response includes a responseBytes with a non-successful status code
86
            const Botan::OCSP::Response resp(
1✔
87
               Test::read_binary_data_file("x509/ocsp/patrickschmidt_ocsp_try_later_wrong_sig.der"));
3✔
88
            result.test_failure("Accepted invalid encoding OCSP response");
×
89
            result.test_enum_eq(
×
90
               "parsing exposes correct status code", resp.status(), Botan::OCSP::Response_Status_Code::Try_Later);
×
91
         } catch(Botan::Exception&) {
1✔
92
            result.test_success("Rejected invalid encoding OCSP response");
1✔
93
         }
1✔
94

95
         // OCSPResponse SEQUENCE { ENUMERATED 0 }: successful status with no
96
         // responseBytes. RFC 6960 4.2.1 requires responseBytes when successful.
97
         const std::vector<uint8_t> successful_no_response_bytes = {0x30, 0x03, 0x0A, 0x01, 0x00};
1✔
98
         result.test_throws<Botan::Decoding_Error>("Successful status without responseBytes is rejected", [&] {
1✔
99
            const Botan::OCSP::Response resp(successful_no_response_bytes);
1✔
100
         });
×
101

102
         // OCSPResponse SEQUENCE { ENUMERATED 4 }: unknown response code
103
         const std::vector<uint8_t> failed_unknown_code = {0x30, 0x03, 0x0A, 0x01, 0x04};
1✔
104
         result.test_throws<Botan::Decoding_Error>("OCSPResponse with unknown response code is rejected",
1✔
105
                                                   [&] { const Botan::OCSP::Response resp(failed_unknown_code); });
2✔
106

107
         // SEQUENCE { ENUMERATED 3 }: bare tryLater with no trailer parses fine.
108
         const std::vector<uint8_t> try_later_no_trailer = {0x30, 0x03, 0x0A, 0x01, 0x03};
1✔
109
         result.test_no_throw("Bare non-successful status parses", [&] {
1✔
110
            const Botan::OCSP::Response r(try_later_no_trailer);
1✔
111
            result.test_enum_eq("parsed as Try_Later", r.status(), Botan::OCSP::Response_Status_Code::Try_Later);
1✔
112
         });
1✔
113

114
         return result;
1✔
115
      }
1✔
116

117
      static Test::Result test_response_with_bykey_responder_id() {
1✔
118
         // RFC 6960's ASN.1 module is "DEFINITIONS EXPLICIT TAGS" and ResponderID's
119
         // CHOICE alternatives are not marked IMPLICIT, so byKey is encoded as
120
         // constructed [2] wrapping a primitive OCTET STRING. The data below
121
         // was produced using `openssl ocsp ... -resp_key_id`
122
         Test::Result result("OCSP response with byKey ResponderID");
1✔
123

124
         const Botan::OCSP::Response resp(Test::read_binary_data_file("x509/ocsp/byKey_responderID.der"));
2✔
125
         result.test_enum_eq(
2✔
126
            "Successful response status", resp.status(), Botan::OCSP::Response_Status_Code::Successful);
1✔
127

128
         const auto responder = load_test_X509_cert("x509/ocsp/byKey_responder.pem");
1✔
129

130
         result.test_is_true("byKey response has empty signer_name", resp.signer_name().empty());
1✔
131
         result.test_sz_eq("byKey hash is 20 bytes (SHA-1)", resp.signer_key_hash().size(), 20);
1✔
132
         result.test_bin_eq("byKey hash matches responder pubkey SHA-1",
1✔
133
                            resp.signer_key_hash(),
134
                            responder.subject_public_key_bitstring_sha1());
135

136
         test_arb_eq(
2✔
137
            result, "Responder is found via byKey", resp.find_signing_certificate(responder), std::optional(responder));
2✔
138

139
         return result;
1✔
140
      }
1✔
141

142
      static Test::Result test_response_certificate_access() {
1✔
143
         Test::Result result("OCSP response certificate access");
1✔
144

145
         try {
1✔
146
            const Botan::OCSP::Response resp1(Test::read_binary_data_file("x509/ocsp/resp1.der"));
2✔
147
            const auto& certs1 = resp1.certificates();
1✔
148
            if(result.test_sz_eq("Expected count of certificates", certs1.size(), 1)) {
1✔
149
               const auto& cert = certs1.front();
1✔
150
               const Botan::X509_DN expected_dn(
1✔
151
                  {std::make_pair("X520.CommonName", "Symantec Class 3 EV SSL CA - G3 OCSP Responder")});
1✔
152
               const bool matches = cert.subject_dn() == expected_dn;
1✔
153
               result.test_is_true("CN matches expected", matches);
1✔
154
            }
1✔
155

156
            const Botan::OCSP::Response resp2(Test::read_binary_data_file("x509/ocsp/resp2.der"));
2✔
157
            const auto& certs2 = resp2.certificates();
1✔
158
            result.test_sz_eq("Expect no certificates", certs2.size(), 0);
1✔
159
         } catch(Botan::Exception& e) {
1✔
160
            result.test_failure("Parsing failed", e.what());
×
161
         }
×
162

163
         return result;
1✔
164
      }
×
165

166
      static Test::Result test_request_encoding() {
1✔
167
         Test::Result result("OCSP request encoding");
1✔
168

169
         const Botan::X509_Certificate end_entity(Test::data_file("x509/ocsp/gmail.pem"));
2✔
170
         const Botan::X509_Certificate issuer(Test::data_file("x509/ocsp/google_g2.pem"));
2✔
171

172
         try {
1✔
173
            const Botan::OCSP::Request bogus(end_entity, issuer);
1✔
174
            result.test_failure("Bad arguments (swapped end entity, issuer) accepted");
×
175
         } catch(Botan::Invalid_Argument&) {
1✔
176
            result.test_success("Bad arguments rejected");
1✔
177
         }
1✔
178

179
         const std::string expected_request =
1✔
180
            "ME4wTKADAgEAMEUwQzBBMAkGBSsOAwIaBQAEFPLgavmFih2NcJtJGSN6qbUaKH5kBBRK3QYWG7z2aLV29YG2u2IaulqBLwIIQkg+DF+RYMY=";
1✔
181

182
         const Botan::OCSP::Request req1(issuer, end_entity);
1✔
183
         result.test_str_eq("Encoded OCSP request", req1.base64_encode(), expected_request);
1✔
184

185
         const Botan::OCSP::Request req2(issuer, BigInt::from_bytes(end_entity.serial_number()));
1✔
186
         result.test_str_eq("Encoded OCSP request", req2.base64_encode(), expected_request);
1✔
187

188
         return result;
2✔
189
      }
2✔
190

191
      static Test::Result test_response_find_signing_certificate() {
1✔
192
         Test::Result result("OCSP response finding signature certificates");
1✔
193

194
         // OCSP response is signed by the issuing CA itself
195
         auto randombit_ocsp = load_test_OCSP_resp("x509/ocsp/randombit_ocsp.der");
1✔
196
         auto randombit_ca = load_test_X509_cert("x509/ocsp/letsencrypt.pem");
1✔
197

198
         // OCSP response is signed by an authorized responder certificate
199
         // issued by the issuing CA and embedded in the response
200
         auto bdr_ocsp = load_test_OCSP_resp("x509/ocsp/bdr-ocsp-resp.der");
1✔
201
         auto bdr_responder = load_test_X509_cert("x509/ocsp/bdr-ocsp-responder.pem");
1✔
202
         auto bdr_ca = load_test_X509_cert("x509/ocsp/bdr-int.pem");
1✔
203

204
         // The response in bdr_ocsp contains two certificates
205
         if(result.test_sz_eq("both certificates found", bdr_ocsp.certificates().size(), 2)) {
1✔
206
            result.test_str_eq("first cert in response",
2✔
207
                               bdr_ocsp.certificates()[0].subject_dn().get_first_attribute("CN"),
1✔
208
                               "D-TRUST OCSP 4 2-2 EV 2016");
209
            result.test_str_eq("second cert in response",
2✔
210
                               bdr_ocsp.certificates()[1].subject_dn().get_first_attribute("CN"),
2✔
211
                               "D-TRUST CA 2-2 EV 2016");
212
         }
213

214
         // Dummy OCSP response is not signed at all
215
         auto dummy_ocsp = Botan::OCSP::Response(Botan::Certificate_Status_Code::OCSP_SERVER_NOT_AVAILABLE);
1✔
216

217
         // OCSP response is signed by 3rd party responder certificate that is
218
         // not included in the OCSP response itself
219
         // See `src/scripts/randombit_ocsp_forger.sh` for a helper script to recreate those.
220
         auto randombit_alt_resp_ocsp = load_test_OCSP_resp("x509/ocsp/randombit_ocsp_forged_valid_nocerts.der");
1✔
221
         auto randombit_alt_resp_cert = load_test_X509_cert("x509/ocsp/randombit_ocsp_forged_responder.pem");
1✔
222

223
         result.test_opt_is_null("Dummy has no signing certificate",
1✔
224
                                 dummy_ocsp.find_signing_certificate(Botan::X509_Certificate()));
2✔
225

226
         test_arb_eq(result,
2✔
227
                     "CA is returned as signing certificate",
228
                     randombit_ocsp.find_signing_certificate(randombit_ca),
1✔
229
                     std::optional(randombit_ca));
1✔
230
         result.test_opt_is_null("No signer certificate is returned when signer couldn't be determined",
1✔
231
                                 randombit_ocsp.find_signing_certificate(bdr_ca));
1✔
232

233
         test_arb_eq(result,
2✔
234
                     "Delegated responder certificate is returned for further validation",
235
                     bdr_ocsp.find_signing_certificate(bdr_ca),
1✔
236
                     std::optional(bdr_responder));
1✔
237

238
         result.test_opt_is_null(
1✔
239
            "Delegated responder without stapled certs does not find signer without user-provided certs",
240
            randombit_alt_resp_ocsp.find_signing_certificate(randombit_ca));
1✔
241

242
         auto trusted_responders = std::make_unique<Botan::Certificate_Store_In_Memory>(randombit_alt_resp_cert);
1✔
243
         test_arb_eq(result,
2✔
244
                     "Delegated responder returns user-provided cert",
245
                     randombit_alt_resp_ocsp.find_signing_certificate(randombit_ca, trusted_responders.get()),
2✔
246
                     std::optional(randombit_alt_resp_cert));
1✔
247

248
         return result;
2✔
249
      }
1✔
250

251
      static Test::Result test_response_verification_with_next_update_without_max_age() {
1✔
252
         Test::Result result("OCSP request check with next_update w/o max_age");
1✔
253

254
         auto ee = load_test_X509_cert("x509/ocsp/randombit.pem");
1✔
255
         auto ca = load_test_X509_cert("x509/ocsp/letsencrypt.pem");
1✔
256
         auto trust_root = load_test_X509_cert("x509/ocsp/geotrust.pem");
1✔
257

258
         const std::vector<Botan::X509_Certificate> cert_path = {ee, ca, trust_root};
4✔
259

260
         auto ocsp = load_test_OCSP_resp("x509/ocsp/randombit_ocsp.der");
1✔
261

262
         Botan::Certificate_Store_In_Memory certstore;
1✔
263
         certstore.add_certificate(trust_root);
1✔
264

265
         auto check_ocsp = [&](const std::chrono::system_clock::time_point valid_time,
5✔
266
                               const Botan::Certificate_Status_Code expected) {
267
            const auto ocsp_status = Botan::PKIX::check_ocsp(
12✔
268
               cert_path, {ocsp}, {&certstore}, valid_time, Botan::Path_Validation_Restrictions());
16✔
269

270
            return result.test_sz_eq("Expected size of ocsp_status", ocsp_status.size(), 2) &&
8✔
271
                   result.test_sz_eq("Expected size of ocsp_status[0]", ocsp_status[0].size(), 1) &&
4✔
272
                   result.test_is_true(std::string("Status: '") + Botan::to_string(expected) + "'",
28✔
273
                                       ocsp_status[0].contains(expected));
12✔
274
         };
8✔
275

276
         check_ocsp(Botan::calendar_point(2016, 11, 11, 12, 30, 0).to_std_timepoint(),
1✔
277
                    Botan::Certificate_Status_Code::OCSP_NOT_YET_VALID);
278
         check_ocsp(Botan::calendar_point(2016, 11, 18, 12, 30, 0).to_std_timepoint(),
1✔
279
                    Botan::Certificate_Status_Code::OCSP_RESPONSE_GOOD);
280
         check_ocsp(Botan::calendar_point(2016, 11, 20, 8, 30, 0).to_std_timepoint(),
1✔
281
                    Botan::Certificate_Status_Code::OCSP_RESPONSE_GOOD);
282
         check_ocsp(Botan::calendar_point(2016, 11, 28, 8, 30, 0).to_std_timepoint(),
1✔
283
                    Botan::Certificate_Status_Code::OCSP_HAS_EXPIRED);
284

285
         return result;
1✔
286
      }
2✔
287

288
      static Test::Result test_response_verification_with_next_update_with_max_age() {
1✔
289
         Test::Result result("OCSP request check with next_update with max_age");
1✔
290

291
         auto ee = load_test_X509_cert("x509/ocsp/randombit.pem");
1✔
292
         auto ca = load_test_X509_cert("x509/ocsp/letsencrypt.pem");
1✔
293
         auto trust_root = load_test_X509_cert("x509/ocsp/geotrust.pem");
1✔
294

295
         const std::vector<Botan::X509_Certificate> cert_path = {ee, ca, trust_root};
4✔
296

297
         auto ocsp = load_test_OCSP_resp("x509/ocsp/randombit_ocsp.der");
1✔
298

299
         Botan::Certificate_Store_In_Memory certstore;
1✔
300
         certstore.add_certificate(trust_root);
1✔
301

302
         // Some arbitrary time within the validity period of the test certs
303
         const auto max_age = std::chrono::minutes(59);
1✔
304

305
         auto check_ocsp = [&](const std::chrono::system_clock::time_point valid_time,
5✔
306
                               const Botan::Certificate_Status_Code expected) {
307
            const Botan::Path_Validation_Restrictions pvr(false, 110, false, max_age);
4✔
308
            const auto ocsp_status = Botan::PKIX::check_ocsp(cert_path, {ocsp}, {&certstore}, valid_time, pvr);
12✔
309

310
            return result.test_sz_eq("Expected size of ocsp_status", ocsp_status.size(), 2) &&
8✔
311
                   result.test_sz_eq("Expected size of ocsp_status[0]", ocsp_status[0].size(), 1) &&
4✔
312
                   result.test_is_true(std::string("Status: '") + Botan::to_string(expected) + "'",
28✔
313
                                       ocsp_status[0].contains(expected));
12✔
314
         };
8✔
315

316
         check_ocsp(Botan::calendar_point(2016, 11, 11, 12, 30, 0).to_std_timepoint(),
1✔
317
                    Botan::Certificate_Status_Code::OCSP_NOT_YET_VALID);
318
         check_ocsp(Botan::calendar_point(2016, 11, 18, 12, 30, 0).to_std_timepoint(),
1✔
319
                    Botan::Certificate_Status_Code::OCSP_RESPONSE_GOOD);
320
         check_ocsp(Botan::calendar_point(2016, 11, 20, 8, 30, 0).to_std_timepoint(),
1✔
321
                    Botan::Certificate_Status_Code::OCSP_RESPONSE_GOOD);
322
         check_ocsp(Botan::calendar_point(2016, 11, 28, 8, 30, 0).to_std_timepoint(),
1✔
323
                    Botan::Certificate_Status_Code::OCSP_HAS_EXPIRED);
324

325
         return result;
1✔
326
      }
2✔
327

328
      static Test::Result test_response_verification_without_next_update_with_max_age() {
1✔
329
         Test::Result result("OCSP request check w/o next_update with max_age");
1✔
330

331
         auto ee = load_test_X509_cert("x509/ocsp/patrickschmidt.pem");
1✔
332
         auto ca = load_test_X509_cert("x509/ocsp/bdrive_encryption.pem");
1✔
333
         auto trust_root = load_test_X509_cert("x509/ocsp/bdrive_root.pem");
1✔
334

335
         const std::vector<Botan::X509_Certificate> cert_path = {ee, ca, trust_root};
4✔
336

337
         auto ocsp = load_test_OCSP_resp("x509/ocsp/patrickschmidt_ocsp.der");
1✔
338

339
         Botan::Certificate_Store_In_Memory certstore;
1✔
340
         certstore.add_certificate(trust_root);
1✔
341

342
         // Some arbitrary time within the validity period of the test certs
343
         const auto max_age = std::chrono::minutes(59);
1✔
344

345
         auto check_ocsp = [&](const std::chrono::system_clock::time_point valid_time,
4✔
346
                               const Botan::Certificate_Status_Code expected) {
347
            const Botan::Path_Validation_Restrictions pvr(false, 110, false, max_age);
3✔
348
            const auto ocsp_status = Botan::PKIX::check_ocsp(cert_path, {ocsp}, {&certstore}, valid_time, pvr);
9✔
349

350
            result.test_sz_eq("Expected size of ocsp_status", ocsp_status.size(), 2);
3✔
351

352
            if(!ocsp_status.empty()) {
3✔
353
               result.test_sz_eq("Expected size of ocsp_status[0]", ocsp_status[0].size(), 1);
3✔
354

355
               result.test_is_true(std::string("Status: '") + Botan::to_string(expected) + "'",
15✔
356
                                   ocsp_status[0].contains(expected));
6✔
357
            }
358
         };
6✔
359

360
         check_ocsp(Botan::calendar_point(2019, 5, 28, 7, 0, 0).to_std_timepoint(),
1✔
361
                    Botan::Certificate_Status_Code::OCSP_NOT_YET_VALID);
362
         check_ocsp(Botan::calendar_point(2019, 5, 28, 7, 30, 0).to_std_timepoint(),
1✔
363
                    Botan::Certificate_Status_Code::OCSP_RESPONSE_GOOD);
364
         check_ocsp(Botan::calendar_point(2019, 5, 28, 8, 0, 0).to_std_timepoint(),
1✔
365
                    Botan::Certificate_Status_Code::OCSP_IS_TOO_OLD);
366

367
         return result;
1✔
368
      }
2✔
369

370
      static Test::Result test_response_verification_without_next_update_without_max_age() {
1✔
371
         Test::Result result("OCSP request check w/o next_update w/o max_age");
1✔
372

373
         auto ee = load_test_X509_cert("x509/ocsp/patrickschmidt.pem");
1✔
374
         auto ca = load_test_X509_cert("x509/ocsp/bdrive_encryption.pem");
1✔
375
         auto trust_root = load_test_X509_cert("x509/ocsp/bdrive_root.pem");
1✔
376

377
         const std::vector<Botan::X509_Certificate> cert_path = {ee, ca, trust_root};
4✔
378

379
         auto ocsp = load_test_OCSP_resp("x509/ocsp/patrickschmidt_ocsp.der");
1✔
380

381
         Botan::Certificate_Store_In_Memory certstore;
1✔
382
         certstore.add_certificate(trust_root);
1✔
383

384
         auto check_ocsp = [&](const std::chrono::system_clock::time_point valid_time,
4✔
385
                               const Botan::Certificate_Status_Code expected) {
386
            const auto ocsp_status = Botan::PKIX::check_ocsp(
9✔
387
               cert_path, {ocsp}, {&certstore}, valid_time, Botan::Path_Validation_Restrictions());
12✔
388

389
            result.test_sz_eq("Expected size of ocsp_status", ocsp_status.size(), 2);
3✔
390

391
            if(!ocsp_status.empty()) {
3✔
392
               result.test_sz_eq("Expected size of ocsp_status[0]", ocsp_status[0].size(), 1);
3✔
393
               result.test_is_true(std::string("Status: '") + Botan::to_string(expected) + "'",
15✔
394
                                   ocsp_status[0].contains(expected));
6✔
395
            }
396
         };
6✔
397

398
         check_ocsp(Botan::calendar_point(2019, 5, 28, 7, 0, 0).to_std_timepoint(),
1✔
399
                    Botan::Certificate_Status_Code::OCSP_NOT_YET_VALID);
400
         check_ocsp(Botan::calendar_point(2019, 5, 28, 7, 30, 0).to_std_timepoint(),
1✔
401
                    Botan::Certificate_Status_Code::OCSP_RESPONSE_GOOD);
402
         check_ocsp(Botan::calendar_point(2019, 5, 28, 8, 0, 0).to_std_timepoint(),
1✔
403
                    Botan::Certificate_Status_Code::OCSP_RESPONSE_GOOD);
404

405
         return result;
1✔
406
      }
2✔
407

408
      static Test::Result test_response_verification_softfail() {
1✔
409
         Test::Result result("OCSP request softfail check");
1✔
410

411
         auto ee = load_test_X509_cert("x509/ocsp/randombit.pem");
1✔
412
         auto ca = load_test_X509_cert("x509/ocsp/letsencrypt.pem");
1✔
413
         auto trust_root = load_test_X509_cert("x509/ocsp/geotrust.pem");
1✔
414

415
         const std::vector<Botan::X509_Certificate> cert_path = {ee, ca, trust_root};
4✔
416

417
         Botan::OCSP::Response ocsp(Botan::Certificate_Status_Code::OCSP_NO_REVOCATION_URL);
1✔
418

419
         Botan::Certificate_Store_In_Memory certstore;
1✔
420
         certstore.add_certificate(trust_root);
1✔
421

422
         // Some arbitrary time within the validity period of the test certs
423
         const auto valid_time = Botan::calendar_point(2016, 11, 20, 8, 30, 0).to_std_timepoint();
1✔
424
         const auto ocsp_status =
1✔
425
            Botan::PKIX::check_ocsp(cert_path, {ocsp}, {&certstore}, valid_time, Botan::Path_Validation_Restrictions());
4✔
426

427
         if(result.test_sz_eq("Expected size of ocsp_status", ocsp_status.size(), 2)) {
1✔
428
            if(result.test_sz_eq("Expected size of ocsp_status[0]", ocsp_status[0].size(), 1)) {
1✔
429
               result.test_sz_gt(
2✔
430
                  "Status warning", ocsp_status[0].count(Botan::Certificate_Status_Code::OCSP_NO_REVOCATION_URL), 0);
1✔
431
            }
432
         }
433

434
         return result;
1✔
435
      }
3✔
436

437
   #if defined(BOTAN_HAS_ONLINE_REVOCATION_CHECKS)
438
      static Test::Result test_online_request() {
1✔
439
         Test::Result result("OCSP online check");
1✔
440

441
         auto cert = load_test_X509_cert("x509/ocsp/digicert-ecdsa-int.pem");
1✔
442
         auto trust_root = load_test_X509_cert("x509/ocsp/digicert-root.pem");
1✔
443

444
         const std::vector<Botan::X509_Certificate> cert_path = {cert, trust_root};
3✔
445

446
         Botan::Certificate_Store_In_Memory certstore;
1✔
447
         certstore.add_certificate(trust_root);
1✔
448

449
         const auto ocsp_timeout = std::chrono::milliseconds(3000);
1✔
450
         const auto now = std::chrono::system_clock::now();
1✔
451
         auto ocsp_status = Botan::PKIX::check_ocsp_online(
1✔
452
            cert_path, {&certstore}, now, ocsp_timeout, Botan::Path_Validation_Restrictions());
2✔
453

454
         if(result.test_sz_eq("Expected size of ocsp_status", ocsp_status.size(), 1)) {
1✔
455
            if(result.test_sz_eq("Expected size of ocsp_status[0]", ocsp_status[0].size(), 1)) {
1✔
456
               const bool status_good = ocsp_status[0].contains(Botan::Certificate_Status_Code::OCSP_RESPONSE_GOOD);
1✔
457
               const bool server_not_found =
1✔
458
                  ocsp_status[0].contains(Botan::Certificate_Status_Code::OCSP_SERVER_NOT_AVAILABLE);
1✔
459
               result.test_is_true("Expected status", status_good || server_not_found);
1✔
460
            }
461
         }
462

463
         return result;
1✔
464
      }
2✔
465
   #endif
466

467
      static Test::Result test_response_verification_with_additionally_trusted_responder() {
1✔
468
         Test::Result result("OCSP response with user-defined (additional) responder certificate");
1✔
469

470
         // OCSP response is signed by 3rd party responder certificate that is
471
         // not included in the OCSP response itself
472
         // See `src/scripts/randombit_ocsp_forger.sh` for a helper script to recreate those.
473
         auto ocsp = load_test_OCSP_resp("x509/ocsp/randombit_ocsp_forged_valid_nocerts.der");
1✔
474
         auto responder = load_test_X509_cert("x509/ocsp/randombit_ocsp_forged_responder.pem");
1✔
475
         auto ca = load_test_X509_cert("x509/ocsp/letsencrypt.pem");
1✔
476

477
         Botan::Certificate_Store_In_Memory trusted_responders;
1✔
478

479
         // without providing the 3rd party responder certificate no issuer will be found
480
         result.test_opt_is_null("cannot find signing certificate without trusted responders",
1✔
481
                                 ocsp.find_signing_certificate(ca));
1✔
482
         result.test_opt_is_null("cannot find signing certificate without additional help",
1✔
483
                                 ocsp.find_signing_certificate(ca, &trusted_responders));
1✔
484

485
         // add the 3rd party responder certificate to the list of trusted OCSP responder certs
486
         // to find the issuer certificate of this response
487
         trusted_responders.add_certificate(responder);
1✔
488
         test_arb_eq(result,
2✔
489
                     "the responder certificate is returned when it is trusted",
490
                     ocsp.find_signing_certificate(ca, &trusted_responders),
1✔
491
                     std::optional(responder));
1✔
492

493
         result.test_enum_eq("the responder's signature checks out",
1✔
494
                             ocsp.verify_signature(responder),
1✔
495
                             Botan::Certificate_Status_Code::OCSP_SIGNATURE_OK);
496

497
         return result;
1✔
498
      }
1✔
499

500
      static Test::Result test_forged_ocsp_signature_is_rejected() {
1✔
501
         Test::Result result("OCSP response with forged signature is rejected by path validation");
1✔
502

503
         auto ee = load_test_X509_cert("x509/ocsp/randombit.pem");
1✔
504
         auto ca = load_test_X509_cert("x509/ocsp/letsencrypt.pem");
1✔
505
         auto trust_root = load_test_X509_cert("x509/ocsp/geotrust.pem");
1✔
506

507
         const std::vector<Botan::X509_Certificate> cert_path = {ee, ca, trust_root};
4✔
508

509
         Botan::Certificate_Store_In_Memory certstore;
1✔
510
         certstore.add_certificate(trust_root);
1✔
511

512
         const auto valid_time = Botan::calendar_point(2016, 11, 18, 12, 30, 0).to_std_timepoint();
1✔
513

514
         // Verify the unmodified response is accepted
515
         {
1✔
516
            auto ocsp = load_test_OCSP_resp("x509/ocsp/randombit_ocsp.der");
1✔
517
            const auto ocsp_status = Botan::PKIX::check_ocsp(
3✔
518
               cert_path, {ocsp}, {&certstore}, valid_time, Botan::Path_Validation_Restrictions());
4✔
519

520
            if(result.test_sz_eq("Legitimate: expected result count", ocsp_status.size(), 2) &&
2✔
521
               result.test_sz_eq("Legitimate: expected status count", ocsp_status[0].size(), 1)) {
1✔
522
               result.test_is_true("Legitimate response is accepted",
2✔
523
                                   ocsp_status[0].contains(Botan::Certificate_Status_Code::OCSP_RESPONSE_GOOD));
2✔
524
            }
525
         }
1✔
526

527
         // Tamper with the signature and verify check_ocsp rejects it
528
         {
1✔
529
            auto ocsp_bytes = Test::read_binary_data_file("x509/ocsp/randombit_ocsp.der");
1✔
530
            ocsp_bytes.back() ^= 0x01;
1✔
531
            Botan::OCSP::Response forged_ocsp(ocsp_bytes.data(), ocsp_bytes.size());
1✔
532

533
            const auto ocsp_status = Botan::PKIX::check_ocsp(
3✔
534
               cert_path, {forged_ocsp}, {&certstore}, valid_time, Botan::Path_Validation_Restrictions());
4✔
535

536
            if(result.test_sz_eq("Forged: expected result count", ocsp_status.size(), 2) &&
2✔
537
               result.test_sz_eq("Forged: expected status count", ocsp_status[0].size(), 1)) {
1✔
538
               result.test_is_true("Forged signature is rejected",
2✔
539
                                   ocsp_status[0].contains(Botan::Certificate_Status_Code::OCSP_SIGNATURE_ERROR));
2✔
540
            }
541
         }
1✔
542

543
         return result;
1✔
544
      }
4✔
545

546
      static Test::Result test_partial_stapling_preserves_per_slot_gap() {
1✔
547
         Test::Result result("OCSP partial stapling preserves per-slot gap for online fallback");
1✔
548

549
         auto ee = load_test_X509_cert("x509/ocsp/mychain_ee.pem");
1✔
550
         auto ca = load_test_X509_cert("x509/ocsp/mychain_int.pem");
1✔
551
         auto trust_root = load_test_X509_cert("x509/ocsp/mychain_root.pem");
1✔
552

553
         auto ocsp_for_ee = load_test_OCSP_resp("x509/ocsp/mychain_ocsp_for_ee.der");
1✔
554
         auto ocsp_for_int = load_test_OCSP_resp("x509/ocsp/mychain_ocsp_for_int_self_signed.der");
1✔
555

556
         Botan::Certificate_Store_In_Memory certstore;
1✔
557
         certstore.add_certificate(trust_root);
1✔
558

559
         const std::vector<Botan::X509_Certificate> cert_path = {ee, ca, trust_root};
4✔
560
         const auto valid_time = Botan::calendar_point(2022, 9, 22, 22, 30, 0).to_std_timepoint();
1✔
561
         const auto restrictions = Botan::Path_Validation_Restrictions();
1✔
562

563
         // Here the intermediate has a stapled OCSP but the leaf does not
564
         {
1✔
565
            const std::vector<std::optional<Botan::OCSP::Response>> staples = {std::nullopt, ocsp_for_int};
3✔
566
            const auto ocsp_status =
1✔
567
               Botan::PKIX::check_ocsp(cert_path, staples, {&certstore}, valid_time, restrictions);
1✔
568

569
            result.test_sz_eq("missing-leaf: ocsp_status sized to non-root certs", ocsp_status.size(), 2);
1✔
570
            if(ocsp_status.size() == 2) {
1✔
571
               result.test_is_true("missing-leaf: leaf slot is empty", ocsp_status[0].empty());
1✔
572
               result.test_is_false("missing-leaf: intermediate slot is filled", ocsp_status[1].empty());
1✔
573
            }
574
         }
1✔
575

576
         // Here the leaf has a stapled OCSP but the intermediate does not
577
         {
1✔
578
            const std::vector<std::optional<Botan::OCSP::Response>> staples = {ocsp_for_ee, std::nullopt};
3✔
579
            const auto ocsp_status =
1✔
580
               Botan::PKIX::check_ocsp(cert_path, staples, {&certstore}, valid_time, restrictions);
1✔
581

582
            result.test_sz_eq("missing-intermediate: ocsp_status sized to non-root certs", ocsp_status.size(), 2);
1✔
583
            if(ocsp_status.size() == 2) {
1✔
584
               result.test_is_false("missing-intermediate: leaf slot is filled", ocsp_status[0].empty());
1✔
585
               result.test_is_true("missing-intermediate: intermediate slot is empty", ocsp_status[1].empty());
1✔
586
            }
587
         }
1✔
588

589
         return result;
1✔
590
      }
4✔
591

592
      static Test::Result test_responder_cert_with_nocheck_extension() {
1✔
593
         Test::Result result("BDr's OCSP response contains certificate featuring NoCheck extension");
1✔
594

595
         auto ocsp = load_test_OCSP_resp("x509/ocsp/bdr-ocsp-resp.der");
1✔
596
         const bool contains_cert_with_nocheck =
1✔
597
            std::find_if(ocsp.certificates().cbegin(), ocsp.certificates().cend(), [](const auto& cert) {
1✔
598
               return cert.v3_extensions().extension_set(Botan::OID::from_string("PKIX.OCSP.NoCheck"));
1✔
599
            }) != ocsp.certificates().end();
1✔
600

601
         result.test_is_true("Contains NoCheck extension", contains_cert_with_nocheck);
1✔
602

603
         return result;
1✔
604
      }
1✔
605

606
   public:
607
      std::vector<Test::Result> run() override {
1✔
608
         std::vector<Test::Result> results;
1✔
609

610
         results.push_back(test_request_encoding());
2✔
611
         results.push_back(test_certid_serial_sign());
2✔
612
         results.push_back(test_response_parsing());
2✔
613
         results.push_back(test_response_with_bykey_responder_id());
2✔
614
         results.push_back(test_response_certificate_access());
2✔
615
         results.push_back(test_response_find_signing_certificate());
2✔
616
         results.push_back(test_response_verification_with_next_update_without_max_age());
2✔
617
         results.push_back(test_response_verification_with_next_update_with_max_age());
2✔
618
         results.push_back(test_response_verification_without_next_update_with_max_age());
2✔
619
         results.push_back(test_response_verification_without_next_update_without_max_age());
2✔
620
         results.push_back(test_response_verification_softfail());
2✔
621
         results.push_back(test_response_verification_with_additionally_trusted_responder());
2✔
622
         results.push_back(test_forged_ocsp_signature_is_rejected());
2✔
623
         results.push_back(test_partial_stapling_preserves_per_slot_gap());
2✔
624
         results.push_back(test_responder_cert_with_nocheck_extension());
2✔
625

626
   #if defined(BOTAN_HAS_ONLINE_REVOCATION_CHECKS)
627
         if(Test::options().run_online_tests()) {
1✔
628
            results.push_back(test_online_request());
2✔
629
         }
630
   #endif
631

632
         return results;
1✔
633
      }
×
634
};
635

636
BOTAN_REGISTER_TEST("x509", "ocsp", OCSP_Tests);
637

638
#endif
639

640
}  // namespace
641

642
}  // namespace Botan_Tests
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc