Botan 3.6.0
Crypto and TLS for C&
hybrid_public_key.cpp
Go to the documentation of this file.
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/pk_algs.h>
14
15#include <botan/internal/fmt.h>
16#include <botan/internal/kex_to_kem_adapter.h>
17#include <botan/internal/pk_ops_impl.h>
18#include <botan/internal/stl_util.h>
19
20namespace Botan::TLS {
21
22namespace {
23
24std::vector<std::pair<std::string, std::string>> algorithm_specs_for_group(Group_Params group) {
25 BOTAN_ARG_CHECK(group.is_pqc_hybrid(), "Group is not hybrid");
26
27 switch(group.code()) {
28 // draft-kwiatkowski-tls-ecdhe-mlkem-02 Section 3
29 //
30 // NIST's special publication 800-56Cr2 approves the usage of HKDF with
31 // two distinct shared secrets, with the condition that the first one
32 // is computed by a FIPS-approved key-establishment scheme. FIPS also
33 // requires a certified implementation of the scheme, which will remain
34 // more ubiqutous for secp256r1 in the coming years.
35 //
36 // For this reason we put the ML-KEM-768 shared secret first in
37 // X25519MLKEM768, and the secp256r1 shared secret first in
38 // SecP256r1MLKEM768.
39 case Group_Params::HYBRID_X25519_ML_KEM_768:
40 return {{"ML-KEM", "ML-KEM-768"}, {"X25519", "X25519"}};
41 case Group_Params::HYBRID_SECP256R1_ML_KEM_768:
42 return {{"ECDH", "secp256r1"}, {"ML-KEM", "ML-KEM-768"}};
43
44 case Group_Params::HYBRID_X25519_KYBER_512_R3_OQS:
45 case Group_Params::HYBRID_X25519_KYBER_512_R3_CLOUDFLARE:
46 return {{"X25519", "X25519"}, {"Kyber", "Kyber-512-r3"}};
47 case Group_Params::HYBRID_X25519_KYBER_768_R3_OQS:
48 return {{"X25519", "X25519"}, {"Kyber", "Kyber-768-r3"}};
49 case Group_Params::HYBRID_X448_KYBER_768_R3_OQS:
50 return {{"X448", "X448"}, {"Kyber", "Kyber-768-r3"}};
51 case Group_Params::HYBRID_X25519_eFRODOKEM_640_SHAKE_OQS:
52 return {{"X25519", "X25519"}, {"FrodoKEM", "eFrodoKEM-640-SHAKE"}};
53 case Group_Params::HYBRID_X25519_eFRODOKEM_640_AES_OQS:
54 return {{"X25519", "X25519"}, {"FrodoKEM", "eFrodoKEM-640-AES"}};
55 case Group_Params::HYBRID_X448_eFRODOKEM_976_SHAKE_OQS:
56 return {{"X448", "X448"}, {"FrodoKEM", "eFrodoKEM-976-SHAKE"}};
57 case Group_Params::HYBRID_X448_eFRODOKEM_976_AES_OQS:
58 return {{"X448", "X448"}, {"FrodoKEM", "eFrodoKEM-976-AES"}};
59
60 case Group_Params::HYBRID_SECP256R1_KYBER_512_R3_OQS:
61 return {{"ECDH", "secp256r1"}, {"Kyber", "Kyber-512-r3"}};
62 case Group_Params::HYBRID_SECP256R1_KYBER_768_R3_OQS:
63 return {{"ECDH", "secp256r1"}, {"Kyber", "Kyber-768-r3"}};
64 case Group_Params::HYBRID_SECP256R1_eFRODOKEM_640_SHAKE_OQS:
65 return {{"ECDH", "secp256r1"}, {"FrodoKEM", "eFrodoKEM-640-SHAKE"}};
66 case Group_Params::HYBRID_SECP256R1_eFRODOKEM_640_AES_OQS:
67 return {{"ECDH", "secp256r1"}, {"FrodoKEM", "eFrodoKEM-640-AES"}};
68
69 case Group_Params::HYBRID_SECP384R1_KYBER_768_R3_OQS:
70 return {{"ECDH", "secp384r1"}, {"Kyber", "Kyber-768-r3"}};
71 case Group_Params::HYBRID_SECP384R1_eFRODOKEM_976_SHAKE_OQS:
72 return {{"ECDH", "secp384r1"}, {"FrodoKEM", "eFrodoKEM-976-SHAKE"}};
73 case Group_Params::HYBRID_SECP384R1_eFRODOKEM_976_AES_OQS:
74 return {{"ECDH", "secp384r1"}, {"FrodoKEM", "eFrodoKEM-976-AES"}};
75
76 case Group_Params::HYBRID_SECP521R1_KYBER_1024_R3_OQS:
77 return {{"ECDH", "secp521r1"}, {"Kyber", "Kyber-1024-r3"}};
78 case Group_Params::HYBRID_SECP521R1_eFRODOKEM_1344_SHAKE_OQS:
79 return {{"ECDH", "secp521r1"}, {"FrodoKEM", "eFrodoKEM-1344-SHAKE"}};
80 case Group_Params::HYBRID_SECP521R1_eFRODOKEM_1344_AES_OQS:
81 return {{"ECDH", "secp521r1"}, {"FrodoKEM", "eFrodoKEM-1344-AES"}};
82
83 default:
84 return {};
85 }
86}
87
88std::vector<AlgorithmIdentifier> algorithm_identifiers_for_group(Group_Params group) {
89 BOTAN_ASSERT_NOMSG(group.is_pqc_hybrid());
90
91 const auto specs = algorithm_specs_for_group(group);
92 std::vector<AlgorithmIdentifier> result;
93 result.reserve(specs.size());
94
95 // This maps the string-based algorithm specs hard-coded above to OID-based
96 // AlgorithmIdentifier objects. The mapping is needed because
97 // load_public_key() depends on those while create_private_key() requires the
98 // strong-based spec.
99 //
100 // TODO: This is inconvenient, confusing and error-prone. Find a better way
101 // to load arbitrary public keys.
102 for(const auto& spec : specs) {
103 result.push_back(AlgorithmIdentifier(spec.second, AlgorithmIdentifier::USE_EMPTY_PARAM));
104 }
105
106 return result;
107}
108
109std::vector<size_t> public_value_lengths_for_group(Group_Params group) {
110 BOTAN_ASSERT_NOMSG(group.is_pqc_hybrid());
111
112 // This duplicates information of the algorithm internals.
113 //
114 // TODO: Find a way to expose important algorithm constants globally
115 // in the library, to avoid violating the DRY principle.
116 switch(group.code()) {
117 case Group_Params::HYBRID_X25519_ML_KEM_768:
118 return {1184, 32};
119 case Group_Params::HYBRID_SECP256R1_ML_KEM_768:
120 return {32, 1184};
121
122 case Group_Params::HYBRID_X25519_KYBER_512_R3_CLOUDFLARE:
123 case Group_Params::HYBRID_X25519_KYBER_512_R3_OQS:
124 return {32, 800};
125 case Group_Params::HYBRID_X25519_KYBER_768_R3_OQS:
126 return {32, 1184};
127 case Group_Params::HYBRID_X448_KYBER_768_R3_OQS:
128 return {56, 1184};
129 case Group_Params::HYBRID_X25519_eFRODOKEM_640_SHAKE_OQS:
130 return {32, 9616};
131 case Group_Params::HYBRID_X25519_eFRODOKEM_640_AES_OQS:
132 return {32, 9616};
133 case Group_Params::HYBRID_X448_eFRODOKEM_976_SHAKE_OQS:
134 return {56, 15632};
135 case Group_Params::HYBRID_X448_eFRODOKEM_976_AES_OQS:
136 return {56, 15632};
137
138 case Group_Params::HYBRID_SECP256R1_KYBER_512_R3_OQS:
139 return {32, 800};
140 case Group_Params::HYBRID_SECP256R1_KYBER_768_R3_OQS:
141 return {32, 1184};
142 case Group_Params::HYBRID_SECP256R1_eFRODOKEM_640_SHAKE_OQS:
143 return {32, 9616};
144 case Group_Params::HYBRID_SECP256R1_eFRODOKEM_640_AES_OQS:
145 return {32, 9616};
146
147 case Group_Params::HYBRID_SECP384R1_KYBER_768_R3_OQS:
148 return {48, 1184};
149 case Group_Params::HYBRID_SECP384R1_eFRODOKEM_976_SHAKE_OQS:
150 return {48, 15632};
151 case Group_Params::HYBRID_SECP384R1_eFRODOKEM_976_AES_OQS:
152 return {48, 15632};
153
154 case Group_Params::HYBRID_SECP521R1_eFRODOKEM_1344_SHAKE_OQS:
155 return {66, 21520};
156 case Group_Params::HYBRID_SECP521R1_eFRODOKEM_1344_AES_OQS:
157 return {66, 21520};
158 case Group_Params::HYBRID_SECP521R1_KYBER_1024_R3_OQS:
159 return {66, 1568};
160
161 default:
162 return {};
163 }
164}
165
166} // namespace
167
168std::unique_ptr<Hybrid_KEM_PublicKey> Hybrid_KEM_PublicKey::load_for_group(
169 Group_Params group, std::span<const uint8_t> concatenated_public_values) {
170 const auto public_value_lengths = public_value_lengths_for_group(group);
171 auto alg_ids = algorithm_identifiers_for_group(group);
172 BOTAN_ASSERT_NOMSG(public_value_lengths.size() == alg_ids.size());
173
174 const auto expected_public_values_length =
175 reduce(public_value_lengths, size_t(0), [](size_t acc, size_t len) { return acc + len; });
176 if(expected_public_values_length != concatenated_public_values.size()) {
177 throw Decoding_Error("Concatenated public values have an unexpected length");
178 }
179
180 BufferSlicer public_value_slicer(concatenated_public_values);
181 std::vector<std::unique_ptr<Public_Key>> pks;
182 for(size_t idx = 0; idx < alg_ids.size(); ++idx) {
183 pks.emplace_back(load_public_key(alg_ids[idx], public_value_slicer.take(public_value_lengths[idx])));
184 }
185 BOTAN_ASSERT_NOMSG(public_value_slicer.empty());
186 return std::make_unique<Hybrid_KEM_PublicKey>(std::move(pks));
187}
188
189Hybrid_KEM_PublicKey::Hybrid_KEM_PublicKey(std::vector<std::unique_ptr<Public_Key>> pks) {
190 BOTAN_ARG_CHECK(pks.size() >= 2, "List of public keys must include at least two keys");
191 BOTAN_ARG_CHECK(std::all_of(pks.begin(), pks.end(), [](const auto& pk) { return pk != nullptr; }),
192 "List of public keys contains a nullptr");
193 BOTAN_ARG_CHECK(std::all_of(pks.begin(),
194 pks.end(),
195 [](const auto& pk) {
196 return pk->supports_operation(PublicKeyOperation::KeyEncapsulation) ||
197 pk->supports_operation(PublicKeyOperation::KeyAgreement);
198 }),
199 "Some provided public key is not compatible with this hybrid wrapper");
200
201 std::transform(
202 pks.begin(), pks.end(), std::back_inserter(m_public_keys), [](auto& key) -> std::unique_ptr<Public_Key> {
203 if(key->supports_operation(PublicKeyOperation::KeyAgreement) &&
204 !key->supports_operation(PublicKeyOperation::KeyEncapsulation)) {
205 return std::make_unique<KEX_to_KEM_Adapter_PublicKey>(std::move(key));
206 } else {
207 return std::move(key);
208 }
209 });
210
211 m_key_length =
212 reduce(m_public_keys, size_t(0), [](size_t kl, const auto& key) { return std::max(kl, key->key_length()); });
213 m_estimated_strength = reduce(
214 m_public_keys, size_t(0), [](size_t es, const auto& key) { return std::max(es, key->estimated_strength()); });
215}
216
217std::string Hybrid_KEM_PublicKey::algo_name() const {
218 std::ostringstream algo_name("Hybrid(");
219 for(size_t i = 0; i < m_public_keys.size(); ++i) {
220 if(i > 0) {
221 algo_name << ",";
222 }
223 algo_name << m_public_keys[i]->algo_name();
224 }
225 algo_name << ")";
226 return algo_name.str();
227}
228
229size_t Hybrid_KEM_PublicKey::estimated_strength() const {
230 return m_estimated_strength;
231}
232
233size_t Hybrid_KEM_PublicKey::key_length() const {
234 return m_key_length;
235}
236
237bool Hybrid_KEM_PublicKey::check_key(RandomNumberGenerator& rng, bool strong) const {
238 return reduce(m_public_keys, true, [&](bool ckr, const auto& key) { return ckr && key->check_key(rng, strong); });
239}
240
241AlgorithmIdentifier Hybrid_KEM_PublicKey::algorithm_identifier() const {
242 throw Botan::Not_Implemented("Hybrid keys don't have an algorithm identifier");
243}
244
245std::vector<uint8_t> Hybrid_KEM_PublicKey::public_key_bits() const {
246 return raw_public_key_bits();
247}
248
249std::vector<uint8_t> Hybrid_KEM_PublicKey::raw_public_key_bits() const {
250 // draft-ietf-tls-hybrid-design-06 3.2
251 // The values are directly concatenated, without any additional encoding
252 // or length fields; this assumes that the representation and length of
253 // elements is fixed once the algorithm is fixed. If concatenation were
254 // to be used with values that are not fixed-length, a length prefix or
255 // other unambiguous encoding must be used to ensure that the composition
256 // of the two values is injective.
257 return reduce(m_public_keys, std::vector<uint8_t>(), [](auto pkb, const auto& key) {
258 return concat(pkb, key->raw_public_key_bits());
259 });
260}
261
262std::unique_ptr<Private_Key> Hybrid_KEM_PublicKey::generate_another(RandomNumberGenerator& rng) const {
263 std::vector<std::unique_ptr<Private_Key>> new_private_keys;
264 std::transform(
265 m_public_keys.begin(), m_public_keys.end(), std::back_inserter(new_private_keys), [&](const auto& public_key) {
266 return public_key->generate_another(rng);
267 });
268 return std::make_unique<Hybrid_KEM_PrivateKey>(std::move(new_private_keys));
269}
270
271bool Hybrid_KEM_PublicKey::supports_operation(PublicKeyOperation op) const {
273}
274
275namespace {
276
277class Hybrid_KEM_Encryption_Operation final : public PK_Ops::KEM_Encryption_with_KDF {
278 public:
279 Hybrid_KEM_Encryption_Operation(const Hybrid_KEM_PublicKey& key,
280 std::string_view kdf,
281 std::string_view provider) :
282 PK_Ops::KEM_Encryption_with_KDF(kdf), m_raw_kem_shared_key_length(0), m_encapsulated_key_length(0) {
283 m_kem_encryptors.reserve(key.public_keys().size());
284 for(const auto& k : key.public_keys()) {
285 const auto& newenc = m_kem_encryptors.emplace_back(*k, "Raw", provider);
286 m_raw_kem_shared_key_length += newenc.shared_key_length(0 /* no KDF */);
287 m_encapsulated_key_length += newenc.encapsulated_key_length();
288 }
289 }
290
291 size_t raw_kem_shared_key_length() const override { return m_raw_kem_shared_key_length; }
292
293 size_t encapsulated_key_length() const override { return m_encapsulated_key_length; }
294
295 void raw_kem_encrypt(std::span<uint8_t> out_encapsulated_key,
296 std::span<uint8_t> raw_shared_key,
297 Botan::RandomNumberGenerator& rng) override {
298 BOTAN_ASSERT_NOMSG(out_encapsulated_key.size() == encapsulated_key_length());
299 BOTAN_ASSERT_NOMSG(raw_shared_key.size() == raw_kem_shared_key_length());
300
301 BufferStuffer encaps_key_stuffer(out_encapsulated_key);
302 BufferStuffer shared_key_stuffer(raw_shared_key);
303
304 for(auto& kem_enc : m_kem_encryptors) {
305 kem_enc.encrypt(encaps_key_stuffer.next(kem_enc.encapsulated_key_length()),
306 shared_key_stuffer.next(kem_enc.shared_key_length(0 /* no KDF */)),
307 rng);
308 }
309 }
310
311 private:
312 std::vector<PK_KEM_Encryptor> m_kem_encryptors;
313 size_t m_raw_kem_shared_key_length;
314 size_t m_encapsulated_key_length;
315};
316
317} // namespace
318
319std::unique_ptr<Botan::PK_Ops::KEM_Encryption> Hybrid_KEM_PublicKey::create_kem_encryption_op(
320 std::string_view kdf, std::string_view provider) const {
321 return std::make_unique<Hybrid_KEM_Encryption_Operation>(*this, kdf, provider);
322}
323
324namespace {
325
326auto extract_public_keys(const std::vector<std::unique_ptr<Private_Key>>& private_keys) {
327 std::vector<std::unique_ptr<Public_Key>> public_keys;
328 public_keys.reserve(private_keys.size());
329 for(const auto& private_key : private_keys) {
330 BOTAN_ARG_CHECK(private_key != nullptr, "List of private keys contains a nullptr");
331 public_keys.push_back(private_key->public_key());
332 }
333 return public_keys;
334}
335
336} // namespace
337
338std::unique_ptr<Hybrid_KEM_PrivateKey> Hybrid_KEM_PrivateKey::generate_from_group(Group_Params group,
340 const auto algo_spec = algorithm_specs_for_group(group);
341 std::vector<std::unique_ptr<Private_Key>> private_keys;
342 private_keys.reserve(algo_spec.size());
343 for(const auto& spec : algo_spec) {
344 private_keys.push_back(create_private_key(spec.first, rng, spec.second));
345 }
346 return std::make_unique<Hybrid_KEM_PrivateKey>(std::move(private_keys));
347}
348
349Hybrid_KEM_PrivateKey::Hybrid_KEM_PrivateKey(std::vector<std::unique_ptr<Private_Key>> sks) :
350 Hybrid_KEM_PublicKey(extract_public_keys(sks)) {
351 BOTAN_ARG_CHECK(sks.size() >= 2, "List of private keys must include at least two keys");
352 BOTAN_ARG_CHECK(std::all_of(sks.begin(),
353 sks.end(),
354 [](const auto& sk) {
355 return sk->supports_operation(PublicKeyOperation::KeyEncapsulation) ||
356 sk->supports_operation(PublicKeyOperation::KeyAgreement);
357 }),
358 "Some provided private key is not compatible with this hybrid wrapper");
359
360 std::transform(
361 sks.begin(), sks.end(), std::back_inserter(m_private_keys), [](auto& key) -> std::unique_ptr<Private_Key> {
362 if(key->supports_operation(PublicKeyOperation::KeyAgreement) &&
363 !key->supports_operation(PublicKeyOperation::KeyEncapsulation)) {
364 auto ka_key = dynamic_cast<PK_Key_Agreement_Key*>(key.get());
365 BOTAN_ASSERT_NONNULL(ka_key);
366 (void)key.release();
367 return std::make_unique<KEX_to_KEM_Adapter_PrivateKey>(std::unique_ptr<PK_Key_Agreement_Key>(ka_key));
368 } else {
369 return std::move(key);
370 }
371 });
372}
373
375 throw Not_Implemented("Hybrid private keys cannot be serialized");
376}
377
378std::unique_ptr<Public_Key> Hybrid_KEM_PrivateKey::public_key() const {
379 return std::make_unique<Hybrid_KEM_PublicKey>(extract_public_keys(m_private_keys));
380}
381
383 return reduce(m_public_keys, true, [&](bool ckr, const auto& key) { return ckr && key->check_key(rng, strong); });
384}
385
386namespace {
387
388class Hybrid_KEM_Decryption final : public PK_Ops::KEM_Decryption_with_KDF {
389 public:
390 Hybrid_KEM_Decryption(const Hybrid_KEM_PrivateKey& key,
392 const std::string_view kdf,
393 const std::string_view provider) :
394 PK_Ops::KEM_Decryption_with_KDF(kdf), m_encapsulated_key_length(0), m_raw_kem_shared_key_length(0) {
395 m_decryptors.reserve(key.private_keys().size());
396 for(const auto& private_key : key.private_keys()) {
397 const auto& newdec = m_decryptors.emplace_back(*private_key, rng, "Raw", provider);
398 m_encapsulated_key_length += newdec.encapsulated_key_length();
399 m_raw_kem_shared_key_length += newdec.shared_key_length(0 /* no KDF */);
400 }
401 }
402
403 void raw_kem_decrypt(std::span<uint8_t> out_shared_key, std::span<const uint8_t> encap_key) override {
404 BOTAN_ASSERT_NOMSG(out_shared_key.size() == raw_kem_shared_key_length());
405 BOTAN_ASSERT_NOMSG(encap_key.size() == encapsulated_key_length());
406
407 BufferSlicer encap_key_slicer(encap_key);
408 BufferStuffer shared_secret_stuffer(out_shared_key);
409
410 for(auto& decryptor : m_decryptors) {
411 decryptor.decrypt(shared_secret_stuffer.next(decryptor.shared_key_length(0 /* no KDF */)),
412 encap_key_slicer.take(decryptor.encapsulated_key_length()));
413 }
414 }
415
416 size_t encapsulated_key_length() const override { return m_encapsulated_key_length; }
417
418 size_t raw_kem_shared_key_length() const override { return m_raw_kem_shared_key_length; }
419
420 private:
421 std::vector<PK_KEM_Decryptor> m_decryptors;
422 size_t m_encapsulated_key_length;
423 size_t m_raw_kem_shared_key_length;
424};
425
426} // namespace
427
428std::unique_ptr<Botan::PK_Ops::KEM_Decryption> Hybrid_KEM_PrivateKey::create_kem_decryption_op(
429 RandomNumberGenerator& rng, std::string_view kdf, std::string_view provider) const {
430 return std::make_unique<Hybrid_KEM_Decryption>(*this, rng, kdf, provider);
431}
432
433} // namespace Botan::TLS
#define BOTAN_ASSERT_NOMSG(expr)
Definition assert.h:59
#define BOTAN_ARG_CHECK(expr, msg)
Definition assert.h:29
bool empty() const
Definition stl_util.h:129
std::span< const uint8_t > take(const size_t count)
Definition stl_util.h:98
Helper class to ease in-place marshalling of concatenated fixed-length values.
Definition stl_util.h:142
bool check_key(RandomNumberGenerator &rng, bool strong) const override
std::unique_ptr< Public_Key > public_key() const override
std::unique_ptr< PK_Ops::KEM_Decryption > create_kem_decryption_op(RandomNumberGenerator &rng, std::string_view kdf, std::string_view provider="base") const override
secure_vector< uint8_t > private_key_bits() const override
std::vector< std::unique_ptr< Public_Key > > m_public_keys
Hybrid_KEM_PublicKey(std::vector< std::unique_ptr< Public_Key > > pks)
bool check_key(RandomNumberGenerator &rng, bool strong) const override
static std::unique_ptr< Hybrid_KEM_PublicKey > load_for_group(Group_Params group, std::span< const uint8_t > concatenated_public_values)
int(* final)(unsigned char *, CTX *)
std::unique_ptr< Private_Key > create_private_key(std::string_view alg_name, RandomNumberGenerator &rng, std::string_view params, std::string_view provider)
Definition pk_algs.cpp:470
PublicKeyOperation
Definition pk_keys.h:45
RetT reduce(const std::vector< KeyT > &keys, RetT acc, ReducerT reducer)
Definition stl_util.h:47
constexpr auto concat(Rs &&... ranges)
Definition stl_util.h:263
std::vector< T, secure_allocator< T > > secure_vector
Definition secmem.h:61
std::unique_ptr< Public_Key > load_public_key(const AlgorithmIdentifier &alg_id, std::span< const uint8_t > key_bits)
Definition pk_algs.cpp:119