use clap::{Parser, Subcommand};
use crypto_vote::{
KeyImage, PublicKey, SecretKey, Signature, generate_identity, sign_vote, verify_vote,
};
use std::fs;
use std::path::PathBuf;
use std::process::ExitCode;
#[derive(Parser, Debug)]
#[command(
name = "cryptovote",
version,
about = "Linkable ring signatures for verifiable voting (Ristretto255 + Blake2b-512)."
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand, Debug)]
enum Command {
Keygen,
Sign {
#[arg(
long,
value_name = "HEX|-",
conflicts_with = "secret_file",
required_unless_present = "secret_file"
)]
secret: Option<String>,
#[arg(
long = "secret-file",
value_name = "FILE",
conflicts_with = "secret",
required_unless_present = "secret"
)]
secret_file: Option<PathBuf>,
#[arg(long)]
vote: String,
#[arg(long = "election-id")]
election_id: String,
#[arg(long)]
ring: PathBuf,
},
Verify {
#[arg(long)]
vote: String,
#[arg(long = "election-id")]
election_id: String,
#[arg(long)]
signature: String,
#[arg(long = "key-image")]
key_image: String,
#[arg(long)]
ring: PathBuf,
},
}
fn main() -> ExitCode {
let cli = Cli::parse();
match dispatch(cli.command) {
Ok(code) => code,
Err(e) => {
eprintln!("error: {e}");
ExitCode::from(2)
}
}
}
fn dispatch(cmd: Command) -> Result<ExitCode, Box<dyn std::error::Error>> {
match cmd {
Command::Keygen => {
let id = generate_identity();
println!("secret={}", id.secret_key.to_prefixed());
println!("public={}", id.public_key.to_prefixed());
Ok(ExitCode::SUCCESS)
}
Command::Sign {
secret,
secret_file,
vote,
election_id,
ring,
} => {
if secret.as_deref() == Some("-") && vote == "-" {
return Err(
"cannot read both --secret and --vote from stdin; use --secret-file".into(),
);
}
let secret = read_secret(secret.as_deref(), secret_file.as_ref())?;
let sk = SecretKey::from_prefixed(secret.trim())?;
let vote_bytes = read_vote(&vote)?;
let ring = read_ring(&ring)?;
let proof = sign_vote(&sk, &vote_bytes, &election_id, &ring)?;
println!("signature={}", proof.signature.to_prefixed());
println!("key_image={}", proof.key_image.to_prefixed());
Ok(ExitCode::SUCCESS)
}
Command::Verify {
vote,
election_id,
signature,
key_image,
ring,
} => {
let vote_bytes = read_vote(&vote)?;
let ring = read_ring(&ring)?;
let signature = Signature::from_prefixed(signature.trim(), ring.len())?;
let key_image = KeyImage::from_prefixed(key_image.trim())?;
let ok = verify_vote(&vote_bytes, &election_id, &signature, &key_image, &ring);
if ok {
println!("valid");
Ok(ExitCode::SUCCESS)
} else {
println!("invalid");
Ok(ExitCode::from(1))
}
}
}
}
fn read_secret(
secret: Option<&str>,
secret_file: Option<&PathBuf>,
) -> Result<String, Box<dyn std::error::Error>> {
match (secret, secret_file) {
(Some("-"), None) => {
let mut buf = String::new();
std::io::Read::read_to_string(&mut std::io::stdin(), &mut buf)?;
Ok(buf)
}
(Some(s), None) => Ok(s.to_owned()),
(None, Some(path)) => Ok(fs::read_to_string(path)?),
_ => Err("provide exactly one of --secret or --secret-file".into()),
}
}
fn read_vote(arg: &str) -> std::io::Result<Vec<u8>> {
if arg == "-" {
let mut buf = Vec::new();
std::io::Read::read_to_end(&mut std::io::stdin(), &mut buf)?;
Ok(buf)
} else {
Ok(arg.as_bytes().to_vec())
}
}
fn read_ring(path: &PathBuf) -> Result<Vec<PublicKey>, Box<dyn std::error::Error>> {
let raw = fs::read_to_string(path)?;
let mut keys = Vec::new();
for (lineno, line) in raw.lines().enumerate() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
match PublicKey::from_prefixed(line) {
Ok(pk) => keys.push(pk),
Err(e) => return Err(format!("ring file line {}: {e}", lineno + 1).into()),
}
}
Ok(keys)
}