Skip to main content
Site logo

Léon Zhang

Software Engineer

Cryptography

OPAQUE Password Authentication: How It Actually Works

A concrete walkthrough of OPAQUE's OPRF, credential envelope, 3DH login exchange, breach resistance, implementation boundaries, and unavoidable limits.

Sep 17, 202514 min readLéon Zhang

OPAQUE Password Authentication: How It Actually Works

OPAQUE is an augmented Password-Authenticated Key Exchange (aPAKE). The password stays inside the client while both parties authenticate and establish a shared session key. Password entropy determines guessing resistance; TLS, rate limiting, MFA, account recovery, and passkeys cover adjacent risks.

This article follows the actual objects and message flow in RFC 9807, then draws the security boundary precisely.

What OPAQUE Changes

With ordinary password-over-TLS authentication, the application server receives the plaintext password and checks it against an Argon2id, scrypt, or bcrypt verifier. Good password hashing protects the database. During login, a compromised server, TLS terminator, log statement, or tracing hook can capture the password.

With OPAQUE, the client and server instead exchange blinded OPRF values, key shares, and transcript MACs. The password stays client-side.

PropertyPassword over TLSOPAQUE
Password location during loginClient and serverClient
Server-side password-derived recordStoredStored
Guess verification from a passive transcriptUnavailable while TLS holdsUnavailable
Guess verification from a stolen record aloneAvailableRequires the OPRF secret
Guess verification after full compromiseAvailableAvailable
Mutually authenticated session keyRequires a separate mechanismProtocol output

OPAQUE is an RFC published through the IRTF's Crypto Forum Research Group. RFC 9807 has Informational status on the IRTF stream, outside the Internet Standards Track. That distinction matters when describing its maturity.

The Right Mental Model

An SSH signature demonstrates possession of a private key. OPAQUE belongs to a different family: two live peers run an interactive OPRF and authenticated key exchange. The client receives a keyed function output; the server sees only a randomized group element. The OPRF construction used by OPAQUE comes from RFC 9497.

The State on Each Side

The client needs the password at registration and login. It regenerates the private key during each run, so the password is the only portable credential.

The server persists four important things:

text
credential_identifier       stable lookup/OPRF key identifier
RegistrationRecord          per-client public record
oprf_seed                   secret used to derive per-client OPRF keys
server_private_key          long-term 3DH authentication secret

The RFC's RegistrationRecord has this shape:

text
RegistrationRecord {
  client_public_key
  masking_key
  Envelope {
    envelope_nonce
    auth_tag
  }
}

Two details are easy to miss:

  1. The server stores the client's public key; the private key is rebuilt on the client.
  2. In RFC 9807, the envelope consists of a nonce and authentication tag. The client deterministically regenerates its private key from the password-derived value and the envelope nonce. The tag detects a wrong password or modified credentials.

The oprf_seed and server_private_key live as separate server secrets outside the registration record. This separation materially changes what a database-only breach exposes.

The OPRF, With the Algebra Left In

The real OPRF uses a prime-order group and a hash-to-group function. The following notation removes encoding and validation details, but preserves the operation that matters.

Let:

text
w = password
k = the server's per-client OPRF private key
r = a fresh random non-zero scalar chosen by the client
H1 = hash-to-group
H2 = the OPRF finalization hash

The client blinds the password:

text
P = H1(w)
M = r * P

It sends M. The server evaluates the blinded point:

text
Z = k * M

The client removes its blinding factor:

text
N = inverse(r) * Z
  = inverse(r) * k * r * P
  = k * P
 
oprf_output = H2(w, N)

The server sees M, which is randomized by a new r on every run. The client gets the same logical output for the same password and per-client OPRF key, while k remains server-side.

The server derives k from oprf_seed and credential_identifier:

text
seed = Expand(oprf_seed, credential_identifier || "OprfKey")
k = DeriveKeyPair(seed, "OPAQUE-DeriveKeyPair").private

This makes the OPRF mapping unique per credential while allowing the server to protect one high-entropy root secret.

The OPRF Security Boundary

For a passive observer or a thief holding only a registration record, local password guesses lack the OPRF key material needed for verification. A thief holding both the records and oprf_seed can derive k, evaluate each guess, and test the result offline.

OPAQUE targets precomputation across users and services. After a complete server compromise, resistance returns to the cost of testing each candidate against the KSF.

Registration: Building the Credential Record

Registration must run over a server-authenticated channel that provides confidentiality and integrity. In a web deployment, that normally means TLS.

text
Client                                             Server
  |                                                   |
  | RegistrationRequest { blinded_message }          |
  |-------------------------------------------------->|
  |                                                   |
  | RegistrationResponse {                            |
  |   evaluated_message, server_public_key            |
  | }                                                 |
  |<--------------------------------------------------|
  |                                                   |
  | RegistrationRecord {                              |
  |   client_public_key, masking_key, envelope        |
  | }                                                 |
  |-------------------------------------------------->|

The steps are:

  1. The client blinds the password and sends a RegistrationRequest.
  2. The server derives the per-client OPRF key, evaluates the blinded element, and returns the result plus its long-term AKE public key.
  3. The client unblinds and finalizes the OPRF.
  4. The client applies a key-stretching function and derives a randomized_password.
  5. From that value, the client derives a masking key, an envelope authentication key, an export key, and a seed for its static 3DH key pair.
  6. The client sends the resulting registration record to the server.

The RFC-shaped derivation is compact enough to show directly:

text
oprf_output = OPRF.Finalize(password, blind, evaluated_element)
stretched = KSF(oprf_output)
randomized_password = HKDF-Extract("", oprf_output || stretched)
 
masking_key = Expand(randomized_password, "MaskingKey")
auth_key = Expand(randomized_password, envelope_nonce || "AuthKey")
export_key = Expand(randomized_password, envelope_nonce || "ExportKey")
key_seed = Expand(randomized_password, envelope_nonce || "PrivateKey")
 
(client_private_key, client_public_key) = DeriveDHKeyPair(key_seed)
auth_tag = MAC(auth_key, envelope_nonce || cleartext_credentials)

cleartext_credentials binds the server public key and, when supplied by the application, the client and server identities. A wrong password later derives a different private key and authentication key, causing envelope recovery to fail.

The export_key is client-only application key material. It becomes available for uses such as encrypted backups after server authentication succeeds during login.

Login: OPRF Recovery and 3DH Run Together

OPAQUE-3DH login has three protocol messages: KE1, KE2, and KE3. In an HTTP API, that usually means two request/response calls.

text
Client                                             Server
  |                                                   |
  | KE1 = blinded password + client nonce             |
  |       + client ephemeral public key               |
  |-------------------------------------------------->|
  |                                                   |
  | KE2 = evaluated OPRF + masked envelope            |
  |       + server nonce + server ephemeral key       |
  |       + server transcript MAC                     |
  |<--------------------------------------------------|
  |                                                   |
  | recover envelope, rebuild client private key,     |
  | derive 3DH secrets, verify server MAC             |
  |                                                   |
  | KE3 = client transcript MAC                       |
  |-------------------------------------------------->|
  |                                                   |
  |                          verify KE3, accept session|

KE1: The Client Starts Both Halves

The client creates a fresh OPRF blind, nonce, and ephemeral 3DH key pair. KE1 contains:

text
CredentialRequest { blinded_message }
AuthRequest {
  client_nonce
  client_public_keyshare
}

The password and the OPRF blind remain in ephemeral client state.

KE2: The Server Returns a Masked Record and Authenticates Itself

The server evaluates the OPRF request. It also expands the stored masking_key with a fresh masking_nonce and XORs the resulting pad with:

text
server_public_key || envelope

This masking is primarily an account-enumeration defense on the wire. The registration record still contains the masking_key, so the mechanism offers no database confidentiality.

At the same time, the server generates an ephemeral 3DH key pair and computes three Diffie-Hellman values:

ValueClient keyServer keyPurpose
dh1EphemeralEphemeralFresh shared secret and forward secrecy
dh2EphemeralStaticAuthenticate the server
dh3StaticEphemeralAuthenticate the client credential

The server concatenates dh1 || dh2 || dh3, runs the key schedule, and obtains Km2, Km3, and session_key. It sends a server_mac over the transcript using Km2.

KE2 therefore contains both flows:

text
CredentialResponse {
  evaluated_message
  masking_nonce
  masked_response
}
AuthResponse {
  server_nonce
  server_public_keyshare
  server_mac
}

KE3: The Client Proves It Recovered the Right Credential

The client finalizes the OPRF, repeats the KSF and HKDF derivation, reconstructs the masking key, unmasks the envelope, regenerates its static private key, and checks the envelope tag.

It can now compute the same three DH values and the same key schedule. The client first verifies server_mac. Only then does it send:

text
KE3 { client_mac }

The server verifies client_mac with Km3. The session key becomes usable only after this check succeeds.

This third message provides explicit client authentication and full forward secrecy against active attackers.

What a Wrong Password Looks Like

A wrong password produces a chain of unrelated-looking values rather than a neat password_mismatch bit at the OPRF step:

text
wrong password
  -> wrong OPRF output
  -> wrong randomized_password
  -> wrong masking key
  -> garbage server key and envelope
  -> envelope authentication failure or server MAC failure
  -> no valid KE3

The application should collapse these protocol failures into one generic authentication error. Distinct public errors reintroduce enumeration and oracle behavior at the application layer.

The Breach Model, Precisely

The phrase “the password database is useless to attackers” is too broad. The answer depends on which secrets were stolen.

Attacker obtainsCan test guesses offline?Practical consequence
Recorded OPAQUE trafficNoFresh blinding and ephemeral keys protect the transcript
RegistrationRecord onlyRequires OPRF materialThe attacker lacks the OPRF key material
Record plus oprf_seedYesThe attacker can derive the per-client OPRF key and test candidates
Full authentication serverYesOffline guessing is inevitable for a single-server aPAKE
Client device or password inputPassword may be exposed directlyEndpoint compromise bypasses the protocol

The KSF makes each offline guess expensive. RFC 9807 explicitly states that a corrupted single server can run an exhaustive offline dictionary attack. A threshold OPRF can split that capability across servers, but it is outside the core protocol.

This leads to a useful deployment rule:

Store the credential records, oprf_seed, and server AKE secret in separate protection domains, leaving a database snapshot with the records alone.

An HSM can protect the server AKE operation. The server can compute the required shared secrets inside that boundary while the raw AKE private key remains non-exportable.

The Remaining System Boundaries

A Malicious Web Origin Can Replace the Client

In a browser deployment, the server normally delivers the JavaScript that runs OPAQUE. A compromised origin can serve modified code that captures the password before the protocol begins. The protocol boundary starts after trusted client code has accepted the password.

A signed native client, browser extension, or separately trusted client bundle has a stronger boundary than JavaScript delivered on every page load.

Online Guessing Still Exists

Attackers can still attempt logins. A malicious server can also perform an online exhaustive attack by interacting with a client. Use server-authenticated TLS, per-account and per-network throttling, abuse detection, and MFA where the risk warrants it.

Registration Can Reveal Account Existence

During login, a server may generate a fake record and fake KE2 for an unknown identity so that registered and unregistered accounts look alike. This is an optional mitigation with a server-side cost.

Registration necessarily reveals whether an account can be created or changed. Restrict and rate limit registration and password-change endpoints separately.

Account Recovery Is a Separate Protocol

Support-assisted resets create a separate route into the account. Recovery codes, verified devices, passkeys, and help-desk procedures therefore need their own threat model.

Passwords Remain Phishable and Reusable

Passkeys add origin binding and phishing resistance. OPAQUE serves deployments where password compatibility remains a requirement.

A Practical Web Integration Boundary

Use an audited, interoperable OPAQUE implementation behind a narrow client and server adapter; the RFC pseudocode defines the protocol rather than a production library.

A typical API surface is:

text
POST /auth/opaque/register/start
  client sends RegistrationRequest
  server returns RegistrationResponse
 
POST /auth/opaque/register/finish
  client sends RegistrationRecord
  server stores it
 
POST /auth/opaque/login/start
  client sends account handle + KE1
  server returns handshake_id + KE2
 
POST /auth/opaque/login/finish
  client sends handshake_id + KE3
  server runs ServerFinish and only then creates the application session

The server needs short-lived, single-use state between KE2 and KE3. Bind that state to the account, protocol configuration, and transcript. Expire it quickly and reject replay.

OPAQUE's session_key is raw cryptographic output. The application maps it to a secure channel, a key confirmation step, or the authenticated boundary after which the backend issues its normal secure cookie. That mapping belongs to the completed transcript and runs only after ServerFinish succeeds.

Configuration Is Part of the Protocol

An OPAQUE-3DH configuration fixes the OPRF, group, hash, KDF, MAC, KSF, and an application context string. The context binds version and application information, preventing silent credential reuse in a weaker or different protocol.

RFC 9807 recommends three profiles in the absence of an application-specific one:

  • ristretto255/SHA-512 with HKDF-SHA-512, HMAC-SHA-512, and Argon2id
  • P-256/SHA-256 with HKDF-SHA-256, HMAC-SHA-256, and Argon2id
  • P-256/SHA-256 with HKDF-SHA-256, HMAC-SHA-256, and scrypt

The listed Argon2id profile uses m = 2^21 KiB: 2 GiB of memory. That is a very real client requirement, especially on mobile devices and browsers. Benchmark the exact application profile on the weakest supported client, fix its parameters explicitly, and treat any change as a credential migration rather than an invisible tuning knob.

Web Crypto covers only part of the OPAQUE stack. A production implementation also needs the selected group operations, RFC 9497 OPRF, hash-to-curve, KSF, constant-time validation, transcript encoding, and the RFC 9807 state machine.

Implementation Checklist

  • Use a reviewed RFC 9807 implementation and verify it against Appendix C test vectors.
  • Require authenticated, confidential registration and password-change channels.
  • Include stable client and server identities in the OPRF input when possible.
  • Bind the application name and protocol version into the OPAQUE context.
  • Keep oprf_seed, registration records, and the server AKE secret in separate protection domains.
  • Use an HSM or equivalent boundary for the server AKE secret where justified.
  • Validate every received group element and public key before use.
  • Use constant-time cryptographic operations and erase ephemeral state after completion.
  • Create fake login responses for unknown accounts if enumeration is in scope.
  • Rate limit login, registration, password change, and recovery independently.
  • Return one public authentication failure; keep diagnostic detail in protected telemetry.
  • Issue an application session only after KE3 passes ServerFinish.
  • Treat a password change as a fresh registration with new randomness.
  • Test downgrade, replay, concurrent login, lost state, and partial-deployment failures.

When OPAQUE Is Worth the Complexity

OPAQUE is a strong fit when all of the following are true:

  • Password compatibility is a product requirement.
  • Preventing the authentication service and its intermediaries from routinely receiving plaintext passwords is valuable.
  • You control or can ship a trustworthy client implementation.
  • You can protect server secrets separately from the credential database.
  • You can afford a three-message login and an expensive client-side KSF.

Choose a simpler design when the client implementation lacks a trustworthy boundary, the team would need to hand-roll the cryptography, or every server secret would sit beside the registration records.

The Short Version

OPAQUE combines three mechanisms:

  1. An OPRF turns the password into a server-assisted secret while keeping the password client-side.
  2. A password-derived envelope lets the client recover its static AKE key and verify the bound credentials.
  3. A three-message 3DH exchange authenticates both sides and produces a fresh session key.

Its strongest practical win is simple: the normal authentication path no longer hands plaintext passwords to the server. Its limit is equally important: if an attacker steals the credential records and the server's OPRF secret, weak passwords can still be guessed offline.

The result is a substantial improvement with a clearly defined boundary.

References

Comments

Related Posts