{"schemaVersion":1,"id":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication","url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication","title":"Zero-Knowledge Proofs para Autenticación: Verificación de Identidad con Preservación de Privacidad","description":"Cómo implementar autenticación basada en ZK usando zk-SNARKs, Groth16 y Plonk. Diseño de circuitos Circom, ejemplos prácticos de probar identidad sin revelar credenciales, y por qué ZK auth es útil mucho más allá de Web3.","language":"es","datePublished":"2025-11-04","dateModified":null,"contentFormat":"text/markdown","content":"Cryptography\n\n# Zero-Knowledge Proofs para Autenticación: Verificación de Identidad con Preservación de Privacidad\n\n2025-11-049 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/zero-knowledge-proofs-web3-authentication>)\n\n## La Idea Central en 30 Segundos\n\nUna prueba de conocimiento cero te permite probar que una declaración es verdadera sin revelar los datos subyacentes. Para autenticación:\n\n```\nAuth tradicional:\n Cliente envía password → Servidor checkea hash → Servidor conoce el password\n\nAuth ZK:\n Cliente genera prueba(\"Conozco un password que hashea a X\") →\n Servidor verifica prueba → Servidor no aprende NADA excepto \"sí, lo sabe\"\n```\n\nEl servidor nunca ve el password. Nunca ve valores intermedios. Solo aprende un único bit de información: la prueba es válida o no lo es.\n\nEsto no es teórico. La matemática es sólida desde los años 80. Lo nuevo es que finalmente tenemos sistemas de prueba lo suficientemente eficientes para hacer esto práctico en un navegador web.\n\n## Cómo Funcionan los zk-SNARKs (Sin los Papers Académicos)\n\nSNARK significa \"Succinct Non-interactive ARgument of Knowledge.\" Déjenme desglosar qué significa cada palabra en la práctica:\n\n- **Succinct (Sucinto)**: La prueba es pequeña (192-512 bytes) sin importar cuán compleja sea la declaración. Verificar la prueba es rápido (milisegundos).\n- **Non-interactive (No interactivo)**: El prover manda la prueba al verifier en un solo mensaje. Sin ida y vuelta.\n- **ARgument of Knowledge (Argumento de Conocimiento)**: El prover realmente conoce los datos secretos, no puede falsificar la prueba sin ellos (bajo supuestos de dureza computacional).\n\nEl proceso tiene tres fases:\n\n### 1. Definición del Circuito\n\nExpresars tu lógica de autenticación como un circuito aritmético, un grafo dirigido acíclico de compuertas de suma y multiplicación sobre un campo finito. A continuación, es donde entra [Circom](<https://docs.circom.io/>).\n\n```circom\n// credential_auth.circom\npragma circom 2.1.0;\n\ninclude \"circomlib/circuits/poseidon.circom\";\ninclude \"circomlib/circuits/comparators.circom\";\n\ntemplate CredentialAuth() {\n // Inputs privados (conocidos solo por el prover)\n signal input secret; // credencial secreta del usuario\n signal input salt; // salt random\n\n // Inputs públicos (conocidos por prover y verifier)\n signal input credentialHash; // Poseidon(secret, salt), registrado en servidor\n signal input nullifier; // previene replay\n signal input domainSeparator; // constante específica del servicio\n\n // Verificar la credencial\n component hasher = Poseidon(2);\n hasher.inputs[0] <== secret;\n hasher.inputs[1] <== salt;\n hasher.out === credentialHash;\n\n // Computar nullifier (previene replay de prueba)\n component nullifierHasher = Poseidon(2);\n nullifierHasher.inputs[0] <== secret;\n nullifierHasher.inputs[1] <== domainSeparator;\n nullifierHasher.out === nullifier;\n}\n\ncomponent main {public [credentialHash, nullifier, domainSeparator]} = CredentialAuth();\n```\n\nEste circuito dice: \"Conozco un ` secret ` y ` salt ` que hashean a ` credentialHash `, y aquí se tiene un ` nullifier ` derivado de mi secreto y tu domain separator.\" El servidor almacena ` credentialHash ` durante el registro y checkea el ` nullifier ` para prevención de replay.\n\n### 2. Trusted Setup\n\nPara Groth16, se necesita una ceremonia de trusted setup que genera la proving key y verification key para este circuito específico:\n\n```bash\n# Compilar el circuito\ncircom credential_auth.circom --r1cs --wasm --sym -o build/\n\n# Iniciar el trusted setup (ceremonia Powers of Tau)\nsnarkjs powersoftau new bn128 14 pot14_0000.ptau\nsnarkjs powersoftau contribute pot14_0000.ptau pot14_0001.ptau \\\n --name=\"Primera contribución\" -e=\"entropía random aquí\"\nsnarkjs powersoftau prepare phase2 pot14_0001.ptau pot14_final.ptau\n\n# Generar claves específicas del circuito\nsnarkjs groth16 setup build/credential_auth.r1cs pot14_final.ptau \\\n credential_auth_0000.zkey\nsnarkjs zkey contribute credential_auth_0000.zkey credential_auth_final.zkey \\\n --name=\"Contribución del circuito\" -e=\"más entropía random\"\n\n# Exportar verification key (esto va al servidor)\nsnarkjs zkey export verificationkey credential_auth_final.zkey verification_key.json\n```\n\nEl trusted setup es la mayor limitación práctica de Groth16. Si se modifica el circuito (incluso agregando un constraint), se necesita una nueva ceremonia. Por eso el setup universal de Plonk es atractivo para sistemas de auth en iteración rápida.\n\n### 3. Generación y Verificación de Pruebas\n\nDel lado del cliente (en el browser via WASM):\n\n```javascript\nimport * as snarkjs from \"snarkjs\";\n\nasync function generateAuthProof(secret, salt, credentialHash, domainSeparator) {\n // Computar el nullifier client-side\n const nullifier = poseidonHash([secret, domainSeparator]);\n\n const input = {\n secret: secret,\n salt: salt,\n credentialHash: credentialHash,\n nullifier: nullifier,\n domainSeparator: domainSeparator,\n };\n\n // Generar la prueba (toma ~1-2 segundos en browser)\n const { proof, publicSignals } = await snarkjs.groth16.fullProve(\n input,\n \"/circuits/credential_auth.wasm\",\n \"/circuits/credential_auth_final.zkey\"\n );\n\n return { proof, publicSignals };\n}\n\n// Enviar al servidor\nconst { proof, publicSignals } = await generateAuthProof(\n mySecret, mySalt, registeredHash, \"app.example.com\"\n);\n\nawait fetch(\"/api/auth/verify\", {\n method: \"POST\",\n body: JSON.stringify({ proof, publicSignals }),\n});\n```\n\nDel lado del servidor:\n\n```javascript\nimport * as snarkjs from \"snarkjs\";\nimport vkey from \"./verification_key.json\";\n\nasync function verifyAuthProof(proof, publicSignals) {\n // publicSignals[0] = credentialHash\n // publicSignals[1] = nullifier\n // publicSignals[2] = domainSeparator\n\n // Verificar la prueba ZK\n const isValid = await snarkjs.groth16.verify(vkey, publicSignals, proof);\n if (!isValid) return { authenticated: false };\n\n // Checkear que el credential hash existe en nuestra DB\n const credential = await db.findOne({\n credentialHash: publicSignals[0]\n });\n if (!credential) return { authenticated: false };\n\n // Checkear que el nullifier no fue usado (prevenir replay)\n const nullifierUsed = await db.nullifiers.findOne({\n value: publicSignals[1]\n });\n if (nullifierUsed) return { authenticated: false, error: \"proof_replayed\" };\n\n // Almacenar el nullifier\n await db.nullifiers.insertOne({\n value: publicSignals[1],\n timestamp: Date.now(),\n credentialId: credential.id,\n });\n\n return { authenticated: true, userId: credential.userId };\n}\n```\n\n## Ejemplo Práctico: Verificación de Edad Sin Revelar la Edad\n\nEste es un caso de uso convincente. A continuación, un circuito que prueba \"soy mayor de 18\" sin revelar la fecha de nacimiento real:\n\n```circom\npragma circom 2.1.0;\n\ninclude \"circomlib/circuits/poseidon.circom\";\ninclude \"circomlib/circuits/comparators.circom\";\ninclude \"circomlib/circuits/bitify.circom\";\n\ntemplate AgeVerification() {\n signal input birthDate; // privado: timestamp Unix de nacimiento\n signal input identitySecret; // privado: secreto de identidad del usuario\n signal input salt; // privado: salt random\n\n signal input currentDate; // público: timestamp Unix actual\n signal input minAge; // público: edad mínima en segundos\n signal input identityHash; // público: Poseidon(identitySecret, salt)\n\n // Verificar propiedad de identidad\n component idHasher = Poseidon(2);\n idHasher.inputs[0] <== identitySecret;\n idHasher.inputs[1] <== salt;\n idHasher.out === identityHash;\n\n // Calcular edad = currentDate - birthDate\n signal age;\n age <== currentDate - birthDate;\n\n // Probar age >= minAge usando GreaterEqThan\n component ageCheck = GreaterEqThan(64);\n ageCheck.in[0] <== age;\n ageCheck.in[1] <== minAge;\n ageCheck.out === 1;\n\n // Asegurar que birthDate es positivo y razonable\n component birthPositive = GreaterThan(64);\n birthPositive.in[0] <== birthDate;\n birthPositive.in[1] <== 0;\n birthPositive.out === 1;\n}\n\ncomponent main {public [currentDate, minAge, identityHash]} = AgeVerification();\n```\n\nEl servidor setea ` currentDate ` y ` minAge ` (18 años en segundos = 567,648,000). El usuario prueba que satisface el requerimiento de edad sin revelar nunca cuándo nació. El servidor ni siquiera puede computar en qué año nació, todo lo que aprende es \"esta persona controla la identidad y tiene suficiente edad.\"\n\n## Consideraciones de Performance\n\nA continuación, benchmarks reales de condiciones similares a producción:\n\n```\nCircuito: CredentialAuth (Poseidon hash + nullifier)\nConstraints: ~640\n\nGroth16:\n Trusted setup: ~2 segundos (una vez)\n Generación de prueba (browser WASM): 1.1 segundos\n Generación de prueba (Node.js nativo): 0.3 segundos\n Tamaño de prueba: 192 bytes\n Tiempo de verificación: 3ms\n\nPlonk:\n Setup universal: ~10 segundos (una vez, reutilizable)\n Generación de prueba (browser WASM): 2.8 segundos\n Generación de prueba (Node.js nativo): 0.8 segundos\n Tamaño de prueba: 512 bytes\n Tiempo de verificación: 5ms\n```\n\nEl número clave es el tiempo de generación de prueba en el browser. A 1-2 segundos, es usable pero no genial para UX. Mostrarías un spinner \"Generando prueba segura...\". Para apps nativas con generación de prueba acelerada por GPU, esto baja a menos de 500ms.\n\n## Más Allá de Auth: ¿Qué Más Se puede Hacer?\n\nUna vez que se tiene la infraestructura ZK lista, los mismos patrones se aplican a:\n\n**Pruebas de propiedad de credenciales:** Probar que se tiene una credencial válida del emisor X sin revelar el contenido. Considerar en: probar que se es empleado verificado de la Empresa Y sin revelar tu ID de empleado, departamento, o salario.\n\n**Membresía a conjuntos:** Probar que se está en una whitelist sin revelar cuál miembro sos. Útil para control de acceso donde se quiere unlinkability, el servidor sabe \"un usuario autorizado accedió a este recurso\" pero no puede decir cuál.\n\n```circom\ntemplate MerkleProof(levels) {\n signal input leaf;\n signal input pathElements[levels];\n signal input pathIndices[levels];\n signal input root; // público: raíz Merkle de la whitelist\n\n component hashers[levels];\n signal intermediate[levels + 1];\n intermediate[0] <== leaf;\n\n for (var i = 0; i < levels; i++) {\n hashers[i] = Poseidon(2);\n hashers[i].inputs[0] <== intermediate[i] + (pathElements[i] - intermediate[i]) * pathIndices[i];\n hashers[i].inputs[1] <== pathElements[i] + (intermediate[i] - pathElements[i]) * pathIndices[i];\n intermediate[i + 1] <== hashers[i].out;\n }\n\n root === intermediate[levels];\n}\n```\n\n**Pruebas de rango:** Probar que un valor está dentro de un rango sin revelar el valor. El score crediticio está arriba de 700. El saldo de cuenta está arriba de $1000.\n\n## La Evaluación Honesta\n\nZK auth tiene limitaciones reales que la máquina de hype ignora:\n\n1. **La gestión de claves es problema del usuario.** Si pierden su secreto, no hay flujo de \"olvidé mi password\". Necesitás un mecanismo de recuperación, que típicamente involucra un tercero de confianza (venciendo algunos beneficios de privacidad) o recuperación social (UX compleja).\n2. **La generación de pruebas en dispositivos low-end es lenta.** Un teléfono Android de $100 puede tardar 5-8 segundos en generar una prueba Groth16. Eso mata la UX para apps mainstream.\n3. **El desarrollo de circuitos es difícil.** Circom no es un lenguaje de propósito general. Debuggear sistemas de constraints es fundamentalmente diferente a debuggear código imperativo. Errores de off-by-one en lógica de constraints pueden crear bugs de soundness que permiten a atacantes falsificar pruebas.\n4. **La cuestión de la ceremonia de setup.** Para Groth16, si el trusted setup está comprometido, un atacante puede falsificar pruebas. Las ceremonias de multi-party computation mitigan esto, pero agregan complejidad operacional.\n\nA pesar de todo esto, la autenticación ZK probablemente se vuelva mainstream dentro de cinco años. No como reemplazo de OAuth en todos lados, sino como herramienta especializada para flujos sensibles a la privacidad: verificación de edad, checkeo de credenciales, votación anónima, y control de acceso donde la unlinkability importa.\n\nLa criptografía es sólida. El tooling está mejorando rápidamente. La pieza faltante es UX, y eso es un problema de ingeniería resoluble. Para quienes construyen sistemas que manejan datos sensibles de identidad, invertir en entender los fundamentos de ZK ahora vale la pena. Para cuando las librerías de ZK auth sean tan fáciles de usar como ` bcrypt `, tener ese conocimiento fundacional será una ventaja significativa.\n\n## Preguntas frecuentes\n\n### ¿Necesito una blockchain para usar zero-knowledge proofs para autenticación?\n\nAbsolutamente no. Las pruebas ZK son una primitiva matemática, funcionan perfectamente bien con un verificador server-side tradicional. El hype blockchain alrededor de ZK ha oscurecido el hecho de que ZK auth es útil para cualquier sistema donde quieras probar identidad sin revelar credenciales.\n\n### ¿Cuánto tarda generar una prueba ZK en el dispositivo del usuario?\n\nPara una prueba de credencial simple (preimagen de hash con Poseidon), aproximadamente 500ms-2s en un smartphone moderno usando provers compilados a WASM. Groth16 es más rápido para probar pero necesita parámetros por circuito descargados previamente. Plonk es más lento pero más flexible.\n\n### ¿La autenticación ZK está lista para producción en 2026?\n\nPara case es de uso específicos, sí. Verificación de edad, propiedad de credenciales y pruebas de membresía están listas para producción. Auth ZK de propósito general reemplazando OAuth todavía es experimental debido a desafíos de UX (tiempo de generación de prueba, gestión de claves).\n\n## Tips\n\n1. **Trade-off Groth16 vs Plonk:** Groth16 produce las pruebas más chicas (~192 bytes) y la verificación más rápida (~3ms), pero requiere un **trusted setup por circuito**. Plonk usa un trusted setup universal que funciona para cualquier circuito hasta un tamaño fijo, con pruebas levemente más grandes (~512 bytes) y ~5ms de verificación. Para auth en producción, utilizar Plonk a menos que el tamaño de prueba sea crítico (ej., verificación on-chain donde el gas importa).\n2. **Asignación vs constraint de señales en Circom:** En Circom, ` <-- ` asigna un valor a una señal (computación del witness) mientras que ` === ` crea un constraint (verificación de prueba). Un bug común es usar ` <-- ` sin un ` === ` correspondiente, lo que significa que el prover puede asignar cualquier valor sin que el verifier lo detecte. Siempre utilizar ` <== ` (asignar + restringir) a menos que tengas una razón específica para separarlos.\n3. **Poseidon sobre Pedersen para hashing ZK:** Utilizar el hash ` Poseidon ` de circomlib en vez de ` Pedersen ` para hashing de credenciales. Poseidon requiere ~300 constraints vs ~1,500 de Pedersen para un hash de 256 bits. Menos constraints = generación de prueba más rápida. En flujos de auth donde el usuario genera pruebas client-side, esto impacta directamente la latencia de UX.\n4. **Patrón de nullifier para prevención de replay:** Toda prueba de ZK auth debería incluir un ` nullifier = Poseidon(secret, domain_separator) `. El verifier almacena nullifiers usados. Esto previene replay de pruebas sin revelar qué credencial se usó. Utilizar un ` domain_separator ` único por servicio para prevenir correlación de nullifiers entre servicios.\n\n- zero-knowledge-proofs\n- zk-snarks\n- groth16\n- plonk\n- circom\n- authentication\n- cryptography\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)","headings":[{"level":1,"text":"Zero-Knowledge Proofs para Autenticación: Verificación de Identidad con Preservación de Privacidad","url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#title"},{"level":2,"text":"La Idea Central en 30 Segundos","url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#la-idea-central-en-30-segundos"},{"level":2,"text":"Cómo Funcionan los zk-SNARKs (Sin los Papers Académicos)","url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#cmo-funcionan-los-zk-snarks-sin-los-papers-acadmicos"},{"level":3,"text":"1. Definición del Circuito","url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#1-definicin-del-circuito"},{"level":3,"text":"2. Trusted Setup","url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#2-trusted-setup"},{"level":3,"text":"3. Generación y Verificación de Pruebas","url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#3-generacin-y-verificacin-de-pruebas"},{"level":2,"text":"Ejemplo Práctico: Verificación de Edad Sin Revelar la Edad","url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#ejemplo-prctico-verificacin-de-edad-sin-revelar-la-edad"},{"level":2,"text":"Consideraciones de Performance","url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#consideraciones-de-performance"},{"level":2,"text":"Más Allá de Auth: ¿Qué Más Se puede Hacer?","url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#ms-all-de-auth-qu-ms-se-puede-hacer"},{"level":2,"text":"La Evaluación Honesta","url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#la-evaluacin-honesta"},{"level":2,"text":"Preguntas frecuentes","url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#questions"},{"level":3,"text":"¿Necesito una blockchain para usar zero-knowledge proofs para autenticación?","url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#question-1"},{"level":3,"text":"¿Cuánto tarda generar una prueba ZK en el dispositivo del usuario?","url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#question-2"},{"level":3,"text":"¿La autenticación ZK está lista para producción en 2026?","url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#article-tips"}],"sections":[{"heading":"Zero-Knowledge Proofs para Autenticación: Verificación de Identidad con Preservación de Privacidad","level":1,"url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication","content":"Cryptography"},{"heading":"Zero-Knowledge Proofs para Autenticación: Verificación de Identidad con Preservación de Privacidad","level":1,"url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#title","content":"2025-11-049 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/zero-knowledge-proofs-web3-authentication>)"},{"heading":"La Idea Central en 30 Segundos","level":2,"url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#la-idea-central-en-30-segundos","content":"Una prueba de conocimiento cero te permite probar que una declaración es verdadera sin revelar los datos subyacentes. Para autenticación:\n\n```\nAuth tradicional:\n Cliente envía password → Servidor checkea hash → Servidor conoce el password\n\nAuth ZK:\n Cliente genera prueba(\"Conozco un password que hashea a X\") →\n Servidor verifica prueba → Servidor no aprende NADA excepto \"sí, lo sabe\"\n```\n\nEl servidor nunca ve el password. Nunca ve valores intermedios. Solo aprende un único bit de información: la prueba es válida o no lo es.\n\nEsto no es teórico. La matemática es sólida desde los años 80. Lo nuevo es que finalmente tenemos sistemas de prueba lo suficientemente eficientes para hacer esto práctico en un navegador web."},{"heading":"Cómo Funcionan los zk-SNARKs (Sin los Papers Académicos)","level":2,"url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#cmo-funcionan-los-zk-snarks-sin-los-papers-acadmicos","content":"SNARK significa \"Succinct Non-interactive ARgument of Knowledge.\" Déjenme desglosar qué significa cada palabra en la práctica:\n\n- **Succinct (Sucinto)**: La prueba es pequeña (192-512 bytes) sin importar cuán compleja sea la declaración. Verificar la prueba es rápido (milisegundos).\n- **Non-interactive (No interactivo)**: El prover manda la prueba al verifier en un solo mensaje. Sin ida y vuelta.\n- **ARgument of Knowledge (Argumento de Conocimiento)**: El prover realmente conoce los datos secretos, no puede falsificar la prueba sin ellos (bajo supuestos de dureza computacional).\n\nEl proceso tiene tres fases:"},{"heading":"1. Definición del Circuito","level":3,"url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#1-definicin-del-circuito","content":"Expresars tu lógica de autenticación como un circuito aritmético, un grafo dirigido acíclico de compuertas de suma y multiplicación sobre un campo finito. A continuación, es donde entra [Circom](<https://docs.circom.io/>).\n\n```circom\n// credential_auth.circom\npragma circom 2.1.0;\n\ninclude \"circomlib/circuits/poseidon.circom\";\ninclude \"circomlib/circuits/comparators.circom\";\n\ntemplate CredentialAuth() {\n // Inputs privados (conocidos solo por el prover)\n signal input secret; // credencial secreta del usuario\n signal input salt; // salt random\n\n // Inputs públicos (conocidos por prover y verifier)\n signal input credentialHash; // Poseidon(secret, salt), registrado en servidor\n signal input nullifier; // previene replay\n signal input domainSeparator; // constante específica del servicio\n\n // Verificar la credencial\n component hasher = Poseidon(2);\n hasher.inputs[0] <== secret;\n hasher.inputs[1] <== salt;\n hasher.out === credentialHash;\n\n // Computar nullifier (previene replay de prueba)\n component nullifierHasher = Poseidon(2);\n nullifierHasher.inputs[0] <== secret;\n nullifierHasher.inputs[1] <== domainSeparator;\n nullifierHasher.out === nullifier;\n}\n\ncomponent main {public [credentialHash, nullifier, domainSeparator]} = CredentialAuth();\n```\n\nEste circuito dice: \"Conozco un ` secret ` y ` salt ` que hashean a ` credentialHash `, y aquí se tiene un ` nullifier ` derivado de mi secreto y tu domain separator.\" El servidor almacena ` credentialHash ` durante el registro y checkea el ` nullifier ` para prevención de replay."},{"heading":"2. Trusted Setup","level":3,"url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#2-trusted-setup","content":"Para Groth16, se necesita una ceremonia de trusted setup que genera la proving key y verification key para este circuito específico:\n\n```bash\n# Compilar el circuito\ncircom credential_auth.circom --r1cs --wasm --sym -o build/\n\n# Iniciar el trusted setup (ceremonia Powers of Tau)\nsnarkjs powersoftau new bn128 14 pot14_0000.ptau\nsnarkjs powersoftau contribute pot14_0000.ptau pot14_0001.ptau \\\n --name=\"Primera contribución\" -e=\"entropía random aquí\"\nsnarkjs powersoftau prepare phase2 pot14_0001.ptau pot14_final.ptau\n\n# Generar claves específicas del circuito\nsnarkjs groth16 setup build/credential_auth.r1cs pot14_final.ptau \\\n credential_auth_0000.zkey\nsnarkjs zkey contribute credential_auth_0000.zkey credential_auth_final.zkey \\\n --name=\"Contribución del circuito\" -e=\"más entropía random\"\n\n# Exportar verification key (esto va al servidor)\nsnarkjs zkey export verificationkey credential_auth_final.zkey verification_key.json\n```\n\nEl trusted setup es la mayor limitación práctica de Groth16. Si se modifica el circuito (incluso agregando un constraint), se necesita una nueva ceremonia. Por eso el setup universal de Plonk es atractivo para sistemas de auth en iteración rápida."},{"heading":"3. Generación y Verificación de Pruebas","level":3,"url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#3-generacin-y-verificacin-de-pruebas","content":"Del lado del cliente (en el browser via WASM):\n\n```javascript\nimport * as snarkjs from \"snarkjs\";\n\nasync function generateAuthProof(secret, salt, credentialHash, domainSeparator) {\n // Computar el nullifier client-side\n const nullifier = poseidonHash([secret, domainSeparator]);\n\n const input = {\n secret: secret,\n salt: salt,\n credentialHash: credentialHash,\n nullifier: nullifier,\n domainSeparator: domainSeparator,\n };\n\n // Generar la prueba (toma ~1-2 segundos en browser)\n const { proof, publicSignals } = await snarkjs.groth16.fullProve(\n input,\n \"/circuits/credential_auth.wasm\",\n \"/circuits/credential_auth_final.zkey\"\n );\n\n return { proof, publicSignals };\n}\n\n// Enviar al servidor\nconst { proof, publicSignals } = await generateAuthProof(\n mySecret, mySalt, registeredHash, \"app.example.com\"\n);\n\nawait fetch(\"/api/auth/verify\", {\n method: \"POST\",\n body: JSON.stringify({ proof, publicSignals }),\n});\n```\n\nDel lado del servidor:\n\n```javascript\nimport * as snarkjs from \"snarkjs\";\nimport vkey from \"./verification_key.json\";\n\nasync function verifyAuthProof(proof, publicSignals) {\n // publicSignals[0] = credentialHash\n // publicSignals[1] = nullifier\n // publicSignals[2] = domainSeparator\n\n // Verificar la prueba ZK\n const isValid = await snarkjs.groth16.verify(vkey, publicSignals, proof);\n if (!isValid) return { authenticated: false };\n\n // Checkear que el credential hash existe en nuestra DB\n const credential = await db.findOne({\n credentialHash: publicSignals[0]\n });\n if (!credential) return { authenticated: false };\n\n // Checkear que el nullifier no fue usado (prevenir replay)\n const nullifierUsed = await db.nullifiers.findOne({\n value: publicSignals[1]\n });\n if (nullifierUsed) return { authenticated: false, error: \"proof_replayed\" };\n\n // Almacenar el nullifier\n await db.nullifiers.insertOne({\n value: publicSignals[1],\n timestamp: Date.now(),\n credentialId: credential.id,\n });\n\n return { authenticated: true, userId: credential.userId };\n}\n```"},{"heading":"Ejemplo Práctico: Verificación de Edad Sin Revelar la Edad","level":2,"url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#ejemplo-prctico-verificacin-de-edad-sin-revelar-la-edad","content":"Este es un caso de uso convincente. A continuación, un circuito que prueba \"soy mayor de 18\" sin revelar la fecha de nacimiento real:\n\n```circom\npragma circom 2.1.0;\n\ninclude \"circomlib/circuits/poseidon.circom\";\ninclude \"circomlib/circuits/comparators.circom\";\ninclude \"circomlib/circuits/bitify.circom\";\n\ntemplate AgeVerification() {\n signal input birthDate; // privado: timestamp Unix de nacimiento\n signal input identitySecret; // privado: secreto de identidad del usuario\n signal input salt; // privado: salt random\n\n signal input currentDate; // público: timestamp Unix actual\n signal input minAge; // público: edad mínima en segundos\n signal input identityHash; // público: Poseidon(identitySecret, salt)\n\n // Verificar propiedad de identidad\n component idHasher = Poseidon(2);\n idHasher.inputs[0] <== identitySecret;\n idHasher.inputs[1] <== salt;\n idHasher.out === identityHash;\n\n // Calcular edad = currentDate - birthDate\n signal age;\n age <== currentDate - birthDate;\n\n // Probar age >= minAge usando GreaterEqThan\n component ageCheck = GreaterEqThan(64);\n ageCheck.in[0] <== age;\n ageCheck.in[1] <== minAge;\n ageCheck.out === 1;\n\n // Asegurar que birthDate es positivo y razonable\n component birthPositive = GreaterThan(64);\n birthPositive.in[0] <== birthDate;\n birthPositive.in[1] <== 0;\n birthPositive.out === 1;\n}\n\ncomponent main {public [currentDate, minAge, identityHash]} = AgeVerification();\n```\n\nEl servidor setea ` currentDate ` y ` minAge ` (18 años en segundos = 567,648,000). El usuario prueba que satisface el requerimiento de edad sin revelar nunca cuándo nació. El servidor ni siquiera puede computar en qué año nació, todo lo que aprende es \"esta persona controla la identidad y tiene suficiente edad.\""},{"heading":"Consideraciones de Performance","level":2,"url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#consideraciones-de-performance","content":"A continuación, benchmarks reales de condiciones similares a producción:\n\n```\nCircuito: CredentialAuth (Poseidon hash + nullifier)\nConstraints: ~640\n\nGroth16:\n Trusted setup: ~2 segundos (una vez)\n Generación de prueba (browser WASM): 1.1 segundos\n Generación de prueba (Node.js nativo): 0.3 segundos\n Tamaño de prueba: 192 bytes\n Tiempo de verificación: 3ms\n\nPlonk:\n Setup universal: ~10 segundos (una vez, reutilizable)\n Generación de prueba (browser WASM): 2.8 segundos\n Generación de prueba (Node.js nativo): 0.8 segundos\n Tamaño de prueba: 512 bytes\n Tiempo de verificación: 5ms\n```\n\nEl número clave es el tiempo de generación de prueba en el browser. A 1-2 segundos, es usable pero no genial para UX. Mostrarías un spinner \"Generando prueba segura...\". Para apps nativas con generación de prueba acelerada por GPU, esto baja a menos de 500ms."},{"heading":"Más Allá de Auth: ¿Qué Más Se puede Hacer?","level":2,"url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#ms-all-de-auth-qu-ms-se-puede-hacer","content":"Una vez que se tiene la infraestructura ZK lista, los mismos patrones se aplican a:\n\n**Pruebas de propiedad de credenciales:** Probar que se tiene una credencial válida del emisor X sin revelar el contenido. Considerar en: probar que se es empleado verificado de la Empresa Y sin revelar tu ID de empleado, departamento, o salario.\n\n**Membresía a conjuntos:** Probar que se está en una whitelist sin revelar cuál miembro sos. Útil para control de acceso donde se quiere unlinkability, el servidor sabe \"un usuario autorizado accedió a este recurso\" pero no puede decir cuál.\n\n```circom\ntemplate MerkleProof(levels) {\n signal input leaf;\n signal input pathElements[levels];\n signal input pathIndices[levels];\n signal input root; // público: raíz Merkle de la whitelist\n\n component hashers[levels];\n signal intermediate[levels + 1];\n intermediate[0] <== leaf;\n\n for (var i = 0; i < levels; i++) {\n hashers[i] = Poseidon(2);\n hashers[i].inputs[0] <== intermediate[i] + (pathElements[i] - intermediate[i]) * pathIndices[i];\n hashers[i].inputs[1] <== pathElements[i] + (intermediate[i] - pathElements[i]) * pathIndices[i];\n intermediate[i + 1] <== hashers[i].out;\n }\n\n root === intermediate[levels];\n}\n```\n\n**Pruebas de rango:** Probar que un valor está dentro de un rango sin revelar el valor. El score crediticio está arriba de 700. El saldo de cuenta está arriba de $1000."},{"heading":"La Evaluación Honesta","level":2,"url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#la-evaluacin-honesta","content":"ZK auth tiene limitaciones reales que la máquina de hype ignora:\n\n1. **La gestión de claves es problema del usuario.** Si pierden su secreto, no hay flujo de \"olvidé mi password\". Necesitás un mecanismo de recuperación, que típicamente involucra un tercero de confianza (venciendo algunos beneficios de privacidad) o recuperación social (UX compleja).\n2. **La generación de pruebas en dispositivos low-end es lenta.** Un teléfono Android de $100 puede tardar 5-8 segundos en generar una prueba Groth16. Eso mata la UX para apps mainstream.\n3. **El desarrollo de circuitos es difícil.** Circom no es un lenguaje de propósito general. Debuggear sistemas de constraints es fundamentalmente diferente a debuggear código imperativo. Errores de off-by-one en lógica de constraints pueden crear bugs de soundness que permiten a atacantes falsificar pruebas.\n4. **La cuestión de la ceremonia de setup.** Para Groth16, si el trusted setup está comprometido, un atacante puede falsificar pruebas. Las ceremonias de multi-party computation mitigan esto, pero agregan complejidad operacional.\n\nA pesar de todo esto, la autenticación ZK probablemente se vuelva mainstream dentro de cinco años. No como reemplazo de OAuth en todos lados, sino como herramienta especializada para flujos sensibles a la privacidad: verificación de edad, checkeo de credenciales, votación anónima, y control de acceso donde la unlinkability importa.\n\nLa criptografía es sólida. El tooling está mejorando rápidamente. La pieza faltante es UX, y eso es un problema de ingeniería resoluble. Para quienes construyen sistemas que manejan datos sensibles de identidad, invertir en entender los fundamentos de ZK ahora vale la pena. Para cuando las librerías de ZK auth sean tan fáciles de usar como ` bcrypt `, tener ese conocimiento fundacional será una ventaja significativa."},{"heading":"Preguntas frecuentes","level":2,"url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#questions","content":""},{"heading":"¿Necesito una blockchain para usar zero-knowledge proofs para autenticación?","level":3,"url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#question-1","content":"Absolutamente no. Las pruebas ZK son una primitiva matemática, funcionan perfectamente bien con un verificador server-side tradicional. El hype blockchain alrededor de ZK ha oscurecido el hecho de que ZK auth es útil para cualquier sistema donde quieras probar identidad sin revelar credenciales."},{"heading":"¿Cuánto tarda generar una prueba ZK en el dispositivo del usuario?","level":3,"url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#question-2","content":"Para una prueba de credencial simple (preimagen de hash con Poseidon), aproximadamente 500ms-2s en un smartphone moderno usando provers compilados a WASM. Groth16 es más rápido para probar pero necesita parámetros por circuito descargados previamente. Plonk es más lento pero más flexible."},{"heading":"¿La autenticación ZK está lista para producción en 2026?","level":3,"url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#question-3","content":"Para case es de uso específicos, sí. Verificación de edad, propiedad de credenciales y pruebas de membresía están listas para producción. Auth ZK de propósito general reemplazando OAuth todavía es experimental debido a desafíos de UX (tiempo de generación de prueba, gestión de claves)."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#article-tips","content":"1. **Trade-off Groth16 vs Plonk:** Groth16 produce las pruebas más chicas (~192 bytes) y la verificación más rápida (~3ms), pero requiere un **trusted setup por circuito**. Plonk usa un trusted setup universal que funciona para cualquier circuito hasta un tamaño fijo, con pruebas levemente más grandes (~512 bytes) y ~5ms de verificación. Para auth en producción, utilizar Plonk a menos que el tamaño de prueba sea crítico (ej., verificación on-chain donde el gas importa).\n2. **Asignación vs constraint de señales en Circom:** En Circom, ` <-- ` asigna un valor a una señal (computación del witness) mientras que ` === ` crea un constraint (verificación de prueba). Un bug común es usar ` <-- ` sin un ` === ` correspondiente, lo que significa que el prover puede asignar cualquier valor sin que el verifier lo detecte. Siempre utilizar ` <== ` (asignar + restringir) a menos que tengas una razón específica para separarlos.\n3. **Poseidon sobre Pedersen para hashing ZK:** Utilizar el hash ` Poseidon ` de circomlib en vez de ` Pedersen ` para hashing de credenciales. Poseidon requiere ~300 constraints vs ~1,500 de Pedersen para un hash de 256 bits. Menos constraints = generación de prueba más rápida. En flujos de auth donde el usuario genera pruebas client-side, esto impacta directamente la latencia de UX.\n4. **Patrón de nullifier para prevención de replay:** Toda prueba de ZK auth debería incluir un ` nullifier = Poseidon(secret, domain_separator) `. El verifier almacena nullifiers usados. Esto previene replay de pruebas sin revelar qué credencial se usó. Utilizar un ` domain_separator ` único por servicio para prevenir correlación de nullifiers entre servicios.\n\n- zero-knowledge-proofs\n- zk-snarks\n- groth16\n- plonk\n- circom\n- authentication\n- cryptography\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)"}],"links":[{"text":"English","url":"https://lucioduran.com/blog/zero-knowledge-proofs-web3-authentication"},{"text":"Circom","url":"https://docs.circom.io/"},{"text":"Volver al Blog","url":"https://lucioduran.com/es/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication"},"headline":"Zero-Knowledge Proofs para Autenticación: Verificación de Identidad con Preservación de Privacidad","description":"Cómo implementar autenticación basada en ZK usando zk-SNARKs, Groth16 y Plonk. Diseño de circuitos Circom, ejemplos prácticos de probar identidad sin revelar credenciales, y por qué ZK auth es útil mucho más allá de Web3.","datePublished":"2025-11-04","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://lucioduran.com/favicons/og-default.png","url":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication","inLanguage":"es","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"Cryptography","keywords":"zero-knowledge-proofs, zk-snarks, groth16, plonk, circom, authentication, cryptography","wordCount":1610,"timeRequired":"PT9M","workTranslation":[{"@type":"BlogPosting","inLanguage":"en","url":"https://lucioduran.com/blog/zero-knowledge-proofs-web3-authentication"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Inicio","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/es/blog"},{"@type":"ListItem","position":3,"name":"Zero-Knowledge Proofs para Autenticación: Verificación de Identidad con Preservación de Privacidad","item":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"¿Necesito una blockchain para usar zero-knowledge proofs para autenticación?","acceptedAnswer":{"@type":"Answer","text":"Absolutamente no. Las pruebas ZK son una primitiva matemática, funcionan perfectamente bien con un verificador server-side tradicional. El hype blockchain alrededor de ZK ha oscurecido el hecho de que ZK auth es útil para cualquier sistema donde quieras probar identidad sin revelar credenciales."}},{"@type":"Question","name":"¿Cuánto tarda generar una prueba ZK en el dispositivo del usuario?","acceptedAnswer":{"@type":"Answer","text":"Para una prueba de credencial simple (preimagen de hash con Poseidon), aproximadamente 500ms-2s en un smartphone moderno usando provers compilados a WASM. Groth16 es más rápido para probar pero necesita parámetros por circuito descargados previamente. Plonk es más lento pero más flexible."}},{"@type":"Question","name":"¿La autenticación ZK está lista para producción en 2026?","acceptedAnswer":{"@type":"Answer","text":"Para case es de uso específicos, sí. Verificación de edad, propiedad de credenciales y pruebas de membresía están listas para producción. Auth ZK de propósito general reemplazando OAuth todavía es experimental debido a desafíos de UX (tiempo de generación de prueba, gestión de claves)."}}]}]}],"contentHash":"71b6fba89beb9befbd27bdc5677c1a165503cdb20efdb61e2ef020c73017a03b","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/es/blog/zero-knowledge-proofs-web3-authentication","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":69881,"contentBytes":15651}}
