SPAKE2+ Password Authenticated Key Exchange¶
Added in version 3.13.0.
An implementation of the SPAKE2+ password authenticated key exchange, compatible with RFC 9383, is included.
SPAKE2+ allows two peers who share a (possibly low entropy) secret such as a password to agree on a strong shared secret key. An attacker who observes or modifies the exchange learns nothing about the password (beyond excluding a single guess per protocol execution) or the session key.
SPAKE2+ is an augmented PAKE, meaning the two sides are asymmetric. The prover (typically a client) knows the password itself, while the verifier (typically a server) stores only a registration record derived from the password. An attacker who steals the registration record cannot impersonate the prover without first performing a successful dictionary attack against the record.
The protocol consists of two phases:
Registration (performed once, over some trusted channel): the prover derives its secret values from the password, and provides the resulting registration record to the verifier.
Online authentication (performed per session): the two sides exchange key shares and key confirmation messages, resulting in a mutually authenticated shared secret.
The online message flow is:
Prover Verifier
ProverContext::generate_message
--- shareP -->
VerifierContext::process_message
<-- shareV || confirmV ---
ProverContext::process_message
--- confirmP -->
VerifierContext::verify_confirmation
After the final step both sides call shared_secret to obtain the session key
(K_shared in RFC 9383).
Some protocols which embed SPAKE2+ perform the prover’s key confirmation themselves
rather than exchanging confirmP. For example in the proposed PAKE extension for TLS
1.3 (draft-bmw-tls-pake13), the server acts as the verifier, feeds the shared secret
into the TLS key schedule immediately after processing the prover’s key share, and the
TLS handshake takes the place of confirmP. To support such protocols, the verifier
may call skip_confirmation in place of verify_confirmation; see below.
System Parameters¶
-
class SPAKE2p::SystemParameters¶
Specifies the ciphersuite in use, namely the elliptic curve group, the SPAKE2+
M/Ngroup elements, and the hash function.SPAKE2+ 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 SPAKE2+ suites described in RFC 9383 Table 1 are not supported.
Static factory functions
rfc9383_p256_sha256,rfc9383_p256_sha512,rfc9383_p384_sha256,rfc9383_p384_sha512, andrfc9383_p521_sha512return the standard ciphersuites from RFC 9383.-
static SystemParameters custom(const EC_Group &group, std::span<const uint8_t> seed, std::string_view hash_fn)¶
Creates custom system parameters for an arbitrary group, deriving the
M/Nelements from the seed using hash to curve (which not all groups support). Both peers must use the same seed.If the seed includes the identities of the participants, this additionally makes the scheme “quantum annoying”: an attacker with a discrete logarithm oracle must compute a new discrete logarithm for each (prover, verifier) pair they wish to attack, rather than being able to attack any user after computing the discrete logarithms of the fixed
M/Nelements once.
-
static SystemParameters custom(const EC_Group &group, std::span<const uint8_t> seed, std::string_view hash_fn)¶
Registration¶
-
class SPAKE2p::ProverSecret¶
The secret values (
w0andw1in RFC 9383) which the prover derives from the password.-
static ProverSecret from_password(const SystemParameters ¶ms, std::string_view password, std::span<const uint8_t> prover_id, std::span<const uint8_t> verifier_id, std::span<const uint8_t> salt)¶
Derives the prover secret from a password using Argon2id, with the memory-constrained parameters recommended in RFC 9106 (m=64 MiB, t=3, p=4).
Following RFC 9383, the Argon2id passphrase input is the concatenation
len(pw) || pw || len(idProver) || idProver || len(idVerifier) || idVerifier, where each length is an 8-byte little-endian count of bytes; the salt is provided to Argon2id directly. The output is split into two halves, each of which is reduced modulo the group order.The identities and the salt may be empty; if a salt is available it should be used, since it prevents precomputed dictionary attacks.
-
static ProverSecret from_prehashed(EC_Scalar w0, EC_Scalar w1)¶
Creates a prover secret from already derived scalars, for applications which require a password hashing scheme other than the default one. The scalars must be derived from the password in a way that produces uniformly random values modulo the group order; see RFC 9383 section 3.2 for the requirements.
-
RegistrationRecord registration_record(RandomNumberGenerator &rng) const¶
Computes the registration record (
w0andL = w1*P) which is provided to the verifier during registration.
-
static ProverSecret deserialize(const SystemParameters ¶ms, std::span<const uint8_t> secret)¶
-
secure_vector<uint8_t> serialize() const¶
Serialization, if the prover wishes to store the derived secret rather than rederiving it from the password each time. The serialized secret is password equivalent, so it should be encrypted if stored persistently.
-
static ProverSecret from_password(const SystemParameters ¶ms, std::string_view password, std::span<const uint8_t> prover_id, std::span<const uint8_t> verifier_id, std::span<const uint8_t> salt)¶
-
class SPAKE2p::RegistrationRecord¶
The information (
w0andLin RFC 9383) which the verifier stores in order to later authenticate the prover.-
static RegistrationRecord from_password(const SystemParameters ¶ms, std::string_view password, std::span<const uint8_t> prover_id, std::span<const uint8_t> verifier_id, std::span<const uint8_t> salt, RandomNumberGenerator &rng)¶
Performs password registration in a single step, equivalent to
ProverSecret::from_passwordfollowed byregistration_record.
-
static RegistrationRecord deserialize(const SystemParameters ¶ms, std::span<const uint8_t> record)¶
-
secure_vector<uint8_t> serialize() const¶
Serialization, for storage by the verifier. While the record does not allow direct impersonation of the prover, it does allow offline password guessing attacks, so it should be encrypted if possible.
-
static RegistrationRecord from_password(const SystemParameters ¶ms, std::string_view password, std::span<const uint8_t> prover_id, std::span<const uint8_t> verifier_id, std::span<const uint8_t> salt, RandomNumberGenerator &rng)¶
Online Authentication¶
-
class SPAKE2p::ProverContext¶
-
ProverContext(const SystemParameters ¶ms, const ProverSecret &secret, std::span<const uint8_t> prover_id, std::span<const uint8_t> verifier_id, std::span<const uint8_t> context = {})¶
Prepares an execution of the protocol. The identities and the context must be agreed upon by both peers, and the identities must match the values used during registration. Even if there is no natural identity available, using fixed labels such as “client” and “server” is preferable to leaving the identities empty. The context should identify the application and protocol version; it may be empty.
-
std::vector<uint8_t> generate_message(RandomNumberGenerator &rng)¶
Returns the prover’s key share (
shareP), which is sent to the verifier. Can be called only once.
-
std::vector<uint8_t> process_message(std::span<const uint8_t> peer_message, RandomNumberGenerator &rng)¶
Consumes the verifier’s response (
shareV || confirmV) and returns the prover’s key confirmation (confirmP), which is sent to the verifier. ThrowsDecoding_Errorif the message is malformed, orInvalid_Authentication_Tagif the key confirmation is wrong (typically meaning the passwords do not match).
Returns the shared secret. May be called only after
process_messagehas succeeded.
-
ProverContext(const SystemParameters ¶ms, const ProverSecret &secret, std::span<const uint8_t> prover_id, std::span<const uint8_t> verifier_id, std::span<const uint8_t> context = {})¶
-
class SPAKE2p::VerifierContext¶
-
VerifierContext(const SystemParameters ¶ms, const RegistrationRecord &record, std::span<const uint8_t> prover_id, std::span<const uint8_t> verifier_id, std::span<const uint8_t> context = {})¶
Prepares an execution of the protocol; see
ProverContextabove for the requirements on the identities and context.
-
std::vector<uint8_t> process_message(std::span<const uint8_t> peer_message, RandomNumberGenerator &rng)¶
Consumes the prover’s key share (
shareP) and returns the verifier’s response (shareV || confirmV), which is sent to the prover. Can be called only once. ThrowsDecoding_Errorif the key share is malformed.
-
void verify_confirmation(std::span<const uint8_t> confirmation)¶
Checks the prover’s key confirmation (
confirmP). ThrowsInvalid_Authentication_Tagif the confirmation is wrong, meaning the prover does not know the password.
-
void skip_confirmation()¶
Can be called after
process_message, in place ofverify_confirmation, to allow extracting the shared secret without having checked the prover’s key confirmation.Warning
After calling this, nothing is known about the peer; only a prover which knows the password can compute the same shared secret, but no evidence of this has been received. It is intended solely for protocols which embed SPAKE2+ and perform the prover’s key confirmation themselves, such as the proposed PAKE extension for TLS 1.3, where the TLS handshake takes the place of
confirmP. Anywhere else, useverify_confirmation.
Returns the shared secret. May be called only after
verify_confirmationhas succeeded, or afterskip_confirmation.
-
VerifierContext(const SystemParameters ¶ms, const RegistrationRecord &record, std::span<const uint8_t> prover_id, std::span<const uint8_t> verifier_id, std::span<const uint8_t> context = {})¶
Code Example: SPAKE2+ PAKE¶
The example below demonstrates using SPAKE2+ to perform a password authenticated key exchange.
#include <botan/auto_rng.h>
#include <botan/spake2p.h>
#include <iostream>
int main() {
const auto as_span = [](std::string_view s) -> std::span<const uint8_t> {
return {reinterpret_cast<const uint8_t*>(s.data()), s.size()};
};
// Both peers must agree on the system parameters, the identities, and
// the context string
const auto params = Botan::SPAKE2p::SystemParameters::rfc9383_p256_sha256();
const auto prover_id = as_span("client");
const auto verifier_id = as_span("server");
const auto context = as_span("botan spake2+ example");
const std::string_view password = "top!secret";
Botan::AutoSeeded_RNG rng;
// Registration, performed once: the prover derives its secret from the
// password, and gives the registration record (along with the salt) to
// the verifier. The verifier stores the record; it never sees the
// password itself.
const auto salt = rng.random_vec(16);
const auto secret = Botan::SPAKE2p::ProverSecret::from_password(params, password, prover_id, verifier_id, salt);
const auto record = secret.registration_record(rng);
// The online phase, performed for each session:
Botan::SPAKE2p::ProverContext prover(params, secret, prover_id, verifier_id, context);
Botan::SPAKE2p::VerifierContext verifier(params, record, prover_id, verifier_id, context);
// First the prover generates its key share and sends it to the verifier
const auto prover_share = prover.generate_message(rng);
// The verifier consumes the prover's share and responds with its own key
// share plus a key confirmation message
const auto verifier_msg = verifier.process_message(prover_share, rng);
// The prover consumes the verifier's message, checking the verifier's key
// confirmation (an exception is thrown if it is invalid), and responds
// with its own key confirmation
const auto prover_confirm = prover.process_message(verifier_msg, rng);
// Finally the verifier checks the prover's key confirmation
verifier.verify_confirmation(prover_confirm);
// Now both sides share a secret key
if(prover.shared_secret() == verifier.shared_secret()) {
std::cout << "Key exchange worked\n";
return 0;
} else {
// This should never happen, as long as verify_confirmation succeeded
std::cout << "Something went wrong\n";
return 1;
}
}