crypto_vote/lib.rs
1//! # `crypto_vote` — a cryptographic oracle for verifiable voting
2//!
3//! This crate is the cryptographic core described in the requirements
4//! spec: a strict, agnostic "mathematical oracle" that knows nothing
5//! about elections, voters, databases or networks, and only answers two
6//! questions:
7//!
8//! - *give me a fresh voter identity*,
9//! - *given this ballot, this proof and this authorised list, is this
10//! a mathematically valid vote?*
11//!
12//! Everything else — who is allowed to vote, whether the election is
13//! open, whether a tag has been seen before — belongs to the **host
14//! system**. The host is expected to follow the anti-double-vote
15//! contract from §2 of the spec: before validating a proof, it must
16//! look the linking tag up in its own storage and reject the
17//! transaction if it has already seen it.
18//!
19//! ## The three operations
20//!
21//! | # | Operation | Where it runs | Function |
22//! |---|-----------|---------------|----------|
23//! | A | Generate identity | Either side | [`generate_identity`] |
24//! | B | Sign a ballot | Voter's device (WASM in the browser) | [`sign_vote`] |
25//! | C | Validate a proof | Host / server | [`verify_vote`] |
26//! | D | Prove ownership of a key image | Voter's device (prove) / anyone (verify) | [`prove_ownership`] / [`verify_ownership`] |
27//!
28//! Operation D is optional and opt-in: it is the inverse of the ring
29//! signature's anonymity. It lets the holder of a secret key prove to an
30//! arbitrary third party — including one external to the election — that a
31//! given key image (and the ballot beside it in the registry) is theirs,
32//! without revealing the secret key. Intended for mandated / proxy voting.
33//! See [`crate::ownership`].
34//!
35//! ## Guardrails against misuse
36//!
37//! Two cheap protections are built into [`sign_vote`] and
38//! [`verify_vote`]:
39//!
40//! - **Non-empty ballot and election identifier**: the library
41//! refuses to sign a zero-byte vote, and silently rejects any proof
42//! whose vote or election ID is empty at verification time. An
43//! empty payload is almost always a caller bug.
44//! - **Election binding**: every signature is bound to an
45//! `election_id` byte string. A proof produced for election X
46//! cannot validate for election Y, even if the ring and the secret
47//! key are identical. The host should pass a stable per-election
48//! identifier (UUID, slug, hash of an event configuration, …) for
49//! every call.
50//!
51//! The library does **not** enforce one further protocol-level
52//! invariant the host is responsible for: the ring (the full set of
53//! authorised public keys) must be **frozen before voting opens** and
54//! stay composition-identical until the election closes. The ring is
55//! part of what gets hashed into every signature, so adding or
56//! removing a member mid-election invalidates every signature
57//! produced before the change. All voter identities must therefore be
58//! generated during the enrolment window. See the README for the
59//! details.
60//!
61//! ## Cryptographic choices
62//!
63//! - **Curve**: Ristretto255 (a prime-order group built on Curve25519).
64//! Picked because it is implemented in pure Rust by
65//! `curve25519-dalek`, has constant-time arithmetic, and avoids the
66//! small-subgroup pitfalls of raw Curve25519.
67//! - **Ring signature**: an experimental BLSAG (Back's Linkable
68//! Spontaneous Anonymous Group) variant implemented locally from the
69//! LSAG/BLSAG equations. The linking tag is scoped by `election_id`,
70//! so the same identity remains linkable inside one election but not
71//! publicly correlatable across different elections.
72//! - **Hash**: Blake2b-512 (via the `blake2` crate). Picked because it
73//! produces a 64-byte digest natively — which is exactly what every
74//! challenge in the BLSAG protocol needs to feed back into a
75//! Ristretto scalar — and because it is already a standard choice
76//! for the same algorithm in other projects.
77//! - **CSPRNG**: `SysRng`. On wasm32 it is wired up to
78//! `Crypto.getRandomValues` via the `getrandom` crate's `wasm_js` feature.
79//!
80//! ## Encoding
81//!
82//! Every public byte string the crate emits is a hex-encoded ASCII
83//! string. Keys, tags and signatures all have a `.to_hex()` /
84//! `from_hex(..)` pair. There are also raw `to_bytes` / `from_bytes`
85//! helpers for callers that want to do their own encoding.
86//!
87//! On top of the bare hex there is a **human-friendly prefixed format**
88//! (`.to_prefixed()` / `from_prefixed(..)`): the same hex body, wrapped
89//! with a self-describing tag (`pk_`, `sk_`, `ki_`, `blsag_`) and a
90//! trailing checksum, e.g. `pk_3f8a…e1c0_d4e9a1b7`. It encodes the exact
91//! same bytes — nothing about the cryptography changes — but the tag
92//! stops a value being pasted in the wrong slot and the checksum catches
93//! typos. See [`crate::encoding`] for the full description.
94//!
95//! ## End-to-end example
96//!
97//! ```
98//! use crypto_vote::{generate_identity, sign_vote, verify_vote};
99//!
100//! // Three authorised voters.
101//! let alice = generate_identity();
102//! let bob = generate_identity();
103//! let charlie = generate_identity();
104//!
105//! let ring = vec![
106//! alice.public_key,
107//! bob.public_key,
108//! charlie.public_key,
109//! ];
110//!
111//! let election_id = "550e8400-e29b-41d4-a716-446655440000"; // any stable string
112//! let ballot = b"option-A";
113//!
114//! // Bob signs his ballot. The host never sees `bob.secret_key`.
115//! let proof = sign_vote(&bob.secret_key, ballot, election_id, &ring).unwrap();
116//!
117//! // The host: 1. would now check `proof.key_image` against its store,
118//! // 2. then asks the oracle.
119//! assert!(verify_vote(
120//! ballot,
121//! election_id,
122//! &proof.signature,
123//! &proof.key_image,
124//! &ring,
125//! ));
126//! ```
127
128mod blsag;
129
130pub mod encoding;
131pub mod error;
132pub mod identity;
133pub mod ownership;
134pub mod signing;
135pub mod types;
136pub mod verifying;
137
138#[cfg(feature = "wasm")]
139pub mod wasm;
140
141#[cfg(feature = "extism")]
142pub mod extism;
143
144// Top-level re-exports so the public API is `crypto_vote::sign_vote`,
145// not `crypto_vote::signing::sign_vote`. Anything not re-exported here
146// is still reachable through its module, but is not considered the
147// canonical entry point.
148pub use crate::error::{Error, Result};
149pub use crate::identity::{Identity, generate_identity};
150pub use crate::ownership::{generate_nonce, prove_ownership, verify_ownership};
151pub use crate::signing::sign_vote;
152pub use crate::types::{
153 KeyImage, Nonce, OwnershipProof, PublicKey, SecretKey, Signature, VoteProof,
154};
155pub use crate::verifying::verify_vote;