diff --git a/src/lib/ffi/ffi.h b/src/lib/ffi/ffi.h index 6f1c8cd33b7..df469d5c36f 100644 --- a/src/lib/ffi/ffi.h +++ b/src/lib/ffi/ffi.h @@ -1534,6 +1534,16 @@ int botan_privkey_view_kyber_raw_key(botan_privkey_t key, botan_view_ctx ctx, bo BOTAN_FFI_EXPORT(3, 1) int botan_pubkey_view_kyber_raw_key(botan_pubkey_t key, botan_view_ctx ctx, botan_view_bin_fn view); +/** +* Algorithm specific key operation: FrodoKEM +*/ + +BOTAN_FFI_EXPORT(3, 6) +int botan_privkey_load_frodokem(botan_privkey_t* key, const uint8_t privkey[], size_t key_len, const char* frodo_mode); + +BOTAN_FFI_EXPORT(3, 6) +int botan_pubkey_load_frodokem(botan_pubkey_t* key, const uint8_t pubkey[], size_t key_len, const char* frodo_mode); + /* * Algorithm specific key operations: ECDSA and ECDH */ diff --git a/src/lib/ffi/ffi_pk_op.cpp b/src/lib/ffi/ffi_pk_op.cpp index e6061a6ee5a..f08d9ec5698 100644 --- a/src/lib/ffi/ffi_pk_op.cpp +++ b/src/lib/ffi/ffi_pk_op.cpp @@ -343,7 +343,7 @@ int botan_pk_op_kem_decrypt_shared_key(botan_pk_op_kem_decrypt_t op, const auto shared_key = kem.decrypt(encapsulated_key, encapsulated_key_len, desired_shared_key_len, salt, salt_len); - write_vec_output(shared_key_out, shared_key_len, shared_key); + return write_vec_output(shared_key_out, shared_key_len, shared_key); }); } diff --git a/src/lib/ffi/ffi_pkey_algs.cpp b/src/lib/ffi/ffi_pkey_algs.cpp index 54a3a299049..9e1b1bfb2e0 100644 --- a/src/lib/ffi/ffi_pkey_algs.cpp +++ b/src/lib/ffi/ffi_pkey_algs.cpp @@ -75,6 +75,10 @@ #include #endif +#if defined(BOTAN_HAS_FRODOKEM) + #include +#endif + namespace { #if defined(BOTAN_HAS_ECC_PUBLIC_KEY_CRYPTO) @@ -1027,6 +1031,50 @@ int botan_pubkey_view_kyber_raw_key(botan_pubkey_t key, botan_view_ctx ctx, bota #endif } +/* +* Algorithm specific key operations: FrodoKEM +*/ + +int botan_privkey_load_frodokem(botan_privkey_t* key, const uint8_t privkey[], size_t key_len, const char* frodo_mode) { +#if defined(BOTAN_HAS_FRODOKEM) + if(key == nullptr || privkey == nullptr || frodo_mode == nullptr) { + return BOTAN_FFI_ERROR_NULL_POINTER; + } + + *key = nullptr; + + return ffi_guard_thunk(__func__, [=]() -> int { + const auto mode = Botan::FrodoKEMMode(frodo_mode); + auto frodo_key = std::make_unique(std::span{privkey, key_len}, mode); + *key = new botan_privkey_struct(std::move(frodo_key)); + return BOTAN_FFI_SUCCESS; + }); +#else + BOTAN_UNUSED(key, privkey, key_len, frodo_mode); + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; +#endif +} + +int botan_pubkey_load_frodokem(botan_pubkey_t* key, const uint8_t pubkey[], size_t key_len, const char* frodo_mode) { +#if defined(BOTAN_HAS_FRODOKEM) + if(key == nullptr || pubkey == nullptr || frodo_mode == nullptr) { + return BOTAN_FFI_ERROR_NULL_POINTER; + } + + *key = nullptr; + + return ffi_guard_thunk(__func__, [=]() -> int { + const auto mode = Botan::FrodoKEMMode(frodo_mode); + auto frodo_key = std::make_unique(std::span{pubkey, key_len}, mode); + *key = new botan_pubkey_struct(std::move(frodo_key)); + return BOTAN_FFI_SUCCESS; + }); +#else + BOTAN_UNUSED(key, pubkey, key_len, frodo_mode); + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; +#endif +} + int botan_pubkey_view_ec_public_point(const botan_pubkey_t key, botan_view_ctx ctx, botan_view_bin_fn view) { #if defined(BOTAN_HAS_ECC_PUBLIC_KEY_CRYPTO) return BOTAN_FFI_VISIT(key, [=](const auto& k) -> int { diff --git a/src/lib/pubkey/frodokem/frodokem_common/frodo_constants.cpp b/src/lib/pubkey/frodokem/frodokem_common/frodo_constants.cpp index 08ca2de6102..bda5ee73363 100644 --- a/src/lib/pubkey/frodokem/frodokem_common/frodo_constants.cpp +++ b/src/lib/pubkey/frodokem/frodokem_common/frodo_constants.cpp @@ -15,7 +15,9 @@ namespace Botan { FrodoKEMConstants::FrodoKEMConstants(FrodoKEMMode mode) : m_mode(mode), m_len_a(128), m_n_bar(8) { - BOTAN_ASSERT(m_mode.is_available(), "Mode is not available."); + if(!mode.is_available()) { + throw Not_Implemented("FrodoKEM mode " + mode.to_string() + " is not available"); + } if(mode.is_ephemeral()) { m_len_salt = 0; diff --git a/src/python/botan3.py b/src/python/botan3.py index 94b99124b89..d4d9a540ce7 100755 --- a/src/python/botan3.py +++ b/src/python/botan3.py @@ -363,6 +363,8 @@ def ffi_api(fn, args, allowed_errors=None): ffi_api(dll.botan_pubkey_load_kyber, [c_void_p, c_char_p, c_int]) ffi_api(dll.botan_privkey_view_kyber_raw_key, [c_void_p, c_void_p, VIEW_BIN_CALLBACK]) ffi_api(dll.botan_pubkey_view_kyber_raw_key, [c_void_p, c_void_p, VIEW_BIN_CALLBACK]) + ffi_api(dll.botan_privkey_load_frodokem, [c_void_p, c_void_p, c_int, c_char_p]) + ffi_api(dll.botan_pubkey_load_frodokem, [c_void_p, c_void_p, c_int, c_char_p]) ffi_api(dll.botan_privkey_load_ecdsa, [c_void_p, c_void_p, c_char_p]) ffi_api(dll.botan_pubkey_load_ecdsa, [c_void_p, c_void_p, c_void_p, c_char_p]) ffi_api(dll.botan_pubkey_load_ecdh, [c_void_p, c_void_p, c_void_p, c_char_p]) @@ -1232,6 +1234,12 @@ def load_kyber(cls, key): _DLL.botan_pubkey_load_kyber(byref(obj), key, len(key)) return PublicKey(obj) + @classmethod + def load_frodokem(cls, frodo_mode, key): + obj = c_void_p(0) + _DLL.botan_pubkey_load_frodokem(byref(obj), key, len(key), _ctype_str(frodo_mode)) + return PublicKey(obj) + def __del__(self): _DLL.botan_pubkey_destroy(self.__obj) @@ -1390,6 +1398,12 @@ def load_kyber(cls, key): _DLL.botan_privkey_load_kyber(byref(obj), key, len(key)) return PrivateKey(obj) + @classmethod + def load_frodokem(cls, frodo_mode, key): + obj = c_void_p(0) + _DLL.botan_privkey_load_frodokem(byref(obj), key, len(key), _ctype_str(frodo_mode)) + return PrivateKey(obj) + def __del__(self): _DLL.botan_privkey_destroy(self.__obj) diff --git a/src/scripts/test_python.py b/src/scripts/test_python.py index 80c14fddc3b..6b4762db354 100644 --- a/src/scripts/test_python.py +++ b/src/scripts/test_python.py @@ -937,6 +937,20 @@ def test_kyber_raw_keys(self): pubkey_read = a_pub.view_kyber_raw_key() self.assertEqual(pubkey_read, a_pub_bits) + def test_frodokem_raw_keys(self): + frodo_mode = "FrodoKEM-640-SHAKE" + sk = botan.PrivateKey.create("FrodoKEM", frodo_mode, botan.RandomNumberGenerator("user")) + pk = sk.get_public_key() + + sk_bits = sk.to_raw() + pk_bits = pk.to_raw() + + sk_read = botan.PrivateKey.load_frodokem(frodo_mode, sk_bits) + pk_read = botan.PublicKey.load_frodokem(frodo_mode, pk_bits) + + self.assertEqual(sk_read.to_raw(), sk_bits) + self.assertEqual(pk_read.to_raw(), pk_bits) + class BotanPythonZfecTests(unittest.TestCase): """ diff --git a/src/tests/test_ffi.cpp b/src/tests/test_ffi.cpp index 3bed9818e0a..80f290d9cfe 100644 --- a/src/tests/test_ffi.cpp +++ b/src/tests/test_ffi.cpp @@ -3323,6 +3323,151 @@ class ViewBytesSink final { std::vector m_buf; }; +/** + * Base class for roundtrip tests of FFI bindings for Key Encapsulation Mechanisms. + */ +class FFI_KEM_Roundtrip_Test : public FFI_Test { + protected: + using privkey_loader_fn_t = int (*)(botan_privkey_t*, const uint8_t[], size_t, const char*); + using pubkey_loader_fn_t = int (*)(botan_pubkey_t*, const uint8_t[], size_t, const char*); + + protected: + virtual const char* algo() const = 0; + virtual privkey_loader_fn_t private_key_load_function() const = 0; + virtual pubkey_loader_fn_t public_key_load_function() const = 0; + virtual std::vector modes() const = 0; + + public: + void ffi_test(Test::Result& result, botan_rng_t rng) override { + for(auto mode : modes()) { + // generate a key pair + botan_privkey_t priv; + botan_pubkey_t pub; + if(!TEST_FFI_INIT(botan_privkey_create, (&priv, algo(), mode, rng))) { + continue; + } + TEST_FFI_OK(botan_privkey_export_pubkey, (&pub, priv)); + + // raw-encode the key pair + ViewBytesSink priv_bytes; + ViewBytesSink pub_bytes; + TEST_FFI_OK(botan_privkey_view_raw, (priv, priv_bytes.delegate(), priv_bytes.callback())); + TEST_FFI_OK(botan_pubkey_view_raw, (pub, pub_bytes.delegate(), pub_bytes.callback())); + + // decode the key pair from raw encoding + botan_privkey_t priv_loaded; + botan_pubkey_t pub_loaded; + TEST_FFI_OK(private_key_load_function(), + (&priv_loaded, priv_bytes.get().data(), priv_bytes.get().size(), mode)); + TEST_FFI_OK(public_key_load_function(), + (&pub_loaded, pub_bytes.get().data(), pub_bytes.get().size(), mode)); + + // re-encode and compare to the first round + ViewBytesSink priv_bytes2; + ViewBytesSink pub_bytes2; + TEST_FFI_OK(botan_privkey_view_raw, (priv_loaded, priv_bytes2.delegate(), priv_bytes2.callback())); + TEST_FFI_OK(botan_pubkey_view_raw, (pub_loaded, pub_bytes2.delegate(), pub_bytes2.callback())); + result.test_eq("private key encoding", priv_bytes.get(), priv_bytes2.get()); + result.test_eq("public key encoding", pub_bytes.get(), pub_bytes2.get()); + + // KEM encryption (using the loaded public key) + botan_pk_op_kem_encrypt_t kem_enc; + TEST_FFI_OK(botan_pk_op_kem_encrypt_create, (&kem_enc, pub_loaded, "Raw")); + + // explicitly query output lengths + size_t shared_key_length = 0; + size_t ciphertext_length = 0; + TEST_FFI_OK(botan_pk_op_kem_encrypt_shared_key_length, (kem_enc, 0, &shared_key_length)); + TEST_FFI_OK(botan_pk_op_kem_encrypt_encapsulated_key_length, (kem_enc, &ciphertext_length)); + + // check that insufficient buffer space is handled correctly + size_t shared_key_length_out = 0; + size_t ciphertext_length_out = 0; + TEST_FFI_RC(BOTAN_FFI_ERROR_INSUFFICIENT_BUFFER_SPACE, + botan_pk_op_kem_encrypt_create_shared_key, + (kem_enc, + rng, + nullptr /* no salt */, + 0, + 0 /* default key length */, + nullptr, + &shared_key_length_out, + nullptr, + &ciphertext_length_out)); + + // TODO: should this report both lengths for usage convenience? + result.confirm("at least one buffer length is reported", + shared_key_length_out == shared_key_length || ciphertext_length_out == ciphertext_length); + + // allocate buffers (with additional space) and perform the actual encryption + shared_key_length_out = shared_key_length * 2; + ciphertext_length_out = ciphertext_length * 2; + Botan::secure_vector shared_key(shared_key_length_out); + std::vector ciphertext(ciphertext_length_out); + TEST_FFI_OK(botan_pk_op_kem_encrypt_create_shared_key, + (kem_enc, + rng, + nullptr /* no salt */, + 0, + 0 /* default key length */, + shared_key.data(), + &shared_key_length_out, + ciphertext.data(), + &ciphertext_length_out)); + result.test_eq("shared key length", shared_key_length, shared_key_length_out); + result.test_eq("ciphertext length", ciphertext_length, ciphertext_length_out); + shared_key.resize(shared_key_length_out); + ciphertext.resize(ciphertext_length_out); + TEST_FFI_OK(botan_pk_op_kem_encrypt_destroy, (kem_enc)); + + // KEM decryption (using the generated private key) + botan_pk_op_kem_decrypt_t kem_dec; + TEST_FFI_OK(botan_pk_op_kem_decrypt_create, (&kem_dec, priv, "Raw")); + size_t shared_key_length2 = 0; + TEST_FFI_OK(botan_pk_op_kem_decrypt_shared_key_length, (kem_dec, shared_key_length, &shared_key_length2)); + result.test_eq("shared key lengths are consistent", shared_key_length, shared_key_length2); + + // check that insufficient buffer space is handled correctly + shared_key_length_out = 0; + TEST_FFI_RC(BOTAN_FFI_ERROR_INSUFFICIENT_BUFFER_SPACE, + botan_pk_op_kem_decrypt_shared_key, + (kem_dec, + nullptr /* no salt */, + 0, + ciphertext.data(), + ciphertext.size(), + 0 /* default length */, + nullptr, + &shared_key_length_out)); + result.test_eq("reported buffer length requirement", shared_key_length, shared_key_length_out); + + // allocate buffer (double the size) and perform the actual decryption + shared_key_length_out = shared_key_length * 2; + Botan::secure_vector shared_key2(shared_key_length_out); + TEST_FFI_OK(botan_pk_op_kem_decrypt_shared_key, + (kem_dec, + nullptr /* no salt */, + 0, + ciphertext.data(), + ciphertext.size(), + 0 /* default length */, + shared_key2.data(), + &shared_key_length_out)); + result.test_eq("shared key output length", shared_key_length, shared_key_length_out); + shared_key2.resize(shared_key_length_out); + TEST_FFI_OK(botan_pk_op_kem_decrypt_destroy, (kem_dec)); + + // final check and clean up + result.test_eq("shared keys match", shared_key, shared_key2); + + TEST_FFI_OK(botan_pubkey_destroy, (pub)); + TEST_FFI_OK(botan_pubkey_destroy, (pub_loaded)); + TEST_FFI_OK(botan_privkey_destroy, (priv)); + TEST_FFI_OK(botan_privkey_destroy, (priv_loaded)); + } + } +}; + class FFI_Kyber512_Test final : public FFI_Test { public: std::string name() const override { return "FFI Kyber512"; } @@ -3458,6 +3603,35 @@ class FFI_Kyber1024_Test final : public FFI_Test { } }; +class FFI_FrodoKEM_Test final : public FFI_KEM_Roundtrip_Test { + public: + std::string name() const override { return "FFI FrodoKEM"; } + + protected: + const char* algo() const override { return "FrodoKEM"; } + + privkey_loader_fn_t private_key_load_function() const override { return botan_privkey_load_frodokem; } + + pubkey_loader_fn_t public_key_load_function() const override { return botan_pubkey_load_frodokem; } + + std::vector modes() const override { + return std::vector{ + "FrodoKEM-640-SHAKE", + "FrodoKEM-976-SHAKE", + "FrodoKEM-1344-SHAKE", + "eFrodoKEM-640-SHAKE", + "eFrodoKEM-976-SHAKE", + "eFrodoKEM-1344-SHAKE", + "FrodoKEM-640-AES", + "FrodoKEM-976-AES", + "FrodoKEM-1344-AES", + "eFrodoKEM-640-AES", + "eFrodoKEM-976-AES", + "eFrodoKEM-1344-AES", + }; + } +}; + class FFI_ElGamal_Test final : public FFI_Test { public: std::string name() const override { return "FFI ElGamal"; } @@ -3700,6 +3874,7 @@ BOTAN_REGISTER_TEST("ffi", "ffi_x448", FFI_X448_Test); BOTAN_REGISTER_TEST("ffi", "ffi_kyber512", FFI_Kyber512_Test); BOTAN_REGISTER_TEST("ffi", "ffi_kyber768", FFI_Kyber768_Test); BOTAN_REGISTER_TEST("ffi", "ffi_kyber1024", FFI_Kyber1024_Test); +BOTAN_REGISTER_TEST("ffi", "ffi_frodokem", FFI_FrodoKEM_Test); BOTAN_REGISTER_TEST("ffi", "ffi_elgamal", FFI_ElGamal_Test); BOTAN_REGISTER_TEST("ffi", "ffi_dh", FFI_DH_Test);