Unlocking Privacy: Implementing COVID-19 certificates verification with Zero Knowledge Proofs

Let's navigate the realm of secure digital health credentials with this implementation guide for COVID-19 certificate verification using Zero Knowledge Proofs. Protect personal data while affirming vaccination status with the technological finesse of blockchain—tailored for real-world application.

💡 Articles
9 April 2024
Article Image

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?

  • We’ll issue certificates with digital signatures to users.
  • We’ll build Zero-knowedge circuit which would verify signature validity.
  • Our users would generate ZK proofs using certificate issued to them.
  • Verifiers would verify the proof by checking that the circuit evaluated correctly.

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:

  • We’ll pass certificate data fields and signature as private fields
  • We’ll pass issue public key as public input
  • We’ll compute MiMc-7 hash using the data fields provided
  • Circuit would evaluate that indeed the private key holder of this public key signed the certificate.
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.