Coverage Report

Created: 2025-04-11 06:34

/src/botan/src/lib/tls/tls13_pqc/hybrid_public_key.cpp
Line
Count
Source (jump to first uncovered line)
1
/**
2
* Composite key pair that exposes the Public/Private key API but combines
3
* multiple key agreement schemes into a hybrid algorithm.
4
*
5
* (C) 2023 Jack Lloyd
6
*     2023 Fabian Albert, René Meusel - Rohde & Schwarz Cybersecurity
7
*
8
* Botan is released under the Simplified BSD License (see license.txt)
9
*/
10
11
#include <botan/internal/hybrid_public_key.h>
12
13
#include <botan/ec_group.h>
14
#include <botan/pk_algs.h>
15
16
#include <botan/internal/fmt.h>
17
#include <botan/internal/hybrid_kem_ops.h>
18
#include <botan/internal/kex_to_kem_adapter.h>
19
#include <botan/internal/pk_ops_impl.h>
20
#include <botan/internal/stl_util.h>
21
22
namespace Botan::TLS {
23
24
namespace {
25
26
0
std::vector<std::pair<std::string, std::string>> algorithm_specs_for_group(Group_Params group) {
27
0
   BOTAN_ARG_CHECK(group.is_pqc_hybrid(), "Group is not hybrid");
28
29
0
   switch(group.code()) {
30
      // draft-kwiatkowski-tls-ecdhe-mlkem-02 Section 3
31
      //
32
      //    NIST's special publication 800-56Cr2 approves the usage of HKDF with
33
      //    two distinct shared secrets, with the condition that the first one
34
      //    is computed by a FIPS-approved key-establishment scheme.  FIPS also
35
      //    requires a certified implementation of the scheme, which will remain
36
      //    more ubiqutous for secp256r1 in the coming years.
37
      //
38
      //    For this reason we put the ML-KEM-768 shared secret first in
39
      //    X25519MLKEM768, and the secp256r1 shared secret first in
40
      //    SecP256r1MLKEM768.
41
0
      case Group_Params::HYBRID_X25519_ML_KEM_768:
42
0
         return {{"ML-KEM", "ML-KEM-768"}, {"X25519", "X25519"}};
43
0
      case Group_Params::HYBRID_SECP256R1_ML_KEM_768:
44
0
         return {{"ECDH", "secp256r1"}, {"ML-KEM", "ML-KEM-768"}};
45
0
      case Group_Params::HYBRID_SECP384R1_ML_KEM_1024:
46
0
         return {{"ECDH", "secp384r1"}, {"ML-KEM", "ML-KEM-1024"}};
47
48
0
      case Group_Params::HYBRID_X25519_eFRODOKEM_640_SHAKE_OQS:
49
0
         return {{"X25519", "X25519"}, {"FrodoKEM", "eFrodoKEM-640-SHAKE"}};
50
0
      case Group_Params::HYBRID_X25519_eFRODOKEM_640_AES_OQS:
51
0
         return {{"X25519", "X25519"}, {"FrodoKEM", "eFrodoKEM-640-AES"}};
52
0
      case Group_Params::HYBRID_X448_eFRODOKEM_976_SHAKE_OQS:
53
0
         return {{"X448", "X448"}, {"FrodoKEM", "eFrodoKEM-976-SHAKE"}};
54
0
      case Group_Params::HYBRID_X448_eFRODOKEM_976_AES_OQS:
55
0
         return {{"X448", "X448"}, {"FrodoKEM", "eFrodoKEM-976-AES"}};
56
57
0
      case Group_Params::HYBRID_SECP256R1_eFRODOKEM_640_SHAKE_OQS:
58
0
         return {{"ECDH", "secp256r1"}, {"FrodoKEM", "eFrodoKEM-640-SHAKE"}};
59
0
      case Group_Params::HYBRID_SECP256R1_eFRODOKEM_640_AES_OQS:
60
0
         return {{"ECDH", "secp256r1"}, {"FrodoKEM", "eFrodoKEM-640-AES"}};
61
62
0
      case Group_Params::HYBRID_SECP384R1_eFRODOKEM_976_SHAKE_OQS:
63
0
         return {{"ECDH", "secp384r1"}, {"FrodoKEM", "eFrodoKEM-976-SHAKE"}};
64
0
      case Group_Params::HYBRID_SECP384R1_eFRODOKEM_976_AES_OQS:
65
0
         return {{"ECDH", "secp384r1"}, {"FrodoKEM", "eFrodoKEM-976-AES"}};
66
67
0
      case Group_Params::HYBRID_SECP521R1_eFRODOKEM_1344_SHAKE_OQS:
68
0
         return {{"ECDH", "secp521r1"}, {"FrodoKEM", "eFrodoKEM-1344-SHAKE"}};
69
0
      case Group_Params::HYBRID_SECP521R1_eFRODOKEM_1344_AES_OQS:
70
0
         return {{"ECDH", "secp521r1"}, {"FrodoKEM", "eFrodoKEM-1344-AES"}};
71
72
0
      default:
73
0
         return {};
74
0
   }
75
0
}
76
77
0
std::vector<AlgorithmIdentifier> algorithm_identifiers_for_group(Group_Params group) {
78
0
   BOTAN_ASSERT_NOMSG(group.is_pqc_hybrid());
79
80
0
   const auto specs = algorithm_specs_for_group(group);
81
0
   std::vector<AlgorithmIdentifier> result;
82
0
   result.reserve(specs.size());
83
84
   // This maps the string-based algorithm specs hard-coded above to OID-based
85
   // AlgorithmIdentifier objects. The mapping is needed because
86
   // load_public_key() depends on those while create_private_key() requires the
87
   // strong-based spec.
88
   //
89
   // TODO: This is inconvenient, confusing and error-prone. Find a better way
90
   //       to load arbitrary public keys.
91
0
   for(const auto& spec : specs) {
92
0
      if(spec.first == "ECDH") {
93
0
         result.push_back(AlgorithmIdentifier("ECDH", EC_Group::from_name(spec.second).DER_encode()));
94
0
      } else {
95
0
         result.push_back(AlgorithmIdentifier(spec.second, AlgorithmIdentifier::USE_EMPTY_PARAM));
96
0
      }
97
0
   }
98
99
0
   return result;
100
0
}
101
102
0
std::vector<size_t> public_key_lengths_for_group(Group_Params group) {
103
0
   BOTAN_ASSERT_NOMSG(group.is_pqc_hybrid());
104
105
   // This duplicates information of the algorithm internals.
106
   //
107
   // TODO: Find a way to expose important algorithm constants globally
108
   //       in the library, to avoid violating the DRY principle.
109
0
   switch(group.code()) {
110
0
      case Group_Params::HYBRID_X25519_ML_KEM_768:
111
0
         return {1184, 32};
112
0
      case Group_Params::HYBRID_SECP256R1_ML_KEM_768:
113
0
         return {65, 1184};
114
0
      case Group_Params::HYBRID_SECP384R1_ML_KEM_1024:
115
0
         return {97, 1568};
116
117
0
      case Group_Params::HYBRID_X25519_eFRODOKEM_640_SHAKE_OQS:
118
0
         return {32, 9616};
119
0
      case Group_Params::HYBRID_X25519_eFRODOKEM_640_AES_OQS:
120
0
         return {32, 9616};
121
0
      case Group_Params::HYBRID_X448_eFRODOKEM_976_SHAKE_OQS:
122
0
         return {56, 15632};
123
0
      case Group_Params::HYBRID_X448_eFRODOKEM_976_AES_OQS:
124
0
         return {56, 15632};
125
126
0
      case Group_Params::HYBRID_SECP256R1_eFRODOKEM_640_SHAKE_OQS:
127
0
         return {65, 9616};
128
0
      case Group_Params::HYBRID_SECP256R1_eFRODOKEM_640_AES_OQS:
129
0
         return {65, 9616};
130
131
0
      case Group_Params::HYBRID_SECP384R1_eFRODOKEM_976_SHAKE_OQS:
132
0
         return {97, 15632};
133
0
      case Group_Params::HYBRID_SECP384R1_eFRODOKEM_976_AES_OQS:
134
0
         return {97, 15632};
135
136
0
      case Group_Params::HYBRID_SECP521R1_eFRODOKEM_1344_SHAKE_OQS:
137
0
         return {133, 21520};
138
0
      case Group_Params::HYBRID_SECP521R1_eFRODOKEM_1344_AES_OQS:
139
0
         return {133, 21520};
140
141
0
      default:
142
0
         return {};
143
0
   }
144
0
}
145
146
0
std::vector<std::unique_ptr<Public_Key>> convert_kex_to_kem_pks(std::vector<std::unique_ptr<Public_Key>> pks) {
147
0
   std::vector<std::unique_ptr<Public_Key>> result;
148
0
   std::transform(pks.begin(), pks.end(), std::back_inserter(result), [](auto& key) -> std::unique_ptr<Public_Key> {
149
0
      BOTAN_ARG_CHECK(key != nullptr, "Public key list contains a nullptr");
150
0
      if(key->supports_operation(PublicKeyOperation::KeyAgreement) &&
151
0
         !key->supports_operation(PublicKeyOperation::KeyEncapsulation)) {
152
0
         return std::make_unique<KEX_to_KEM_Adapter_PublicKey>(std::move(key));
153
0
      } else {
154
0
         return std::move(key);
155
0
      }
156
0
   });
157
0
   return result;
158
0
}
159
160
0
std::vector<std::unique_ptr<Private_Key>> convert_kex_to_kem_sks(std::vector<std::unique_ptr<Private_Key>> sks) {
161
0
   std::vector<std::unique_ptr<Private_Key>> result;
162
0
   std::transform(sks.begin(), sks.end(), std::back_inserter(result), [](auto& key) -> std::unique_ptr<Private_Key> {
163
0
      BOTAN_ARG_CHECK(key != nullptr, "Private key list contains a nullptr");
164
0
      if(key->supports_operation(PublicKeyOperation::KeyAgreement) &&
165
0
         !key->supports_operation(PublicKeyOperation::KeyEncapsulation)) {
166
0
         auto ka_key = dynamic_cast<PK_Key_Agreement_Key*>(key.get());
167
0
         BOTAN_ASSERT_NONNULL(ka_key);
168
0
         (void)key.release();
169
0
         return std::make_unique<KEX_to_KEM_Adapter_PrivateKey>(std::unique_ptr<PK_Key_Agreement_Key>(ka_key));
170
0
      } else {
171
0
         return std::move(key);
172
0
      }
173
0
   });
174
0
   return result;
175
0
}
176
177
template <typename KEM_Operation>
178
void concat_secret_combiner(KEM_Operation& op,
179
                            std::span<uint8_t> out_shared_secret,
180
                            const std::vector<secure_vector<uint8_t>>& shared_secrets,
181
0
                            size_t desired_shared_key_len) {
182
0
   BOTAN_ARG_CHECK(out_shared_secret.size() == op.shared_key_length(desired_shared_key_len),
183
0
                   "Invalid output buffer size");
184
185
0
   BufferStuffer shared_secret_stuffer(out_shared_secret);
186
0
   for(size_t idx = 0; idx < shared_secrets.size(); idx++) {
187
0
      shared_secret_stuffer.append(shared_secrets.at(idx));
188
0
   }
189
0
   BOTAN_ASSERT_NOMSG(shared_secret_stuffer.full());
190
0
}
Unexecuted instantiation: hybrid_public_key.cpp:void Botan::TLS::(anonymous namespace)::concat_secret_combiner<Botan::TLS::(anonymous namespace)::Hybrid_TLS_KEM_Encryptor>(Botan::TLS::(anonymous namespace)::Hybrid_TLS_KEM_Encryptor&, std::__1::span<unsigned char, 18446744073709551615ul>, std::__1::vector<std::__1::vector<unsigned char, Botan::secure_allocator<unsigned char> >, std::__1::allocator<std::__1::vector<unsigned char, Botan::secure_allocator<unsigned char> > > > const&, unsigned long)
Unexecuted instantiation: hybrid_public_key.cpp:void Botan::TLS::(anonymous namespace)::concat_secret_combiner<Botan::TLS::(anonymous namespace)::Hybrid_TLS_KEM_Decryptor>(Botan::TLS::(anonymous namespace)::Hybrid_TLS_KEM_Decryptor&, std::__1::span<unsigned char, 18446744073709551615ul>, std::__1::vector<std::__1::vector<unsigned char, Botan::secure_allocator<unsigned char> >, std::__1::allocator<std::__1::vector<unsigned char, Botan::secure_allocator<unsigned char> > > > const&, unsigned long)
191
192
template <typename KEM_Operation>
193
0
size_t concat_shared_key_length(const std::vector<KEM_Operation>& operation) {
194
0
   return reduce(
195
0
      operation, size_t(0), [](size_t acc, const auto& op) { return acc + op.shared_key_length(0 /*no KDF*/); });
Unexecuted instantiation: hybrid_public_key.cpp:auto Botan::TLS::(anonymous namespace)::concat_shared_key_length<Botan::PK_KEM_Encryptor>(std::__1::vector<Botan::PK_KEM_Encryptor, std::__1::allocator<Botan::PK_KEM_Encryptor> > const&)::{lambda(unsigned long, auto:1 const&)#1}::operator()<Botan::PK_KEM_Encryptor>(unsigned long, Botan::PK_KEM_Encryptor const&) const
Unexecuted instantiation: hybrid_public_key.cpp:auto Botan::TLS::(anonymous namespace)::concat_shared_key_length<Botan::PK_KEM_Decryptor>(std::__1::vector<Botan::PK_KEM_Decryptor, std::__1::allocator<Botan::PK_KEM_Decryptor> > const&)::{lambda(unsigned long, auto:1 const&)#1}::operator()<Botan::PK_KEM_Decryptor>(unsigned long, Botan::PK_KEM_Decryptor const&) const
196
0
}
Unexecuted instantiation: hybrid_public_key.cpp:unsigned long Botan::TLS::(anonymous namespace)::concat_shared_key_length<Botan::PK_KEM_Encryptor>(std::__1::vector<Botan::PK_KEM_Encryptor, std::__1::allocator<Botan::PK_KEM_Encryptor> > const&)
Unexecuted instantiation: hybrid_public_key.cpp:unsigned long Botan::TLS::(anonymous namespace)::concat_shared_key_length<Botan::PK_KEM_Decryptor>(std::__1::vector<Botan::PK_KEM_Decryptor, std::__1::allocator<Botan::PK_KEM_Decryptor> > const&)
197
198
/// Encryptor that simply concatenates the multiple shared secrets
199
class Hybrid_TLS_KEM_Encryptor final : public KEM_Encryption_with_Combiner {
200
   public:
201
      Hybrid_TLS_KEM_Encryptor(const std::vector<std::unique_ptr<Public_Key>>& public_keys, std::string_view provider) :
202
0
            KEM_Encryption_with_Combiner(public_keys, provider) {}
203
204
      void combine_shared_secrets(std::span<uint8_t> out_shared_secret,
205
                                  const std::vector<secure_vector<uint8_t>>& shared_secrets,
206
                                  const std::vector<std::vector<uint8_t>>& /*ciphertexts*/,
207
                                  size_t desired_shared_key_len,
208
0
                                  std::span<const uint8_t> /*salt*/) override {
209
0
         concat_secret_combiner(*this, out_shared_secret, shared_secrets, desired_shared_key_len);
210
0
      }
211
212
0
      size_t shared_key_length(size_t /*desired_shared_key_len*/) const override {
213
0
         return concat_shared_key_length(encryptors());
214
0
      }
215
};
216
217
/// Decryptor that simply concatenates the multiple shared secrets
218
class Hybrid_TLS_KEM_Decryptor final : public KEM_Decryption_with_Combiner {
219
   public:
220
      Hybrid_TLS_KEM_Decryptor(const std::vector<std::unique_ptr<Private_Key>>& private_keys,
221
                               RandomNumberGenerator& rng,
222
                               const std::string_view provider) :
223
0
            KEM_Decryption_with_Combiner(private_keys, rng, provider) {}
224
225
      void combine_shared_secrets(std::span<uint8_t> out_shared_secret,
226
                                  const std::vector<secure_vector<uint8_t>>& shared_secrets,
227
                                  const std::vector<std::vector<uint8_t>>& /*ciphertexts*/,
228
                                  size_t desired_shared_key_len,
229
0
                                  std::span<const uint8_t> /*salt*/) override {
230
0
         concat_secret_combiner(*this, out_shared_secret, shared_secrets, desired_shared_key_len);
231
0
      }
232
233
0
      size_t shared_key_length(size_t /*desired_shared_key_len*/) const override {
234
0
         return concat_shared_key_length(decryptors());
235
0
      }
236
};
237
238
}  // namespace
239
240
std::unique_ptr<Hybrid_KEM_PublicKey> Hybrid_KEM_PublicKey::load_for_group(
241
0
   Group_Params group, std::span<const uint8_t> concatenated_public_keys) {
242
0
   const auto public_key_lengths = public_key_lengths_for_group(group);
243
0
   auto alg_ids = algorithm_identifiers_for_group(group);
244
0
   BOTAN_ASSERT_NOMSG(public_key_lengths.size() == alg_ids.size());
245
246
0
   const auto expected_public_keys_length =
247
0
      reduce(public_key_lengths, size_t(0), [](size_t acc, size_t len) { return acc + len; });
248
0
   if(expected_public_keys_length != concatenated_public_keys.size()) {
249
0
      throw Decoding_Error("Concatenated public values have an unexpected length");
250
0
   }
251
252
0
   BufferSlicer public_key_slicer(concatenated_public_keys);
253
0
   std::vector<std::unique_ptr<Public_Key>> pks;
254
0
   pks.reserve(alg_ids.size());
255
0
   for(size_t idx = 0; idx < alg_ids.size(); ++idx) {
256
0
      pks.emplace_back(load_public_key(alg_ids[idx], public_key_slicer.take(public_key_lengths[idx])));
257
0
   }
258
0
   BOTAN_ASSERT_NOMSG(public_key_slicer.empty());
259
0
   return std::make_unique<Hybrid_KEM_PublicKey>(std::move(pks));
260
0
}
261
262
Hybrid_KEM_PublicKey::Hybrid_KEM_PublicKey(std::vector<std::unique_ptr<Public_Key>> pks) :
263
0
      Hybrid_PublicKey(convert_kex_to_kem_pks(std::move(pks))) {}
Unexecuted instantiation: Botan::TLS::Hybrid_KEM_PublicKey::Hybrid_KEM_PublicKey(std::__1::vector<std::__1::unique_ptr<Botan::Public_Key, std::__1::default_delete<Botan::Public_Key> >, std::__1::allocator<std::__1::unique_ptr<Botan::Public_Key, std::__1::default_delete<Botan::Public_Key> > > >)
Unexecuted instantiation: Botan::TLS::Hybrid_KEM_PublicKey::Hybrid_KEM_PublicKey(std::__1::vector<std::__1::unique_ptr<Botan::Public_Key, std::__1::default_delete<Botan::Public_Key> >, std::__1::allocator<std::__1::unique_ptr<Botan::Public_Key, std::__1::default_delete<Botan::Public_Key> > > >)
264
265
Hybrid_KEM_PrivateKey::Hybrid_KEM_PrivateKey(std::vector<std::unique_ptr<Private_Key>> sks) :
266
0
      Hybrid_PublicKey(convert_kex_to_kem_pks(extract_public_keys(sks))),
267
0
      Hybrid_PrivateKey(convert_kex_to_kem_sks(std::move(sks))) {}
Unexecuted instantiation: Botan::TLS::Hybrid_KEM_PrivateKey::Hybrid_KEM_PrivateKey(std::__1::vector<std::__1::unique_ptr<Botan::Private_Key, std::__1::default_delete<Botan::Private_Key> >, std::__1::allocator<std::__1::unique_ptr<Botan::Private_Key, std::__1::default_delete<Botan::Private_Key> > > >)
Unexecuted instantiation: Botan::TLS::Hybrid_KEM_PrivateKey::Hybrid_KEM_PrivateKey(std::__1::vector<std::__1::unique_ptr<Botan::Private_Key, std::__1::default_delete<Botan::Private_Key> >, std::__1::allocator<std::__1::unique_ptr<Botan::Private_Key, std::__1::default_delete<Botan::Private_Key> > > >)
268
269
0
std::string Hybrid_KEM_PublicKey::algo_name() const {
270
0
   std::ostringstream algo_name("Hybrid(");
271
0
   for(size_t i = 0; i < public_keys().size(); ++i) {
272
0
      if(i > 0) {
273
0
         algo_name << ",";
274
0
      }
275
0
      algo_name << public_keys().at(i)->algo_name();
276
0
   }
277
0
   algo_name << ")";
278
0
   return algo_name.str();
279
0
}
280
281
0
AlgorithmIdentifier Hybrid_KEM_PublicKey::algorithm_identifier() const {
282
0
   throw Botan::Not_Implemented("Hybrid keys don't have an algorithm identifier");
283
0
}
284
285
0
std::vector<uint8_t> Hybrid_KEM_PublicKey::public_key_bits() const {
286
0
   return raw_public_key_bits();
287
0
}
288
289
0
std::vector<uint8_t> Hybrid_KEM_PublicKey::raw_public_key_bits() const {
290
   // draft-ietf-tls-hybrid-design-06 3.2
291
   //   The values are directly concatenated, without any additional encoding
292
   //   or length fields; this assumes that the representation and length of
293
   //   elements is fixed once the algorithm is fixed.  If concatenation were
294
   //   to be used with values that are not fixed-length, a length prefix or
295
   //   other unambiguous encoding must be used to ensure that the composition
296
   //   of the two values is injective.
297
0
   return reduce(public_keys(), std::vector<uint8_t>(), [](auto pkb, const auto& key) {
298
0
      return concat(pkb, key->raw_public_key_bits());
299
0
   });
300
0
}
301
302
0
std::unique_ptr<Private_Key> Hybrid_KEM_PublicKey::generate_another(RandomNumberGenerator& rng) const {
303
0
   return std::make_unique<Hybrid_KEM_PrivateKey>(generate_other_sks_from_pks(rng));
304
0
}
305
306
std::unique_ptr<Botan::PK_Ops::KEM_Encryption> Hybrid_KEM_PublicKey::create_kem_encryption_op(
307
0
   std::string_view params, std::string_view provider) const {
308
0
   if(params != "Raw" && !params.empty()) {
309
0
      throw Botan::Invalid_Argument("Hybrid KEM encryption does not support KDFs");
310
0
   }
311
0
   return std::make_unique<Hybrid_TLS_KEM_Encryptor>(public_keys(), provider);
312
0
}
313
314
std::unique_ptr<Hybrid_KEM_PrivateKey> Hybrid_KEM_PrivateKey::generate_from_group(Group_Params group,
315
0
                                                                                  RandomNumberGenerator& rng) {
316
0
   const auto algo_spec = algorithm_specs_for_group(group);
317
0
   std::vector<std::unique_ptr<Private_Key>> private_keys;
318
0
   private_keys.reserve(algo_spec.size());
319
0
   for(const auto& spec : algo_spec) {
320
0
      private_keys.push_back(create_private_key(spec.first, rng, spec.second));
321
0
   }
322
0
   return std::make_unique<Hybrid_KEM_PrivateKey>(std::move(private_keys));
323
0
}
324
325
std::unique_ptr<Botan::PK_Ops::KEM_Decryption> Hybrid_KEM_PrivateKey::create_kem_decryption_op(
326
0
   RandomNumberGenerator& rng, std::string_view params, std::string_view provider) const {
327
0
   if(params != "Raw" && !params.empty()) {
328
0
      throw Botan::Invalid_Argument("Hybrid KEM decryption does not support KDFs");
329
0
   }
330
0
   return std::make_unique<Hybrid_TLS_KEM_Decryptor>(private_keys(), rng, provider);
331
0
}
332
333
}  // namespace Botan::TLS