From 5bd0bf01d2875ce47e16514248096868846b1e8a Mon Sep 17 00:00:00 2001 From: Divide-By-0 Date: Mon, 5 Feb 2024 23:54:15 -0500 Subject: [PATCH 01/11] fix #15 --- circuits/circom/verify_nullifier.circom | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circuits/circom/verify_nullifier.circom b/circuits/circom/verify_nullifier.circom index bdc6ac7..3975a07 100644 --- a/circuits/circom/verify_nullifier.circom +++ b/circuits/circom/verify_nullifier.circom @@ -244,7 +244,7 @@ template a_div_b_pow_c(n, k) { b_pow_c_inv_y.underflow === 0; - // Calculates a^s * (b^c)-1 + // Calculates a * (b^c)-1 component final_result = Secp256k1AddUnequal(n, k); final_result.a <== a; From 602ec7bdab5f298b09eff637a4a80ea868557076 Mon Sep 17 00:00:00 2001 From: Sergey Kaunov Date: Tue, 6 Feb 2024 11:24:10 +0300 Subject: [PATCH 02/11] Solve #60 (#91) * Sane error handling and some simplification (ditch unwrap) * excessive code and entities were removed --- rust-arkworks/src/error.rs | 29 +- rust-arkworks/src/hash_to_curve.rs | 30 +- rust-arkworks/src/lib.rs | 465 +++++++++++++---------------- rust-arkworks/src/tests.rs | 32 +- 4 files changed, 256 insertions(+), 300 deletions(-) diff --git a/rust-arkworks/src/error.rs b/rust-arkworks/src/error.rs index 85a7fbe..ac7546f 100644 --- a/rust-arkworks/src/error.rs +++ b/rust-arkworks/src/error.rs @@ -1,11 +1,26 @@ -use thiserror::Error; +// Legacy definitions for errors which will be gone with arkworks upgrade to `>=0.4.0`. +// `use ark_ec::hashing::HashToCurveError;` + +// use thiserror::Error; /// This is an error that could occur when running a cryptograhic primitive -#[derive(Error, Debug, PartialEq)] -pub enum CryptoError { - #[error("Cannot hash to curve")] - CannotHashToCurve, +// #[derive(Error, Debug, PartialEq)] +// pub enum CryptoError { +// #[error("Cannot hash to curve")] +// CannotHashToCurve, + +// #[error("Cannot encode a point not on the curve")] +// PointNotOnCurve, +// } - #[error("Cannot encode a point not on the curve")] - PointNotOnCurve, +// Let's outline what errors will be in `~0.4.0` +#[derive(Debug, Clone)] +pub enum HashToCurveError { + UnsupportedCurveError(String), + MapToCurveError(String), + /* let's add two more items to absorb everything + in `crate::hash_to_curve` which is + subject to deprecation */ + Legacy, + ReferenceTryAndIncrement, } diff --git a/rust-arkworks/src/hash_to_curve.rs b/rust-arkworks/src/hash_to_curve.rs index 6802654..8b19697 100644 --- a/rust-arkworks/src/hash_to_curve.rs +++ b/rust-arkworks/src/hash_to_curve.rs @@ -1,21 +1,19 @@ -use crate::error::CryptoError; +use crate::error::HashToCurveError; use ark_ec::short_weierstrass_jacobian::GroupAffine; use ark_ec::{AffineCurve, ProjectiveCurve}; use ark_ff::FromBytes; use elliptic_curve::hash2curve::{ExpandMsgXmd, GroupDigest}; use elliptic_curve::sec1::ToEncodedPoint; -use k256::sha2::Sha256; -use k256::AffinePoint; -use k256::{ProjectivePoint, Secp256k1}; +// TODO why not ark libs for these? oO +use k256::{sha2::Sha256, AffinePoint, ProjectivePoint, Secp256k1}; use secp256k1::Sec1EncodePoint; use tiny_keccak::{Hasher, Shake, Xof}; pub fn hash_to_curve( msg: &[u8], pk: &GroupAffine

, -) -> GroupAffine

{ - let pk_encoded = pk.to_encoded_point(true); - let b = hex::decode(pk_encoded).unwrap(); +) -> Result, HashToCurveError> { + let b = hex::decode(&pk.to_encoded_point(true)).expect(super::EXPECT_MSG_DECODE); let x = [msg, b.as_slice()]; let x = x.concat().clone(); let x = x.as_slice(); @@ -24,7 +22,7 @@ pub fn hash_to_curve( &[x], b"QUUX-V01-CS02-with-secp256k1_XMD:SHA-256_SSWU_RO_", ) - .unwrap(); + .map_err(|_| HashToCurveError::Legacy)?; let pt_affine = pt.to_affine(); @@ -33,13 +31,13 @@ pub fn hash_to_curve( pub fn k256_affine_to_arkworks_secp256k1_affine( k_pt: AffinePoint, -) -> GroupAffine

{ +) -> Result, HashToCurveError> { let encoded_pt = k_pt.to_encoded_point(false); let num_field_bytes = 40; // extract k_pt.x - let k_pt_x_bytes = encoded_pt.x().unwrap(); + let k_pt_x_bytes = encoded_pt.x().ok_or(HashToCurveError::Legacy)?; // pad x bytes let mut k_pt_x_bytes_vec = vec![0u8; num_field_bytes]; @@ -50,10 +48,10 @@ pub fn k256_affine_to_arkworks_secp256k1_affine( ); } let reader = std::io::BufReader::new(k_pt_x_bytes_vec.as_slice()); - let g_x = P::BaseField::read(reader).unwrap(); + let g_x = P::BaseField::read(reader).map_err(|_| HashToCurveError::Legacy)?; // extract k_pt.y - let k_pt_y_bytes = encoded_pt.y().unwrap(); + let k_pt_y_bytes = encoded_pt.y().ok_or(HashToCurveError::Legacy)?; // pad y bytes let mut k_pt_y_bytes_vec = vec![0u8; num_field_bytes]; @@ -65,13 +63,13 @@ pub fn k256_affine_to_arkworks_secp256k1_affine( } let reader = std::io::BufReader::new(k_pt_y_bytes_vec.as_slice()); - let g_y = P::BaseField::read(reader).unwrap(); + let g_y = P::BaseField::read(reader).map_err(|_| HashToCurveError::Legacy)?; - GroupAffine::

::new(g_x, g_y, false) + Ok(GroupAffine::

::new(g_x, g_y, false)) } /// Kobi's hash_to_curve function, here for reference only -pub fn _try_and_increment(msg: &[u8]) -> Result { +pub fn _try_and_increment(msg: &[u8]) -> Result { for nonce in 0u8..=255 { let mut h = Shake::v128(); h.update(&[nonce]); @@ -85,5 +83,5 @@ pub fn _try_and_increment(msg: &[u8]) -> Result { - _group: PhantomData, - _field: PhantomData, - _parameters: PhantomData

, - _message_lifetime: PhantomData<&'a ()>, - } +const EXPECT_MSG_DECODE: &str = "the value decoded have been generated by a function which is improbable to output a malformed hexstring (still a place for refactoring)"; - pub fn affine_to_bytes(point: &GroupAffine

) -> Vec { - let encoded = point.to_encoded_point(true); - let b = hex::decode(encoded).unwrap(); - b.to_vec() - } +pub enum PlumeVersion { + V1, + V2, +} - fn compute_h<'a, C: ProjectiveCurve, Fq: PrimeField, P: SWModelParameters>( - pk: &GroupAffine

, - message: &'a [u8], - ) -> Result, CryptoError> { - //let pk_affine_bytes_vec = affine_to_bytes::

(pk); - //let m_pk = [message, pk_affine_bytes_vec.as_slice()].concat(); - //hash_to_curve::try_and_increment::(m_pk.as_slice()) - Ok(hash_to_curve::hash_to_curve::(message, pk)) - } +pub fn affine_to_bytes(point: &GroupAffine

) -> Vec { + hex::decode(point.to_encoded_point(true)) + .expect(EXPECT_MSG_DECODE) + .to_vec() +} - fn compute_c_v1( - g_point: &GroupAffine

, - pk: &GroupAffine

, - hashed_to_curve: &GroupAffine

, - nullifier: &GroupAffine

, - r_point: &GroupAffine

, - hashed_to_curve_r: &GroupAffine

, - ) -> Output { - // Compute c = sha512([g, pk, h, nul, g^r, z]) - let c_preimage_vec = [ - affine_to_bytes::

(g_point), - affine_to_bytes::

(pk), - affine_to_bytes::

(hashed_to_curve), - affine_to_bytes::

(nullifier), - affine_to_bytes::

(r_point), - affine_to_bytes::

(hashed_to_curve_r), - ] - .concat(); - - Sha256::digest(c_preimage_vec.as_slice()) - } +fn compute_h<'a, C: ProjectiveCurve, Fq: PrimeField, P: SWModelParameters>( + pk: &GroupAffine

, + message: &'a [u8], +) -> Result, HashToCurveError> { + //let pk_affine_bytes_vec = affine_to_bytes::

(pk); + //let m_pk = [message, pk_affine_bytes_vec.as_slice()].concat(); + //hash_to_curve::try_and_increment::(m_pk.as_slice()) + hash_to_curve::(message, pk) +} - fn compute_c_v2( - nullifier: &GroupAffine

, - r_point: &GroupAffine

, - hashed_to_curve_r: &GroupAffine

, - ) -> Output { - // Compute c = sha512([nul, g^r, z]) - let nul_bytes = affine_to_bytes::

(nullifier); - let g_r_bytes = affine_to_bytes::

(r_point); - let z_bytes = affine_to_bytes::

(hashed_to_curve_r); +fn compute_c_v1( + g_point: &GroupAffine

, + pk: &GroupAffine

, + hashed_to_curve: &GroupAffine

, + nullifier: &GroupAffine

, + r_point: &GroupAffine

, + hashed_to_curve_r: &GroupAffine

, +) -> Output { + // Compute c = sha512([g, pk, h, nul, g^r, z]) + let c_preimage_vec = [ + affine_to_bytes::

(g_point), + affine_to_bytes::

(pk), + affine_to_bytes::

(hashed_to_curve), + affine_to_bytes::

(nullifier), + affine_to_bytes::

(r_point), + affine_to_bytes::

(hashed_to_curve_r), + ] + .concat(); + + Sha256::digest(c_preimage_vec.as_slice()) +} - let c_preimage_vec = [nul_bytes, g_r_bytes, z_bytes].concat(); +fn compute_c_v2( + nullifier: &GroupAffine

, + r_point: &GroupAffine

, + hashed_to_curve_r: &GroupAffine

, +) -> Output { + // Compute c = sha512([nul, g^r, z]) + let nul_bytes = affine_to_bytes::

(nullifier); + let g_r_bytes = affine_to_bytes::

(r_point); + let z_bytes = affine_to_bytes::

(hashed_to_curve_r); - Sha256::digest(c_preimage_vec.as_slice()) - } + let c_preimage_vec = [nul_bytes, g_r_bytes, z_bytes].concat(); - pub trait VerifiableUnpredictableFunction { - type Message: ToBytes; - type Parameters: CanonicalSerialize + CanonicalDeserialize; - type PublicKey: CanonicalSerialize + CanonicalDeserialize; - type SecretKey: CanonicalSerialize + CanonicalDeserialize; - type Signature: CanonicalSerialize + CanonicalDeserialize; - - /// Generate a public key and a private key. - fn keygen( - pp: &Self::Parameters, - rng: &mut R, - ) -> Result<(Self::PublicKey, Self::SecretKey), CryptoError>; - - /// Sign a message. - fn sign( - pp: &Self::Parameters, - rng: &mut R, - keypair: (&Self::PublicKey, &Self::SecretKey), - message: Self::Message, - version: PlumeVersion, - ) -> Result; - - /// Sign a message using an specified r value - fn sign_with_r( - pp: &Self::Parameters, - keypair: (&Self::PublicKey, &Self::SecretKey), - message: Self::Message, - r_scalar: Self::SecretKey, - version: PlumeVersion, - ) -> Result; - - fn verify_non_zk( - pp: &Self::Parameters, - pk: &Self::PublicKey, - sig: &Self::Signature, - message: Self::Message, - version: PlumeVersion, - ) -> Result; - } + Sha256::digest(c_preimage_vec.as_slice()) +} - #[derive( - Copy, - Clone, - ark_serialize_derive::CanonicalSerialize, - ark_serialize_derive::CanonicalDeserialize, - )] - pub struct Parameters { - pub g_point: GroupAffine

, - } +#[derive( + Copy, + Clone, + ark_serialize_derive::CanonicalSerialize, + ark_serialize_derive::CanonicalDeserialize, +)] +pub struct Parameters { + pub g_point: GroupAffine

, +} - #[derive( - Copy, - Clone, - ark_serialize_derive::CanonicalSerialize, - ark_serialize_derive::CanonicalDeserialize, - )] - pub struct Signature { - pub hashed_to_curve_r: GroupAffine

, - pub r_point: GroupAffine

, - pub s: P::ScalarField, - pub c: P::ScalarField, - pub nullifier: GroupAffine

, +#[derive( + Copy, + Clone, + ark_serialize_derive::CanonicalSerialize, + ark_serialize_derive::CanonicalDeserialize, +)] +pub struct PlumeSignature { + pub hashed_to_curve_r: GroupAffine

, + pub r_point: GroupAffine

, + pub s: P::ScalarField, + pub c: P::ScalarField, + pub nullifier: GroupAffine

, +} + +// These aliases should be gone in #88 . If they won't TODO pay attention to the warning about `trait` boundaries being not checked for aliases +// also not enforcing trait bounds can impact PublicKey -- it's better to find appropriate upstream type +type Message<'a> = &'a [u8]; +type PublicKey = GroupAffine

; +type SecretKeyMaterial = P::ScalarField; + +impl PlumeSignature

{ + /// Generate the public key and a private key. + fn keygen(pp: &Parameters

, rng: &mut impl Rng) -> (PublicKey

, SecretKeyMaterial

) { + let secret_key = SecretKeyMaterial::

::rand(rng); + let public_key = pp.g_point.mul(secret_key).into(); + (public_key, secret_key) } - impl<'a, C: ProjectiveCurve, Fq: PrimeField, P: SWModelParameters> - VerifiableUnpredictableFunction for DeterministicNullifierSignatureScheme<'a, C, Fq, P> - { - type Message = &'a [u8]; - type Parameters = Parameters

; - type PublicKey = GroupAffine

; - type SecretKey = P::ScalarField; - type Signature = Signature

; - - fn keygen( - pp: &Self::Parameters, - rng: &mut R, - ) -> Result<(Self::PublicKey, Self::SecretKey), CryptoError> { - let secret_key = Self::SecretKey::rand(rng).into(); - let public_key = pp.g_point.mul(secret_key).into(); - Ok((public_key, secret_key)) - } + /// Sign a message using a specified r value + fn sign_with_r( + pp: &Parameters

, + keypair: (&PublicKey

, &SecretKeyMaterial

), + message: Message, + r_scalar: P::ScalarField, + version: PlumeVersion, + ) -> Result { + let g_point = pp.g_point; + let r_point = g_point.mul(r_scalar).into_affine(); - fn sign_with_r( - pp: &Self::Parameters, - keypair: (&Self::PublicKey, &Self::SecretKey), - message: Self::Message, - r_scalar: P::ScalarField, - version: PlumeVersion, - ) -> Result { - let g_point = pp.g_point; - let r_point = g_point.mul(r_scalar).into_affine(); - - // Compute h = htc([m, pk]) - let hashed_to_curve = compute_h::(&keypair.0, &message).unwrap(); - - // Compute z = h^r - let hashed_to_curve_r = hashed_to_curve.mul(r_scalar).into_affine(); - - // Compute nul = h^sk - let nullifier = hashed_to_curve.mul(*keypair.1).into_affine(); - - // Compute c = sha512([g, pk, h, nul, g^r, z]) - let c = match version { - PlumeVersion::V1 => compute_c_v1::

( - &g_point, - keypair.0, - &hashed_to_curve, - &nullifier, - &r_point, - &hashed_to_curve_r, - ), - PlumeVersion::V2 => compute_c_v2(&nullifier, &r_point, &hashed_to_curve_r), - }; - let c_scalar = P::ScalarField::from_be_bytes_mod_order(c.as_ref()); - // Compute s = r + sk ⋅ c - let sk_c = keypair.1.into_repr().into() * c_scalar.into_repr().into(); - let s = r_scalar.into_repr().into() + sk_c; - - let s_scalar = P::ScalarField::from(s); - - let signature = Signature { - hashed_to_curve_r, - s: s_scalar, - r_point, - c: c_scalar, - nullifier, - }; - Ok(signature) - } + // Compute h = htc([m, pk]) + let hashed_to_curve = + compute_h::(&keypair.0, &message)?; - fn sign( - pp: &Self::Parameters, - rng: &mut R, - keypair: (&Self::PublicKey, &Self::SecretKey), - message: Self::Message, - version: PlumeVersion, - ) -> Result { - // Pick a random r from Fp - let r_scalar: P::ScalarField = Self::SecretKey::rand(rng).into(); - - Self::sign_with_r(pp, keypair, message, r_scalar, version) - } + // Compute z = h^r + let hashed_to_curve_r = hashed_to_curve.mul(r_scalar).into_affine(); - fn verify_non_zk( - pp: &Self::Parameters, - pk: &Self::PublicKey, - sig: &Self::Signature, - message: Self::Message, - version: PlumeVersion, - ) -> Result { - // Compute h = htc([m, pk]) - let hashed_to_curve = compute_h::(pk, message).unwrap(); - - // TODO [replace SHA-512](https://github.com/plume-sig/zk-nullifier-sig/issues/39#issuecomment-1732497672) - // Compute c' = sha512([g, pk, h, nul, g^r, z]) for v1 - // c' = sha512([nul, g^r, z]) for v2 - let c = match version { - PlumeVersion::V1 => compute_c_v1::

( - &pp.g_point, - pk, - &hashed_to_curve, - &sig.nullifier, - &sig.r_point, - &sig.hashed_to_curve_r, - ), - PlumeVersion::V2 => { - compute_c_v2(&sig.nullifier, &sig.r_point, &sig.hashed_to_curve_r) - } - }; - let c_scalar = P::ScalarField::from_be_bytes_mod_order(c.as_ref()); - - // Reject if g^s ⋅ pk^{-c} != g^r - let g_s = pp.g_point.mul(sig.s); - let pk_c = pk.mul(sig.c); - let g_s_pk_c = g_s - pk_c; - - if sig.r_point != g_s_pk_c { - return Ok(false); - } + // Compute nul = h^sk + let nullifier = hashed_to_curve.mul(*keypair.1).into_affine(); - // Reject if h^s ⋅ nul^{-c} = z - let h_s = hashed_to_curve.mul(sig.s); - let nul_c = sig.nullifier.mul(sig.c); - let h_s_nul_c = h_s - nul_c; + // Compute c = sha512([g, pk, h, nul, g^r, z]) + let c = match version { + PlumeVersion::V1 => compute_c_v1::

( + &g_point, + keypair.0, + &hashed_to_curve, + &nullifier, + &r_point, + &hashed_to_curve_r, + ), + PlumeVersion::V2 => compute_c_v2(&nullifier, &r_point, &hashed_to_curve_r), + }; + let c_scalar = P::ScalarField::from_be_bytes_mod_order(c.as_ref()); + // Compute s = r + sk ⋅ c + let sk_c = keypair.1.into_repr().into() * c_scalar.into_repr().into(); + let s = r_scalar.into_repr().into() + sk_c; + + let s_scalar = P::ScalarField::from(s); + + let signature = PlumeSignature { + hashed_to_curve_r, + s: s_scalar, + r_point, + c: c_scalar, + nullifier, + }; + Ok(signature) + } - if sig.hashed_to_curve_r != h_s_nul_c { - return Ok(false); - } + /// Sign a message. + fn sign( + pp: &Parameters

, + rng: &mut impl Rng, + keypair: (&PublicKey

, &SecretKeyMaterial

), + message: Message, + version: PlumeVersion, + ) -> Result { + // Pick a random r from Fp + let r_scalar = P::ScalarField::rand(rng); + + Self::sign_with_r(pp, keypair, message, r_scalar, version) + } - // Reject if c != c' - if c_scalar != sig.c { - return Ok(false); + fn verify_non_zk( + self, + pp: &Parameters

, + pk: &PublicKey

, + message: Message, + version: PlumeVersion, + ) -> Result { + // Compute h = htc([m, pk]) + let hashed_to_curve = + compute_h::(pk, message)?; + + // TODO [replace SHA-512](https://github.com/plume-sig/zk-nullifier-sig/issues/39#issuecomment-1732497672) + // Compute c' = sha512([g, pk, h, nul, g^r, z]) for v1 + // c' = sha512([nul, g^r, z]) for v2 + let c = match version { + PlumeVersion::V1 => compute_c_v1::

( + &pp.g_point, + pk, + &hashed_to_curve, + &self.nullifier, + &self.r_point, + &self.hashed_to_curve_r, + ), + PlumeVersion::V2 => { + compute_c_v2(&self.nullifier, &self.r_point, &self.hashed_to_curve_r) } + }; + let c_scalar = P::ScalarField::from_be_bytes_mod_order(c.as_ref()); + + // Reject if g^s ⋅ pk^{-c} != g^r + let g_s = pp.g_point.mul(self.s); + let pk_c = pk.mul(self.c); + let g_s_pk_c = g_s - pk_c; + + if self.r_point != g_s_pk_c { + return Ok(false); + } - Ok(true) + // Reject if h^s ⋅ nul^{-c} = z + let h_s = hashed_to_curve.mul(self.s); + let nul_c = self.nullifier.mul(self.c); + let h_s_nul_c = h_s - nul_c; + + if self.hashed_to_curve_r != h_s_nul_c { + return Ok(false); + } + + // Reject if c != c' + if c_scalar != self.c { + return Ok(false); } + + Ok(true) } } diff --git a/rust-arkworks/src/tests.rs b/rust-arkworks/src/tests.rs index d0eba46..1f54c89 100644 --- a/rust-arkworks/src/tests.rs +++ b/rust-arkworks/src/tests.rs @@ -1,7 +1,5 @@ use crate::hash_to_curve::{hash_to_curve, k256_affine_to_arkworks_secp256k1_affine}; -use crate::sig::DeterministicNullifierSignatureScheme; -use crate::sig::PlumeVersion; -use crate::sig::VerifiableUnpredictableFunction; +use crate::{PlumeSignature, PlumeVersion}; use ark_ec::models::short_weierstrass_jacobian::GroupAffine; use ark_ec::{AffineCurve, ProjectiveCurve}; use ark_ff::biginteger; @@ -13,7 +11,7 @@ use secp256k1::curves::Affine; use secp256k1::curves::Secp256k1Parameters; use secp256k1::fields::Fq; -type Parameters = crate::sig::Parameters; +type Parameters = crate::Parameters; fn test_template() -> (ThreadRng, Affine) { let rng = thread_rng(); @@ -22,9 +20,6 @@ fn test_template() -> (ThreadRng, Affine) { (rng, g) } -type Scheme<'a> = - DeterministicNullifierSignatureScheme<'a, secp256k1::Projective, Fq, Secp256k1Parameters>; - #[test] pub fn test_k256_affine_to_arkworks_secp256k1_affine() { for i in 1..50 { @@ -41,7 +36,7 @@ pub fn test_k256_affine_to_arkworks_secp256k1_affine() { k256_affine_to_arkworks_secp256k1_affine::(k256_pt.to_affine()); // The points should match - assert_eq!(ark_pt.into_affine(), converted_pt); + assert_eq!(ark_pt.into_affine(), converted_pt.unwrap()); } } @@ -84,7 +79,7 @@ pub fn test_keygen() { let (mut rng, g) = test_template(); let pp = Parameters { g_point: g }; - let (pk, sk) = Scheme::keygen(&pp, &mut rng).unwrap(); + let (pk, sk) = PlumeSignature::keygen(&pp, &mut rng); let expected_pk = g.mul(sk); assert_eq!(pk, expected_pk); @@ -96,9 +91,9 @@ pub fn test_sign_and_verify() { let pp = Parameters { g_point: g }; let message = b"Message"; - let keypair = Scheme::keygen(&pp, &mut rng).unwrap(); + let keypair = PlumeSignature::keygen(&pp, &mut rng); - let sig = Scheme::sign( + let sig = PlumeSignature::sign( &pp, &mut rng, (&keypair.0, &keypair.1), @@ -107,10 +102,10 @@ pub fn test_sign_and_verify() { ) .unwrap(); - let is_valid = Scheme::verify_non_zk(&pp, &keypair.0, &sig, message, PlumeVersion::V1); + let is_valid = sig.verify_non_zk(&pp, &keypair.0, message, PlumeVersion::V1); assert!(is_valid.unwrap()); - let sig = Scheme::sign( + let sig = PlumeSignature::sign( &pp, &mut rng, (&keypair.0, &keypair.1), @@ -119,7 +114,7 @@ pub fn test_sign_and_verify() { ) .unwrap(); - let is_valid = Scheme::verify_non_zk(&pp, &keypair.0, &sig, message, PlumeVersion::V2); + let is_valid = sig.verify_non_zk(&pp, &keypair.0, message, PlumeVersion::V2); assert!(is_valid.unwrap()); } @@ -132,8 +127,7 @@ pub fn compute_h() -> GroupAffine { let pk_projective = g.mul(sk); let pk = GroupAffine::::from(pk_projective); - let h = hash_to_curve::(message, &pk); - h + hash_to_curve::(message, &pk).unwrap() } #[test] @@ -236,7 +230,8 @@ pub fn test_against_zk_nullifier_sig_c_and_s() { let keypair = (pk, sk); let sig = - Scheme::sign_with_r(&pp, (&keypair.0, &keypair.1), message, r, PlumeVersion::V1).unwrap(); + PlumeSignature::sign_with_r(&pp, (&keypair.0, &keypair.1), message, r, PlumeVersion::V1) + .unwrap(); assert_eq!( coord_to_hex(sig.c.into()), @@ -248,7 +243,8 @@ pub fn test_against_zk_nullifier_sig_c_and_s() { ); let sig = - Scheme::sign_with_r(&pp, (&keypair.0, &keypair.1), message, r, PlumeVersion::V2).unwrap(); + PlumeSignature::sign_with_r(&pp, (&keypair.0, &keypair.1), message, r, PlumeVersion::V2) + .unwrap(); assert_eq!( coord_to_hex(sig.c.into()), From fa25172f048d64083a259bbb2c1ea7310f0a6dee Mon Sep 17 00:00:00 2001 From: Sergey Kaunov Date: Tue, 6 Feb 2024 11:25:57 +0300 Subject: [PATCH 03/11] Solve #69 (Tests improvements) (#87) here I came to * Refactor tests And reread the thing estimating clarity. * `fmt` ready * Tests improvements Divides tests into unit and integrational. Clean and structure info it provided. Disjoints `mod helpers` from the `release`; along the road where pastes and adapts "one-liners" where it relied on the actual code under the testing (preserves used function calls indication on the best efforts as comments, they're good to be removed on a next iteration). should add that it's also needed to * convert `tests` to integration, * clean hex strings assertions for further comprehensibilty. \ (This well might demote `AsRef` issue to a _nice to have_ thing.) --- rust-k256/Cargo.toml | 2 +- rust-k256/src/lib.rs | 306 +++----------------------------- rust-k256/tests/verification.rs | 304 +++++++++++++++++++++++++++++++ 3 files changed, 330 insertions(+), 282 deletions(-) create mode 100644 rust-k256/tests/verification.rs diff --git a/rust-k256/Cargo.toml b/rust-k256/Cargo.toml index 98ae3a4..8e89385 100644 --- a/rust-k256/Cargo.toml +++ b/rust-k256/Cargo.toml @@ -7,7 +7,6 @@ edition = "2021" [dependencies] rand_core = "0.6.3" -hex-literal = "0.3.4" hash2field = "0.4.0" num-bigint = "0.4.3" num-integer = "0.1.45" @@ -15,3 +14,4 @@ k256 = {version = "0.13.2", features = ["arithmetic", "hash2curve", "expose-fiel [dev-dependencies] hex = "0.4.3" +hex-literal = "0.3.4" \ No newline at end of file diff --git a/rust-k256/src/lib.rs b/rust-k256/src/lib.rs index ecb8e6d..baaea72 100644 --- a/rust-k256/src/lib.rs +++ b/rust-k256/src/lib.rs @@ -7,14 +7,16 @@ use k256::{ elliptic_curve::sec1::ToEncodedPoint, elliptic_curve::{bigint::ArrayEncoding, group::ff::PrimeField}, sha2::{digest::Output, Digest, Sha256}, - FieldBytes, ProjectivePoint, Scalar, Secp256k1, U256, + FieldBytes, Scalar, Secp256k1, U256, }; // requires 'getrandom' feature use std::panic; +// TODO #86 +pub use k256::ProjectivePoint; const L: usize = 48; const COUNT: usize = 2; const OUT: usize = L * COUNT; -const DST: &[u8] = b"QUUX-V01-CS02-with-secp256k1_XMD:SHA-256_SSWU_RO_"; // Hash to curve algorithm +pub const DST: &[u8] = b"QUUX-V01-CS02-with-secp256k1_XMD:SHA-256_SSWU_RO_"; // Hash to curve algorithm fn print_type_of(_: &T) { println!("{}", std::any::type_name::()); @@ -59,7 +61,10 @@ fn sha256hash6signals( } // Hashes two values to the curve -fn hash_to_curve(m: &[u8], pk: &ProjectivePoint) -> Result { +fn hash_to_curve( + m: &[u8], + pk: &ProjectivePoint, +) -> Result { Secp256k1::hash_from_bytes::>( &[[m, &encode_pt(pk)].concat().as_slice()], //b"CURVE_XMD:SHA-256_SSWU_RO_", @@ -71,6 +76,7 @@ fn hash_to_curve(m: &[u8], pk: &ProjectivePoint) -> Result { pub message: &'a [u8], pub pk: &'a ProjectivePoint, @@ -79,6 +85,7 @@ pub struct PlumeSignature<'a> { pub s: &'a Scalar, pub v1: Option>, } +#[derive(Debug)] pub struct PlumeSignatureV1Fields<'a> { pub r_point: &'a ProjectivePoint, pub hashed_to_curve_r: &'a ProjectivePoint, @@ -90,8 +97,10 @@ impl PlumeSignature<'_> { // c = hash2(g, g^sk, hash[m, g^sk], hash[m, pk]^sk, gr, hash[m, pk]^r) pub fn verify_signals(&self) -> bool { // 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 = panic::catch_unwind(|| Output::::from_slice(self.c)); + if c.is_err() { + return false; + } let c = c.unwrap(); // TODO should we allow `c` input greater than BaseField::MODULUS? @@ -160,292 +169,27 @@ fn byte_array_to_scalar(bytes: &[u8]) -> Scalar { #[cfg(test)] mod tests { use super::*; + use hex_literal::hex; - use helpers::{gen_test_scalar_sk, hash_to_secp, test_gen_signals, PlumeVersion}; - mod helpers { - use super::*; - use hex_literal::hex; - - #[derive(Debug)] - pub enum PlumeVersion { - V1, - V2, - } - - // Generates a deterministic secret key for deterministic testing. Should be replaced by random oracle in production deployments. - pub fn gen_test_scalar_sk() -> Scalar { - Scalar::from_repr( - hex!("519b423d715f8b581f4fa8ee59f4771a5b44c8130b4e3eacca54a56dda72b464").into(), - ) - .unwrap() - } - - // Generates a deterministic r for deterministic testing. Should be replaced by random oracle in production deployments. - fn gen_test_scalar_r() -> Scalar { - Scalar::from_repr( - hex!("93b9323b629f251b8f3fc2dd11f4672c5544e8230d493eceea98a90bda789808").into(), - ) - .unwrap() - } - - // Calls the hash to curve function for secp256k1, and returns the result as a ProjectivePoint - pub fn hash_to_secp(s: &[u8]) -> ProjectivePoint { - let pt: ProjectivePoint = Secp256k1::hash_from_bytes::>( - &[s], - //b"CURVE_XMD:SHA-256_SSWU_RO_" - &[DST], - ) - .unwrap(); - pt - } - - // These generate test signals as if it were passed from a secure enclave to wallet. Note that leaking these signals would leak pk, but not sk. - // Outputs these 6 signals, in this order - // g^sk (private) - // hash[m, pk]^sk public nullifier - // c = hash2(g, pk, hash[m, pk], hash[m, pk]^sk, gr, hash[m, pk]^r) (public or private) - // r + sk * c (public or private) - // g^r (private, optional) - // hash[m, pk]^r (private, optional) - pub fn test_gen_signals( - m: &[u8], - version: PlumeVersion, - ) -> ( - ProjectivePoint, - ProjectivePoint, - Output, - Scalar, - Option, - Option, - ) { - // The base point or generator of the curve. - let g = ProjectivePoint::GENERATOR; - - // The signer's secret key. It is only accessed within the secure enclave. - let sk = gen_test_scalar_sk(); - - // A random value r. It is only accessed within the secure enclave. - let r = gen_test_scalar_r(); - - // The user's public key: g^sk. - let pk = &g * &sk; - - // The generator exponentiated by r: g^r. - let g_r = &g * &r; - - // hash[m, pk] - let hash_m_pk = hash_to_curve(m, &pk).unwrap(); - - println!( - "h.x: {:?}", - hex::encode(hash_m_pk.to_affine().to_encoded_point(false).x().unwrap()) - ); - println!( - "h.y: {:?}", - hex::encode(hash_m_pk.to_affine().to_encoded_point(false).y().unwrap()) - ); - - // hash[m, pk]^r - let hash_m_pk_pow_r = &hash_m_pk * &r; - println!( - "hash_m_pk_pow_r.x: {:?}", - hex::encode( - hash_m_pk_pow_r - .to_affine() - .to_encoded_point(false) - .x() - .unwrap() - ) - ); - println!( - "hash_m_pk_pow_r.y: {:?}", - hex::encode( - hash_m_pk_pow_r - .to_affine() - .to_encoded_point(false) - .y() - .unwrap() - ) - ); - - // The public nullifier: hash[m, pk]^sk. - let nullifier = &hash_m_pk * &sk; - - // The Fiat-Shamir type step. - let c = match version { - PlumeVersion::V1 => c_sha256_vec_signal(vec![ - &g, - &pk, - &hash_m_pk, - &nullifier, - &g_r, - &hash_m_pk_pow_r, - ]), - PlumeVersion::V2 => c_sha256_vec_signal(vec![&nullifier, &g_r, &hash_m_pk_pow_r]), - }; - dbg!(&c, version); - - let c_scalar = &Scalar::reduce_nonzero(U256::from_be_byte_array(c.to_owned())); - // This value is part of the discrete log equivalence (DLEQ) proof. - let r_sk_c = r + sk * c_scalar; - - // Return the signature. - (pk, nullifier, c, r_sk_c, Some(g_r), Some(hash_m_pk_pow_r)) - } - } - + // Test encode_pt() #[test] - fn plume_v1_test() { - let g = ProjectivePoint::GENERATOR; - - let m = b"An example app message string"; - - // Fixed key nullifier, secret key, and random value for testing - // Normally a secure enclave would generate these values, and output to a wallet implementation - let (pk, nullifier, c, r_sk_c, g_r, hash_m_pk_pow_r) = - test_gen_signals(m, PlumeVersion::V1); - - // The signer's secret key. It is only accessed within the secure enclave. - let sk = gen_test_scalar_sk(); - - // The user's public key: g^sk. - let pk = &g * &sk; - - // Verify the signals, normally this would happen in ZK with only the nullifier public, which would have a zk verifier instead - // The wallet should probably run this prior to snarkify-ing as a sanity check - // m and nullifier should be public, so we can verify that they are correct - let verified = PlumeSignature { - message: m, - pk: &pk, - nullifier: &nullifier, - c: &c, - s: &r_sk_c, - v1: Some(PlumeSignatureV1Fields { - r_point: &g_r.unwrap(), - hashed_to_curve_r: &hash_m_pk_pow_r.unwrap(), - }), - } - .verify_signals(); - println!("Verified: {}", verified); - - // Print nullifier - println!( - "nullifier.x: {:?}", - hex::encode(nullifier.to_affine().to_encoded_point(false).x().unwrap()) - ); - println!( - "nullifier.y: {:?}", - hex::encode(nullifier.to_affine().to_encoded_point(false).y().unwrap()) - ); - - // Print c - println!("c: {:?}", hex::encode(&c)); - - // Print r_sk_c - println!("r_sk_c: {:?}", hex::encode(r_sk_c.to_bytes())); - - // Print g_r - println!( - "g_r.x: {:?}", - hex::encode( - g_r.unwrap() - .to_affine() - .to_encoded_point(false) - .x() - .unwrap() - ) - ); - println!( - "g_r.y: {:?}", - hex::encode( - g_r.unwrap() - .to_affine() - .to_encoded_point(false) - .y() - .unwrap() - ) - ); - - // Print hash_m_pk_pow_r - println!( - "hash_m_pk_pow_r.x: {:?}", - hex::encode( - hash_m_pk_pow_r - .unwrap() - .to_affine() - .to_encoded_point(false) - .x() - .unwrap() - ) - ); - println!( - "hash_m_pk_pow_r.y: {:?}", - hex::encode( - hash_m_pk_pow_r - .unwrap() - .to_affine() - .to_encoded_point(false) - .y() - .unwrap() - ) - ); - - // Test encode_pt() - let g_as_bytes = encode_pt(&g); + fn test_encode_pt() { + let g_as_bytes = encode_pt(&ProjectivePoint::GENERATOR); assert_eq!( hex::encode(g_as_bytes), "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" ); + } - // Test byte_array_to_scalar() - let scalar = byte_array_to_scalar(&c); // TODO this `fn` looks suspicious as in reproducing const time ops + // Test byte_array_to_scalar() + #[test] + 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" ); - - // Test the hash-to-curve algorithm - let h = hash_to_secp(b"abc"); - assert_eq!( - hex::encode(h.to_affine().to_encoded_point(false).x().unwrap()), - "3377e01eab42db296b512293120c6cee72b6ecf9f9205760bd9ff11fb3cb2c4b" - ); - assert_eq!( - hex::encode(h.to_affine().to_encoded_point(false).y().unwrap()), - "7f95890f33efebd1044d382a01b1bee0900fb6116f94688d487c6c7b9c8371f6" - ); - assert!(verified); - } - - #[test] - fn plume_v2_test() { - let g = ProjectivePoint::GENERATOR; - - let m = b"An example app message string"; - - // Fixed key nullifier, secret key, and random value for testing - // Normally a secure enclave would generate these values, and output to a wallet implementation - let (pk, nullifier, c, r_sk_c, g_r, hash_m_pk_pow_r) = - test_gen_signals(m, PlumeVersion::V2); - - // The signer's secret key. It is only accessed within the secu`re enclave. - let sk = gen_test_scalar_sk(); - - // The user's public key: g^sk. - let pk = &g * &sk; - - // Verify the signals, normally this would happen in ZK with only the nullifier public, which would have a zk verifier instead - // The wallet should probably run this prior to snarkify-ing as a sanity check - // m and nullifier should be public, so we can verify that they are correct - let verified = PlumeSignature { - message: m, - pk: &pk, - nullifier: &nullifier, - c: &c, - s: &r_sk_c, - v1: None, - } - .verify_signals(); - assert!(verified) } } diff --git a/rust-k256/tests/verification.rs b/rust-k256/tests/verification.rs new file mode 100644 index 0000000..8fb6e67 --- /dev/null +++ b/rust-k256/tests/verification.rs @@ -0,0 +1,304 @@ +//! The suite consists of two tests; one for each type of signature. One of them also do printings of the values, +//! which can be useful to you when comparing different implementations. +//! 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 zk_nullifier::{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"); + +// `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 + +// `gen_test_scalar_sk()` provides the signer's secret key. It is only accessed within the secure enclave. + +// The user's public key goes to the `pk` field as $g^sk$. + +// Both tests finish with the signals verification, normally this would happen in ZK with only the nullifier public, which would have a zk verifier instead +// The wallet should probably run this prior to snarkify-ing as a sanity check +// `M` and nullifier should be public, so we can verify that they are correct + +#[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 sig = PlumeSignature { + message: M, + pk: &(G * gen_test_scalar_sk()), + nullifier: &test_data.1, + c: C_V1, + s: &test_data.3, + v1: Some(PlumeSignatureV1Fields { + r_point, + hashed_to_curve_r, + }), + }; + let verified = sig.verify_signals(); + println!("Verified: {}", verified); + + // Print nullifier + println!( + "nullifier.x: {:?}", + hex::encode( + sig.nullifier + .to_affine() + .to_encoded_point(false) + .x() + .unwrap() + ) + ); + println!( + "nullifier.y: {:?}", + hex::encode( + sig.nullifier + .to_affine() + .to_encoded_point(false) + .y() + .unwrap() + ) + ); + // Print c + println!("c: {:?}", hex::encode(C_V1)); + // Print r_sk_c + println!("r_sk_c: {:?}", hex::encode(sig.s.to_bytes())); + // Print g_r + println!( + "g_r.x: {:?}", + hex::encode(r_point.to_affine().to_encoded_point(false).x().unwrap()) + ); + println!( + "g_r.y: {:?}", + hex::encode(r_point.to_affine().to_encoded_point(false).y().unwrap()) + ); + // Print hash_m_pk_pow_r + println!( + "hash_m_pk_pow_r.x: {:?}", + hex::encode( + hashed_to_curve_r + .to_affine() + .to_encoded_point(false) + .x() + .unwrap() + ) + ); + println!( + "hash_m_pk_pow_r.y: {:?}", + hex::encode( + hashed_to_curve_r + .to_affine() + .to_encoded_point(false) + .y() + .unwrap() + ) + ); + + assert!(verified); +} + +#[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 + } + .verify_signals()); +} + +mod helpers { + /* Feels like this one could/should be replaced with static/constant values. Preserved for historical reasons. + For the same reasons calls for internal `fn` are commented and replaced by "one-liners" adapted from current implementation. */ + use super::*; + use hex_literal::hex; + use k256::{ + elliptic_curve::{ + bigint::ArrayEncoding, + hash2curve::{ExpandMsgXmd, GroupDigest}, + ops::ReduceNonZero, + PrimeField, + }, + sha2::{digest::Output, Digest, Sha256}, + Scalar, Secp256k1, U256, + }; + + #[derive(Debug)] + pub enum PlumeVersion { + V1, + V2, + } + + // Generates a deterministic secret key for deterministic testing. Should be replaced by random oracle in production deployments. + pub fn gen_test_scalar_sk() -> Scalar { + Scalar::from_repr( + hex!("519b423d715f8b581f4fa8ee59f4771a5b44c8130b4e3eacca54a56dda72b464").into(), + ) + .unwrap() + } + + // Generates a deterministic r for deterministic testing. Should be replaced by random oracle in production deployments. + fn gen_test_scalar_r() -> Scalar { + Scalar::from_repr( + hex!("93b9323b629f251b8f3fc2dd11f4672c5544e8230d493eceea98a90bda789808").into(), + ) + .unwrap() + } + + // Calls the hash to curve function for secp256k1, and returns the result as a ProjectivePoint + pub fn hash_to_secp(s: &[u8]) -> ProjectivePoint { + let pt: ProjectivePoint = Secp256k1::hash_from_bytes::>( + &[s], + //b"CURVE_XMD:SHA-256_SSWU_RO_" + &[zk_nullifier::DST], + ) + .unwrap(); + pt + } + + // These generate test signals as if it were passed from a secure enclave to wallet. Note that leaking these signals would leak pk, but not sk. + // Outputs these 6 signals, in this order + // g^sk (private) + // hash[m, pk]^sk public nullifier + // c = hash2(g, pk, hash[m, pk], hash[m, pk]^sk, gr, hash[m, pk]^r) (public or private) + // r + sk * c (public or private) + // g^r (private, optional) + // hash[m, pk]^r (private, optional) + pub fn test_gen_signals( + m: &[u8], + version: PlumeVersion, + ) -> ( + ProjectivePoint, + ProjectivePoint, + Output, + Scalar, + Option, + Option, + ) { + // The base point or generator of the curve. + let g = ProjectivePoint::GENERATOR; + + // The signer's secret key. It is only accessed within the secure enclave. + let sk = gen_test_scalar_sk(); + + // A random value r. It is only accessed within the secure enclave. + let r = gen_test_scalar_r(); + + // The user's public key: g^sk. + let pk = &g * &sk; + + // The generator exponentiated by r: g^r. + let g_r = &g * &r; + + // hash[m, pk] + let hash_m_pk = + // zk_nullifier::hash_to_curve(m, &pk) + Secp256k1::hash_from_bytes::>( + &[[ + m, + // &encode_pt(pk) + &pk.to_encoded_point(true).to_bytes().to_vec() + ].concat().as_slice()], + //b"CURVE_XMD:SHA-256_SSWU_RO_", + &[zk_nullifier::DST], + ) + .unwrap(); + + println!( + "h.x: {:?}", + hex::encode(hash_m_pk.to_affine().to_encoded_point(false).x().unwrap()) + ); + println!( + "h.y: {:?}", + hex::encode(hash_m_pk.to_affine().to_encoded_point(false).y().unwrap()) + ); + + // hash[m, pk]^r + let hash_m_pk_pow_r = &hash_m_pk * &r; + println!( + "hash_m_pk_pow_r.x: {:?}", + hex::encode( + hash_m_pk_pow_r + .to_affine() + .to_encoded_point(false) + .x() + .unwrap() + ) + ); + println!( + "hash_m_pk_pow_r.y: {:?}", + hex::encode( + hash_m_pk_pow_r + .to_affine() + .to_encoded_point(false) + .y() + .unwrap() + ) + ); + + // The public nullifier: hash[m, pk]^sk. + let nullifier = &hash_m_pk * &sk; + + // The Fiat-Shamir type step. + let c = match version { + PlumeVersion::V1 => Sha256::digest( + vec![&g, &pk, &hash_m_pk, &nullifier, &g_r, &hash_m_pk_pow_r] + .into_iter() + .map(|x| x.to_encoded_point(true).to_bytes().to_vec()) + .collect::>() + .concat() + .as_slice(), + ), + PlumeVersion::V2 => { + dbg!("entering `Sha256::digest` for `V2`"); + let result = Sha256::digest( + vec![&nullifier, &g_r, &hash_m_pk_pow_r] + .into_iter() + .map(|x| x.to_encoded_point(true).to_bytes().to_vec()) + .collect::>() + .concat() + .as_slice(), + ); + dbg!("finished `Sha256::digest` for `V2`"); + result + } + }; + dbg!(&c, version); + + let c_scalar = &Scalar::reduce_nonzero(U256::from_be_byte_array(c.to_owned())); + // This value is part of the discrete log equivalence (DLEQ) proof. + let r_sk_c = r + sk * c_scalar; + + // Return the signature. + (pk, nullifier, c, r_sk_c, Some(g_r), Some(hash_m_pk_pow_r)) + } + + /* Yes, testing the tests isn't a conventional things. + This should be straightened if `helpers` will be refactored. */ + #[cfg(test)] + mod tests { + use super::*; + use k256::elliptic_curve::sec1::ToEncodedPoint; + // Test the hash-to-curve algorithm + #[test] + fn test_hash_to_curve() { + let h = hash_to_secp(b"abc"); + assert_eq!( + hex::encode(h.to_affine().to_encoded_point(false).x().unwrap()), + "3377e01eab42db296b512293120c6cee72b6ecf9f9205760bd9ff11fb3cb2c4b" + ); + assert_eq!( + hex::encode(h.to_affine().to_encoded_point(false).y().unwrap()), + "7f95890f33efebd1044d382a01b1bee0900fb6116f94688d487c6c7b9c8371f6" + ); + } + } +} From f8d656037e5355ac188051155e6561032a28df28 Mon Sep 17 00:00:00 2001 From: Yush G Date: Sat, 10 Feb 2024 20:43:52 -0500 Subject: [PATCH 04/11] typos --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 86c2f91..d970d3c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # PLUME: Verifiably Deterministic Signatures on ECDSA -This repository provides libraries for the construction of deterministic nullifiers on Ethereum keys, [ERC 7524]([https://ethereum-magicians.org/t/erc-7524-plume-signature-in-wallets/15902](https://github.com/ethereum/EIPs/pull/7775)). We call them Privately Linked Unique Message Entities (or PLUMEs). PLUMEs enable zk voting, anonymous proof of solvency, and anonymous message board moderation to be possible with Ethereum keys directly, and so we think it is a critical primitive to push forwards blockchain adoption. To understand how this primitive works and the reason for design decisions, we recommend checking out [our blog post](https://blog.aayushg.com/posts/plume). +This repository provides libraries for the construction of deterministic nullifiers on Ethereum keys, [ERC 7524]([https://ethereum-magicians.org/t/erc-7524-plume-signature-in-wallets/15902](https://github.com/ethereum/EIPs/pull/7775)). We call them Privately Linked Unique Message Entities (or PLUMEs). PLUMEs enable zk voting, anonymous proof of solvency, and anonymous message board moderation to be possible with Ethereum keys directly, and so we think it is a critical primitive to push forward blockchain adoption. To understand how this primitive works and the reason for design decisions, we recommend checking out [our blog post](https://blog.aayushg.com/posts/plume). We hope that wallets integrate the javascript, rust, or C repositories for both software and hardware signature generation, and dapps integrate the zk proof in the circuits/ directory. @@ -10,7 +10,7 @@ If you would like to get a grant to create PLUME applications or improve the lib If you'd like to contribute, we offer $50 bounties in Eth/DAI for resolving any of the bugs in our issues! Each of them is quite small. That includes [#28](https://github.com/plume-sig/zk-nullifier-sig/issues/28), [#24](https://github.com/plume-sig/zk-nullifier-sig/issues/24), -[#14](https://github.com/plume-sig/zk-nullifier-sig/issues/14),and [#13](https://github.com/plume-sig/zk-nullifier-sig/issues/13). +[#14](https://github.com/plume-sig/zk-nullifier-sig/issues/14), and [#13](https://github.com/plume-sig/zk-nullifier-sig/issues/13). ## Implementations @@ -27,7 +27,7 @@ If you'd like to contribute, we offer $50 bounties in Eth/DAI for resolving any - Aztec: WIP, grant out to implement in Noir. ### Audits -We have been audited by 0xbok for these three implementations V1 and V2 implementations, as well as for V1 circuits in circom. We expect the halo2 circuits to be runnable on mobile (once we have aduited that code and put up a recursive proving infrastructure setup). +We have been audited by 0xbok for these three implementations V1 and V2 implementations, as well as for V1 circuits in circom. We expect the halo2 circuits to be runnable on mobile (once we have audited that code and put up a recursive proving infrastructure setup). ## Testing the circom circuit From c2fe28c9e43c0ab09cb72094c6ca5908d33cf074 Mon Sep 17 00:00:00 2001 From: Yush G Date: Wed, 14 Feb 2024 15:04:20 -0500 Subject: [PATCH 05/11] Update README.md with rabby url --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d970d3c..5d3dfbc 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,10 @@ If you'd like to contribute, we offer $50 bounties in Eth/DAI for resolving any - Mina: Uses it for nullifiers [here](https://github.com/o1-labs/o1js/blob/main/src/lib/nullifier.ts) and [here](https://github.com/o1-labs/o1js/blob/main/src/mina-signer/src/nullifier.ts). We are working with them to be fully ERC compliant! - Taho: We have an [open PR](https://github.com/tahowallet/extension/pull/3638) that we are waiting on them to merge! +- Rabby: We have an [open PR](https://github.com/RabbyHub/Rabby/pull/2047) that we are waiting on them to merge! - Metamask: We have an open PR set ([rpc](https://github.com/MetaMask/eth-json-rpc-middleware/pull/198 ), [api](https://github.com/MetaMask/api-specs/pull/120), [core](https://github.com/MetaMask/metamask-extension/pull/17482)) that we are waiting on them to merge! -- Aztec: WIP, grant out to implement in Noir. +- Aztec: WIP, pending implementation in Noir. ### Audits We have been audited by 0xbok for these three implementations V1 and V2 implementations, as well as for V1 circuits in circom. We expect the halo2 circuits to be runnable on mobile (once we have audited that code and put up a recursive proving infrastructure setup). From 71cea990302fa8cc1f11bdd11d0ca1049f9f3261 Mon Sep 17 00:00:00 2001 From: Yush G Date: Wed, 14 Feb 2024 15:09:35 -0500 Subject: [PATCH 06/11] added snaps support info --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5d3dfbc..d2d006e 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ If you'd like to contribute, we offer $50 bounties in Eth/DAI for resolving any - Taho: We have an [open PR](https://github.com/tahowallet/extension/pull/3638) that we are waiting on them to merge! - Rabby: We have an [open PR](https://github.com/RabbyHub/Rabby/pull/2047) that we are waiting on them to merge! - Metamask: We have an open PR set ([rpc](https://github.com/MetaMask/eth-json-rpc-middleware/pull/198 -), [api](https://github.com/MetaMask/api-specs/pull/120), [core](https://github.com/MetaMask/metamask-extension/pull/17482)) that we are waiting on them to merge! +), [api](https://github.com/MetaMask/api-specs/pull/120), [core](https://github.com/MetaMask/metamask-extension/pull/17482)) that we are waiting on them to merge! Snaps [dropped support for secret key access](https://github.com/MetaMask/snaps/issues/1665) so a Metamask Snap is no longer a tenable path, although we did have a snap as well. - Aztec: WIP, pending implementation in Noir. ### Audits From e5febe3cf3e4fcf62f7eb3a40c7b7337e8795d5b Mon Sep 17 00:00:00 2001 From: Sergey Kaunov Date: Thu, 15 Feb 2024 23:18:30 +0300 Subject: [PATCH 07/11] Resolve half of #67 (Part 1/2) (#93) * current progress * link to relevant issue * crate name edit * Flat the docs entities * meta information * Update the crate name in `tests` * current progress --- rust-arkworks/Cargo.toml | 20 ++-- rust-arkworks/README.MD | 3 + rust-arkworks/src/error.rs | 7 +- rust-arkworks/src/lib.rs | 61 +++++++++-- rust-k256/.vscode/launch.json | 41 ------- rust-k256/Cargo.toml | 7 +- rust-k256/README.MD | 1 + rust-k256/src/lib.rs | 188 +++++++++++++++++--------------- rust-k256/src/utils.rs | 25 +++++ rust-k256/tests/verification.rs | 14 +-- 10 files changed, 213 insertions(+), 154 deletions(-) create mode 100644 rust-arkworks/README.MD delete mode 100644 rust-k256/.vscode/launch.json create mode 100644 rust-k256/README.MD create mode 100644 rust-k256/src/utils.rs diff --git a/rust-arkworks/Cargo.toml b/rust-arkworks/Cargo.toml index 77c8da0..1dc7a90 100644 --- a/rust-arkworks/Cargo.toml +++ b/rust-arkworks/Cargo.toml @@ -1,18 +1,22 @@ [package] -name = "sig" -version = "0.1.0" +name = "plume_arkworks" +version = "0.0.1" edition = "2021" +license = "MIT" +description = "Implementation of PLUME: nullifier friendly signature scheme on ECDSA; using the `arkworks-rs` libraries" +repository = "https://github.com/plume-sig/zk-nullifier-sig/" +categories = ["cryptography", "cryptography::cryptocurrencies"] +keywords = ["nullifier", "zero-knowledge", "ECDSA", "PLUME"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] ark-ec = "~0.3.0" -ark-ff = "0.3.0" -ark-std = "0.3.0" -ark-serialize = "0.3.0" -ark-serialize-derive = "0.3.0" -thiserror = "1.0.30" -secp256k1 = { git = "https://github.com/geometryresearch/ark-secp256k1.git" } +ark-ff = "~0.3.0" +ark-std = "~0.3.0" +ark-serialize = "~0.3.0" +ark-serialize-derive = "~0.3.0" +secp256k1 = { git = "https://github.com/geometryresearch/ark-secp256k1.git", version = "0.1.0" } rand_core = { version = "0.6", default-features = false, features = [ "getrandom", ] } diff --git a/rust-arkworks/README.MD b/rust-arkworks/README.MD new file mode 100644 index 0000000..51d111c --- /dev/null +++ b/rust-arkworks/README.MD @@ -0,0 +1,3 @@ +https://github.com/plume-sig/zk-nullifier-sig/blob/main/README.md +# HAZMAT +Please note that until `v0.1.0` this is very much a preview crate which lets you have some preliminary feel of the structure and the reference implementation approach. \ No newline at end of file diff --git a/rust-arkworks/src/error.rs b/rust-arkworks/src/error.rs index ac7546f..151cad8 100644 --- a/rust-arkworks/src/error.rs +++ b/rust-arkworks/src/error.rs @@ -3,7 +3,7 @@ // use thiserror::Error; -/// This is an error that could occur when running a cryptograhic primitive +// /// This is an error that could occur when running a cryptograhic primitive // #[derive(Error, Debug, PartialEq)] // pub enum CryptoError { // #[error("Cannot hash to curve")] @@ -14,13 +14,18 @@ // } // Let's outline what errors will be in `~0.4.0` +/// It's an interim `enum` between legacy definition of the errors and prospective which will be relying on [`ark_ec::hashing::HashToCurveError`]. #[derive(Debug, Clone)] pub enum HashToCurveError { + /// Mimics the `ark_ec::hashing::HashToCurveError` enum UnsupportedCurveError(String), + /// Mimics the `ark_ec::hashing::HashToCurveError` enum MapToCurveError(String), /* let's add two more items to absorb everything in `crate::hash_to_curve` which is subject to deprecation */ + /// Absorbs any legacy error in [`mod@crate::hash_to_curve`]. They will be deprecated with upgrade to `~0.4.0`. Legacy, + /// A special case for a reference function. It will be moved to <./examples> with the upgrade to `~0.4.0`. ReferenceTryAndIncrement, } diff --git a/rust-arkworks/src/lib.rs b/rust-arkworks/src/lib.rs index 380c5a7..2497515 100644 --- a/rust-arkworks/src/lib.rs +++ b/rust-arkworks/src/lib.rs @@ -1,10 +1,26 @@ -use crate::error::HashToCurveError; +/// This crate provides the PLUME signature scheme. +/// +/// See for more information. +/// +/// Find RustCrypto crate as `plume_rustcrypto`. + +pub use crate::error::HashToCurveError; use crate::hash_to_curve::hash_to_curve; -use ark_ec::short_weierstrass_jacobian::GroupAffine; -use ark_ec::{models::SWModelParameters, AffineCurve, ProjectiveCurve}; + +/// Re-exports the `GroupAffine` and `SWModelParameters` types from the `ark_ec` crate. +/// +/// `GroupAffine` represents an affine point on a short Weierstrass elliptic curve. +/// `SWModelParameters` contains the parameters defining a short Weierstrass curve. +pub use ark_ec::{models::SWModelParameters, short_weierstrass_jacobian::GroupAffine}; +/// Re-exports the `Rng` trait from the `rand` crate in `ark_std`. +/// +/// `Rng` provides methods for generating random values. +pub use ark_std::rand::Rng; + +use ark_ec::{AffineCurve, ProjectiveCurve}; use ark_ff::PrimeField; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize, Read, SerializationError, Write}; -use ark_std::{rand::Rng, UniformRand}; +use ark_std::UniformRand; use secp256k1::sec1::Sec1EncodePoint; use sha2::digest::Output; use sha2::{Digest, Sha256}; @@ -14,11 +30,15 @@ mod hash_to_curve; const EXPECT_MSG_DECODE: &str = "the value decoded have been generated by a function which is improbable to output a malformed hexstring (still a place for refactoring)"; +/// An `enum` representing the variant of the PLUME protocol. pub enum PlumeVersion { V1, V2, } +/// Converts an affine point on the curve to the byte representation. +/// +/// Serializes the affine point to its SEC1 encoding and returns the raw bytes. pub fn affine_to_bytes(point: &GroupAffine

) -> Vec { hex::decode(point.to_encoded_point(true)) .expect(EXPECT_MSG_DECODE) @@ -72,6 +92,8 @@ fn compute_c_v2( Sha256::digest(c_preimage_vec.as_slice()) } +/// A struct containing parameters for the SW model, including the generator point `g_point`. +/// This struct implements traits for (de)serialization. #[derive( Copy, Clone, @@ -79,9 +101,11 @@ fn compute_c_v2( ark_serialize_derive::CanonicalDeserialize, )] pub struct Parameters { + /// The generator point for the SW model parameters. pub g_point: GroupAffine

, } +/// A struct containing the PLUME signature data #[derive( Copy, Clone, @@ -89,28 +113,37 @@ pub struct Parameters { ark_serialize_derive::CanonicalDeserialize, )] pub struct PlumeSignature { + /// The hash-to-curve output multiplied by the random `r`. pub hashed_to_curve_r: GroupAffine

, + /// The randomness `r` represented as the curve point. pub r_point: GroupAffine

, pub s: P::ScalarField, pub c: P::ScalarField, + /// The nullifier. pub nullifier: GroupAffine

, } // These aliases should be gone in #88 . If they won't TODO pay attention to the warning about `trait` boundaries being not checked for aliases // also not enforcing trait bounds can impact PublicKey -- it's better to find appropriate upstream type -type Message<'a> = &'a [u8]; -type PublicKey = GroupAffine

; -type SecretKeyMaterial = P::ScalarField; + +/// A type alias for a byte slice reference, used for representing the message. +pub type Message<'a> = &'a [u8]; +/// The public key. +pub type PublicKey = GroupAffine

; +/// The scalar field element representing the secret key. +pub type SecretKeyMaterial = P::ScalarField; impl PlumeSignature

{ /// Generate the public key and a private key. - fn keygen(pp: &Parameters

, rng: &mut impl Rng) -> (PublicKey

, SecretKeyMaterial

) { + /// # HAZMAT + /// No measures yet taken for the [`SecretKeyMaterial`] protection + pub fn keygen(pp: &Parameters

, rng: &mut impl Rng) -> (PublicKey

, SecretKeyMaterial

) { let secret_key = SecretKeyMaterial::

::rand(rng); let public_key = pp.g_point.mul(secret_key).into(); (public_key, secret_key) } - /// Sign a message using a specified r value + /// Sign a message using the specified `r` value fn sign_with_r( pp: &Parameters

, keypair: (&PublicKey

, &SecretKeyMaterial

), @@ -174,6 +207,16 @@ impl PlumeSignature

{ Self::sign_with_r(pp, keypair, message, r_scalar, version) } + /// Verifies a PLUME signature. + /// Returns `true` if the signature is valid, `false` otherwise. + /// + /// Computes the curve points and scalars needed for verification from the + /// signature parameters. Then performs the verification steps: + /// - Confirm g^s * pk^-c = g^r + /// - Confirm h^s * nul^-c = z + /// - Confirm c = c' + /// + /// Rejects if any check fails. fn verify_non_zk( self, pp: &Parameters

, diff --git a/rust-k256/.vscode/launch.json b/rust-k256/.vscode/launch.json deleted file mode 100644 index 78ac8a9..0000000 --- a/rust-k256/.vscode/launch.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "lldb", - "request": "launch", - "name": "Debug executable 'zk-nullifier'", - "cargo": { - "args": ["build", "--bin=zk-nullifier", "--package=zk-nullifier"], - "filter": { - "name": "zk-nullifier", - "kind": "bin" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug unit tests in executable 'zk-nullifier'", - "cargo": { - "args": [ - "test", - "--no-run", - "--bin=zk-nullifier", - "--package=zk-nullifier" - ], - "filter": { - "name": "zk-nullifier", - "kind": "bin" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - } - ] -} diff --git a/rust-k256/Cargo.toml b/rust-k256/Cargo.toml index 8e89385..8d25ce6 100644 --- a/rust-k256/Cargo.toml +++ b/rust-k256/Cargo.toml @@ -1,7 +1,12 @@ [package] -name = "zk-nullifier" +name = "plume_rustcrypto" version = "0.1.0" edition = "2021" +license = "MIT" +description = "Implementation of PLUME: nullifier friendly signature scheme on ECDSA; using the k256 library" +repository = "https://github.com/plume-sig/zk-nullifier-sig/" +categories = ["cryptography", "cryptography::cryptocurrencies"] +keywords = ["nullifier", "zero-knowledge", "ECDSA", "PLUME"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/rust-k256/README.MD b/rust-k256/README.MD new file mode 100644 index 0000000..ff91c89 --- /dev/null +++ b/rust-k256/README.MD @@ -0,0 +1 @@ +https://github.com/plume-sig/zk-nullifier-sig/blob/main/README.md \ No newline at end of file diff --git a/rust-k256/src/lib.rs b/rust-k256/src/lib.rs index baaea72..8e78c2a 100644 --- a/rust-k256/src/lib.rs +++ b/rust-k256/src/lib.rs @@ -1,101 +1,81 @@ // #![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. +//! +//! 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::hash2curve::{ExpandMsgXmd, GroupDigest}, elliptic_curve::ops::ReduceNonZero, - elliptic_curve::sec1::ToEncodedPoint, elliptic_curve::{bigint::ArrayEncoding, group::ff::PrimeField}, - sha2::{digest::Output, Digest, Sha256}, - FieldBytes, Scalar, Secp256k1, U256, + FieldBytes, U256, }; // requires 'getrandom' feature -use std::panic; -// TODO #86 +// 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; -const L: usize = 48; -const COUNT: usize = 2; -const OUT: usize = L * COUNT; -pub const DST: &[u8] = b"QUUX-V01-CS02-with-secp256k1_XMD:SHA-256_SSWU_RO_"; // Hash to curve algorithm - -fn print_type_of(_: &T) { - println!("{}", std::any::type_name::()); -} - -fn c_sha256_vec_signal(values: Vec<&ProjectivePoint>) -> Output { - let preimage_vec = values - .into_iter() - .map(encode_pt) - .collect::>() - .concat(); - let mut sha256_hasher = Sha256::new(); - sha256_hasher.update(preimage_vec.as_slice()); - sha256_hasher.finalize() -} - -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); +mod utils; +// not published due to use of `Projective...`; these utils can be found in other crates +use utils::*; - 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() -} - -// Hashes two values to the curve -fn hash_to_curve( - m: &[u8], - pk: &ProjectivePoint, -) -> Result { - Secp256k1::hash_from_bytes::>( - &[[m, &encode_pt(pk)].concat().as_slice()], - //b"CURVE_XMD:SHA-256_SSWU_RO_", - &[DST], - ) -} +/// 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 -/* currently seems to right place for this `struct` declaration; -should be moved (to the beginning of the file?) during refactoring for proper order of the items */ -/* while no consistent #API is present here it's completely `pub`; -when API will be designed it should include this `struct` (and it also probably will hold values instead of references) */ -#[derive(Debug)] +/// Struct holding signature data for a PLUME signature. +/// +/// `v1` field differintiate whether V1 or V2 protocol will be used. pub struct PlumeSignature<'a> { + /// The message that was signed. pub message: &'a [u8], + /// The public key used to verify the signature. pub pk: &'a ProjectivePoint, + /// The nullifier. pub nullifier: &'a ProjectivePoint, + /// Part of the signature data. pub c: &'a [u8], + /// Part of the signature data, a scalar value. pub s: &'a Scalar, + /// Optional signature data for variant 1 signatures. pub v1: Option>, } +/// Nested struct holding additional signature data used in variant 1 of the protocol. #[derive(Debug)] pub struct PlumeSignatureV1Fields<'a> { + /// Part of the signature data, a curve point. pub r_point: &'a ProjectivePoint, + /// Part of the signature data, a curve point. pub hashed_to_curve_r: &'a ProjectivePoint, } impl PlumeSignature<'_> { - // Verifier check in SNARK: - // g^[r + sk * c] / (g^sk)^c = g^r - // 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) - pub fn verify_signals(&self) -> bool { + /// Verifies a PLUME signature. + /// Returns `true` if the signature is valid. + pub fn verify(&self) -> bool { + // Verifier check in SNARK: + // g^[r + sk * c] / (g^sk)^c = g^r + // 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() { @@ -148,22 +128,43 @@ impl PlumeSignature<'_> { } } -/// Encodes the point by compressing it to 33 bytes -fn encode_pt(point: &ProjectivePoint) -> Vec { - point.to_encoded_point(true).to_bytes().to_vec() +fn c_sha256_vec_signal(values: Vec<&ProjectivePoint>) -> Output { + let preimage_vec = values + .into_iter() + .map(encode_pt) + .collect::>() + .concat(); + let mut sha256_hasher = Sha256::new(); + sha256_hasher.update(preimage_vec.as_slice()); + sha256_hasher.finalize() } -/// Convert a 32-byte array to a scalar -fn byte_array_to_scalar(bytes: &[u8]) -> Scalar { - // From https://docs.rs/ark-ff/0.3.0/src/ark_ff/fields/mod.rs.html#371-393 - assert!(bytes.len() == 32); - let mut res = Scalar::from(0u64); - let window_size = Scalar::from(256u64); - for byte in bytes.iter() { - res *= window_size; - res += Scalar::from(*byte as u64); - } - res +// 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)] @@ -181,6 +182,19 @@ mod tests { ); } + /// Convert a 32-byte array to a scalar + fn byte_array_to_scalar(bytes: &[u8]) -> Scalar { + // From https://docs.rs/ark-ff/0.3.0/src/ark_ff/fields/mod.rs.html#371-393 + assert!(bytes.len() == 32); + let mut res = Scalar::from(0u64); + let window_size = Scalar::from(256u64); + for byte in bytes.iter() { + res *= window_size; + res += Scalar::from(*byte as u64); + } + res + } + // Test byte_array_to_scalar() #[test] fn test_byte_array_to_scalar() { diff --git a/rust-k256/src/utils.rs b/rust-k256/src/utils.rs new file mode 100644 index 0000000..86501e6 --- /dev/null +++ b/rust-k256/src/utils.rs @@ -0,0 +1,25 @@ +use super::*; +use k256::{ + elliptic_curve::{ + hash2curve::{ExpandMsgXmd, GroupDigest}, + sec1::ToEncodedPoint, + }, + ProjectivePoint, Secp256k1, +}; // requires 'getrandom' feature + +// Hashes two values to the curve +pub(crate) fn hash_to_curve( + m: &[u8], + pk: &ProjectivePoint, +) -> Result { + Secp256k1::hash_from_bytes::>( + &[[m, &encode_pt(pk)].concat().as_slice()], + //b"CURVE_XMD:SHA-256_SSWU_RO_", + &[DST], + ) +} + +/// Encodes the point by compressing it to 33 bytes +pub(crate) fn encode_pt(point: &ProjectivePoint) -> Vec { + point.to_encoded_point(true).to_bytes().to_vec() +} diff --git a/rust-k256/tests/verification.rs b/rust-k256/tests/verification.rs index 8fb6e67..6b7d1e0 100644 --- a/rust-k256/tests/verification.rs +++ b/rust-k256/tests/verification.rs @@ -4,7 +4,7 @@ use helpers::{gen_test_scalar_sk, test_gen_signals, PlumeVersion}; use k256::elliptic_curve::sec1::ToEncodedPoint; -use zk_nullifier::{PlumeSignature, PlumeSignatureV1Fields, ProjectivePoint}; +use plume_rustcrypto::{PlumeSignature, PlumeSignatureV1Fields, ProjectivePoint}; const G: ProjectivePoint = ProjectivePoint::GENERATOR; const M: &[u8; 29] = b"An example app message string"; @@ -39,7 +39,7 @@ fn plume_v1_test() { hashed_to_curve_r, }), }; - let verified = sig.verify_signals(); + let verified = sig.verify(); println!("Verified: {}", verified); // Print nullifier @@ -112,7 +112,7 @@ fn plume_v2_test() { s: &test_data.3, v1: None } - .verify_signals()); + .verify()); } mod helpers { @@ -158,7 +158,7 @@ mod helpers { let pt: ProjectivePoint = Secp256k1::hash_from_bytes::>( &[s], //b"CURVE_XMD:SHA-256_SSWU_RO_" - &[zk_nullifier::DST], + &[plume_rustcrypto::DST], ) .unwrap(); pt @@ -199,16 +199,16 @@ mod helpers { let g_r = &g * &r; // hash[m, pk] - let hash_m_pk = + let hash_m_pk = // zk_nullifier::hash_to_curve(m, &pk) Secp256k1::hash_from_bytes::>( &[[ - m, + m, // &encode_pt(pk) &pk.to_encoded_point(true).to_bytes().to_vec() ].concat().as_slice()], //b"CURVE_XMD:SHA-256_SSWU_RO_", - &[zk_nullifier::DST], + &[plume_rustcrypto::DST], ) .unwrap(); From 5bb3dda0d6ff34724d4c55658b85878081640b22 Mon Sep 17 00:00:00 2001 From: Yush G Date: Wed, 28 Feb 2024 02:17:58 -0700 Subject: [PATCH 08/11] Add docs and tests (#100) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See issues #67 and #84 . Few snippets of other paths I considered. ```rust ... [ProjectivePoint::GENERATOR.to_encoded_point(true).as_bytes(), &pk_bytes, enc!(hashed_to_curve)].map(|b| hashers[0].update(b)); for b in [enc!(nullifier), enc![r_point], enc!(hashed_to_curve_r)] { hashers[0].update(b); hashers[1].update(b); } let c = hashers.map(|h| h.finalize()); let c_scalar = c.clone().map(|c_i| NonZeroScalar::reduce_nonzero(U256::from_be_byte_array(c_i))); // Compute s = r + sk ⋅ c let s_scalar = c_scalar.map(|c_scalar_i| NonZeroScalar::new(*r_scalar + *(self.to_nonzero_scalar() * c_scalar_i)) .expect("something is terribly wrong if the nonce is equal to negated product of the secret and the hash")); Ok(PlumeSignatureCombined{ message: msg.to_owned(), pk: pk.into(), nullifier: nullifier.to_point(), v2_c: c[1], v2_s: *s_scalar[1], v1_c: c[0], v1_s: *s_scalar[0], v1_r_point: r_point.into(), v1_hashed_to_curve_r: hashed_to_curve_r.to_point(), }) } } /// Struct holding mandatory signature data for ... PLUME signature #[derive(Debug)] pub struct PlumeSignatureCombined { /// The message that was signed. pub message: Vec, /// The public key used to verify the signature. pub pk: ProjectivePoint, /// The nullifier. pub nullifier: ProjectivePoint, /// Part of the signature data. pub v2_c: Output, /// Part of the signature data, a scalar value. pub v2_s: Scalar, /// Part of the signature data. pub v1_c: Output, /// Part of the signature data, a scalar value. pub v1_s: Scalar, /// Part of the V1 signature data, a curve point. pub v1_r_point: ProjectivePoint, /// Part of the V1 signature data, a curve point. pub v1_hashed_to_curve_r: ProjectivePoint, } impl PlumeSignatureCombined { pub fn separate(self) -> (PlumeSignature, PlumeSignature) { let (pk, nullifier) = (self.pk, self.nullifier); ( PlumeSignature{ message: self.message.clone(), pk, nullifier, c: self.v1_c, s: self.v1_s, v1specific: Some( PlumeSignatureV1Fields{ r_point: self.v1_r_point, hashed_to_curve_r: self.v1_hashed_to_curve_r } ) }, PlumeSignature{ message: self.message, pk, nullifier, c: self.v2_c, s: self.v2_s, v1specific: None }, ) } } ``` _____________________________________ ```rust pub enum PlumeSignature{ V1(PlumeSignatureV1), V2(PlumeSignatureV2), } pub struct PlumeSignatureV1 { v2: PlumeSignatureV2, v1: PlumeSignatureV1Fields } /// Struct holding mandatory signature data for a PLUME signature. pub struct PlumeSignatureV2 { /// The message that was signed. pub message: Vec, /// The public key used to verify the signature. pub pk: ProjectivePoint, /// The nullifier. pub nullifier: ProjectivePoint, /// Part of the signature data. pub c: Output, /// Part of the signature data, a scalar value. pub s: Scalar, // /// Optional signature data for variant 1 signatures. // pub v1: Option, } /// struct holding additional signature data used in variant 1 of the protocol. #[derive(Debug)] pub struct PlumeSignatureV1Fields { /// Part of the signature data, a curve point. pub r_point: ProjectivePoint, /// Part of the signature data, a curve point. pub hashed_to_curve_r: ProjectivePoint, } impl signature::RandomizedSigner for SecretKey {} impl signature::RandomizedSigner for SecretKey {} ``` --------- Co-authored-by: skaunov --- 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; From a44d60c78e57facd26b196ba5665189c6c888603 Mon Sep 17 00:00:00 2001 From: Sergey Kaunov Date: Thu, 29 Feb 2024 18:52:09 +0300 Subject: [PATCH 09/11] Npm repository link (#102) * add the link to the repo for NPM * version bump / sync --- javascript/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/javascript/package.json b/javascript/package.json index cd25ba0..b52e7d1 100644 --- a/javascript/package.json +++ b/javascript/package.json @@ -1,6 +1,7 @@ { "name": "plume-sig", - "version": "2.0.3", + "version": "2.0.5", + "repository": "https://github.com/plume-sig/zk-nullifier-sig/", "pnpm": { "overrides": { "@noble/secp256k1": "$@noble/secp256k1" From e956b27ee1b3c2cf051e03ab1b6aabde8bc66780 Mon Sep 17 00:00:00 2001 From: Sergey Kaunov Date: Thu, 29 Feb 2024 23:46:39 +0300 Subject: [PATCH 10/11] Draft of for NPM (#103) I believe these should be organized other way (divide the monorepo?). But @Divide-by-0 seems to really need something to start with rn --- javascript/README.MD | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 javascript/README.MD diff --git a/javascript/README.MD b/javascript/README.MD new file mode 100644 index 0000000..1b0b707 --- /dev/null +++ b/javascript/README.MD @@ -0,0 +1,20 @@ +JavaScript implementation of the PLUME signature scheme. + +## API +### sign(message, privateKey) +Signs a message using the provided private key. +* `message` - String message to sign +* `privateKey` - Hex private key + +Returns the PLUME signature. + +### `verify(message, publicKey, signature)` +Verifies a signature matches the message and public key. +* `message` - Original string message +* `publicKey` - Hex public key +* `signature` - PLUME signature + +Returns true if the signature is valid, false otherwise. + +### License +MIT \ No newline at end of file From bc1d2f873db9c025223d58b4d49832142336b344 Mon Sep 17 00:00:00 2001 From: Yush G Date: Tue, 5 Mar 2024 16:25:31 -0800 Subject: [PATCH 11/11] Update Mina references --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d2d006e..506bf2a 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ If you'd like to contribute, we offer $50 bounties in Eth/DAI for resolving any ### Wallet Implementations -- Mina: Uses it for nullifiers [here](https://github.com/o1-labs/o1js/blob/main/src/lib/nullifier.ts) and [here](https://github.com/o1-labs/o1js/blob/main/src/mina-signer/src/nullifier.ts). We are working with them to be fully ERC compliant! +- Mina: Uses it for nullifiers in their code [here](https://github.com/o1-labs/o1js/blob/main/src/lib/nullifier.ts) and [here](https://github.com/o1-labs/o1js/blob/main/src/mina-signer/src/nullifier.ts). They use Poseidon for the hash function instead, which makes it slower to generate in hardware wallets, but faster to prove. Their [docs for this scheme are here](https://docs.minaprotocol.com/zkapps/o1js-reference/classes/Nullifier). - Taho: We have an [open PR](https://github.com/tahowallet/extension/pull/3638) that we are waiting on them to merge! - Rabby: We have an [open PR](https://github.com/RabbyHub/Rabby/pull/2047) that we are waiting on them to merge! - Metamask: We have an open PR set ([rpc](https://github.com/MetaMask/eth-json-rpc-middleware/pull/198