Part 1: The Cryptographic Foundation
Passkeys rely on digital signatures to prove possession of a private key without ever revealing it. A digital signature allows a device to produce a short piece of data that can only be generated with the private key, yet can be verified by anyone holding the corresponding public key.
Two signature algorithms dominate real-world passkey implementations:
- RSA (RS256): Legacy signatures based on prime factorization.
- ECDSA with P-256 (ES256): Modern signatures based on elliptic-curve cryptography.
Both algorithms follow the same high-level pattern: a private key signs a message, and the public key verifies that signature. The security properties of passkeys come from the fact that producing a valid signature is computationally infeasible without the private key. The underlying mathematics differ, so we will cover both.
1a. RSA Digital Signatures (RS256)
Key Generation
RSA key generation produces three numbers:
- N — the modulus, the product of two large secret primes p and q
- e — the public exponent (almost universally 65537)
- d — the private exponent, computed such that
e × d ≡ 1 (mod λ(N)), where λ(N) is the totient of N
These form two keys:
Public key: (e, N) # shared with the server
Private key: (d, N) # sealed in the secure enclave, never exported
Signing and Verification
In practice, we never sign the raw message — we sign its hash. This produces a fixed-size input for the modular arithmetic and ensures that changing even one byte of the original message completely invalidates the signature.
# Signing (private key operation)
signature = Hash(message)d mod N
# Verification (public key operation)
recovered = signaturee mod N
valid ↔ recovered == Hash(message)
Only someone holding d can produce a value that, when raised to e mod N, recovers the original hash. Reversing this to find d from (e, N) requires factoring N — computationally infeasible for 2048-bit or larger moduli.
1b. ECDSA Digital Signatures (ES256)
ECDSA uses elliptic curve cryptography over the P-256 curve (also called secp256r1). Rather than modular exponentiation, the math is built on point multiplication on an elliptic curve.
Elliptic Curve Basics
An elliptic curve over a finite field is the set of points (x, y) satisfying:
For P-256, the constants a, b, and prime p are fixed and public. The curve also has a designated generator point G — a specific point on the curve agreed upon by the standard.
Point multiplication is defined as repeated point addition:
Q = d × G (add G to itself d times, using the elliptic curve addition rules)
This operation is a one-way trapdoor: given d and G, computing Q is fast. But given only Q and G, recovering d is the elliptic curve discrete logarithm problem — computationally infeasible for 256-bit curves.
Key Generation
Private key: d # a random 256-bit integer, sealed in the secure enclave
Public key: Q = d × G # a point on the P-256 curve, shared with the server
A 256-bit ECC key pair provides roughly the same security as a 3072-bit RSA key pair, in a much smaller package — which is why P-256 is preferred for passkeys.
Signing
To sign a message:
# 1. Hash the message
h = Hash(message) e.g. SHA-256
# 2. Generate a random nonce point
k = random integer in [1, curve_order - 1]
R = k × G (a point on the curve)
r = R.x mod curve_order (take only the x-coordinate)
# 3. Compute the signature scalar
s = k-1 × (h + r × d) mod curve_order
# Signature output: (r, s)
The signature is the pair (r, s). The nonce k must be unique and secret per signature — reusing k across two signatures leaks the private key d.
Verification
Given the message, signature (r, s), and public key Q:
h = Hash(message)
w = s-1 mod curve_order
u1 = h × w mod curve_order
u2 = r × w mod curve_order
X = u1 × G + u2 × Q (elliptic curve point addition)
valid ↔ X.x mod curve_order == r
The verification recovers the nonce point R using the public key Q instead of the private key d. If the x-coordinate matches r, the signature checks out. No knowledge of d is required — only Q, which is public.
Part 2: Key Generation on Device
Regardless of which signature algorithm is used (RSA or ECDSA), the private key is generated inside a secure hardware boundary on the user’s device. This boundary is designed specifically to prevent key extraction, even if the rest of the operating system is compromised.
Common implementations include:
- Apple devices: Secure Enclave (dedicated cryptographic coprocessor)
- Windows / Android: Trusted Platform Module (TPM) or hardware-backed keystore
- Hardware security keys: FIDO2-certified secure element (e.g., YubiKey)
The secure enclave is physically and logically isolated from the main processor. Key material is generated internally using a hardware random number generator and never leaves the enclave in plaintext form.
When a passkey is created:
- The enclave generates the key pair.
- The private key is sealed inside the hardware boundary.
- The public key is returned to the operating system for registration with the server.
The private key cannot be exported, read, or copied. The operating system does not receive the key itself — it receives only a handle that allows it to request cryptographic operations.
From that point forward, the enclave will perform a signing operation only after local user verification (Face ID, Touch ID, device PIN, etc.). The verification step gates access to the key’s signing capability but does not expose the key material.
Even if malware gains full control of the operating system, it cannot extract the private key. At most, it could attempt to request signatures — and those requests would still require successful user verification.
This hardware confinement of the private key is one of the core security properties of passkeys. The server never sees the private key. The operating system cannot read it. And attackers cannot steal it through database breaches or phishing.
Part 3: Registration Flow
Registration establishes a passkey between the user’s device and a website (the relying party). Its purpose is to securely associate a newly generated public key with a user account and bind that key to the site’s domain.
At the end of registration, the server stores a public key and a credential identifier. The corresponding private key remains sealed inside the user’s device.
Step 1 — Server Issues a Registration Challenge
The server begins registration by generating a fresh random nonce and sending it to the browser along with the relying party identifier and user information.
This challenge serves two purposes:
- It ensures the registration response was generated for this specific session.
- It prevents replay of previously captured registration data.
The challenge is single-use and short-lived.
Server generates:
challenge_reg = 0x9f3a...c2b1 (random 32-byte nonce, single-use)
relying_party = "jchowlabs.com"
user_id = "alice"
Server ——› Browser: { challenge_reg, relying_party, user_id }
The challenge is single-use and short-lived. It ensures the response was generated right now for this session — not replayed from a previous one.
Step 2 — Device Generates a Key Pair
The browser invokes the WebAuthn API, which asks the secure enclave to generate a new key pair bound to this relying party.
# RSA path
Enclave generates: p, q → N = p × q
e = 65537
d such that e × d ≡ 1 (mod λ(N))
# ECDSA path
Enclave generates: d = random 256-bit integer
Q = d × G (on P-256)
In both cases:
Private key → sealed in enclave, tagged to "jchowlabs.com"
Public key → exportable
credential_id = opaque handle used to look up this key later
Step 3 — Device Builds and Signs the Attestation
The device assembles authenticator data — a structured payload packaging the key and binding it to the domain:
authenticator_data = {
rp_id_hash: SHA-256("jchowlabs.com"), ← domain hash, binds credential to this site
flags: user_present=1, user_verified=1,
sign_counter: 0,
credential_id: <opaque handle>,
public_key: (e, N) or Q ← RSA or ECDSA public key
}
The device also builds client data, which embeds the original challenge:
client_data = {
type: "webauthn.create",
challenge: challenge_reg, ← echo of server's nonce
origin: "https://jchowlabs.com"
}
client_data_hash = SHA-256(client_data)
Now the enclave signs the combined payload:
message_to_sign = authenticator_data || client_data_hash
# RSA path
signature_reg = Hash(message_to_sign)d mod N
# ECDSA path
h = Hash(message_to_sign)
k = random nonce
R = k × G
r = R.x mod curve_order
s = k-1 × (h + r × d) mod curve_order
signature_reg = (r, s)
Step 4 — Device Sends the Registration Response
Device ——› Server:
{
credential_id,
public_key: (e, N) or Q,
authenticator_data,
client_data_hash,
signature_reg
}
Step 5 — Server Verifies the Registration
# 1. Verify domain binding
SHA-256("jchowlabs.com") == authenticator_data.rp_id_hash ✓
client_data.origin == "https://jchowlabs.com" ✓
# 2. Verify challenge freshness (no replay)
client_data.challenge == challenge_reg ✓
# 3a. Verify signature — RSA path
recovered = signature_rege mod N
expected = Hash(authenticator_data || client_data_hash)
recovered == expected ✓
# 3b. Verify signature — ECDSA path
h = Hash(authenticator_data || client_data_hash)
w = s-1 mod curve_order
u1 = h × w mod curve_order
u2 = r × w mod curve_order
X = u1 × G + u2 × Q
X.x mod curve_order == r ✓
# 4. Store credential
Server stores: { user_id → (credential_id, public_key) }
The server stores only the public key. It never sees d, and d cannot be derived from the public key — factoring N (RSA) or solving the elliptic curve discrete log (ECDSA) are both computationally infeasible.
Part 4: Authentication Flow
Authentication is simpler than registration. No new keys are generated. The device simply proves — in real time — that it still possesses the private key corresponding to the public key stored by the server.
Where registration establishes trust, authentication exercises it.
Step 1 — Server Issues an Authentication Challenge
Server generates:
challenge_auth = 0x44bc...91e7 (fresh nonce, different from registration)
Server ——› Browser: { challenge_auth, [credential_id_1, credential_id_2, ...] }
The list of credential IDs tells the device which keys are valid for this site, so the enclave can locate the right private key.
Step 2 — User Verifies Locally
Before the enclave will sign anything, the device requires local user verification (biometric or PIN).
This step enforces two conditions:
- The device is physically present.
- The legitimate user is interacting with it.
Possession of the device alone is insufficient. The enclave will not produce a signature without successful verification.
Step 3 — Device Builds Authenticator Data and Signs
authenticator_data = {
rp_id_hash: SHA-256("jchowlabs.com"),
flags: user_present=1, user_verified=1,
sign_counter: 1 ← incremented from last use
}
client_data = {
type: "webauthn.get",
challenge: challenge_auth, ← server's fresh nonce
origin: "https://jchowlabs.com"
}
client_data_hash = SHA-256(client_data)
message_to_sign = authenticator_data || client_data_hash
# RSA path
signature_auth = Hash(message_to_sign)d mod N
# ECDSA path
h = Hash(message_to_sign)
k = random nonce (fresh, never reused)
R = k × G
r = R.x mod curve_order
s = k-1 × (h + r × d) mod curve_order
signature_auth = (r, s)
The sign counter increments with every authentication. The server tracks the last counter value it accepted — any non-increasing counter is rejected as a potential replay or cloned credential.
Step 4 — Device Sends the Authentication Response
Device ——› Server:
{
credential_id,
authenticator_data,
client_data_hash,
signature_auth
}
Note: the public key is not sent this time. The server already has it from registration.
Step 5 — Server Verifies the Authentication
# 1. Look up public key from registration store
public_key = lookup(credential_id) ← (e, N) for RSA, Q for ECDSA
# 2. Verify domain binding and origin
SHA-256("jchowlabs.com") == authenticator_data.rp_id_hash ✓
client_data.origin == "https://jchowlabs.com" ✓
# 3. Verify challenge freshness (no replay)
client_data.challenge == challenge_auth ✓
# 4. Verify counter incremented (anti-clone / anti-replay)
authenticator_data.sign_counter > stored_counter ✓
stored_counter ← authenticator_data.sign_counter (update)
# 5a. Verify signature — RSA path
recovered = signature_authe mod N
expected = Hash(authenticator_data || client_data_hash)
recovered == expected ✓
# 5b. Verify signature — ECDSA path
h = Hash(authenticator_data || client_data_hash)
w = s-1 mod curve_order
u1 = h × w mod curve_order
u2 = r × w mod curve_order
X = u1 × G + u2 × Q
X.x mod curve_order == r ✓
→ Authentication succeeds. Access granted.
Part 5: Why This Is Secure
The security of passkeys follows directly from the cryptographic and system properties described in the previous sections. Each major class of attack against passwords is either eliminated or substantially reduced by design.
No shared secret
Passwords are shared secrets: the user knows them, and the server stores something derived from them. Any server breach creates the risk of offline guessing or credential reuse.
Passkeys eliminate shared secrets entirely. The server stores only a public key. Possession of that public key does not enable authentication — producing a valid signature requires the private key d, which never leaves the user’s device and cannot be derived from the public key.
For RSA, deriving d requires factoring N.
For ECDSA, it requires solving the elliptic curve discrete logarithm problem.
Both problems are computationally infeasible with current cryptographic parameters.
Phishing resistance
With passwords, users can be tricked into revealing credentials to a convincing imitation of a site.
Passkeys are resistant to phishing by construction. The relying party’s domain is hashed into authenticator_data and covered by the signature. The device will only sign a payload that includes the hash of the current domain.
If an attacker tricks a user into visiting jchow1abs.com, the device will produce a signature over:
When that signature is sent to the real server (jchowlabs.com), verification fails automatically. No warning, no user decision, and no opportunity for error.
Replay attack prevention
Every registration and authentication request includes a fresh, unpredictable challenge generated by the server. Each signature is therefore bound to a single session.
A signature captured during one login attempt cannot be reused in another. The challenge will not match, and verification will fail.
The sign counter provides an additional safeguard. The server tracks the highest counter value it has accepted. Any response with a non-increasing counter indicates potential replay or credential cloning and is rejected.
Private key confinement
The private key d is generated inside secure hardware and never exits it.
The secure enclave enforces a strict interface:
- It accepts a message.
- It returns a signature.
- It never reveals key material.
Even a fully compromised operating system cannot extract the private key. At worst, malware could attempt to request signatures — but those requests still require successful local user verification.
This confinement property is critical: database breaches, phishing attacks, and credential stuffing all fail because there is no extractable secret to steal.
Summary
Passkeys replace a fragile, shared secret with a cryptographic proof of possession. Authentication succeeds only when all of the following are true:
- The user’s device holds the correct private key.
- The request is bound to the correct domain.
- The challenge is fresh and un-replayed.
- The user is locally present and verified.
These guarantees are enforced by cryptography, hardware isolation, and protocol design — not by user behavior.
To see this flow in action, explore the Passkey: Interactive Demo or experiment directly at www.jchowlabs.me, where you can observe registration and authentication step by step.