From f0a0a467de2859362246b2049b56a722ac7298e8 Mon Sep 17 00:00:00 2001 From: Rene Meusel Date: Fri, 29 Aug 2025 14:08:07 +0200 Subject: [PATCH 1/4] Refactor: move and extend View*Sink helpers --- src/tests/test_ffi.cpp | 128 +++++++++++++++++++++-------------------- 1 file changed, 66 insertions(+), 62 deletions(-) diff --git a/src/tests/test_ffi.cpp b/src/tests/test_ffi.cpp index 02a13e4bf51..a3ca7a50995 100644 --- a/src/tests/test_ffi.cpp +++ b/src/tests/test_ffi.cpp @@ -61,6 +61,72 @@ namespace { // NOLINTEND(*-macro-usage) +/** + * Helper class for testing "view"-style API functions that take a callback + * that gets passed a variable-length buffer of bytes. + * + * Example: + * botan_privkey_t priv; + * ViewBytesSink sink; + * botan_privkey_view_raw(priv, sink.delegate(), sink.callback()); + * std::cout << hex_encode(sink.get()) << std::endl; + */ +class ViewBytesSink final { + public: + void* delegate() { return this; } + + botan_view_bin_fn callback() { return &write_fn; } + + std::span get() const { return m_buf; } + + const uint8_t* data() const { return m_buf.data(); } + + size_t size() const { return m_buf.size(); } + + private: + static int write_fn(void* ctx, const uint8_t buf[], size_t len) { + if(ctx == nullptr || buf == nullptr) { + return BOTAN_FFI_ERROR_NULL_POINTER; + } + + auto* sink = static_cast(ctx); + sink->m_buf.assign(buf, buf + len); + + return BOTAN_FFI_SUCCESS; + } + + private: + std::vector m_buf; +}; + +/** + * See ViewBytesSink for how to use this. Works for `botan_view_str_fn` instead. +*/ +class ViewStringSink final { + public: + void* delegate() { return this; } + + botan_view_str_fn callback() { return &write_fn; } + + std::string_view get() { return m_str; } + + private: + static int write_fn(void* ctx, const char* str, size_t len) { + if(ctx == nullptr || str == nullptr) { + return BOTAN_FFI_ERROR_NULL_POINTER; + } + + auto* sink = static_cast(ctx); + // discard the null terminator + sink->m_str = std::string(str, len - 1); + + return BOTAN_FFI_SUCCESS; + } + + private: + std::string m_str; +}; + // NOLINTBEGIN(*-init-variables) class FFI_Test : public Test { @@ -3408,68 +3474,6 @@ class FFI_X448_Test final : public FFI_Test { } }; -/** - * Helper class for testing "view"-style API functions that take a callback - * that gets passed a variable-length buffer of bytes. - * - * Example: - * botan_privkey_t priv; - * ViewBytesSink sink; - * botan_privkey_view_raw(priv, sink.delegate(), sink.callback()); - * std::cout << hex_encode(sink.get()) << std::endl; - */ -class ViewBytesSink final { - public: - void* delegate() { return this; } - - botan_view_bin_fn callback() { return &write_fn; } - - const std::vector& get() { return m_buf; } - - private: - static int write_fn(void* ctx, const uint8_t buf[], size_t len) { - if(ctx == nullptr || buf == nullptr) { - return BOTAN_FFI_ERROR_NULL_POINTER; - } - - auto* sink = static_cast(ctx); - sink->m_buf.assign(buf, buf + len); - - return 0; - } - - private: - std::vector m_buf; -}; - -/** - * See ViewBytesSink for how to use this. Works for `botan_view_str_fn` instead. -*/ -class ViewStringSink final { - public: - void* delegate() { return this; } - - botan_view_str_fn callback() { return &write_fn; } - - std::string_view get() { return m_str; } - - private: - static int write_fn(void* ctx, const char* str, size_t len) { - if(ctx == nullptr || str == nullptr) { - return BOTAN_FFI_ERROR_NULL_POINTER; - } - - auto* sink = static_cast(ctx); - // discard the null terminator - sink->m_str = std::string(str, len - 1); - - return 0; - } - - private: - std::string m_str; -}; - /** * Base class for roundtrip tests of FFI bindings for Key Encapsulation Mechanisms. */ From b24417ca9b29ce33b63e10751ad7d1fb8e5c975e Mon Sep 17 00:00:00 2001 From: Rene Meusel Date: Fri, 29 Aug 2025 14:26:00 +0200 Subject: [PATCH 2/4] FFI: add botan_pubkey_load_ec*_sec1() functions This allows loading ECDH, ECDSA, and SM2 public keys that are encoded in SEC1 format (both compressed and uncompressed). Without that, there's no convenient way to load compressed SEC1 encoded ECC public keys. For uncompressed points one could have manually parse the coordinates and pass them into botan_pubkey_load_ec*_load(). --- src/lib/ffi/ffi.h | 9 ++++ src/lib/ffi/ffi_pkey_algs.cpp | 87 +++++++++++++++++++++++++++++++++++ src/tests/test_ffi.cpp | 51 ++++++++++++++------ 3 files changed, 134 insertions(+), 13 deletions(-) diff --git a/src/lib/ffi/ffi.h b/src/lib/ffi/ffi.h index 3bb64aad1f6..d693b1d5da7 100644 --- a/src/lib/ffi/ffi.h +++ b/src/lib/ffi/ffi.h @@ -1862,15 +1862,24 @@ int botan_privkey_load_ecdsa(botan_privkey_t* key, botan_mp_t scalar, const char BOTAN_FFI_EXPORT(2, 2) int botan_pubkey_load_ecdsa(botan_pubkey_t* key, botan_mp_t public_x, botan_mp_t public_y, const char* curve_name); +BOTAN_FFI_EXPORT(3, 10) +int botan_pubkey_load_ecdsa_sec1(botan_pubkey_t* key, const uint8_t sec1[], size_t sec1_len, const char* curve_name); + BOTAN_FFI_EXPORT(2, 2) int botan_pubkey_load_ecdh(botan_pubkey_t* key, botan_mp_t public_x, botan_mp_t public_y, const char* curve_name); +BOTAN_FFI_EXPORT(3, 10) +int botan_pubkey_load_ecdh_sec1(botan_pubkey_t* key, const uint8_t sec1[], size_t sec1_len, const char* curve_name); + BOTAN_FFI_EXPORT(2, 2) int botan_privkey_load_ecdh(botan_privkey_t* key, botan_mp_t scalar, const char* curve_name); BOTAN_FFI_EXPORT(2, 2) int botan_pubkey_load_sm2(botan_pubkey_t* key, botan_mp_t public_x, botan_mp_t public_y, const char* curve_name); +BOTAN_FFI_EXPORT(3, 10) +int botan_pubkey_load_sm2_sec1(botan_pubkey_t* key, const uint8_t sec1[], size_t sec1_len, const char* curve_name); + BOTAN_FFI_EXPORT(2, 2) int botan_privkey_load_sm2(botan_privkey_t* key, botan_mp_t scalar, const char* curve_name); diff --git a/src/lib/ffi/ffi_pkey_algs.cpp b/src/lib/ffi/ffi_pkey_algs.cpp index 77d85533ec5..a122dcb9e05 100644 --- a/src/lib/ffi/ffi_pkey_algs.cpp +++ b/src/lib/ffi/ffi_pkey_algs.cpp @@ -139,6 +139,24 @@ int pubkey_load_ec(std::unique_ptr& key, } } +template +int pubkey_load_ec_sec1(std::unique_ptr& key, + std::span sec1, + std::string_view curve_name) { + if(!Botan::EC_Group::supports_named_group(curve_name)) { + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; + } + + const auto group = Botan::EC_Group::from_name(curve_name); + + if(auto pt = Botan::EC_AffinePoint::deserialize(group, sec1)) { + key.reset(new ECPublicKey_t(group, pt.value())); + return BOTAN_FFI_SUCCESS; + } else { + return BOTAN_FFI_ERROR_BAD_PARAMETER; + } +} + #endif Botan::BigInt pubkey_get_field(const Botan::Public_Key& key, std::string_view field) { @@ -452,6 +470,29 @@ int botan_pubkey_load_ecdsa(botan_pubkey_t* key, #endif } +int botan_pubkey_load_ecdsa_sec1(botan_pubkey_t* key, const uint8_t sec1[], size_t sec1_len, const char* curve_name) { +#if defined(BOTAN_HAS_ECDSA) + if(key == nullptr || sec1 == nullptr || curve_name == nullptr) { + return BOTAN_FFI_ERROR_NULL_POINTER; + } + *key = nullptr; + + return ffi_guard_thunk(__func__, [=]() -> int { + std::unique_ptr p_key; + + int rc = pubkey_load_ec_sec1(p_key, {sec1, sec1_len}, curve_name); + if(rc == BOTAN_FFI_SUCCESS) { + ffi_new_object(key, std::move(p_key)); + } + + return rc; + }); +#else + BOTAN_UNUSED(key, sec1, sec1_len, curve_name); + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; +#endif +} + int botan_privkey_load_ecdsa(botan_privkey_t* key, const botan_mp_t scalar, const char* curve_name) { #if defined(BOTAN_HAS_ECDSA) if(key == nullptr || curve_name == nullptr) { @@ -619,6 +660,29 @@ int botan_pubkey_load_ecdh(botan_pubkey_t* key, #endif } +int botan_pubkey_load_ecdh_sec1(botan_pubkey_t* key, const uint8_t sec1[], size_t sec1_len, const char* curve_name) { +#if defined(BOTAN_HAS_ECDH) + if(key == nullptr || sec1 == nullptr || curve_name == nullptr) { + return BOTAN_FFI_ERROR_NULL_POINTER; + } + *key = nullptr; + + return ffi_guard_thunk(__func__, [=]() -> int { + std::unique_ptr p_key; + + int rc = pubkey_load_ec_sec1(p_key, {sec1, sec1_len}, curve_name); + if(rc == BOTAN_FFI_SUCCESS) { + ffi_new_object(key, std::move(p_key)); + } + + return rc; + }); +#else + BOTAN_UNUSED(key, sec1, sec1_len, curve_name); + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; +#endif +} + int botan_privkey_load_ecdh(botan_privkey_t* key, const botan_mp_t scalar, const char* curve_name) { #if defined(BOTAN_HAS_ECDH) if(key == nullptr || curve_name == nullptr) { @@ -698,6 +762,29 @@ int botan_pubkey_load_sm2(botan_pubkey_t* key, #endif } +int botan_pubkey_load_sm2_sec1(botan_pubkey_t* key, const uint8_t sec1[], size_t sec1_len, const char* curve_name) { +#if defined(BOTAN_HAS_SM2) + if(key == nullptr || sec1 == nullptr || curve_name == nullptr) { + return BOTAN_FFI_ERROR_NULL_POINTER; + } + *key = nullptr; + + return ffi_guard_thunk(__func__, [=]() -> int { + std::unique_ptr p_key; + + int rc = pubkey_load_ec_sec1(p_key, {sec1, sec1_len}, curve_name); + if(rc == BOTAN_FFI_SUCCESS) { + ffi_new_object(key, std::move(p_key)); + } + + return rc; + }); +#else + BOTAN_UNUSED(key, sec1, sec1_len, curve_name); + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; +#endif +} + int botan_privkey_load_sm2(botan_privkey_t* key, const botan_mp_t scalar, const char* curve_name) { #if defined(BOTAN_HAS_SM2) if(key == nullptr || curve_name == nullptr) { diff --git a/src/tests/test_ffi.cpp b/src/tests/test_ffi.cpp index a3ca7a50995..a19d8204e87 100644 --- a/src/tests/test_ffi.cpp +++ b/src/tests/test_ffi.cpp @@ -2833,6 +2833,7 @@ class FFI_ECDSA_Test final : public FFI_Test { botan_mp_t private_scalar; botan_mp_t public_x; botan_mp_t public_y; + ViewBytesSink sec1; botan_mp_init(&private_scalar); botan_mp_init(&public_x); botan_mp_init(&public_y); @@ -2843,13 +2844,17 @@ class FFI_ECDSA_Test final : public FFI_Test { TEST_FFI_OK(botan_privkey_get_field, (private_scalar, priv, "x")); TEST_FFI_OK(botan_pubkey_get_field, (public_x, pub, "public_x")); TEST_FFI_OK(botan_pubkey_get_field, (public_y, pub, "public_y")); + TEST_FFI_OK(botan_pubkey_view_raw, (pub, sec1.delegate(), sec1.callback())); botan_privkey_t loaded_privkey; - botan_pubkey_t loaded_pubkey; + botan_pubkey_t loaded_pubkey1; + botan_pubkey_t loaded_pubkey2; TEST_FFI_OK(botan_privkey_load_ecdsa, (&loaded_privkey, private_scalar, kCurve)); - TEST_FFI_OK(botan_pubkey_load_ecdsa, (&loaded_pubkey, public_x, public_y, kCurve)); + TEST_FFI_OK(botan_pubkey_load_ecdsa, (&loaded_pubkey1, public_x, public_y, kCurve)); + TEST_FFI_OK(botan_pubkey_load_ecdsa_sec1, (&loaded_pubkey2, sec1.data(), sec1.size(), kCurve)); TEST_FFI_OK(botan_privkey_check_key, (loaded_privkey, rng, 0)); - TEST_FFI_OK(botan_pubkey_check_key, (loaded_pubkey, rng, 0)); + TEST_FFI_OK(botan_pubkey_check_key, (loaded_pubkey1, rng, 0)); + TEST_FFI_OK(botan_pubkey_check_key, (loaded_pubkey2, rng, 0)); std::array namebuf{}; size_t name_len = namebuf.size(); @@ -2918,7 +2923,8 @@ class FFI_ECDSA_Test final : public FFI_Test { TEST_FFI_OK(botan_pubkey_destroy, (pub)); TEST_FFI_OK(botan_privkey_destroy, (priv)); TEST_FFI_OK(botan_privkey_destroy, (loaded_privkey)); - TEST_FFI_OK(botan_pubkey_destroy, (loaded_pubkey)); + TEST_FFI_OK(botan_pubkey_destroy, (loaded_pubkey1)); + TEST_FFI_OK(botan_pubkey_destroy, (loaded_pubkey2)); } }; @@ -2932,7 +2938,8 @@ class FFI_SM2_Sig_Test final : public FFI_Test { botan_privkey_t priv; botan_pubkey_t pub; botan_privkey_t loaded_privkey; - botan_pubkey_t loaded_pubkey; + botan_pubkey_t loaded_pubkey1; + botan_pubkey_t loaded_pubkey2; if(!TEST_FFI_INIT(botan_privkey_create, (&priv, "SM2_Sig", kCurve, rng))) { return; @@ -2949,6 +2956,7 @@ class FFI_SM2_Sig_Test final : public FFI_Test { botan_mp_t private_scalar; botan_mp_t public_x; botan_mp_t public_y; + ViewBytesSink sec1; botan_mp_init(&private_scalar); botan_mp_init(&public_x); botan_mp_init(&public_y); @@ -2956,10 +2964,13 @@ class FFI_SM2_Sig_Test final : public FFI_Test { TEST_FFI_OK(botan_privkey_get_field, (private_scalar, priv, "x")); TEST_FFI_OK(botan_pubkey_get_field, (public_x, pub, "public_x")); TEST_FFI_OK(botan_pubkey_get_field, (public_y, pub, "public_y")); + TEST_FFI_OK(botan_pubkey_view_raw, (pub, sec1.delegate(), sec1.callback())); REQUIRE_FFI_OK(botan_privkey_load_sm2, (&loaded_privkey, private_scalar, kCurve)); - REQUIRE_FFI_OK(botan_pubkey_load_sm2, (&loaded_pubkey, public_x, public_y, kCurve)); + REQUIRE_FFI_OK(botan_pubkey_load_sm2, (&loaded_pubkey1, public_x, public_y, kCurve)); + REQUIRE_FFI_OK(botan_pubkey_load_sm2_sec1, (&loaded_pubkey2, sec1.data(), sec1.size(), kCurve)); TEST_FFI_OK(botan_privkey_check_key, (loaded_privkey, rng, 0)); - TEST_FFI_OK(botan_pubkey_check_key, (loaded_pubkey, rng, 0)); + TEST_FFI_OK(botan_pubkey_check_key, (loaded_pubkey1, rng, 0)); + TEST_FFI_OK(botan_pubkey_check_key, (loaded_pubkey2, rng, 0)); std::array namebuf{}; size_t name_len = namebuf.size(); @@ -3021,7 +3032,8 @@ class FFI_SM2_Sig_Test final : public FFI_Test { TEST_FFI_OK(botan_pubkey_destroy, (pub)); TEST_FFI_OK(botan_privkey_destroy, (priv)); TEST_FFI_OK(botan_privkey_destroy, (loaded_privkey)); - TEST_FFI_OK(botan_pubkey_destroy, (loaded_pubkey)); + TEST_FFI_OK(botan_pubkey_destroy, (loaded_pubkey1)); + TEST_FFI_OK(botan_pubkey_destroy, (loaded_pubkey2)); } }; @@ -3034,7 +3046,8 @@ class FFI_SM2_Enc_Test final : public FFI_Test { botan_privkey_t priv; botan_pubkey_t pub; botan_privkey_t loaded_privkey; - botan_pubkey_t loaded_pubkey; + botan_pubkey_t loaded_pubkey1; + botan_pubkey_t loaded_pubkey2; if(!TEST_FFI_INIT(botan_privkey_create, (&priv, "SM2_Enc", kCurve, rng))) { return; @@ -3051,6 +3064,7 @@ class FFI_SM2_Enc_Test final : public FFI_Test { botan_mp_t private_scalar; botan_mp_t public_x; botan_mp_t public_y; + ViewBytesSink sec1; botan_mp_init(&private_scalar); botan_mp_init(&public_x); botan_mp_init(&public_y); @@ -3058,10 +3072,13 @@ class FFI_SM2_Enc_Test final : public FFI_Test { TEST_FFI_OK(botan_privkey_get_field, (private_scalar, priv, "x")); TEST_FFI_OK(botan_pubkey_get_field, (public_x, pub, "public_x")); TEST_FFI_OK(botan_pubkey_get_field, (public_y, pub, "public_y")); + TEST_FFI_OK(botan_pubkey_view_raw, (pub, sec1.delegate(), sec1.callback())); REQUIRE_FFI_OK(botan_privkey_load_sm2_enc, (&loaded_privkey, private_scalar, kCurve)); - REQUIRE_FFI_OK(botan_pubkey_load_sm2_enc, (&loaded_pubkey, public_x, public_y, kCurve)); + REQUIRE_FFI_OK(botan_pubkey_load_sm2_enc, (&loaded_pubkey1, public_x, public_y, kCurve)); + REQUIRE_FFI_OK(botan_pubkey_load_sm2_sec1, (&loaded_pubkey2, sec1.data(), sec1.size(), kCurve)); TEST_FFI_OK(botan_privkey_check_key, (loaded_privkey, rng, 0)); - TEST_FFI_OK(botan_pubkey_check_key, (loaded_pubkey, rng, 0)); + TEST_FFI_OK(botan_pubkey_check_key, (loaded_pubkey1, rng, 0)); + TEST_FFI_OK(botan_pubkey_check_key, (loaded_pubkey2, rng, 0)); std::array namebuf{}; size_t name_len = namebuf.size(); @@ -3075,7 +3092,7 @@ class FFI_SM2_Enc_Test final : public FFI_Test { TEST_FFI_OK(botan_rng_get, (rng, message.data(), message.size())); botan_pk_op_encrypt_t enc; - if(TEST_FFI_OK(botan_pk_op_encrypt_create, (&enc, loaded_pubkey, "", 0))) { + if(TEST_FFI_OK(botan_pk_op_encrypt_create, (&enc, loaded_pubkey1, "", 0))) { size_t ctext_len; TEST_FFI_OK(botan_pk_op_encrypt_output_length, (enc, message.size(), &ctext_len)); @@ -3102,7 +3119,8 @@ class FFI_SM2_Enc_Test final : public FFI_Test { TEST_FFI_OK(botan_pubkey_destroy, (pub)); TEST_FFI_OK(botan_privkey_destroy, (priv)); TEST_FFI_OK(botan_privkey_destroy, (loaded_privkey)); - TEST_FFI_OK(botan_pubkey_destroy, (loaded_pubkey)); + TEST_FFI_OK(botan_pubkey_destroy, (loaded_pubkey1)); + TEST_FFI_OK(botan_pubkey_destroy, (loaded_pubkey2)); } }; @@ -3129,6 +3147,7 @@ class FFI_ECDH_Test final : public FFI_Test { botan_mp_t private_scalar; botan_mp_t public_x; botan_mp_t public_y; + ViewBytesSink sec1; botan_mp_init(&private_scalar); botan_mp_init(&public_x); botan_mp_init(&public_y); @@ -3136,15 +3155,20 @@ class FFI_ECDH_Test final : public FFI_Test { TEST_FFI_OK(botan_privkey_get_field, (private_scalar, priv1, "x")); TEST_FFI_OK(botan_pubkey_get_field, (public_x, pub1, "public_x")); TEST_FFI_OK(botan_pubkey_get_field, (public_y, pub1, "public_y")); + TEST_FFI_OK(botan_pubkey_view_raw, (pub1, sec1.delegate(), sec1.callback())); botan_privkey_t loaded_privkey1; botan_pubkey_t loaded_pubkey1; + botan_pubkey_t loaded_pubkey2; REQUIRE_FFI_OK(botan_privkey_load_ecdh, (&loaded_privkey1, private_scalar, "secp256r1")); REQUIRE_FFI_OK(botan_pubkey_load_ecdh, (&loaded_pubkey1, public_x, public_y, "secp256r1")); + REQUIRE_FFI_OK(botan_pubkey_load_ecdh_sec1, (&loaded_pubkey2, sec1.data(), sec1.size(), "secp256r1")); TEST_FFI_OK(botan_privkey_check_key, (loaded_privkey1, rng, 0)); TEST_FFI_OK(botan_pubkey_check_key, (loaded_pubkey1, rng, 0)); + TEST_FFI_OK(botan_pubkey_check_key, (loaded_pubkey2, rng, 0)); ffi_test_pubkey_export(result, loaded_pubkey1, priv1, rng); + ffi_test_pubkey_export(result, loaded_pubkey2, priv1, rng); ffi_test_pubkey_export(result, pub2, priv2, rng); #if defined(BOTAN_HAS_KDF2) && defined(BOTAN_HAS_SHA_256) @@ -3202,6 +3226,7 @@ class FFI_ECDH_Test final : public FFI_Test { TEST_FFI_OK(botan_pubkey_destroy, (pub2)); TEST_FFI_OK(botan_privkey_destroy, (loaded_privkey1)); TEST_FFI_OK(botan_pubkey_destroy, (loaded_pubkey1)); + TEST_FFI_OK(botan_pubkey_destroy, (loaded_pubkey2)); } }; From 40d97925128cd2390a1688fd1a3b5fb6a3a991e0 Mon Sep 17 00:00:00 2001 From: Rene Meusel Date: Fri, 29 Aug 2025 15:30:42 +0200 Subject: [PATCH 3/4] Python: allow loading SEC1-encoded ECC keys --- src/python/botan3.py | 21 +++++++++++++++++++++ src/scripts/test_python.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/python/botan3.py b/src/python/botan3.py index 46bf286f969..997680adef0 100755 --- a/src/python/botan3.py +++ b/src/python/botan3.py @@ -411,9 +411,12 @@ def ffi_api(fn, args, allowed_errors=None): ffi_api(dll.botan_pubkey_load_classic_mceliece, [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_ecdsa_sec1, [c_void_p, c_void_p, c_size_t, c_char_p]) ffi_api(dll.botan_pubkey_load_ecdh, [c_void_p, c_void_p, c_void_p, c_char_p]) ffi_api(dll.botan_privkey_load_ecdh, [c_void_p, c_void_p, c_char_p]) + ffi_api(dll.botan_pubkey_load_ecdh_sec1, [c_void_p, c_void_p, c_size_t, c_char_p]) ffi_api(dll.botan_pubkey_load_sm2, [c_void_p, c_void_p, c_void_p, c_char_p]) + ffi_api(dll.botan_pubkey_load_sm2_sec1, [c_void_p, c_void_p, c_size_t, c_char_p]) ffi_api(dll.botan_privkey_load_sm2, [c_void_p, c_void_p, c_char_p]) ffi_api(dll.botan_pubkey_load_sm2_enc, [c_void_p, c_void_p, c_void_p, c_char_p]) ffi_api(dll.botan_privkey_load_sm2_enc, [c_void_p, c_void_p, c_char_p]) @@ -1257,6 +1260,12 @@ def load_ecdsa(cls, curve, pub_x, pub_y): _DLL.botan_pubkey_load_ecdsa(byref(pub.handle_()), pub_x.handle_(), pub_y.handle_(), _ctype_str(curve)) return pub + @classmethod + def load_ecdsa_sec1(cls, curve, sec1_encoding): + pub = PublicKey() + _DLL.botan_pubkey_load_ecdsa_sec1(byref(pub.handle_()), _ctype_bits(sec1_encoding), len(sec1_encoding), _ctype_str(curve)) + return pub + @classmethod def load_ecdh(cls, curve, pub_x, pub_y): pub = PublicKey() @@ -1265,6 +1274,12 @@ def load_ecdh(cls, curve, pub_x, pub_y): _DLL.botan_pubkey_load_ecdh(byref(pub.handle_()), pub_x.handle_(), pub_y.handle_(), _ctype_str(curve)) return pub + @classmethod + def load_ecdh_sec1(cls, curve, sec1_encoding): + pub = PublicKey() + _DLL.botan_pubkey_load_ecdh_sec1(byref(pub.handle_()), _ctype_bits(sec1_encoding), len(sec1_encoding), _ctype_str(curve)) + return pub + @classmethod def load_sm2(cls, curve, pub_x, pub_y): pub = PublicKey() @@ -1273,6 +1288,12 @@ def load_sm2(cls, curve, pub_x, pub_y): _DLL.botan_pubkey_load_sm2(byref(pub.handle_()), pub_x.handle_(), pub_y.handle_(), _ctype_str(curve)) return pub + @classmethod + def load_sm2_sec1(cls, curve, sec1_encoding): + pub = PublicKey() + _DLL.botan_pubkey_load_sm2_sec1(byref(pub.handle_()), _ctype_bits(sec1_encoding), len(sec1_encoding), _ctype_str(curve)) + return pub + @classmethod def load_kyber(cls, key): pub = PublicKey() diff --git a/src/scripts/test_python.py b/src/scripts/test_python.py index 0d147c17156..52ebdd96e7d 100644 --- a/src/scripts/test_python.py +++ b/src/scripts/test_python.py @@ -509,6 +509,13 @@ def verify_positive_and_negative(verifier, sig): verify = botan.PKVerify(pk, param_str) verify_positive_and_negative(verify, sig) + @staticmethod + def _ecc_sec1_convert_to_compressed(uncompressed_sec1): + assert uncompressed_sec1[0] == 0x04 + is_odd = uncompressed_sec1[-1] & 0x01 != 0 + x = uncompressed_sec1[1:1 + (len(uncompressed_sec1) - 1) // 2] + return (b"\x03" if is_odd else b"\x02") + x + def test_rsa(self): rng = botan.RandomNumberGenerator() rsapriv = botan.PrivateKey.create('RSA', '1024', rng) @@ -609,6 +616,15 @@ def test_ecdsa(self): priv2 = botan.PrivateKey.load_ecdsa(group, priv.get_field('x')) self._pksign_roundtrips(priv2, pub, hash_fn) + # Load public key from SEC.1 encoding + uncompressed_sec1 = pub.to_raw() + pub3 = botan.PublicKey.load_ecdsa_sec1(group, uncompressed_sec1) + self._pksign_roundtrips(priv, pub3, hash_fn) + + compressed_sec1 = BotanPythonTests._ecc_sec1_convert_to_compressed(uncompressed_sec1) + pub4 = botan.PublicKey.load_ecdsa_sec1(group, compressed_sec1) + self._pksign_roundtrips(priv, pub4, hash_fn) + def test_sm2(self): rng = botan.RandomNumberGenerator() @@ -636,6 +652,15 @@ def test_sm2(self): priv2 = botan.PrivateKey.load_sm2(group, priv.get_field('x')) self._pksign_roundtrips(priv2, pub, hash_fn) + # Load public key from SEC.1 encoding + uncompressed_sec1 = pub.to_raw() + pub3 = botan.PublicKey.load_sm2_sec1(group, uncompressed_sec1) + self._pksign_roundtrips(priv, pub3, hash_fn) + + compressed_sec1 = BotanPythonTests._ecc_sec1_convert_to_compressed(uncompressed_sec1) + pub4 = botan.PublicKey.load_sm2_sec1(group, compressed_sec1) + self._pksign_roundtrips(priv, pub4, hash_fn) + def test_ecdh(self): a_rng = botan.RandomNumberGenerator('user') b_rng = botan.RandomNumberGenerator('user') @@ -681,6 +706,17 @@ def test_ecdh(self): a_raw = hex_encode(a_priv.to_raw()) self.assertEqual(int(a_raw, base=16), a_priv_x) + uncompressed_sec1 = a_priv.get_public_key().to_raw() + compressed_sec1 = BotanPythonTests._ecc_sec1_convert_to_compressed(uncompressed_sec1) + new_a_pub1 = botan.PublicKey.load_ecdh_sec1(grp, uncompressed_sec1) + new_a_pub2 = botan.PublicKey.load_ecdh_sec1(grp, compressed_sec1) + self.assertEqual(new_a_pub1.to_raw(), new_a_pub2.to_raw()) + self.assertEqual(new_a_pub1.to_raw(), a_priv.get_public_key().to_raw()) + b_op2 = botan.PKKeyAgreement(b_priv, kdf) + b_key2 = b_op2.agree(compressed_sec1, 32, salt) + self.assertEqual(b_key2, b_key) + + def test_rfc7748_kex(self): rng = botan.RandomNumberGenerator() From 6babfa38abcc6e29b7ea3d663ae1daedc2b9eb9b Mon Sep 17 00:00:00 2001 From: Rene Meusel Date: Fri, 29 Aug 2025 15:21:01 +0200 Subject: [PATCH 4/4] Bump FFI API version --- doc/api_ref/ffi.rst | 1 + src/lib/ffi/ffi.cpp | 5 +++++ src/lib/ffi/ffi.h | 2 +- src/lib/ffi/info.txt | 2 +- src/python/botan3.py | 6 ++++-- 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/doc/api_ref/ffi.rst b/doc/api_ref/ffi.rst index 5d0e97b1ff6..cf512d8f923 100644 --- a/doc/api_ref/ffi.rst +++ b/doc/api_ref/ffi.rst @@ -249,6 +249,7 @@ supported it. ============== =================== FFI Version Supported Starting ============== =================== +20250829 3.10.0 20250506 3.8.0 20240408 3.4.0 20231009 3.2.0 diff --git a/src/lib/ffi/ffi.cpp b/src/lib/ffi/ffi.cpp index 7185ca8ff8a..59ed3579372 100644 --- a/src/lib/ffi/ffi.cpp +++ b/src/lib/ffi/ffi.cpp @@ -219,6 +219,11 @@ uint32_t botan_ffi_api_version() { } int botan_ffi_supports_api(uint32_t api_version) { + // This is the API introduced in 3.10 + if(api_version == 20250829) { + return BOTAN_FFI_SUCCESS; + } + // This is the API introduced in 3.8 if(api_version == 20250506) { return BOTAN_FFI_SUCCESS; diff --git a/src/lib/ffi/ffi.h b/src/lib/ffi/ffi.h index d693b1d5da7..ff3792961bb 100644 --- a/src/lib/ffi/ffi.h +++ b/src/lib/ffi/ffi.h @@ -69,7 +69,7 @@ API follows a few simple rules: * that declaration is not visible here since this header is intentionally * free-standing, depending only on a few C standard library headers. */ -#define BOTAN_FFI_API_VERSION 20250506 +#define BOTAN_FFI_API_VERSION 20250829 /** * BOTAN_FFI_EXPORT indicates public FFI functions. diff --git a/src/lib/ffi/info.txt b/src/lib/ffi/info.txt index c5fe00dc9b6..a10f8727187 100644 --- a/src/lib/ffi/info.txt +++ b/src/lib/ffi/info.txt @@ -1,5 +1,5 @@ -FFI -> 20250506 +FFI -> 20250829 diff --git a/src/python/botan3.py b/src/python/botan3.py index 997680adef0..fa71882abcb 100755 --- a/src/python/botan3.py +++ b/src/python/botan3.py @@ -26,8 +26,10 @@ from datetime import datetime from collections.abc import Iterable -# This Python module requires the FFI API version introduced in Botan 3.8.0 -BOTAN_FFI_VERSION = 20250506 +# This Python module requires the FFI API version introduced in Botan 3.10.0 +# +# 3.10.0 - introduced botan_pubkey_load_ec*_sec1() +BOTAN_FFI_VERSION = 20250829 # # Base exception for all exceptions raised from this module