Unlocking Privacy: Implementing COVID-19 certificates verification with Zero Knowledge Proofs
Zero-knowledge proofs have been around for quite some time their application remains largely confined to research project. You can find thousand of theory based articles on what Zero-knowledge proofs are and what problem they solve but none of these articles describe how to implement solutions to these problems. This article aims to provide practical implementation of zero-knowledge proofs with real-world use case.
What we’ll be building
We’ll be creating a ZK based system to prove that we’re vaccinated and have took required number of dosages without revealing actual number of dosages or any other personal information.
We’ll also deploy solidity verifier to verify proofs on-chain, for use cases such as airdropping NFTs to vaccinated people only!
You can find the complete source at https://github.com/antematter/zkp-covid19-certs.
How will our system work?
EU Digital Certificates
Digital certificates also called verifiable credentials are regular certificates in digital form.They contain personal information such as full name, some issuer institution data and signature of institution. In July 2021, EU started issuing digital COVID-19 certificates (https://commission.europa.eu/strategy-and-policy/coronavirus-response/safe-covid-19-vaccines-europeans/eu-digital-covid-certificate_en).
We’ll be using stripped down version of this certificate to prove to 3rd parties that we possess the certificate and have took required number of doses.
Our COVID-19 certificate in JSON would look like:
{
"data": {
"id": "3210264000943",
"name": "Humayun Javed",
"doses": 2,
"dob": 931892400,
"issuedOn": 1711566475
},
"signature": "jspjD02rynDmv542VNBuA4mzNk5MWZ0DAS5hAkj4goXaG2pgrda/C8mTNH3NpiVgG9/U/ZaXlDF79HIdCD6PAA=="
}
Govt. would issue this certificate to us and we’ll use ZKPs to prove to institutions we have this certificate and required doses without revealing any thing from this certificate not even the signature. Cool Huh?!
Setting up tooling
We’ll be using circom and snarkjs to write Zero-knowledge circuits. Follow the guide for Circom docs https://docs.circom.io/ to install.
We’ll also use Circomlib https://www.npmjs.com/package/circomlib and CircomlibJS https://www.npmjs.com/package/circomlibjs/v/0.1.2 libraries.
MiMc-7 and EdDSA
We’ll use MiMc-8 hashing and EdDSA (with MiMc-7) signature scheme as MiMc-7 and EdDSA are Zero-knowledge friendly algorithms.
Issuing certificates
Let’s write script to issue certificates. In real world, an institution would verify the data before issuing certificate.
// issue-cert.js
// ---- omitted ---
async function issueCert(id, name, doses, dob) {
// ---- omitted ---
const issuedOn = Math.floor(Date.now() / 1000); // to Unix EPOCH
const priv = Buffer.from(process.env.SIGNER_PRIVATE_KEY, `hex`); // Govt. private key to sign certs
const certInputs = [
Buffer.from(id, "utf-8"),
Buffer.from(name, "utf-8"),
doses,dob,issuedOn,
];
const hash = mimc.multiHash(certInputs); // create data hash
const sig = eddsa.signMiMC(priv, hash); // sign the hash
return {
data: {
id,name,doses,dob,issuedOn,
},
signature: Buffer.from(eddsa.packSignature(sig)).toString("base64"),
};
}
issueCert("3210264000943", "Humayun Javed", 2, 931892400).then((cert) => {
console.log(JSON.stringify(cert));
});
We get the certificates like above.
Writing Circom circuit
We’ll create circuit in circom to verify EdDSA signature.
Working of circuit:
pragma circom 2.0.0;
include "./node_modules/circomlib/circuits/eddsamimc.circom";
include "./node_modules/circomlib/circuits/mimc.circom";
include "./node_modules/circomlib/circuits/comparators.circom";
template CovidCert() {
// data fields
signal input id;
signal input name;
signal input dob;
signal input doses;
signal input issuedOn;
// signature fields
signal input R8x;
signal input R8y;
signal input S;
// public key to verify
signal input PubKeyX;
signal input PubKeyY;
// predicates
signal input requiredDoses;
// evaluate conditions
component gte = GreaterEqThan(252);
gte.in[0] <== doses;
gte.in[1] <== requiredDoses;
gte.out === 1;
// create hash
component msg = MultiMiMC7(5,91);
msg.in[0] <== id;
msg.in[1] <== name;
msg.in[2] <== doses;
msg.in[3] <== dob;
msg.in[4] <== issuedOn;
msg.k <== 0;
// verify signature
component verifier = EdDSAMiMCVerifier();
verifier.enabled <== 1;
verifier.Ax <== PubKeyX;
verifier.Ay <== PubKeyY;
verifier.R8x <== R8x;
verifier.R8y <== R8y;
verifier.S <== S;
verifier.M <== msg.out;
}
component main {public [PubKeyX,PubKeyY,requiredDoses]} = CovidCert();
Compile the circuit for Snarkjs (make sure you’ve “build” directory already created)
领英推荐
circom covid-cert.circom --r1cs --wasm -o build
Trusted setup
Follow the snarkjs guide to create Groth16 trusted setup. Part 1 (Powers of tau) is only required once while Part-2 is required for every circuit.
Powers of Tau
snarkjs powersoftau new bn128 14 setup/pot14_0000.ptau -v
snarkjs powersoftau contribute setup/pot14_0000.ptau setup/pot14_0001.ptau --name="First contribution" -v
snarkjs powersoftau prepare phase2 setup/pot14_0001.ptau setup/pot14_final.ptau -v
Make sure you choose 14 (or greater) as power of two for max number of constraints.
Phase 2
snarkjs groth16 setup build/covid-cert.r1cs setup/pot14_final.ptau setup/covid-cert_0000.zkey
snarkjs zkey contribute setup/covid-cert_0000.zkey setup/covid-cert_final.zkey --name="Self" -v
snarkjs zkey export verificationkey setup/covid-cert_final.zkey setup/covid-cert_verification_key.json
Generating Proof
Now we’ve ZKey, lets create script to generate proof
// gen-proof.js
-- snip --
async function generateProof(cert, requiredDoses, pubKey) {
const F = (await circomlib.buildBabyjub()).F;
const eddsa = await circomlib.buildEddsa();
const cData = cert.data;
const certInputs = [
F.toObject(Buffer.from(cData.id, "utf-8")),
F.toObject(Buffer.from(cData.name, "utf-8")),
cData.doses,cData.dob,cData.issuedOn,
];
const sig = eddsa.unpackSignature(Buffer.from(cert.signature, "base64"));
const inputs = {
id: certInputs[0],
name: certInputs[1],
doses: certInputs[2],
dob: certInputs[3],
issuedOn: certInputs[4],
R8x: F.toObject(sig.R8[0]),
R8y: F.toObject(sig.R8[1]),
S: sig.S,
PubKeyX: F.toObject(pubKey[0]),
PubKeyY: F.toObject(pubKey[1]),
requiredDoses
};
const { proof, publicSignals } = await snarkjs.groth16.fullProve(
inputs,
"build/covid-cert_js/covid-cert.wasm",
"setup/covid-cert_final.zkey"
);
return { proof, publicSignals };
}
(async function main() {
const eddsa = await circomlib.buildEddsa();
const priv = Buffer.from(process.env.SIGNER_PRIVATE_KEY, `hex`);
const pubKey = eddsa.prv2pub(priv);
const proof = await generateProof(
JSON.parse(fs.readFileSync("cert.json")),
2, // required doses
pubKey
);
console.log(JSON.stringify(proof));
fs.writeFileSync("proof.json", JSON.stringify(proof, null, 2));
})().then(() => process.exit(0));
We get the proof in following format
{
"proof": {
"pi_a": [...],
"pi_b": [...],
"pi_c": [...],
"protocol": "groth16",
"curve": "bn128"
},
"publicSignals": [...]
}
Verifying Proof
We verify proof off-chain first by writing a simple script
-- snip --
async function verifyProof({ proof, publicSignals }) {
const vKey = JSON.parse(
fs.readFileSync("setup/covid-cert_verification_key.json")
);
const res = await snarkjs.groth16.verify(vKey, publicSignals, proof);
if (res) {
console.log("Evaluation successful!");
} else console.log("Evaluation failed!");
}
verifyProof(JSON.parse(fs.readFileSync("proof.json"))).then(() =>
process.exit(0)
);
If the proof is valid we get “Evaluation successful!" !!!
Verifying On-chain
To verify on-chain we must export a solidity verifier contract with snarkjs
snarkjs zkey export solidityverifier setup/covid-cert_final.zkey solidity/src/covid-cert_verifier.sol
and deploy on evm compatible chain
cd solidity && forge create --rpc-url "<https://polygon-mumbai.blockpi.network/v1/rpc/public>" --private-key "YOUR_PRIVATE_KEY" "CovidCertGroth16Verifier" --etherscan-api-key "MUMBAI_SCAN_API_KEY" --verify
I’ve used forge (https://github.com/foundry-rs/foundry/) to deploy the contract but Remix or other tool can be deployed. My verifier is deployed at https://mumbai.polygonscan.com/address/0xfa494625b1229664bc59687f328adf70aed13575#code
Verification Script
-- snip --
async function verifyProof({ proof, publicSignals }) {
const abi = new ethers.Interface(
fs.readFileSync("solidity/abi.json", "utf-8")
);
const verifierContract = new ethers.Contract(DEPLOYED_ADDRESS, abi, provider);
const callData = await snarkjs.groth16.exportSolidityCallData(
proof,
publicSignals
); // generate calldata from proof
const args = JSON.parse(`[${callData}]`); // convert into ethers format
const res = await verifierContract.verifyProof(...args);
if (res) {
console.log("Evaluation successful!");
} else console.log("Evaluation failed!");
}
verifyProof(JSON.parse(fs.readFileSync("proof.json"))).then(() =>
process.exit(0)
);
and we get “Evaluation successful!” ??
Conclusion
We issued digital COVID-19 certificates to users and built zero knowledge verifiers using Circom to verify in a privacy first manner if the user has took required number of dosages or not. None of the information got exposed to verifiers not even the signature which could be used to track individuals. In real-world we can use this system to get access to airports, restaurants or even prove on-chain that we’re vaccinated.
Zero-knowledge tools have been around for a couple years now, you can start using them in production to build products prioritizing user privacy specially in medical and finance fields.
This article is written by Humayun Javed , Web3 Lead at Antematter