Add SPAKE2+ password authenticated key exchange (RFC 9383)#5711
Add SPAKE2+ password authenticated key exchange (RFC 9383)#5711randombit wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces SPAKE2+ (RFC 9383) as a new PAKE implementation in Botan 3, including native C++ APIs, FFI bindings, Python bindings, tests (KATs + round trips), and documentation/example material.
Changes:
- Adds a new SPAKE2+ module with RFC 9383 ciphersuites and support for custom M/N generation via hash-to-curve.
- Exposes SPAKE2+ via the C FFI and the Python
botan3.pywrapper, including functional tests for both. - Adds RFC test vectors, new unit tests, and new API reference documentation (C++ + Python + FFI).
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/lib/pake/spake2p/spake2p.h | New public C++ API for SPAKE2+ (system parameters, registration, prover/verifier contexts). |
| src/lib/pake/spake2p/spake2p.cpp | New SPAKE2+ implementation (password derivation, key schedule, protocol flow). |
| src/lib/pake/spake2p/info.txt | Registers the new SPAKE2+ module and its dependencies. |
| src/lib/ffi/ffi_spake2p.cpp | Adds FFI implementation for SPAKE2+ APIs and context objects. |
| src/lib/ffi/ffi.h | Bumps FFI API version and declares new SPAKE2+ FFI functions/types. |
| src/lib/ffi/ffi.cpp | Extends botan_ffi_supports_api for the new FFI API version. |
| src/lib/ffi/info.txt | Bumps the FFI module version define. |
| src/python/botan3.py | Bumps required FFI version and adds Python SPAKE2+ wrapper APIs/classes. |
| src/scripts/test_python.py | Adds a Python-level integration test for the new SPAKE2+ bindings. |
| src/tests/test_spake2p.cpp | Adds SPAKE2+ KAT tests, round-trip tests, and password registration tests. |
| src/tests/data/pake/spake2p.vec | Adds RFC 9383 Appendix C test vectors. |
| src/tests/test_ffi.cpp | Adds SPAKE2+ FFI tests (happy path + tamper failure). |
| src/examples/spake2p.cpp | Adds a C++ example program demonstrating SPAKE2+ usage. |
| doc/api_ref/spake2p.rst | Adds C++ API reference documentation for SPAKE2+. |
| doc/api_ref/python.rst | Documents SPAKE2+ Python API additions. |
| doc/api_ref/ffi.rst | Updates the FFI version mapping table for the new API version. |
| doc/api_ref/contents.rst | Adds spake2p to the API reference table of contents. |
| doc/roadmap.rst | Removes SPAKE2+ from the roadmap since it is now implemented. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
a493e35 to
9260c04
Compare
8611d31 to
0153be5
Compare
| params = cls() | ||
| _DLL.botan_spake2p_params_init_custom(byref(params.handle_()), | ||
| group.handle_(), | ||
| seed, len(seed), |
There was a problem hiding this comment.
ruff will almost certainly reformat stuff like this into one item per line
| An implementation of the SPAKE2+ password authenticated key exchange | ||
| (RFC 9383). The prover knows the password itself, while the verifier | ||
| stores only a registration record derived from the password. | ||
|
|
||
| .. py:class:: Spake2pParams(ciphersuite) | ||
|
|
||
| The system parameters, selecting the elliptic curve group, the SPAKE2+ | ||
| M/N group elements, and the hash function. The ciphersuite is one of | ||
| "P256-SHA256", "P256-SHA512", "P384-SHA256", "P384-SHA512", or | ||
| "P521-SHA512". |
There was a problem hiding this comment.
There are slight differences to the botan3.py doc here that will disappear when / if #5233 is merged, just fyi
79c35c3 to
30ba816
Compare
arckoor
left a comment
There was a problem hiding this comment.
A couple are missing 😓
The extra checks on _t structs kind of throw my whole motivation of "don't change behaviour based on features" out of the window, since everywhere else they are not checked beforehand (and thus do change), but for consistency might still be better to leave those checks to safe_get
IDK, your call, the extra checks certainly don't hurt anyone
| if(any_null_pointers(params, rng, secret)) { | ||
| return BOTAN_FFI_ERROR_NULL_POINTER; | ||
| } |
There was a problem hiding this comment.
secret sure, but _t structs like params and rng are not explicitly checked anywhere else I think, because safe_get takes care of that 🤔
There was a problem hiding this comment.
True. I changed this PR to match the existing convention, though I think checking the struct params explicitly is a bit easier to understand and makes the invariants clearer. But changing things in that direction should be a mass PR changing all of FFI rather than in some incremental way.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (2)
src/lib/ffi/ffi_spake2p.cpp:94
- Same as botan_spake2p_params_init: if SystemParameters::custom throws before ffi_new_object runs, the caller's output handle is left unchanged. Setting *params=nullptr at the start of the thunk matches other FFI init/create functions and prevents accidental use/destroy of a garbage handle after failure.
return ffi_guard_thunk(__func__, [=]() -> int {
auto p = std::make_unique<Botan::SPAKE2p::SystemParameters>(
Botan::SPAKE2p::SystemParameters::custom(safe_get(group), spake2p_opt_span(seed, seed_len), hash_fn));
return ffi_new_object(params, std::move(p));
});
src/lib/ffi/ffi_spake2p.cpp:280
- Verifier init only checks record for null but then proceeds to use verifier and params without pre-validation, and on exceptions the output handle may be left unchanged. For consistency with other FFI init functions, check verifier/params too and clear *verifier at the start of the guarded thunk.
if(any_null_pointers(record)) {
return BOTAN_FFI_ERROR_NULL_POINTER;
}
#if defined(BOTAN_HAS_PAKE_SPAKE2PLUS)
return ffi_guard_thunk(__func__, [=]() -> int {
const auto& p = safe_get(params);
| SPAKE+ parameters include a KDF and an authentication code. The KDF is always HKDF | ||
| using the specified hash, and the MAC is always HMAC using the specified hash. In | ||
| particular the CMAC-based SPAKE+ suites described in RFC 9383 Table 1 are not supported. |
| return ffi_guard_thunk(__func__, [=]() -> int { | ||
| auto p = std::make_unique<Botan::SPAKE2p::SystemParameters>(spake2p_params_from_name(ciphersuite)); | ||
| return ffi_new_object(params, std::move(p)); | ||
| }); |
| return ffi_guard_thunk(__func__, [=]() -> int { | ||
| const auto& p = safe_get(params); | ||
| const auto sec = Botan::SPAKE2p::ProverSecret::deserialize(p, {secret, secret_len}); | ||
| auto ctx = std::make_unique<Botan::SPAKE2p::ProverContext>(p, | ||
| sec, | ||
| spake2p_opt_span(prover_id, prover_id_len), | ||
| spake2p_opt_span(verifier_id, verifier_id_len), | ||
| spake2p_opt_span(context, context_len)); | ||
| return ffi_new_object(prover, std::move(ctx)); | ||
| }); |
No description provided.