Skip to main content

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