Todos los artículos

Derivación de claves biométricas para identidad descentralizada con fuzzy extractors

·
21 oct 2025
·
11 min de lectura
·
••• visitas
·
••• me gusta

Traducido del artículo original en inglés. Leer en inglés

Derivación de claves ligada a la biometría para DID usando fuzzy extractors

Los sistemas de identidad de hoy depositan muchísima confianza en las empresas para que guarden tus datos de forma segura. Varios proveedores de KYC conservan tus datos indefinidamente. Cada app recibe todos tus datos aunque solo necesite saber que tienes más de 18 años, lo cual es demasiada información para compartir y dejar fragmentada por todas partes.

La identidad autosoberana SSI resuelve esto dándote el control de tus credenciales, puedes compartir solo lo necesario y probar afirmaciones sin revelar datos. básicamente, las credenciales verificables VC te permiten probar que tienes más de 18 años sin mostrar tu fecha de nacimiento exacta. Pero SSI tiene un problema - ¿dónde guardas las claves privadas que controlan tu identidad?

Hay muchas soluciones de fondo para esto y una de ellas son las biometric crypto identities, que no es de las más populares, la pregunta es: ¿y si tu biometric, por ejemplo tu face, fuera tu private-key? Sin seed phrases que perder, sin passwords que olvidar, sin keys que guardar. Tu biometric genera tu wallet, que guarda tus credenciales verificables, que pruebas usando ZKPs.

Esto es posible gracias a fuzzy extractors + zero-knowledge proofs. Así es como funciona.

El problema real que esto podría resolver

Los sistemas criptográficos tradicionales se diseñaron para bits reproducibles. Pero la biometría rompe dos reglas clave de las que dependen estos sistemas:

Las claves deben ser perfectamente reproducibles - Exigen que la clave se reproduzca bit a bit. Tu clave privada para firmar transacciones debe ser idéntica todas y cada una de las veces. Pero la biometría tiene ruido, hay muchas variables que considerar. El escaneo de tu huella de hoy no va a coincidir bit a bit con el de ayer por variaciones del sensor, la posición del dedo, la humedad, la temperatura - y la lista sigue y sigue. Lo mismo pasa con los escaneos faciales, los patrones del iris o cualquier medición biológica.

Las claves deben ser uniformemente aleatorias - La seguridad criptográfica asume que tu clave se toma de forma uniformemente aleatoria de todo el espacio de claves. Pero los datos biométricos tienen estructura y patrones. Tu iris no es uniformemente aleatorio, tiene restricciones biológicas, similitudes a nivel de población y entropía concentrada en rasgos específicos.

Esto crea el típico problema del huevo y la gallina: no puedes usar la biometría directamente como clave tiene muchísimo ruido, no puedes guardarla de forma segura es PII e inmutable, y no puedes simplemente hashearla el ruido rompe las funciones hash, como ya sabes. Entonces, ¿qué haces?

¡Y aquí viene lo bueno!

¿Qué son los fuzzy extractors?

Piensa en los fuzzy extractors como criptografía con corrección de errores para datos con ruido. Son un par de algoritmos, Gen generar y Rep reproducir, que hacen algo muy emocionante:

Gen(w)→(R,P)Gen(w) \rightarrow (R, P) - Toma tu entrada biométrica con ruido w y devuelve un string uniformemente aleatorio R la clave criptográfica en sí y un string auxiliar público P. Aquí va otro dato curioso: P se puede publicar abiertamente sin comprometer la seguridad. No revela absolutamente ninguna información significativa sobre tu biometría.

Rep(w′,P)→RRep(w', P) \rightarrow R - Toma una lectura biométrica distinta w' y el mismo auxiliar público P, y reproduce exactamente el mismo R siempre que w' esté lo bastante cerca del w original. Ya sé que eso suena aterrador, pero en este caso lo bastante cerca se mide como una distancia en algún espacio métrico distancia de Hamming para strings binarios, distancia de edición para secuencias, diferencia de conjuntos para vectores de características.

La garantía de seguridad es de teoría de la información, incluso conociendo P, la clave extraída R está ε\varepsilon-cerca de la uniforme siempre que tu biometría tenga una min-entropía ≥m\geq m. No hace falta ninguna suposición de dureza computacional. Un adversario con poder de cómputo infinito sigue sin poder distinguir R de una clave realmente aleatoria con una ventaja mayor que ε\varepsilon.

Las matemáticas son preciosas. Para un fuzzy extractor con parámetros (m,ℓ,t,ε)(m, \ell, t, \varepsilon):

Cómo funciona esto en realidad

La construcción usa dos primitivas que trabajan juntas:

Secure Sketches (SS,Rec)(SS, Rec)

Un secure sketch es como un fuzzy commitment. SS(w)→sSS(w) \rightarrow s devuelve un sketch s que revela la información justa para corregir errores, pero ni de lejos la suficiente para reconstruir el propio w. Luego Rec(w′,s)→wRec(w', s) \rightarrow w recupera el w original a partir de un w' con ruido usando el sketch.

Para strings binarios con distancia de Hamming, la Syndrome Construction es óptima:

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

Lo que está pasando aquí:

Ahora, de sketch a extractor

Después de la corrección de errores, hasheas la biometría recuperada para extraer una clave uniforme, por ejemplo:

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 función fuzzy_gen():

La función fuzzy_rep():

Para las biometrías típicas:

Nota: Las representaciones biométricas en bruto contienen redundancia y correlaciones. Los códigos de iris muestran una proporción de ~8:1 entre los bits en bruto (2048) y la entropía en términos de teoría de la información (~249 bits). La extracción de claves debe respetar la entropía real, no el tamaño de la representación.

La jugada de la identidad

El modelo de confianza de SSI tiene tres actores:

El flujo completo: Biometría → Wallet → Credenciales → Pruebas ZK → Identidad verificada

Veámoslo paso a paso:

De la biometría a la 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
    };
}

El proceso:

Para Solana en concreto es una curva distinta:

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

Nota: Solana usa Ed25519, no secp256k1. Así que en tu implementación usa ed25519-hd-key para cumplir con SLIP-0010 coincide con Phantom, Solflare.

Obteniendo tus credenciales verificables

Tu wallet ya tiene un DID, pero para probar cualquier cosa necesitas unas Verifiable Credentials VC. Aquí es donde empieza el triángulo de confianza. El emisor verifica tus documentos off-chain y emite una prueba firmada criptográficamente:

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

Lo que pasó:

El schema de la credencial define qué hay en la 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..."
  }
}

Ahora, cuando un verificador te pide prueba que tienes más de 18, generas una ZKP a partir de esta credencial.

La capa de pruebas de conocimiento cero

Aquí es donde puedes probar tus datos personales, como edad, ciudadanía, un título, dirección, fecha de nacimiento, etc., sin revelar esos datos 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 };
}

Lo que se prueba:

El circuito en 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;
}

Verificación 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);
    }
}

El contrato:

Así que en este flujo puedes ver cómo tu biometría se convirtió en:

Y esto no son varios sistemas separados - es UN solo flujo unificado. La generación de la wallet ocurre una sola vez durante el registro. El flujo de ZKP se puede usar cada vez que haga falta verificar la identidad. Piénsalo así: puedes usar tu wallet biométrica igual que cualquier otra wallet cripto para transacciones normales. Pero cuando una dApp necesita KYC o cualquier tipo de verificación basada en PII como los casos de uso de Polygon ID, entra en acción la capa ZKP para probar credenciales sin exponer tus datos.

Algunos casos de uso posibles

  1. Pasaportes para agentes:

Cada agente de IA puede tener un DID derivado de su biometría. Imagina un agente de trading con IA que tiene su propia wallet y credenciales verificadas:

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

El agente:

SingularityNET está construyendo exactamente esto, un AI Agent Trust Registry donde los agentes prueban su autenticidad, seguridad y estado de auditoría usando credenciales verificables. El flujo biometric-wallet-ZKP permite que los agentes operen por su cuenta sin dejar de rendir cuentas.

  1. Capa de identidad para dApps:

Un protocolo DeFi necesita KYC pero no quiere guardar datos de usuarios responsabilidad regulatoria + un honeypot para hackers:

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

El protocolo nunca ve tu pasaporte, tu nombre ni tu dirección. Cumplimiento logrado, privacidad preservada.

El mismo patrón sirve para:

  1. Login sin contraseña en 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);
}

Cada sitio recibe una clave derivada única. No se envía ninguna contraseña, no hay credenciales que robar con phishing.

  1. Pagos biométricos:

Tu biometría autoriza la transacción:

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 ya implementa esto: todas tus wallets se derivan por completo de tu biometría "Tu cara" y se aprovisionan wallets en cadenas EVM y tokens específicos, ¡todo con tu cara!

  1. Base de datos para prevenir estafas:
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);
    }
}

Las dApps consultan antes de permitir el acceso. Los estafadores conocidos quedan bloqueados en toda la red y los que tienen un historial de actividad rara también quedan marcados, mientras que los integradores reciben una notificación de la actividad sospechosa pasada.

Integraciones para desarrolladores

Construir con identidades biométricas necesita infraestructura. El ecosistema ofrece tres componentes centrales:

Issuer Node

Las organizaciones corren issuer nodes self-hosted para emitir credenciales:

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

Funcionalidades:

Ejemplo de la 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 }
});

Ecosistema de herramientas

Algunos retos técnicos y sus soluciones

1: Variabilidad biométrica

Tu huella dactilar varía un 10-15% entre escaneos. Solución: los códigos BCH son óptimos para la corrección de errores a nivel de bit. Para un código de iris de 2048 bits que contiene ~249 bits de entropía real con un 10% de errores (~205 bits erróneos):

Distinción crítica: Los 2048 bits son la representación en BRUTO del código de iris la salida del algoritmo de Daugman. La entropía real en términos de teoría de la información es de solo ~249 bits por las correlaciones biológicas y la redundancia proporción de ~8:1.

2: Envejecimiento de la plantilla

Problema: Tu cara cambia con el tiempo envejecimiento, peso, vello facial, etc.

Solución: Registro periódico con migración de claves:

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

Vuelve a registrarte cada 2-3 años para la cara, cada 5 años para el iris, cada año para la voz, etc.

3: Detección de vida

Problema: Un atacante con tu foto no debería poder acceder a tu wallet.

Solución: Comprobaciones de vida durante la captura biométrica:

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

El Orb de WorldCoin hace esto con escaneos de iris: más de 25 comprobaciones de vida, imágenes 3D, cámaras infrarrojas + RGB. Funciona increíblemente bien - tasa de falsa aceptación <0.0001%< 0.0001\%.

4: Privacidad del string auxiliar

¿El P que expliqué antes filtra información biométrica? No. Según el teorema Dodis et al., el atacante aprende como máximo n−kn-k bits longitud del sketch, y la clave R sigue siendo estadísticamente aleatoria. Para nuestro iris: aprende 408 bits, quedan 392 bits de entropía.

¿Está listo para producción?

Las piezas ya existen:

¿Qué falta? Integraciones...

Ejemplo de implementación en producción

Así se ve usar este sistema de verdad en un entorno de producción

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

El flujo normal de la wallet y el flujo de identidad conviven. La ZKP entra en juego solo cuando hace falta una verificación, ya sea para KYC o para cualquier verificación basada en PII, como edad, ciudadanía, un título, dirección, fecha de nacimiento, etc.

Tu biometría es tu clave, tu wallet es tu identidad en todas las cadenas soportadas y nunca tienes que preocuparte por perder tu seed phrase o tus claves privadas, todo está ligado a tus atributos físicos, ¡y en la implementación de BMoni es tu cara!

Enlaces esenciales

•••
Comparte este artículo:

Mantente al día

Recibe los últimos artículos en tu bandeja de entrada.