diff --git a/doc/api_ref/contents.rst b/doc/api_ref/contents.rst index f15f5771bf..53d9f2cb32 100644 --- a/doc/api_ref/contents.rst +++ b/doc/api_ref/contents.rst @@ -24,6 +24,7 @@ API Reference keywrap passhash cryptobox + spake2 srp psk_db filters diff --git a/doc/api_ref/spake2.rst b/doc/api_ref/spake2.rst new file mode 100644 index 0000000000..1cac14d12f --- /dev/null +++ b/doc/api_ref/spake2.rst @@ -0,0 +1,80 @@ +SPAKE2 Password Authenticated Key Exchange +============================================= + +.. versionadded:: 3.7.0 + +An implementation of SPAKE2 password authenticated key exchange +compatible with RFC 9832 is included. + +SPAKE2 requires each peer know its "role" within the protocol, namely being A +or B. This is common in most protocols; for example in a client/server +architecture, the client could be A and the server B. + +This implementation of SPAKE2 does not include the key confirmation step. Thus, +on its own, there is no guarantee that the two peers actually share the same +secret key. Normally the SPAKE2 shared secret is subsequently used to encrypt +one or more messages; this serves to confirm the key. It is possible to +implement RFC 9832 compatible key confirmation, as described in RFC 9832 Section 4. + +Each instance is configured with a set of parameters + +.. cpp:class:: SPAKE2_Parameters + + .. cpp:function:: SPAKE2_Parameters(const EC_Group& group, \ + std::string_view shared_secret, \ + std::span a_identity = {}, \ + std::span b_identity = {}, \ + std::span context = {}, \ + std::string_view hash = "SHA-512", \ + bool per_user_params = true) + + Constructs a new set of parameters. + + The elliptic curve group should typically be P-256, P-384, or P-521. + + The ``shared_secret`` is the low entropy user secret. This is hashed using + Argon2id to generate the SPAKE2 ``w`` parameter. + + The identities of the two peers are specified in ``a_identity`` and + ``b_identity``. These can be left empty if there is no possible identity; + however even the strings "client" and "server" would be preferable rather + than leaving them completely blank. + + The ``context`` is some arbitrary bytestring which is included when hashing + the shared secret. It can be left empty, or can be used to identity eg + the protocol in use. + + The ``hash_fn`` parameter specifies a hash function to use. Use SHA-512. + + If ``per_user_params`` is true, then SPAKE2 will proceed using system + parameters N/M which were generated using RFC 9380 hash to curve using the + identities and context string as inputs. This makes SPAKE2 "quantum + annoying"; baseline SPAKE2 can be broken by anyone who can recover the + discrete logarithms of the fixed N/M parameters included in the RFC. This + makes life difficult for an attacker who can compute discrete logarithms, + but cannot do so cheaply. + +.. cpp:enum-class:: SPAKE2_PeerId + + .. cpp:enumerator:: SPAKE2_PeerId::PeerA + + .. cpp:enumerator:: SPAKE2_PeerId::PeerB + + +.. cpp:class:: SPAKE2_Context + + .. cpp:function:: SPAKE2_Context(SPAKE2_PeerId whoami, \ + const SPAKE2_Parameters& params, \ + RandomNumberGenerator& rng) + + Prepare for a SPAKE2 exchange + + .. cpp:function:: std::vector generate_message() + + Proceed with the protocol. Generate a message, which must be sent + to the peer. + + .. cpp:function:: secure_vector process_message(std::span peer_message) + + Complete the key exchange, returning the shared secret. Will throw an exception + if an error occurs (eg the peer message is not formatted correctly) diff --git a/src/lib/pake/spake2/info.txt b/src/lib/pake/spake2/info.txt new file mode 100644 index 0000000000..bb7708432c --- /dev/null +++ b/src/lib/pake/spake2/info.txt @@ -0,0 +1,19 @@ + +PAKE_SPAKE2 -> 20240821 + + + +name -> "SPAKE2" +brief -> "SPAKE2 PAKE" + + + +spake2.h + + + +argon2 +hkdf +hmac +sha2_64 + diff --git a/src/lib/pake/spake2/spake2.cpp b/src/lib/pake/spake2/spake2.cpp new file mode 100644 index 0000000000..106d15a57c --- /dev/null +++ b/src/lib/pake/spake2/spake2.cpp @@ -0,0 +1,195 @@ +/* +* (C) 2024 Jack Lloyd +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#include + +#include +#include +#include +#include +#include + +namespace Botan { + +namespace { + +std::vector format_spake2_ad(std::span a_identity, + std::span b_identity, + std::span context) { + std::vector ad(a_identity.size() + b_identity.size() + context.size() + 3 * 8); + BufferStuffer stuffer(ad); + + auto append_with_le64 = [&](std::span data) { + stuffer.append(store_le(static_cast(data.size()))); + stuffer.append(data); + }; + + append_with_le64(a_identity); + append_with_le64(b_identity); + append_with_le64(context); + return ad; +} + +} // namespace + +EC_Scalar SPAKE2_Parameters::hash_shared_secret(const EC_Group& group, + std::string_view shared_secret, + std::span a_identity, + std::span b_identity, + std::span context) { + constexpr size_t M = 128 * 1024; + constexpr size_t t = 3; + constexpr size_t p = 1; + + const auto ad = format_spake2_ad(a_identity, b_identity, context); + + auto pwhash_fam = PasswordHashFamily::create_or_throw("Argon2id"); + auto pwhash = pwhash_fam->from_params(M, t, p); + + secure_vector w_bytes(group.get_order_bytes() + 16); + pwhash->hash(w_bytes, shared_secret, {}, ad, {}); + + return EC_Scalar::from_bytes_mod_order(group, w_bytes); +} + +SPAKE2_Parameters::SPAKE2_Parameters(const EC_Group& group, + std::string_view shared_secret, + std::span a_identity, + std::span b_identity, + std::span context, + std::string_view hash, + bool per_user_params) : + SPAKE2_Parameters(group, + SPAKE2_Parameters::hash_shared_secret(group, shared_secret, a_identity, b_identity, context), + a_identity, + b_identity, + context, + hash, + per_user_params) {} + +namespace { + +std::pair spake2_params(const EC_Group& group, + std::string_view hash, + std::span a_identity, + std::span b_identity, + std::span context, + bool per_user_params) { + BOTAN_ARG_CHECK(group.has_cofactor() == false, "SPAKE2 not supported with this curve"); + + if(per_user_params) { + auto input = format_spake2_ad(a_identity, b_identity, context); + + constexpr uint8_t spake2_m[] = {'S', 'P', 'A', 'K', 'E', '2', ' ', 'M'}; + constexpr uint8_t spake2_n[] = {'S', 'P', 'A', 'K', 'E', '2', ' ', 'N'}; + + auto m = EC_AffinePoint::hash_to_curve_ro(group, hash, input, spake2_m); + auto n = EC_AffinePoint::hash_to_curve_ro(group, hash, input, spake2_n); + + return std::make_pair(m, n); + } else { + const OID& group_id = group.get_curve_oid(); + + auto decode_pt = [&](std::string_view pt) -> EC_AffinePoint { return EC_AffinePoint(group, hex_decode(pt)); }; + + if(group_id == OID{1, 2, 840, 10045, 3, 1, 7}) { + auto m = decode_pt("02886e2f97ace46e55ba9dd7242579f2993b64e16ef3dcab95afd497333d8fa12f"); + auto n = decode_pt("03d8bbd6c639c62937b04d997f38c3770719c629d7014d49a24b4f98baa1292b49"); + return std::make_pair(m, n); + } else if(group_id == OID{1, 3, 132, 0, 34}) { + auto m = decode_pt( + "030ff0895ae5ebf6187080a82d82b42e2765e3b2f8749c7e05eba366434b363d3dc36f15314739074d2eb8613fceec2853"); + auto n = decode_pt( + "02c72cf2e390853a1c1c4ad816a62fd15824f56078918f43f922ca21518f9c543bb252c5490214cf9aa3f0baab4b665c10"); + return std::make_pair(m, n); + } else if(group_id == OID{1, 3, 132, 0, 35}) { + auto m = decode_pt( + "02003f06f38131b2ba2600791e82488e8d20ab889af753a41806c5db18d37d85608cfae06b82e4a72cd744c719193562a653ea1f119eef9356907edc9b56979962d7aa"); + auto n = decode_pt( + "0200c7924b9ec017f3094562894336a53c50167ba8c5963876880542bc669e494b2532d76c5b53dfb349fdf69154b9e0048c58a42e8ed04cef052a3bc349d95575cd25"); + return std::make_pair(m, n); + } else { + throw Not_Implemented("There are no defined SPAKE2 parameters for this curve"); + } + } +} + +} // namespace + +SPAKE2_Parameters::SPAKE2_Parameters(const EC_Group& group, + const EC_Scalar& shared_secret, + std::span a_identity, + std::span b_identity, + std::span context, + std::string_view hash, + bool per_user_params) : + m_group(group), + m_params(spake2_params(m_group, hash, a_identity, b_identity, context, per_user_params)), + m_w(shared_secret), + m_hash_fn(hash), + m_a_identity(a_identity.begin(), a_identity.end()), + m_b_identity(b_identity.begin(), b_identity.end()) {} + +std::vector SPAKE2_Context::generate_message() { + BOTAN_STATE_CHECK(!m_our_message.has_value()); + + const auto eph_key = EC_Scalar::random(m_params.group(), m_rng); + + const auto& N_or_M = m_params.spake2_our_pt(m_whoami); + const auto& g = EC_AffinePoint::generator(m_params.group()); + // Compute g*x + w*{M,N} + auto msg = EC_AffinePoint::mul_px_qy(g, eph_key, N_or_M, m_params.spake2_w(), m_rng).serialize_uncompressed(); + + m_our_message = std::make_pair(msg, eph_key); + + return msg; +} + +secure_vector SPAKE2_Context::process_message(std::span peer_message) { + BOTAN_STATE_CHECK(m_our_message.has_value()); + + // Reject anything except uncompressed points + if(peer_message.empty() || peer_message[0] != 0x04) { + throw Decoding_Error("SPAKE2 key share was invalid"); + } + + // Will throw if not on the curve + EC_AffinePoint peer_pt(m_params.group(), peer_message); + + const auto& [our_pt, eph_key] = m_our_message.value(); + const auto& N_or_M = m_params.spake2_their_pt(m_whoami); + // Compute x*(pt-w*N_or_M) + const auto neg_xw = eph_key.negate() * m_params.spake2_w(); + const auto K = EC_AffinePoint::mul_px_qy(peer_pt, eph_key, N_or_M, neg_xw, m_rng); + + auto hash_fn = HashFunction::create_or_throw(m_params.hash_function()); + + auto append_to_hash_with_le64 = [&](std::span data) { + hash_fn->update(store_le(static_cast(data.size()))); + hash_fn->update(data); + }; + + append_to_hash_with_le64(m_params.a_identity()); + append_to_hash_with_le64(m_params.b_identity()); + + // Always pA followed by pB: + if(m_whoami == SPAKE2_PeerId::PeerA) { + append_to_hash_with_le64(our_pt); + append_to_hash_with_le64(peer_message); + } else { + append_to_hash_with_le64(peer_message); + append_to_hash_with_le64(our_pt); + } + + append_to_hash_with_le64(K.serialize_uncompressed()); + append_to_hash_with_le64(m_params.spake2_w().serialize()); + + m_our_message.reset(); + + return hash_fn->final(); +} + +} // namespace Botan diff --git a/src/lib/pake/spake2/spake2.h b/src/lib/pake/spake2/spake2.h new file mode 100644 index 0000000000..4487da64f4 --- /dev/null +++ b/src/lib/pake/spake2/spake2.h @@ -0,0 +1,201 @@ +/* +* (C) 2024 Jack Lloyd +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#ifndef BOTAN_PAKE_SPAKE2_H_ +#define BOTAN_PAKE_SPAKE2_H_ + +#include +#include +#include +#include +#include +#include + +namespace Botan { + +class RandomNumberGenerator; + +/** +* Identifies which peer we are in the protocol +*/ +enum class SPAKE2_PeerId { + PeerA, + PeerB, +}; + +/** +* SPAKE2 (RFC 9382) Parameters +* +* This implementation of SPAKE2 requires asymmetric exchange, ie that +* each party knows if it is A or B. +* +* The key confirmation step is omitted; it is assumed that further +* uses of the shared secret will confirm the key. The shared secret is +* equivalent to Hash(TT) in RFC 9382 so it is possible to implement +* RFC 9382 conformant key confirmation if necessary. +*/ +class BOTAN_PUBLIC_API(3, 7) SPAKE2_Parameters final { + public: + /** + * RFC 9382 compatible SPAKE2 configuration + * + * The shared secret is hashed with Argon2id M=131072,t=3,p=1 and with the + * context string as the AD + * + * If the group is not P-256, P-384, or P-521, then the group must support + * RFC 9380 hash to curve. Curves with a cofactor are not supported. + * + * If per_user_params is true, this uses the "quantum annoying" variant + * where N/M are the output of hash to curve; this requires an attacker + * with a quantum computer to perform a discrete logarithm calculator per + * PAKE, rather than just once for the default (fixed) SPAKE2 parameters. + * This variant is described in RFC 9382 section 5, however note that a + * different input is used to hash_to_curve which includes not just the + * user identifiers but also the context string. + * + * @param group the elliptic curve group to operate in + * @param shared_secret the shared secret (eg a password) + * @param a_identity the (optional) identity string of peer A + * @param b_identity the (optional) identity string of peer B + * @param context an optional context string (for example a protocol identifier) + * @param hash the hash function to use (SHA-512 highly recommended) + * @param per_user_params if true then per-user N/M are used + */ + SPAKE2_Parameters(const EC_Group& group, + std::string_view shared_secret, + std::span a_identity = {}, + std::span b_identity = {}, + std::span context = {}, + std::string_view hash = "SHA-512", + bool per_user_params = true); + + /** + * RFC 9382 compatible SPAKE2 configuration + * + * If the group is not P-256, P-384, or P-521, then the group must support + * RFC 9380 hash to curve. Curves with a cofactor are not supported. + * + * If per_user_params is true, this uses the "quantum annoying" variant + * where N/M are the output of hash to curve; this requires an attacker + * with a quantum computer to perform a discrete logarithm calculator per + * PAKE, rather than just once for the default (fixed) SPAKE2 parameters. + * + * Here the shared secret a random scalar. It should have been generated + * using a memory hard function such as Argon2id. + * + * # ⚠️ Warning + * + * This interface exists primarily for testing, and is not safe for general use. + * + * @param group the elliptic curve group to operate in + * @param shared_secret an integer that is the hash of a shared secret + * @param a_identity the (optional) identity string of peer A + * @param b_identity the (optional) identity string of peer B + * @param context an optional context string (for example a protocol identifier) + * @param hash the hash function to use (SHA-512 highly recommended) + * @param per_user_params if true then per-user N/M are used + */ + SPAKE2_Parameters(const EC_Group& group, + const EC_Scalar& shared_secret, + std::span a_identity = {}, + std::span b_identity = {}, + std::span context = {}, + std::string_view hash = "SHA-512", + bool per_user_params = true); + + /** + * Return the default mapping from a shared secret (plus identifiers) to + * an elliptic curve scalar. + * + * The shared secret is hashed with Argon2id M=131072,t=3,p=1 + * + * The output is converted to a scalar by generating a bytestring of length + * equal to the scalar, plus 128 bits. This is then reduced modulo the order. + * Note this differs from the recommendation in RFC 9382 to use 64 excess bits. + */ + static EC_Scalar hash_shared_secret(const EC_Group& group, + std::string_view shared_secret, + std::span a_identity = {}, + std::span b_identity = {}, + std::span context = {}); + + const EC_Group& group() const { return m_group; } + + const EC_AffinePoint& spake2_m() const { return m_params.first; } + + const EC_AffinePoint& spake2_n() const { return m_params.second; } + + const EC_AffinePoint& spake2_our_pt(SPAKE2_PeerId whoami) const { + if(whoami == SPAKE2_PeerId::PeerA) { + return m_params.first; // M + } else { + return m_params.second; // N + } + } + + const EC_AffinePoint& spake2_their_pt(SPAKE2_PeerId whoami) const { + if(whoami == SPAKE2_PeerId::PeerA) { + return m_params.second; // N + } else { + return m_params.first; // M + } + } + + const EC_Scalar& spake2_w() const { return m_w; } + + const std::string& hash_function() const { return m_hash_fn; } + + std::span a_identity() const { return m_a_identity; } + + std::span b_identity() const { return m_b_identity; } + + private: + EC_Group m_group; + std::pair m_params; + EC_Scalar m_w; + std::string m_hash_fn; + std::vector m_a_identity; + std::vector m_b_identity; +}; + +/* +* SPAKE2 (RFC 9382) Protocol Context +* +* This implementation of SPAKE2 requires asymmetric exchange, ie that +* each party knows if it is A or B. +* +* The key confirmation step is omitted; it is assumed that further +* uses of the shared secret will confirm the key. The shared secret is +* equivalent to Hash(TT) in RFC 9382 so it is possible to implement +* RFC 9382 conformant key confirmation if necessary. +*/ +class BOTAN_PUBLIC_API(3, 7) SPAKE2_Context final { + public: + SPAKE2_Context(SPAKE2_PeerId whoami, const SPAKE2_Parameters& params, RandomNumberGenerator& rng) : + m_rng(rng), m_whoami(whoami), m_params(params) {} + + /** + * Generate a message for the peer. This can be called only once. + */ + std::vector generate_message(); + + /** + * Consume the message from the peer and return the shared secret. + * + * The context should not be used anymore after this point + */ + secure_vector process_message(std::span peer_message); + + private: + RandomNumberGenerator& m_rng; + SPAKE2_PeerId m_whoami; + SPAKE2_Parameters m_params; + std::optional, EC_Scalar>> m_our_message; +}; + +} // namespace Botan + +#endif diff --git a/src/tests/data/pake/spake2.vec b/src/tests/data/pake/spake2.vec new file mode 100644 index 0000000000..bccc2b2a3a --- /dev/null +++ b/src/tests/data/pake/spake2.vec @@ -0,0 +1,38 @@ + +# From RFC 9382 + +Group = secp256r1 +Hash = SHA-256 +AId = 736572766572 +BId = 636C69656E74 +W = 0x2ee57912099d31560b3a44b1184b9b4866e904c49d12ac5042c97dca461b1a5f +X = 0x43dd0fd7215bdcb482879fca3220c6a968e66d70b1356cac18bb26c84a78d729 +Y = 0xdcb60106f276b02606d8ef0a328c02e4b629f84f89786af5befb0bc75b6e66be +SS = 0e0672dc86f8e45565d338b0540abe6915bdf72e2b35b5c9e5663168e960a91b + +Group = secp256r1 +Hash = SHA-256 +AId = +BId = 636C69656E74 +W = 0x0548d8729f730589e579b0475a582c1608138ddf7054b73b5381c7e883e2efae +X = 0x403abbe3b1b4b9ba17e3032849759d723939a27a27b9d921c500edde18ed654b +Y = 0x903023b6598908936ea7c929bd761af6039577a9c3f9581064187c3049d87065 +SS = 642f05c473c2cd79909f9a841e2f30a70bf89b18180af97353ba198789c2b963 + +Group = secp256r1 +Hash = SHA-256 +AId = 736572766572 +BId = +W = 0x626e0cdc7b14c9db3e52a0b1b3a768c98e37852d5db30febe0497b14eae8c254 +X = 0x07adb3db6bc623d3399726bfdbfd3d15a58ea776ab8a308b00392621291f9633 +Y = 0xb6a4fc8dbb629d4ba51d6f91ed1532cf87adec98f25dd153a75accafafedec16 +SS = 005184ff460da2ce59062c87733c299c3521297d736598fc0a1127600efa1afb + +Group = secp256r1 +Hash = SHA-256 +AId = +BId = +W = 0x7bf46c454b4c1b25799527d896508afd5fc62ef4ec59db1efb49113063d70cca +X = 0x8cef65df64bb2d0f83540c53632de911b5b24b3eab6cc74a97609fd659e95473 +Y = 0xd7a66f64074a84652d8d623a92e20c9675c61cb5b4f6a0063e4648a2fdc02d53 +SS = 0xfc6374762ba5cf11f4b2caa08b2cd1b9907ae0e26e8d6234318d91583cd74c86 diff --git a/src/tests/data/pake/spake2_rt.vec b/src/tests/data/pake/spake2_rt.vec new file mode 100644 index 0000000000..a1220545fe --- /dev/null +++ b/src/tests/data/pake/spake2_rt.vec @@ -0,0 +1,36 @@ + +Group = secp256r1 +Secret = squirrel +Hash = SHA-256 +AId = F00F +BId = ABE5 + +Group = secp256r1 +Secret = squirrel +Hash = SHA-384 +AId = F00F +BId = ABE5 + +Group = secp256r1 +Secret = squirrel +Hash = SHA-512 +AId = F00F +BId = ABE5 + +Group = secp384r1 +Secret = squirrel +Hash = SHA-384 +AId = F00F +BId = ABE5 + +Group = secp384r1 +Secret = squirrel +Hash = SHA-512 +AId = F00F +BId = ABE5 + +Group = secp521r1 +Secret = squirrel +Hash = SHA-512 +AId = F00F +BId = ABE5 diff --git a/src/tests/test_spake2.cpp b/src/tests/test_spake2.cpp new file mode 100644 index 0000000000..5dbeb1e62c --- /dev/null +++ b/src/tests/test_spake2.cpp @@ -0,0 +1,112 @@ +/* +* (C) 2024 Jack Lloyd +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#include "tests.h" + +#if defined(BOTAN_HAS_PAKE_SPAKE2) + #include "test_rng.h" + #include +#endif + +namespace Botan_Tests { + +namespace { + +#if defined(BOTAN_HAS_PAKE_SPAKE2) + +class SPAKE2_KAT_Tests final : public Text_Based_Test { + public: + SPAKE2_KAT_Tests() : Text_Based_Test("pake/spake2.vec", "Group,W,X,Y,Hash,AId,BId,SS") {} + + Test::Result run_one_test(const std::string& /*header*/, const VarMap& vars) override { + Test::Result result("SPAKE2 KAT"); + + const auto group = Botan::EC_Group::from_name(vars.get_req_str("Group")); + const std::string hash_fn = vars.get_req_str("Hash"); + const std::vector a_id = vars.get_req_bin("AId"); + const std::vector b_id = vars.get_req_bin("BId"); + const std::vector exp_ss = vars.get_req_bin("SS"); + + const Botan::EC_Scalar w(group, vars.get_req_bin("W")); + + Botan::SPAKE2_Parameters params(group, w, a_id, b_id, {}, hash_fn, false); + + Fixed_Output_RNG x_rng(rng()); + x_rng.add_entropy(vars.get_req_bin("X")); + Botan::SPAKE2_Context a_ctx(Botan::SPAKE2_PeerId::PeerA, params, x_rng); + const auto a_msg = a_ctx.generate_message(); + + Fixed_Output_RNG y_rng(rng()); + y_rng.add_entropy(vars.get_req_bin("Y")); + Botan::SPAKE2_Context b_ctx(Botan::SPAKE2_PeerId::PeerB, params, y_rng); + const auto b_msg = b_ctx.generate_message(); + + const auto a_ss = a_ctx.process_message(b_msg); + result.test_eq("Shared secret A matches", a_ss, exp_ss); + + const auto b_ss = b_ctx.process_message(a_msg); + result.test_eq("Shared secret B matches", b_ss, exp_ss); + + return result; + } +}; + +BOTAN_REGISTER_TEST("pake", "spake2_kat", SPAKE2_KAT_Tests); + +class SPAKE2_RT_Tests final : public Text_Based_Test { + public: + SPAKE2_RT_Tests() : Text_Based_Test("pake/spake2_rt.vec", "Group,Secret,Hash,AId,BId") {} + + Test::Result run_one_test(const std::string& /*header*/, const VarMap& vars) override { + Test::Result result("SPAKE2 round trip"); + + const auto group = Botan::EC_Group::from_name(vars.get_req_str("Group")); + const std::string hash_fn = vars.get_req_str("Hash"); + const std::vector a_id = vars.get_req_bin("AId"); + const std::vector b_id = vars.get_req_bin("BId"); + const std::string secret = vars.get_req_str("Secret"); + + const bool h2c_supported = [&]() { + try { + Botan::EC_AffinePoint::hash_to_curve_nu(group, hash_fn, {}, {}); + return true; + } catch(Botan::Not_Implemented&) { + return false; + } + }(); + + // Avoid doing Argon2 twice for each test + const auto w = Botan::SPAKE2_Parameters::hash_shared_secret(group, secret, a_id, b_id, {}); + + for(bool per_user_params : {true, false}) { + if(per_user_params && !h2c_supported) { + continue; + } + + Botan::SPAKE2_Parameters params(group, w, a_id, b_id, {}, hash_fn, per_user_params); + + Botan::SPAKE2_Context a_ctx(Botan::SPAKE2_PeerId::PeerA, params, rng()); + const auto a_msg = a_ctx.generate_message(); + + Botan::SPAKE2_Context b_ctx(Botan::SPAKE2_PeerId::PeerB, params, rng()); + const auto b_msg = b_ctx.generate_message(); + + const auto a_ss = a_ctx.process_message(b_msg); + const auto b_ss = b_ctx.process_message(a_msg); + + result.test_eq("Peers produced the same shared secret", a_ss, b_ss); + } + + return result; + } +}; + +BOTAN_REGISTER_TEST("pake", "spake2_rt", SPAKE2_RT_Tests); +#endif + +} // namespace + +} // namespace Botan_Tests