MVP transcript hashing. Blake3 + curve25519-dalek + rand_core.
This commit is contained in:
parent
82d632137a
commit
9c41ab5e1d
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
Cargo.lock
|
|
@ -0,0 +1,33 @@
|
|||
[package]
|
||||
name = "transcript-hashing"
|
||||
version = "0.1.0"
|
||||
authors = ["Wild Developers"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
||||
[features]
|
||||
default = ["blake3"]
|
||||
|
||||
[dependencies.digest]
|
||||
version = "0.9.0"
|
||||
default_features = false
|
||||
|
||||
[dependencies.blake3]
|
||||
version = "0.3.7"
|
||||
optional = true
|
||||
default_features = false
|
||||
|
||||
[dependencies.rand_core]
|
||||
version = "0.6.1"
|
||||
optional = true
|
||||
|
||||
[dependencies.curve25519-dalek]
|
||||
version = "3.0.2"
|
||||
optional = true
|
||||
|
||||
[dev-dependencies.rand]
|
||||
version = "0.8.3"
|
||||
|
||||
[dev-dependencies.transcript-hashing]
|
||||
path = "."
|
||||
features = ["blake3"]
|
|
@ -1,5 +1,7 @@
|
|||
|
||||
= A Wild project
|
||||
= Transcript hashing
|
||||
|
||||
A simpler alternative to https://docs.rs/merlin
|
||||
|
||||
== Licenses
|
||||
|
||||
|
|
|
@ -0,0 +1,317 @@
|
|||
#![no_std]
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
//! Transcript hashing is frequently used to automate the Fiat-Shamir transform
|
||||
//! when implementing zero-knowledge proofs.
|
||||
//!
|
||||
//! FIXME: compare against https://docs.rs/merlin
|
||||
//! - `impl Message` vs. `trait MyTranscript + impl MyTranscript for merlin::Transcript`
|
||||
//! - merlin::Transcript contexts don't terminate with zeros or embed length tags, thus
|
||||
//! require any divergent paths to use distinct contexts. I.e. one cannot be a prefix
|
||||
//! of another.
|
||||
//! - STROBE/keccak-f vs. Blake3 (or any XOF)
|
||||
//! - clone vs. fork -> forced witness divergence
|
||||
//! - `merlin::TranscriptRngBuilder` is annoying to work with. `Transcript`
|
||||
//! records all append/challenge/witness/random calls and absorbs all prior inputs,
|
||||
//! not just those prior to invoking the CSPRNG.
|
||||
|
||||
use digest::XofReader;
|
||||
|
||||
/// A shorthand for `Digest + ExtendableOutput + Clone`.
|
||||
// pub trait Digest = digest::Digest + digest::ExtendableOutput + Clone;
|
||||
pub trait Digest: digest::Digest + digest::ExtendableOutput + Clone {}
|
||||
|
||||
impl<D> Digest for D where D: digest::Digest + digest::ExtendableOutput + Clone {}
|
||||
|
||||
pub trait ToCanonical {
|
||||
/// A unique name for the data type. This name MUST NOT contain a
|
||||
/// zero-byte.
|
||||
///
|
||||
/// ```rust
|
||||
/// const NAME: &'static [u8] = concat!(module_path!(), "::MyType").as_bytes();
|
||||
/// ```
|
||||
const NAME: &'static [u8];
|
||||
|
||||
type Canonical: AsRef<[u8]>;
|
||||
|
||||
/// Canonically encode `Self` as an array of bytes.
|
||||
fn to_canonical(&self) -> Self::Canonical;
|
||||
}
|
||||
|
||||
pub trait FromUniform: ToCanonical {
|
||||
/// A hack until `impl Default for [T; N]`.
|
||||
const ZERO: Self::Uniform;
|
||||
|
||||
type Uniform: AsMut<[u8]>;
|
||||
|
||||
/// Construct `Self` from an array of uniform bytes.
|
||||
fn from_uniform(bytes: Self::Uniform) -> Self;
|
||||
}
|
||||
|
||||
pub struct Transcript<D> {
|
||||
public: D,
|
||||
secret: D,
|
||||
dirty: bool,
|
||||
}
|
||||
|
||||
#[cfg(feature = "blake3")]
|
||||
pub type Blake3Transcript = Transcript<blake3::Hasher>;
|
||||
|
||||
impl<D> Transcript<D>
|
||||
where
|
||||
D: Digest,
|
||||
{
|
||||
/// Create a new transcript with a domain separator and a cryptographically
|
||||
/// secure pseudorandom number generator.
|
||||
#[cfg(feature = "rand_core")]
|
||||
pub fn new(context: &[u8], rng: &mut (impl rand_core::CryptoRng + rand_core::RngCore)) -> Self {
|
||||
let mut ts = Self::new_deterministic(context);
|
||||
ts.witness_rng(rng);
|
||||
ts
|
||||
}
|
||||
|
||||
/// Create a new deterministic transcript with a domain separator. This is
|
||||
/// intended primarily for testing.
|
||||
///
|
||||
/// Warning: You should probably use `Transcript::new`!
|
||||
pub fn new_deterministic(context: &[u8]) -> Self {
|
||||
let digest = D::new()
|
||||
.chain(concat!(module_path!(), "::Transcript::new_deterministic"))
|
||||
.chain(context);
|
||||
Self {
|
||||
public: digest.clone().chain(b"public"),
|
||||
secret: digest.chain(b"secret"),
|
||||
dirty: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Append a public (or shared secret) message (and return the message for
|
||||
/// convenience.)
|
||||
pub fn append<T: ToCanonical>(&mut self, msg: T) -> T {
|
||||
debug_assert!(!T::NAME.contains(&0u8));
|
||||
self.public.update(b"append");
|
||||
self.public.update(T::NAME);
|
||||
self.public.update(b"\0");
|
||||
self.public.update(msg.to_canonical());
|
||||
self.dirty = true;
|
||||
msg
|
||||
}
|
||||
|
||||
/// Challenge a message from the "verifier".
|
||||
///
|
||||
/// The `name` argument is used to identify the /intent/ of the challenge
|
||||
/// to force divergence if challenges are requested in inconsistent order.
|
||||
pub fn challenge<T: FromUniform>(&mut self, name: &[u8]) -> T {
|
||||
debug_assert!(!T::NAME.contains(&0u8));
|
||||
self.public.update(b"challenge");
|
||||
self.public.update(T::NAME);
|
||||
self.public.update(name);
|
||||
self.public.update(b"\0");
|
||||
let mut msg = T::ZERO;
|
||||
self.public.clone().finalize_xof().read(msg.as_mut());
|
||||
T::from_uniform(msg)
|
||||
}
|
||||
|
||||
/// Witness a presently secret message (and return the message for convenience.)
|
||||
pub fn witness<T: ToCanonical>(&mut self, msg: T) -> T {
|
||||
debug_assert!(!T::NAME.contains(&0u8));
|
||||
self.secret.update(b"witness");
|
||||
self.secret.update(T::NAME);
|
||||
self.secret.update(b"\0");
|
||||
self.secret.update(msg.to_canonical());
|
||||
msg
|
||||
}
|
||||
|
||||
/// Witness 32 bytes from a cryptographically secure pseudorandom number generator.
|
||||
#[cfg(feature = "rand_core")]
|
||||
pub fn witness_rng(&mut self, rng: &mut (impl rand_core::CryptoRng + rand_core::RngCore)) {
|
||||
let mut bytes = [0u8; 32];
|
||||
rng.fill_bytes(&mut bytes);
|
||||
|
||||
self.secret.update(b"rng");
|
||||
self.secret.update(&bytes);
|
||||
}
|
||||
|
||||
/// Generate a pseudorandom secret value.
|
||||
pub fn random<T: FromUniform>(&mut self) -> T {
|
||||
debug_assert!(!T::NAME.contains(&0u8));
|
||||
self.secret.update(b"random");
|
||||
self.secret.update(T::NAME);
|
||||
self.secret.update(b"\0");
|
||||
if self.dirty {
|
||||
self.secret
|
||||
.update(&self.public.clone().chain(b"random").finalize());
|
||||
self.dirty = false;
|
||||
}
|
||||
let mut msg = T::ZERO;
|
||||
self.secret.clone().finalize_xof().read(msg.as_mut());
|
||||
T::from_uniform(msg)
|
||||
}
|
||||
|
||||
/// Explicit transcript forking. We can't use `Clone` because each branch
|
||||
/// must witness unique paths through the tree. Forking does not influence
|
||||
/// the public transcript.
|
||||
pub fn fork(&mut self) -> Self {
|
||||
let right = Self {
|
||||
secret: self.secret.clone().chain(b"fork-right"),
|
||||
public: self.public.clone(),
|
||||
dirty: self.dirty,
|
||||
};
|
||||
self.secret.update(b"fork-left");
|
||||
right
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ToCanonical> ToCanonical for &'_ T {
|
||||
const NAME: &'static [u8] = T::NAME;
|
||||
|
||||
type Canonical = T::Canonical;
|
||||
|
||||
fn to_canonical(&self) -> Self::Canonical {
|
||||
T::to_canonical(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToCanonical for bool {
|
||||
const NAME: &'static [u8] = b"core::bool";
|
||||
|
||||
type Canonical = [u8; 1];
|
||||
|
||||
fn to_canonical(&self) -> Self::Canonical {
|
||||
[*self as u8]
|
||||
}
|
||||
}
|
||||
|
||||
impl FromUniform for bool {
|
||||
const ZERO: Self::Uniform = [0];
|
||||
|
||||
type Uniform = [u8; 1];
|
||||
|
||||
fn from_uniform(b: Self::Uniform) -> Self {
|
||||
b[0] == 1
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impls {
|
||||
($($ty:ty)*) => {$(
|
||||
impl ToCanonical for $ty {
|
||||
const NAME: &'static [u8] = concat!("core::", stringify!($ty)).as_bytes();
|
||||
|
||||
type Canonical = [u8; core::mem::size_of::<$ty>()];
|
||||
|
||||
fn to_canonical(&self) -> Self::Canonical {
|
||||
self.to_le_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromUniform for $ty {
|
||||
const ZERO: Self::Uniform = [0u8; core::mem::size_of::<$ty>()];
|
||||
|
||||
type Uniform = [u8; core::mem::size_of::<$ty>()];
|
||||
|
||||
fn from_uniform(bytes: Self::Uniform) -> Self {
|
||||
Self::from_le_bytes(bytes)
|
||||
}
|
||||
}
|
||||
)*}
|
||||
}
|
||||
|
||||
impls!(
|
||||
u8 u16 u32 u64
|
||||
i8 i16 i32 i64
|
||||
);
|
||||
|
||||
impl<const N: usize> ToCanonical for [u8; N] {
|
||||
const NAME: &'static [u8] = b"core::array::<u8>";
|
||||
|
||||
type Canonical = [u8; N];
|
||||
|
||||
fn to_canonical(&self) -> Self::Canonical {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> FromUniform for [u8; N] {
|
||||
const ZERO: Self::Uniform = [0u8; N];
|
||||
|
||||
type Uniform = [u8; N];
|
||||
|
||||
fn from_uniform(bytes: Self::Uniform) -> Self {
|
||||
bytes
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: Commit to the length of the slice. Alternatively impl Digest for
|
||||
// Transcript or a wrapper, to absorb untyped blobs.
|
||||
impl<'a> ToCanonical for &'a [u8] {
|
||||
const NAME: &'static [u8] = b"core::slice::<u8>";
|
||||
|
||||
type Canonical = &'a [u8];
|
||||
|
||||
fn to_canonical(&self) -> Self::Canonical {
|
||||
&self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "curve25519-dalek")]
|
||||
mod dalek {
|
||||
use crate::{FromUniform, ToCanonical};
|
||||
|
||||
use curve25519_dalek::{
|
||||
ristretto::{CompressedRistretto, RistrettoPoint},
|
||||
scalar::Scalar,
|
||||
};
|
||||
|
||||
impl ToCanonical for Scalar {
|
||||
const NAME: &'static [u8] = b"curve25519_dalek::scalar::Scalar";
|
||||
|
||||
type Canonical = [u8; 32];
|
||||
|
||||
fn to_canonical(&self) -> Self::Canonical {
|
||||
self.to_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromUniform for Scalar {
|
||||
const ZERO: Self::Uniform = [0u8; 64];
|
||||
|
||||
type Uniform = [u8; 64];
|
||||
|
||||
fn from_uniform(bytes: Self::Uniform) -> Self {
|
||||
Self::from_bytes_mod_order_wide(&bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToCanonical for CompressedRistretto {
|
||||
const NAME: &'static [u8] = b"curve25519_dalek::ristretto::RistrettoPoint";
|
||||
|
||||
type Canonical = [u8; 32];
|
||||
|
||||
fn to_canonical(&self) -> Self::Canonical {
|
||||
self.to_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToCanonical for RistrettoPoint {
|
||||
const NAME: &'static [u8] = b"curve25519_dalek::ristretto::RistrettoPoint";
|
||||
|
||||
type Canonical = [u8; 32];
|
||||
|
||||
/// This compresses the point. If you have already compressed the point,
|
||||
/// you may append or witness it directly.
|
||||
fn to_canonical(&self) -> Self::Canonical {
|
||||
self.compress().to_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromUniform for RistrettoPoint {
|
||||
const ZERO: Self::Uniform = [0u8; 64];
|
||||
|
||||
type Uniform = [u8; 64];
|
||||
|
||||
fn from_uniform(bytes: Self::Uniform) -> Self {
|
||||
Self::from_uniform_bytes(&bytes)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
#![cfg(feature = "curve25519-dalek")]
|
||||
#![forbid(unsafe_code)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use transcript_hashing::Blake3Transcript;
|
||||
|
||||
#[test]
|
||||
fn check() {
|
||||
use curve25519_dalek::{ristretto::RistrettoPoint, scalar::Scalar};
|
||||
|
||||
let mut ts = Blake3Transcript::new_deterministic(b"Ristretto is awesome!");
|
||||
|
||||
if ts.append(1 == 1) {
|
||||
let _chal: RistrettoPoint = ts.challenge(b"chal");
|
||||
let _nonce: Scalar = ts.random();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
#![forbid(unsafe_code)]
|
||||
|
||||
use transcript_hashing::Blake3Transcript;
|
||||
|
||||
#[test]
|
||||
fn witness_appends() {
|
||||
let mut ts1 = Blake3Transcript::new_deterministic(b"witness appends");
|
||||
let mut ts2 = Blake3Transcript::new_deterministic(b"witness appends");
|
||||
|
||||
assert_eq!(ts1.challenge::<u64>(b"a"), ts2.challenge::<u64>(b"a"));
|
||||
assert_eq!(ts1.random::<u64>(), ts2.random::<u64>());
|
||||
|
||||
// Appending to one transcript but not the other will diverge.
|
||||
ts1.append(b"Hello world".as_ref());
|
||||
|
||||
assert_ne!(ts1.challenge::<u64>(b"b"), ts2.challenge::<u64>(b"b"));
|
||||
assert_ne!(ts1.random::<u64>(), ts2.random::<u64>());
|
||||
|
||||
// Appending the same message to the other will not converge if
|
||||
// interleaved with challenges.
|
||||
ts2.append(b"Hello world".as_ref());
|
||||
|
||||
assert_ne!(ts1.challenge::<u64>(b"c"), ts2.challenge::<u64>(b"c"));
|
||||
assert_ne!(ts1.random::<u64>(), ts2.random::<u64>());
|
||||
}
|
Loading…
Reference in New Issue