Skip to content

Split vote configuration into static trust anchor and signed dynamic round registry - #20

Open
p0mvn wants to merge 3 commits into
adam/shielded-voting-wallet-apifrom
roman/wallet-api-config-trust-model
Open

Split vote configuration into static trust anchor and signed dynamic round registry#20
p0mvn wants to merge 3 commits into
adam/shielded-voting-wallet-apifrom
roman/wallet-api-config-trust-model

Conversation

@p0mvn

@p0mvn p0mvn commented May 1, 2026

Copy link
Copy Markdown

Summary

Restructures the vote configuration in the Shielded Voting Wallet API ZIP to introduce a cryptographic chain of trust and a per-round authentication registry, replacing the single-document CDN-trusted shape.

Motivation

The previous shape had three problems:

  1. No chain of trust. A CDN compromise could substitute any field undetectably — vote_servers, pir_endpoints, ea_pk — and the wallet had no way to verify the EA public key against an out-of-band trust anchor.
  2. Brittle round binding. The single vote_round_id pin coupled the publisher to chain-round activation timing, producing transient validation failures during round transitions and requiring an awkward stale-config recovery path in wallet code.
  3. No support for multiple or historic rounds. A configuration could authenticate exactly one round at a time. Tally review for finalized rounds, or any future need to surface multiple concurrent rounds, fell off the end.

Design

Configuration is split into two artifacts:

  • Static configuration — bundled in the wallet binary. Carries trusted_keys (Ed25519 admin pubkeys) and dynamic_config_url. Travels with the signed app and is the trust anchor.
  • Dynamic configuration — server-published. Wrapper fields (vote_servers, pir_endpoints, supported_versions) plus a rounds registry keyed by vote_round_id. Each round entry carries its own `signatures` list, attesting to the entry's ea_pk under one of the static-config trusted keys.

Trust binding (per round)

  1. Wallet looks up the chain round's vote_round_id in rounds.
  2. Wallet verifies at least one signature on the entry against a trusted key (per the entry's `auth_version`-defined scope).
  3. Wallet confirms the entry's `ea_pk` is byte-equal to the chain round's `ea_pk`.

A break in either step (signature or chain mismatch) is detected. Failures are scoped to the affected round; other authenticated rounds remain usable.

Versioning hooks

  • static_config_version — versions the static document.
  • config_version — versions the dynamic config wrapper.
  • auth_version (per-entry) — versions the round-entry schema and signature scope.

Each is independently bumpable, so widening the v1 signature scope (e.g. to cover endpoints) is a non-breaking addition for older entries.

Append-only registry

Per-entry signatures (rather than one signature over the whole `rounds` map) make the registry append-only at the cryptographic level. Adding a round signs only the new entry; existing entries' signatures remain valid forever and are never re-signed. This decouples each round's authenticity from the publisher's current state and naturally supports admin key rotation across the registry's lifetime.

Operational decoupling

Bringing a new vote server or PIR operator into rotation is now governed by ordinary PR-merge to the publishing repository — not coupled to wallet releases or chain deploys. Adding, removing, or swapping operators picks up on the wallet's next configuration fetch.

v1 trust scope (deliberately narrow)

The signature covers an entry's `ea_pk` only. v1 does NOT defend against:

  • A compromised host substituting wrapper fields (`vote_servers`, `pir_endpoints`, `supported_versions`).
  • A compromised host omitting an entry from `rounds` (mitigation: spec REQUIRES wallets surface unauthenticated rounds in UI rather than silently filtering).
  • A compromised host adding spurious entries (rejected by signature verification).

These are documented gaps with a forward path: widen signature scope by introducing `auth_version: 2` (entry-level) or `config_version: 2` (wrapper-level).

Spec sections changed

  • `# High level summary` — steps 1–4 split wrapper validation from per-round authentication; step 4 binds on `ea_pk` lookup, not single-round id.
  • `## Vote Discovery` — split into `### Static Configuration` and `### Dynamic Configuration`. Reframes the dynamic config as service discovery + registry.
  • `#### Wrapper Field Definitions` and `#### Round Entry Field Definitions` — replace the previous flat field table.
  • `### Signature Verification` — now per-`auth_version`. v1 covered bytes = entry's raw 32-byte `ea_pk`. Single valid signature suffices; multi-sig and m-of-n flagged as non-breaking future extensions.
  • `### Wrapper Validation Rules` and `### Per-Round Authentication` — replace the old `### Validation Rules`. Wrapper failure blocks session; per-round failure scoped to that round.
  • `### Distribution` — invokes both rule sets; explicit list of v1 non-coverage.
  • `### Active Round` — binding paragraph rewritten to invoke per-round authentication.
  • `## Encoding Conventions` — drops the now-obsolete `vote_round_id` config row from the encoding table.
  • `# Rationale` — adds `## Static and Dynamic Configuration Split` (three wins + operator-flexibility paragraph) and `## Per-Round Registry Model` (map shape, per-entry signatures, `auth_version` extension hook, v1 non-coverage with mitigations).
  • `# References` — adds RFC 8032.

Test plan

  • Spec proofread by ZIP owners (Adam, Greg, Dev, Roman).
  • Confirm with chain team: ea_pk is unique per round (no cross-round reuse). If reuse is possible, add tiebreaker field at `auth_version: 2`.
  • Reference implementation work in `token-holder-voting-config` (publisher: signing pipeline + new schema + CI verification) and `zodl-ios` (wallet: split `VotingServiceConfig`, add Ed25519 verification, restructure `VotingStore+Session.swift` per-round auth) follows once spec is agreed.

Made with Cursor

p0mvn and others added 3 commits May 1, 2026 13:14
…round registry

The previous shape conflated the trust anchor, operational endpoints,
and round binding into a single CDN-served document with no
cryptographic chain of trust. A CDN compromise could substitute any
field undetectably, and the per-config `vote_round_id` pin coupled
publishers to chain-round activation timing, producing transient
validation failures during round transitions.

This change splits the configuration into two artifacts:

- A wallet-bundled static configuration carrying the trusted admin
  pubkeys and the dynamic-config URL. Travels with the signed wallet
  binary and is the root of trust.
- A server-published dynamic configuration containing operational
  endpoints (vote servers, PIR endpoints, supported versions) and a
  `rounds` registry keyed by `vote_round_id`. Each entry carries its
  own Ed25519 signatures over the entry's `ea_pk`, attested by one of
  the static-config trusted keys.

The resulting trust chain is two steps per round: admin signature
attests the entry's `ea_pk`, chain query attests that the round in
progress uses that key. A single break in either step is detected.

The registry is append-only at the cryptographic level: adding a round
signs only the new entry, and existing entries' signatures remain
valid forever. This naturally supports multiple concurrent
authenticated rounds and historic rounds for tally review and audit,
without the per-round CDN republish race the old design suffered from.

Per-entry `auth_version` is the per-round extension hook. Future
revisions can introduce wider signature scopes (e.g., per-round
endpoint pins) without invalidating older entries; wallets that don't
recognise an `auth_version` reject only that entry, not the whole
configuration.

The split also decouples operator participation from wallet releases
and chain deploys: bringing a new vote server or PIR operator into
rotation is an ordinary PR merge to the publishing repository.

v1 keeps the signature scope deliberately narrow (entry `ea_pk` only).
Wrapper fields and `rounds` membership remain CDN-trusted in v1, with
explicit threat-model documentation and a forward path via
`auth_version` / `config_version` bumps. The list shape of
`signatures` admits multi-admin co-signing and m-of-n thresholds as
future, non-breaking extensions.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant