diff --git a/builder/Dockerfile_Debian b/builder/Dockerfile_Debian index ee8cd1f..e6a17a1 100644 --- a/builder/Dockerfile_Debian +++ b/builder/Dockerfile_Debian @@ -18,7 +18,9 @@ RUN pip install --trusted-host pypi.python.org --no-cache-dir -r /tmp/requiremen RUN mkdir /packages && chmod 777 /packages COPY src /zk-firma-digital_base -COPY build /zk-artifacts +COPY build/firma-verifier.zkey /zk-artifacts/firma-verifier.zkey +COPY build/firma-verifier_js /zk-artifacts/firma-verifier_js +COPY build/vkey.json /zk-artifacts/vkey.json COPY builder/debian/copyright /tmp/copyright COPY builder/debian/postinst /tmp/postinst diff --git a/builder/build_linux.sh b/builder/build_linux.sh index 2cb7cca..9159229 100755 --- a/builder/build_linux.sh +++ b/builder/build_linux.sh @@ -2,7 +2,7 @@ set -xe -VERSION=0.4 +VERSION=0.5 NAME='zk-firma-digital' PACKAGE='zk-firma-digital' ARCH='amd64' diff --git a/builder/entrypoint_debian.sh b/builder/entrypoint_debian.sh index 5598166..71809ef 100755 --- a/builder/entrypoint_debian.sh +++ b/builder/entrypoint_debian.sh @@ -6,8 +6,8 @@ cd /zk-firma-digital/ # Create binary installer for the app pyinstaller --clean --onefile -n zk-firma-digital --upx-dir=/usr/local/share/ \ - --noconfirm --log-level=WARN --windowed --distpath=/data/ --workpath=/tmp/zk-firma-digital_build \ - --hidden-import 'pkcs11.defaults' main.py + --noconfirm --log-level=WARN --windowed --distpath=/data/ \ + --workpath=/tmp/zk-firma-digital_build main.py mkdir -p /data/build/ cd /data/build/ @@ -66,7 +66,7 @@ cp /zk-firma-digital/os_libs/linux/${ARCH}/libASEP11.so \ cp -a /zk-firma-digital/CA-certificates/ $DEB_HOMEDIR/usr/share/zk-firma-digital/ cp -a /zk-artifacts/firma-verifier_js $DEB_HOMEDIR/usr/share/zk-firma-digital/zk-artifacts cp -a /zk-artifacts/vkey.json $DEB_HOMEDIR/usr/share/zk-firma-digital/zk-artifacts -# cp -a /zk-artifacts/firma-verifier.zkey $DEB_HOMEDIR/usr/share/zk-firma-digital/zk-artifacts +cp -a /zk-artifacts/firma-verifier.zkey $DEB_HOMEDIR/usr/share/zk-firma-digital/zk-artifacts dpkg-deb --build --root-owner-group $DEB_HOMEDIR alien -t $DEB_HOMEDIR.deb --scripts diff --git a/circuits/firma-certificate-verifier.circom b/circuits/firma-certificate-verifier.circom index a368da2..305cafc 100644 --- a/circuits/firma-certificate-verifier.circom +++ b/circuits/firma-certificate-verifier.circom @@ -19,7 +19,7 @@ include "./helpers/nullifier.circom"; /// @input nullifierSeed A random value used as an input to compute the nullifier; for example: applicationId, actionId /// @input public signalHash Any message to commit to (to make it part of the proof) /// @output pubkeyHash Poseidon hash of the RSA public key (after merging nearby chunks) -/// @output nullifier A unique value derived from nullifierSeed and Firma Digital data to nullify the proof/user +/// @output nullifier A unique value derived from nullifierSeed and the signature data to nullify the proof/user /// @output timestamp Timestamp of when the data was signed - extracted and converted to Unix timestamp /// @output ageAbove18 Boolean flag indicating age is above 18; 0 if not revealed template FirmaDigitalCRVerifier(n, k, maxDataLength) { @@ -28,6 +28,7 @@ template FirmaDigitalCRVerifier(n, k, maxDataLength) { signal input signature[k]; signal input pubKey[k]; signal input revealAgeAbove18; + signal input userSignature[k]; // Public inputs signal input nullifierSeed; @@ -59,9 +60,8 @@ template FirmaDigitalCRVerifier(n, k, maxDataLength) { // For Firma CR, age is always above 18 ageAbove18 <== revealAgeAbove18 * 1; - // TODO: create an actual Nullifier // Calculate nullifier - nullifier <== Nullifier(n, k)(nullifierSeed, signature); + nullifier <== Nullifier(n, k)(nullifierSeed, userSignature); // Dummy square to prevent signal tampering // (in rare cases where non-constrained inputs are ignored) diff --git a/circuits/helpers/signature.circom b/circuits/helpers/signature.circom index fa11bb4..ef6ac3b 100644 --- a/circuits/helpers/signature.circom +++ b/circuits/helpers/signature.circom @@ -10,7 +10,8 @@ include "@zk-email/circuits/lib/sha.circom"; /// @param n - RSA pubic key size per chunk /// @param k - Number of chunks the RSA public key is split into /// @param maxDataLength - Maximum length of the data -/// @input certDataPadded - cert data without the signature; each number represent ascii byte; remaining space is padded with 0 +/// @input certDataPadded - cert data without the signature; +/// each number represent ascii byte; remaining space is padded with 0 /// @input certDataPaddedLength - Length of padded cert data /// @input signature - RSA signature /// @input pubKey - RSA public key @@ -55,8 +56,10 @@ template SignatureVerifier(n, k, maxDataLength) { rsa.signature <== signature; // Calculate Poseidon hash of the public key (609 constraints) - // Poseidon component can take only 16 inputs, so we convert k chunks to k/2 chunks. - // We are assuming k is > 16 and <= 32 (i.e we merge two consecutive item in array to bring down the size) + // Poseidon component can take only 16 inputs, so we convert k chunks + // to k/2 chunks. + // We are assuming k is > 16 and <= 32 (i.e we merge two consecutive + // item in array to bring down the size) var poseidonInputSize = k \ 2; if (k % 2 == 1) { poseidonInputSize++; diff --git a/src/circom.py b/src/circom.py index 8c0c06a..cddca0c 100755 --- a/src/circom.py +++ b/src/circom.py @@ -1,7 +1,7 @@ #!python # Import the necessary libraries -import os.path +import os from zkpy.circuit import Circuit, GROTH, PLONK, FFLONK from zkpy.ptau import PTau @@ -10,14 +10,15 @@ # This class provides utilitis to compile the circom circuts # fot the Firma Digital class Circom(): - def __init__(self) -> None: + def __init__(self, _type="runtime") -> None: # Define variables - self.config = Configuration() + self.config = Configuration(type=_type) # Create comilation object print("Open circuit") self.circuit = Circuit(circ_file=self.config.circ_file, - output_dir=self.config.output_dir, + output_dir=str(self.config.output_dir), + node_module_dir=self.config.node_module_dir, js_dir=self.config.js_dir, r1cs=self.config.r1cs, sym_file=self.config.sym_file, @@ -32,17 +33,21 @@ def __init__(self) -> None: # Actually compile or circom code def compile_circuit(self) -> None: - print("Compile circuit") - self.circuit.compile() - self.circuit.check_circ_compiled() - print("Get info") - self.circuit.get_info() + if not self.circuit.check_circ_compiled(): + print("Compile circuit") + if not os.path.exists(self.config.output_dir): + os.makedirs(self.config.output_dir) + self.circuit.compile() + print("Get info") + self.circuit.get_info() + else: + print("Circuit already compiled.") # Start the Power of Tau ceremony # TODO: allow contributions def power_of_tau(self) -> None: - print("Power of Tau ceremony") if(not os.path.isfile(self.config.ptau_file)): + print("Power of Tau ceremony") print("start()") self.ptau.start(curve='bn128', constraints='23') print("contribute()") @@ -51,26 +56,38 @@ def power_of_tau(self) -> None: self.ptau.beacon() print("prep_phase2()") self.ptau.prep_phase2() + else: + print("power_of_tau already done") # Create the input from the user in a way snarks undertands it def generate_witness(self) -> None: - if(not os.path.isfile(self.config.witness)): - print("circuit.gen_witness") - self.circuit.gen_witness(self.config.input_file) + print("circuit.gen_witness") + self.circuit.gen_witness( + self.config.input_file, + output_file=self.config.witness) # Setup the keys based on the ceremony def setup(self) -> None: if(not os.path.isfile(self.config.output_file)): print("setup") self.circuit.setup(GROTH, self.ptau) + else: + print("Setup already done") # Calculate the ZK proof based on the circuit and the key def prove(self) -> None: print("prove") self.circuit.prove(GROTH) - print("export_vkey") - self.circuit.export_vkey(zkey_file=self.config.zkey_file, - output_file=self.config.output_file) + + def export_vkey(self) -> None: + if not os.path.isfile(self.config.output_file): + print("export_vkey") + self.circuit.export_vkey( + zkey_file=str(self.config.output_dir)+'/firma-verifier.zkey', + output_file=self.config.output_file + ) + else: + print("export_vkey already done") # Verify that the created user ZK proof is valida for the circuit # i.e. the users posses a Firma Digital but we don't any information @@ -86,7 +103,8 @@ def verify(self) -> None: # Runs the full compilation process if __name__ == "__main__": - circom = Circom() + circom = Circom(_type="compile") circom.compile_circuit() circom.power_of_tau() - circom.setup() \ No newline at end of file + circom.setup() + circom.export_vkey() \ No newline at end of file diff --git a/src/configuration.py b/src/configuration.py index 41d9fe5..83e39ba 100644 --- a/src/configuration.py +++ b/src/configuration.py @@ -7,7 +7,7 @@ os_type = platform.system() class Configuration: - def __init__(self) -> None: + def __init__(self, type="runtime") -> None: # Define OS specific paths # Check what operation system we re running on if os_type == 'Windows': @@ -20,7 +20,6 @@ def __init__(self) -> None: print("Unknown operating system") self.user_path = os.path.join(Path.home(), Path('.zk-firma-digital/')) - self.build_path = Path("build/") self.credentials_path = os.path.join(self.user_path, Path('credentials/')) self.credential_file = os.path.join(self.credentials_path, Path('credential.json')) @@ -32,14 +31,19 @@ def __init__(self) -> None: Path('CA-certificates/certificado-cadena-confianza.pem')) self.JWT_cert_path = os.path.join(self.installation_path, Path('CA-certificates/JWT_public_key.pem')) - self.output_dir = os.path.join(self.user_path, self.build_path) self.credentials_path = self.credentials_path # Define where to find the diferent components # of thew compilation process - + if type == "compile": + self.build_path = Path("../build/") + self.output_dir = self.build_path + self.js_dir = os.path.join(self.build_path, Path('firma-verifier_js/')) + elif type == "runtime": + self.build_path = Path("build/") + self.output_dir = os.path.join(self.user_path, self.build_path) + self.js_dir = os.path.join(self.zk_artifacts_path, Path('firma-verifier_js/')) # Files for proof and verification - self.js_dir = os.path.join(self.zk_artifacts_path, Path('firma-verifier_js/')) self.vkey_file = os.path.join(self.zk_artifacts_path, Path('vkey.json')) self.wasm = os.path.join(self.js_dir, Path('firma-verifier.wasm')) @@ -54,5 +58,5 @@ def __init__(self) -> None: self.r1cs = os.path.join(self.output_dir, Path('firma-verifier.r1cs')) self.sym_file = os.path.join(self.output_dir, Path('firma-verifier.sym')) self.ptau_file = os.path.join(self.output_dir, Path('firma-verifier-final.ptau')) - self.circ_file = '../circuits/firma-verifier.circom' - + self.circ_file = Path('../circuits/firma-verifier.circom') + self.node_module_dir = Path('../circuits/node_modules/') \ No newline at end of file diff --git a/src/main.py b/src/main.py index 69cadbf..acd5863 100755 --- a/src/main.py +++ b/src/main.py @@ -6,7 +6,6 @@ import json import datetime import logging -import jwt from urllib.parse import urlparse, parse_qs # We will use the PyQt6 to provide a grafical interface for the user diff --git a/src/signature.py b/src/signature.py index b9de31d..cdd5991 100644 --- a/src/signature.py +++ b/src/signature.py @@ -1,7 +1,6 @@ # Import required libraries import json import hashlib -import base64 import os import datetime import binascii @@ -130,3 +129,42 @@ def sign_file(self, file_path): logging.error(message+" "+str(error), exc_info=True) return message return "Se firmó el archivo correctamente!" + + def sign_data(self, data_to_sign): + # Open a session with the token + slots = self.pkcs11.getSlotList(tokenPresent=True) + if not slots: + raise Exception("No token found") + + slot = slots[0] + + session = None + + try: + session = self.pkcs11.openSession(slot, PyKCS11.CKF_SERIAL_SESSION | PyKCS11.CKF_RW_SESSION) + + # Login with your PIN + session.login(self.pin) + + # Find the private key object + private_key = session.findObjects([(PyKCS11.CKA_CLASS, PyKCS11.CKO_PRIVATE_KEY)])[0] + + # Canonicalize and hash the JSON data (using SHA-256) + hashed_data = hashlib.sha256(data_to_sign).hexdigest() + + # Sign the hash using the private key + mechanism = PyKCS11.Mechanism(PyKCS11.CKM_SHA256_RSA_PKCS, None) + signature = session.sign(private_key, hashed_data, mechanism) + + # Logout and close the session + session.logout() + session.closeSession() + + return signature + except PyKCS11Error as error: + message = """Hubo un error al leer la tarjeta,\ + por favor verifique que esta conectada correctamente\ + y que ingreso el pin correcto.""" + logging.error(message+" "+str(error), exc_info=True) + return message + return "Se firmó el archivo correctamente!" diff --git a/src/verification.py b/src/verification.py index ffaea12..d002c0f 100644 --- a/src/verification.py +++ b/src/verification.py @@ -1,5 +1,4 @@ # Import the required libraries -import base64 import json import os import sys @@ -9,6 +8,7 @@ from asn1crypto import pem, x509 from certvalidator import CertificateValidator, ValidationContext, errors from configuration import Configuration +from signature import Signature # This class helps to validate the certificate extracted from the smart card # to see if it actually was signed by the goverment chain of trust @@ -19,6 +19,7 @@ def __init__(self, pin, signal_hash=None): self.config = Configuration() self.user_path = self.config.user_path self.credentials_path=self.config.credentials_path + self.user_signature = Signature(pin) # We have a folder with the goverment certificates self.root_CA_path = self.config.root_CA_path @@ -98,6 +99,12 @@ def get_certificate_info(self, cert, root_cert): # Nullifier seed nullifier_seed = int.from_bytes(os.urandom(4), sys.byteorder) + self.user_signature.load_library() + user_signature = self.user_signature.sign_data(tbs_bytes) + # Convert the signature to a big integer + user_signature_int = int.from_bytes(user_signature, byteorder='big') + # Print the big integer in chunks + user_signature_str = splitToWords(user_signature_int, 121, 17) if signature_str is not None: json_data = { @@ -107,7 +114,8 @@ def get_certificate_info(self, cert, root_cert): "pubKey": public_key_str, "nullifierSeed": str(nullifier_seed), "signalHash": signal_hash, - "revealAgeAbove18": "1" + "revealAgeAbove18": "1", + "userSignature": user_signature_str } json_data = json.dumps(json_data, indent=4) with open(self.config.input_file, 'w') as json_file: diff --git a/src/zkpy/circuit.py b/src/zkpy/circuit.py index 0fd0594..701d429 100644 --- a/src/zkpy/circuit.py +++ b/src/zkpy/circuit.py @@ -42,15 +42,17 @@ def __init__( circ_file, output_dir="./", working_dir="./", + node_module_dir=None, r1cs=None, sym_file=None, js_dir=None, wasm=None, witness=None, zkey=None, - vkey=None, + vkey=None ): self.circ_file = circ_file + self.node_modules_dir = node_module_dir self.output_dir = output_dir self.working_dir = working_dir self.r1cs_file = r1cs @@ -68,11 +70,14 @@ def __init__( def compile(self): """Compiles the circuit and generates an r1cs file, a symbols file, a wasm file, and a js dir""" - subprocess.run( - ["circom", self.circ_file, "--r1cs", "--sym", "--wasm", '-o', self.output_dir], + proc = subprocess.run( + ["circom", self.circ_file, "--r1cs", "--sym", "--wasm", + '-o', self.output_dir, '-l' , self.node_modules_dir], capture_output=True, cwd=self.working_dir, + text=True ) + print(proc.stdout) self.r1cs_file = utils.get_r1cs_file(self.circ_file, self.output_dir) self.sym_file = utils.get_sym_file(self.circ_file, self.output_dir) self.wasm_file = utils.get_wasm_file(self.circ_file, self.output_dir) @@ -217,7 +222,8 @@ def prove(self, scheme, proof_out=None, public_out=None): if public_out is None: public_out = os.path.join(self.output_dir, "public.json") proc = subprocess.run( - [self.snarkjs_command, scheme, "prove", self.zkey_file, self.wtns_file, proof_out, public_out], + [self.snarkjs_command, scheme, "prove", + self.zkey_file, self.wtns_file, proof_out, public_out], capture_output=True, cwd=self.working_dir, check=True, @@ -236,7 +242,8 @@ def verify_zkey(self, ptau, zkey_file=None): if zkey_file is None: zkey_file = self.zkey_file proc = subprocess.run( - [self.snarkjs_command, "zkey", "verify", self.r1cs_file, ptau.ptau_file, zkey_file], + [self.snarkjs_command, "zkey", "verify", + self.r1cs_file, ptau.ptau_file, zkey_file], capture_output=True, cwd=self.working_dir, check=True, @@ -259,7 +266,8 @@ def export_vkey(self, zkey_file=None, output_file=None): if output_file is None: output_file = os.path.join(self.output_dir, utils.gen_rand_filename() + '.json') subprocess.run( - [self.snarkjs_command, "zkey", "export", "verificationkey", zkey_file, output_file], + [self.snarkjs_command, "zkey", "export", "verificationkey", + zkey_file, output_file], capture_output=True, cwd=self.working_dir, check=True, @@ -284,7 +292,8 @@ def verify(self, scheme, vkey_file=None, public_file=None, proof_file=None): if proof_file is None: proof_file = self.proof_file proc = subprocess.run( - [self.snarkjs_command, scheme, "verify", vkey_file, public_file, proof_file], + [self.snarkjs_command, scheme, "verify", + vkey_file, public_file, proof_file], capture_output=True, cwd=self.working_dir, check=True, @@ -326,7 +335,8 @@ def export_sol(self, output_file): if self.zkey_file is None or not utils.exists(self.zkey_file): raise ValueError(f"zkey file {self.zkey_file} does not exist") proc = subprocess.run( - [self.snarkjs_command, "zkey", "export", "solidityverifier", self.zkey_file, output_file], + [self.snarkjs_command, "zkey", "export", "solidityverifier", + self.zkey_file, output_file], capture_output=True, cwd=self.working_dir, check=True,