ZKP Biometric Authentication
ZeroAuth's ZKP flow provides privacy-preserving biometric authentication. The server verifies a mathematical proof instead of retaining reusable biometric material — zero biometric data stored, ever.
End-to-End Flow
- The client captures a biometric template.
- The client submits the template to
POST /v1/auth/zkp/registervia the ZeroAuth API. - ZeroAuth derives identity materials and optionally anchors the biometric hash to a DID on Base Sepolia.
- ZeroAuth returns
did,commitment,didHash,biometricSecret, andsalt. - The client stores
biometricSecretandsaltsecurely. - The client generates a Groth16 proof locally.
- The client requests a fresh nonce from
GET /v1/auth/zkp/nonce. - The client submits the proof, public signals, nonce, and timestamp to
POST /v1/auth/zkp/verify. - ZeroAuth verifies the proof and issues JWTs on success.
Registration
Request
curl -X POST https://zeroauth.dev/v1/auth/zkp/register \
-H "Authorization: Bearer za_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"biometricTemplate": "<base64-encoded-template>"
}'
Success response
{
"did": "did:zeroauth:base:9d32c2b87ebfa39cbe3f1a0e103b65d9",
"commitment": "1877552082745230970102393847",
"didHash": "8181234412277001983374",
"biometricSecret": "1569829922277132991011",
"salt": "5566092238018732991001",
"txHash": "0xabc123...",
"blockNumber": 24801234,
"dataStored": false,
"message": "Identity registered. Store biometricSecret and salt securely — they will not be sent again. Zero biometric data stored on server."
}
Validation rules
biometricTemplatemust be a string.- The decoded template must be at least 16 bytes.
- Your API key must have the
zkp:registerscope.
Identity Materials Returned to the Client
did
The decentralized identifier generated for this user in the form:
did:zeroauth:base:<32 hex chars>
commitment
Poseidon commitment used as a public circuit input.
didHash
Poseidon-compatible DID hash used as a public circuit input.
biometricSecret
A client-held secret derived from the SHA-256 biometric hash and the generated salt.
salt
A random field-safe value used in commitment derivation and proof generation.
Client-Side Proof Generation
The server never generates proofs. The client must do that locally.
The circuit expects:
- private inputs:
biometricSecret,salt - public inputs:
commitment,didHash,identityBinding
identityBinding is defined by the circuit as:
Poseidon(biometricSecret, didHash)
Example browser or Node client flow:
import * as snarkjs from 'snarkjs';
import { buildPoseidon } from 'circomlibjs';
const poseidon = await buildPoseidon();
const F = poseidon.F;
const identityBinding = F.toString(
poseidon([BigInt(biometricSecret), BigInt(didHash)])
);
const { proof, publicSignals } = await snarkjs.groth16.fullProve(
{
biometricSecret,
salt,
commitment,
didHash,
identityBinding
},
wasmPath,
zkeyPath
);
Artifacts you need on the client side:
identity_proof.wasmcircuit_final.zkey
You can inspect the configured circuit metadata from:
curl https://zeroauth.dev/v1/auth/zkp/circuit-info \
-H "Authorization: Bearer za_live_YOUR_KEY"
The API does not directly expose the proving key, so most teams either:
- package the
zkeywith a frontend SDK, or - host it as a static artifact separately.
Nonce and Freshness
Fetch a nonce before proof submission:
curl https://zeroauth.dev/v1/auth/zkp/nonce \
-H "Authorization: Bearer za_live_YOUR_KEY"
Example response:
{
"nonce": "8eb8b0db-c143-4e29-8e6c-6c26078ba2c8",
"timestamp": "2026-03-14T10:15:30.000Z",
"expiresIn": 300
}
Verification checks:
- the nonce must be a UUID v4,
- the submitted timestamp must be within five minutes of server time.
Proof Verification
Request
curl -X POST https://zeroauth.dev/v1/auth/zkp/verify \
-H "Authorization: Bearer za_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"proof": {
"pi_a": ["1", "2", "1"],
"pi_b": [["1", "2"], ["3", "4"], ["1", "0"]],
"pi_c": ["5", "6", "1"],
"protocol": "groth16",
"curve": "bn128"
},
"publicSignals": [
"<commitment>",
"<didHash>",
"<identityBinding>"
],
"nonce": "<uuid-v4>",
"timestamp": "2026-03-14T10:15:30.000Z"
}'
Success response
{
"accessToken": "<jwt>",
"refreshToken": "<jwt>",
"tokenType": "Bearer",
"expiresIn": 3600,
"verified": true,
"sessionId": "6e92d480-9d01-4892-8ca4-7012910d8bf4",
"provider": "zkp",
"dataStorageConfirmation": {
"biometricDataStored": false,
"message": "Zero biometric data stored. Ever. Breach-proof by architecture."
}
}
Failure response
{
"verified": false,
"error": "Biometric proof verification failed",
"dataStored": false,
"message": "Zero biometric data stored. Ever. Breach-proof by architecture."
}
Verification Modes
Off-chain verification
ZeroAuth verifies the Groth16 proof using snarkjs.groth16.verify(...) with the loaded verification key. This is the default operating mode.
On-chain verification
When enabled for your account, ZeroAuth also calls the Groth16 verifier contract on Base Sepolia after off-chain verification passes. On-chain verification is available on Starter plans and above.
Recommended Client Responsibilities
- Never send raw biometric templates during verification.
- Store
biometricSecretandsaltsecurely after registration. - Treat registration response values as unrecoverable from the server once lost.
- Fetch a fresh nonce immediately before proof submission.
- Pin artifact versions so the circuit, proving key, verification key, and Solidity verifier all match.
For endpoint-level details, see API Reference.