nym_compact_ecash/scheme/withdrawal.rs
1// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::common_types::{BlindedSignature, Signature, SignerIndex};
5use crate::error::{CompactEcashError, Result};
6use crate::helpers::{date_scalar, type_scalar};
7use crate::proofs::proof_withdrawal::{
8 WithdrawalReqInstance, WithdrawalReqProof, WithdrawalReqWitness,
9};
10use crate::scheme::keygen::{PublicKeyUser, SecretKeyAuth, SecretKeyUser, VerificationKeyAuth};
11use crate::scheme::setup::GroupParameters;
12use crate::scheme::PartialWallet;
13use crate::utils::{check_bilinear_pairing, hash_g1};
14use crate::{constants, ecash_group_parameters, Attribute, EncodedDate, EncodedTicketType};
15use group::{Curve, Group, GroupEncoding};
16use nym_bls12_381_fork::{multi_miller_loop, G1Projective, G2Prepared, G2Projective, Scalar};
17use serde::{Deserialize, Serialize};
18use std::ops::Neg;
19use zeroize::{Zeroize, ZeroizeOnDrop};
20
21/// Represents a withdrawal request generate by the client who wants to obtain a zk-nym credential.
22///
23/// This struct encapsulates the necessary components for a withdrawal request, including the joined commitment hash, the joined commitment,
24/// individual Pedersen commitments for private attributes, and a zero-knowledge proof for the withdrawal request.
25///
26/// # Fields
27///
28/// * `joined_commitment_hash` - The joined commitment hash represented as a G1Projective element.
29/// * `joined_commitment` - The joined commitment represented as a G1Projective element.
30/// * `private_attributes_commitments` - A vector of individual Pedersen commitments for private attributes represented as G1Projective elements.
31/// * `zk_proof` - The zero-knowledge proof for the withdrawal request.
32///
33/// # Derives
34///
35/// The struct derives `Debug` and `PartialEq` to provide debug output and basic comparison functionality.
36///
37#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
38pub struct WithdrawalRequest {
39 joined_commitment_hash: G1Projective,
40 joined_commitment: G1Projective,
41 private_attributes_commitments: Vec<G1Projective>,
42 zk_proof: WithdrawalReqProof,
43}
44
45impl WithdrawalRequest {
46 pub fn get_private_attributes_commitments(&self) -> &[G1Projective] {
47 &self.private_attributes_commitments
48 }
49}
50
51/// Represents information associated with a withdrawal request.
52///
53/// This structure holds the commitment hash, commitment opening, private attributes openings,
54/// the wallet secret (scalar), and the expiration date related to a withdrawal request.
55#[derive(Debug, Clone, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
56pub struct RequestInfo {
57 joined_commitment_hash: G1Projective,
58 joined_commitment_opening: Scalar,
59 private_attributes_openings: Vec<Scalar>,
60 wallet_secret: Scalar,
61 expiration_date: Scalar,
62 t_type: Scalar,
63}
64
65impl RequestInfo {
66 pub fn get_joined_commitment_hash(&self) -> &G1Projective {
67 &self.joined_commitment_hash
68 }
69 pub fn get_joined_commitment_opening(&self) -> &Scalar {
70 &self.joined_commitment_opening
71 }
72 pub fn get_private_attributes_openings(&self) -> &[Scalar] {
73 &self.private_attributes_openings
74 }
75 pub fn get_v(&self) -> &Scalar {
76 &self.wallet_secret
77 }
78 pub fn get_expiration_date(&self) -> &Scalar {
79 &self.expiration_date
80 }
81 pub fn get_t_type(&self) -> &Scalar {
82 &self.t_type
83 }
84}
85
86/// Computes Pedersen commitments for private attributes.
87///
88/// Given a set of private attributes and the commitment hash for all attributes,
89/// this function generates random blinding factors (`openings`) and computes corresponding
90/// Pedersen commitments for each private attribute.
91/// Pedersen commitments have the hiding and binding properties, providing a secure way
92/// to represent private values in a commitment scheme.
93///
94/// # Arguments
95///
96/// * `params` - Group parameters for the cryptographic group.
97/// * `joined_commitment_hash` - The commitment hash to be used in the Pedersen commitments.
98/// * `private_attributes` - A slice of private attributes to be committed.
99///
100/// # Returns
101///
102/// A tuple containing vectors of blinding factors (`openings`) and corresponding
103/// Pedersen commitments for each private attribute.
104fn compute_private_attribute_commitments(
105 params: &GroupParameters,
106 joined_commitment_hash: &G1Projective,
107 private_attributes: &[&Scalar],
108) -> (Vec<Scalar>, Vec<G1Projective>) {
109 let (openings, commitments): (Vec<Scalar>, Vec<G1Projective>) = private_attributes
110 .iter()
111 .map(|&m_j| {
112 let o_j = params.random_scalar();
113 (o_j, params.gen1() * o_j + joined_commitment_hash * m_j)
114 })
115 .unzip();
116
117 (openings, commitments)
118}
119/// Generates a non-identity hash of joined commitment.
120///
121/// This function attempts to create a valid joined commitment and hash by
122/// repeatedly generating a random `joined_commitment_opening` and computing
123/// the corresponding `joined_commitment` and `joined_commitment_hash`.
124/// It continues this process until the `joined_commitment_hash` is not the
125/// identity element.
126fn generate_non_identity_h(
127 params: &GroupParameters,
128 sk_user: &SecretKeyUser,
129 v: &Scalar,
130 expiration_date: Scalar,
131 t_type: Scalar,
132) -> (G1Projective, G1Projective, Scalar) {
133 let gamma = params.gammas();
134
135 loop {
136 let joined_commitment_opening = params.random_scalar();
137
138 // Compute joined commitment for all attributes (public and private)
139 let joined_commitment =
140 params.gen1() * joined_commitment_opening + gamma[0] * sk_user.sk + gamma[1] * v;
141
142 // Compute commitment hash h
143 let joined_commitment_hash = hash_g1(
144 (joined_commitment + gamma[2] * expiration_date + gamma[3] * t_type).to_bytes(),
145 );
146
147 // Check if the joined_commitment_hash is not the identity element
148 if !bool::from(joined_commitment_hash.is_identity()) {
149 return (
150 joined_commitment,
151 joined_commitment_hash,
152 joined_commitment_opening,
153 );
154 }
155 }
156}
157/// Generates a withdrawal request for the given user to request a zk-nym credential wallet.
158///
159/// # Arguments
160///
161/// * `sk_user` - A reference to the user's secret key.
162/// * `expiration_date` - The expiration date for the withdrawal request.
163/// * `t_type` - The type of the ticket book
164///
165/// # Returns
166///
167/// A tuple containing the generated `WithdrawalRequest` and `RequestInfo`, or an error if the operation fails.
168///
169/// # Details
170///
171/// The function starts by generating a random, unique wallet secret `v` and computing the joined commitment for all attributes,
172/// including public (expiration date) and private ones (user secret key and wallet secret).
173/// It then calculates the commitment hash (`joined_commitment_hash`) and computes Pedersen commitments for private attributes.
174/// A zero-knowledge proof of knowledge is constructed to prove possession of specific attributes.
175///
176/// The resulting `WithdrawalRequest` includes the commitment hash, joined commitment, commitments for private
177/// attributes, and the constructed zero-knowledge proof.
178///
179/// The associated `RequestInfo` includes information such as commitment hash, commitment opening,
180/// openings for private attributes, `v`, and the expiration date.
181pub fn withdrawal_request(
182 sk_user: &SecretKeyUser,
183 expiration_date: EncodedDate,
184 t_type: EncodedTicketType,
185) -> Result<(WithdrawalRequest, RequestInfo)> {
186 let params = ecash_group_parameters();
187 // Generate random and unique wallet secret
188 let v = params.random_scalar();
189 let expiration_date = date_scalar(expiration_date);
190 let t_type = type_scalar(t_type);
191
192 // Generate a non-identity commitment hash
193 let (joined_commitment, joined_commitment_hash, joined_commitment_opening) =
194 generate_non_identity_h(params, sk_user, &v, expiration_date, t_type);
195
196 // Compute Pedersen commitments for private attributes (wallet secret and user's secret)
197 let private_attributes = vec![&sk_user.sk, &v];
198 let (private_attributes_openings, private_attributes_commitments) =
199 compute_private_attribute_commitments(params, &joined_commitment_hash, &private_attributes);
200
201 // construct a NIZK proof of knowledge proving possession of m1, m2, o, o1, o2
202 let instance = WithdrawalReqInstance {
203 joined_commitment,
204 joined_commitment_hash,
205 private_attributes_commitments: private_attributes_commitments.clone(),
206 pk_user: PublicKeyUser {
207 pk: params.gen1() * sk_user.sk,
208 },
209 };
210
211 let witness = WithdrawalReqWitness {
212 private_attributes,
213 joined_commitment_opening: &joined_commitment_opening,
214 private_attributes_openings: &private_attributes_openings,
215 };
216 let zk_proof = WithdrawalReqProof::construct(&instance, &witness);
217
218 // Create and return WithdrawalRequest and RequestInfo
219 Ok((
220 WithdrawalRequest {
221 joined_commitment_hash,
222 joined_commitment,
223 private_attributes_commitments,
224 zk_proof,
225 },
226 RequestInfo {
227 joined_commitment_hash,
228 joined_commitment_opening,
229 private_attributes_openings,
230 wallet_secret: v,
231 expiration_date,
232 t_type,
233 },
234 ))
235}
236
237/// Verifies the integrity of a withdrawal request, including the joined commitment hash
238/// and the zero-knowledge proof of knowledge.
239///
240/// # Arguments
241///
242/// * `req` - The withdrawal request to be verified.
243/// * `pk_user` - Public key of the user associated with the withdrawal request.
244/// * `expiration_date` - Expiration date for the ticket book.
245/// * `t_type` - The type of the ticket book
246///
247/// # Returns
248///
249/// Returns `Ok(true)` if the verification is successful, otherwise returns an error
250/// with a specific message indicating the verification failure.
251pub fn request_verify(
252 req: &WithdrawalRequest,
253 pk_user: PublicKeyUser,
254 expiration_date: EncodedDate,
255 t_type: EncodedTicketType,
256) -> Result<()> {
257 let params = ecash_group_parameters();
258
259 let gamma = params.gammas();
260 let expiration_date = date_scalar(expiration_date);
261 let t_type = type_scalar(t_type);
262
263 if bool::from(req.joined_commitment_hash.is_identity()) {
264 return Err(CompactEcashError::IdentityCommitmentHash);
265 }
266
267 let expected_commitment_hash = hash_g1(
268 (req.joined_commitment + gamma[2] * expiration_date + gamma[3] * t_type).to_bytes(),
269 );
270 if req.joined_commitment_hash != expected_commitment_hash {
271 return Err(CompactEcashError::WithdrawalRequestVerification);
272 }
273 // Verify zk proof
274 let instance = WithdrawalReqInstance {
275 joined_commitment: req.joined_commitment,
276 joined_commitment_hash: req.joined_commitment_hash,
277 private_attributes_commitments: req.private_attributes_commitments.clone(),
278 pk_user,
279 };
280 if !req.zk_proof.verify(&instance) {
281 return Err(CompactEcashError::WithdrawalRequestVerification);
282 }
283 Ok(())
284}
285
286/// Signs an expiration date using a joined commitment hash and a secret key.
287///
288/// Given a joined commitment hash (`joined_commitment_hash`), an expiration date (`expiration_date`),
289/// and a secret key for authentication (`sk_auth`), this function computes the signature of the
290/// expiration date by multiplying the commitment hash with the blinding factor derived from the secret key
291/// and the expiration date.
292///
293/// # Arguments
294///
295/// * `joined_commitment_hash` - The G1Projective point representing the joined commitment hash.
296/// * `expiration_date` - The expiration date timestamp to be signed.
297/// * `sk_auth` - The secret key of the signing authority. Assumes key is long enough.
298///
299/// # Returns
300///
301/// A `Result` containing the resulting G1Projective point if successful, or an error if the
302/// authentication secret key index is out of bounds.
303fn sign_expiration_date(
304 joined_commitment_hash: &G1Projective,
305 expiration_date: EncodedDate,
306 sk_auth: &SecretKeyAuth,
307) -> G1Projective {
308 joined_commitment_hash * (sk_auth.ys[2] * date_scalar(expiration_date))
309}
310
311/// Signs a transaction type using a joined commitment hash and a secret key.
312///
313/// Given a joined commitment hash (`joined_commitment_hash`), a ticket type (`t_type`),
314/// and a secret key for authentication (`sk_auth`), this function computes the signature of the
315/// ticket type.
316///
317/// # Arguments
318///
319/// * `joined_commitment_hash` - The G1Projective point representing the joined commitment hash.
320/// * `t_type` - The ticket type identifier to be signed.
321/// * `sk_auth` - The secret key of the signing authority.
322///
323/// # Returns
324///
325/// The resulting G1Projective point representing the signed ticket type.
326fn sign_t_type(
327 joined_commitment_hash: &G1Projective,
328 t_type: EncodedTicketType,
329 sk_auth: &SecretKeyAuth,
330) -> G1Projective {
331 joined_commitment_hash * (sk_auth.ys[3] * type_scalar(t_type))
332}
333
334/// Issues a blinded signature for a withdrawal request, after verifying its integrity.
335///
336/// This function first verifies the withdrawal request using the provided group parameters,
337/// user's public key, and expiration date. If the verification is successful,
338/// the function proceeds to blind sign the private attributes and sign the expiration date,
339/// combining both signatures into a final signature.
340///
341/// # Arguments
342///
343/// * `sk_auth` - Secret key of the signing authority.
344/// * `pk_user` - Public key of the user associated with the withdrawal request.
345/// * `withdrawal_req` - The withdrawal request to be signed.
346/// * `expiration_date` - Expiration date for the withdrawal request.
347///
348/// # Returns
349///
350/// Returns a `BlindedSignature` if the issuance process is successful, otherwise returns an error
351/// with a specific message indicating the failure.
352pub fn issue(
353 sk_auth: &SecretKeyAuth,
354 pk_user: PublicKeyUser,
355 withdrawal_req: &WithdrawalRequest,
356 expiration_date: EncodedDate,
357 t_type: EncodedTicketType,
358) -> Result<BlindedSignature> {
359 // Verify the withdrawal request
360 request_verify(withdrawal_req, pk_user, expiration_date, t_type)?;
361 // Verify `sk_auth` is long enough
362 if sk_auth.ys.len() < constants::ATTRIBUTES_LEN {
363 return Err(CompactEcashError::KeyTooShort);
364 }
365 // Blind sign the private attributes
366 let blind_signatures: G1Projective = withdrawal_req
367 .private_attributes_commitments
368 .iter()
369 .zip(sk_auth.ys.iter().take(2))
370 .map(|(pc, yi)| pc * yi)
371 .sum();
372 // Sign the expiration date
373 //SAFETY: key length was verified before
374 let expiration_date_sign = sign_expiration_date(
375 &withdrawal_req.joined_commitment_hash,
376 expiration_date,
377 sk_auth,
378 );
379 // Sign the type
380 let t_type_sign = sign_t_type(&withdrawal_req.joined_commitment_hash, t_type, sk_auth);
381 // Combine both signatures
382 let signature = blind_signatures
383 + withdrawal_req.joined_commitment_hash * sk_auth.x
384 + expiration_date_sign
385 + t_type_sign;
386
387 Ok(BlindedSignature {
388 h: withdrawal_req.joined_commitment_hash,
389 c: signature,
390 })
391}
392
393/// Verifies the integrity and correctness of a blinded signature
394/// and returns an unblinded partial zk-nym wallet.
395///
396/// This function first verifies the integrity of the received blinded signature by checking
397/// if the joined commitment hash matches the one provided in the `req_info`. If the verification
398/// is successful, it proceeds to unblind the blinded signature and verify its correctness.
399///
400/// # Arguments
401///
402/// * `vk_auth` - Verification key of the signing authority.
403/// * `sk_user` - Secret key of the user.
404/// * `blind_signature` - Blinded signature received from the authority.
405/// * `req_info` - Information associated with the request, including the joined commitment hash,
406/// private attributes openings, v, and expiration date.
407///
408/// # Returns
409///
410/// Returns a `PartialWallet` if the verification process is successful, otherwise returns an error
411/// with a specific message indicating the failure.
412pub fn issue_verify(
413 vk_auth: &VerificationKeyAuth,
414 sk_user: &SecretKeyUser,
415 blind_signature: &BlindedSignature,
416 req_info: &RequestInfo,
417 signer_index: SignerIndex,
418) -> Result<PartialWallet> {
419 let params = ecash_group_parameters();
420 // Verify the integrity of the response from the authority
421 if req_info.joined_commitment_hash != blind_signature.h {
422 return Err(CompactEcashError::IssuanceVerification);
423 }
424 if bool::from(blind_signature.h.is_identity()) {
425 return Err(CompactEcashError::IdentitySignature);
426 }
427
428 // Unblind the blinded signature on the partial signature
429 let blinding_removers = vk_auth
430 .beta_g1
431 .iter()
432 .zip(&req_info.private_attributes_openings)
433 .map(|(beta, opening)| beta * opening)
434 .sum::<G1Projective>();
435 let unblinded_c = blind_signature.c - blinding_removers;
436
437 let attr = [
438 sk_user.sk,
439 req_info.wallet_secret,
440 req_info.expiration_date,
441 req_info.t_type,
442 ];
443
444 let signed_attributes = attr
445 .iter()
446 .zip(vk_auth.beta_g2.iter())
447 .map(|(attr, beta_i)| beta_i * attr)
448 .sum::<G2Projective>();
449
450 // Verify the signature correctness on the wallet share
451 if !check_bilinear_pairing(
452 &blind_signature.h.to_affine(),
453 &G2Prepared::from((vk_auth.alpha + signed_attributes).to_affine()),
454 &unblinded_c.to_affine(),
455 params.prepared_miller_g2(),
456 ) {
457 return Err(CompactEcashError::IssuanceVerification);
458 }
459
460 Ok(PartialWallet {
461 sig: Signature {
462 h: blind_signature.h,
463 s: unblinded_c,
464 },
465 v: req_info.wallet_secret,
466 idx: signer_index,
467 expiration_date: req_info.expiration_date,
468 t_type: req_info.t_type,
469 })
470}
471
472/// Verifies a partial blind signature using the provided parameters and validator's verification key.
473///
474/// # Arguments
475///
476/// * `blind_sign_request` - A reference to the blind signature request signed by the client.
477/// * `public_attributes` - A reference to the public attributes included in the client's request.
478/// * `blind_sig` - A reference to the issued partial blinded signature to be verified.
479/// * `partial_verification_key` - A reference to the validator's partial verification key.
480///
481/// # Returns
482///
483/// A boolean indicating whether the partial blind signature is valid (`true`) or not (`false`).
484///
485/// # Remarks
486///
487/// This function verifies the correctness and validity of a partial blind signature using
488/// the provided cryptographic parameters, blind signature request, blinded signature,
489/// and partial verification key.
490/// It calculates pairings based on the provided values and checks whether the partial blind signature
491/// is consistent with the verification key and commitments in the blind signature request.
492/// The function returns `true` if the partial blind signature is valid, and `false` otherwise.
493pub fn verify_partial_blind_signature(
494 private_attribute_commitments: &[G1Projective],
495 public_attributes: &[&Attribute],
496 blind_sig: &BlindedSignature,
497 partial_verification_key: &VerificationKeyAuth,
498) -> bool {
499 let params = ecash_group_parameters();
500 let num_private_attributes = private_attribute_commitments.len();
501 if num_private_attributes + public_attributes.len() > partial_verification_key.beta_g2.len() {
502 return false;
503 }
504 // Note: This check is useful if someone uses the code of those functions
505 // to verify Pointcheval-Sanders signatures in a context different for their use
506 // in zk-nyms
507 if bool::from(blind_sig.h.is_identity()) {
508 return false;
509 }
510 // TODO: we're losing some memory here due to extra allocation,
511 // but worst-case scenario (given SANE amount of attributes), it's just few kb at most
512 let c_neg = blind_sig.c.to_affine().neg();
513 let g2_prep = params.prepared_miller_g2();
514
515 let mut terms = vec![
516 // (c^{-1}, g2)
517 (c_neg, g2_prep.clone()),
518 // (s, alpha)
519 (
520 blind_sig.h.to_affine(),
521 G2Prepared::from(partial_verification_key.alpha.to_affine()),
522 ),
523 ];
524
525 // for each private attribute, add (cm_i, beta_i) to the miller terms
526 for (private_attr_commit, beta_g2) in private_attribute_commitments
527 .iter()
528 .zip(&partial_verification_key.beta_g2)
529 {
530 // (cm_i, beta_i)
531 terms.push((
532 private_attr_commit.to_affine(),
533 G2Prepared::from(beta_g2.to_affine()),
534 ))
535 }
536
537 // for each public attribute, add (s^pub_j, beta_{priv + j}) to the miller terms
538 for (&pub_attr, beta_g2) in public_attributes.iter().zip(
539 partial_verification_key
540 .beta_g2
541 .iter()
542 .skip(num_private_attributes),
543 ) {
544 // (s^pub_j, beta_j)
545 terms.push((
546 (blind_sig.h * pub_attr).to_affine(),
547 G2Prepared::from(beta_g2.to_affine()),
548 ))
549 }
550
551 // get the references to all the terms to get the arguments the miller loop expects
552 #[allow(clippy::map_identity)]
553 let terms_refs = terms.iter().map(|(g1, g2)| (g1, g2)).collect::<Vec<_>>();
554
555 // since checking whether e(a, b) == e(c, d)
556 // is equivalent to checking e(a, b) • e(c, d)^{-1} == id
557 // and thus to e(a, b) • e(c^{-1}, d) == id
558 //
559 // compute e(c^{-1}, g2) • e(s, alpha) • e(cm_0, beta_0) • e(cm_i, beta_i) • (s^pub_0, beta_{i+1}) (s^pub_j, beta_{i + j})
560 multi_miller_loop(&terms_refs)
561 .final_exponentiation()
562 .is_identity()
563 .into()
564}
565
566#[cfg(test)]
567mod tests {
568 use super::{generate_non_identity_h, verify_partial_blind_signature};
569 use crate::common_types::BlindedSignature;
570 use crate::ecash_group_parameters;
571 use crate::scheme::keygen::{SecretKeyUser, VerificationKeyAuth};
572 use nym_bls12_381_fork::G1Projective;
573
574 #[test]
575 fn test_generate_non_identity_h() {
576 let params = ecash_group_parameters();
577 // Create dummy values for testing
578 let sk_user = SecretKeyUser {
579 sk: params.random_scalar(),
580 };
581 let v = params.random_scalar();
582 let expiration_date = params.random_scalar();
583 let t_type = params.random_scalar();
584
585 // Generate the commitment and hash
586 let (_, joined_commitment_hash, _) =
587 generate_non_identity_h(params, &sk_user, &v, expiration_date, t_type);
588
589 // Ensure that the joined_commitment_hash is not the identity element
590 assert!(
591 !bool::from(joined_commitment_hash.is_identity()),
592 "Joined commitment hash should not be the identity element"
593 );
594 }
595
596 #[test]
597 fn test_verify_partial_blind_signature_blind_sig_identity() {
598 let params = ecash_group_parameters();
599 let private_attribute_commitments = vec![params.gen1() * params.random_scalar()];
600 let public_attributes = vec![];
601 // Create a blinded signature with h being the identity element
602 let blind_sig = BlindedSignature {
603 h: G1Projective::identity(),
604 c: params.gen1() * params.random_scalar(),
605 };
606 // Create a mock partial verification key
607 let partial_verification_key = VerificationKeyAuth {
608 alpha: params.gen2() * params.random_scalar(),
609 beta_g1: vec![params.gen1() * params.random_scalar()],
610 beta_g2: vec![params.gen2() * params.random_scalar()],
611 };
612
613 // Test with identity h, expecting false
614 assert!(
615 !verify_partial_blind_signature(
616 &private_attribute_commitments,
617 &public_attributes,
618 &blind_sig,
619 &partial_verification_key
620 ),
621 "Expected verification to return false for identity h in blind signature"
622 );
623 }
624}