
Identity systems of today introduces so much trust in companies to store your data securely. Multiple KYC providers hold your data indefinitely. Each app gets your full details even when they only need to know your age is over 18+ years, which is too much information to share and leave fragmented all over the place.
Self-Sovereign Identity SSI fixes this by giving you control of your credentials, you can share only what's necessary, prove claims without revealing data. basically verifiable credentials VC lets you prove you are over 18+ without showing your exact birth date. But SSI has an issue - where do you store the private keys that control your identity?
There are lots of fundamental solutions to this and one is biometric crypto identities, which is not one of the popular ones, the question is what if your biometric e.g. face is your private-key? No seed phrases to lose, no passwords to forget, no keys to store. Your biometric generates your wallet, which holds your verifiable credentials, which you prove using ZKPs.
This is possible through fuzzy extractors + zero-knowledge proofs. Here's how it works.
Traditional cryptographic systems were designed for reproducible bits. But biometrics breaks two key rules these systems depend on:
Keys must be perfectly reproducible - They demand bit-perfect key reproduction. Your private key for signing transactions must be identical every single time. But biometrics are noisy, lot's of variables to consider. Your fingerprint scan today won't match yesterday's scan bit-for-bit due to sensor variations, finger placement, moisture, temperature - the list goes on and on. Same with face scans, iris patterns, or any biological measurement.
Keys must be uniformly random - Cryptographic security assumes your key is drawn uniformly at random from the entire keyspace. But biometric data has structure and patterns. Your iris isn't uniformly random, it has biological constraints, population-level similarities, and entropy concentrated in specific features.
This creates the usual chicken-and-egg problem: you can't use biometrics directly as keys they're very noisy, you can't store them securely they're PII and immutable, and you can't just hash them noise breaks hash functions as you know. So what do you do?
Now the sweet spot!
Look at fuzzy extractors as error-correcting cryptography for noisy data. They're a pair of algorithms, Gen generate and Rep reproduce that do something very exciting:
- Takes your noisy biometric input w, outputs a uniformly random string R the actual cryptographic key and a public helper string P. Here's another fun fact: P can be published openly without compromising security. It doesn't reveal any meaningful information about your biometric whatsoever.
- Takes a different biometric reading w' and the public same helper P, reproduces the exact same R as long as w' is close enough to the original w. Now I know that's scary, but in this case close enough is measured by distance in some metric space Hamming distance for binary strings, edit distance for sequences, set difference for feature vectors.
The security guarantee is information-theoretic, even given P, the extracted key R is -close to uniform as long as your biometric has min-entropy . No computational hardness assumptions is needed. An adversary with infinite computing power still cannot distinguish R from a truly random key with advantage greater than .
The mathematics is beautiful. For a fuzzy extractor with parameters :
Rw' can be from wThe construction uses two primitives working together:
A secure sketch is like a fuzzy commitment. outputs a sketch s that reveals just enough information to correct errors, but not anywhere near enough to reconstruct w itself. Then recovers the original w from a noisy w' using the sketch.
For binary strings with Hamming distance, the Syndrome Construction is optimal:
class SyndromeSketch:
def __init__(self, n, k, t):
self.n, self.k, self.t = n, k, t
self.syn_matrix = generate_bch_syndrome_matrix(n, k)
def sketch(self, w):
return np.dot(self.syn_matrix, w) % 2
def recover(self, w_prime, s):
s_prime = np.dot(self.syn_matrix, w_prime) % 2
error_syn = (s_prime - s) % 2
e = bch_decode(error_syn, self.t)
return (w_prime - e) % 2What's happening here:
sketch() computes syndrome of biometric w, outputs s $n-k$ bitsrecover() corrects errors in noisy w_prime using sketch s$n-k$ bitsBCH code Bose–Chaudhuri–Hocquenghem codes: lose 64 bits, correct up to errorsAfter the error correction, you hash the recovered biometric to extract a uniform key e.g.
pub fn fuzzy_gen(biometric: &[u8]) -> (Vec<u8>, Vec<u8>) {
let sketch = secure_sketch(biometric);
let seed: Vec<u8> = (0..32).map(|_| rng.gen()).collect();
let key = extract_key(biometric, &seed);
let helper = [sketch, seed].concat();
(key, helper)
}
pub fn fuzzy_rep(noisy_biometric: &[u8], helper: &[u8]) -> Vec<u8> {
let (sketch, seed) = split_helper(helper);
let recovered = sketch_recover(noisy_biometric, sketch);
extract_key(&recovered, &seed)
}The fuzzy_gen() function:
hash(biometric || seed)key, helper where helper = sketch, seedThe fuzzy_rep() function:
For the typical biometrics:
Note: Raw biometric representations contain redundancy and correlations. Iris codes exhibit ~8:1 ratio between raw bits (2048) and information-theoretic entropy (~249 bits). Key extraction must respect actual entropy, not representation size.

The SSI trust model has three actors:
The complete flow: Biometric → Wallet → Credentials → ZK Proofs → Verified Identity

Let's look at it step by step:
async function biometricToWallet(rawBiometric) {
const binary = await preprocessBiometric(rawBiometric);
const { key, helper } = await fuzzyExtractor.gen(binary);
const expandedKey = await hkdf(
key,
128,
'bip39-seed',
await crypto.getRandomValues(new Uint8Array(32))
);
const mnemonic = bip39.entropyToMnemonic(expandedKey);
const seed = await bip39.mnemonicToSeed(mnemonic);
const master = bip32.fromSeed(seed);
return {
btc: deriveBitcoin(master),
eth: deriveEthereum(master),
sol: deriveSolana(seed),
helper,
salt
};
}The process:
fuzzy_gen() → get 40-128 bit key + public helperBIP39 mnemonic → derive HD wallet seedP2WPKH, ETH EVM, SOL Ed25519For Solana specifically it's a different curve:
import { derivePath } from 'ed25519-hd-key';
const solSeed = derivePath("m/44'/501'/0'/0'", seed.toString('hex')).key;
const solKeypair = Keypair.fromSeed(solSeed);
const solAddress = solKeypair.publicKey.toBase58();Note: Solana uses
Ed25519, notsecp256k1. So in your implementation useed25519-hd-keyforSLIP-0010compliance matchesPhantom,Solflare.
Your wallet now has a DID, but to prove anything, you need a Verifiable Credentials VC. This is where the trust triangle starts.
The issuer verifies your documents off-chain and issues a cryptographically signed proof:
async function requestCredential(wallet, issuerEndpoint, claimData) {
const connectionRequest = await issuerEndpoint.createConnection(wallet.did);
const credentialOffer = await issuerEndpoint.offerCredential({
type: 'KYCCredential',
schema: 'https://schema.org/kyc-v1',
claims: {
country: claimData.country,
birthYear: claimData.birthYear,
verified: true
}
});
const signedVC = await wallet.acceptCredential(credentialOffer);
await wallet.storeCredential(signedVC);
return signedVC;
}What happened:
cryptographically signed VC with your claimsVC includes issuer's DID signature proves authenticityVCs from different issuers education, employment, KYCThe credential schema defines what's in the VC:
{
"@context": "https://www.w3.org/2018/credentials/v1",
"type": ["VerifiableCredential", "KYCCredential"],
"issuer": "did:polygonid:polygon:amoy:2qH7...",
"issuanceDate": "2025-01-15T00:00:00Z",
"credentialSubject": {
"id": "did:polygonid:polygon:amoy:2qPz...",
"birthYear": 1995,
"country": "US",
"kycVerified": true
},
"proof": {
"type": "BJJSignature2021",
"issuerData": {...},
"signature": "0x..."
}
}Now when a verifier asks prove you're 18+, you generate a ZKP from this credential.
This is where you can prove your personal details like age, citizenship, a degree, address, birthdate, etc. without revealing these sensitive data.
async function proveAge(wallet, minAge) {
const credential = await wallet.getCredential('kyc');
const inputs = {
birthYear: credential.birthYear,
currentYear: 2025,
minAge: minAge,
signature: credential.signature
};
const { proof, publicSignals } = await snarkjs.groth16.fullProve(
inputs,
"ageCheck.wasm",
"ageCheck.zkey"
);
return { proof, publicSignals };
}What's proven:
minAge years oldThe Circom circuit:
template AgeCheck() {
signal input birthYear;
signal input currentYear;
signal input minAge;
signal age <== currentYear - birthYear;
component check = GreaterEqThan(16);
check.in[0] <== age;
check.in[1] <== minAge;
signal output isValid;
isValid <== check.out;
}On-chain verification - Solidity:
contract IdentityGate {
mapping(address => mapping(uint256 => bool)) public verified;
function submitProof(
uint256 requestId,
uint[2] calldata pA,
uint[2][2] calldata pB,
uint[2] calldata pC,
uint[3] calldata pubSignals
) external {
require(zkpVerifier.verifyProof(pA, pB, pC, pubSignals), "Invalid proof");
require(pubSignals[2] >= minAge, "Age requirement not met");
verified[msg.sender][requestId] = true;
_grantAccess(msg.sender);
}
}The contract:
So from this flow you could see how your biometric became:
DID anchored to your biometricVC from trusted issuersZKP reveal nothing about youAnd this isn't multiple separate systems - it's ONE unified flow. The wallet generation happens once during enrollment. The ZKP flow can be used whenever identity verification is needed. Think of it like this: you can use your biometric wallet just like any other crypto wallet for normal transactions. But when a dApp needs KYC or any sort of PII based verification like Polygon ID use cases, the ZKP layer kicks in to prove credentials without exposing your data.
Every AI agent can have a biometric-derived DID. Imagine an AI trading agent with its own wallet and verified credentials:
class TradingAgent {
constructor(agentFingerprint) {
this.wallet = await biometricToWallet(agentFingerprint);
this.did = await createDID(this.wallet);
}
async trade(dex, params) {
const proof = await this.proveReputation();
return await dex.trade(params, proof);
}
}The agent:
biometric neural network hashZKPSingularityNET are building exactly this, an AI Agent Trust Registry where agents prove authenticity, safety, and audit status using verifiable credentials. The biometric-wallet-ZKP flow enables agents to operate on their own while maintaining accountability.
A DeFi protocol needs KYC but doesn't want to store user data regulatory liability + honeypot for hackers:
contract DeFiProtocol {
modifier onlyKYCVerified() {
require(identityGate.verified(msg.sender, KYC_ID), "KYC required");
_;
}
function borrowUndercollateralized(uint256 amount) external onlyKYCVerified {
_issueLoan(msg.sender, amount);
}
}The protocol never sees your passport, name, or address. Compliance achieved, privacy preserved.
The same pattern for:
async function login(website) {
const { key } = await fuzzyExtractor.rep(biometric, helper);
const siteKey = deriveKey(key, website.domain);
const signature = sign(challenge, siteKey);
await website.verify(signature);
}Each site gets a unique derived key. No password sent, no credentials to phish.
Your biometric authorizes the transaction:
async function pay(merchant, amount) {
const wallet = await biometricToWallet(scan);
if (amount > 1000) {
await merchant.verifyProof(generateKYCProof(wallet));
}
await wallet.sendTransaction({ to: merchant.address, value: amount });
}BMONI + BKey currently implements this where your wallets are all fully derived from your biometric "Your Face" and provisiond wallets across specifc EVM chains & tokens, all via your face!
contract AML {
bytes32 public scamMerkleRoot;
function checkClean(address wallet, bytes32[] calldata proof) public view returns (bool) {
bytes32 hash = keccak256(abi.encodePacked(wallet));
return !MerkleProof.verify(proof, scamMerkleRoot, hash);
}
}dApps query before allowing access. Known scammers are blocked network-wide and some with a history of weird activity are noted as well, while integrators receives a notification of past suspicious activity.
Building with biometric identities needs infrastructure. The ecosystem provides three core components:
Organizations run self-hosted issuer nodes to issue credentials:
docker-compose up -d
make import-private-key-to-kms private_key=<eth-key>
# access the ui at localhost:8088
# access the api at localhost:3001Features:
DIDschemasVCsAPI example:
const issuerAPI = new IssuerNode({
url: 'https://issuer.example.com',
auth: { user: 'admin', password: 'secure' }
});
await issuerAPI.createIdentity({
blockchain: 'polygon',
network: 'amoy',
method: 'polygonid'
});
await issuerAPI.issueCredential({
did: 'did:polygonid:...',
schema: 'KYCCredential',
claims: { ageOver: 18 }
});Tools Ecosystem
tools.privado.id/buildertools.privado.id/query-builderschemasissuers1: Biometric Variability
Your fingerprint varies 10-15% between scans. Solution: BCH codes are optimal for bit-level error correction. For 2048-bit iris code which contains ~249 bits of actual entropy with 10% errors (~205 bit errors):
Critical distinction: The 2048 bits are the RAW iris code representation Daugman's algorithm output. Actual information-theoretic entropy is only ~249 bits due to biological correlations and redundancy ~8:1 ratio.
2: Template Aging
Problem: Your face changes over time aging, weight, facial hair, etc.
Solution: Periodic re-enrollment with key migration:
async function migrate(oldBio, newBio) {
const oldWallet = await recover(oldBio);
const { newWallet, newHelper } = await enroll(newBio);
await oldWallet.transferAll(newWallet.address);
await storage.update(newHelper);
}Re-enroll every 2-3 years for face, 5 years for iris, annually for voice etc.
3: Liveness Detection
Problem: Attackers with your photo shouldn't be able to access your wallet.
Solution: Liveness checks during biometric capture:
const checks = await Promise.all([
depthSensor.measure(),
motionDetector.verify(['blink', 'nod']),
thermalCamera.capture(),
videoAnalyzer.checkTemporal()
]);WorldCoin's Orb does this with iris scans: 25+ liveness checks, 3D imaging, infrared + RGB cameras. Works incredibly well - false accept rate .
4: Helper String Privacy
Does P explained earlier leak biometric info? No. Theorem Dodis et al., attacker learns at most bits sketch length, key R remains statistically random. For our iris: learns 408 bits, 392 bits entropy remain.
The pieces exist already:
CompFE library (MIT), runs on mobileWhat's missing? Integrations...
Here's what it looks like to actually use this system in prod environment
async function enrollUser() {
const bio = await captureLive();
const { key, helper } = await fuzzyExtractor.gen(bio);
const salt = await crypto.getRandomValues(new Uint8Array(32));
const expandedKey = await hkdf('sha256', key, salt, 'bip39-seed', 128/8);
const wallet = await deriveWallet(expandedKey);
const did = await createDID(wallet.eth);
const kycCred = await requestKYC(did, { passport, selfie });
await storage.save({ helper, salt, did, credentials: [kycCred] });
return { wallet, did };
}
async function accessDApp(dappURL) {
const bio = await captureLive();
const { helper, salt } = await storage.getHelperData();
const key = await fuzzyExtractor.rep(bio, helper);
const expandedKey = await hkdf('sha256', key, salt, 'bip39-seed', 128/8);
const wallet = await deriveWallet(expandedKey);
const proof = await generateProof(wallet, dappURL.request);
await submitProof(wallet, proof, dappURL.verifier);
await dappURL.grantAccess(wallet.address);
}
async function sendPayment(recipient, amount) {
const bio = await captureLive();
const { helper, salt } = await storage.getHelperData();
const key = await fuzzyExtractor.rep(bio, helper);
const expandedKey = await hkdf('sha256', key, salt, 'bip39-seed', 128/8);
const wallet = await deriveWallet(expandedKey);
await wallet.send({ to: recipient, value: amount });
}Normal wallet flow and identity flow coexist. ZKP comes up only when verification is needed, either for KYC or for any PII based verification like age, citizenship, a degree, address, birthdate, etc.
Your biometric is your key, your wallet is your identity across all supported chains & you never have to worry about loosing your seed phase or private keys, it's all tied to your physical attributes, in BMoni's implementation it's your face!