Skip to content

Add SPAKE2+ password authenticated key exchange (RFC 9383)#5711

Open
randombit wants to merge 1 commit into
masterfrom
jack/spake2+
Open

Add SPAKE2+ password authenticated key exchange (RFC 9383)#5711
randombit wants to merge 1 commit into
masterfrom
jack/spake2+

Conversation

@randombit

Copy link
Copy Markdown
Owner

No description provided.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.py wrapper, 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.

Comment thread src/lib/pake/spake2p/spake2p.cpp
Comment thread src/lib/pake/spake2p/spake2p.cpp
@randombit
randombit force-pushed the jack/spake2+ branch 4 times, most recently from a493e35 to 9260c04 Compare July 14, 2026 14:37
@randombit
randombit force-pushed the jack/spake2+ branch 2 times, most recently from 8611d31 to 0153be5 Compare July 24, 2026 15:47
Comment thread src/python/botan3.py Outdated
Comment thread src/python/botan3.py Outdated
Comment thread src/python/botan3.py
Comment thread src/python/botan3.py
params = cls()
_DLL.botan_spake2p_params_init_custom(byref(params.handle_()),
group.handle_(),
seed, len(seed),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ruff will almost certainly reformat stuff like this into one item per line

Comment thread doc/api_ref/python.rst
Comment on lines +730 to +739
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".

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are slight differences to the botan3.py doc here that will disappear when / if #5233 is merged, just fyi

Comment thread src/lib/ffi/ffi_spake2p.cpp
@randombit
randombit force-pushed the jack/spake2+ branch 3 times, most recently from 79c35c3 to 30ba816 Compare July 25, 2026 12:43

@arckoor arckoor left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/lib/ffi/ffi_spake2p.cpp
Comment on lines +173 to +175
if(any_null_pointers(params, rng, secret)) {
return BOTAN_FFI_ERROR_NULL_POINTER;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

secret sure, but _t structs like params and rng are not explicitly checked anywhere else I think, because safe_get takes care of that 🤔

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/lib/ffi/ffi_spake2p.cpp
Comment thread src/lib/ffi/ffi_spake2p.cpp Outdated
Comment thread src/lib/ffi/ffi_spake2p.cpp Outdated
Comment thread src/lib/ffi/ffi_spake2p.cpp Outdated
Comment thread src/lib/ffi/ffi_spake2p.cpp Outdated
Comment thread src/lib/ffi/ffi_spake2p.cpp Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Comment thread doc/api_ref/spake2p.rst
Comment on lines +59 to +61
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.
Comment on lines +74 to +77
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));
});
Comment on lines +202 to +211
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));
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants