Friendly command-line client for the Parallelia ec
Electoral Commission daemon. Written in Rust and 100% compatible with the current ec
gRPC Admin API (proto.admin.Admin).
eccli lets an operator create and manage elections, add candidates, issue anonymous
registration tokens, and inspect state — all over the ec daemon's gRPC admin interface.
Experimental, unaudited software — part of the Criptocracia trustless e-voting experiment. Do not use in real elections.
Requires protoc (apt install protobuf-compiler).
cargo build --release
# binary at target/release/eccli# Point at your ec daemon (default is http://127.0.0.1:50051)
eccli --server http://127.0.0.1:50051 check
# Create an election that starts in 1 minute and lasts 1 hour, with candidates
eccli create-election \
--name "2025 Student Council" \
--start-time 60 --duration 3600 \
--rules-id plurality \
--candidates-file candidates.json
# Issue 100 anonymous registration tokens and save them
eccli generate-tokens --election-id <ID> --count 100 --output tokens.txt| Option | Description |
|---|---|
--server <URL> |
ec gRPC endpoint. Default http://127.0.0.1:50051. Env: EC_SERVER. |
--token <TOKEN> |
Admin bearer token. Only needed when the ec sets EC_ADMIN_TOKEN. Env: EC_ADMIN_TOKEN. |
--json |
Emit machine-readable JSON (success and error) instead of human output. |
--yes, -y |
Skip confirmation prompts (required for destructive ops in scripts). |
Every command accepts the global options above. Short flags are consistent across
commands: -e is always --election-id and -n always --name. Note that -c is
--candidate-id for add-candidate but --count for generate-tokens.
Verify connectivity — and, when the ec requires auth, that your token is accepted.
It lists elections and reports how many are visible, so a 0 count still means the
connection succeeded.
eccli checkCreate a new election. Exactly one of --duration or --end-time is required.
eccli create-election --name "My Election" \
--start-time 60 --duration 3600 \
--rules-id plurality \
--candidates-file candidates.json| Parameter | Description |
|---|---|
--name, -n |
Election name. |
--start-time |
Relative seconds from now (< 1_000_000_000) or an absolute unix timestamp. |
--duration |
Length in seconds (mutually exclusive with --end-time). |
--end-time |
End as relative seconds or absolute unix ts (mutually exclusive with --duration). |
--rules-id |
Counting rules id. The ec ships plurality and stv. Default plurality. |
--candidates-file |
Path to a JSON file of candidates (see below). Optional. |
--candidates-json |
Candidates as an inline JSON string. Optional. |
Candidates are optional at creation; the client adds each one via a follow-up call and
reports per-candidate success. You can also add them later with add-candidate.
--candidates-file and --candidates-json are mutually exclusive — passing both is an
error. Inline JSON is handy for short lists:
eccli create-election -n "Quick Poll" --start-time 60 --duration 3600 \
--candidates-json '[{"id":1,"name":"Yes"},{"id":2,"name":"No"}]'candidates.json format (ids must be 0–255):
[
{ "id": 1, "name": "Environmental Party" },
{ "id": 2, "name": "Tech Innovation Party" }
]Because the election is created first and candidates are added afterwards, a candidate
failure leaves the election in place. create-election reports each outcome and exits
non-zero, so re-add only the ones that failed with add-candidate — do not re-run
create-election, which would create a second election.
eccli add-candidate --election-id <ID> --candidate-id 4 --name "Independent"--candidate-id must be 0–255 (the ec stores it as a u8). The election must still
be open; the ec rejects additions once voting has started.
eccli get-election --election-id <ID>Note: the ec's
GetElectionreturns election metadata only (id, name, status, rules, window, RSA public key). Candidate lists and vote tallies are published to Nostr (kind 35000), not exposed over the gRPC admin API, so they are not shown here.
eccli list-electionsPrompts for confirmation unless --yes is given.
eccli cancel-election --election-id <ID> --yesThe prompt needs a terminal. In a script, a pipeline, or a CI job — anywhere stdin is
not a TTY — the command fails rather than assuming an answer, so --yes is required
there. Answering anything other than y/yes aborts without contacting the ec.
Issue anonymous registration tokens for an election (see the model note below).
eccli generate-tokens --election-id <ID> --count 100 --output tokens.txt| Parameter | Description |
|---|---|
--election-id, -e |
Target election. |
--count, -c |
Number of tokens (1–10000). |
--output, -o |
Optional file to write the raw tokens, one per line. |
Tokens are returned once and cannot be retrieved again — save them with --output
and distribute them securely. The file is written atomically and owner-only (0600); if
a symlink occupies the path it is replaced rather than followed. On non-Unix platforms,
where those permissions cannot be enforced, --output is refused and the tokens are
printed instead so none are lost.
The same fallback covers any failed write (unwritable directory, full disk): the ec
has already minted the tokens, so eccli prints them to stdout and exits non-zero rather
than losing credentials it can never recover. If generate-tokens fails, capture the
output before retrying — a retry mints a new batch, it does not re-issue the old one.
In --json mode the tokens appear inline under .tokens whenever .saved_to is null.
Show each token's id (a truncated SHA-256 the ec exposes) and whether it has been used.
eccli list-tokens --election-id <ID>The previous CLI (now eccli_deprecated)
had add-voter/list-voters, which registered voters by name + public key. The current
ec deliberately does not support that, because linking a voter identity to the system
would undermine ballot anonymity.
Instead, the ec issues anonymous registration tokens. Voters redeem a token over Nostr
(NIP-59 Gift Wrap) to obtain a blind-RSA-signed voting credential, so the EC can verify that
a ballot came from an authorized voter without ever learning which voter cast it. In
eccli this maps to generate-tokens (issue) and list-tokens (audit usage).
When the ec is started with EC_ADMIN_TOKEN, every admin call must carry a bearer token.
Provide it with --token or the EC_ADMIN_TOKEN environment variable:
export EC_ADMIN_TOKEN=your-secret
eccli list-electionsWithout a token against an auth-enabled server you'll get a clear Unauthenticated error.
The token is read from the environment by default, so prefer EC_ADMIN_TOKEN over
--token on shared machines — an argument is visible to anyone who can run ps.
eccli does not support TLS yet. All traffic is plaintext HTTP/2, which matters because both the admin token and freshly generated registration tokens travel over it.
https://URLs are rejected outright. No TLS is compiled in, so honoring one would silently send cleartext to a port the operator believes is encrypted.- Non-local
http://hosts connect but print a cleartext warning to stderr. Loopback addresses (127.0.0.0/8,::1,localhost) are considered local and stay quiet.
To administer a remote ec, tunnel the connection and point eccli at the local end:
ssh -N -L 50051:127.0.0.1:50051 operator@ec.example.org &
eccli --server http://127.0.0.1:50051 list-electionsConnections give up after 10 seconds, so a firewalled or hung endpoint fails with
failed to connect to ec gRPC server at … (followed by the underlying transport
error) instead of hanging indefinitely.
--start-time and --end-time accept two forms:
- Relative (
< 1_000_000_000): seconds from now —60= 1 minute,3600= 1 hour. - Absolute (
>= 1_000_000_000): a unix timestamp.
Human mode writes results to stdout and errors to stderr, colorized when stdout is a
TTY. --json emits exactly one JSON document to stdout on every path — success or
failure — so it is safe to pipe into jq.
Every response carries an ok boolean; failures add an error string. A command
exits 0 only when ok is true. Notably, create-election reports each candidate
outcome and still exits non-zero if any candidate failed to be added, and
cancel-election exits non-zero when the ec refuses the cancellation.
Argument errors are part of that contract: with --json, a malformed invocation emits
an {"ok": false, "error": ...} document and exits 1 instead of clap's usage text.
Without --json, usage errors keep clap's behavior — human-readable text on stderr and
exit 2. --help and --version always print normally and exit 0.
In --json mode destructive commands never prompt; they require --yes explicitly.
eccli --json list-elections | jq '.elections[].id'Every document has ok; failures add error. Keys are stable — fields that do not
apply are null rather than absent, so jq filters never break between runs.
| Command | Fields |
|---|---|
check |
elections_visible |
create-election |
election fields (below), plus candidates[] of {id, name, ok, error} |
add-candidate |
election_id, candidate_id, name |
get-election |
election fields (below) |
list-elections |
elections[], each with the election fields |
cancel-election |
election_id, message |
generate-tokens |
count, saved_to, tokens[] (null when saved to a file) |
list-tokens |
used, total, tokens[] of {token_id, used} |
An election object carries id, name, status, rules_id, start_time, end_time,
created_at, and rsa_pub_key. Timestamps are unix seconds in JSON; human output
renders them as UTC.
# Which tokens are still unredeemed?
eccli --json list-tokens -e "$ID" | jq -r '.tokens[] | select(.used | not) | .token_id'
# Which candidates failed to be added?
eccli --json create-election -n "Board" --start-time 60 --duration 3600 \
--candidates-file candidates.json | jq -r '.candidates[] | select(.ok | not) | .name'Errors from the ec carry an actionable hint:
| Message | Cause | Fix |
|---|---|---|
Unauthenticated |
The ec runs with EC_ADMIN_TOKEN, your call had no valid token. |
Pass --token or export EC_ADMIN_TOKEN. |
Unavailable |
The daemon is not reachable at --server. |
Check the ec is running and the URL/port are right. |
NotFound |
The election id does not exist. | list-elections shows valid ids. |
does not support TLS yet |
You passed an https:// URL. |
Use http:// over a tunnel — see Transport and TLS. |
refusing to proceed without confirmation |
A destructive command had no TTY to prompt on. | Pass --yes. |
set -euo pipefail
# 1. Create the election and capture its id.
# `jq -e` exits non-zero when `.id` is absent, so a failed creation stops
# here instead of leaving ID as "null" and cascading into the next steps.
ID=$(eccli --json create-election -n "Board 2025" --start-time 300 --duration 86400 \
--rules-id plurality --candidates-file candidates.json | jq -er .id)
# 2. (Optionally) add more candidates
eccli add-candidate -e "$ID" -c 4 --name "Write-in"
# 3. Issue registration tokens for your voters
eccli generate-tokens -e "$ID" -c 500 -o tokens.txt
# 4. Audit token usage during/after the election
eccli list-tokens -e "$ID"cargo fmt --all
cargo clippy --all-targets --all-features -- -D warnings
cargo test
cargo llvm-cov --summary-only # coverage (needs cargo-llvm-cov + llvm-tools-preview)MIT — see LICENSE.