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

randombit / botan / 14576613838

21 Apr 2025 10:22AM UTC coverage: 91.316% (-0.01%) from 91.326%
14576613838

push

github

web-flow
Merge pull request #4829 from randombit/jack/marvin-cli-improvements

Some small usability improvements to the MARVIN test cli util

95648 of 104744 relevant lines covered (91.32%)

13237762.16 hits per line

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

90.1
/src/cli/timing_tests.cpp
1
/*
2
* Timing Analysis Tests
3
*
4
* These tests are not for performance, but verifying that two inputs are not handled
5
* in a way that is vulnerable to simple timing attacks.
6
*
7
* Produces output which can be analyzed with the Mona reporting library
8
*
9
* $ git clone https://github.com/seecurity/mona-timing-report.git
10
* $ cd mona-timing-report && ant
11
* $ java -jar ReportingTool.jar --lowerBound=0.4 --upperBound=0.5 --inputFile=$file --name=$file
12
*
13
* (C) 2016 Juraj Somorovsky - juraj.somorovsky@hackmanit.de
14
* (C) 2017 Neverhub
15
* (C) 2017,2018,2019 Jack Lloyd
16
*
17
* Botan is released under the Simplified BSD License (see license.txt)
18
*/
19

20
#include "cli.h"
21

22
#include <botan/hex.h>
23
#include <botan/rng.h>
24
#include <botan/internal/filesystem.h>
25
#include <botan/internal/fmt.h>
26
#include <botan/internal/loadstor.h>
27
#include <botan/internal/parsing.h>
28
#include <botan/internal/target_info.h>
29
#include <chrono>
30
#include <fstream>
31
#include <iostream>
32
#include <sstream>
33

34
#if defined(BOTAN_HAS_BIGINT)
35
   #include <botan/bigint.h>
36
#endif
37

38
#if defined(BOTAN_HAS_NUMBERTHEORY)
39
   #include <botan/numthry.h>
40
   #include <botan/internal/mod_inv.h>
41
#endif
42

43
#if defined(BOTAN_HAS_ECC_GROUP)
44
   #include <botan/ec_group.h>
45
#endif
46

47
#if defined(BOTAN_HAS_DL_GROUP)
48
   #include <botan/dl_group.h>
49
#endif
50

51
#if defined(BOTAN_HAS_PUBLIC_KEY_CRYPTO)
52
   #include <botan/pkcs8.h>
53
   #include <botan/pubkey.h>
54
#endif
55

56
#if defined(BOTAN_HAS_RSA)
57
   #include <botan/rsa.h>
58
#endif
59

60
#if defined(BOTAN_HAS_TLS_CBC)
61
   #include <botan/tls_exceptn.h>
62
   #include <botan/internal/tls_cbc.h>
63
#endif
64

65
#if defined(BOTAN_HAS_ECDSA)
66
   #include <botan/ecdsa.h>
67
#endif
68

69
#if defined(BOTAN_HAS_SYSTEM_RNG)
70
   #include <botan/system_rng.h>
71
#endif
72

73
#if defined(BOTAN_HAS_CHACHA_RNG)
74
   #include <botan/chacha_rng.h>
75
#endif
76

77
#if defined(BOTAN_TARGET_OS_HAS_POSIX1)
78
   #include <signal.h>
79
   #include <stdlib.h>
80
#endif
81

82
namespace Botan_CLI {
83

84
namespace {
85

86
class TimingTestTimer {
87
   public:
88
      TimingTestTimer() { m_start = get_high_resolution_clock(); }
1,282✔
89

90
      uint64_t complete() const { return get_high_resolution_clock() - m_start; }
1,282✔
91

92
   private:
93
      static uint64_t get_high_resolution_clock() {
1,282✔
94
         // TODO use cpu clock where possible/relevant incl serializing instructions
95
         auto now = std::chrono::high_resolution_clock::now().time_since_epoch();
1,206✔
96
         return std::chrono::duration_cast<std::chrono::nanoseconds>(now).count();
1,282✔
97
      }
98

99
      uint64_t m_start;
100
};
101

102
}  // namespace
103

104
class Timing_Test {
105
   public:
106
      Timing_Test() {
10✔
107
         /*
108
         A constant seed is ok here since the timing test rng just needs to be
109
         "random" but not cryptographically secure - even std::rand() would be ok.
110
         */
111
         const std::string drbg_seed(64, 'A');
10✔
112
         m_rng = cli_make_rng("", drbg_seed);  // throws if it can't find anything to use
10✔
113
      }
10✔
114

115
      virtual ~Timing_Test() = default;
×
116

117
      Timing_Test(const Timing_Test& other) = delete;
118
      Timing_Test(Timing_Test&& other) = delete;
119
      Timing_Test& operator=(const Timing_Test& other) = delete;
120
      Timing_Test& operator=(Timing_Test&& other) = delete;
121

122
      std::vector<std::vector<uint64_t>> execute_evaluation(const std::vector<std::string>& inputs,
123
                                                            size_t warmup_runs,
124
                                                            size_t measurement_runs);
125

126
      virtual std::vector<uint8_t> prepare_input(const std::string& input) { return Botan::hex_decode(input); }
9✔
127

128
      virtual uint64_t measure_critical_function(const std::vector<uint8_t>& input) = 0;
129

130
   protected:
131
      Botan::RandomNumberGenerator& timing_test_rng() { return (*m_rng); }
2✔
132

133
   private:
134
      std::shared_ptr<Botan::RandomNumberGenerator> m_rng;
135
};
136

137
#if defined(BOTAN_HAS_RSA) && defined(BOTAN_HAS_EME_PKCS1) && defined(BOTAN_HAS_EME_RAW)
138

139
class Bleichenbacker_Timing_Test final : public Timing_Test {
×
140
   public:
141
      explicit Bleichenbacker_Timing_Test(size_t keysize) :
1✔
142
            m_privkey(timing_test_rng(), keysize),
1✔
143
            m_pubkey(m_privkey),
1✔
144
            m_enc(m_pubkey, timing_test_rng(), "Raw"),
1✔
145
            m_dec(m_privkey, timing_test_rng(), "PKCS1v15") {}
2✔
146

147
      std::vector<uint8_t> prepare_input(const std::string& input) override {
4✔
148
         const std::vector<uint8_t> input_vector = Botan::hex_decode(input);
4✔
149
         return m_enc.encrypt(input_vector, timing_test_rng());
4✔
150
      }
4✔
151

152
      uint64_t measure_critical_function(const std::vector<uint8_t>& input) override {
76✔
153
         TimingTestTimer timer;
76✔
154
         m_dec.decrypt_or_random(input.data(), m_ctext_length, m_expected_content_size, timing_test_rng());
76✔
155
         return timer.complete();
76✔
156
      }
157

158
   private:
159
      const size_t m_expected_content_size = 48;
160
      const size_t m_ctext_length = 256;
161
      Botan::RSA_PrivateKey m_privkey;
162
      Botan::RSA_PublicKey m_pubkey;
163
      Botan::PK_Encryptor_EME m_enc;
164
      Botan::PK_Decryptor_EME m_dec;
165
};
166

167
#endif
168

169
#if defined(BOTAN_HAS_RSA) && defined(BOTAN_HAS_EME_OAEP) && defined(BOTAN_HAS_EME_RAW)
170

171
/*
172
* Test Manger OAEP side channel
173
*
174
* "A Chosen Ciphertext Attack on RSA Optimal Asymmetric Encryption
175
* Padding (OAEP) as Standardized in PKCS #1 v2.0" James Manger
176
* http://archiv.infsec.ethz.ch/education/fs08/secsem/Manger01.pdf
177
*/
178
class Manger_Timing_Test final : public Timing_Test {
×
179
   public:
180
      explicit Manger_Timing_Test(size_t keysize) :
1✔
181
            m_privkey(timing_test_rng(), keysize),
1✔
182
            m_pubkey(m_privkey),
1✔
183
            m_enc(m_pubkey, timing_test_rng(), m_encrypt_padding),
1✔
184
            m_dec(m_privkey, timing_test_rng(), m_decrypt_padding) {}
2✔
185

186
      std::vector<uint8_t> prepare_input(const std::string& input) override {
2✔
187
         const std::vector<uint8_t> input_vector = Botan::hex_decode(input);
2✔
188
         return m_enc.encrypt(input_vector, timing_test_rng());
2✔
189
      }
2✔
190

191
      uint64_t measure_critical_function(const std::vector<uint8_t>& input) override {
38✔
192
         TimingTestTimer timer;
38✔
193
         try {
38✔
194
            m_dec.decrypt(input.data(), m_ctext_length);
38✔
195
         } catch(Botan::Decoding_Error&) {}
38✔
196
         return timer.complete();
38✔
197
      }
198

199
   private:
200
      const std::string m_encrypt_padding = "Raw";
201
      const std::string m_decrypt_padding = "EME1(SHA-256)";
202
      const size_t m_ctext_length = 256;
203
      Botan::RSA_PrivateKey m_privkey;
204
      Botan::RSA_PublicKey m_pubkey;
205
      Botan::PK_Encryptor_EME m_enc;
206
      Botan::PK_Decryptor_EME m_dec;
207
};
208

209
#endif
210

211
#if defined(BOTAN_HAS_TLS_CBC)
212

213
/*
214
* Test handling of countermeasure to the Lucky13 attack
215
*/
216
class Lucky13_Timing_Test final : public Timing_Test {
×
217
   public:
218
      Lucky13_Timing_Test(const std::string& mac_name, size_t mac_keylen) :
4✔
219
            m_mac_algo(mac_name),
8✔
220
            m_mac_keylen(mac_keylen),
4✔
221
            m_dec(Botan::BlockCipher::create_or_throw("AES-128"),
12✔
222
                  Botan::MessageAuthenticationCode::create_or_throw("HMAC(" + m_mac_algo + ")"),
12✔
223
                  16,
224
                  m_mac_keylen,
4✔
225
                  Botan::TLS::Protocol_Version::TLS_V12,
226
                  false) {}
8✔
227

228
      std::vector<uint8_t> prepare_input(const std::string& input) override;
229
      uint64_t measure_critical_function(const std::vector<uint8_t>& input) override;
230

231
   private:
232
      const std::string m_mac_algo;
233
      const size_t m_mac_keylen;
234
      Botan::TLS::TLS_CBC_HMAC_AEAD_Decryption m_dec;
235
};
236

237
std::vector<uint8_t> Lucky13_Timing_Test::prepare_input(const std::string& input) {
12✔
238
   const std::vector<uint8_t> input_vector = Botan::hex_decode(input);
12✔
239
   const std::vector<uint8_t> key(16);
12✔
240
   const std::vector<uint8_t> iv(16);
12✔
241

242
   auto enc = Botan::Cipher_Mode::create("AES-128/CBC/NoPadding", Botan::Cipher_Dir::Encryption);
12✔
243
   enc->set_key(key);
12✔
244
   enc->start(iv);
12✔
245
   Botan::secure_vector<uint8_t> buf(input_vector.begin(), input_vector.end());
12✔
246
   enc->finish(buf);
12✔
247

248
   return unlock(buf);
12✔
249
}
60✔
250

251
uint64_t Lucky13_Timing_Test::measure_critical_function(const std::vector<uint8_t>& input) {
228✔
252
   Botan::secure_vector<uint8_t> data(input.begin(), input.end());
228✔
253
   Botan::secure_vector<uint8_t> aad(13);
228✔
254
   const Botan::secure_vector<uint8_t> iv(16);
228✔
255
   Botan::secure_vector<uint8_t> key(16 + m_mac_keylen);
228✔
256

257
   m_dec.set_key(unlock(key));
228✔
258
   m_dec.set_associated_data(aad);
228✔
259
   m_dec.start(unlock(iv));
228✔
260

261
   TimingTestTimer timer;
228✔
262
   try {
228✔
263
      m_dec.finish(data);
228✔
264
   } catch(Botan::TLS::TLS_Exception&) {}
228✔
265
   return timer.complete();
228✔
266
}
912✔
267

268
#endif
269

270
#if defined(BOTAN_HAS_ECDSA)
271

272
class ECDSA_Timing_Test final : public Timing_Test {
×
273
   public:
274
      explicit ECDSA_Timing_Test(const std::string& ecgroup);
275

276
      uint64_t measure_critical_function(const std::vector<uint8_t>& input) override;
277

278
   private:
279
      const Botan::EC_Group m_group;
280
      const Botan::ECDSA_PrivateKey m_privkey;
281
      const Botan::EC_Scalar m_x;
282
      Botan::EC_Scalar m_b;
283
      Botan::EC_Scalar m_b_inv;
284
};
285

286
ECDSA_Timing_Test::ECDSA_Timing_Test(const std::string& ecgroup) :
1✔
287
      m_group(Botan::EC_Group::from_name(ecgroup)),
1✔
288
      m_privkey(timing_test_rng(), m_group),
1✔
289
      m_x(m_privkey._private_key()),
1✔
290
      m_b(Botan::EC_Scalar::random(m_group, timing_test_rng())),
1✔
291
      m_b_inv(m_b.invert()) {}
2✔
292

293
uint64_t ECDSA_Timing_Test::measure_critical_function(const std::vector<uint8_t>& input) {
38✔
294
   const auto k = Botan::EC_Scalar::from_bytes_with_trunc(m_group, input);
38✔
295
   // fixed message to minimize noise
296
   const auto m = Botan::EC_Scalar::from_bytes_with_trunc(m_group, std::vector<uint8_t>{5});
38✔
297

298
   TimingTestTimer timer;
38✔
299

300
   // the following ECDSA operations involve and should not leak any information about k
301
   const auto r = Botan::EC_Scalar::gk_x_mod_order(k, timing_test_rng());
38✔
302
   const auto k_inv = k.invert();
38✔
303
   m_b.square_self();
38✔
304
   m_b_inv.square_self();
38✔
305
   const auto xr_m = ((m_x * m_b) * r) + (m * m_b);
38✔
306
   const auto s = (k_inv * xr_m) * m_b_inv;
38✔
307
   BOTAN_UNUSED(r, s);
38✔
308

309
   return timer.complete();
76✔
310
}
38✔
311

312
#endif
313

314
#if defined(BOTAN_HAS_ECC_GROUP)
315

316
class ECC_Mul_Timing_Test final : public Timing_Test {
×
317
   public:
318
      explicit ECC_Mul_Timing_Test(std::string_view ecgroup) : m_group(Botan::EC_Group::from_name(ecgroup)) {}
1✔
319

320
      uint64_t measure_critical_function(const std::vector<uint8_t>& input) override;
321

322
   private:
323
      const Botan::EC_Group m_group;
324
};
325

326
uint64_t ECC_Mul_Timing_Test::measure_critical_function(const std::vector<uint8_t>& input) {
38✔
327
   const auto k = Botan::EC_Scalar::from_bytes_with_trunc(m_group, input);
38✔
328

329
   TimingTestTimer timer;
38✔
330
   const auto kG = Botan::EC_AffinePoint::g_mul(k, timing_test_rng());
38✔
331
   return timer.complete();
76✔
332
}
38✔
333

334
#endif
335

336
#if defined(BOTAN_HAS_DL_GROUP)
337

338
class Powmod_Timing_Test final : public Timing_Test {
×
339
   public:
340
      explicit Powmod_Timing_Test(std::string_view dl_group) : m_group(Botan::DL_Group::from_name(dl_group)) {}
1✔
341

342
      uint64_t measure_critical_function(const std::vector<uint8_t>& input) override;
343

344
   private:
345
      Botan::DL_Group m_group;
346
};
347

348
uint64_t Powmod_Timing_Test::measure_critical_function(const std::vector<uint8_t>& input) {
57✔
349
   const Botan::BigInt x(input.data(), input.size());
57✔
350
   const size_t max_x_bits = m_group.p_bits();
57✔
351

352
   TimingTestTimer timer;
57✔
353

354
   const Botan::BigInt g_x_p = m_group.power_g_p(x, max_x_bits);
57✔
355

356
   return timer.complete();
114✔
357
}
57✔
358

359
#endif
360

361
#if defined(BOTAN_HAS_NUMBERTHEORY)
362

363
class Invmod_Timing_Test final : public Timing_Test {
×
364
   public:
365
      explicit Invmod_Timing_Test(size_t p_bits) { m_p = Botan::random_prime(timing_test_rng(), p_bits); }
2✔
366

367
      uint64_t measure_critical_function(const std::vector<uint8_t>& input) override;
368

369
   private:
370
      Botan::BigInt m_p;
371
};
372

373
uint64_t Invmod_Timing_Test::measure_critical_function(const std::vector<uint8_t>& input) {
38✔
374
   const Botan::BigInt k(input.data(), input.size());
38✔
375

376
   TimingTestTimer timer;
38✔
377
   const Botan::BigInt inv = Botan::inverse_mod_secret_prime(k, m_p);
38✔
378
   return timer.complete();
76✔
379
}
38✔
380

381
#endif
382

383
std::vector<std::vector<uint64_t>> Timing_Test::execute_evaluation(const std::vector<std::string>& raw_inputs,
10✔
384
                                                                   size_t warmup_runs,
385
                                                                   size_t measurement_runs) {
386
   std::vector<std::vector<uint64_t>> all_results(raw_inputs.size());
10✔
387
   std::vector<std::vector<uint8_t>> inputs(raw_inputs.size());
10✔
388

389
   for(auto& result : all_results) {
37✔
390
      result.reserve(measurement_runs);
27✔
391
   }
392

393
   for(size_t i = 0; i != inputs.size(); ++i) {
37✔
394
      inputs[i] = prepare_input(raw_inputs[i]);
27✔
395
   }
396

397
   // arbitrary upper bounds of 1 and 10 million resp
398
   if(warmup_runs > 1000000 || measurement_runs > 100000000) {
10✔
399
      throw CLI_Error("Requested execution counts too large, rejecting");
×
400
   }
401

402
   size_t total_runs = 0;
10✔
403
   std::vector<uint64_t> results(inputs.size());
10✔
404

405
   while(total_runs < (warmup_runs + measurement_runs)) {
200✔
406
      for(size_t i = 0; i != inputs.size(); ++i) {
703✔
407
         results[i] = measure_critical_function(inputs[i]);
513✔
408
      }
409

410
      total_runs++;
190✔
411

412
      if(total_runs > warmup_runs) {
190✔
413
         for(size_t i = 0; i != results.size(); ++i) {
592✔
414
            all_results[i].push_back(results[i]);
432✔
415
         }
416
      }
417
   }
418

419
   return all_results;
10✔
420
}
10✔
421

422
class Timing_Test_Command final : public Command {
423
   public:
424
      Timing_Test_Command() :
11✔
425
            Command(
426
               "timing_test test_type --test-data-file= --test-data-dir=src/tests/data/timing "
427
               "--warmup-runs=5000 --measurement-runs=50000") {}
22✔
428

429
      std::string group() const override { return "testing"; }
1✔
430

431
      std::string description() const override { return "Run various timing side channel tests"; }
1✔
432

433
      void go() override {
10✔
434
         const std::string test_type = get_arg("test_type");
10✔
435
         const size_t warmup_runs = get_arg_sz("warmup-runs");
10✔
436
         const size_t measurement_runs = get_arg_sz("measurement-runs");
10✔
437

438
         std::unique_ptr<Timing_Test> test = lookup_timing_test(test_type);
10✔
439

440
         if(!test) {
10✔
441
            throw CLI_Error("Unknown or unavailable test type '" + test_type + "'");
×
442
         }
443

444
         std::string filename = get_arg_or("test-data-file", "");
20✔
445

446
         if(filename.empty()) {
10✔
447
            const std::string test_data_dir = get_arg("test-data-dir");
10✔
448
            filename = test_data_dir + "/" + test_type + ".vec";
20✔
449
         }
10✔
450

451
         std::vector<std::string> lines = read_testdata(filename);
10✔
452

453
         std::vector<std::vector<uint64_t>> results = test->execute_evaluation(lines, warmup_runs, measurement_runs);
10✔
454

455
         size_t unique_id = 0;
10✔
456
         std::ostringstream oss;
10✔
457
         for(size_t secret_id = 0; secret_id != results.size(); ++secret_id) {
37✔
458
            for(size_t i = 0; i != results[secret_id].size(); ++i) {
459✔
459
               oss << unique_id++ << ";" << secret_id << ";" << results[secret_id][i] << "\n";
432✔
460
            }
461
         }
462

463
         output() << oss.str();
10✔
464
      }
20✔
465

466
   private:
467
      static std::vector<std::string> read_testdata(const std::string& filename) {
10✔
468
         std::vector<std::string> lines;
10✔
469
         std::ifstream infile(filename);
10✔
470
         if(infile.good() == false) {
10✔
471
            throw CLI_Error("Error reading test data from '" + filename + "'");
×
472
         }
473
         std::string line;
10✔
474
         while(std::getline(infile, line)) {
70✔
475
            if(!line.empty() && line.at(0) != '#') {
60✔
476
               lines.push_back(line);
27✔
477
            }
478
         }
479
         return lines;
20✔
480
      }
10✔
481

482
      static std::unique_ptr<Timing_Test> lookup_timing_test(std::string_view test_type);
483

484
      std::string help_text() const override {
×
485
         // TODO check feature macros
486
         return (Command::help_text() +
×
487
                 "\ntest_type can take on values "
488
                 "bleichenbacher "
489
                 "manger "
490
                 "ecdsa "
491
                 "ecc_mul "
492
                 "inverse_mod "
493
                 "pow_mod "
494
                 "lucky13sec3 "
495
                 "lucky13sec4sha1 "
496
                 "lucky13sec4sha256 "
497
                 "lucky13sec4sha384 ");
×
498
      }
499
};
500

501
std::unique_ptr<Timing_Test> Timing_Test_Command::lookup_timing_test(std::string_view test_type) {
10✔
502
#if defined(BOTAN_HAS_RSA) && defined(BOTAN_HAS_EME_PKCS1) && defined(BOTAN_HAS_EME_RAW)
503
   if(test_type == "bleichenbacher") {
10✔
504
      return std::make_unique<Bleichenbacker_Timing_Test>(2048);
1✔
505
   }
506
#endif
507

508
#if defined(BOTAN_HAS_RSA) && defined(BOTAN_HAS_EME_OAEP) && defined(BOTAN_HAS_EME_RAW)
509
   if(test_type == "manger") {
9✔
510
      return std::make_unique<Manger_Timing_Test>(2048);
1✔
511
   }
512
#endif
513

514
#if defined(BOTAN_HAS_ECDSA)
515
   if(test_type == "ecdsa") {
8✔
516
      return std::make_unique<ECDSA_Timing_Test>("secp384r1");
1✔
517
   }
518
#endif
519

520
#if defined(BOTAN_HAS_ECC_GROUP)
521
   if(test_type == "ecc_mul") {
7✔
522
      return std::make_unique<ECC_Mul_Timing_Test>("brainpool512r1");
1✔
523
   }
524
#endif
525

526
#if defined(BOTAN_HAS_NUMBERTHEORY)
527
   if(test_type == "inverse_mod") {
6✔
528
      return std::make_unique<Invmod_Timing_Test>(512);
1✔
529
   }
530
#endif
531

532
#if defined(BOTAN_HAS_DL_GROUP)
533
   if(test_type == "pow_mod") {
5✔
534
      return std::make_unique<Powmod_Timing_Test>("modp/ietf/1024");
1✔
535
   }
536
#endif
537

538
#if defined(BOTAN_HAS_TLS_CBC)
539
   if(test_type == "lucky13sec3" || test_type == "lucky13sec4sha1") {
6✔
540
      return std::make_unique<Lucky13_Timing_Test>("SHA-1", 20);
2✔
541
   }
542
   if(test_type == "lucky13sec4sha256") {
2✔
543
      return std::make_unique<Lucky13_Timing_Test>("SHA-256", 32);
1✔
544
   }
545
   if(test_type == "lucky13sec4sha384") {
1✔
546
      return std::make_unique<Lucky13_Timing_Test>("SHA-384", 48);
1✔
547
   }
548
#endif
549

550
   BOTAN_UNUSED(test_type);
×
551

552
   return nullptr;
×
553
}
554

555
BOTAN_REGISTER_COMMAND("timing_test", Timing_Test_Command);
11✔
556

557
#if defined(BOTAN_HAS_RSA) && defined(BOTAN_HAS_EME_PKCS1) && defined(BOTAN_TARGET_OS_HAS_FILESYSTEM) && \
558
   defined(BOTAN_HAS_SYSTEM_RNG)
559

560
class MARVIN_Test_Command final : public Command {
561
   public:
562
      MARVIN_Test_Command() :
2✔
563
            Command("marvin_test key_file ctext_dir --runs=1000 --report-every=0 --output-nsec --expect-pt-len=0") {}
4✔
564

565
      std::string group() const override { return "testing"; }
1✔
566

567
      std::string description() const override { return "Run a test for MARVIN attack"; }
1✔
568

569
   #if defined(BOTAN_TARGET_OS_HAS_POSIX1)
570
      static inline volatile sig_atomic_t g_sigint_recv = 0;
571

572
      static void marvin_sigint_handler(int /*signal*/) { g_sigint_recv = 1; }
×
573
   #endif
574

575
      void go() override {
1✔
576
         const std::string key_file = get_arg("key_file");
1✔
577
         const std::string ctext_dir = get_arg("ctext_dir");
1✔
578
         const size_t measurement_runs = get_arg_sz("runs");
1✔
579
         const size_t expect_pt_len = get_arg_sz("expect-pt-len");
1✔
580
         const size_t report_every = get_arg_sz("report-every");
1✔
581
         const bool output_nsec = flag_set("output-nsec");
1✔
582

583
   #if defined(BOTAN_TARGET_OS_HAS_POSIX1)
584
         ::setenv("BOTAN_THREAD_POOL_SIZE", "none", /*overwrite?*/ 1);
1✔
585

586
         struct sigaction sigaction;
1✔
587

588
         sigaction.sa_handler = marvin_sigint_handler;
1✔
589
         sigemptyset(&sigaction.sa_mask);
1✔
590
         sigaction.sa_flags = 0;
1✔
591

592
         int rc = ::sigaction(SIGINT, &sigaction, nullptr);
1✔
593
         if(rc != 0) {
1✔
594
            throw CLI_Error("Failed to set SIGINT handler");
×
595
         }
596
   #endif
597

598
         Botan::DataSource_Stream key_src(key_file);
1✔
599
         const auto key = Botan::PKCS8::load_key(key_src);
1✔
600

601
         if(key->algo_name() != "RSA") {
1✔
602
            throw CLI_Usage_Error("Unexpected key type for MARVIN test");
×
603
         }
604

605
         const size_t modulus_bytes = (key->key_length() + 7) / 8;
1✔
606

607
         std::vector<std::string> names;
1✔
608
         std::vector<uint8_t> ciphertext_data;
1✔
609

610
         for(const auto& filename : Botan::get_files_recursive(ctext_dir)) {
5✔
611
            const auto contents = this->slurp_file(filename);
4✔
612

613
            if(contents.size() != modulus_bytes) {
4✔
614
               throw CLI_Usage_Error(
×
615
                  Botan::fmt("The ciphertext file {} had different size ({}) than the RSA modulus ({})",
×
616
                             filename,
617
                             contents.size(),
×
618
                             modulus_bytes));
×
619
            }
620

621
            const auto parts = Botan::split_on(filename, '/');
4✔
622

623
            names.push_back(parts[parts.size() - 1]);
4✔
624
            ciphertext_data.insert(ciphertext_data.end(), contents.begin(), contents.end());
4✔
625
         }
9✔
626

627
         if(names.empty()) {
1✔
628
            throw CLI_Usage_Error("Empty ciphertext directory for MARVIN test");
×
629
         }
630

631
   #if defined(BOTAN_HAS_CHACHA_RNG)
632
         auto rng = Botan::ChaCha_RNG(Botan::system_rng());
1✔
633
   #else
634
         auto& rng = Botan::system_rng();
635
   #endif
636

637
         Botan::PK_Decryptor_EME op(*key, rng, "PKCS1v15");
1✔
638

639
         std::vector<size_t> indexes;
1✔
640
         for(size_t i = 0; i != names.size(); ++i) {
5✔
641
            indexes.push_back(i);
4✔
642
         }
643

644
         std::vector<std::vector<uint64_t>> measurements(names.size());
1✔
645
         for(auto& m : measurements) {
5✔
646
            m.reserve(measurement_runs);
4✔
647
         }
648

649
         // This is only set differently if we exit early from the loop
650
         size_t runs_completed = measurement_runs;
33✔
651

652
         for(size_t r = 0; r != measurement_runs; ++r) {
33✔
653
            if(r > 0 && report_every > 0 && (r % report_every) == 0) {
32✔
654
               std::cerr << "Gathering sample # " << r << "\n";
×
655
            }
656

657
            shuffle(indexes, rng);
32✔
658

659
            std::vector<uint8_t> ciphertext(modulus_bytes);
32✔
660
            for(size_t i = 0; i != indexes.size(); ++i) {
160✔
661
               const size_t testcase = indexes[i];
128✔
662

663
               // FIXME should this load be constant time?
664
               Botan::copy_mem(&ciphertext[0], &ciphertext_data[testcase * modulus_bytes], modulus_bytes);
128✔
665

666
               TimingTestTimer timer;
128✔
667
               op.decrypt_or_random(ciphertext.data(), modulus_bytes, expect_pt_len, rng);
128✔
668
               const uint64_t duration = timer.complete();
128✔
669
               BOTAN_ASSERT_NOMSG(measurements[testcase].size() == r);
128✔
670
               measurements[testcase].push_back(duration);
128✔
671
            }
672

673
   #if defined(BOTAN_TARGET_OS_HAS_POSIX1)
674
            // Early exit check
675
            if(g_sigint_recv != 0) {
32✔
676
               std::cerr << "Exiting early after " << r << " measurements\n";
×
677
               runs_completed = r;
×
678
               break;
×
679
            }
680
   #endif
681
         }
32✔
682

683
         for(size_t t = 0; t != names.size(); ++t) {
5✔
684
            if(t > 0) {
4✔
685
               output() << ",";
3✔
686
            }
687
            output() << names[t];
4✔
688
         }
689
         output() << "\n";
1✔
690

691
         for(size_t r = 0; r != runs_completed; ++r) {
33✔
692
            for(size_t t = 0; t != names.size(); ++t) {
160✔
693
               if(t > 0) {
128✔
694
                  output() << ",";
96✔
695
               }
696

697
               const uint64_t dur_nsec = measurements[t][r];
128✔
698
               if(output_nsec) {
128✔
699
                  output() << dur_nsec;
×
700
               } else {
701
                  const double dur_s = static_cast<double>(dur_nsec) / 1000000000.0;
128✔
702
                  output() << dur_s;
128✔
703
               }
704
            }
705
            output() << "\n";
32✔
706
         }
707
      }
3✔
708

709
      template <typename T>
710
      void shuffle(std::vector<T>& vec, Botan::RandomNumberGenerator& rng) {
32✔
711
         const size_t n = vec.size();
32✔
712
         for(size_t i = 0; i != n; ++i) {
160✔
713
            uint8_t jb[sizeof(uint64_t)];
714
            rng.randomize(jb, sizeof(jb));
128✔
715
            uint64_t j8 = Botan::load_le<uint64_t>(jb, 0);
128✔
716
            size_t j = i + static_cast<size_t>(j8) % (n - i);
128✔
717
            std::swap(vec[i], vec[j]);
128✔
718
         }
719
      }
32✔
720
};
721

722
BOTAN_REGISTER_COMMAND("marvin_test", MARVIN_Test_Command);
2✔
723

724
#endif
725

726
}  // namespace Botan_CLI
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