A HIPAA-aware zero-knowledge proof system that proves a patient qualifies for a clinical trial without ever exposing their medical record, privacy enforced by mathematics instead of policy or trust.
To enroll in a clinical trial, a patient normally hands their full medical record to the trial sponsor so the sponsor can check eligibility. The sponsor only needs one bit of information, do you qualify, yes or no?, but to get it they receive everything: diagnoses, lab values, medications, age.
HIPAA's "minimum necessary" rule says you should only share the minimum data required. In practice that rule is enforced by policy and trust: contracts, audits, and the hope that nobody misuses the data. I wanted to enforce it with math instead.
I wrote a Circom arithmetic circuit enforcing eight constraints over private inputs: an age range check (18–65), two required ICD-10 diagnosis codes verified by set membership, a consent flag, a lab value within range, and a medication-exclusion check.
The circuit compiles to an R1CS constraint system and is proven with Groth16 zk-SNARKs (snarkjs) over the BN254 elliptic curve. The output is a ~192-byte proof that anyone can verify but that reveals none of the inputs.
Because the record's 25 private values exceed circomlib's 16-input cap, I built a two-stage Poseidon hash: commitment = Poseidon( Poseidon(age, 14 diagnosis codes, consent), Poseidon(lab value, 8 medication codes) ). This stops a proof being reused for a different patient.
A hospital prover (Node / Express with AES-256-GCM-encrypted SQLite) holds the PHI behind a firewall and emits the proof. A separate sponsor verifier checks the proof against a public key and learns only eligible: true / false.
A Flutter intake app, an EHR-style provider portal, and a public landing page with a math explainer. Validated end to end with 10 synthetic patients (3 eligible, 7 ineligible) through create → prove → verify.
A proof is only useful if it cannot be copied onto a different patient. That is the job of the commitment. Before any proof is made, the hospital hashes the record's 25 private values into a single number with Poseidon, a hash function built to run cheaply inside a circuit.
circomlib caps Poseidon at 16 inputs and the record has 25, so I split it: one hash over the age, the 14 diagnosis codes and the consent flag, a second hash over the lab value and the 8 medication codes, then a final hash of those two results. The circuit recomputes that commitment from the private inputs and forces it to equal the public commitment the verifier already holds.
Change one value and the commitment no longer matches, so the proof fails. That is what binds each proof to exactly one record, and why the sponsor can trust a result without ever seeing the data behind it.
Two pieces carry the proof: the Circom circuit that encodes the eight eligibility constraints, and the snarkjs trusted setup that compiles it and confirms the proving system is sound.


The full system, including the math explainer, is deployed. Explore the live build below.
The hardest part wasn't the cryptography library, it was threat-modeling honestly. A proof is only meaningful if you also stop it being copied, reused, or computed over fake data, which is why the Poseidon commitment and the two-service separation matter as much as the SNARK itself.
If you're learning this: start with what a zk-SNARK actually guarantees (you know a secret that satisfies a public set of constraints) before touching Circom. Understand R1CS, then Groth16's trusted setup, then build the smallest possible circuit and prove it before adding constraints.