Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modify circom circuits to improve user privacy #31

Merged
merged 4 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion builder/Dockerfile_Debian
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion builder/build_linux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

set -xe

VERSION=0.4
VERSION=0.5
NAME='zk-firma-digital'
PACKAGE='zk-firma-digital'
ARCH='amd64'
Expand Down
6 changes: 3 additions & 3 deletions builder/entrypoint_debian.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions circuits/firma-certificate-verifier.circom
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
9 changes: 6 additions & 3 deletions circuits/helpers/signature.circom
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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++;
Expand Down
54 changes: 36 additions & 18 deletions src/circom.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
Expand All @@ -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()")
Expand All @@ -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
Expand All @@ -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()
circom.setup()
circom.export_vkey()
18 changes: 11 additions & 7 deletions src/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand All @@ -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'))
Expand All @@ -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'))

Expand All @@ -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/')
1 change: 0 additions & 1 deletion src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 39 additions & 1 deletion src/signature.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Import required libraries
import json
import hashlib
import base64
import os
import datetime
import binascii
Expand Down Expand Up @@ -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!"
12 changes: 10 additions & 2 deletions src/verification.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# Import the required libraries
import base64
import json
import os
import sys
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 = {
Expand All @@ -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:
Expand Down
Loading