use crate::blsag::hash_public_key_to_point;
use crate::signing::normalise_election_id;
use crate::types::{KeyImage, Nonce, OwnershipProof, PublicKey, SecretKey};
use blake2::{Blake2b512, Digest};
use curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT;
use curve25519_dalek::ristretto::RistrettoPoint;
use curve25519_dalek::scalar::Scalar;
use curve25519_dalek::traits::MultiscalarMul;
use rand::rngs::SysRng;
use rand_core::UnwrapErr;
const OWNERSHIP_DOMAIN: &[u8] = b"crypto_vote:ownership:challenge";
pub fn generate_nonce() -> Nonce {
let mut rng = UnwrapErr(SysRng);
Nonce::from_bytes(Scalar::random(&mut rng).to_bytes())
}
pub fn prove_ownership(
secret_key: &SecretKey,
election_id: &str,
context: &[u8],
) -> OwnershipProof {
let normalized = normalise_election_id(election_id);
let election_id = normalized.as_bytes();
let public_key = secret_key.public_key().point;
let base = hash_public_key_to_point(election_id, public_key);
let key_image = secret_key.scalar * base;
let mut rng = UnwrapErr(SysRng);
let r = Scalar::random(&mut rng);
let commitment_g = r * RISTRETTO_BASEPOINT_POINT;
let commitment_b = r * base;
let challenge = challenge_scalar(
election_id,
context,
public_key,
base,
key_image,
commitment_g,
commitment_b,
);
let response = r + challenge * secret_key.scalar;
OwnershipProof {
challenge,
response,
}
}
pub fn verify_ownership(
public_key: &PublicKey,
key_image: &KeyImage,
election_id: &str,
context: &[u8],
proof: &OwnershipProof,
) -> bool {
let normalized = normalise_election_id(election_id);
let election_id = normalized.as_bytes();
let public_key = public_key.point;
let key_image = key_image.point;
let base = hash_public_key_to_point(election_id, public_key);
let neg_challenge = -proof.challenge;
let commitment_g = RistrettoPoint::multiscalar_mul(
&[proof.response, neg_challenge],
&[RISTRETTO_BASEPOINT_POINT, public_key],
);
let commitment_b =
RistrettoPoint::multiscalar_mul(&[proof.response, neg_challenge], &[base, key_image]);
let expected = challenge_scalar(
election_id,
context,
public_key,
base,
key_image,
commitment_g,
commitment_b,
);
expected == proof.challenge
}
#[allow(clippy::too_many_arguments)]
fn challenge_scalar(
election_id: &[u8],
context: &[u8],
public_key: RistrettoPoint,
base: RistrettoPoint,
key_image: RistrettoPoint,
commitment_g: RistrettoPoint,
commitment_b: RistrettoPoint,
) -> Scalar {
let mut hash = Blake2b512::new();
hash.update(OWNERSHIP_DOMAIN);
hash.update((election_id.len() as u64).to_be_bytes());
hash.update(election_id);
hash.update((context.len() as u64).to_be_bytes());
hash.update(context);
hash.update(public_key.compress().as_bytes());
hash.update(base.compress().as_bytes());
hash.update(key_image.compress().as_bytes());
hash.update(commitment_g.compress().as_bytes());
hash.update(commitment_b.compress().as_bytes());
Scalar::from_hash(hash)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::generate_identity;
use crate::sign_vote;
const EID: &str = "election-2026";
const NONCE: &[u8] = b"verifier-chosen-nonce-123";
fn key_image_of(sk: &SecretKey, election_id: &str) -> KeyImage {
let normalized = normalise_election_id(election_id);
let base = hash_public_key_to_point(normalized.as_bytes(), sk.public_key().point);
KeyImage {
point: sk.scalar * base,
}
}
#[test]
fn round_trip_succeeds() {
let voter = generate_identity();
let ki = key_image_of(&voter.secret_key, EID);
let proof = prove_ownership(&voter.secret_key, EID, NONCE);
assert!(verify_ownership(&voter.public_key, &ki, EID, NONCE, &proof));
}
#[test]
fn key_image_matches_the_one_a_real_vote_produces() {
let voter = generate_identity();
let other = generate_identity().public_key;
let ring = vec![voter.public_key, other];
let vote_proof = sign_vote(&voter.secret_key, b"option-A", EID, &ring).unwrap();
let ki = key_image_of(&voter.secret_key, EID);
assert_eq!(ki, vote_proof.key_image);
let proof = prove_ownership(&voter.secret_key, EID, NONCE);
assert!(verify_ownership(
&voter.public_key,
&vote_proof.key_image,
EID,
NONCE,
&proof
));
}
#[test]
fn wrong_nonce_is_rejected() {
let voter = generate_identity();
let ki = key_image_of(&voter.secret_key, EID);
let proof = prove_ownership(&voter.secret_key, EID, NONCE);
assert!(!verify_ownership(
&voter.public_key,
&ki,
EID,
b"a-different-nonce",
&proof
));
}
#[test]
fn wrong_election_is_rejected() {
let voter = generate_identity();
let ki = key_image_of(&voter.secret_key, "election-A");
let proof = prove_ownership(&voter.secret_key, "election-A", NONCE);
assert!(!verify_ownership(
&voter.public_key,
&ki,
"election-B",
NONCE,
&proof
));
}
#[test]
fn cannot_prove_someone_elses_key_image() {
let alice = generate_identity();
let mallory = generate_identity();
let alice_ki = key_image_of(&alice.secret_key, EID);
let forged = prove_ownership(&mallory.secret_key, EID, NONCE);
assert!(!verify_ownership(
&alice.public_key,
&alice_ki,
EID,
NONCE,
&forged
));
}
#[test]
fn proof_against_wrong_public_key_is_rejected() {
let voter = generate_identity();
let other = generate_identity().public_key;
let ki = key_image_of(&voter.secret_key, EID);
let proof = prove_ownership(&voter.secret_key, EID, NONCE);
assert!(!verify_ownership(&other, &ki, EID, NONCE, &proof));
}
#[test]
fn tampered_proof_is_rejected() {
let voter = generate_identity();
let ki = key_image_of(&voter.secret_key, EID);
let mut proof = prove_ownership(&voter.secret_key, EID, NONCE);
proof.response += Scalar::ONE;
assert!(!verify_ownership(
&voter.public_key,
&ki,
EID,
NONCE,
&proof
));
}
#[test]
fn election_id_is_normalised_to_nfc() {
let nfc = "élection-2026";
let nfd = "e\u{0301}lection-2026";
let voter = generate_identity();
let ki = key_image_of(&voter.secret_key, nfd);
let proof = prove_ownership(&voter.secret_key, nfd, NONCE);
assert!(verify_ownership(&voter.public_key, &ki, nfc, NONCE, &proof));
}
#[test]
fn generated_nonces_are_distinct() {
assert_ne!(generate_nonce().to_bytes(), generate_nonce().to_bytes());
}
#[test]
fn round_trip_with_generated_nonce() {
let voter = generate_identity();
let ki = key_image_of(&voter.secret_key, EID);
let nonce = generate_nonce();
let proof = prove_ownership(&voter.secret_key, EID, nonce.as_bytes());
assert!(verify_ownership(
&voter.public_key,
&ki,
EID,
nonce.as_bytes(),
&proof
));
}
#[test]
fn nonce_round_trips_through_prefixed_encoding() {
let voter = generate_identity();
let ki = key_image_of(&voter.secret_key, EID);
let nonce = generate_nonce();
let decoded = Nonce::from_prefixed(&nonce.to_prefixed()).unwrap();
assert_eq!(nonce.to_bytes(), decoded.to_bytes());
let proof = prove_ownership(&voter.secret_key, EID, nonce.as_bytes());
assert!(verify_ownership(
&voter.public_key,
&ki,
EID,
decoded.as_bytes(),
&proof
));
}
#[test]
fn proof_round_trips_through_prefixed_encoding() {
let voter = generate_identity();
let ki = key_image_of(&voter.secret_key, EID);
let proof = prove_ownership(&voter.secret_key, EID, NONCE);
let wire = proof.to_prefixed();
let decoded = OwnershipProof::from_prefixed(&wire).unwrap();
assert!(verify_ownership(
&voter.public_key,
&ki,
EID,
NONCE,
&decoded
));
}
}