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/N group 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, and rfc9383_p521_sha512 return 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/N elements 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/N elements once.

Registration

class SPAKE2p::ProverSecret

The secret values (w0 and w1 in RFC 9383) which the prover derives from the password.

static ProverSecret from_password(const SystemParameters &params, 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 (w0 and L = w1*P) which is provided to the verifier during registration.

static ProverSecret deserialize(const SystemParameters &params, 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.

class SPAKE2p::RegistrationRecord

The information (w0 and L in RFC 9383) which the verifier stores in order to later authenticate the prover.

static RegistrationRecord from_password(const SystemParameters &params, 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_password followed by registration_record.

static RegistrationRecord deserialize(const SystemParameters &params, 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.

Online Authentication

class SPAKE2p::ProverContext
ProverContext(const SystemParameters &params, 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. Throws Decoding_Error if the message is malformed, or Invalid_Authentication_Tag if the key confirmation is wrong (typically meaning the passwords do not match).

secure_vector<uint8_t> shared_secret() const

Returns the shared secret. May be called only after process_message has succeeded.

class SPAKE2p::VerifierContext
VerifierContext(const SystemParameters &params, 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 ProverContext above 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. Throws Decoding_Error if the key share is malformed.

void verify_confirmation(std::span<const uint8_t> confirmation)

Checks the prover’s key confirmation (confirmP). Throws Invalid_Authentication_Tag if the confirmation is wrong, meaning the prover does not know the password.

void skip_confirmation()

Can be called after process_message, in place of verify_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, use verify_confirmation.

secure_vector<uint8_t> shared_secret() const

Returns the shared secret. May be called only after verify_confirmation has succeeded, or after skip_confirmation.

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