MVP transcript hashing. Blake3 + curve25519-dalek + rand_core.

This commit is contained in:
James McGlashan 2021-02-07 08:36:06 +00:00
parent 82d632137a
commit 9c41ab5e1d
Signed by untrusted user who does not match committer: wildfox
GPG Key ID: C2062B23665983D5
6 changed files with 397 additions and 1 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
Cargo.lock

33
Cargo.toml Normal file
View File

@ -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"]

View File

@ -1,5 +1,7 @@
= A Wild project
= Transcript hashing
A simpler alternative to https://docs.rs/merlin
== Licenses

317
src/lib.rs Normal file
View File

@ -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)
}
}
}

17
tests/ristretto.rs Normal file
View File

@ -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();
}
}

25
tests/transcript.rs Normal file
View File

@ -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>());
}