DEV Community

Ahmed Castro
Ahmed Castro

Posted on

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 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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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

snarkjs zkey export solidityverifier creditCheck_0001.zkey verifier.sol
Enter fullscreen mode Exit fullscreen mode

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];
    }
}
Enter fullscreen mode Exit fullscreen mode

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)