#![deny(rustdoc::broken_intra_doc_links)]
#![allow(non_snake_case)]
use curve25519_dalek::{
constants::RISTRETTO_BASEPOINT_TABLE,
ristretto::{CompressedRistretto, RistrettoPoint},
scalar::Scalar,
traits::Identity,
};
use rand_core::{CryptoRng, RngCore};
use sha2::{Digest, Sha512};
use subtle::{ConstantTimeEq, CtOption};
const CHALLENGE_GENERATION_DOMAIN_SEPARATOR_FRONT: &[u8] = b"\x02";
const CHALLENGE_GENERATION_DOMAIN_SEPARATOR_BACK: &[u8] = b"\x00";
const PROOF_TO_HASH_DOMAIN_SEPARATOR_FRONT: &[u8] = b"\x03";
const PROOF_TO_HASH_DOMAIN_SEPARATOR_BACK: &[u8] = b"\x00";
const SUITE_STRING: &[u8] = b"\xFFc2sp.org/vrf-r255";
use RISTRETTO_BASEPOINT_TABLE as B;
const PT_LEN: usize = 32;
const C_LEN: usize = 16;
const Q_LEN: usize = 32;
const H_LEN: usize = 64;
type Hash = Sha512;
const NONCE_GENERATION_DOMAIN_SEPARATOR: &[u8] = b"\x81";
const ENCODE_TO_CURVE_DOMAIN_SEPARATOR: &[u8] = b"\x82";
fn encode_to_curve(encode_to_curve_salt: &[u8], alpha_string: &[u8]) -> RistrettoPoint {
let mut hasher = Hash::new_with_prefix(SUITE_STRING);
hasher.update(ENCODE_TO_CURVE_DOMAIN_SEPARATOR);
hasher.update(encode_to_curve_salt);
hasher.update(alpha_string);
RistrettoPoint::from_uniform_bytes(&hasher.finalize().into())
}
#[derive(Clone, Copy, Debug)]
struct Challenge(Scalar);
impl Challenge {
fn generate(points: [RistrettoPoint; 5]) -> Self {
let mut hasher = Hash::new_with_prefix(SUITE_STRING);
hasher.update(CHALLENGE_GENERATION_DOMAIN_SEPARATOR_FRONT);
for point in points {
hasher.update(point.compress().as_bytes());
}
hasher.update(CHALLENGE_GENERATION_DOMAIN_SEPARATOR_BACK);
let c_string = hasher.finalize();
Self::parse(c_string[..C_LEN].try_into().unwrap())
}
fn parse(c_string: [u8; C_LEN]) -> Self {
let mut tmp = [0; 32];
tmp[0..C_LEN].copy_from_slice(&c_string);
Challenge(Scalar::from_canonical_bytes(tmp).unwrap())
}
fn encode(&self) -> [u8; C_LEN] {
self.0.as_bytes()[..C_LEN].try_into().unwrap()
}
}
impl ConstantTimeEq for Challenge {
fn ct_eq(&self, other: &Self) -> subtle::Choice {
self.0.as_bytes()[..C_LEN].ct_eq(&other.0.as_bytes()[..C_LEN])
}
}
#[derive(Clone, Copy, Debug)]
pub struct SecretKey {
x: Scalar,
pk: PublicKey,
}
impl SecretKey {
pub fn generate<R: RngCore + CryptoRng>(mut rng: R) -> Self {
let x = Scalar::random(&mut rng);
Self::from_scalar(x).unwrap()
}
pub fn from_bytes(bytes: [u8; 32]) -> CtOption<Self> {
let x = Scalar::from_canonical_bytes(bytes);
x.and_then(Self::from_scalar)
}
fn from_scalar(x: Scalar) -> CtOption<Self> {
let Y = &x * B;
let Y_bytes = Y.compress();
CtOption::new(
SecretKey {
x,
pk: PublicKey { Y, Y_bytes },
},
!Y_bytes.ct_eq(&CompressedRistretto::identity()),
)
}
pub fn to_bytes(&self) -> [u8; 32] {
self.x.to_bytes()
}
fn generate_nonce(&self, h_string: &[u8]) -> Scalar {
let mut hasher = Hash::new_with_prefix(SUITE_STRING);
hasher.update(NONCE_GENERATION_DOMAIN_SEPARATOR);
hasher.update(self.x.as_bytes());
hasher.update(h_string);
Scalar::from_bytes_mod_order_wide(&hasher.finalize().into())
}
pub fn prove(&self, alpha_string: &[u8]) -> Proof {
let H = encode_to_curve(self.pk.Y_bytes.as_bytes(), alpha_string);
let h_string = H.compress();
let Gamma = self.x * H;
let k = self.generate_nonce(h_string.as_bytes());
let c = Challenge::generate([self.pk.Y, H, Gamma, &k * B, k * H]);
let s = k + c.0 * self.x;
Proof { Gamma, c, s }
}
}
#[derive(Clone, Copy, Debug, Eq)]
pub struct PublicKey {
Y: RistrettoPoint,
Y_bytes: CompressedRistretto,
}
impl From<SecretKey> for PublicKey {
fn from(sk: SecretKey) -> Self {
sk.pk
}
}
impl ConstantTimeEq for PublicKey {
fn ct_eq(&self, other: &Self) -> subtle::Choice {
self.Y_bytes.ct_eq(&other.Y_bytes)
}
}
impl PartialEq<PublicKey> for PublicKey {
fn eq(&self, other: &PublicKey) -> bool {
self.ct_eq(other).into()
}
}
impl PublicKey {
pub fn from_bytes(bytes: [u8; 32]) -> Option<Self> {
let Y_bytes = CompressedRistretto(bytes);
Y_bytes
.decompress()
.filter(|_| !Y_bytes.eq(&CompressedRistretto::identity()))
.map(|Y| PublicKey { Y, Y_bytes })
}
pub fn to_bytes(&self) -> [u8; 32] {
self.Y_bytes.to_bytes()
}
pub fn verify(&self, alpha_string: &[u8], pi: &Proof) -> CtOption<[u8; H_LEN]> {
let H = encode_to_curve(self.Y_bytes.as_bytes(), alpha_string);
let U = &pi.s * B - pi.c.0 * self.Y;
let V = pi.s * H - pi.c.0 * pi.Gamma;
let c_prime = Challenge::generate([self.Y, H, pi.Gamma, U, V]);
CtOption::new(pi.derive_hash(), pi.c.ct_eq(&c_prime))
}
}
#[derive(Clone, Copy, Debug)]
pub struct Proof {
Gamma: RistrettoPoint,
c: Challenge,
s: Scalar,
}
impl ConstantTimeEq for Proof {
fn ct_eq(&self, other: &Self) -> subtle::Choice {
self.Gamma.ct_eq(&other.Gamma) & self.c.ct_eq(&other.c) & self.s.ct_eq(&other.s)
}
}
impl PartialEq for Proof {
fn eq(&self, other: &Self) -> bool {
self.ct_eq(other).into()
}
}
impl Proof {
pub fn from_bytes(pi_string: [u8; PT_LEN + C_LEN + Q_LEN]) -> Option<Self> {
let Gamma = CompressedRistretto(pi_string[0..PT_LEN].try_into().expect("correct length"))
.decompress();
let c = Challenge::parse(pi_string[PT_LEN..PT_LEN + C_LEN].try_into().unwrap());
let s = Scalar::from_canonical_bytes(
pi_string[PT_LEN + C_LEN..PT_LEN + C_LEN + Q_LEN]
.try_into()
.unwrap(),
);
Gamma.zip(s.into()).map(|(Gamma, s)| Proof { Gamma, c, s })
}
pub fn to_bytes(&self) -> [u8; PT_LEN + C_LEN + Q_LEN] {
let mut pi_string = [0u8; PT_LEN + C_LEN + Q_LEN];
pi_string[0..PT_LEN].copy_from_slice(self.Gamma.compress().as_bytes());
pi_string[PT_LEN..PT_LEN + C_LEN].copy_from_slice(&self.c.encode());
pi_string[PT_LEN + C_LEN..PT_LEN + C_LEN + Q_LEN].copy_from_slice(&self.s.to_bytes());
pi_string
}
fn derive_hash(&self) -> [u8; H_LEN] {
assert_eq!(Hash::output_size(), H_LEN);
let mut beta_string = Hash::new_with_prefix(SUITE_STRING);
beta_string.update(PROOF_TO_HASH_DOMAIN_SEPARATOR_FRONT);
beta_string.update(self.Gamma.compress().as_bytes());
beta_string.update(PROOF_TO_HASH_DOMAIN_SEPARATOR_BACK);
beta_string.finalize().into()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vector() {
let tv_sk = "3431c2b03533e280b23232e280b34e2c3132c2b03238e280b23131e280b34500";
let tv_pk = "54136cd90d99fbd1d4e855d9556efea87ba0337f2a6ce22028d0f5726fcb854e";
let tv_alpha = "633273702e6f72672f7672662d72323535";
let tv_H = "f245308737c2a888ba56448c8cdbce9d063b57b147e063ce36c580194ef31a63";
let tv_k_string = "b5eb28143d9defee6faa0c02ff0168b7ac80ea89fe9362845af15cabd100a91ed6251dfa52be36405576eca4a0970f91225b85c8813206d13bd8b42fd11a00fe";
let tv_k = "d32fcc5ae91ba05704da9df434f22fd4c2c373fdd8294bbb58bf27292aeec00a";
let tv_U = "9a30709d72de12d67f7af1cd8695ff16214d2d4600ae5f478873d2e7ed0ece73";
let tv_V = "5e727d972b11f6490b0b1ba8147775bceb1a2cb523b381fa22d5a5c0e97d4744";
let tv_pi = "0a97d961262fb549b4175c5117860f42ae44a123f93c476c439eddd1c0cff9265c805525233e2284dbed45e593b8eea31d5ca9734d72bcbba9738d5237f955f3b2422351149d1312503b6441a47c940c";
let tv_beta = "dd653f0879b48c3ef69e13551239bec4cbcc1c18fe8894de2e9e1c790e18273603bf1c6c25d7a797aeff3c43fd32b974d3fcbd4bcce916007097922a3ea3a794";
let tv_sk = hex::decode(tv_sk).unwrap();
let tv_pk = hex::decode(tv_pk).unwrap();
let tv_alpha = hex::decode(tv_alpha).unwrap();
let tv_H = hex::decode(tv_H).unwrap();
let tv_k_string = hex::decode(tv_k_string).unwrap();
let tv_k = hex::decode(tv_k).unwrap();
let tv_U = hex::decode(tv_U).unwrap();
let tv_V = hex::decode(tv_V).unwrap();
let tv_pi = hex::decode(tv_pi).unwrap();
let tv_beta = hex::decode(tv_beta).unwrap();
let sk = SecretKey::from_bytes(tv_sk.try_into().unwrap()).unwrap();
let pk = PublicKey::from_bytes(tv_pk.try_into().unwrap()).unwrap();
assert_eq!(PublicKey::from(sk), pk);
let H = CompressedRistretto::from_slice(&tv_H)
.unwrap()
.decompress()
.unwrap();
assert_eq!(encode_to_curve(pk.Y_bytes.as_bytes(), &tv_alpha), H);
let k = Scalar::from_canonical_bytes(tv_k.try_into().unwrap()).unwrap();
assert_eq!(
Scalar::from_bytes_mod_order_wide(&tv_k_string.try_into().unwrap()),
k
);
assert_eq!(sk.generate_nonce(H.compress().as_bytes()), k);
let U = CompressedRistretto::from_slice(&tv_U)
.unwrap()
.decompress()
.unwrap();
let V = CompressedRistretto::from_slice(&tv_V)
.unwrap()
.decompress()
.unwrap();
assert_eq!(&k * B, U);
assert_eq!(k * H, V);
let pi = Proof::from_bytes(tv_pi.try_into().unwrap()).unwrap();
assert_eq!(sk.prove(&tv_alpha), pi);
assert_eq!(
pk.verify(&tv_alpha, &pi).unwrap(),
<[u8; H_LEN]>::try_from(tv_beta).unwrap()
);
}
}