use crypto_vote::{
Error, KeyImage, PublicKey, Signature, generate_identity, sign_vote, verify_vote,
};
const EID: &str = "550e8400-e29b-41d4-a716-446655440000";
fn fresh_election(n: usize) -> (crypto_vote::SecretKey, Vec<PublicKey>) {
let voter = generate_identity();
let mut ring = vec![voter.public_key];
for _ in 1..n {
ring.push(generate_identity().public_key);
}
(voter.secret_key, ring)
}
#[test]
fn full_flow_round_trip() {
let (sk, ring) = fresh_election(5);
let ballot = b"option-A";
let proof = sign_vote(&sk, ballot, EID, &ring).expect("sign");
assert!(verify_vote(
ballot,
EID,
&proof.signature,
&proof.key_image,
&ring
));
}
#[test]
fn serialised_proof_round_trip() {
let (sk, ring) = fresh_election(4);
let ballot = b"option-X";
let proof = sign_vote(&sk, ballot, EID, &ring).unwrap();
let sig_hex = proof.signature.to_hex();
let tag_hex = proof.key_image.to_hex();
let sig = Signature::from_hex(&sig_hex, ring.len()).unwrap();
let tag = KeyImage::from_hex(&tag_hex).unwrap();
assert!(verify_vote(ballot, EID, &sig, &tag, &ring));
}
#[test]
fn serialised_proof_round_trip_prefixed() {
let (sk, ring) = fresh_election(4);
let ballot = b"option-X";
let proof = sign_vote(&sk, ballot, EID, &ring).unwrap();
let sig_str = proof.signature.to_prefixed();
let tag_str = proof.key_image.to_prefixed();
assert!(sig_str.starts_with("blsag_"));
assert!(tag_str.starts_with("ki_"));
let sig = Signature::from_prefixed(&sig_str, ring.len()).unwrap();
let tag = KeyImage::from_prefixed(&tag_str).unwrap();
let ring_prefixed: Vec<PublicKey> = ring
.iter()
.map(|pk| PublicKey::from_prefixed(&pk.to_prefixed()).unwrap())
.collect();
assert!(verify_vote(ballot, EID, &sig, &tag, &ring_prefixed));
}
#[test]
fn prefixed_rejects_cross_type_and_corruption() {
let (sk, ring) = fresh_election(3);
let proof = sign_vote(&sk, b"option-A", EID, &ring).unwrap();
let tag_str = proof.key_image.to_prefixed();
assert!(matches!(
PublicKey::from_prefixed(&tag_str),
Err(Error::InvalidPrefix { expected: "pk", .. })
));
let pk_str = ring[0].to_prefixed();
let mut bytes = pk_str.into_bytes();
let last = bytes.last_mut().unwrap();
*last = if *last == b'0' { b'1' } else { b'0' };
let corrupted = String::from_utf8(bytes).unwrap();
assert_eq!(
PublicKey::from_prefixed(&corrupted).unwrap_err(),
Error::InvalidChecksum
);
}
#[test]
fn host_can_detect_double_vote_via_tag() {
let (sk, ring) = fresh_election(3);
let p1 = sign_vote(&sk, b"option-A", EID, &ring).unwrap();
let p2 = sign_vote(&sk, b"option-B", EID, &ring).unwrap();
assert_eq!(p1.key_image, p2.key_image);
}
#[test]
fn key_images_are_not_linkable_across_elections() {
let (sk, ring) = fresh_election(3);
let p1 = sign_vote(&sk, b"option-A", "election-A", &ring).unwrap();
let p2 = sign_vote(&sk, b"option-A", "election-B", &ring).unwrap();
assert_ne!(p1.key_image, p2.key_image);
}
#[test]
fn election_binding_isolates_signatures_across_elections() {
let (sk, ring) = fresh_election(4);
let proof = sign_vote(&sk, b"option-A", "election-X", &ring).unwrap();
assert!(!verify_vote(
b"option-A",
"election-Y",
&proof.signature,
&proof.key_image,
&ring,
));
}
#[test]
fn empty_inputs_are_rejected_at_signing() {
let (sk, ring) = fresh_election(3);
assert_eq!(
sign_vote(&sk, b"", EID, &ring).unwrap_err(),
Error::EmptyVote
);
assert_eq!(
sign_vote(&sk, b"yes", "", &ring).unwrap_err(),
Error::EmptyElectionId
);
}
#[test]
fn anonymity_is_indistinguishable_by_index() {
let voter_a = generate_identity();
let voter_b = generate_identity();
let voter_c = generate_identity();
let ring = vec![voter_a.public_key, voter_b.public_key, voter_c.public_key];
let p_a = sign_vote(&voter_a.secret_key, b"yes", EID, &ring).unwrap();
let p_b = sign_vote(&voter_b.secret_key, b"yes", EID, &ring).unwrap();
let p_c = sign_vote(&voter_c.secret_key, b"yes", EID, &ring).unwrap();
assert_eq!(
p_a.signature.to_bytes().len(),
p_b.signature.to_bytes().len()
);
assert_eq!(
p_b.signature.to_bytes().len(),
p_c.signature.to_bytes().len()
);
}
#[test]
fn signature_byte_length_matches_ring_size() {
let (sk, ring) = fresh_election(7);
let proof = sign_vote(&sk, b"yes", EID, &ring).unwrap();
assert_eq!(proof.signature.to_bytes().len(), 32 * (1 + ring.len()));
}
#[test]
fn parsing_signature_with_wrong_ring_size_fails() {
let (sk, ring) = fresh_election(3);
let proof = sign_vote(&sk, b"yes", EID, &ring).unwrap();
let err = Signature::from_hex(&proof.signature.to_hex(), 4).unwrap_err();
assert!(matches!(err, Error::InvalidLength { .. }));
}
#[test]
fn invalid_hex_inputs_are_rejected_cleanly() {
assert!(matches!(
PublicKey::from_hex("not-a-hex-string").unwrap_err(),
Error::InvalidHex
));
assert!(matches!(
KeyImage::from_hex("deadbeef").unwrap_err(),
Error::InvalidLength { .. }
));
}
fn split_range(len: usize, n_chunks: usize) -> Vec<std::ops::Range<usize>> {
let n = n_chunks.max(1).min(len.max(1));
let base = len / n;
let extra = len % n;
let mut ranges = Vec::with_capacity(n);
let mut start = 0;
for i in 0..n {
let end = start + base + usize::from(i < extra);
ranges.push(start..end);
start = end;
}
ranges
}
fn num_test_threads() -> usize {
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4)
}
#[test]
fn signature_is_bit_malleability_resistant() {
let (sk, ring) = fresh_election(3);
let proof = sign_vote(&sk, b"option-A", EID, &ring).unwrap();
let original = proof.signature.to_bytes();
std::thread::scope(|s| {
for range in split_range(original.len(), num_test_threads()) {
let original = &original;
let ring = ˚
let key_image = &proof.key_image;
s.spawn(move || {
for byte_index in range {
for bit in 0..8u8 {
let mut tampered = original.clone();
tampered[byte_index] ^= 1 << bit;
if let Ok(sig) = Signature::from_bytes(&tampered, ring.len()) {
assert!(
!verify_vote(b"option-A", EID, &sig, key_image, ring),
"tampered signature accepted at byte {byte_index} bit {bit}"
);
}
}
}
});
}
});
}
#[test]
fn key_image_is_bit_malleability_resistant() {
let (sk, ring) = fresh_election(3);
let proof = sign_vote(&sk, b"option-A", EID, &ring).unwrap();
let original = proof.key_image.to_bytes();
std::thread::scope(|s| {
for range in split_range(original.len(), num_test_threads()) {
let ring = ˚
let signature = &proof.signature;
s.spawn(move || {
for byte_index in range {
for bit in 0..8u8 {
let mut tampered = original;
tampered[byte_index] ^= 1 << bit;
if let Ok(ki) = KeyImage::from_bytes(&tampered) {
assert!(
!verify_vote(b"option-A", EID, signature, &ki, ring),
"tampered key image accepted at byte {byte_index} bit {bit}"
);
}
}
}
});
}
});
}
#[test]
fn random_key_image_is_rejected_by_verifier() {
use curve25519_dalek::ristretto::RistrettoPoint;
use rand::rngs::SysRng;
use rand_core::UnwrapErr;
let (sk, ring) = fresh_election(3);
let proof = sign_vote(&sk, b"option-A", EID, &ring).unwrap();
let mut rng = UnwrapErr(SysRng);
let bytes = loop {
let p = RistrettoPoint::random(&mut rng);
let compressed = p.compress().to_bytes();
if KeyImage::from_bytes(&compressed).is_ok() {
break compressed;
}
};
let bogus = KeyImage::from_bytes(&bytes).unwrap();
assert!(!verify_vote(
b"option-A",
EID,
&proof.signature,
&bogus,
&ring
));
}
#[test]
fn signature_does_not_verify_against_subset_or_superset_ring() {
let (sk, mut ring) = fresh_election(4);
let proof = sign_vote(&sk, b"yes", EID, &ring).unwrap();
let mut subset = ring.clone();
subset.pop();
assert!(!verify_vote(
b"yes",
EID,
&proof.signature,
&proof.key_image,
&subset
));
ring.push(generate_identity().public_key);
assert!(!verify_vote(
b"yes",
EID,
&proof.signature,
&proof.key_image,
&ring
));
}
#[test]
fn cannot_impersonate_by_swapping_in_someone_elses_key_image() {
let (sk_alice, mut ring) = fresh_election(3);
let bob = generate_identity();
ring.push(bob.public_key);
let bob_proof = sign_vote(&bob.secret_key, b"yes", EID, &ring).unwrap();
let alice_proof = sign_vote(&sk_alice, b"yes", EID, &ring).unwrap();
assert!(!verify_vote(
b"yes",
EID,
&alice_proof.signature,
&bob_proof.key_image,
&ring
));
assert!(!verify_vote(
b"yes",
EID,
&bob_proof.signature,
&alice_proof.key_image,
&ring
));
}
#[test]
fn signature_parser_rejects_truncated_and_oversized_payloads() {
let (sk, ring) = fresh_election(3);
let proof = sign_vote(&sk, b"yes", EID, &ring).unwrap();
let bytes = proof.signature.to_bytes();
assert!(Signature::from_bytes(&bytes[..bytes.len() - 1], ring.len()).is_err());
let mut padded = bytes.clone();
padded.push(0);
assert!(Signature::from_bytes(&padded, ring.len()).is_err());
assert!(Signature::from_bytes(&[], ring.len()).is_err());
}