extern crate std;
use crate::{AliceId, AliceSig, Identity};
use alice_crypto as crypto;
use curve25519_dalek::edwards::{CompressedEdwardsY, EdwardsPoint};
use curve25519_dalek::scalar::Scalar;
use subtle::ConstantTimeEq;
use zeroize::Zeroize;
#[derive(Clone, Copy, PartialEq, Eq)]
#[repr(C)]
pub struct SchnorrProof {
pub commitment: [u8; 32],
pub response: [u8; 32],
}
impl core::fmt::Debug for SchnorrProof {
#[inline(always)]
fn fmt(&self, _: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
Ok(())
}
}
impl SchnorrProof {
pub const N: usize = 64;
#[inline]
#[must_use]
pub fn to_bytes(&self) -> [u8; 64] {
let mut out = [0u8; 64];
out[..32].copy_from_slice(&self.commitment);
out[32..].copy_from_slice(&self.response);
out
}
#[must_use]
pub fn from_bytes(b: &[u8; 64]) -> Self {
let mut commitment = [0u8; 32];
let mut response = [0u8; 32];
commitment.copy_from_slice(&b[..32]);
response.copy_from_slice(&b[32..]);
Self {
commitment,
response,
}
}
#[inline]
#[must_use]
pub fn to_sig(&self) -> AliceSig {
AliceSig::new(self.to_bytes())
}
}
#[inline]
fn fiat_shamir(commitment_r: &[u8; 32], public_key: &[u8; 32], message: &[u8]) -> Scalar {
let mut input = std::vec::Vec::with_capacity(64 + message.len());
input.extend_from_slice(commitment_r);
input.extend_from_slice(public_key);
input.extend_from_slice(message);
let mut wide = [0u8; 64];
wide[..32].copy_from_slice(&crypto::derive_key("alice-schnorr-challenge-lo", &input));
wide[32..].copy_from_slice(&crypto::derive_key("alice-schnorr-challenge-hi", &input));
Scalar::from_bytes_mod_order_wide(&wide)
}
fn hedged_nonce(identity: &Identity, message: &[u8]) -> crate::Result<Scalar> {
let random: [u8; 32] = crate::rand()?;
let mut seed = identity.seed();
let mut nonce_input = std::vec::Vec::with_capacity(64 + message.len());
nonce_input.extend_from_slice(&seed);
nonce_input.extend_from_slice(message);
nonce_input.extend_from_slice(&random);
let mut wide = [0u8; 64];
wide[..32].copy_from_slice(&crypto::derive_key(
"alice-schnorr-nonce-v1-lo",
&nonce_input,
));
wide[32..].copy_from_slice(&crypto::derive_key(
"alice-schnorr-nonce-v1-hi",
&nonce_input,
));
seed.zeroize();
nonce_input.zeroize();
Ok(Scalar::from_bytes_mod_order_wide(&wide))
}
pub fn prove(identity: &Identity, message: &[u8]) -> crate::Result<SchnorrProof> {
let mut r = hedged_nonce(identity, message)?;
let big_r = EdwardsPoint::mul_base(&r);
let big_r_compressed = big_r.compress();
let public_key = identity.id();
let e = fiat_shamir(&big_r_compressed.0, public_key.as_bytes(), message);
let mut x = secret_scalar(identity);
let s = r + e * x;
r.zeroize();
x.zeroize();
Ok(SchnorrProof {
commitment: big_r_compressed.0,
response: s.to_bytes(),
})
}
#[must_use]
pub fn verify_proof(id: &AliceId, message: &[u8], proof: &SchnorrProof) -> bool {
let big_r = match CompressedEdwardsY(proof.commitment).decompress() {
Some(p) => p,
None => return false,
};
let big_p = match CompressedEdwardsY(*id.as_bytes()).decompress() {
Some(p) => p,
None => return false,
};
let s: Option<Scalar> = Scalar::from_canonical_bytes(proof.response).into();
let s = match s {
Some(s) => s,
None => return false,
};
let e = fiat_shamir(&proof.commitment, id.as_bytes(), message);
let lhs = EdwardsPoint::mul_base(&s);
let rhs = big_r + big_p * e;
lhs.compress()
.as_bytes()
.ct_eq(rhs.compress().as_bytes())
.into()
}
#[must_use]
pub fn verify_batch(items: &[(&AliceId, &[u8], &SchnorrProof)]) -> bool {
if items.is_empty() {
return true;
}
if items.len() == 1 {
return verify_proof(items[0].0, items[0].1, items[0].2);
}
let mut weight_seed_input = std::vec::Vec::with_capacity(items.len() * 64);
for (id, msg, proof) in items {
weight_seed_input.extend_from_slice(id.as_bytes());
weight_seed_input.extend_from_slice(&proof.commitment);
weight_seed_input.extend_from_slice(&proof.response);
weight_seed_input.extend_from_slice(msg);
}
let weight_seed = crypto::derive_key("alice-schnorr-batch-v1", &weight_seed_input);
let mut total_s = Scalar::ZERO;
let mut scalars = std::vec::Vec::with_capacity(items.len() * 2);
let mut points = std::vec::Vec::with_capacity(items.len() * 2);
for (i, (id, msg, proof)) in items.iter().enumerate() {
let big_r = match CompressedEdwardsY(proof.commitment).decompress() {
Some(p) => p,
None => return false,
};
let big_p = match CompressedEdwardsY(*id.as_bytes()).decompress() {
Some(p) => p,
None => return false,
};
let s: Option<Scalar> = Scalar::from_canonical_bytes(proof.response).into();
let s = match s {
Some(s) => s,
None => return false,
};
let mut idx_input = [0u8; 36];
idx_input[..32].copy_from_slice(&weight_seed);
idx_input[32..36].copy_from_slice(&(i as u32).to_le_bytes());
let mut z_wide = [0u8; 64];
z_wide[..32].copy_from_slice(&crypto::derive_key(
"alice-schnorr-batch-weight-lo",
&idx_input,
));
z_wide[32..].copy_from_slice(&crypto::derive_key(
"alice-schnorr-batch-weight-hi",
&idx_input,
));
let z = Scalar::from_bytes_mod_order_wide(&z_wide);
let e = fiat_shamir(&proof.commitment, id.as_bytes(), msg);
total_s += z * s;
scalars.push(z);
points.push(big_r);
scalars.push(z * e);
points.push(big_p);
}
let lhs = EdwardsPoint::mul_base(&total_s);
let rhs =
<EdwardsPoint as curve25519_dalek::traits::VartimeMultiscalarMul>::vartime_multiscalar_mul(
scalars.iter(),
points.iter(),
);
lhs.compress()
.as_bytes()
.ct_eq(rhs.compress().as_bytes())
.into()
}
fn secret_scalar(identity: &Identity) -> Scalar {
use ed25519_dalek::hazmat::ExpandedSecretKey;
let mut seed = identity.seed();
let expanded = ExpandedSecretKey::from(&seed);
seed.zeroize();
expanded.scalar
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn prove_verify_basic() {
let id = Identity::gen().unwrap();
let proof = prove(&id, b"hello world").unwrap();
assert!(verify_proof(&id.id(), b"hello world", &proof));
}
#[test]
fn wrong_message_fails() {
let id = Identity::gen().unwrap();
let proof = prove(&id, b"hello").unwrap();
assert!(!verify_proof(&id.id(), b"goodbye", &proof));
}
#[test]
fn wrong_identity_fails() {
let alice = Identity::gen().unwrap();
let bob = Identity::gen().unwrap();
let proof = prove(&alice, b"msg").unwrap();
assert!(!verify_proof(&bob.id(), b"msg", &proof));
}
#[test]
fn empty_message() {
let id = Identity::gen().unwrap();
let proof = prove(&id, b"").unwrap();
assert!(verify_proof(&id.id(), b"", &proof));
}
#[test]
fn large_message() {
let id = Identity::gen().unwrap();
let msg = vec![0xAA; 4096];
let proof = prove(&id, &msg).unwrap();
assert!(verify_proof(&id.id(), &msg, &proof));
}
#[test]
fn proof_to_bytes_roundtrip() {
let id = Identity::gen().unwrap();
let proof = prove(&id, b"roundtrip").unwrap();
let bytes = proof.to_bytes();
let restored = SchnorrProof::from_bytes(&bytes);
assert_eq!(proof, restored);
assert!(verify_proof(&id.id(), b"roundtrip", &restored));
}
#[test]
fn proof_to_sig_compatibility() {
let id = Identity::gen().unwrap();
let proof = prove(&id, b"wire").unwrap();
let sig = proof.to_sig();
assert_eq!(sig.as_bytes().len(), 64);
let restored = SchnorrProof::from_bytes(sig.as_bytes());
assert!(verify_proof(&id.id(), b"wire", &restored));
}
#[test]
fn two_proofs_differ() {
let id = Identity::gen().unwrap();
let p1 = prove(&id, b"same").unwrap();
let p2 = prove(&id, b"same").unwrap();
assert_ne!(p1.commitment, p2.commitment);
assert!(verify_proof(&id.id(), b"same", &p1));
assert!(verify_proof(&id.id(), b"same", &p2));
}
#[test]
fn tampered_commitment_fails() {
let id = Identity::gen().unwrap();
let mut proof = prove(&id, b"tamper").unwrap();
proof.commitment[0] ^= 0xFF;
assert!(!verify_proof(&id.id(), b"tamper", &proof));
}
#[test]
fn tampered_response_fails() {
let id = Identity::gen().unwrap();
let mut proof = prove(&id, b"tamper").unwrap();
proof.response[0] ^= 0xFF;
assert!(!verify_proof(&id.id(), b"tamper", &proof));
}
#[test]
fn proof_size_is_64() {
assert_eq!(SchnorrProof::N, 64);
assert_eq!(core::mem::size_of::<SchnorrProof>(), 64);
}
#[test]
fn non_canonical_response_rejected() {
let id = Identity::gen().unwrap();
let mut proof = prove(&id, b"canonical").unwrap();
proof.response = [0xFF; 32];
assert!(!verify_proof(&id.id(), b"canonical", &proof));
}
#[test]
fn batch_verify_empty() {
assert!(verify_batch(&[]));
}
#[test]
fn batch_verify_single() {
let id = Identity::gen().unwrap();
let proof = prove(&id, b"single").unwrap();
assert!(verify_batch(&[(&id.id(), &b"single"[..], &proof)]));
}
#[test]
fn batch_verify_multiple_valid() {
let ids: Vec<_> = (0..5).map(|_| Identity::gen().unwrap()).collect();
let messages: Vec<&[u8]> = vec![b"msg0", b"msg1", b"msg2", b"msg3", b"msg4"];
let proofs: Vec<_> = ids
.iter()
.zip(messages.iter())
.map(|(id, msg)| prove(id, msg).unwrap())
.collect();
let pub_ids: Vec<_> = ids.iter().map(|id| id.id()).collect();
let items: Vec<_> = pub_ids
.iter()
.zip(messages.iter())
.zip(proofs.iter())
.map(|((id, msg), proof)| (id, *msg, proof))
.collect();
assert!(verify_batch(&items));
}
#[test]
fn batch_verify_one_invalid_fails() {
let ids: Vec<_> = (0..3).map(|_| Identity::gen().unwrap()).collect();
let messages: Vec<&[u8]> = vec![b"a", b"b", b"c"];
let mut proofs: Vec<_> = ids
.iter()
.zip(messages.iter())
.map(|(id, msg)| prove(id, msg).unwrap())
.collect();
proofs[1].response[0] ^= 0xFF;
let pub_ids: Vec<_> = ids.iter().map(|id| id.id()).collect();
let items: Vec<_> = pub_ids
.iter()
.zip(messages.iter())
.zip(proofs.iter())
.map(|((id, msg), proof)| (id, *msg, proof))
.collect();
assert!(!verify_batch(&items));
}
#[test]
fn batch_verify_consistent_with_individual() {
let ids: Vec<_> = (0..4).map(|_| Identity::gen().unwrap()).collect();
let messages: Vec<&[u8]> = vec![b"w", b"x", b"y", b"z"];
let proofs: Vec<_> = ids
.iter()
.zip(messages.iter())
.map(|(id, msg)| prove(id, msg).unwrap())
.collect();
let pub_ids: Vec<_> = ids.iter().map(|id| id.id()).collect();
for ((id, msg), proof) in pub_ids.iter().zip(messages.iter()).zip(proofs.iter()) {
assert!(verify_proof(id, msg, proof));
}
let items: Vec<_> = pub_ids
.iter()
.zip(messages.iter())
.zip(proofs.iter())
.map(|((id, msg), proof)| (id, *msg, proof))
.collect();
assert!(verify_batch(&items));
}
}