Skip to content

Commit

Permalink
Merge pull request #31 from kuronosec/review-circuits
Browse files Browse the repository at this point in the history
Modify circom circuits to improve user privacy
  • Loading branch information
kuronosec authored Dec 17, 2024
2 parents 097fa71 + 128ce44 commit 2ce9ea9
Show file tree
Hide file tree
Showing 11 changed files with 130 additions and 48 deletions.
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

0 comments on commit 2ce9ea9

Please sign in to comment.