nym_compact_ecash/scheme/
expiration_date_signatures.rs

1// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::common_types::{Signature, SignerIndex};
5use crate::error::{CompactEcashError, Result};
6use crate::helpers::date_scalar;
7use crate::scheme::keygen::{SecretKeyAuth, VerificationKeyAuth};
8use crate::utils::generate_lagrangian_coefficients_at_origin;
9use crate::utils::{batch_verify_signatures, hash_g1};
10use crate::{constants, EncodedDate};
11use itertools::Itertools;
12use nym_bls12_381_fork::{G1Projective, Scalar};
13use serde::{Deserialize, Serialize};
14use std::borrow::Borrow;
15
16/// A structure representing an expiration date signature.
17pub type ExpirationDateSignature = Signature;
18pub type PartialExpirationDateSignature = ExpirationDateSignature;
19
20#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
21pub struct AnnotatedExpirationDateSignature {
22    pub signature: ExpirationDateSignature,
23    pub expiration_timestamp: EncodedDate,
24    pub spending_timestamp: EncodedDate,
25}
26
27impl Borrow<ExpirationDateSignature> for AnnotatedExpirationDateSignature {
28    fn borrow(&self) -> &ExpirationDateSignature {
29        &self.signature
30    }
31}
32
33impl From<AnnotatedExpirationDateSignature> for ExpirationDateSignature {
34    fn from(value: AnnotatedExpirationDateSignature) -> Self {
35        value.signature
36    }
37}
38
39pub struct ExpirationDateSignatureShare<B = PartialExpirationDateSignature>
40where
41    B: Borrow<PartialExpirationDateSignature> + Send + Sync,
42{
43    pub index: SignerIndex,
44    pub key: VerificationKeyAuth,
45    pub signatures: Vec<B>,
46}
47
48/// Signs given expiration date for a specified validity period using the given secret key of a single authority.
49///
50/// # Arguments
51///
52/// * `params` - The cryptographic parameters used in the signing process.
53/// * `sk_auth` - The secret key of the signing authority.
54/// * `expiration_unix_timestamp` - The expiration date for which signatures will be generated (as unix timestamp).
55///
56/// # Returns
57///
58/// A vector containing partial signatures for each date within the validity period (i.e.,
59/// from expiration_date - CRED_VALIDITY_PERIOD till expiration_date.
60///
61/// # Note
62///
63/// This function is executed by a single singing authority and generates partial expiration date
64/// signatures for a specified validity period. Each signature is created by combining cryptographic
65/// attributes derived from the expiration date, and the resulting vector contains signatures for
66/// each date within the defined validity period till expiration date.
67/// The validity period is determined by the constant `CRED_VALIDITY_PERIOD` in the `constants` module.
68pub fn sign_expiration_date(
69    sk_auth: &SecretKeyAuth,
70    expiration_unix_timestamp: EncodedDate,
71) -> Result<Vec<AnnotatedExpirationDateSignature>> {
72    if sk_auth.ys.len() < 3 {
73        return Err(CompactEcashError::KeyTooShort);
74    }
75    let m0: Scalar = date_scalar(expiration_unix_timestamp);
76    let m2: Scalar = constants::TYPE_EXP;
77
78    let partial_s_exponent = sk_auth.x + sk_auth.ys[0] * m0 + sk_auth.ys[2] * m2;
79
80    let sign_expiration = |offset: u32| {
81        // we produce tuples of (assuming CRED_VALIDITY_PERIOD_DAYS = 30):
82        // (expiration, expiration - 29)
83        // (expiration, expiration - 28)
84        // ...
85        // (expiration, expiration)
86        let spending_unix_timestamp = expiration_unix_timestamp
87            - ((constants::CRED_VALIDITY_PERIOD_DAYS - offset - 1) * constants::SECONDS_PER_DAY);
88        let m1: Scalar = date_scalar(spending_unix_timestamp);
89        // Compute the hash
90        let h = hash_g1([m0.to_bytes(), m1.to_bytes()].concat());
91        // Sign the attributes by performing scalar-point multiplications and accumulating the result
92        let s_exponent = partial_s_exponent + sk_auth.ys[1] * m1;
93
94        // Create the signature struct on the expiration date
95        let signature = PartialExpirationDateSignature {
96            h,
97            s: h * s_exponent,
98        };
99
100        AnnotatedExpirationDateSignature {
101            signature,
102            expiration_timestamp: expiration_unix_timestamp,
103            spending_timestamp: spending_unix_timestamp,
104        }
105    };
106
107    cfg_if::cfg_if! {
108        if #[cfg(feature = "par_signing")] {
109            use rayon::prelude::*;
110
111            Ok((0..constants::CRED_VALIDITY_PERIOD_DAYS)
112                .into_par_iter()
113                .map(sign_expiration)
114                .collect())
115        } else {
116           Ok((0..constants::CRED_VALIDITY_PERIOD_DAYS).map(sign_expiration).collect())
117        }
118    }
119}
120
121/// Verifies the expiration date signatures against the given verification key.
122///
123/// This function iterates over the provided valid date signatures and verifies each one
124/// against the provided verification key. It computes the hash and checks the correctness of the
125/// signature using bilinear pairings.
126///
127/// # Arguments
128///
129/// * `vkey` - The verification key of the signing authority.
130/// * `signatures` - The list of date signatures to be verified.
131/// * `expiration_date` - The expiration date for which signatures are being issued (as unix timestamp).
132///
133/// # Returns
134///
135/// Returns `Ok(true)` if all signatures are verified successfully, otherwise returns an error
136///
137pub fn verify_valid_dates_signatures<B>(
138    vk: &VerificationKeyAuth,
139    signatures: &[B],
140    expiration_date: EncodedDate,
141) -> Result<()>
142where
143    B: Borrow<ExpirationDateSignature>,
144{
145    let m0: Scalar = date_scalar(expiration_date);
146    let m2: Scalar = constants::TYPE_EXP;
147
148    let partially_signed = vk.alpha + vk.beta_g2[0] * m0 + vk.beta_g2[2] * m2;
149    let mut pairing_terms = Vec::with_capacity(signatures.len());
150
151    for (i, sig) in signatures.iter().enumerate() {
152        let l = i as u32;
153        let valid_date = expiration_date
154            - ((constants::CRED_VALIDITY_PERIOD_DAYS - l - 1) * constants::SECONDS_PER_DAY);
155        let m1: Scalar = date_scalar(valid_date);
156
157        // Compute the hash
158        let h = hash_g1([m0.to_bytes(), m1.to_bytes()].concat());
159
160        let sig = *sig.borrow();
161        // Check if the hash is matching
162        if sig.h != h {
163            return Err(CompactEcashError::ExpirationDateSignatureVerification);
164        }
165
166        // let partially_signed_attributes = partially_signed + vk.beta_g2[1] * m1;
167        pairing_terms.push((sig, partially_signed + vk.beta_g2[1] * m1));
168    }
169
170    if !batch_verify_signatures(pairing_terms.iter()) {
171        return Err(CompactEcashError::ExpirationDateSignatureVerification);
172    }
173    Ok(())
174}
175
176/// Aggregates partial expiration date signatures into a list of aggregated expiration date signatures.
177///
178/// # Arguments
179///
180/// * `vk_auth` - The global verification key.
181/// * `expiration_date` - The expiration date for which the signatures are being aggregated (as unix timestamp).
182/// * `signatures_shares` - A list of tuples containing unique indices, verification keys, and partial expiration date signatures corresponding to the signing authorities.
183///
184/// # Returns
185///
186/// A `Result` containing a vector of `ExpirationDateSignature` if the aggregation is successful,
187/// or an `Err` variant with a description of the encountered error.
188///
189/// # Errors
190///
191/// This function returns an error if there is a mismatch in the lengths of `signatures`. This occurs
192/// when the number of tuples in `signatures` is not equal to the expected number of signing authorities.
193/// Each tuple should contain a unique index, a verification key, and a list of partial signatures.
194///
195/// It also returns an error if there are not enough unique indices. This happens when the number
196/// of unique indices in the tuples is less than the total number of signing authorities.
197///
198/// Additionally, an error is returned if the verification of the partial or aggregated signatures fails.
199/// This can occur if the cryptographic verification process fails for any of the provided signatures.
200///
201fn _aggregate_expiration_signatures<B>(
202    vk: &VerificationKeyAuth,
203    expiration_date: EncodedDate,
204    signatures_shares: &[ExpirationDateSignatureShare<B>],
205    validate_shares: bool,
206) -> Result<Vec<ExpirationDateSignature>>
207where
208    B: Borrow<ExpirationDateSignature> + Send + Sync,
209{
210    // Check if all indices are unique
211    if signatures_shares
212        .iter()
213        .map(|share| share.index)
214        .unique()
215        .count()
216        != signatures_shares.len()
217    {
218        return Err(CompactEcashError::AggregationDuplicateIndices);
219    }
220
221    // Evaluate at 0 the Lagrange basis polynomials k_i
222    let coefficients = generate_lagrangian_coefficients_at_origin(
223        &signatures_shares
224            .iter()
225            .map(|share| share.index)
226            .collect::<Vec<_>>(),
227    );
228
229    // Verify that all signatures are valid
230    if validate_shares {
231        cfg_if::cfg_if! {
232            if #[cfg(feature = "par_verify")] {
233                use rayon::prelude::*;
234
235                signatures_shares.par_iter().try_for_each(|share| {
236                    verify_valid_dates_signatures(&share.key, &share.signatures, expiration_date)
237                })?;
238            } else {
239                signatures_shares.iter().try_for_each(|share| verify_valid_dates_signatures(&share.key, &share.signatures, expiration_date))?;
240            }
241        }
242    }
243
244    // Pre-allocate vectors
245    let mut aggregated_date_signatures: Vec<ExpirationDateSignature> =
246        Vec::with_capacity(constants::CRED_VALIDITY_PERIOD_DAYS as usize);
247
248    let m0: Scalar = date_scalar(expiration_date);
249
250    for l in 0..constants::CRED_VALIDITY_PERIOD_DAYS {
251        let valid_date = expiration_date
252            - ((constants::CRED_VALIDITY_PERIOD_DAYS - l - 1) * constants::SECONDS_PER_DAY);
253        let m1: Scalar = date_scalar(valid_date);
254        // Compute the hash
255        let h = hash_g1([m0.to_bytes(), m1.to_bytes()].concat());
256
257        // Collect the partial signatures for the same valid date
258        let collected_at_l: Vec<_> = signatures_shares
259            .iter()
260            .filter_map(|share| share.signatures.get(l as usize))
261            .collect();
262
263        // Aggregate partial signatures for each validity date
264        let aggr_s: G1Projective = coefficients
265            .iter()
266            .zip(collected_at_l.iter())
267            .map(|(coeff, &sig)| sig.borrow().s * coeff)
268            .sum();
269        let aggr_sig = ExpirationDateSignature { h, s: aggr_s };
270        aggregated_date_signatures.push(aggr_sig);
271    }
272    verify_valid_dates_signatures(vk, &aggregated_date_signatures, expiration_date)?;
273    Ok(aggregated_date_signatures)
274}
275
276/// Aggregates partial expiration date signatures into a list of aggregated expiration date signatures.
277///
278/// # Arguments
279///
280/// * `vk_auth` - The global verification key.
281/// * `expiration_date` - The expiration date for which the signatures are being aggregated (as unix timestamp).
282/// * `signatures_shares` - A list of tuples containing unique indices, verification keys, and partial expiration date signatures corresponding to the signing authorities.
283///
284/// # Returns
285///
286/// A `Result` containing a vector of `ExpirationDateSignature` if the aggregation is successful,
287/// or an `Err` variant with a description of the encountered error.
288///
289/// # Errors
290///
291/// This function returns an error if there is a mismatch in the lengths of `signatures`. This occurs
292/// when the number of tuples in `signatures` is not equal to the expected number of signing authorities.
293/// Each tuple should contain a unique index, a verification key, and a list of partial signatures.
294///
295/// It also returns an error if there are not enough unique indices. This happens when the number
296/// of unique indices in the tuples is less than the total number of signing authorities.
297///
298/// Additionally, an error is returned if the verification of the partial or aggregated signatures fails.
299/// This can occur if the cryptographic verification process fails for any of the provided signatures.
300///
301pub fn aggregate_expiration_signatures<B>(
302    vk: &VerificationKeyAuth,
303    expiration_date: EncodedDate,
304    signatures_shares: &[ExpirationDateSignatureShare<B>],
305) -> Result<Vec<ExpirationDateSignature>>
306where
307    B: Borrow<PartialExpirationDateSignature> + Send + Sync,
308{
309    _aggregate_expiration_signatures(vk, expiration_date, signatures_shares, true)
310}
311
312/// Perform aggregation and verification of partial expiration date signatures with
313/// an additional check ensuring correct ordering of provided shares
314///
315/// It further annotates the result with timestamp information
316pub fn aggregate_annotated_expiration_signatures(
317    vk: &VerificationKeyAuth,
318    expiration_date: EncodedDate,
319    signatures_shares: &[ExpirationDateSignatureShare<AnnotatedExpirationDateSignature>],
320) -> Result<Vec<AnnotatedExpirationDateSignature>> {
321    // it's sufficient to just verify the first share as if the rest of them don't match,
322    // the aggregation will fail anyway
323    let Some(share) = signatures_shares.first() else {
324        return Ok(Vec::new());
325    };
326
327    if share.signatures.len() != constants::CRED_VALIDITY_PERIOD_DAYS as usize {
328        return Err(CompactEcashError::ExpirationDateSignatureVerification);
329    }
330
331    for (i, sig) in share.signatures.iter().enumerate() {
332        if sig.expiration_timestamp != expiration_date {
333            return Err(CompactEcashError::ExpirationDateSignatureVerification);
334        }
335
336        let l = i as u32;
337        let expected_spending = sig.expiration_timestamp
338            - ((constants::CRED_VALIDITY_PERIOD_DAYS - l - 1) * constants::SECONDS_PER_DAY);
339
340        if sig.spending_timestamp != expected_spending {
341            return Err(CompactEcashError::ExpirationDateSignatureVerification);
342        }
343    }
344
345    let aggregated = aggregate_expiration_signatures(vk, expiration_date, signatures_shares)?;
346    assert_eq!(aggregated.len(), share.signatures.len());
347
348    Ok(aggregated
349        .into_iter()
350        .zip(share.signatures.iter())
351        .map(|(signature, sh)| AnnotatedExpirationDateSignature {
352            signature,
353            expiration_timestamp: sh.expiration_timestamp,
354            spending_timestamp: sh.spending_timestamp,
355        })
356        .collect())
357}
358
359/// An unchecked variant of `aggregate_expiration_signatures` that does not perform
360/// validation of intermediate signatures.
361///
362/// It is expected the caller has already pre-validated them via manual calls to `verify_valid_dates_signatures`
363pub fn unchecked_aggregate_expiration_signatures(
364    vk: &VerificationKeyAuth,
365    expiration_date: EncodedDate,
366    signatures_shares: &[ExpirationDateSignatureShare],
367) -> Result<Vec<ExpirationDateSignature>> {
368    _aggregate_expiration_signatures(vk, expiration_date, signatures_shares, false)
369}
370
371/// Finds the index corresponding to the given spend date based on the expiration date.
372///
373/// This function calculates the index such that the following equality holds:
374/// `spend_date = expiration_date - 30 + index`
375/// This index is used to retrieve a corresponding signature.
376///
377/// # Arguments
378///
379/// * `spend_date` - The spend date for which to find the index.
380/// * `expiration_date` - The expiration date used in the calculation.
381///
382/// # Returns
383///
384/// If a valid index is found, returns `Ok(index)`. If no valid index is found
385/// (i.e., `spend_date` is earlier than `expiration_date - 30`), returns `Err(InvalidDateError)`.
386///
387pub fn find_index(spend_date: EncodedDate, expiration_date: EncodedDate) -> Result<usize> {
388    let start_date =
389        expiration_date - ((constants::CRED_VALIDITY_PERIOD_DAYS - 1) * constants::SECONDS_PER_DAY);
390
391    if spend_date >= start_date {
392        let index_a = ((spend_date - start_date) / constants::SECONDS_PER_DAY) as usize;
393        if index_a as u32 >= constants::CRED_VALIDITY_PERIOD_DAYS {
394            Err(CompactEcashError::SpendDateTooLate)
395        } else {
396            Ok(index_a)
397        }
398    } else {
399        Err(CompactEcashError::SpendDateTooEarly)
400    }
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406    use crate::scheme::aggregation::aggregate_verification_keys;
407    use crate::scheme::keygen::ttp_keygen;
408
409    #[test]
410    fn test_find_index() {
411        let expiration_date = 1701993600; // Dec 8 2023
412        for i in 0..constants::CRED_VALIDITY_PERIOD_DAYS {
413            let current_spend_date = expiration_date - i * 86400;
414            assert_eq!(
415                find_index(current_spend_date, expiration_date).unwrap(),
416                (constants::CRED_VALIDITY_PERIOD_DAYS - 1 - i) as usize
417            )
418        }
419
420        let late_spend_date = expiration_date + 86400;
421        assert!(find_index(late_spend_date, expiration_date).is_err());
422
423        let early_spend_date = expiration_date - (constants::CRED_VALIDITY_PERIOD_DAYS) * 86400;
424        assert!(find_index(early_spend_date, expiration_date).is_err());
425    }
426
427    #[test]
428    fn test_sign_expiration_date() {
429        let expiration_date = 1702050209; // Dec 8 2023
430
431        let authorities_keys = ttp_keygen(2, 3).unwrap();
432        let sk_i_auth = authorities_keys[0].secret_key();
433        let vk_i_auth = authorities_keys[0].verification_key();
434        let partial_exp_sig = sign_expiration_date(sk_i_auth, expiration_date).unwrap();
435
436        assert!(
437            verify_valid_dates_signatures(&vk_i_auth, &partial_exp_sig, expiration_date).is_ok()
438        );
439    }
440
441    #[test]
442    fn test_aggregate_expiration_signatures() {
443        let expiration_date = 1702050209; // Dec 8 2023
444
445        let authorities_keypairs = ttp_keygen(2, 3).unwrap();
446        let indices: [u64; 3] = [1, 2, 3];
447        // list of secret keys of each authority
448        let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs
449            .iter()
450            .map(|keypair| keypair.secret_key())
451            .collect();
452        // list of verification keys of each authority
453        let verification_keys_auth: Vec<VerificationKeyAuth> = authorities_keypairs
454            .iter()
455            .map(|keypair| keypair.verification_key())
456            .collect();
457        // the global master verification key
458        let verification_key =
459            aggregate_verification_keys(&verification_keys_auth, Some(&indices)).unwrap();
460
461        let mut edt_partial_signatures: Vec<Vec<_>> =
462            Vec::with_capacity(constants::CRED_VALIDITY_PERIOD_DAYS as usize);
463        for sk_auth in secret_keys_authorities.iter() {
464            let sign = sign_expiration_date(sk_auth, expiration_date).unwrap();
465            edt_partial_signatures.push(sign);
466        }
467
468        let combined_data = indices
469            .iter()
470            .zip(
471                verification_keys_auth
472                    .iter()
473                    .zip(edt_partial_signatures.iter()),
474            )
475            .map(|(i, (vk, sigs))| ExpirationDateSignatureShare {
476                index: *i,
477                key: vk.clone(),
478                signatures: sigs.clone(),
479            })
480            .collect::<Vec<_>>();
481
482        assert!(aggregate_expiration_signatures(
483            &verification_key,
484            expiration_date,
485            &combined_data,
486        )
487        .is_ok());
488    }
489}