diff --git a/.github/workflows/readme.yml b/.github/workflows/readme.yml index 1da05a3..1b2504d 100644 --- a/.github/workflows/readme.yml +++ b/.github/workflows/readme.yml @@ -19,6 +19,8 @@ jobs: crate: cargo-rdme - uses: webfactory/ssh-agent@v0.7.0 with: - ssh-private-key: ${{ secrets.STARK_CURVE_KEY }} + ssh-private-key: | + ${{ secrets.STARK_CURVE_KEY }} + ${{ secrets.UDIGEST_KEY }} - name: Check that readme matches lib.rs run: cargo rdme -w generic-ec -r README.md --check diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index c07ba1c..a403dbb 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -15,7 +15,9 @@ jobs: - uses: actions/checkout@v3 - uses: webfactory/ssh-agent@v0.7.0 with: - ssh-private-key: ${{ secrets.STARK_CURVE_KEY }} + ssh-private-key: | + ${{ secrets.STARK_CURVE_KEY }} + ${{ secrets.UDIGEST_KEY }} - uses: Swatinem/rust-cache@v2 with: cache-on-failure: "true" @@ -41,7 +43,9 @@ jobs: - uses: actions/checkout@v3 - uses: webfactory/ssh-agent@v0.7.0 with: - ssh-private-key: ${{ secrets.STARK_CURVE_KEY }} + ssh-private-key: | + ${{ secrets.STARK_CURVE_KEY }} + ${{ secrets.UDIGEST_KEY }} - uses: Swatinem/rust-cache@v2 with: cache-on-failure: "true" diff --git a/generic-ec-curves/src/rust_crypto/mod.rs b/generic-ec-curves/src/rust_crypto/mod.rs index 040e262..39302ea 100644 --- a/generic-ec-curves/src/rust_crypto/mod.rs +++ b/generic-ec-curves/src/rust_crypto/mod.rs @@ -99,8 +99,8 @@ impl PartialEq for RustCryptoCurve { impl Eq for RustCryptoCurve {} impl PartialOrd for RustCryptoCurve { - fn partial_cmp(&self, _other: &Self) -> Option { - Some(core::cmp::Ordering::Equal) + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) } } diff --git a/generic-ec-zkp/Cargo.toml b/generic-ec-zkp/Cargo.toml index 41a6a11..d9bc683 100644 --- a/generic-ec-zkp/Cargo.toml +++ b/generic-ec-zkp/Cargo.toml @@ -7,9 +7,9 @@ edition = "2021" [dependencies] generic-ec = { path = "../generic-ec", default-features = false } +udigest = { git = "https://github.com/dfns-labs/udigest", branch = "m", features = ["derive"], optional = true } subtle = "2.4" -digest = "0.10" rand_core = "0.6" serde = { version = "1", features = ["derive"], optional = true } @@ -29,5 +29,6 @@ generic-ec = { path = "../generic-ec", default-features = false, features = ["al [features] default = ["std"] std = ["alloc"] -alloc = [] +alloc = ["udigest/alloc"] serde = ["dep:serde", "generic-ec/serde", "generic-array/serde"] +udigest = ["dep:udigest", "generic-ec/udigest"] diff --git a/generic-ec-zkp/src/hash_commitment.rs b/generic-ec-zkp/src/hash_commitment.rs deleted file mode 100644 index a21a05e..0000000 --- a/generic-ec-zkp/src/hash_commitment.rs +++ /dev/null @@ -1,288 +0,0 @@ -//! Hash commitment -//! -//! Hash commitment is a procedure that allows player $\P$ to commit some value (scalar, point, byte string, etc.), -//! and reveal it later on. Player $\V$ can verify that commitment matches revealed value. -//! -//! ## Example -//! -//! 1. $\P$ commits some data (point, scalars, slices, whatever that can be represented as bytes): -//! ```rust -//! # use generic_ec::{Point, Scalar, Curve}; -//! # use generic_ec_zkp::hash_commitment::HashCommit; -//! # use sha2::Sha256; use rand::rngs::OsRng; -//! # -//! # fn doc_fn() { -//! let point: Point = some_point(); -//! let scalars: &[Scalar] = some_scalars(); -//! let arbitrary_data: &[&[u8]] = some_arbitrary_data(); -//! -//! let (commit, decommit) = HashCommit::::builder() -//! .mix(point) -//! .mix_many(scalars) -//! .mix_many_bytes(arbitrary_data) -//! .commit(&mut OsRng); -//! # } -//! # fn some_point() -> Point { unimplemented!() } -//! # fn some_scalars() -> T { unimplemented!() } -//! # fn some_arbitrary_data() -> T { unimplemented!() } -//! ``` -//! 2. $\P$ sends `commit` to $\V$ -//! 3. At some point, $\P$ chooses to reveal committed data. It sends data + `decommit` to $\V$. -//! -//! $\V$ verifies that revealed data matches `commit`: -//! -//! ```rust -//! # use generic_ec::{Point, Scalar, Curve}; -//! # use generic_ec_zkp::hash_commitment::{HashCommit, DecommitNonce, MismatchedRevealedData}; -//! # use sha2::Sha256; -//! # -//! # fn doc_fn() -> Result<(), MismatchedRevealedData> { -//! # let commit: HashCommit = unimplemented!(); -//! # let (point, scalars, arbitrary_data): (Point, &[Scalar], &[&[u8]]) = unimplemented!(); -//! # let decommit: DecommitNonce = unimplemented!(); -//! HashCommit::::builder() -//! .mix(point) -//! .mix_many(scalars) -//! .mix_many_bytes(arbitrary_data) -//! .verify(&commit, &decommit)?; -//! # } -//! ``` -//! -//! ## Algorithm -//! Underlying algorithm of hashing input data is based on merkle trees. - -use digest::{generic_array::GenericArray, Digest, Output}; -use rand_core::RngCore; -use subtle::ConstantTimeEq; - -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -use generic_ec::{Curve, Point, Scalar}; - -/// Builder for commitment/verification -pub struct Builder(D); - -impl Builder { - /// Creates an instance of [`Builder`] - pub fn new() -> Self { - // Initialize hash with context separation string - Self(D::new_with_prefix(b"GENERIC_EC_HASH_COMMITMENT")) - } - - /// Mixes value serialized to bytes into commitment - /// - /// You can use this method with [`Point`](Point) or [`Scalar`](Scalar). Also you can - /// implement [`EncodesToBytes`] for your own types. - /// - /// ```rust - /// use generic_ec::{Point, Scalar}; - /// use generic_ec_zkp::hash_commitment::HashCommit; - /// # use sha2::Sha256; - /// # use rand::rngs::OsRng; - /// - /// # use generic_ec::Curve; - /// # fn doc_fn() { - /// let point: Point = some_point(); - /// let scalar: Scalar = some_scalar(); - /// - /// let (commit, decommit) = HashCommit::::builder() - /// .mix(point) - /// .mix(scalar) - /// .commit(&mut OsRng); - /// # } - /// # fn some_point() -> Point { unimplemented!() } - /// # fn some_scalar() -> Scalar { unimplemented!() } - /// ``` - pub fn mix(self, encodable: T) -> Self - where - T: EncodesToBytes, - { - self.mix_bytes(encodable.to_bytes()) - } - - /// Mixes values serialized to bytes into commitment - /// - /// ```rust - /// use generic_ec::Point; - /// use generic_ec_zkp::hash_commitment::HashCommit; - /// # use sha2::Sha256; - /// # use rand::rngs::OsRng; - /// - /// # use generic_ec::Curve; - /// # fn doc_fn() { - /// let points: Vec> = some_points(); - /// - /// let (commit, decommit) = HashCommit::::builder() - /// .mix_many(&points) - /// .commit(&mut OsRng); - /// # } - /// # fn some_points() -> Vec> { unimplemented!() } - /// ``` - pub fn mix_many(self, encodables: impl IntoIterator) -> Self { - self.mix_many_bytes(encodables.into_iter().map(|i| i.to_bytes())) - } - - /// Mixes bytes into commitment - /// - /// ```rust - /// use generic_ec_zkp::hash_commitment::HashCommit; - /// # use sha2::Sha256; - /// # use rand::rngs::OsRng; - /// - /// # use generic_ec::Curve; - /// # fn doc_fn() { - /// let (commit, decommit) = HashCommit::::builder() - /// .mix_bytes(b"some message") - /// .commit(&mut OsRng); - /// # } - /// ``` - pub fn mix_bytes(self, data: impl AsRef<[u8]>) -> Self { - Self(self.0.chain_update(digest_leaf::(data))) - } - - /// Mixes list of byte strings into commitment - /// - /// ```rust - /// use generic_ec_zkp::hash_commitment::HashCommit; - /// # use sha2::Sha256; - /// # use rand::rngs::OsRng; - /// - /// # use generic_ec::Curve; - /// # fn doc_fn() { - /// let (commit, decommit) = HashCommit::::builder() - /// .mix_many_bytes(&[b"some message".as_slice(), b"another message"]) - /// .commit(&mut OsRng); - /// # } - /// ``` - pub fn mix_many_bytes(self, list: impl IntoIterator>) -> Self { - let hash = list - .into_iter() - .fold(digest_subtree::(), |d, i| { - d.chain_update(digest_leaf::(i)) - }) - .finalize(); - Self(self.0.chain_update(hash)) - } - - /// Performs commitment - /// - /// Decommitment nonce is generated from provided randomness source - pub fn commit(self, rng: &mut R) -> (HashCommit, DecommitNonce) { - let mut nonce = DecommitNonce::::default(); - rng.fill_bytes(&mut nonce.nonce); - (self.commit_with_fixed_nonce(&nonce), nonce) - } - - /// Performs commitment with specified decommitment nonce - pub fn commit_with_fixed_nonce(mut self, nonce: &DecommitNonce) -> HashCommit { - self = self.mix_bytes(&nonce.nonce); - let resulting_hash = self.0.finalize(); - HashCommit(resulting_hash) - } - - /// Verifies that provided data matches commitment and decommitment - pub fn verify( - self, - commit: &HashCommit, - nonce: &DecommitNonce, - ) -> Result<(), MismatchedRevealedData> { - let should_be = self.commit_with_fixed_nonce(nonce); - if commit.0.ct_eq(&should_be.0).into() { - Ok(()) - } else { - Err(MismatchedRevealedData) - } - } -} - -impl Default for Builder { - fn default() -> Self { - Self::new() - } -} - -/// Committed value -#[derive(Clone)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(bound = ""))] -pub struct HashCommit(pub Output); - -impl HashCommit { - pub fn builder() -> Builder { - Builder::new() - } -} - -/// Random nonce that was used to "blind" commitment -#[derive(Clone)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(bound = ""))] -pub struct DecommitNonce { - pub nonce: GenericArray, -} - -impl Default for DecommitNonce { - fn default() -> Self { - Self { - nonce: Default::default(), - } - } -} - -fn digest_leaf(bytes: impl AsRef<[u8]>) -> digest::Output { - D::new_with_prefix([0]).chain_update(bytes).finalize() -} - -fn digest_subtree() -> D { - D::new_with_prefix([1]) -} - -/// Infallibly encodable to bytes -/// -/// Used in [`hash_commitment::Builder`](Builder) methods [`mix`](Builder::mix) and [`mix_many`](Builder::mix_many) to convert given value to bytes. -pub trait EncodesToBytes { - /// Value byte representation - type Bytes: AsRef<[u8]>; - /// Encodes value to bytes - fn to_bytes(&self) -> Self::Bytes; -} - -impl EncodesToBytes for &T { - type Bytes = T::Bytes; - fn to_bytes(&self) -> Self::Bytes { - ::to_bytes(*self) - } -} - -impl EncodesToBytes for Point { - type Bytes = generic_ec::EncodedPoint; - fn to_bytes(&self) -> Self::Bytes { - self.to_bytes(true) - } -} - -impl EncodesToBytes for Scalar { - type Bytes = generic_ec::EncodedScalar; - fn to_bytes(&self) -> Self::Bytes { - self.to_be_bytes() - } -} - -impl EncodesToBytes for u16 { - type Bytes = [u8; 2]; - fn to_bytes(&self) -> Self::Bytes { - self.to_be_bytes() - } -} - -/// Error indicating that revealed data doesn't match commitment -#[derive(Debug, Clone, Copy)] -pub struct MismatchedRevealedData; - -impl core::fmt::Display for MismatchedRevealedData { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.write_str("revealed data doesn't match commitment") - } -} - -#[cfg(feature = "std")] -impl std::error::Error for MismatchedRevealedData {} diff --git a/generic-ec-zkp/src/lib.rs b/generic-ec-zkp/src/lib.rs index 443543d..0456b63 100644 --- a/generic-ec-zkp/src/lib.rs +++ b/generic-ec-zkp/src/lib.rs @@ -9,6 +9,5 @@ extern crate alloc; // We don't want this dependency to trigger unused dep lint use generic_array as _; -pub mod hash_commitment; pub mod polynomial; pub mod schnorr_pok; diff --git a/generic-ec-zkp/src/polynomial.rs b/generic-ec-zkp/src/polynomial.rs index ef879c7..4d508e7 100644 --- a/generic-ec-zkp/src/polynomial.rs +++ b/generic-ec-zkp/src/polynomial.rs @@ -18,6 +18,7 @@ mod requires_alloc { /// Polynomial is generic over type of coefficients `C`, it can be `Scalar`, `NonZero>`, `SecretScalar`, `Point`, /// or any other type that implements necessary traits. #[derive(Debug, Clone)] + #[cfg_attr(feature = "udigest", derive(udigest::Digestable))] pub struct Polynomial { /// `coefs[i]` is coefficient of `x^i` term /// diff --git a/generic-ec-zkp/src/schnorr_pok.rs b/generic-ec-zkp/src/schnorr_pok.rs index b40bf3c..e2f1a54 100644 --- a/generic-ec-zkp/src/schnorr_pok.rs +++ b/generic-ec-zkp/src/schnorr_pok.rs @@ -89,6 +89,7 @@ use serde::{Deserialize, Serialize}; /// Committed prover ephemeral secret #[derive(Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(bound = ""))] +#[cfg_attr(feature = "udigest", derive(udigest::Digestable), udigest(bound = ""))] pub struct Commit(pub Point); /// Prover ephemeral secret @@ -99,6 +100,7 @@ pub struct ProverSecret { /// Challenge generated by verifier #[derive(Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(bound = ""))] +#[cfg_attr(feature = "udigest", derive(udigest::Digestable), udigest(bound = ""))] pub struct Challenge { pub nonce: Scalar, } @@ -115,6 +117,7 @@ impl Challenge { /// The proof that can convince $\V$ that $\P$ knows secret $x$ #[derive(Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(bound = ""))] +#[cfg_attr(feature = "udigest", derive(udigest::Digestable), udigest(bound = ""))] pub struct Proof(pub Scalar); impl Proof { diff --git a/generic-ec/Cargo.toml b/generic-ec/Cargo.toml index 5bb50b8..8493359 100644 --- a/generic-ec/Cargo.toml +++ b/generic-ec/Cargo.toml @@ -13,6 +13,7 @@ categories = [] [dependencies] generic-ec-core = { path = "../generic-ec-core" } generic-ec-curves = { path = "../generic-ec-curves", optional = true } +udigest = { git = "https://github.com/dfns-labs/udigest", branch = "m", features = ["derive"], optional = true } subtle = { version = "2.4", default-features = false } rand_core = { version = "0.6", default-features = false } @@ -37,6 +38,7 @@ default = ["std", "serde"] std = ["alloc"] alloc = ["hex/alloc"] serde = ["dep:serde", "generic-ec-core/serde", "hex", "serde_with"] +udigest = ["dep:udigest"] curves = ["generic-ec-curves"] curve-secp256k1 = ["curves", "generic-ec-curves/secp256k1"] diff --git a/generic-ec/src/coords.rs b/generic-ec/src/coords.rs index c3c9c98..0ae9190 100644 --- a/generic-ec/src/coords.rs +++ b/generic-ec/src/coords.rs @@ -136,7 +136,7 @@ impl Eq for Coordinate {} impl PartialOrd for Coordinate { fn partial_cmp(&self, other: &Self) -> Option { - self.as_be_bytes().partial_cmp(other.as_be_bytes()) + Some(self.cmp(other)) } } diff --git a/generic-ec/src/non_zero/definition.rs b/generic-ec/src/non_zero/definition.rs index bde43d1..64ef90e 100644 --- a/generic-ec/src/non_zero/definition.rs +++ b/generic-ec/src/non_zero/definition.rs @@ -13,6 +13,7 @@ use zeroize::Zeroize; )), serde(into = "T", try_from = "T") )] +#[cfg_attr(feature = "udigest", derive(udigest::Digestable))] pub struct NonZero(T); impl NonZero { diff --git a/generic-ec/src/point/mod.rs b/generic-ec/src/point/mod.rs index d44e3fb..643e164 100644 --- a/generic-ec/src/point/mod.rs +++ b/generic-ec/src/point/mod.rs @@ -173,9 +173,7 @@ impl Hash for Point { impl PartialOrd for Point { fn partial_cmp(&self, other: &Self) -> Option { - self.to_bytes(true) - .as_bytes() - .partial_cmp(other.to_bytes(true).as_bytes()) + Some(self.cmp(other)) } } @@ -202,3 +200,16 @@ impl crate::traits::Zero for Point { x.ct_eq(&Self::zero()) } } + +#[cfg(feature = "udigest")] +impl udigest::Digestable for Point { + fn unambiguously_encode(&self, encoder: udigest::encoding::EncodeValue) + where + B: udigest::Buffer, + { + let mut s = encoder.encode_struct(); + s.add_field("curve").encode_leaf_value(E::CURVE_NAME); + s.add_field("point").encode_leaf_value(self.to_bytes(true)); + s.finish(); + } +} diff --git a/generic-ec/src/scalar.rs b/generic-ec/src/scalar.rs index afeecc3..4f32a5a 100644 --- a/generic-ec/src/scalar.rs +++ b/generic-ec/src/scalar.rs @@ -342,9 +342,7 @@ impl Hash for Scalar { impl PartialOrd for Scalar { fn partial_cmp(&self, other: &Self) -> Option { - self.to_be_bytes() - .as_bytes() - .partial_cmp(other.to_be_bytes().as_bytes()) + Some(self.cmp(other)) } } @@ -355,3 +353,16 @@ impl Ord for Scalar { .cmp(other.to_be_bytes().as_bytes()) } } + +#[cfg(feature = "udigest")] +impl udigest::Digestable for Scalar { + fn unambiguously_encode(&self, encoder: udigest::encoding::EncodeValue) + where + B: udigest::Buffer, + { + let mut s = encoder.encode_struct(); + s.add_field("curve").encode_leaf_value(E::CURVE_NAME); + s.add_field("scalar").encode_leaf_value(self.to_be_bytes()); + s.finish(); + } +}