Trusted platform
How the platform works inside: what it does during the vote, the rules it holds itself to, and what the register lets anyone check afterwards.
This page describes those rules at the level of principles, like the whitepaper: database, signing window, transport, publication, secrets management, hosting. They do not replace verification: every claim the platform makes is published in the register and can be checked without it.
Protocol v1
Trusted during the vote, verifiable afterwards.
The platform is a trusted third party whose trust is bounded in time. This exposure is a deliberate, documented design decision, not an oversight.
-
During the vote
What it is trusted with
It serves the voting page, hands out the secret keys in the default provisioning mode and receives each signed ballot over the voter's VoteLink session. During that window it transiently knows who votes and, in the default mode, which key it handed to whom.
-
Afterwards
What can be checked without it
Every claim it makes is materialized in the public register and independently verifiable. The two sides of the register, participation and ballots, are published so that they cannot be joined into a voter-to-ballot link.
The level of trust is set per election: in BYO-only mode, where every voter brings their own key, the platform never holds any voter's signing secret, and the trust window shrinks to receiving the ballots.
What “trusted third party” commits the platform to, and what it does not, is written in the charter. It is neither an accreditation, nor a certification issued by an authority, nor a label.
The trusted third party charterPostgreSQL, the single arbiter.
Every user-facing invariant is enforced in PostgreSQL by guarded conditional writes and uniqueness constraints, never by in-process state: correct by construction whatever the number of workers and instances, with no lock ordering and no deadlocks.
The rules written into the schema
-
ballots (election_id, key_image)unique - A key image votes only once per election. A safety net that holds even against an application bug.
-
ring_keys (election_id, public_key)unique - A public key appears only once among the election's ring members.
-
chain_blocks (election_id, block_number)unique - The absolute backstop against forks: a losing concurrent writer fails and retries against the new chain head.
-
ballotsappend-only · trigger - Any UPDATE or DELETE raises an exception. The one codified exception: the is_chain_published flag, flipped from false to true in the transaction that commits a block.
-
chain_blocksappend-only · trigger - The same rule for chain anchors; the one exception is the OpenTimestamps status, one-way pending → completed.
-
ballots.payloadbytea - The signed bytes, stored verbatim. Never in a normalizing JSON type: jsonb reorders keys and rewrites whitespace, which would break byte-exact verification.
-
ballots.idUUID v4, no timestamp - A random identifier and no creation date: nothing gives away the submission order.
Writes with no possible race
- Guarded writes. The has_voted flip, the key distribution cap and claiming a key are conditional UPDATEs: zero affected rows means refusal. Never an in-memory read-modify-write.
- Cryptography outside locks. Signature verification, pure computation over public data, runs before the write transaction: cryptography never holds a lock.
- An open election, re-asserted. On submission, the election's open state is re-asserted inside the transaction, serialized against the close: a ballot either commits before the close and is published by it, or is refused. Never a stored ballot the chain would leave out.
- Serialized blocks. Blocks are produced by queued jobs, serialized per election; the committed database row is the arbiter of every block.
Keep only what is needed, date nothing.
The platform stores nothing the protocol does not need and publishes nothing that dates or identifies a ballot. Vote timing is treated as a side channel and removed end to end.
Minimization
- The voter's data. Their e-mail address, and optionally their name and weight: nothing more is asked of the organizer.
- Secrets encrypted, then erased. Secret voting keys are encrypted at rest and deleted at their point of use. Once handed out, a key no longer exists in the database: it survives, encrypted, only in a cache of at most 180 minutes for re-delivery, and is cleared from it on voting.
- Memory wiped. Transient secrets are explicitly zeroed, on the PHP side (value objects backed by sodium_memzero) as on the Rust/WASM side (zeroed buffers).
- No tracking. No analytics and no tracking, on the voting page as on the rest of the site: only server logs remain.
Chronology hygiene
- No ballot timestamp. Neither in the database nor in the register; random UUID v4 identifiers across the whole public surface.
- Batch publication. Ballots go out in batches of at least 600, on a randomized schedule, sorted by key image; the participation register is published once, at close.
- Physical scrub. PostgreSQL stamps every row with the transaction that wrote it: a ballot and its participation flip briefly share the same stamp, absent from a logical export but readable with direct access to the database. A twice-daily job rewrites those rows under a single mute transaction, then runs a VACUUM: a fresh stamp survives at most one scrub cycle.
Protecting the moment the key is in memory.
The voter's secret key lives in their browser only for the lifetime of the voting page. It is the most exposed moment on the client side, and the one the platform protects most strictly.
The voting page
- Standalone. A framework-free page, outside the organizer's application. Every navigation away from the vote is a full page load, which clears the key from memory.
- The strictest CSP. First-party only, no third-party script, no analytics; WebAssembly execution allowed, form submissions disabled in favor of fetch-only submission.
- Integrity of the signing code. Compiled assets carry subresource integrity hashes (SHA-384): a compromised CDN node cannot silently alter the signing code path.
- Its own subdomain. Voting lives on voting.condorcet.vote, with host-only cookies: its session state is isolated from the rest of the site.
Transport, links and sessions
- TLS up to the application. TLS is handled by the application's own web server, Caddy (written in Go, through FrankenPHP), with no intermediate proxy. Its Let's Encrypt certificates are issued and renewed automatically, every HTTP request is redirected to HTTPS, and its modern TLS defaults apply: TLS 1.3 preferred, 1.2 at minimum.
- No access log on the vote. The voting subdomain is never access-logged: its URLs carry the VoteLink token, and timestamped logs would recreate the vote chronology the database refuses to keep.
- Straight to the origin, over TLS. Rings, key distribution and ballot submission travel directly between the browser and the platform. No CDN sits in between for voting data; only compiled static assets are cached at the edge.
- A capability link, no account. Voters never create an account or a password: their VoteLink, e-mailed by the platform, is a bearer capability. Only the token hash is stored; the link is tied to a single voter, its session has a narrow scope and it is rate-limited.
- Server-side sessions. Sessions are held in Valkey (Redis). In the browser, only the strictly necessary state: session, language, theme.
- The rest of the site. Everything, scripts and WebAssembly included, is served first-party under a strict CSP; no third-party origin on public pages.
- Organizers. Sign-in with a password or a passkey, optional TOTP second factor, attempts throttled per login and per IP address.
What the application cannot rewrite.
A block is valid if and only if its bytes hash to the committed digest and its signature verifies, wherever it is read from. Storage adds barriers of its own.
Publication isolation
- Separate storage. Chain files live in their own object-storage bucket, with their own access credentials, separate from any other stored content and served from their own CDN hostname: a flaw in any upload path can never write to the chain.
- Write once. The bucket enables the provider's object lock: once written, a block file can be neither overwritten nor deleted, even with the platform's storage credentials. It complements the database triggers and the hash chain.
- Raw bytes. Blocks are served as is; no framework layer can reformat them. Since the content is deterministic, a replayed write is idempotent.
The signing key
- A pinned tool. Every block is signed in OpenPGP v6 (RFC 9580, Ed25519) by a pinned, vendored build of Sequoia sq, in background jobs; no signature math on the PHP side.
- Private keys outside the database. Public certificates are in the database; private keys never enter it. They are kept in the OVHcloud secret manager (OKMS), to the highest security standards: encrypted at rest, one isolated domain per environment, access by client certificate over mutual TLS. OKMS only keeps them: signing stays local, reads a fixed version of the key and checks its fingerprint.
- Rotation that contains. On rotation, the outgoing key gets an expiration at that very instant and its versions are deactivated: a retired key still verifies, it no longer signs.
- What the signature does not prove. It proves that the platform produced a block, not that it has not rewritten history, since it holds the key. Immutability comes from independent mirroring, and the OpenTimestamps anchor of every block proves when its bytes existed.
Where it runs, how it is built.
Wider operations are not described here; only the points that carry a guarantee are.
Hosting
- In France. The platform is hosted in France and everything is served from its own domain: no third-party service loads in voters' browsers.
- A tight stack. PostgreSQL for data; Valkey for sessions, queues and cache, never as a primary store. Isolated job queues, so that a burst on one never starves the others of workers.
- Secrets outside the configuration. On the remote environments, no credential lives in environment files: all of them are held by OKMS, the OVHcloud secret manager, with one isolated domain per environment.
Engineering discipline
- Real cryptography in tests. The test suite exercises the real WebAssembly module, never a mock, on the server side as in the browser, up to complete browser-driven voting journeys.
- Pinned vectors. The cryptographic core pins protocol vectors: any silent change to a domain string, a format or a byte layout fails the build. It also runs bit-level malleability sweeps and fuzzing targets on all its parsers, and audits its dependencies in CI.
- Specifications in sync. The specifications are versioned with the code and kept in step with it by explicit convention; the whitepaper and these pages are their published form.
What remains a matter of trust.
These limits are documented, not hidden. They mostly concern the voting window; afterwards, the register bounds what a platform could do unseen.
What it must be trusted with
- An actively malicious platform at vote time. In server-generated key mode, it could precompute the key image of every key it handed out and de-anonymize the ballots. Imported or enrolled keys remove that power; BYO-only elections remove every server-held secret.
- Fabricated participation. It could mark an absent voter as having voted and add a ballot within the published bounds; only the voter concerned can detect it, by checking their own participation row.
- The browser during signing. A script injection during the signing window remains the residual client-side risk: the CSP minimizes it but cannot eliminate it.
- Contact data at rest. Voters' names and e-mail addresses are held in clear, because VoteLinks go out by e-mail and the organizer needs a readable list: a database compromise would expose them. Secret voting keys, by contrast, are always encrypted.
- Physical storage. Archived WAL, backups and web access logs remain an infrastructure and retention concern.
- An external review, to come. An independent review of the protocol and its implementation is planned; its report will be linked from the security page.
What bounds that trust
- The register. Valid ring signatures, no key that signed twice, no more ballots than registered voters: everything is checked from the published register alone.
- The recount. The result is recomputed from the register, with software that is not the platform's.
- The chain and its mirrors. Raw files, signed and chained, that anyone can keep: a rewrite becomes evident to any witness who kept an earlier chain head.
- The code that matters. The cryptographic core (CryptoVote) and the tally library (CondorcetPHP) are open source: what signs, verifies and counts can be inspected.
- Written commitments. The charter states what the platform commits to doing, and not doing: a published register is never withdrawn or altered at the organizer's request.
See also
- Public register What every election publishes, whatever carries it: definition, rings, participation register, ballots.
- Platform keys The OpenPGP v6 certificates that sign the blocks, their rotation and their machine endpoint.
- Verification procedure Verifying an election end to end: from the register alone, or with the blockchain.