From 57d5b390064bdb76b302b087013ec91032ab14ba Mon Sep 17 00:00:00 2001 From: Sergey Kaunov Date: Tue, 27 Feb 2024 23:12:36 +0300 Subject: [PATCH] Resolve #84 (#98) * current progress * current progress * current progress * current progress * current progress * semantically finished * `fmt` * that's for another issue and is more complex --- rust-arkworks/src/tests.rs | 6 +- rust-k256/Cargo.toml | 13 ++- rust-k256/src/lib.rs | 177 ++++++++++++++---------------- rust-k256/src/randomizedsigner.rs | 110 +++++++++++++++++++ rust-k256/tests/signing.rs | 64 +++++++++++ rust-k256/tests/verification.rs | 39 ++++--- 6 files changed, 289 insertions(+), 120 deletions(-) create mode 100644 rust-k256/src/randomizedsigner.rs create mode 100644 rust-k256/tests/signing.rs diff --git a/rust-arkworks/src/tests.rs b/rust-arkworks/src/tests.rs index 1f54c89..d6b7eef 100644 --- a/rust-arkworks/src/tests.rs +++ b/rust-arkworks/src/tests.rs @@ -41,8 +41,8 @@ pub fn test_k256_affine_to_arkworks_secp256k1_affine() { } fn hex_to_fr(hex: &str) -> secp256k1::fields::Fr { - let num_field_bytes = 320; - let mut sk_bytes_vec = vec![0u8; num_field_bytes]; + let num_field_bits = 320; + let mut sk_bytes_vec = vec![0u8; num_field_bits]; let mut sk_bytes = hex::decode(hex).unwrap(); sk_bytes.reverse(); @@ -245,7 +245,7 @@ pub fn test_against_zk_nullifier_sig_c_and_s() { let sig = PlumeSignature::sign_with_r(&pp, (&keypair.0, &keypair.1), message, r, PlumeVersion::V2) .unwrap(); - + assert_eq!( coord_to_hex(sig.c.into()), "00000000000000003dbfb717705010d4f44a70720c95e74b475bd3a783ab0b9e8a6b3b363434eb96" diff --git a/rust-k256/Cargo.toml b/rust-k256/Cargo.toml index 8d25ce6..5d3df2e 100644 --- a/rust-k256/Cargo.toml +++ b/rust-k256/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "plume_rustcrypto" -version = "0.1.0" +version = "0.2.0" edition = "2021" license = "MIT" description = "Implementation of PLUME: nullifier friendly signature scheme on ECDSA; using the k256 library" @@ -11,11 +11,12 @@ keywords = ["nullifier", "zero-knowledge", "ECDSA", "PLUME"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -rand_core = "0.6.3" -hash2field = "0.4.0" -num-bigint = "0.4.3" -num-integer = "0.1.45" -k256 = {version = "0.13.2", features = ["arithmetic", "hash2curve", "expose-field", "sha2"]} +rand_core = "~0.6.3" +# hash2field = "0.4.0" +num-bigint = "~0.4.3" +num-integer = "~0.1.45" +k256 = {version = "~0.13.3", features = ["arithmetic", "hash2curve", "expose-field", "sha2"]} +signature = "^2.2.0" [dev-dependencies] hex = "0.4.3" diff --git a/rust-k256/src/lib.rs b/rust-k256/src/lib.rs index 8e78c2a..533639f 100644 --- a/rust-k256/src/lib.rs +++ b/rust-k256/src/lib.rs @@ -1,73 +1,84 @@ // #![feature(generic_const_expr)] // #![allow(incomplete_features)] -//! A library for generating (coming [soon](https://github.com/plume-sig/zk-nullifier-sig/issues/84)) and verifying PLUME signatures. +//! A library for generating and verifying PLUME signatures. //! //! See for more information. //! // Find `arkworks-rs` crate as `plume_arkworks`. // -// # Examples -// For V2 just set `v1` to `None` -// ```rust -// # fn main() { -// let sig_good = PlumeSignature<'a>{ -// message: &b"An example app message string", -// pk: ProjectivePoint::GENERATOR * Scalar::from_repr(hex!("519b423d715f8b581f4fa8ee59f4771a5b44c8130b4e3eacca54a56dda72b464").into()).unwrap(), -// ... -// }; -// # } -// ``` - -use k256::{ - elliptic_curve::ops::ReduceNonZero, - elliptic_curve::{bigint::ArrayEncoding, group::ff::PrimeField}, - FieldBytes, U256, -}; // requires 'getrandom' feature +//! # Examples +//! If you want more control or to be more generic on traits `use` [`PlumeSigner`] from [`randomizedsigner`] +//! ```rust +//! use plume_rustcrypto::{PlumeSignature, SecretKey}; +//! use rand_core::OsRng; +//! # fn main() { +//! # let sk = SecretKey::random(&mut OsRng); +//! # +//! let sig_v1 = PlumeSignature::sign_v1( +//! &sk, b"ZK nullifier signature", &mut OsRng +//! ); +//! assert!(sig_v1.verify()); +//! +//! let sig_v2 = PlumeSignature::sign_v2( +//! &sk, b"ZK nullifier signature", &mut OsRng +//! ); +//! assert!(sig_v2.verify()); +//! # } +//! ``` + +use k256::elliptic_curve::bigint::ArrayEncoding; +use k256::elliptic_curve::ops::Reduce; +use k256::sha2::{digest::Output, Digest, Sha256}; // requires 'getrandom' feature +use k256::Scalar; +use k256::U256; +use signature::RandomizedSigner; // TODO pub use k256::ProjectivePoint; -/// Re-exports the [`Scalar`] type, [`Sha256`] hash function, and [`Output`] type -/// from the [`k256`] crate's [`sha2`] module. This allows them to be used -/// from the current module. -pub use k256::{ - sha2::{digest::Output, Digest, Sha256}, - Scalar, -}; -use std::panic; +/// Re-exports the `NonZeroScalar` and `SecretKey` types from the `k256` crate. +/// These are used for generating secret keys and non-zero scalars for signing. +pub use k256::{NonZeroScalar, SecretKey}; +/// Re-exports the [`CryptoRngCore`] trait from the [`rand_core`] crate. +/// This allows it to be used from the current module. +pub use rand_core::CryptoRngCore; mod utils; // not published due to use of `Projective...`; these utils can be found in other crates use utils::*; +/// Provides the [`RandomizedSigner`] trait implementation over [`PlumeSignature`]. +pub mod randomizedsigner; +use randomizedsigner::PlumeSigner; + /// The domain separation tag used for hashing to the `secp256k1` curve pub const DST: &[u8] = b"QUUX-V01-CS02-with-secp256k1_XMD:SHA-256_SSWU_RO_"; // Hash to curve algorithm /// Struct holding signature data for a PLUME signature. /// -/// `v1` field differintiate whether V1 or V2 protocol will be used. -pub struct PlumeSignature<'a> { +/// `v1specific` field differintiate whether V1 or V2 protocol will be used. +pub struct PlumeSignature { /// The message that was signed. - pub message: &'a [u8], + pub message: Vec, /// The public key used to verify the signature. - pub pk: &'a ProjectivePoint, + pub pk: ProjectivePoint, /// The nullifier. - pub nullifier: &'a ProjectivePoint, - /// Part of the signature data. - pub c: &'a [u8], + pub nullifier: ProjectivePoint, + /// Part of the signature data. SHA-256 interpreted as a scalar. + pub c: NonZeroScalar, /// Part of the signature data, a scalar value. - pub s: &'a Scalar, + pub s: NonZeroScalar, /// Optional signature data for variant 1 signatures. - pub v1: Option>, + pub v1specific: Option, } /// Nested struct holding additional signature data used in variant 1 of the protocol. #[derive(Debug)] -pub struct PlumeSignatureV1Fields<'a> { +pub struct PlumeSignatureV1Fields { /// Part of the signature data, a curve point. - pub r_point: &'a ProjectivePoint, + pub r_point: ProjectivePoint, /// Part of the signature data, a curve point. - pub hashed_to_curve_r: &'a ProjectivePoint, + pub hashed_to_curve_r: ProjectivePoint, } -impl PlumeSignature<'_> { +impl PlumeSignature { /// Verifies a PLUME signature. /// Returns `true` if the signature is valid. pub fn verify(&self) -> bool { @@ -76,56 +87,64 @@ impl PlumeSignature<'_> { // hash[m, gsk]^[r + sk * c] / (hash[m, pk]^sk)^c = hash[m, pk]^r // c = hash2(g, g^sk, hash[m, g^sk], hash[m, pk]^sk, gr, hash[m, pk]^r) - // don't forget to check `c` is `Output` in the #API - let c = panic::catch_unwind(|| Output::::from_slice(self.c)); - if c.is_err() { - return false; - } - let c = c.unwrap(); + let c_scalar = *self.c; - // TODO should we allow `c` input greater than BaseField::MODULUS? - // TODO `reduce_nonzero` doesn't seems to be correct here. `NonZeroScalar` should be appropriate. - let c_scalar = &Scalar::reduce_nonzero(U256::from_be_byte_array(c.to_owned())); + let r_point = (ProjectivePoint::GENERATOR * *self.s) - (self.pk * (c_scalar)); - let r_point = ProjectivePoint::GENERATOR * self.s - self.pk * c_scalar; - - let hashed_to_curve = hash_to_curve(self.message, self.pk); + let hashed_to_curve = hash_to_curve(&self.message, &self.pk); if hashed_to_curve.is_err() { return false; } let hashed_to_curve = hashed_to_curve.unwrap(); - let hashed_to_curve_r = hashed_to_curve * self.s - self.nullifier * c_scalar; + let hashed_to_curve_r = hashed_to_curve * *self.s - self.nullifier * (c_scalar); if let Some(PlumeSignatureV1Fields { r_point: sig_r_point, hashed_to_curve_r: sig_hashed_to_curve_r, - }) = self.v1 + }) = self.v1specific { // Check whether g^r equals g^s * pk^{-c} - if &r_point != sig_r_point { + if r_point != sig_r_point { return false; } // Check whether h^r equals h^{r + sk * c} * nullifier^{-c} - if &hashed_to_curve_r != sig_hashed_to_curve_r { + if hashed_to_curve_r != sig_hashed_to_curve_r { return false; } // Check if the given hash matches - c == &c_sha256_vec_signal(vec![ - &ProjectivePoint::GENERATOR, - self.pk, - &hashed_to_curve, - self.nullifier, - &r_point, - &hashed_to_curve_r, - ]) + c_scalar + == Scalar::reduce(U256::from_be_byte_array(c_sha256_vec_signal(vec![ + &ProjectivePoint::GENERATOR, + &self.pk, + &hashed_to_curve, + &self.nullifier, + &r_point, + &hashed_to_curve_r, + ]))) } else { // Check if the given hash matches - c == &c_sha256_vec_signal(vec![self.nullifier, &r_point, &hashed_to_curve_r]) + c_scalar + == Scalar::reduce(U256::from_be_byte_array(c_sha256_vec_signal(vec![ + &self.nullifier, + &r_point, + &hashed_to_curve_r, + ]))) } } + + /// Yields the signature with `None` for `v1specific`. Same as using [`RandomizedSigner`] with [`PlumeSigner`]; + /// use it when you don't want to `use` PlumeSigner and the trait in your code. + pub fn sign_v1(secret_key: &SecretKey, msg: &[u8], rng: &mut impl CryptoRngCore) -> Self { + PlumeSigner::new(secret_key, true).sign_with_rng(rng, msg) + } + /// Yields the signature with `Some` for `v1specific`. Same as using [`RandomizedSigner`] with [`PlumeSigner`]; + /// use it when you don't want to `use` PlumeSigner and the trait in your code. + pub fn sign_v2(secret_key: &SecretKey, msg: &[u8], rng: &mut impl CryptoRngCore) -> Self { + PlumeSigner::new(secret_key, false).sign_with_rng(rng, msg) + } } fn c_sha256_vec_signal(values: Vec<&ProjectivePoint>) -> Output { @@ -139,34 +158,6 @@ fn c_sha256_vec_signal(values: Vec<&ProjectivePoint>) -> Output { sha256_hasher.finalize() } -// Withhold removing this before implementing `sign` -fn sha256hash6signals( - g: &ProjectivePoint, - pk: &ProjectivePoint, - hash_m_pk: &ProjectivePoint, - nullifier: &ProjectivePoint, - g_r: &ProjectivePoint, - hash_m_pk_pow_r: &ProjectivePoint, -) -> Scalar { - let g_bytes = encode_pt(g); - let pk_bytes = encode_pt(pk); - let h_bytes = encode_pt(hash_m_pk); - let nul_bytes = encode_pt(nullifier); - let g_r_bytes = encode_pt(g_r); - let z_bytes = encode_pt(hash_m_pk_pow_r); - - let c_preimage_vec = [g_bytes, pk_bytes, h_bytes, nul_bytes, g_r_bytes, z_bytes].concat(); - - //println!("c_preimage_vec: {:?}", c_preimage_vec); - - let mut sha256_hasher = Sha256::new(); - sha256_hasher.update(c_preimage_vec.as_slice()); - let sha512_hasher_result = sha256_hasher.finalize(); //512 bit hash - - let c_bytes = FieldBytes::from_iter(sha512_hasher_result.iter().copied()); - Scalar::from_repr(c_bytes).unwrap() -} - #[cfg(test)] mod tests { use super::*; @@ -200,7 +191,7 @@ mod tests { fn test_byte_array_to_scalar() { let scalar = byte_array_to_scalar(&hex!( "c6a7fc2c926ddbaf20731a479fb6566f2daa5514baae5223fe3b32edbce83254" - )); // TODO this `fn` looks suspicious as in reproducing const time ops + )); assert_eq!( hex::encode(scalar.to_bytes()), "c6a7fc2c926ddbaf20731a479fb6566f2daa5514baae5223fe3b32edbce83254" diff --git a/rust-k256/src/randomizedsigner.rs b/rust-k256/src/randomizedsigner.rs new file mode 100644 index 0000000..5e25857 --- /dev/null +++ b/rust-k256/src/randomizedsigner.rs @@ -0,0 +1,110 @@ +use super::{ + CryptoRngCore, NonZeroScalar, PlumeSignature, PlumeSignatureV1Fields, ProjectivePoint, + SecretKey, DST, +}; +use k256::{ + elliptic_curve::{ + hash2curve::{ExpandMsgXmd, GroupDigest}, + point::NonIdentity, + sec1::ToEncodedPoint, + }, + sha2::{Digest, Sha256}, + Secp256k1, +}; +// Removed `pub` from this, since it's only interested to those who already imported `signature` +use signature::{Error, RandomizedSigner}; + +/// `PlumeSigner` is a `struct` that contains a reference to a secret key and a +/// boolean defining output [`PlumeSignature`] variant. It implements the +/// `RandomizedSigner` trait to generate signatures using the provided secret +/// key. The struct is generic over the lifetime of the secret key reference +/// so that the key can be borrowed immutably. +pub struct PlumeSigner<'signing> { + /// The secret key to use for signing. This is borrowed immutably. + secret_key: &'signing SecretKey, + /// Whether to generate a PlumeSignature V1 (true) or PlumeSignature V2 (false). + /// + /// `bool` is fine to use here since the choice affects only the hashing which doesn't + /// involve the key material, and distinguishing on it doesn't look possible + // Since #lastoponsecret seems to me indistinguishible between variants here's `bool` is used instead of `subtle` + pub v1: bool, +} +impl<'signing> PlumeSigner<'signing> { + /// Creates a new `PlumeSigner` instance with the given secret key and signature + /// variant. + pub fn new(secret_key: &SecretKey, v1: bool) -> PlumeSigner { + PlumeSigner { secret_key, v1 } + } +} +impl<'signing> RandomizedSigner for PlumeSigner<'signing> { + fn try_sign_with_rng( + &self, + rng: &mut impl CryptoRngCore, + msg: &[u8], + ) -> Result { + // Pick a random r from Fp + let r_scalar = SecretKey::random(rng); + + let r_point = r_scalar.public_key(); + + let pk = self.secret_key.public_key(); + let pk_bytes = pk.to_encoded_point(true).to_bytes(); + + // Compute h = htc([m, pk]) + let hashed_to_curve = NonIdentity::new( + Secp256k1::hash_from_bytes::>(&[msg, &pk_bytes], &[DST]) + .map_err(|_| Error::new())?, + ) + .expect("something is drammatically wrong if the input hashed to the identity"); + + // it feels not that scary to store `r_scalar` as `NonZeroScalar` (compared to `self.secret_key`) + let r_scalar = r_scalar.to_nonzero_scalar(); + + // Compute z = h^r + let hashed_to_curve_r = hashed_to_curve * r_scalar; + + // Compute nul = h^sk + let nullifier = hashed_to_curve * self.secret_key.to_nonzero_scalar(); + + // Compute c = sha512([g, pk, h, nul, g^r, z]) + let mut hasher = Sha256::new(); + // shorthand for updating the hasher which repeats a lot below + macro_rules! updhash { + ($p:ident) => { + hasher.update($p.to_encoded_point(true).as_bytes()) + }; + } + if self.v1 { + hasher.update(ProjectivePoint::GENERATOR.to_encoded_point(true).as_bytes()); + hasher.update(pk_bytes); + updhash!(hashed_to_curve); + } + updhash!(nullifier); + updhash!(r_point); + updhash!(hashed_to_curve_r); + + let c = hasher.finalize(); + let c_scalar = NonZeroScalar::from_repr(c) + .expect("it should be impossible to get the hash equal to zero"); + + // Compute $s = r + sk ⋅ c$. #lastoponsecret + let s_scalar = NonZeroScalar::new(*r_scalar + *(c_scalar * self.secret_key.to_nonzero_scalar())) + .expect("something is terribly wrong if the nonce is equal to negated product of the secret and the hash"); + + Ok(PlumeSignature { + message: msg.to_owned(), + pk: pk.into(), + nullifier: nullifier.to_point(), + c: c_scalar, + s: s_scalar, + v1specific: if self.v1 { + Some(PlumeSignatureV1Fields { + r_point: r_point.into(), + hashed_to_curve_r: hashed_to_curve_r.to_point(), + }) + } else { + None + }, + }) + } +} diff --git a/rust-k256/tests/signing.rs b/rust-k256/tests/signing.rs new file mode 100644 index 0000000..931192d --- /dev/null +++ b/rust-k256/tests/signing.rs @@ -0,0 +1,64 @@ +use k256::{ + elliptic_curve::{point::AffineCoordinates, PrimeField}, + FieldBytes, Scalar, +}; +use plume_rustcrypto::{PlumeSignature, SecretKey}; +use rand_core::CryptoRng; +use signature::RandomizedSigner; + +const message: &[u8; 29] = b"An example app message string"; +const R: &[u8] = + &hex_literal::hex!("93b9323b629f251b8f3fc2dd11f4672c5544e8230d493eceea98a90bda789808"); +const SK: [u8; 32] = + hex_literal::hex!("519b423d715f8b581f4fa8ee59f4771a5b44c8130b4e3eacca54a56dda72b464"); +const V1_C: [u8; 32] = + hex_literal::hex!("c6a7fc2c926ddbaf20731a479fb6566f2daa5514baae5223fe3b32edbce83254"); +const V1_S: [u8; 32] = + hex_literal::hex!("e69f027d84cb6fe5f761e333d12e975fb190d163e8ea132d7de0bd6079ba28ca"); +const V2_C: [u8; 32] = + hex_literal::hex!("3dbfb717705010d4f44a70720c95e74b475bd3a783ab0b9e8a6b3b363434eb96"); +const V2_S: [u8; 32] = + hex_literal::hex!("528e8fbb6452f82200797b1a73b2947a92524bd611085a920f1177cb8098136b"); + +struct Mock {} +impl rand_core::RngCore for Mock { + fn next_u32(&mut self) -> u32 { + unimplemented!() + } + fn next_u64(&mut self) -> u64 { + unimplemented!() + } + + fn fill_bytes(&mut self, dest: &mut [u8]) { + assert!(dest.len() == R.len()); + assert!(dest.len() == 32); + dest.iter_mut() + .enumerate() + .for_each(|(i, x)| *x = R[/* 31 - */i]); + } + + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> { + unimplemented!() + } +} +impl CryptoRng for Mock {} + +// values for both test are from `plume_arkworks` + +#[test] +pub fn test_sign_v1() { + let sk = SecretKey::from_bytes(&SK.into()).unwrap(); + + let sig = PlumeSignature::sign_v1(&sk, message, &mut Mock {}); + assert_eq!(Scalar::from_repr(V1_C.into()).unwrap(), *sig.c); + assert_eq!(Scalar::from_repr(V1_S.into()).unwrap(), *sig.s); +} + +#[test] +pub fn test_sign_v2() { + let sk = SecretKey::from_bytes(&SK.into()).unwrap(); + + let sig = PlumeSignature::sign_v2(&sk, message, &mut Mock {}); + assert_eq!(Scalar::from_repr(V2_C.into()).unwrap(), *sig.c); + assert_eq!(Scalar::from_repr(V2_S.into()).unwrap(), *sig.s); +} diff --git a/rust-k256/tests/verification.rs b/rust-k256/tests/verification.rs index 6b7d1e0..c8cbc5b 100644 --- a/rust-k256/tests/verification.rs +++ b/rust-k256/tests/verification.rs @@ -3,13 +3,13 @@ //! Their setup is shared, `mod helpers` contains barely not refactored code, which is still instrumental to the tests. use helpers::{gen_test_scalar_sk, test_gen_signals, PlumeVersion}; -use k256::elliptic_curve::sec1::ToEncodedPoint; +use k256::{elliptic_curve::sec1::ToEncodedPoint, NonZeroScalar}; use plume_rustcrypto::{PlumeSignature, PlumeSignatureV1Fields, ProjectivePoint}; const G: ProjectivePoint = ProjectivePoint::GENERATOR; const M: &[u8; 29] = b"An example app message string"; -const C_V1: &[u8] = - &hex_literal::hex!("c6a7fc2c926ddbaf20731a479fb6566f2daa5514baae5223fe3b32edbce83254"); +const C_V1: [u8; 32] = + hex_literal::hex!("c6a7fc2c926ddbaf20731a479fb6566f2daa5514baae5223fe3b32edbce83254"); // `test_gen_signals` provides fixed key nullifier, secret key, and the random value for testing // Normally a secure enclave would generate these values, and output to a wallet implementation @@ -25,16 +25,19 @@ const C_V1: &[u8] = #[test] fn plume_v1_test() { let test_data = test_gen_signals(M, PlumeVersion::V1); - let r_point = &test_data.4.unwrap(); - let hashed_to_curve_r = &test_data.5.unwrap(); + let r_point = test_data.4.unwrap(); + let hashed_to_curve_r = test_data.5.unwrap(); + + println!("{:?}", test_data.3); + println!("{}", NonZeroScalar::new(test_data.3).unwrap().to_string()); let sig = PlumeSignature { - message: M, - pk: &(G * gen_test_scalar_sk()), - nullifier: &test_data.1, - c: C_V1, - s: &test_data.3, - v1: Some(PlumeSignatureV1Fields { + message: M.to_owned().into(), + pk: G * gen_test_scalar_sk(), + nullifier: test_data.1, + c: NonZeroScalar::from_repr(C_V1.into()).unwrap(), + s: NonZeroScalar::new(test_data.3).unwrap(), + v1specific: Some(PlumeSignatureV1Fields { r_point, hashed_to_curve_r, }), @@ -105,12 +108,12 @@ fn plume_v1_test() { fn plume_v2_test() { let test_data = test_gen_signals(M, PlumeVersion::V2); assert!(PlumeSignature { - message: M, - pk: &(G * gen_test_scalar_sk()), - nullifier: &test_data.1, - c: &test_data.2, - s: &test_data.3, - v1: None + message: M.to_owned().into(), + pk: G * gen_test_scalar_sk(), + nullifier: test_data.1, + c: NonZeroScalar::from_repr(test_data.2).unwrap(), + s: NonZeroScalar::new(test_data.3).unwrap(), + v1specific: None } .verify()); } @@ -273,7 +276,7 @@ mod helpers { }; dbg!(&c, version); - let c_scalar = &Scalar::reduce_nonzero(U256::from_be_byte_array(c.to_owned())); + let c_scalar = Scalar::from_repr(c).unwrap(); // This value is part of the discrete log equivalence (DLEQ) proof. let r_sk_c = r + sk * c_scalar;