diff --git a/doc/api_ref/contents.rst b/doc/api_ref/contents.rst index a75bd8f46c3..96e0901103a 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 00000000000..43d6c72daa7 --- /dev/null +++ b/doc/api_ref/spake2.rst @@ -0,0 +1,89 @@ +SPAKE2 Password Authenticated Key Exchange +============================================= + +.. versionadded:: 3.10.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) + +Code Example: SPAKE2 PAKE +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The example below demonstrates using SPAKE2 to perform a password authenticated +key exchange. + +.. literalinclude:: /../src/examples/spake2.cpp + :language: cpp diff --git a/src/examples/spake2.cpp b/src/examples/spake2.cpp new file mode 100644 index 00000000000..91435df2f41 --- /dev/null +++ b/src/examples/spake2.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +int main() { + const auto as_span = [](std::string_view s) -> std::span { + return {reinterpret_cast(s.data()), s.size()}; + }; + + // Peers A and B have to agree on all of those, including + // the association of A and B to the individual identities. + const auto a_identity = as_span("Jack"); + const auto b_identity = as_span("René"); + const auto context = as_span("botan example"); + const std::string_view shared_secret = "top!secret"; + const std::string_view hash = "SHA-256"; + const auto group = Botan::EC_Group::from_name("secp256r1"); + + const auto params = Botan::SPAKE2::Parameters(group, shared_secret, a_identity, b_identity, context, hash); + auto rng = Botan::AutoSeeded_RNG(); + + Botan::SPAKE2::Context jack_ctx(Botan::SPAKE2::PeerId::PeerA, params, rng); + // First Jack creates a message and sends it to René + const auto jacks_message = jack_ctx.generate_message(); + + Botan::SPAKE2::Context rene_ctx(Botan::SPAKE2::PeerId::PeerB, params, rng); + // Then René receives the messsage and creates his message to Jack + const auto renes_message = rene_ctx.generate_message(); + + // Already René knows what the shared secret will be + const auto shared_secret_rene = rene_ctx.process_message(jacks_message); + + // Eventually Jack receives the reply and calculates the shared secret + const auto shared_secret_jack = jack_ctx.process_message(renes_message); + + if(shared_secret_jack == shared_secret_rene) { + std::cout << "Key exchange worked\n"; + return 0; + } else { + std::cerr << "Something went wrong\n"; + return 1; + } +} diff --git a/src/lib/pake/spake2/info.txt b/src/lib/pake/spake2/info.txt new file mode 100644 index 00000000000..4b286f36172 --- /dev/null +++ b/src/lib/pake/spake2/info.txt @@ -0,0 +1,21 @@ + +PAKE_SPAKE2 -> 20240821 + + + +name -> "SPAKE2" +brief -> "SPAKE2 PAKE" + + + +spake2.h + + + +argon2 +ec_group +hkdf +hmac +sha2_32 +sha2_64 + diff --git a/src/lib/pake/spake2/spake2.cpp b/src/lib/pake/spake2/spake2.cpp new file mode 100644 index 00000000000..17e3a780a88 --- /dev/null +++ b/src/lib/pake/spake2/spake2.cpp @@ -0,0 +1,217 @@ +/* +* (C) 2024,2025 Jack Lloyd +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#include + +#include +#include +#include +#include +#include +#include + +namespace Botan::SPAKE2 { + +namespace { + +const EC_AffinePoint& spake2_our_pt(const Parameters& params, PeerId whoami) { + return (whoami == PeerId::PeerA) ? params.spake2_m() : params.spake2_n(); +} + +const EC_AffinePoint& spake2_their_pt(const Parameters& params, PeerId whoami) { + return (whoami == PeerId::PeerA) ? params.spake2_n() : params.spake2_m(); +} + +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 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); +} + +Parameters::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) : + Parameters(group, + 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_fn, + 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); + + auto m = EC_AffinePoint::hash_to_curve_ro(group, hash_fn, input, cstr_as_span_of_bytes("SPAKE2 M")); + auto n = EC_AffinePoint::hash_to_curve_ro(group, hash_fn, input, cstr_as_span_of_bytes("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}) { // secp256r1 + 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}) { // secp384r1 + 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}) { // secp521r1 + 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 + +Parameters::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_fn, + bool per_user_params) : + m_group(group), + m_params(spake2_params(m_group, hash_fn, a_identity, b_identity, context, per_user_params)), + m_w(shared_secret), + m_hash_fn(hash_fn), + m_a_identity(a_identity.begin(), a_identity.end()), + m_b_identity(b_identity.begin(), b_identity.end()), + m_context(context.begin(), context.end()) {} + +std::vector 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 = spake2_our_pt(m_params, m_whoami); + const auto& g = EC_AffinePoint::generator(m_params.group()); + // Compute g*x + w*{M,N} + + if(auto pt = EC_AffinePoint::mul_px_qy(g, eph_key, N_or_M, m_params.spake2_w(), m_rng)) { + auto msg = pt->serialize_uncompressed(); + m_our_message = std::make_pair(msg, eph_key); + return msg; + } else { + throw Internal_Error("Computed the identity element during SPAKE2 key exchange"); + } +} + +secure_vector 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 + const EC_AffinePoint peer_pt(m_params.group(), peer_message); + + const auto& [our_pt, eph_key] = m_our_message.value(); + const auto& N_or_M = spake2_their_pt(m_params, 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); + + if(!K) { + throw Internal_Error("Computed identity element during SPAKE2 key exchange"); + } + + auto hash = HashFunction::create_or_throw(m_params.hash_function()); + + // Now we compute Hash(TT) as described in RFC 9382 section 3.3 and section 4 + + auto append_to_hash_with_le64 = [&](std::span data) { + hash->update(store_le(static_cast(data.size()))); + hash->update(data); + }; + + // The context string is an extension to SPAKE2 (it is included in SPAKE2+) + // To maintain RFC 9382 compatability we omit it if empty + if(!m_params.context().empty()) { + append_to_hash_with_le64(m_params.context()); + } + + 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 == 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->final(); +} + +} // namespace Botan::SPAKE2 diff --git a/src/lib/pake/spake2/spake2.h b/src/lib/pake/spake2/spake2.h new file mode 100644 index 00000000000..e6190129d7c --- /dev/null +++ b/src/lib/pake/spake2/spake2.h @@ -0,0 +1,193 @@ +/* +* (C) 2024,2025 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; + +} + +namespace Botan::SPAKE2 { + +/** +* Identifies which peer we are in the protocol +*/ +enum class PeerId : uint8_t { + 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, 10) 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 calculation 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 + */ + 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 calculation 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 is potentially unsafe, depending upon how shared_secret is + * derived from the password. The scalar value must be uniform random, and preferably + * computed in a way such that testing values is expensive for an attacker. It exists + * to support testing, as well as applications which require using a different + * password hashing scheme than the default one implemented by `hash_shared_secret` + * + * @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 + */ + 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_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; } + + std::span context() const { return m_context; } + + 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; + std::vector m_context; +}; + +/* +* 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, 10) Context final { + public: + Context(PeerId whoami, const 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; + PeerId m_whoami; + Parameters m_params; + std::optional, EC_Scalar>> m_our_message; +}; + +} // namespace Botan::SPAKE2 + +#endif diff --git a/src/tests/data/pake/spake2.vec b/src/tests/data/pake/spake2.vec new file mode 100644 index 00000000000..bccc2b2a3a7 --- /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/test_spake2.cpp b/src/tests/test_spake2.cpp new file mode 100644 index 00000000000..89c74b36743 --- /dev/null +++ b/src/tests/test_spake2.cpp @@ -0,0 +1,182 @@ +/* +* (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 + #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 Test { + public: + std::vector run() override { + std::vector results; + + const std::vector groups = {"secp256r1", "secp384r1", "secp521r1"}; + const std::vector hash_fns = {"SHA-256", "SHA-384", "SHA-512"}; + + const std::vector a_id = {0xF0, 0x0F, 0xEE}; + const std::vector b_id = {0xAB, 0xE0}; + const std::string secret = "squirrel"; + const auto context = rng().random_vec(32); + const auto bad_context = rng().random_vec(32); + + for(const auto& group_name : groups) { + Test::Result result("SPAKE2 round trip " + group_name); + result.start_timer(); + + if(!Botan::EC_Group::supports_named_group(group_name)) { + continue; + } + + const auto group = Botan::EC_Group::from_name(group_name); + + for(const auto& hash_fn : hash_fns) { + const bool h2c_supported = [&]() { + try { + Botan::EC_AffinePoint::hash_to_curve_nu(group, hash_fn, {}, ""); + return true; + } catch(Botan::Not_Implemented&) { + return false; + } + }(); + + const auto w = test_hash_shared_secret(group, secret, a_id, b_id, context); + const auto bad_w = w + Botan::EC_Scalar::one(group); + + for(bool per_user_params : {true, false}) { + if(per_user_params && !h2c_supported) { + continue; + } + + result.confirm( + "Same shared key in normal operation", + spake2_test_run(rng(), group, hash_fn, per_user_params, a_id, w, context, b_id, w, context)); + + // clang-format off + result.confirm("Different shared key if secret is different", + !spake2_test_run(rng(), group, hash_fn, per_user_params, a_id, w, context, b_id, bad_w, context)); + + result.confirm("Different shared key if context is different", + !spake2_test_run(rng(), group, hash_fn, per_user_params, a_id, w, context, b_id, w, bad_context)); + // clang-format on + } + } + + result.end_timer(); + results.push_back(result); + } + + return results; + } + + private: + static bool spake2_test_run(Botan::RandomNumberGenerator& rng, + const Botan::EC_Group& group, + std::string_view hash_fn, + bool per_user_params, + std::span a_id, + const Botan::EC_Scalar& a_secret, + std::span a_context, + std::span b_id, + const Botan::EC_Scalar& b_secret, + std::span b_context) { + Botan::SPAKE2::Parameters a_params(group, a_secret, a_id, b_id, a_context, hash_fn, per_user_params); + Botan::SPAKE2::Context a_ctx(Botan::SPAKE2::PeerId::PeerA, a_params, rng); + const auto a_msg = a_ctx.generate_message(); + + Botan::SPAKE2::Parameters b_params(group, b_secret, a_id, b_id, b_context, hash_fn, per_user_params); + Botan::SPAKE2::Context b_ctx(Botan::SPAKE2::PeerId::PeerB, b_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); + + return (a_ss == b_ss); + } + + static Botan::EC_Scalar test_hash_shared_secret(const Botan::EC_Group& group, + std::string_view secret, + std::span a_id, + std::span b_id, + std::span context) { + /*** + * WARNING do not copy this into production code + * + * This is just a standin for SPAKE2::Parameters::hash_shared_secret that is + * fast, to avoid repeated Argon2 calculations in the test + ***/ + + const std::string_view hash_fn = "SHA-512"; + + auto hash = Botan::HashFunction::create_or_throw(hash_fn); + + auto a_h = hash->process(a_id); + auto b_h = hash->process(b_id); + auto c_h = hash->process(context); + + hash->update(a_h); + hash->update(b_h); + hash->update(c_h); + hash->update(secret); + + return Botan::EC_Scalar::hash(group, hash_fn, hash->final(), {}); + } +}; + +BOTAN_REGISTER_TEST("pake", "spake2_rt", SPAKE2_RT_Tests); +#endif + +} // namespace + +} // namespace Botan_Tests