Expand description
§spake2plus
SPAKE2+ (RFC 9383) in Rust: the augmented password-authenticated key
exchange, with both roles, no_std and without alloc.
A client (the prover, which knows the password) and a server (the verifier, which stores only a record derived from it) agree on a shared key over an untrusted channel, and each proves to the other that it holds the password or its record. Nothing on the wire lets an eavesdropper test password guesses, an active attacker gets one guess per run, and someone who steals the server’s records must still guess each password before posing as its client.
Use it where a password (or a PIN, or a setup code) is the only secret the two
sides share and the server should not hold the password itself: device
pairing and logins to local devices; TP-Link’s TPAP, for one, is built on
it. If both sides hold the same secret, the
symmetric SPAKE2 (RFC 9382, the spake2 crate) is enough.
§Example
use spake2plus::{Context, P256Sha256, Prover, ProverSecret, Verifier};
fn main() -> Result<(), spake2plus::Error> {
// Registration: the prover's secret from its password hash's output (see
// below), and the record the verifier stores.
let pbkdf_output = [7; 80]; // stands in for the password hash's output
let secret = ProverSecret::<P256Sha256>::from_pbkdf_output(&pbkdf_output)?;
let record = secret.verifier_record();
// Both sides bind the same context and identities into the run.
let context = Context {
context: b"MyApp v1",
prover_id: b"alice",
verifier_id: b"server",
};
let mut rng = rand::rng();
// Prover -> verifier: shareP.
let (prover, share_p) = Prover::start(&secret, &context, &mut rng);
// Verifier -> prover: shareV and confirmV.
let (verifier, share_v) = Verifier::start(&record, &context, &mut rng);
let (verifier, confirm_v) = verifier.receive(&share_p)?;
// The prover checks confirmV, then sends confirmP.
let (confirm_p, prover_key) = prover.receive(&share_v)?.finish(&confirm_v)?;
// The verifier checks confirmP.
let verifier_key = verifier.finish(&confirm_p)?;
assert_eq!(prover_key.as_bytes(), verifier_key.as_bytes());
Ok(())
}Every message has as_bytes() to send and from_bytes() to parse what the
peer sent. Each step consumes its session, so a run cannot be repeated or
confirmed twice, and the SharedKey exists only once the peer’s
confirmation has been checked. Derive the application’s keys from it, for
example with HKDF.
§Registration: the password hash is yours
RFC 9383 leaves the password hash to the application. Run a slow, salted password hash (Argon2id or scrypt are the RFC’s examples) over
len(pw) || pw || len(idProver) || idProver || len(idVerifier) || idVerifier(each len eight bytes, little-endian), and pass its output, at least 80
bytes for P-256, to ProverSecret::from_pbkdf_output. The crate
documentation shows it with PBKDF2. The verifier stores
secret.verifier_record().to_bytes() and never the password or the
ProverSecret.
§Confirming first
RFC 9383 has the prover check the verifier’s confirmation before sending its
own. Some protocols (TP-Link’s TPAP among them) send the prover’s
confirmation first; ProverConfirming::confirm_first() does that. It lets an
impostor posing as the verifier test password guesses offline against the
prover’s confirmation, so use it only when the protocol forces that order,
and with a slow password hash.
§Suites
| Suite | Group | Hash, KDF, MAC |
|---|---|---|
P256Sha256 | P-256 | SHA-256, HKDF-SHA256, HMAC-SHA256 |
P256Sha512 | P-256 | SHA-512, HKDF-SHA512, HMAC-SHA512 |
Both pass every value of RFC 9383’s test vectors for them, in both roles. The
Suite trait is sealed, so the RFC’s other suites can be added without
breaking callers.
§Security
- No audit. This code has not been reviewed by a third party. It follows the RFC and is tested against its vectors, but treat it accordingly.
- Constant time. Scalar multiplication is the
p256crate’s, which is constant time, and confirmations are compared in constant time (subtle). Not constant time, by design: parsing and validating shares, records and lengths, which only handle public data, and the check that a share does not cancel the password’s mask, whose outcome ends the run anyway. - Validation. A share must be an uncompressed SEC1 point on the curve and
not the identity, or it does not parse. A share equal to the password’s
mask (
w0*Nfrom a verifier,w0*Mfrom a prover) is refused, since it would leave nothing secret in the key, and so is a run whose own random scalar is zero. Registration refuses a zerow0orw1. - Zeroization. The prover’s secret, the verifier’s record, every session
and the shared key are zeroized on drop (
zeroize), and so are the hash and HMAC states (thezeroizefeatures ofsha2andhmac). Copies the compiler makes when values move, and bytes the caller copies out, are beyond its reach. - Randomness. The ephemeral scalars come from the
CryptoRngthe caller passes (spake2plus::rand_corere-exports the version this crate takes), so it must be a cryptographically secure one, such asrand::rng().
§Minimum supported Rust
1.85 (rust-version in Cargo.toml).
§Contributing
bin/setup installs the pinned tools through mise (mise.toml, with Rust
pinned in rust-toolchain.toml), the pinned toolchain’s no_std target, and,
through rustup, the oldest supported Rust that rust-version in
Cargo.toml names. Then:
| Command | What it does |
|---|---|
bin/check | The read-only gate: guards, shellcheck, rustfmt, clippy, rustdoc, the oldest supported Rust, unused dependencies, licenses. |
bin/test | Every test once, then the canaries that prove each check can fail. |
bin/fix | Safe lint fixes and formatting. |
cargo deny check advisories | Known vulnerabilities in the dependencies, from an outside database, so not part of the gate. |
The fuzz target parses every kind of message and runs the steps that take them. It needs cargo-fuzz 0.13.2 and a nightly toolchain:
cargo install cargo-fuzz --version 0.13.2 --locked
rustup toolchain install nightly
cargo +nightly fuzz run parse_messages§License
MIT: see LICENSE.
Re-exports§
Structs§
- ConfirmP
confirmP, the prover’s key confirmation:MAC(K_confirmP, shareV).- ConfirmV
confirmV, the verifier’s key confirmation:MAC(K_confirmV, shareP).- Context
- What both sides bind into the transcript besides the password: an application-specific context and the two identities, each empty when absent.
- Prover
- The prover after sending
shareP, waiting forshareV. - Prover
Confirmed First - The prover that sent
confirmPfirst, waiting forconfirmV; seeProverConfirming::confirm_first. - Prover
Confirming - The prover holding both confirmations, waiting for
confirmV. - Prover
Secret - The prover’s secret,
w0andw1, derived from the password. - ShareP
shareP, the prover’s share:X = x*P + w0*M.- ShareV
shareV, the verifier’s share:Y = y*P + w0*N.- Shared
Key K_shared, the key both sides hold once the run is confirmed.- Verifier
- The verifier with its share,
shareV, waiting forshareP. - Verifier
Confirming - The verifier that has sent
confirmV, or holds it ready, waiting forconfirmP. - Verifier
Record - The verifier’s registration record:
w0andL = w1*P.
Enums§
- Error
- Why a message, a record or a PBKDF output was refused, or a run aborted.
- P256
Sha256 - P-256 with SHA-256, HKDF-SHA256 and HMAC-SHA256.
- P256
Sha512 - P-256 with SHA-512, HKDF-SHA512 and HMAC-SHA512.