Veilproof logo

Rust · RFC 5114 Schnorr group · no crypto crates · no curves

A zero-knowledge proof system
you can read end to end.

Veilproof proves you know a secret, or that a hidden number satisfies a property, without ever revealing it. SHA-256, the Fiat-Shamir transform, and every Sigma protocol are written from scratch. No black box. Below is the real math, running live in your browser, animated so you can watch each move land.

Try it live View on GitHub
How to use this playground
Each card below is one real Sigma protocol running live in your browser. Enter a secret (a discrete log, a value in range, or a shared exponent), press Generate proof, and watch the three moves cross the channel: the prover's commitment, the Fiat-Shamir challenge derived by hashing the transcript, and the response the verifier checks. A green verdict means the verification equation held; the secret itself never appears in what is sent. Flip a lie toggle to prove with the wrong secret or forge a bit, and watch the verifier reject it. This is the same math as the Rust CLI, ported to JavaScript.
Simulation 1 of 4

Prove you know a secret

Pick a secret x. The page computes y = gx mod p and proves knowledge of x with a Schnorr proof, without ever putting x itself in the proof. Watch the commitment, challenge, and response cross the channel between prover and verifier.

prover and verifier, connected only by what is sent below

Prover
P
holds x
Verifier
V
checks gz == t·yc
statement y = g^x mod p
commitment t = g^k mod p
challenge c = H(g,y,t) mod q
response z = k + c·x mod q
generate a proof to see the verifier's verdict

Simulation 2 of 4

Range proof: prove a number is in range, without saying what it is

Pick a secret value from 0 to 255. The page commits to it with Pedersen commitments, decomposes it into 8 bits, proves every bit is honestly 0 or 1 with an OR-proof, and ties the bits back to the commitment. The value itself never leaves its sealed commitment.

each cell is one OR-proof: honestly 0 or 1, never which

secret value sealed inside the commitment

C ?= ∏i Ci2^i

generate a range proof to see the verifier's verdict
Simulation 3 of 4

Equality proof: show two public values share one secret

Chaum-Pedersen. From a single secret x the page publishes y1 = g^x and y2 = h^x on two different bases, then proves the same x sits behind both, without revealing x. This is the check behind honest re-encryption and verifiable shuffles.

one secret, two bases, one shared response

secret x
y1 = g^x
y2 = h^x
response z (shared)
g^z ?= t1·y1^c
h^z ?= t2·y2^c
generate an equality proof to see the verifier's verdict
Simulation 4 of 4

Ring proof: prove you are one of the group, not which one

A ring of public keys is generated and the page proves knowledge of the secret behind exactly one of them, without revealing which. The proof has the same shape at every position, so it cannot leak the index, not even in this visualization.

every key looks the same to the verifier, including yours

generate a ring proof to see the verifier's verdict

running g^q mod p == 1 self-check and a sample proof verification…

How it works

Every step below is code you can read, no dependency does the cryptography for you.

1

A fixed group

All arithmetic happens mod a 2048-bit RFC 5114 prime, inside the 256-bit prime-order subgroup generated by g, self-checked on load.

2

SHA-256

The compression function, message schedule, and padding are written from the FIPS 180-4 spec, no crates, matching a known-answer vector.

3

Fiat-Shamir

The verifier's random challenge becomes a SHA-256 hash of the full transcript and statement, reduced mod q, so no interaction is needed.

4

Schnorr proof

Knowledge of a discrete log x is proven with a commitment t, a challenge c, and a response z, checked by g^z == t · y^c.

5

OR-proof

A bit commitment is proven to open to 0 or 1 by genuinely proving the true branch and simulating the false one.

6

Range proof

n bit proofs, tied back to one commitment through the Pedersen homomorphism, prove a value lies in [0, 2^n) bit by bit.

How it compares

Veilproof solves a deliberately smaller problem than a general proving framework, and that is the point. The comparison is about comprehensibility and footprint, not capability.

Arkworks

Builds succinct proofs over arbitrary circuits on elliptic curves and pairings. Powerful and necessarily large, spanning many crates. Best for production succinct proofs over general circuits.

Circom + snarkjs

A circuit compiler plus a JavaScript toolchain, elliptic curves and pairings, usually with a trusted setup. Best for production circuits and dapps.

Veilproof

A fixed menu of Sigma protocols in a Schnorr group over a finite field, no curves, no trusted setup, three dependencies. About 2,800 lines with tests, readable in one sitting. Best for learning how zero-knowledge proofs actually work.

Not for production

Teaching-grade, not audited, not reviewed outside this project. It makes convenience choices a real deployment should not inherit blindly, most notably the derivation of the second Pedersen generator h. Do not use it to protect anything that matters.

Same math, two surfaces.

CLI & library

veilproof prove-dlog, verify-dlog, and demo, plus a plain Rust library with typed errors, never a panic.

Browser

The demo above is the same p, q, g, h, the same hand-written SHA-256, and the same Fiat-Shamir logic, ported to JavaScript with native BigInt.

Tests

cargo test checks completeness, soundness against wrong witnesses and tampered bytes, determinism, and that the witness never appears in a serialized proof.

MCP server

Exposes the verifiers (verify_dlog, verify_dleq, verify_range, verify_ring) as tools, so an agent can produce a proof and then mathematically verify its own output against the Rust binary.

# prove knowledge of a secret
veilproof prove-dlog --secret 424242

# verify it
veilproof verify-dlog --statement <hex> --proof <hex>

# run the full range-proof demo
veilproof demo
$ veilproof demo
veilproof demo: proving a committed value lies in a range, without revealing it

secret value: withheld (that is the point)
public claim: the committed value fits in 16 bits, i.e. [0, 2^16)

commitment C = g^m * h^r mod p:
  12ef947a73b4b599585e1597ffc625e3a10aa0385aafa1bdd077fd52381bd2f4...
blinding factor r: withheld (not printed, this is what makes C hiding)

range proof: 16 bit commitments, each with an OR-proof of being 0 or 1
  bit  0 commitment: 73909ca5ac24f7a8...
  bit  1 commitment: 09d0367bf83bc8a8...
  ... 14 more ...
  bit 15 commitment: 2f2f53e91c799158...

verifier recomputes the weighted product of bit commitments,
checks it equals C, and checks every bit OR-proof: ACCEPT

$ veilproof verify-dlog --statement <hex> --proof <hex>
accept