diff --git a/Cargo.lock b/Cargo.lock index 7ed0410..679838a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -729,7 +729,7 @@ dependencies = [ [[package]] name = "generic-ec-core" -version = "0.2.2" +version = "0.2.3" dependencies = [ "generic-array", "rand_core", @@ -740,7 +740,7 @@ dependencies = [ [[package]] name = "generic-ec-curves" -version = "0.2.3" +version = "0.2.4" dependencies = [ "criterion", "curve25519-dalek", @@ -764,7 +764,9 @@ version = "0.1.0" dependencies = [ "anyhow", "criterion", + "digest", "generic-ec", + "generic-ec-zkp", "generic-tests", "hex", "plotters", @@ -775,14 +777,16 @@ dependencies = [ "serde_json", "serde_test", "serde_with", + "sha2", "tabled", ] [[package]] name = "generic-ec-zkp" -version = "0.4.3" +version = "0.4.4" dependencies = [ "criterion", + "digest", "generic-array", "generic-ec", "generic-tests", diff --git a/Cargo.toml b/Cargo.toml index d358916..be11819 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ hex = { version = "0.4", default-features = false } rand = "0.8" rand_core = { version = "0.6", default-features = false } +rand_hash = { version = "0.1", default-features = false } serde = { version = "1", default-features = false } serde_json = "1" @@ -28,6 +29,7 @@ sha2 = { version = "0.10", default-features = false } subtle = { version = "2.4", default-features = false } +digest = "0.10" udigest = { version = "0.2.1", default-features = false } zeroize = { version = "1", default-features = false } diff --git a/generic-ec-zkp/CHANGELOG.md b/generic-ec-zkp/CHANGELOG.md index 03a9169..571474f 100644 --- a/generic-ec-zkp/CHANGELOG.md +++ b/generic-ec-zkp/CHANGELOG.md @@ -1,3 +1,8 @@ +## v0.4.4 +* Add `dlog_eq` proof [#54] + +[#54]: https://github.com/LFDT-Lockness/generic-ec/pull/54 + ## v0.4.3 * Fix double header menu issue on docs.rs [#49] diff --git a/generic-ec-zkp/Cargo.toml b/generic-ec-zkp/Cargo.toml index 37c67fd..efff7e9 100644 --- a/generic-ec-zkp/Cargo.toml +++ b/generic-ec-zkp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "generic-ec-zkp" -version = "0.4.3" +version = "0.4.4" edition = "2021" license = "MIT OR Apache-2.0" repository = "https://github.com/LFDT-Lockness/generic-ec" @@ -13,6 +13,7 @@ keywords = ["elliptic-curves", "zk-proof"] [dependencies] generic-ec = { version = "0.4.0", path = "../generic-ec", default-features = false } +digest = { workspace = true, optional = true } udigest = { workspace = true, features = ["derive"], optional = true } subtle.workspace = true @@ -39,7 +40,7 @@ default = ["std"] std = ["alloc"] alloc = ["udigest?/alloc", "serde?/alloc"] serde = ["dep:serde", "generic-ec/serde", "generic-array/serde"] -udigest = ["dep:udigest", "generic-ec/udigest"] +udigest = ["dep:udigest", "dep:digest", "generic-ec/udigest", "generic-ec/hash-to-scalar"] [package.metadata.docs.rs] all-features = true diff --git a/generic-ec-zkp/src/dlog_eq.rs b/generic-ec-zkp/src/dlog_eq.rs new file mode 100644 index 0000000..165f62e --- /dev/null +++ b/generic-ec-zkp/src/dlog_eq.rs @@ -0,0 +1,311 @@ +//! Knowledge of equality of two discrete logarithms $\Pi^\text{dlog_eq}$ +//! +//! This is a protocol that lets the prover $\P$ convince the +//! verifier $\V$ that it knows the discrete logarithms of two values, and that +//! those logarithms are equal. $\P$ knows the secret data $x$ - the discrete +//! logarithm. $\P$ and $\V$ share common data: +//! - $G$, $H$ - some generators of the elliptic curve +//! - $X = x \cdot G$ +//! - $Z = x \cdot H$ +//! +//! Thus $log_G X = log_H Z = x$. Then $\P$ proves this fact with this protocol. +//! A common instantiation is when $x$ is a private key. Then $X$ is a public +//! key, a fact known to both parties, and the proof is that $H \cdot x = Z$ +//! without disclosing $x$. +//! +//! Since in this library we use additive notation, some terminology has been +//! adjusted in function arguments, for example what would be an `exponent` is +//! called a `product`. +//! +//! ## Non-interactive example +//! +//! ```rust +//! # use generic_ec::{Curve, Scalar, SecretScalar, Point}; +//! # use generic_ec_zkp::dlog_eq::non_interactive; +//! # use rand::rngs::OsRng; +//! # use generic_ec::curves::Secp256k1 as E; +//! # fn send(_: T) {} +//! +//! // `x` is a secret discrete logarithm only known to prover +//! let x = SecretScalar::::random(&mut OsRng); +//! +//! // `X` is a public point corresponding to `x` +//! let X = Point::generator() * &x; +//! // `H` is some point known to both parties +//! let H = Point::generator() * Scalar::random(&mut OsRng); +//! // `Z` is the object of the proof +//! let Z = H * &x; +//! +//! let data = non_interactive::Data::from_secret_key(&x, H); +//! assert_eq!(Z, data.prod2); +//! +//! // Prover proves in zero knowledge the equality of `H * x = Z` +//! let proof = non_interactive::prove::(&mut OsRng, &"shared_state", &x, data); +//! // The proof is sent to the Verifier +//! send(proof); +//! +//! // Verifier checks that the proof is correct +//! non_interactive::verify::(&"shared_state", data, proof) +//! .expect("Verification failed!"); +//! ``` +//! +//! ## Interactive example +//! +//! ```rust +//! # use generic_ec::{Curve, Scalar, SecretScalar, Point}; +//! # use generic_ec_zkp::dlog_eq::interactive; +//! # use rand::rngs::OsRng; +//! # use generic_ec::curves::Secp256k1 as E; +//! # fn send(_: T) {} +//! +//! // `x` is a secret discrete logarithm only known to prover +//! let x = SecretScalar::::random(&mut OsRng); +//! +//! // `X` is a public point corresponding to `x` +//! let X = Point::generator() * &x; +//! // `H` is some point known to both parties +//! let H = Point::generator() * Scalar::random(&mut OsRng); +//! // `Z` is the object of the proof +//! let Z = H * &x; +//! +//! let data = interactive::Data::from_secret_key(&x, H); +//! assert_eq!(Z, data.prod2); +//! +//! // Prover commits to the data +//! let (commitment, private_commitment) = interactive::commit_data(&mut OsRng, data); +//! // Prover sends this commitment to the verifier +//! send(commitment); +//! +//! // Verifier sends a challenge to the prover +//! let challenge = Scalar::random(&mut OsRng); +//! send(challenge); +//! +//! // Prover receives the challenge and computes the proof of equality `H * x = Z` +//! let proof = interactive::prove(private_commitment, challenge, &x); +//! // This proof is sent to the verifier +//! send(proof); +//! +//! // Verifier checks that the proof is correct +//! interactive::verify(data, commitment, challenge, proof) +//! .expect("Verification failed!"); +//! ``` + +use generic_ec::{Curve, Point}; + +/// Object of the proof: `log_gen1 prod1 == log_gen2 prod2` +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "udigest", derive(udigest::Digestable), udigest(bound = ""))] +pub struct Data { + /// `G`, a generator, in multiplicative notation the base for one logarithm + pub gen1: Point, + /// `X`, a point `G * x`, in multiplicative notation the value inside one logarithm + pub prod1: Point, + /// `H`, a generator, in multiplicative notation the base for the other logarithm + pub gen2: Point, + /// `Z`, a point `H * x`, in multiplicative notation the value inside the other logarithm + pub prod2: Point, +} + +impl Data { + /// Create the data for the common case where `G` is the principal group + /// generator, and `x` is a secret key + /// + /// In this case, we set `H = gen` as gen2 and `Z = G * x` as prod2 + pub fn from_secret_key(x: &generic_ec::SecretScalar, gen: Point) -> Data { + Self { + gen1: Point::generator().into(), + prod1: Point::generator() * x, + gen2: gen, + prod2: gen * x, + } + } +} + +/// Functions for interactive protocol for the proof. See usage example in +/// [`dlog_eq`](crate::dlog_eq) +/// +/// Based on "Zero-Knowledge Proofs Notes", 2024 by Jorge L. Villar +pub mod interactive { + use generic_ec::{Curve, Point, Scalar, SecretScalar}; + + /// Commitment to `H`, sent in the first round + pub type Commitment = (Point, Point); + /// Private part of commitment, the nonce `r`. Kept by the Prover + pub type PrivateCommitment = Scalar; + /// Challenge, sent to the Prover in the first round + pub type Challenge = Scalar; + /// Proof data, sent by prover to the verifier + pub type Proof = Scalar; + pub use super::Data; + pub use super::InvalidProof; + + /// First round of the protocol: commit to `H` by itself. Produces + /// [`Commitment`] to be sent + /// + /// `rng` is used to generate the nonce for the private commitment + pub fn commit( + rng: &mut (impl rand_core::RngCore + rand_core::CryptoRng), + gen1: Point, + gen2: Point, + ) -> (Commitment, PrivateCommitment) { + let r = Scalar::random(rng); + let comm = (gen1 * r, gen2 * r); + (comm, r) + } + /// First round of the protocol: commit to well-constructed [`Data`] by + /// itself. Produces [`Commitment`] to be sent + /// + /// `rng` is used to generate the nonce for the private commitment + pub fn commit_data( + rng: &mut (impl rand_core::RngCore + rand_core::CryptoRng), + data: Data, + ) -> (Commitment, PrivateCommitment) { + commit(rng, data.gen1, data.gen2) + } + + /// First round of the protocol: verifier generates a challenge. A challenge + /// is simply a random scalar + pub fn challenge( + rng: &mut (impl rand_core::RngCore + rand_core::CryptoRng), + ) -> Challenge { + Scalar::random(rng) + } + + /// Second round of the protocol: produce the proof for [`Data`] with the + /// given private commitment, public part of which was communicated to the + /// verifier + /// + /// - `r` - private commitment, the same as given by [`commit`] or [`commit_data`] + /// - `challenge` - challenge sent by the Verifier + /// - `x` - secret value of discrete logarithm + pub fn prove( + r: PrivateCommitment, + challenge: Scalar, + x: &SecretScalar, + ) -> Proof { + r + challenge * x + } + + /// Verify the validity of the ZK proof + /// + /// - `data` - data for which the proof was computed + /// - `comm` - commitment sent by the Prover in the first round + /// - `challenge` - the same challenge as was sent by this verifier in the + /// first round + /// - `proof` - produced by the Prover in the second round for this data, + /// commitment and challenge + pub fn verify( + data: Data, + comm: Commitment, + challenge: Scalar, + proof: Proof, + ) -> Result<(), InvalidProof> { + let (a1, a2) = comm; + // equation 1 + let lhs = data.gen1 * proof; + let rhs = a1 + data.prod1 * challenge; + if lhs != rhs { + return Err(InvalidProof); + } + // equation 2 + let lhs = data.gen2 * proof; + let rhs = a2 + data.prod2 * challenge; + if lhs != rhs { + return Err(InvalidProof); + } + + Ok(()) + } +} + +/// Functions for non interactive variant of the proof. See usage example in +/// [`dlog_eq`](crate::dlog_eq) +/// +/// Compared to naive protocol produced by Fiat-Shamir heuristic, this is +/// optimized to send less data in the proof. It's based on `PrEq` functionality +/// in this paper: +#[cfg(feature = "udigest")] +pub mod non_interactive { + use generic_ec::{Curve, Scalar, SecretScalar}; + + const TAG: &str = "generic-ec-zkp.dlog_eq.non_interactive"; + + pub use super::Data; + pub use super::InvalidProof; + + /// Proof data, sent by prover to the verifier + #[derive(Debug, Clone, Copy)] + #[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(bound = "") + )] + pub struct Proof { + /// Deterministic challenge + pub ch: generic_ec::Scalar, + /// Proof itself + pub res: generic_ec::Scalar, + } + + /// Create a proof deterministically for given data + /// + /// - `shared_state` - shared data not known to this proof, used to protect + /// from replay attacks + /// - `x` - the secret value of discrete logarithm + /// - `data` - data to compute the proof for + /// - `rng` - used to generate a random nonce for proof + pub fn prove( + rng: &mut (impl rand_core::RngCore + rand_core::CryptoRng), + shared_state: &impl udigest::Digestable, + x: &SecretScalar, + data: Data, + ) -> Proof { + let r = Scalar::random(rng); + let com1 = data.gen1 * r; + let com2 = data.gen2 * r; + + let seed = udigest::inline_struct!(TAG { + shared_state, + data, + com1, + com2, + }); + let ch = Scalar::from_hash::(&seed); + + let res = r + x * ch; + Proof { ch, res } + } + + /// Verify the validity of the ZK proof + /// + /// - `shared_state` - shared data not known to this proof, used to protect + /// from replay attacks + /// - `data` - data for which the proof was computed + /// - `proof` - produced by the Prover in the second round for this data and + /// shared_state + pub fn verify( + shared_state: &impl udigest::Digestable, + data: Data, + proof: Proof, + ) -> Result<(), InvalidProof> { + let com1 = data.gen1 * proof.res - data.prod1 * proof.ch; + let com2 = data.gen2 * proof.res - data.prod2 * proof.ch; + + let seed = udigest::inline_struct!(TAG { + shared_state, + data, + com1, + com2, + }); + let ch = Scalar::from_hash::(&seed); + + if ch != proof.ch { + Err(InvalidProof) + } else { + Ok(()) + } + } +} + +#[derive(Debug)] +pub struct InvalidProof; diff --git a/generic-ec-zkp/src/lib.rs b/generic-ec-zkp/src/lib.rs index 9f26498..5f9cf31 100644 --- a/generic-ec-zkp/src/lib.rs +++ b/generic-ec-zkp/src/lib.rs @@ -13,5 +13,6 @@ extern crate alloc; // We don't want this dependency to trigger unused dep lint use generic_array as _; +pub mod dlog_eq; pub mod polynomial; pub mod schnorr_pok; diff --git a/generic-ec-zkp/src/schnorr_pok.rs b/generic-ec-zkp/src/schnorr_pok.rs index e2f1a54..df2f668 100644 --- a/generic-ec-zkp/src/schnorr_pok.rs +++ b/generic-ec-zkp/src/schnorr_pok.rs @@ -6,7 +6,7 @@ //! ## Example //! //! 0. $\P$ knows a secret $x$ and wants to prove its knowledge. -//! ```rust +//! ```rust,no_run //! # use generic_ec::{Curve, SecretScalar, Point}; //! # use rand::rngs::OsRng; //! # fn doc_fn() { @@ -15,7 +15,7 @@ //! # } //! ``` //! 1. $\P$ generates and commits an ephemeral secret. Committed secret is sent to $\V$. -//! ```rust +//! ```rust,no_run //! # use generic_ec::Curve; //! # use generic_ec_zkp::schnorr_pok::*; //! # use rand::rngs::OsRng; @@ -26,7 +26,7 @@ //! # fn send(_: T) {} //! ``` //! 2. $\V$ receives commitment, and responds with challenge. -//! ```rust +//! ```rust,no_run //! # use generic_ec::Curve; //! # use generic_ec_zkp::schnorr_pok::*; //! # use rand::rngs::OsRng; @@ -39,7 +39,7 @@ //! # fn receive() -> T { unimplemented!() } //! ``` //! 3. $\P$ receives a challenge and responds with proof. -//! ```rust +//! ```rust,no_run //! # use generic_ec::{Curve, SecretScalar}; //! # use generic_ec_zkp::schnorr_pok::*; //! # use rand::rngs::OsRng; @@ -54,7 +54,7 @@ //! # fn recall() -> T { unimplemented!() } //! ``` //! 4. $\V$ receives a proof and verifies it. -//! ```rust +//! ```rust,no_run //! # use generic_ec::{Curve, Point}; //! # use generic_ec_zkp::schnorr_pok::*; //! # use rand::rngs::OsRng; diff --git a/tests/Cargo.toml b/tests/Cargo.toml index f0badeb..8a9b549 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -8,12 +8,14 @@ publish = false [dependencies] generic-ec = { path = "../generic-ec", default-features = false, features = ["all-curves", "serde"] } +generic-ec-zkp = { path = "../generic-ec-zkp", default-features = false, features = ["serde", "udigest"] } plotters = "0.3" anyhow = "1" regex = "1.10" tabled = { version = "0.15", default-features = false, features = ["std"] } +digest.workspace = true serde = { workspace = true, features = ["derive"] } serde_with.workspace = true serde_test.workspace = true @@ -24,6 +26,7 @@ hex.workspace = true generic-tests.workspace = true rand_dev.workspace = true rand.workspace = true +sha2.workspace = true criterion = { workspace = true, features = ["html_reports"] } diff --git a/tests/tests/dlog_eq.rs b/tests/tests/dlog_eq.rs new file mode 100644 index 0000000..304abe4 --- /dev/null +++ b/tests/tests/dlog_eq.rs @@ -0,0 +1,91 @@ +#[generic_tests::define] +mod interactive { + use generic_ec_zkp::dlog_eq::interactive as dlog_eq; + + #[test] + fn passing_test() { + let mut rng = rand_dev::DevRng::new(); + + let secret_share = generic_ec::SecretScalar::::random(&mut rng); + + let base = generic_ec::Point::generator() * generic_ec::Scalar::random(&mut rng); + let data = dlog_eq::Data::from_secret_key(&secret_share, base); + + let (comm, pcomm) = dlog_eq::commit_data(&mut rng, data); + let challenge = dlog_eq::challenge(&mut rng); + let proof = dlog_eq::prove(pcomm, challenge, &secret_share); + dlog_eq::verify(data, comm, challenge, proof).unwrap() + } + #[test] + fn failing_test() { + let mut rng = rand_dev::DevRng::new(); + + let secret_share = generic_ec::SecretScalar::::random(&mut rng); + + let base = generic_ec::Point::generator() * generic_ec::Scalar::random(&mut rng); + let mut data = dlog_eq::Data::from_secret_key(&secret_share, base); + // Replace the exponentiation with something wrong + data.prod2 += generic_ec::Point::generator(); + + let (comm, pcomm) = dlog_eq::commit_data(&mut rng, data); + let challenge = dlog_eq::challenge(&mut rng); + let proof = dlog_eq::prove(pcomm, challenge, &secret_share); + assert!( + dlog_eq::verify(data, comm, challenge, proof).is_err(), + "proof should fail" + ) + } + + #[instantiate_tests()] + mod secp256k1 {} + #[instantiate_tests()] + mod secp256r1 {} + #[instantiate_tests()] + mod stark {} + #[instantiate_tests()] + mod ed25519 {} +} + +#[generic_tests::define] +mod non_interactive { + use generic_ec_zkp::dlog_eq::non_interactive as dlog_eq; + + #[test] + fn passing_test() { + let mut rng = rand_dev::DevRng::new(); + let shared_state = "shared state"; + + let secret_share = generic_ec::SecretScalar::random(&mut rng); + + let base = generic_ec::Point::generator() * generic_ec::Scalar::random(&mut rng); + let data = dlog_eq::Data::from_secret_key(&secret_share, base); + + let proof = dlog_eq::prove::(&mut rng, &shared_state, &secret_share, data); + dlog_eq::verify::(&shared_state, data, proof).unwrap(); + } + + #[test] + fn failing_test() { + let mut rng = rand_dev::DevRng::new(); + let shared_state = "shared state"; + + let secret_share = generic_ec::SecretScalar::random(&mut rng); + + let base = generic_ec::Point::generator() * generic_ec::Scalar::random(&mut rng); + let mut data = dlog_eq::Data::from_secret_key(&secret_share, base); + // make exp2 wrong so that proof won't hold + data.prod2 += generic_ec::Point::generator(); + + let proof = dlog_eq::prove::(&mut rng, &shared_state, &secret_share, data); + assert!(dlog_eq::verify::(&shared_state, data, proof).is_err()); + } + + #[instantiate_tests()] + mod secp256k1_sha256 {} + #[instantiate_tests()] + mod secp256r1_sha256 {} + #[instantiate_tests()] + mod stark_sha256 {} + #[instantiate_tests()] + mod ed25519_sha256 {} +}