use crate::error::{Error, Result};
use crate::types::{KeyImage, PublicKey, SecretKey, Signature, VoteProof};
use curve25519_dalek::ristretto::RistrettoPoint;
use unicode_normalization::UnicodeNormalization;
pub fn sign_vote(
secret_key: &SecretKey,
vote: &[u8],
election_id: &str,
ring: &[PublicKey],
) -> Result<VoteProof> {
if vote.is_empty() {
return Err(Error::EmptyVote);
}
if election_id.is_empty() {
return Err(Error::EmptyElectionId);
}
let normalized = normalise_election_id(election_id);
let election_id = normalized.as_bytes();
let sorted = canonicalised_ring(ring)?;
let signer_pk = secret_key.public_key();
let secret_index = sorted
.iter()
.position(|pk| *pk == signer_pk)
.ok_or(Error::SignerNotInRing)?;
let ring_points: Vec<RistrettoPoint> = sorted.iter().map(|pk| pk.point).collect();
let bound = bind_to_election(election_id, vote);
let blsag = crate::blsag::sign(
secret_key.scalar,
&ring_points,
secret_index,
election_id,
&bound,
);
Ok(VoteProof {
signature: Signature {
challenge: blsag.challenge,
responses: blsag.responses,
},
key_image: KeyImage {
point: blsag.key_image,
},
})
}
pub(crate) fn bind_to_election(election_id: &[u8], vote: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(8 + election_id.len() + vote.len());
out.extend_from_slice(&(election_id.len() as u64).to_be_bytes());
out.extend_from_slice(election_id);
out.extend_from_slice(vote);
out
}
pub(crate) fn normalise_election_id(election_id: &str) -> String {
election_id.nfc().collect()
}
pub(crate) fn canonicalised_ring(ring: &[PublicKey]) -> Result<Vec<PublicKey>> {
if ring.len() < 2 {
return Err(Error::RingTooSmall);
}
let mut sorted: Vec<PublicKey> = ring.to_vec();
sorted.sort_by_key(|pk| pk.to_bytes());
for pair in sorted.windows(2) {
if pair[0] == pair[1] {
return Err(Error::DuplicateRingMember);
}
}
Ok(sorted)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::generate_identity;
const EID: &str = "election-2026";
#[test]
fn rejects_ring_too_small() {
let voter = generate_identity();
let err = sign_vote(&voter.secret_key, b"yes", EID, &[voter.public_key]).unwrap_err();
assert_eq!(err, Error::RingTooSmall);
}
#[test]
fn rejects_duplicate_ring_members() {
let voter = generate_identity();
let dup = voter.public_key;
let other = generate_identity().public_key;
let err = sign_vote(
&voter.secret_key,
b"yes",
EID,
&[voter.public_key, dup, other],
)
.unwrap_err();
assert_eq!(err, Error::DuplicateRingMember);
}
#[test]
fn rejects_signer_outside_ring() {
let voter = generate_identity();
let a = generate_identity().public_key;
let b = generate_identity().public_key;
let err = sign_vote(&voter.secret_key, b"yes", EID, &[a, b]).unwrap_err();
assert_eq!(err, Error::SignerNotInRing);
}
#[test]
fn rejects_empty_vote() {
let voter = generate_identity();
let other = generate_identity().public_key;
let err = sign_vote(&voter.secret_key, b"", EID, &[voter.public_key, other]).unwrap_err();
assert_eq!(err, Error::EmptyVote);
}
#[test]
fn rejects_empty_election_id() {
let voter = generate_identity();
let other = generate_identity().public_key;
let err = sign_vote(&voter.secret_key, b"yes", "", &[voter.public_key, other]).unwrap_err();
assert_eq!(err, Error::EmptyElectionId);
}
#[test]
fn election_id_is_normalised_to_nfc() {
let nfc = "élection-2026";
let nfd = "e\u{0301}lection-2026";
assert_ne!(nfc.as_bytes(), nfd.as_bytes());
assert_eq!(normalise_election_id(nfc), normalise_election_id(nfd));
let voter = generate_identity();
let other = generate_identity().public_key;
let ring = vec![voter.public_key, other];
let proof = sign_vote(&voter.secret_key, b"yes", nfd, &ring).unwrap();
assert!(crate::verify_vote(
b"yes",
nfc,
&proof.signature,
&proof.key_image,
&ring,
));
}
#[test]
fn binding_is_unambiguous_between_eid_and_vote() {
let a = bind_to_election(b"AB", b"C");
let b = bind_to_election(b"A", b"BC");
assert_ne!(a, b);
}
}