Evita ataques de fuerza bruta en ZK: No cometas este error 🙅

Al comenzar nuestra jornada en el desarrollo de aplicaciones ZK, adaptarnos a este nuevo paradigma, centrado en la privacidad, puede resultar desafiante. En este artículo, exploraremos uno de los errores más comunes en este ámbito: escribir circuitos s…


This content originally appeared on DEV Community and was authored by Ahmed Castro

Al comenzar nuestra jornada en el desarrollo de aplicaciones ZK, adaptarnos a este nuevo paradigma, centrado en la privacidad, puede resultar desafiante. En este artículo, exploraremos uno de los errores más comunes en este ámbito: escribir circuitos sujetos al ataque de fuerza bruta.

Para ilustrarlo, construiremos una aplicación de préstamos sin colateral, donde una empresa puede emitir pruebas privadas de salario. Estas pruebas permiten que el empleado las presente en protocolos DeFi y reciba préstamos de forma automática, sin comprometer la privacidad de sus datos financieros.

Implementación ingenua y equivocada 🙅

Material de apoyo: Mi guía completa sobre ZK

Una de las primeras intuiciones de los desarrolladores provenientes de web2 o web3 sería declarar una variable privada income que represente el salario del empleado.

Aunque esta variable income es importante, no resuelve completamente el problema. Para hacerlo, necesitamos introducir un mecanismo adicional que veremos más adelante. Por ahora, examinemos cómo construir un circuito básico con esta idea y cuáles serían sus problemas.

El siguiente circuito devuelve 1 si el income es mayor a 2000, de lo contrario devuelve 0. Combinandolo con un smart contract podríamos otorgar préstamos de manera autómatica a usuarios que tengan un salario mayor a, por ejemplo 2000$. Pero si prestas atención tiene dos grandes fallas:

  1. No hay una manera de verificar que el income es auténtico: Nada impide que un usuario declare cualquier valor de income, lo que hace imposible validar de la prueba.
  2. El income puede ser descubierto mediante un ataque de fuerza bruta: En la mayoría de los backends ZK, sería posible revelar el valor de un income en una prueba simplemente generando muchas pruebas de manera secuencial hasta encontrar una que sea igual a una prueba antes enviada on-chain. Revelando así el salario de cada usaurio.
pragma circom 2.0.0;

include "circomlib/circuits/comparators.circom";

template NaiveCreditCheck() {
    signal input income;
    signal output isEligible;

    component gtComponent = GreaterThan(32);
    gtComponent.in <== [income, 2000];
    isEligible <== gtComponent.out;
}

component main = NaiveCreditCheck();

Solución

Ambos problemas pueden ser solucionados al introducir un salt como parámetro privado. El salt es una llave privada que, hasheándolo con el income, protege el valor real del salario y evita ataques de fuerza bruta.

En el circuito a continuación hasheamos el salt junto con el income para mantenerlo seguro ante ataques de fuerza bruta. Como ya sabemos, los algorimos de hasheo nos permiten ofuscar valores de manera que a partir de salt e income podemos recontruir el publicHash. Pero a partir del publicHash no podemos saber el salt e income.

publicHash juega un rol importante, pues es una variable pública que será almacenada en un smart contract controlado por el empleador y será verificado por el protocolo DeFi.

creditCheck.circom

pragma circom 2.0.0;

include "circomlib/circuits/comparators.circom";
include "circomlib/circuits/poseidon.circom";

template SecureCreditCheck() {
    signal input salt;
    signal input income;
    signal input publicHash;
    signal output isEligible;

    component gtComponent = GreaterThan(32);
    gtComponent.in <== [income, 2000];

    component poseidonComponent = Poseidon(2);
    poseidonComponent.inputs <== [salt, income];
    log(poseidonComponent.out);

    assert(poseidonComponent.out == publicHash);

    isEligible <== gtComponent.out;
    log(isEligible);
}

component main {public [publicHash]} = SecureCreditCheck();

Genera una prueba

Antes de continuar, instala circom si aún no lo tienes

curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
git clone https://github.com/iden3/circom.git
cd circom
cargo build --release
cargo install --path circom
npm install -g snarkjs

Crea tu archivo de entrada e ingresa los valores correspondientes. Ten en cuenta que publicHash es la combinación de salt e income. Si deseas modificar alguno de los inputs, deberás regenerar la prueba, lo que imprimirá en la consola el hash en la línea log(poseidonComponent.out);. Obviamente, esta prueba fallará inicialmente, así que asegúrate de actualizarla, y esta vez funcionará correctamente.

input.json

{
    "salt": "123",
    "income": "2100",
    "publicHash": "6503990210857427912452445629871225279898500973314213585899222629849816319239"
}

Asegúrate de instalar la dependencia circomlib que nos proporciona la función GreaterThan y la implementación de Poseidon.

git clone https://github.com/iden3/circomlib.git

Ahora realizamos la trusted setup.

circom creditCheck.circom --r1cs --wasm --sym --c
node creditCheck_js/generate_witness.js creditCheck_js/creditCheck.wasm input.json witness.wtns
snarkjs powersoftau new bn128 12 pot12_0000.ptau -v
snarkjs powersoftau contribute pot12_0000.ptau pot12_0001.ptau --name="First contribution" -v -e="123"
snarkjs powersoftau prepare phase2 pot12_0001.ptau pot12_final.ptau -v
snarkjs groth16 setup creditCheck.r1cs pot12_final.ptau creditCheck_0000.zkey
snarkjs zkey contribute creditCheck_0000.zkey creditCheck_0001.zkey --name="1st Contributor Name" -v  -v -e="123"
snarkjs zkey export verificationkey creditCheck_0001.zkey verification_key.json
snarkjs groth16 prove creditCheck_0001.zkey witness.wtns proof.json public.json
snarkjs groth16 verify verification_key.json public.json proof.json
snarkjs generatecall

Al finalizar, en la terminal se mostrará una prueba en el formato que la espera Remix como la que se muestra a continuación. Usaremos esta prueba más adelante en este artículo.

["0x132fcb46c5367914fba5b87838a810834952017b0f4a077fac783036ed5b6f4b", "0x1ec919d24da6b8fd247d1a121e60eafce18f436677f9b5dbeee5f4f9e887d7aa"],[["0x15fb725e134fc3190beb3cef4bc7554c759b29797bcd951de786b3e80f230c89", "0x15e50cc61eb63094ffd3b38a7b6ba6d22a614220ee0d45ed9604c1ae47ec268d"],["0x0898c8cf1920275203bec3c4e7bcdf0316448da37f3241500756945f3f236a57", "0x303c7e6da24dc86e8f40cb74ffa43885bb202ea72ddaa52030f022c29f50059d"]],["0x054b51b0b19ee17a7568209045e50b0766f715a5df6a3f35f6aff7aa91c1e987", "0x05322e325708ca2e1b93bd1e509333f8993345794c7229f164b0e30b7a0c51cc"],["0x0000000000000000000000000000000000000000000000000000000000000001","0x0e6120c4f0f48c3d7ac1f6d1d2c364b6c7af3906375777d309f881a28f7f4507"]

Luego genera el contrato verificador ZK en verifier.sol. Lánzalo on-chain.

snarkjs zkey export solidityverifier creditCheck_0001.zkey verifier.sol

Obtén el préstamo on-chain

Lanzamos el contrato del emisor de las pruebas de salario. En este tutorial usaremos remix.

// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.20;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract ProofOfSalary is Ownable {

    constructor() Ownable(msg.sender) {}

    mapping(uint proof => address employeeAccount) salaryProofs;
    function addProof(uint proof, address employee) public onlyOwner {
        salaryProofs[proof] = employee;
    }

    function getAddress(uint proof) public view returns(address) {
        return salaryProofs[proof];
    }
}

El empleador puede colocar pruebas de salario asociadas con una cuenta que será capaz de claimear los préstamos.

add Proof of salary

Ahora lanzamos el contrato de préstamos. En el constructor pasamos los dos contratos que recién lanzamos: El verificador de Circom y el de de ProofOfSalary.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

interface ICircomVerifier {
    function verifyProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[2] calldata _pubSignals) external view returns (bool);
}

interface IProofOfSalary {
    function getAddress(uint proof) external view returns(address);
}

contract ZKLoan is ERC20, ERC20Burnable, Ownable {
    ICircomVerifier circomVerifier;
    IProofOfSalary proofOfSalary;
    uint public publicInput;
    mapping(uint publicHash => bool isNullified) nullifiers;

    constructor(address circomVeriferAddress, address proofOfSalaryAddress)
        ERC20("Debt Token", "DT")
        Ownable(msg.sender)
    {
        circomVerifier = ICircomVerifier(circomVeriferAddress);
        proofOfSalary = IProofOfSalary(proofOfSalaryAddress);
    }

    // Public functions

    function getLoan(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[2] calldata _pubSignals) public {
        circomVerifier.verifyProof(_pA, _pB, _pC, _pubSignals);

        bool isEligible = _pubSignals[0] == 1;
        uint publicHash = _pubSignals[1];

        require(isEligible, "Not eligible");
        require(!nullifiers[publicHash], "Loan already processed");

        nullifiers[publicHash] = true;

        address recipient = proofOfSalary.getAddress(publicHash);
        _mint(recipient, 1 ether);

        (bool sent, bytes memory data) = msg.sender.call{value: 1 ether}("");
        data;
        require(sent, "Failed to send Ether");
    }

    function repayLoan() payable public {
        require(msg.value == 1 ether);
        _burn(msg.sender, 1 ether);
    }

    // Owner functions
    function deposit() public payable onlyOwner {
    }

    function withdraw(uint amount) public onlyOwner {
        (bool sent, bytes memory data) = msg.sender.call{value: amount}("");
        data;
        require(sent, "Failed to send Ether");
    }
}

Ahora deposita al menos 1 ether, pasándolo como value

1 ether

Deposítalo llamando la función deposit.

deposit eth

Pasa los parámetros que obtuvimos en la terminal anteriormente para sacar el préstamo llamando la función getLoan.

get loan on chain with zk

Observa cómo obtuviste la deuda en formato del token ERC20.

loan value in erc20 format

Finalmente podrás repagarla pasando de vuelta 1 ether como parámetro y verás que tus tokens de deuda serán quemados.

send 1 ether as value

repay loan

Preguntas frecuentes

¿Puede el Salt ser la llave privada de mi wallet?

¡Sí puede! De hecho, sería lo ideal pues si no lo es nos tocará guardarla en algún lugar seguro, al igual que nuestra llave privada o 12 palabras. El problema es que las wallets, con justa razón, no tienen ningún mecanismo para otorgarle las llaves privadas a una aplicación. Es por eso que usualmente archivos, también llamados "notes", que descargamos y sirven como salt o llaves privadas. También existen mecanismos de firmas con ECDSA. Cabe mencionar que en mi opinión este es el hoy el problema más grande en la UX de ZK.

¿Qué son los nullifiers?

Si prestaste atención al contrato ZKLoan a detalle, habrás observado que tiene una variable tipo mapping llamada nullifiers. Esta almacena todos los hashes publicos a los que se le han entregado préstamos anteriormente, esto se lleva a cabo den la función getLoan que también valida que solo se otorgue un préstamos por persona. Los nullifiers son un mecanismo común en construcciones ZK.

Conclusiones

Es imporante comprender cómo nuestros datos privados pueden ser expuestos a través de ataques de fuerza bruta si no utilizamos un salt o un mecanismo similar. Esto es especialmente relevante para conjuntos de datos finitos y pequeños, incluyendo salarios, edades, países de residencia, o miembros de grupos, como los holders de NFTs.

En este tutorial, aprendimos a proteger la privacidad utilizando una función de hashing como Poseidon. Si te interesa profundizar en el tema de la privacidad y ZK, te invito a ver mi guía completa de ZK y a seguirme aquí en dev.to para estar al tanto de mis nuevas publicaciónes.

Además, tocamos el tema de los préstamos on-chain. En este caso, no generamos intereses sobre el préstamo, pero si te interesa el mundo de los préstamos on-chain, te recomiendo completar mi guía sobre Aave, que permite a los usuarios generar intereses de manera intuitiva a través de su token que utiliza mecanismos de Rebase.

¡Gracias por ver este tutorial!

Sígueme en dev.to y en Youtube para todo lo relacionado al desarrollo en Blockchain en Español.


This content originally appeared on DEV Community and was authored by Ahmed Castro


Print Share Comment Cite Upload Translate Updates
APA

Ahmed Castro | Sciencx (2024-09-26T16:26:29+00:00) Evita ataques de fuerza bruta en ZK: No cometas este error 🙅. Retrieved from https://www.scien.cx/2024/09/26/evita-ataques-de-fuerza-bruta-en-zk-no-cometas-este-error-%f0%9f%99%85/

MLA
" » Evita ataques de fuerza bruta en ZK: No cometas este error 🙅." Ahmed Castro | Sciencx - Thursday September 26, 2024, https://www.scien.cx/2024/09/26/evita-ataques-de-fuerza-bruta-en-zk-no-cometas-este-error-%f0%9f%99%85/
HARVARD
Ahmed Castro | Sciencx Thursday September 26, 2024 » Evita ataques de fuerza bruta en ZK: No cometas este error 🙅., viewed ,<https://www.scien.cx/2024/09/26/evita-ataques-de-fuerza-bruta-en-zk-no-cometas-este-error-%f0%9f%99%85/>
VANCOUVER
Ahmed Castro | Sciencx - » Evita ataques de fuerza bruta en ZK: No cometas este error 🙅. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2024/09/26/evita-ataques-de-fuerza-bruta-en-zk-no-cometas-este-error-%f0%9f%99%85/
CHICAGO
" » Evita ataques de fuerza bruta en ZK: No cometas este error 🙅." Ahmed Castro | Sciencx - Accessed . https://www.scien.cx/2024/09/26/evita-ataques-de-fuerza-bruta-en-zk-no-cometas-este-error-%f0%9f%99%85/
IEEE
" » Evita ataques de fuerza bruta en ZK: No cometas este error 🙅." Ahmed Castro | Sciencx [Online]. Available: https://www.scien.cx/2024/09/26/evita-ataques-de-fuerza-bruta-en-zk-no-cometas-este-error-%f0%9f%99%85/. [Accessed: ]
rf:citation
» Evita ataques de fuerza bruta en ZK: No cometas este error 🙅 | Ahmed Castro | Sciencx | https://www.scien.cx/2024/09/26/evita-ataques-de-fuerza-bruta-en-zk-no-cometas-este-error-%f0%9f%99%85/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.