use alloc::vec::Vec;
use curve25519_dalek::ristretto::{CompressedRistretto, RistrettoPoint};
use curve25519_dalek::Scalar;
use pakery_core::crypto::oprf::{Oprf, OprfClientState};
use pakery_core::PakeError;
use rand_core::CryptoRng;
use sha2::Sha512;
use subtle::ConstantTimeEq;
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
use crate::oprf_common::{expand_message_xmd, finalize_hash, i2osp_2};
const HASH_TO_GROUP_DST: &[u8] = b"HashToGroup-OPRFV1-\x00-ristretto255-SHA512";
#[cfg(test)]
const HASH_TO_SCALAR_DST: &[u8] = b"HashToScalar-OPRFV1-\x00-ristretto255-SHA512";
const DERIVE_KEYPAIR_DST: &[u8] = b"DeriveKeyPairOPRFV1-\x00-ristretto255-SHA512";
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct Ristretto255OprfClientState {
blind: [u8; 32],
}
impl OprfClientState for Ristretto255OprfClientState {
fn finalize(
&self,
password: &[u8],
evaluated_bytes: &[u8],
) -> Result<Zeroizing<Vec<u8>>, PakeError> {
let z_bytes: [u8; 32] = evaluated_bytes
.try_into()
.map_err(|_| PakeError::InvalidInput("invalid evaluation element length"))?;
let z = CompressedRistretto(z_bytes)
.decompress()
.ok_or(PakeError::InvalidInput("invalid evaluation element"))?;
{
use curve25519_dalek::traits::Identity;
if bool::from(z.ct_eq(&RistrettoPoint::identity())) {
return Err(PakeError::InvalidInput("evaluation element is identity"));
}
}
let blind_copy = Zeroizing::new(self.blind);
pakery_core::ct::declassify(&*blind_copy);
let blind_scalar = Scalar::from_canonical_bytes(*blind_copy)
.into_option()
.ok_or(PakeError::ProtocolError("invalid blind scalar"))?;
if bool::from(blind_scalar.ct_eq(&Scalar::ZERO)) {
return Err(PakeError::ProtocolError("blind scalar is zero"));
}
let r_inv = blind_scalar.invert();
let n = r_inv * z;
Ok(Zeroizing::new(finalize_hash::<Sha512>(
password,
&n.compress().to_bytes(),
)?))
}
}
pub struct Ristretto255Oprf;
fn hash_to_group(input: &[u8]) -> Result<RistrettoPoint, PakeError> {
let uniform = expand_message_xmd::<Sha512>(&[input], HASH_TO_GROUP_DST, 64)?;
let arr: [u8; 64] = uniform
.try_into()
.expect("expand_message_xmd returned 64 bytes");
Ok(RistrettoPoint::from_uniform_bytes(&arr))
}
#[cfg(test)]
fn hash_to_scalar(input: &[&[u8]]) -> Result<Scalar, PakeError> {
hash_to_scalar_with_dst(input, HASH_TO_SCALAR_DST)
}
fn hash_to_scalar_with_dst(input: &[&[u8]], dst: &[u8]) -> Result<Scalar, PakeError> {
let uniform = expand_message_xmd::<Sha512>(input, dst, 64)?;
let arr: [u8; 64] = uniform
.try_into()
.expect("expand_message_xmd returned 64 bytes");
Ok(Scalar::from_bytes_mod_order_wide(&arr))
}
impl Oprf for Ristretto255Oprf {
type ClientState = Ristretto255OprfClientState;
fn client_blind(
password: &[u8],
rng: &mut impl CryptoRng,
) -> Result<(Self::ClientState, Vec<u8>), PakeError> {
let mut r = loop {
let mut wide = Zeroizing::new([0u8; 64]);
rng.fill_bytes(&mut *wide);
pakery_core::ct::mark_secret(&*wide);
let s = Scalar::from_bytes_mod_order_wide(&wide);
if !pakery_core::ct::declassify_choice(s.ct_eq(&Scalar::ZERO)) {
break s;
}
};
let t = hash_to_group(password)?;
let blinded = r * t;
let blind = r.to_bytes();
r.zeroize();
let blinded_bytes = blinded.compress().to_bytes().to_vec();
pakery_core::ct::declassify(&blinded_bytes);
Ok((Ristretto255OprfClientState { blind }, blinded_bytes))
}
fn server_evaluate(oprf_key: &[u8], blinded_bytes: &[u8]) -> Result<Vec<u8>, PakeError> {
let sk_bytes: [u8; 32] = oprf_key
.try_into()
.map_err(|_| PakeError::InvalidInput("invalid OPRF key length"))?;
let sk_bytes = Zeroizing::new(sk_bytes);
pakery_core::ct::declassify(&*sk_bytes);
let sk = Scalar::from_canonical_bytes(*sk_bytes)
.into_option()
.ok_or(PakeError::InvalidInput("invalid OPRF key"))?;
if bool::from(sk.ct_eq(&Scalar::ZERO)) {
return Err(PakeError::InvalidInput("OPRF key is zero"));
}
let blinded_arr: [u8; 32] = blinded_bytes
.try_into()
.map_err(|_| PakeError::InvalidInput("invalid blinded element length"))?;
let blinded = CompressedRistretto(blinded_arr)
.decompress()
.ok_or(PakeError::InvalidInput("invalid blinded element"))?;
{
use curve25519_dalek::traits::Identity;
use subtle::ConstantTimeEq;
if bool::from(blinded.ct_eq(&RistrettoPoint::identity())) {
return Err(PakeError::InvalidInput("blinded element is identity"));
}
}
let evaluated = sk * blinded;
Ok(evaluated.compress().to_bytes().to_vec())
}
fn derive_key(seed: &[u8], info: &[u8]) -> Result<Zeroizing<Vec<u8>>, PakeError> {
let info_len = i2osp_2(info.len())?;
for counter in 0u8..=255 {
let sk =
hash_to_scalar_with_dst(&[seed, &info_len, info, &[counter]], DERIVE_KEYPAIR_DST)?;
if !pakery_core::ct::declassify_choice(sk.ct_eq(&Scalar::ZERO)) {
return Ok(Zeroizing::new(sk.to_bytes().to_vec()));
}
}
Err(PakeError::ProtocolError(
"DeriveKeyPair: all counters yielded zero",
))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn hex(s: &str) -> Vec<u8> {
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
.collect()
}
const SEED: &str = "a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3";
const KEY_INFO: &str = "74657374206b6579";
const SK_SM: &str = "5ebcea5ee37023ccb9fc2d2019f9d7737be85591ae8652ffa9ef0f4d37063b0e";
#[test]
fn derive_key_pair() {
let sk = Ristretto255Oprf::derive_key(&hex(SEED), &hex(KEY_INFO)).unwrap();
assert_eq!(*sk, hex(SK_SM));
}
#[test]
fn test_vector_1() {
let input = hex("00");
let blind_bytes = hex("64d37aed22a27f5191de1c1d69fadb899d8862b58eb4220029e036ec4c1f6706");
let expected_blinded =
hex("609a0ae68c15a3cf6903766461307e5c8bb2f95e7e6550e1ffa2dc99e412803c");
let expected_eval = hex("7ec6578ae5120958eb2db1745758ff379e77cb64fe77b0b2d8cc917ea0869c7e");
let expected_output = hex("527759c3d9366f277d8c6020418d96bb393ba2afb20ff90df23fb7708264e2f3ab9135e3bd69955851de4b1f9fe8a0973396719b7912ba9ee8aa7d0b5e24bcf6");
let blind = Scalar::from_canonical_bytes(blind_bytes.as_slice().try_into().unwrap())
.into_option()
.unwrap();
let t = hash_to_group(&input).unwrap();
let blinded = blind * t;
assert_eq!(blinded.compress().to_bytes().to_vec(), expected_blinded);
let sk = hex(SK_SM);
let eval = Ristretto255Oprf::server_evaluate(&sk, &expected_blinded).unwrap();
assert_eq!(eval, expected_eval);
let state = Ristretto255OprfClientState {
blind: blind_bytes.as_slice().try_into().unwrap(),
};
let output = state.finalize(&input, &expected_eval).unwrap();
assert_eq!(*output, expected_output);
}
#[test]
fn test_vector_2() {
let input = hex("5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a");
let blind_bytes = hex("64d37aed22a27f5191de1c1d69fadb899d8862b58eb4220029e036ec4c1f6706");
let expected_blinded =
hex("da27ef466870f5f15296299850aa088629945a17d1f5b7f5ff043f76b3c06418");
let expected_eval = hex("b4cbf5a4f1eeda5a63ce7b77c7d23f461db3fcab0dd28e4e17cecb5c90d02c25");
let expected_output = hex("f4a74c9c592497375e796aa837e907b1a045d34306a749db9f34221f7e750cb4f2a6413a6bf6fa5e19ba6348eb673934a722a7ede2e7621306d18951e7cf2c73");
let blind = Scalar::from_canonical_bytes(blind_bytes.as_slice().try_into().unwrap())
.into_option()
.unwrap();
let t = hash_to_group(&input).unwrap();
let blinded = blind * t;
assert_eq!(blinded.compress().to_bytes().to_vec(), expected_blinded);
let sk = hex(SK_SM);
let eval = Ristretto255Oprf::server_evaluate(&sk, &expected_blinded).unwrap();
assert_eq!(eval, expected_eval);
let state = Ristretto255OprfClientState {
blind: blind_bytes.as_slice().try_into().unwrap(),
};
let output = state.finalize(&input, &expected_eval).unwrap();
assert_eq!(*output, expected_output);
}
#[test]
fn roundtrip() {
use rand_core::{OsRng, UnwrapErr};
let sk = Ristretto255Oprf::derive_key(&hex(SEED), &hex(KEY_INFO)).unwrap();
let password = b"hunter2";
let (state, blinded) =
Ristretto255Oprf::client_blind(password, &mut UnwrapErr(OsRng)).unwrap();
let eval = Ristretto255Oprf::server_evaluate(&sk, &blinded).unwrap();
let output = state.finalize(password, &eval).unwrap();
assert_eq!(output.len(), 64);
let (state2, blinded2) =
Ristretto255Oprf::client_blind(password, &mut UnwrapErr(OsRng)).unwrap();
let eval2 = Ristretto255Oprf::server_evaluate(&sk, &blinded2).unwrap();
let output2 = state2.finalize(password, &eval2).unwrap();
assert_eq!(output, output2);
}
#[test]
fn invalid_key_length() {
assert!(Ristretto255Oprf::server_evaluate(&[0u8; 16], &[0u8; 32]).is_err());
}
#[test]
fn invalid_blinded_element() {
let sk = hex(SK_SM);
assert!(Ristretto255Oprf::server_evaluate(&sk, &[0u8; 16]).is_err());
}
#[test]
fn server_evaluate_rejects_identity() {
let sk = hex(SK_SM);
let identity = [0u8; 32];
assert!(Ristretto255Oprf::server_evaluate(&sk, &identity).is_err());
}
#[test]
fn finalize_rejects_zero_blind() {
let state = Ristretto255OprfClientState { blind: [0u8; 32] };
let eval = hex("7ec6578ae5120958eb2db1745758ff379e77cb64fe77b0b2d8cc917ea0869c7e");
assert!(state.finalize(&[0x00], &eval).is_err());
}
#[test]
fn finalize_rejects_identity_evaluation() {
let mut rng = rand_core::UnwrapErr(rand_core::OsRng);
let (state, _) = Ristretto255Oprf::client_blind(b"password", &mut rng).unwrap();
assert!(state.finalize(b"password", &[0u8; 32]).is_err());
}
#[test]
fn hash_to_scalar_not_zero() {
let s = hash_to_scalar(&[b"test input"]).unwrap();
assert_ne!(s, Scalar::ZERO);
}
#[test]
fn server_evaluate_rejects_zero_key() {
let blinded = hex("609a0ae68c15a3cf6903766461307e5c8bb2f95e7e6550e1ffa2dc99e412803c");
assert!(Ristretto255Oprf::server_evaluate(&[0u8; 32], &blinded).is_err());
}
#[test]
fn different_passwords_different_outputs() {
use rand_core::{OsRng, UnwrapErr};
let sk = Ristretto255Oprf::derive_key(&hex(SEED), &hex(KEY_INFO)).unwrap();
let (state_a, blinded_a) =
Ristretto255Oprf::client_blind(b"password-A", &mut UnwrapErr(OsRng)).unwrap();
let eval_a = Ristretto255Oprf::server_evaluate(&sk, &blinded_a).unwrap();
let output_a = state_a.finalize(b"password-A", &eval_a).unwrap();
let (state_b, blinded_b) =
Ristretto255Oprf::client_blind(b"password-B", &mut UnwrapErr(OsRng)).unwrap();
let eval_b = Ristretto255Oprf::server_evaluate(&sk, &blinded_b).unwrap();
let output_b = state_b.finalize(b"password-B", &eval_b).unwrap();
assert_ne!(output_a, output_b);
}
#[test]
fn empty_password_roundtrip() {
use rand_core::{OsRng, UnwrapErr};
let sk = Ristretto255Oprf::derive_key(&hex(SEED), &hex(KEY_INFO)).unwrap();
let (state, blinded) = Ristretto255Oprf::client_blind(b"", &mut UnwrapErr(OsRng)).unwrap();
let eval = Ristretto255Oprf::server_evaluate(&sk, &blinded).unwrap();
let output = state.finalize(b"", &eval).unwrap();
assert_eq!(output.len(), 64);
let (state2, blinded2) =
Ristretto255Oprf::client_blind(b"", &mut UnwrapErr(OsRng)).unwrap();
let eval2 = Ristretto255Oprf::server_evaluate(&sk, &blinded2).unwrap();
let output2 = state2.finalize(b"", &eval2).unwrap();
assert_eq!(output, output2);
}
#[test]
fn different_keys_different_outputs() {
use rand_core::{OsRng, UnwrapErr};
let sk1 = Ristretto255Oprf::derive_key(&hex(SEED), b"key1").unwrap();
let sk2 = Ristretto255Oprf::derive_key(&hex(SEED), b"key2").unwrap();
let (state1, blinded1) =
Ristretto255Oprf::client_blind(b"password", &mut UnwrapErr(OsRng)).unwrap();
let eval1 = Ristretto255Oprf::server_evaluate(&sk1, &blinded1).unwrap();
let output1 = state1.finalize(b"password", &eval1).unwrap();
let (state2, blinded2) =
Ristretto255Oprf::client_blind(b"password", &mut UnwrapErr(OsRng)).unwrap();
let eval2 = Ristretto255Oprf::server_evaluate(&sk2, &blinded2).unwrap();
let output2 = state2.finalize(b"password", &eval2).unwrap();
assert_ne!(output1, output2);
}
#[test]
fn invalid_evaluation_element_length() {
let state = Ristretto255OprfClientState {
blind: hex("64d37aed22a27f5191de1c1d69fadb899d8862b58eb4220029e036ec4c1f6706")
.try_into()
.unwrap(),
};
assert!(state.finalize(b"test", &[0u8; 16]).is_err());
assert!(state.finalize(b"test", &[0u8; 64]).is_err());
}
#[test]
fn zeroize_clears_all_secret_fields() {
let mut state = Ristretto255OprfClientState { blind: [0xAA; 32] };
state.zeroize();
assert_eq!(state.blind, [0u8; 32]);
}
#[test]
fn invalid_evaluation_element_not_on_curve() {
let state = Ristretto255OprfClientState {
blind: hex("64d37aed22a27f5191de1c1d69fadb899d8862b58eb4220029e036ec4c1f6706")
.try_into()
.unwrap(),
};
assert!(state.finalize(b"test", &[0xFF; 32]).is_err());
}
}