Skip to main content

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}