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

randombit / botan / 25476046290

07 May 2026 01:38AM UTC coverage: 89.331%. Remained the same
25476046290

push

github

randombit
Update for 3.12.0 release

107574 of 120422 relevant lines covered (89.33%)

11308499.77 hits per line

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

89.29
/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/ct_utils.h>
25
#include <botan/internal/filesystem.h>
26
#include <botan/internal/fmt.h>
27
#include <botan/internal/loadstor.h>
28
#include <botan/internal/parsing.h>
29
#include <botan/internal/target_info.h>
30
#include <chrono>
31
#include <fstream>
32
#include <iostream>
33
#include <numeric>
34
#include <sstream>
35

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

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

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

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

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

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

62
#if defined(BOTAN_HAS_TLS_CBC)
63
   #include <botan/block_cipher.h>
64
   #include <botan/mac.h>
65
   #include <botan/tls_exceptn.h>
66
   #include <botan/tls_version.h>
67
   #include <botan/internal/tls_cbc.h>
68
#endif
69

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

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

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

83
namespace Botan_CLI {
84

85
namespace {
86

87
void shuffle_idx(std::vector<size_t>& vec, Botan::RandomNumberGenerator& rng) {
222✔
88
   const size_t n = vec.size();
222✔
89
   for(size_t i = 0; i != n; ++i) {
863✔
90
      uint8_t jb[sizeof(uint64_t)];
641✔
91
      rng.randomize(jb, sizeof(jb));
641✔
92
      const uint64_t j8 = Botan::load_le<uint64_t>(jb, 0);
641✔
93
      const size_t j = i + static_cast<size_t>(j8) % (n - i);
641✔
94
      std::swap(vec[i], vec[j]);
641✔
95
   }
96
}
222✔
97

98
class TimingTestTimer final {
99
   public:
100
      TimingTestTimer() : m_start(get_high_resolution_clock()) {}
641✔
101

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

104
   private:
105
      static uint64_t get_high_resolution_clock() {
1,282✔
106
         // TODO use cpu clock where possible/relevant incl serializing instructions
107
         auto now = std::chrono::high_resolution_clock::now().time_since_epoch();
1,206✔
108
         return std::chrono::duration_cast<std::chrono::nanoseconds>(now).count();
1,282✔
109
      }
110

111
      uint64_t m_start;
112
};
113

114
class Timing_Test {
115
   public:
116
      Timing_Test() {
10✔
117
         /*
118
         A constant seed is ok here since the timing test rng just needs to be
119
         "random" but not cryptographically secure - even std::rand() would be ok.
120
         */
121

122
#if defined(BOTAN_HAS_CHACHA_RNG)
123
         m_rng = std::make_unique<Botan::ChaCha_RNG>(std::vector<uint8_t>(64, 0));
10✔
124
#elif defined(BOTAN_HAS_SYSTEM_RNG)
125
         m_rng = std::make_unique<Botan::System_RNG>();
126
#else
127
         throw Botan::Not_Implemented("Missing RNG for timing_test");
128
#endif
129
      }
10✔
130

131
      virtual ~Timing_Test() = default;
×
132

133
      Timing_Test(const Timing_Test& other) = delete;
134
      Timing_Test(Timing_Test&& other) = delete;
135
      Timing_Test& operator=(const Timing_Test& other) = delete;
136
      Timing_Test& operator=(Timing_Test&& other) = delete;
137

138
      std::vector<std::vector<uint64_t>> execute_evaluation(const std::vector<std::string>& inputs,
139
                                                            size_t warmup_runs,
140
                                                            size_t measurement_runs);
141

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

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

146
   protected:
147
      Botan::RandomNumberGenerator& timing_test_rng() { return (*m_rng); }
2✔
148

149
   private:
150
      std::unique_ptr<Botan::RandomNumberGenerator> m_rng;
151
};
152

153
#if defined(BOTAN_HAS_RSA) && defined(BOTAN_HAS_EME_PKCS1) && defined(BOTAN_HAS_EME_RAW)
154

155
class Bleichenbacker_Timing_Test final : public Timing_Test {
×
156
   public:
157
      explicit Bleichenbacker_Timing_Test(size_t keysize) :
1✔
158
            m_privkey(timing_test_rng(), keysize),
1✔
159
            m_pubkey(m_privkey.public_key()),
1✔
160
            m_enc(*m_pubkey, timing_test_rng(), "Raw"),
1✔
161
            m_dec(m_privkey, timing_test_rng(), "PKCS1v15") {}
2✔
162

163
      std::vector<uint8_t> prepare_input(const std::string& input) override {
4✔
164
         const std::vector<uint8_t> input_vector = Botan::hex_decode(input);
4✔
165
         return m_enc.encrypt(input_vector, timing_test_rng());
4✔
166
      }
4✔
167

168
      uint64_t measure_critical_function(const std::vector<uint8_t>& input) override {
76✔
169
         const TimingTestTimer timer;
76✔
170
         m_dec.decrypt_or_random(input.data(), m_ctext_length, m_expected_content_size, timing_test_rng());
76✔
171
         return timer.complete();
76✔
172
      }
173

174
   private:
175
      const size_t m_expected_content_size = 48;
176
      const size_t m_ctext_length = 256;
177
      Botan::RSA_PrivateKey m_privkey;
178
      std::unique_ptr<Botan::Public_Key> m_pubkey;
179
      Botan::PK_Encryptor_EME m_enc;
180
      Botan::PK_Decryptor_EME m_dec;
181
};
182

183
#endif
184

185
#if defined(BOTAN_HAS_RSA) && defined(BOTAN_HAS_EME_OAEP) && defined(BOTAN_HAS_EME_RAW)
186

187
/*
188
* Test Manger OAEP side channel
189
*
190
* "A Chosen Ciphertext Attack on RSA Optimal Asymmetric Encryption
191
* Padding (OAEP) as Standardized in PKCS #1 v2.0" James Manger
192
* http://archiv.infsec.ethz.ch/education/fs08/secsem/Manger01.pdf
193
*/
194
class Manger_Timing_Test final : public Timing_Test {
×
195
   public:
196
      explicit Manger_Timing_Test(size_t keysize) :
1✔
197
            m_privkey(timing_test_rng(), keysize),
1✔
198
            m_pubkey(m_privkey.public_key()),
1✔
199
            m_enc(*m_pubkey, timing_test_rng(), m_encrypt_padding),
1✔
200
            m_dec(m_privkey, timing_test_rng(), m_decrypt_padding) {}
2✔
201

202
      std::vector<uint8_t> prepare_input(const std::string& input) override {
2✔
203
         const std::vector<uint8_t> input_vector = Botan::hex_decode(input);
2✔
204
         return m_enc.encrypt(input_vector, timing_test_rng());
2✔
205
      }
2✔
206

207
      uint64_t measure_critical_function(const std::vector<uint8_t>& input) override {
38✔
208
         const TimingTestTimer timer;
38✔
209
         try {
38✔
210
            m_dec.decrypt(input.data(), m_ctext_length);
38✔
211
         } catch(Botan::Decoding_Error&) {}
38✔
212
         return timer.complete();
38✔
213
      }
214

215
   private:
216
      const std::string m_encrypt_padding = "Raw";
217
      const std::string m_decrypt_padding = "EME1(SHA-256)";
218
      const size_t m_ctext_length = 256;
219
      Botan::RSA_PrivateKey m_privkey;
220
      std::unique_ptr<Botan::Public_Key> m_pubkey;
221
      Botan::PK_Encryptor_EME m_enc;
222
      Botan::PK_Decryptor_EME m_dec;
223
};
224

225
#endif
226

227
#if defined(BOTAN_HAS_TLS_CBC)
228

229
/*
230
* Test handling of countermeasure to the Lucky13 attack
231
*/
232
class Lucky13_Timing_Test final : public Timing_Test {
×
233
   public:
234
      Lucky13_Timing_Test(const std::string& mac_name, size_t mac_keylen) :
4✔
235
            m_mac_algo(mac_name),
8✔
236
            m_mac_keylen(mac_keylen),
4✔
237
            m_dec(Botan::BlockCipher::create_or_throw("AES-128"),
12✔
238
                  Botan::MessageAuthenticationCode::create_or_throw("HMAC(" + m_mac_algo + ")"),
12✔
239
                  16,
240
                  m_mac_keylen,
4✔
241
                  Botan::TLS::Protocol_Version::TLS_V12,
242
                  false) {}
8✔
243

244
      std::vector<uint8_t> prepare_input(const std::string& input) override;
245
      uint64_t measure_critical_function(const std::vector<uint8_t>& input) override;
246

247
   private:
248
      const std::string m_mac_algo;
249
      const size_t m_mac_keylen;
250
      Botan::TLS::TLS_CBC_HMAC_AEAD_Decryption m_dec;
251
};
252

253
std::vector<uint8_t> Lucky13_Timing_Test::prepare_input(const std::string& input) {
12✔
254
   const std::vector<uint8_t> input_vector = Botan::hex_decode(input);
12✔
255
   const std::vector<uint8_t> key(16);
12✔
256
   const std::vector<uint8_t> iv(16);
12✔
257

258
   auto enc = Botan::Cipher_Mode::create("AES-128/CBC/NoPadding", Botan::Cipher_Dir::Encryption);
12✔
259
   enc->set_key(key);
12✔
260
   enc->start(iv);
12✔
261
   Botan::secure_vector<uint8_t> buf(input_vector.begin(), input_vector.end());
12✔
262
   enc->finish(buf);
12✔
263

264
   return unlock(buf);
12✔
265
}
60✔
266

267
uint64_t Lucky13_Timing_Test::measure_critical_function(const std::vector<uint8_t>& input) {
228✔
268
   Botan::secure_vector<uint8_t> data(input.begin(), input.end());
228✔
269
   Botan::secure_vector<uint8_t> aad(13);
228✔
270
   const Botan::secure_vector<uint8_t> iv(16);
228✔
271
   const Botan::secure_vector<uint8_t> key(16 + m_mac_keylen);
228✔
272

273
   m_dec.set_key(unlock(key));
228✔
274
   m_dec.set_associated_data(aad);
228✔
275
   m_dec.start(unlock(iv));
228✔
276

277
   const TimingTestTimer timer;
228✔
278
   try {
228✔
279
      m_dec.finish(data);
228✔
280
   } catch(Botan::TLS::TLS_Exception&) {}
228✔
281
   return timer.complete();
228✔
282
}
912✔
283

284
#endif
285

286
#if defined(BOTAN_HAS_ECDSA)
287

288
class ECDSA_Timing_Test final : public Timing_Test {
×
289
   public:
290
      explicit ECDSA_Timing_Test(const std::string& ecgroup);
291

292
      uint64_t measure_critical_function(const std::vector<uint8_t>& input) override;
293

294
   private:
295
      const Botan::EC_Group m_group;
296
      const Botan::EC_Scalar m_x;
297
      Botan::EC_Scalar m_b;
298
};
299

300
ECDSA_Timing_Test::ECDSA_Timing_Test(const std::string& ecgroup) :
1✔
301
      m_group(Botan::EC_Group::from_name(ecgroup)),
1✔
302
      m_x(Botan::EC_Scalar::random(m_group, timing_test_rng())),
1✔
303
      m_b(Botan::EC_Scalar::random(m_group, timing_test_rng())) {}
2✔
304

305
uint64_t ECDSA_Timing_Test::measure_critical_function(const std::vector<uint8_t>& input) {
38✔
306
   const auto k = Botan::EC_Scalar::from_bytes_with_trunc(m_group, input);
38✔
307
   // fixed message to minimize noise
308
   const auto m = Botan::EC_Scalar::from_bytes_with_trunc(m_group, std::vector<uint8_t>{5});
38✔
309

310
   const TimingTestTimer timer;
38✔
311

312
   // the following ECDSA operations involve and should not leak any information about k
313

314
   const auto r = Botan::EC_Scalar::gk_x_mod_order(k, timing_test_rng());
38✔
315

316
   const auto k_inv = (m_b * k).invert();
38✔
317

318
   const auto xr_m = ((m_x * m_b) * r) + (m * m_b);
38✔
319

320
   const auto s = (k_inv * xr_m);
38✔
321

322
   // Generate the next blinding value via modular squaring
323
   m_b.square_self();
38✔
324

325
   BOTAN_UNUSED(r, s);
38✔
326

327
   return timer.complete();
76✔
328
}
38✔
329

330
#endif
331

332
#if defined(BOTAN_HAS_ECC_GROUP)
333

334
class ECC_Mul_Timing_Test final : public Timing_Test {
×
335
   public:
336
      explicit ECC_Mul_Timing_Test(std::string_view ecgroup) : m_group(Botan::EC_Group::from_name(ecgroup)) {}
1✔
337

338
      uint64_t measure_critical_function(const std::vector<uint8_t>& input) override;
339

340
   private:
341
      const Botan::EC_Group m_group;
342
};
343

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

347
   const TimingTestTimer timer;
38✔
348
   const auto kG = Botan::EC_AffinePoint::g_mul(k, timing_test_rng());
38✔
349
   return timer.complete();
76✔
350
}
38✔
351

352
#endif
353

354
#if defined(BOTAN_HAS_DL_GROUP)
355

356
class Powmod_Timing_Test final : public Timing_Test {
×
357
   public:
358
      explicit Powmod_Timing_Test(std::string_view dl_group) : m_group(Botan::DL_Group::from_name(dl_group)) {}
1✔
359

360
      uint64_t measure_critical_function(const std::vector<uint8_t>& input) override;
361

362
   private:
363
      Botan::DL_Group m_group;
364
};
365

366
uint64_t Powmod_Timing_Test::measure_critical_function(const std::vector<uint8_t>& input) {
57✔
367
   const Botan::BigInt x(input.data(), input.size());
57✔
368
   const size_t max_x_bits = m_group.p_bits();
57✔
369

370
   const TimingTestTimer timer;
57✔
371

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

374
   return timer.complete();
114✔
375
}
57✔
376

377
#endif
378

379
#if defined(BOTAN_HAS_NUMBERTHEORY)
380

381
class Invmod_Timing_Test final : public Timing_Test {
×
382
   public:
383
      explicit Invmod_Timing_Test(size_t p_bits) { m_p = Botan::random_prime(timing_test_rng(), p_bits); }
2✔
384

385
      uint64_t measure_critical_function(const std::vector<uint8_t>& input) override;
386

387
   private:
388
      Botan::BigInt m_p;
389
};
390

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

394
   const TimingTestTimer timer;
38✔
395
   const Botan::BigInt inv = Botan::inverse_mod_secret_prime(k, m_p);
38✔
396
   return timer.complete();
76✔
397
}
38✔
398

399
#endif
400

401
std::vector<std::vector<uint64_t>> Timing_Test::execute_evaluation(const std::vector<std::string>& raw_inputs,
10✔
402
                                                                   size_t warmup_runs,
403
                                                                   size_t measurement_runs) {
404
   std::vector<std::vector<uint64_t>> all_results(raw_inputs.size());
10✔
405
   std::vector<std::vector<uint8_t>> inputs(raw_inputs.size());
10✔
406

407
   for(auto& result : all_results) {
37✔
408
      result.reserve(measurement_runs);
27✔
409
   }
410

411
   for(size_t i = 0; i != inputs.size(); ++i) {
37✔
412
      inputs[i] = prepare_input(raw_inputs[i]);
27✔
413
   }
414

415
   // arbitrary upper bounds of 1 and 10 million resp
416
   if(warmup_runs > 1000000 || measurement_runs > 100000000) {
10✔
417
      throw CLI_Error("Requested execution counts too large, rejecting");
×
418
   }
419

420
   size_t total_runs = 0;
10✔
421
   std::vector<uint64_t> results(inputs.size());
10✔
422

423
   std::vector<size_t> indexes(inputs.size());
10✔
424
   std::iota(indexes.begin(), indexes.end(), size_t{0});
10✔
425

426
   while(total_runs < (warmup_runs + measurement_runs)) {
200✔
427
      shuffle_idx(indexes, *m_rng);
190✔
428

429
      for(const size_t testcase : indexes) {
703✔
430
         results[testcase] = measure_critical_function(inputs[testcase]);
513✔
431
      }
432

433
      total_runs++;
190✔
434

435
      if(total_runs > warmup_runs) {
190✔
436
         for(size_t i = 0; i != results.size(); ++i) {
592✔
437
            all_results[i].push_back(results[i]);
432✔
438
         }
439
      }
440
   }
441

442
   return all_results;
10✔
443
}
10✔
444

445
class Timing_Test_Command final : public Command {
446
   public:
447
      Timing_Test_Command() :
11✔
448
            Command(
449
               "timing_test test_type --test-data-file= --test-data-dir=src/tests/data/timing "
450
               "--warmup-runs=5000 --measurement-runs=50000") {}
22✔
451

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

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

456
      void go() override {
10✔
457
         const std::string test_type = get_arg("test_type");
10✔
458
         const size_t warmup_runs = get_arg_sz("warmup-runs");
10✔
459
         const size_t measurement_runs = get_arg_sz("measurement-runs");
10✔
460

461
         std::unique_ptr<Timing_Test> test = lookup_timing_test(test_type);
10✔
462

463
         if(!test) {
10✔
464
            throw CLI_Error("Unknown or unavailable test type '" + test_type + "'");
×
465
         }
466

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

469
         if(filename.empty()) {
10✔
470
            const std::string test_data_dir = get_arg("test-data-dir");
10✔
471
            filename = test_data_dir + "/" + test_type + ".vec";
20✔
472
         }
10✔
473

474
         const std::vector<std::string> lines = read_testdata(filename);
10✔
475

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

478
         size_t unique_id = 0;
10✔
479
         std::ostringstream oss;
10✔
480
         for(size_t secret_id = 0; secret_id != results.size(); ++secret_id) {
37✔
481
            for(size_t i = 0; i != results[secret_id].size(); ++i) {
459✔
482
               oss << unique_id++ << ";" << secret_id << ";" << results[secret_id][i] << "\n";
432✔
483
            }
484
         }
485

486
         output() << oss.str();
10✔
487
      }
20✔
488

489
   private:
490
      static std::vector<std::string> read_testdata(const std::string& filename) {
10✔
491
         std::vector<std::string> lines;
10✔
492
         std::ifstream infile(filename);
10✔
493
         if(!infile.good()) {
10✔
494
            throw CLI_Error("Error reading test data from '" + filename + "'");
×
495
         }
496
         std::string line;
10✔
497
         while(std::getline(infile, line)) {
70✔
498
            if(!line.empty() && line.at(0) != '#') {
60✔
499
               lines.push_back(line);
27✔
500
            }
501
         }
502
         return lines;
20✔
503
      }
10✔
504

505
      static std::unique_ptr<Timing_Test> lookup_timing_test(std::string_view test_type);
506

507
      std::string help_text() const override {
×
508
         // TODO check feature macros
509
         return (Command::help_text() +
×
510
                 "\ntest_type can take on values "
511
                 "bleichenbacher "
512
                 "manger "
513
                 "ecdsa "
514
                 "ecc_mul "
515
                 "inverse_mod "
516
                 "pow_mod "
517
                 "lucky13sec3 "
518
                 "lucky13sec4sha1 "
519
                 "lucky13sec4sha256 "
520
                 "lucky13sec4sha384 ");
×
521
      }
522
};
523

524
std::unique_ptr<Timing_Test> Timing_Test_Command::lookup_timing_test(std::string_view test_type) {
10✔
525
#if defined(BOTAN_HAS_RSA) && defined(BOTAN_HAS_EME_PKCS1) && defined(BOTAN_HAS_EME_RAW)
526
   if(test_type == "bleichenbacher") {
10✔
527
      return std::make_unique<Bleichenbacker_Timing_Test>(2048);
1✔
528
   }
529
#endif
530

531
#if defined(BOTAN_HAS_RSA) && defined(BOTAN_HAS_EME_OAEP) && defined(BOTAN_HAS_EME_RAW)
532
   if(test_type == "manger") {
9✔
533
      return std::make_unique<Manger_Timing_Test>(2048);
1✔
534
   }
535
#endif
536

537
#if defined(BOTAN_HAS_ECDSA)
538
   if(test_type == "ecdsa") {
8✔
539
      return std::make_unique<ECDSA_Timing_Test>("secp384r1");
1✔
540
   }
541
#endif
542

543
#if defined(BOTAN_HAS_ECC_GROUP)
544
   if(test_type == "ecc_mul") {
7✔
545
      return std::make_unique<ECC_Mul_Timing_Test>("brainpool512r1");
1✔
546
   }
547
#endif
548

549
#if defined(BOTAN_HAS_NUMBERTHEORY)
550
   if(test_type == "inverse_mod") {
6✔
551
      return std::make_unique<Invmod_Timing_Test>(512);
1✔
552
   }
553
#endif
554

555
#if defined(BOTAN_HAS_DL_GROUP)
556
   if(test_type == "pow_mod") {
5✔
557
      return std::make_unique<Powmod_Timing_Test>("modp/ietf/1024");
1✔
558
   }
559
#endif
560

561
#if defined(BOTAN_HAS_TLS_CBC)
562
   if(test_type == "lucky13sec3" || test_type == "lucky13sec4sha1") {
6✔
563
      return std::make_unique<Lucky13_Timing_Test>("SHA-1", 20);
2✔
564
   }
565
   if(test_type == "lucky13sec4sha256") {
2✔
566
      return std::make_unique<Lucky13_Timing_Test>("SHA-256", 32);
1✔
567
   }
568
   if(test_type == "lucky13sec4sha384") {
1✔
569
      return std::make_unique<Lucky13_Timing_Test>("SHA-384", 48);
1✔
570
   }
571
#endif
572

573
   BOTAN_UNUSED(test_type);
×
574

575
   return nullptr;
×
576
}
577

578
BOTAN_REGISTER_COMMAND("timing_test", Timing_Test_Command);
11✔
579

580
#if defined(BOTAN_HAS_RSA) && defined(BOTAN_HAS_EME_PKCS1) && defined(BOTAN_TARGET_OS_HAS_FILESYSTEM) && \
581
   defined(BOTAN_HAS_SYSTEM_RNG)
582

583
class MARVIN_Test_Command final : public Command {
584
   public:
585
      MARVIN_Test_Command() :
2✔
586
            Command("marvin_test key_file ctext_dir --runs=1K --report-every=0 --output-nsec --expect-pt-len=0") {}
4✔
587

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

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

592
   #if defined(BOTAN_TARGET_OS_HAS_POSIX1)
593
      static inline volatile sig_atomic_t g_sigint_recv = 0;
594

595
      static void marvin_sigint_handler(int /*signal*/) { g_sigint_recv = 1; }
×
596
   #endif
597

598
      void go() override {
1✔
599
         const std::string key_file = get_arg("key_file");
1✔
600
         const std::string ctext_dir = get_arg("ctext_dir");
1✔
601
         const size_t measurement_runs = parse_runs_arg(get_arg("runs"));
1✔
602
         const size_t expect_pt_len = get_arg_sz("expect-pt-len");
1✔
603
         const size_t report_every = get_arg_sz("report-every");
1✔
604
         const bool output_nsec = flag_set("output-nsec");
1✔
605

606
   #if defined(BOTAN_TARGET_OS_HAS_POSIX1)
607
         ::setenv("BOTAN_THREAD_POOL_SIZE", "none", /*overwrite?*/ 1);
1✔
608

609
         struct sigaction sigaction {};
1✔
610

611
         sigaction.sa_handler = marvin_sigint_handler;
1✔
612
         sigemptyset(&sigaction.sa_mask);
1✔
613
         sigaction.sa_flags = 0;
1✔
614

615
         const int rc = ::sigaction(SIGINT, &sigaction, nullptr);
1✔
616
         if(rc != 0) {
1✔
617
            throw CLI_Error("Failed to set SIGINT handler");
×
618
         }
619
   #endif
620

621
         Botan::DataSource_Stream key_src(key_file);
1✔
622
         const auto key = Botan::PKCS8::load_key(key_src);
1✔
623

624
         if(key->algo_name() != "RSA") {
1✔
625
            throw CLI_Usage_Error("Unexpected key type for MARVIN test");
×
626
         }
627

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

630
         std::vector<std::string> names;
1✔
631
         std::vector<uint8_t> ciphertext_data;
1✔
632

633
         for(const auto& filename : Botan::get_files_recursive(ctext_dir)) {
5✔
634
            const auto contents = this->slurp_file(filename);
4✔
635

636
            if(contents.size() != modulus_bytes) {
4✔
637
               throw CLI_Usage_Error(
×
638
                  Botan::fmt("The ciphertext file {} had different size ({}) than the RSA modulus ({})",
×
639
                             filename,
640
                             contents.size(),
×
641
                             modulus_bytes));
×
642
            }
643

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

646
            names.push_back(parts[parts.size() - 1]);
4✔
647
            ciphertext_data.insert(ciphertext_data.end(), contents.begin(), contents.end());
4✔
648
         }
9✔
649

650
         if(names.empty()) {
1✔
651
            throw CLI_Usage_Error("Empty ciphertext directory for MARVIN test");
×
652
         }
653

654
         auto& test_results_file = output();
1✔
655

656
         const size_t testcases = names.size();
1✔
657

658
   #if defined(BOTAN_HAS_CHACHA_RNG)
659
         auto rng = Botan::ChaCha_RNG(Botan::system_rng());
1✔
660
   #else
661
         auto& rng = Botan::system_rng();
662
   #endif
663

664
         const Botan::PK_Decryptor_EME op(*key, rng, "PKCS1v15");
1✔
665

666
         std::vector<size_t> indexes(testcases);
1✔
667
         std::iota(indexes.begin(), indexes.end(), size_t{0});
1✔
668

669
         std::vector<std::vector<uint64_t>> measurements(testcases);
1✔
670
         for(auto& m : measurements) {
5✔
671
            m.reserve(measurement_runs);
4✔
672
         }
673

674
         // This is only set differently if we exit early from the loop
675
         size_t runs_completed = measurement_runs;
1✔
676

677
         std::vector<uint8_t> ciphertext(modulus_bytes);
1✔
678

679
         for(size_t r = 0; r != measurement_runs; ++r) {
33✔
680
            if(r > 0 && report_every > 0 && (r % report_every) == 0) {
32✔
681
               std::cerr << "Gathering sample # " << r << "\n";
×
682
            }
683

684
            shuffle_idx(indexes, rng);
32✔
685

686
            for(const size_t testcase : indexes) {
160✔
687
               // Load the test ciphertext in constant time to avoid cache pollution
688
               for(size_t j = 0; j != testcases; ++j) {
640✔
689
                  const auto j_eq_testcase = Botan::CT::Mask<size_t>::is_equal(j, testcase).as_choice();
512✔
690
                  const auto* testcase_j = &ciphertext_data[j * modulus_bytes];
512✔
691
                  Botan::CT::conditional_assign_mem(j_eq_testcase, ciphertext.data(), testcase_j, modulus_bytes);
1,024✔
692
               }
693

694
               const TimingTestTimer timer;
128✔
695
               op.decrypt_or_random(ciphertext.data(), modulus_bytes, expect_pt_len, rng);
128✔
696
               const uint64_t duration = timer.complete();
128✔
697
               BOTAN_ASSERT_NOMSG(measurements[testcase].size() == r);
128✔
698
               measurements[testcase].push_back(duration);
128✔
699
            }
700

701
   #if defined(BOTAN_TARGET_OS_HAS_POSIX1)
702
            // Early exit check
703
            if(g_sigint_recv != 0) {
32✔
704
               std::cerr << "Exiting early after " << r << " measurements\n";
×
705
               runs_completed = r;
706
               break;
707
            }
708
   #endif
709
         }
710

711
         report_results(test_results_file, names, measurements, runs_completed, output_nsec);
1✔
712
      }
3✔
713

714
   private:
715
      static void report_results(std::ostream& output,
1✔
716
                                 std::span<const std::string> names,
717
                                 std::span<const std::vector<uint64_t>> measurements,
718
                                 size_t runs_completed,
719
                                 bool output_nsec) {
720
         for(size_t t = 0; t != names.size(); ++t) {
5✔
721
            if(t > 0) {
4✔
722
               output << ",";
3✔
723
            }
724
            output << names[t];
4✔
725
         }
726
         output << "\n";
1✔
727

728
         for(size_t r = 0; r != runs_completed; ++r) {
33✔
729
            for(size_t t = 0; t != names.size(); ++t) {
160✔
730
               if(t > 0) {
128✔
731
                  output << ",";
96✔
732
               }
733

734
               const uint64_t dur_nsec = measurements[t][r];
128✔
735
               if(output_nsec) {
128✔
736
                  output << dur_nsec;
×
737
               } else {
738
                  const double dur_s = static_cast<double>(dur_nsec) / 1000000000.0;
128✔
739
                  output << dur_s;
128✔
740
               }
741
            }
742
            output << "\n";
32✔
743
         }
744
      }
1✔
745

746
      static size_t parse_runs_arg(const std::string& param) {
1✔
747
         if(param.starts_with("-")) {
1✔
748
            throw CLI_Usage_Error("Cannot have a negative run count");
×
749
         }
750

751
         if(param.ends_with("m") || param.ends_with("M")) {
1✔
752
            return parse_runs_arg(param.substr(0, param.size() - 1)) * 1'000'000;
×
753
         } else if(param.ends_with("k") || param.ends_with("K")) {
1✔
754
            return parse_runs_arg(param.substr(0, param.size() - 1)) * 1'000;
×
755
         } else {
756
            try {
1✔
757
               return static_cast<size_t>(std::stoul(param));
1✔
758
            } catch(std::exception&) {
×
759
               throw CLI_Usage_Error("Unexpected syntax for --runs option (try 1000, 1K, or 2M)");
×
760
            }
×
761
         }
762
      }
763
};
764

765
BOTAN_REGISTER_COMMAND("marvin_test", MARVIN_Test_Command);
2✔
766

767
#endif
768

769
}  // namespace
770

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