Tous les articles

Dérivation de clés biométriques pour l'identité décentralisée avec les fuzzy extractors

·
21 oct. 2025
·
11 min de lecture
·
••• vues
·
••• j'aime

Traduit de l'article original en anglais. Lire en anglais

Dérivation de clés liée à la biométrie pour la DID avec les fuzzy extractors

Les systèmes d'identité actuels nous obligent à faire énormément confiance aux entreprises pour stocker nos données de manière sécurisée. De nombreux fournisseurs de KYC conservent vos données indéfiniment. Chaque app reçoit l'intégralité de vos informations, même quand elle a seulement besoin de savoir que vous avez plus de 18 ans, ce qui fait beaucoup trop d'informations partagées et éparpillées un peu partout.

L'identité auto-souveraine (Self-Sovereign Identity) SSI règle ce problème en vous donnant le contrôle de vos credentials, vous pouvez partager uniquement ce qui est nécessaire et prouver des affirmations sans révéler de données. en gros, les verifiable credentials VC vous permettent de prouver que vous avez plus de 18 ans sans montrer votre date de naissance exacte. Mais la SSI a un problème - où stocker les clés privées qui contrôlent votre identité ?

Il existe beaucoup de solutions de fond à ce problème, et l'une d'elles est celle des biometric crypto identities, qui ne fait pas partie des plus populaires, la question est la suivante : et si votre biometric, par exemple votre face, était votre private-key ? Pas de seed phrases à perdre, pas de passwords à oublier, pas de keys à stocker. Votre biometric génère votre wallet, qui contient vos verifiable credentials, que vous prouvez à l'aide de ZKP.

C'est possible grâce aux fuzzy extractors + zero-knowledge proofs. Voici comment ça marche.

Le vrai problème que cela pourrait résoudre

Les systèmes cryptographiques traditionnels ont été conçus pour des bits reproductibles. Mais la biométrie enfreint deux règles essentielles sur lesquelles reposent ces systèmes :

Les clés doivent être parfaitement reproductibles - Ces systèmes exigent une reproduction des clés exacte au bit près. Votre clé privée pour signer des transactions doit être identique à chaque fois. Mais la biométrie est bruitée, il y a beaucoup de variables à prendre en compte. Le scan de votre empreinte digitale aujourd'hui ne correspondra pas bit pour bit à celui d'hier, à cause des variations du capteur, du placement du doigt, de l'humidité, de la température - et la liste est encore longue. Même chose pour les scans du visage, les motifs de l'iris ou n'importe quelle mesure biologique.

Les clés doivent être uniformément aléatoires - La sécurité cryptographique suppose que votre clé est tirée uniformément au hasard dans tout l'espace des clés. Mais les données biométriques ont une structure et des motifs. Votre iris n'est pas uniformément aléatoire, il est soumis à des contraintes biologiques, présente des similarités à l'échelle de la population, et son entropie est concentrée dans certaines caractéristiques précises.

Cela crée le problème classique de l'œuf et de la poule : vous ne pouvez pas utiliser directement la biométrie comme clé elle est très bruitée, vous ne pouvez pas la stocker de manière sécurisée ce sont des PII, et elles sont immuables, et vous ne pouvez pas simplement la hacher le bruit casse les fonctions de hachage, comme vous le savez. Alors, que faire ?

Et c'est là que ça devient intéressant !

Que sont les fuzzy extractors ?

Voyez les fuzzy extractors comme de la cryptographie à correction d'erreurs pour les données bruitées. Il s'agit d'une paire d'algorithmes, Gen générer et Rep reproduire, qui font quelque chose de vraiment passionnant :

Gen(w)→(R,P)Gen(w) \rightarrow (R, P) - Prend votre entrée biométrique bruitée w et produit une chaîne uniformément aléatoire R la vraie clé cryptographique ainsi qu'une chaîne d'aide publique P. Et voici un autre fait amusant : P peut être publiée ouvertement sans compromettre la sécurité. Elle ne révèle absolument aucune information utile sur votre biométrie.

Rep(w′,P)→RRep(w', P) \rightarrow R - Prend une autre lecture biométrique w' et la même chaîne d'aide publique P, puis reproduit exactement la même R tant que w' est suffisamment proche de la w d'origine. Je sais que ça peut faire peur, mais ici, suffisamment proche se mesure par une distance dans un certain espace métrique distance de Hamming pour les chaînes binaires, distance d'édition pour les séquences, différence d'ensembles pour les vecteurs de caractéristiques.

La garantie de sécurité relève de la théorie de l'information : même en connaissant P, la clé extraite R est ε\varepsilon-proche de l'uniforme tant que votre biométrie a une min-entropie ≥m\geq m. Aucune hypothèse de difficulté calculatoire n'est nécessaire. Un adversaire doté d'une puissance de calcul infinie ne peut toujours pas distinguer R d'une clé réellement aléatoire avec un avantage supérieur à ε\varepsilon.

Les mathématiques sont magnifiques. Pour un fuzzy extractor de paramètres (m,ℓ,t,ε)(m, \ell, t, \varepsilon) :

Comment ça marche concrètement

La construction utilise deux primitives qui travaillent ensemble :

Les secure sketches (SS,Rec)(SS, Rec)

Un secure sketch ressemble à un engagement flou (fuzzy commitment). SS(w)→sSS(w) \rightarrow s produit un sketch s qui révèle juste assez d'informations pour corriger les erreurs, mais loin d'être assez pour reconstruire w elle-même. Ensuite, Rec(w′,s)→wRec(w', s) \rightarrow w retrouve la w d'origine à partir d'une w' bruitée, grâce au sketch.

Pour les chaînes binaires avec la distance de Hamming, la Syndrome Construction est optimale :

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) % 2

Ce qui se passe ici :

Maintenant, du sketch à l'extracteur

Après la correction d'erreurs, on hache la biométrie retrouvée pour en extraire une clé uniforme, par exemple

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)
}

La fonction fuzzy_gen() :

La fonction fuzzy_rep() :

Pour les biométries courantes :

Note : Les représentations biométriques brutes contiennent de la redondance et des corrélations. Les codes d'iris présentent un ratio de ~8:1 entre les bits bruts (2048) et l'entropie au sens de la théorie de l'information (~249 bits). L'extraction de clé doit respecter l'entropie réelle, pas la taille de la représentation.

Le volet identité

Le modèle de confiance de la SSI compte trois acteurs :

Le flux complet : Biométrie → Wallet → Credentials → Preuves ZK → Identité vérifiée

Voyons cela étape par étape :

De la biométrie au wallet

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
    };
}

Le processus :

Pour Solana en particulier, c'est une courbe différente :

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 utilise Ed25519, pas secp256k1. Dans votre implémentation, utilisez donc ed25519-hd-key pour être conforme à SLIP-0010 correspond à Phantom, Solflare.

Obtenir vos verifiable credentials

Votre wallet a maintenant un DID, mais pour prouver quoi que ce soit, il vous faut un Verifiable Credential VC. C'est là que commence le triangle de confiance. L'émetteur vérifie vos documents off-chain et émet une preuve signée cryptographiquement :

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;
}

Ce qui s'est passé :

Le schéma du credential définit ce que contient le 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..."
  }
}

Désormais, quand un vérificateur vous demande de prouver que vous avez plus de 18 ans, vous générez une ZKP à partir de ce credential.

La couche de preuve à divulgation nulle de connaissance

C'est ici que vous pouvez prouver vos informations personnelles, comme l'âge, la citoyenneté, un diplôme, l'adresse, la date de naissance, etc., sans révéler ces données sensibles.

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 };
}

Ce qui est prouvé :

Le circuit Circom :

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;
}

Vérification on-chain - 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);
    }
}

Le contrat :

À partir de ce flux, vous voyez donc comment votre biométrie est devenue :

Et il ne s'agit pas de plusieurs systèmes séparés - c'est UN seul flux unifié. La génération du wallet a lieu une seule fois, lors de l'enrôlement. Le flux ZKP peut être utilisé chaque fois qu'une vérification d'identité est nécessaire. Voyez-le ainsi : vous pouvez utiliser votre wallet biométrique comme n'importe quel autre wallet crypto pour vos transactions habituelles. Mais quand une dApp a besoin d'un KYC ou de n'importe quel type de vérification basée sur des PII comme les cas d'usage de Polygon ID, la couche ZKP entre en jeu pour prouver vos credentials sans exposer vos données.

Quelques cas d'usage possibles

  1. Passeports agentiques :

Chaque agent IA peut avoir un DID dérivé d'une biométrie. Imaginez un agent IA de trading avec son propre wallet et des credentials vérifiés :

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);
    }
}

L'agent :

SingularityNET construit exactement ça, un AI Agent Trust Registry où les agents prouvent leur authenticité, leur sûreté et leur statut d'audit à l'aide de verifiable credentials. Le flux biometric-wallet-ZKP permet aux agents d'opérer de manière autonome tout en restant responsables de leurs actes.

  1. Couche d'identité pour dApps :

Un protocole DeFi a besoin du KYC mais ne veut pas stocker les données des utilisateurs responsabilité réglementaire + pot de miel pour les hackers :

contract DeFiProtocol {
    modifier onlyKYCVerified() {
        require(identityGate.verified(msg.sender, KYC_ID), "KYC required");
        _;
    }
 
    function borrowUndercollateralized(uint256 amount) external onlyKYCVerified {
        _issueLoan(msg.sender, amount);
    }
}

Le protocole ne voit jamais votre passeport, votre nom ni votre adresse. Conformité assurée, vie privée préservée.

Le même schéma s'applique pour :

  1. Connexion sans mot de passe pour le Web2
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);
}

Chaque site obtient une clé dérivée unique. Aucun mot de passe envoyé, aucun identifiant à hameçonner.

  1. Paiements biométriques :

Votre biométrie autorise la 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 implémente actuellement ce principe : vos wallets sont entièrement dérivés de votre biométrie « votre visage » et provisionnés sur certaines chaînes EVM et certains tokens, le tout via votre visage !

  1. Base de données anti-arnaque :
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);
    }
}

Les dApps l'interrogent avant d'accorder l'accès. Les arnaqueurs connus sont bloqués sur tout le réseau et ceux qui ont un historique d'activité étrange sont également signalés, tandis que les intégrateurs reçoivent une notification de l'activité suspecte passée.

Intégrations pour les développeurs

Construire avec des identités biométriques nécessite de l'infrastructure. L'écosystème fournit trois composants principaux :

Issuer Node

Les organisations font tourner des issuer nodes auto-hébergés pour émettre des 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:3001

Fonctionnalités :

Exemple d'API :

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 }
});

Écosystème d'outils

Quelques défis techniques et leurs solutions

1 : Variabilité biométrique

Votre empreinte digitale varie de 10 à 15 % d'un scan à l'autre. Solution : les codes BCH sont optimaux pour la correction d'erreurs au niveau du bit. Pour un code d'iris de 2048 bits qui contient ~249 bits d'entropie réelle avec 10 % d'erreurs (~205 bits erronés) :

Distinction essentielle : les 2048 bits correspondent à la représentation BRUTE du code d'iris sortie de l'algorithme de Daugman. L'entropie réelle, au sens de la théorie de l'information, n'est que de ~249 bits, à cause des corrélations biologiques et de la redondance ratio de ~8:1.

2 : Vieillissement du modèle biométrique

Problème : votre visage change avec le temps vieillissement, poids, pilosité faciale, etc.

Solution : un ré-enrôlement périodique avec migration des clés :

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);
}

Ré-enrôlez-vous tous les 2-3 ans pour le visage, tous les 5 ans pour l'iris, chaque année pour la voix, etc.

3 : Détection du vivant

Problème : un attaquant qui possède votre photo ne devrait pas pouvoir accéder à votre wallet.

Solution : des contrôles de détection du vivant lors de la capture biométrique :

const checks = await Promise.all([
    depthSensor.measure(),
    motionDetector.verify(['blink', 'nod']),
    thermalCamera.capture(),
    videoAnalyzer.checkTemporal()
]);

L'Orb de WorldCoin fait cela avec des scans de l'iris : plus de 25 contrôles de détection du vivant, de l'imagerie 3D, des caméras infrarouges + RVB. Ça fonctionne incroyablement bien - taux de fausse acceptation <0.0001%< 0.0001\%.

4 : Confidentialité de la chaîne d'aide

Est-ce que P, expliquée plus haut, fait fuiter des informations biométriques ? Non. D'après le théorème Dodis et al., l'attaquant apprend au plus n−kn-k bits la longueur du sketch, et la clé R reste statistiquement aléatoire. Pour notre iris : il apprend 408 bits, il reste 392 bits d'entropie.

Est-ce prêt pour la production ?

Les briques existent déjà :

Qu'est-ce qui manque ? Les intégrations...

Exemple d'implémentation en production

Voici à quoi ressemble concrètement l'utilisation de ce système dans un environnement de production

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 });
}

Le flux de wallet classique et le flux d'identité coexistent. La ZKP n'intervient que lorsqu'une vérification est nécessaire, que ce soit pour le KYC ou pour toute vérification basée sur des PII, comme l'âge, la citoyenneté, un diplôme, l'adresse, la date de naissance, etc.

Votre biométrie est votre clé, votre wallet est votre identité sur toutes les chaînes prises en charge, et vous n'avez plus jamais à vous soucier de perdre votre seed phrase ou vos clés privées, tout est lié à vos attributs physiques, et dans l'implémentation de BMoni, c'est votre visage !

Liens essentiels

•••
Partager cet article :

Restez informé

Recevez les derniers articles dans votre boîte mail.