Skip to content
Condorcet.vote Alpha
← Technical documentation

Vote proof

Proving that a published ballot is your own without revealing your secret key, but revealing your vote: cryptoVote Operation D and the cvproof/1 envelope that carries it.

This page specifies the vote proof used by the public tools: what it establishes, who can produce it, the nonce flow, the exchanged envelope field by field, and how it is verified. It is deliberately explicit about the consequences: a proof handed over is a disclosure of the vote, checkable by anyone and impossible to take back.

Protocol v1

What it establishes

This key image is mine.

A published ballot carries an election-scoped key image, never an identity. The proof ties that key image to a public key, and so to whoever holds the matching secret key.

  • A Chaum–Pedersen proof. A non-interactive proof of equality of discrete logarithms: the prover knows one scalar x such that the public key is P = x·G and the key image is I = x·B, where B is the election-scoped base. Nothing else about x is revealed.
  • The secret key stays secret. The proof neither contains the secret key nor allows it to be reconstructed; handing it over does not give the key away.
  • Bound to one election and one nonce. The election UUID fixes the base B and the nonce is folded into the challenge: a proof only holds for that election and that nonce.
  • It reveals the vote. Matched against the register, the proof means: the holder of this key cast this published ballot, whose ranking is in clear. Proving ownership of a key image is provably disclosing how you voted.
Statement proven · Operation D
∃ x :  P = x·G  and  I = x·B
B = H_p(domain ‖ len(election_id) ‖ election_id ‖ P)

Ristretto255, Blake2b-512. B is the base the ballot derives its key image from: the same secret key yields a different key image in every election.

The key image in the signed ballot
Who produces it

The key holder, and no one else.

Producing a proof requires the voter's secret key. No one can therefore prove a voter's ballot on their behalf, or against their will.

  • At their sole initiative. The proof is a voluntary act of the voter, after the vote. No step of the election triggers or requires it.
  • Outside the organizer's control. It is a property of the cryptography, not a platform setting: no switch enables or forbids it, and the organizer can neither prevent nor produce it.
  • The key comes from the receipt. The secret key sk_… and the ballot's key image ki_… are in the .condorcet_receipt vote receipt, which the tool reads directly. That receipt is issued once and never re-issued.
  • Nothing goes through the platform. The platform never reads, writes or stores the envelope: it travels between voter and requester, over whatever channel they choose.
Use case

Mandated voting.

The need behind Operation D: a voter, or a mandate-holder who voted on someone's behalf, demonstrates after the fact how a ballot was cast.

The vote receipt, field by field
Flow

One challenge, one proof, one check.

Three roles, three cryptoVote calls. The nonce is generated by whoever asks for the proof, never by the voter.

  1. 1 Requester Generates a fresh random nonce (generate_nonce_wasm, which returns nonce_…) and sends it to the voter, outside the platform. A new nonce for every request.
  2. 2 Voter Enters their secret key sk_…, the election UUID, their ballot's key image ki_… (or loads their receipt) and the nonce received. The tool computes prove_ownership_wasm(secret, election, nonce), which returns own_…, derives the public key pk_… and assembles the cvproof/1 envelope.
  3. 3 Voter Reads the warning (the proof reveals their vote to whoever receives it, who can convince others in turn), then hands the envelope over, voluntarily. Their secret key never left their browser.
  4. 4 Requester, or anyone Verifies the proof (verify_ownership_wasm), then looks the key image up in the election's published register to read the matching ballot.

Freshness, not non-transferability.

The nonce is folded into the proof's challenge: it cannot be precomputed or replayed under another nonce, which assures the requester it was produced for their request. But it stays publicly checkable: whoever holds the pair (nonce, proof) can convince anyone else. It is not a designated-verifier proof, and nothing restricts it to the requester alone.

Envelope

cvproof/1, six fields.

A single UTF-8 JSON object, the only interchange format the tools produce or accept. It concerns the voter and the requester only: no server-side parser, no storage.

version string literal
Exactly cvproof/1; any other value is rejected.
election UUID
The election UUID, the very value passed to the WASM calls. Accepted in mixed case, lowercased to its canonical form.
public_key pk_<64 hex>_<8 hex>
The public key P, derived from the secret key by the voter's tool.
key_image ki_<64 hex>_<8 hex>
The election-scoped key image I: that of the published ballot whose ownership is proven.
nonce nonce_<64 hex>_<8 hex>
The nonce issued by the requester, copied verbatim.
proof own_<128 hex>_<8 hex>
The prove_ownership_wasm output (64 bytes), copied verbatim.
Example · cvproof/1
{
    "version": "cvproof/1",
    "election": "1f0c9b6e-4d2a-4c8e-9b7a-3e5f6a7b8c9d",
    "public_key": "pk_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef_a1b2c3d4",
    "key_image": "ki_fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210_0badc0de",
    "nonce": "nonce_00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff_5eed5eed",
    "proof": "own_0f1e2d3c4b5a69780f1e2d3c4b5a69780f1e2d3c4b5a69780f1e2d3c4b5a69780f1e2d3c4b5a69780f1e2d3c4b5a69780f1e2d3c4b5a69780f1e2d3c4b5a6978_c0ffee00"
}

Illustrative values: exact shapes and lengths, but made-up patterns and checksums. This envelope does not verify.

  • Canonical prefixed strings. public_key, key_image, nonce and proof are the <tag>_<hex>_<checksum> strings emitted by cryptoVote: lowercase hex of whole bytes, a 4-byte checksum (8 hex). They are carried verbatim and passed unchanged to verify_ownership_wasm; bare hex is rejected.
  • Field order does not matter. It is a JSON object: the reader reads by key. The tool writes the fields in the order of the table above.
  • Unknown keys are ignored. A reader MUST ignore keys it does not know, to stay compatible with future versions; the tool drops them when reading.
  • Format before cryptography. version and the prefixes are validated before any cryptographic call. The checksum guards against typos, never against forgery: authenticity rests on the proof alone.
Verification

Valid, then tied to the register.

The procedure for whoever receives an envelope. A valid proof alone says: this public key owns this key image. Only the register cross-check turns it into: this voter cast this published ballot.

  1. 1 Read the input as a JSON object; reject invalid JSON or any other type (array, scalar, null).
  2. 2 Require version to equal cvproof/1.
  3. 3 Require a UUID in election and lowercase it.
  4. 4 Require public_key as pk_, key_image as ki_, nonce as nonce_ and proof as own_, each in its canonical form and at its exact length; ignore unknown keys.
  5. 5 Compare nonce with the one you issued: freshness only holds if it is yours.
  6. 6 Call verify_ownership_wasm(public_key, key_image, election, nonce, proof) and continue only on true.
  7. 7 Look key_image up among the election's ballots and read the ballot found: its ranking is the proven vote.

Register look-up outcome

published
The ballot is published in the election's chain; the tool displays its content.
pending
A matching ballot was received but awaits publication in a batch: its content is not readable yet, so check again after the election closes.
not_found
No ballot of this election carries this key image: the proof, valid as it may be, ties to no vote, and the tool says so.
Limits

What it does not add.

The proof standardises a receipt that was already provable. It is not a privacy feature, and its effects are final.

  • No coercion resistance. The voter already holds their secret key and key image: a provable receipt existed before Operation D, which only standardises it. It neither adds nor removes coercion resistance, consistent with the deliberate choice of verifiable receipts over receipt-freeness.
  • No designated recipient. The proof is transferable: the requester can show it to third parties, who will verify it just as well. It cannot be "for their eyes only".
  • No way back. An envelope handed over cannot be withdrawn: whoever holds it can verify it and show it, with no time limit.
  • No civil identity. The proof establishes possession of a secret key, not a person's identity: the envelope carries no voter ID and no name. Linking the key to a person depends on the context of the exchange.
Public tool

Everything happens in the browser.

The public vote-proof tools implement this specification: generate a nonce, prove my vote, verify a proof. No account, no setting, always available; in practice verification assumes a closed election with a published register, even though the cryptography does not depend on the election's lifecycle.

  • Local computation. The nonce, the proof and the verification are computed in the browser by @condorcet.vote/crypto-vote, with no server round-trip.
  • The secret key never leaves the browser. It is entered and used locally, never sent to a server. Nothing is kept beyond the page: no key, no nonce, no proof.
  • One request, on public values. The register cross-check queries a same-host look-up endpoint with the election UUID and the key image, two public values. Neither the proof, nor the nonce, nor any secret is sent to it.
  • The voting page's posture. A standalone, Livewire-free page under the strictest CSP (first-party resources only); the WASM module is checked against its integrity hash on load.
Open the vote-proof tools