Skip to main content

lib_q_zkp/ip/
credential.rs

1//! IP Credential Module - Selective attribute disclosure
2//!
3//! This module provides functions for selective disclosure of credential
4//! attributes in an Identity Protocol, allowing users to reveal only specific attributes
5//! while keeping others secret.
6
7extern crate alloc;
8
9use alloc::string::ToString;
10use alloc::vec::Vec;
11use alloc::{
12    format,
13    vec,
14};
15
16use lib_q_core::Result;
17
18use crate::ZkpProof;
19use crate::air::credential::attr_to_left_right;
20use crate::air::{
21    CredentialAir,
22    CredentialInput,
23    CredentialSchema,
24    TraceGenerator,
25};
26use crate::stark::{
27    StarkProver,
28    StarkVerifier,
29    default_config,
30};
31
32/// IP credential structure
33#[derive(Debug, Clone)]
34pub struct IpCredential {
35    /// All credential attributes
36    pub attributes: Vec<Vec<u8>>,
37}
38
39/// Prove credential attributes with selective disclosure
40///
41/// This generates a zero-knowledge proof that the prover knows a credential
42/// with specific attributes, revealing only those attributes marked in
43/// `reveal_mask`.
44///
45/// # Arguments
46///
47/// * `credential` - The credential with all attributes
48/// * `reveal_mask` - Boolean mask indicating which attributes to reveal
49///
50/// # Returns
51///
52/// A zero-knowledge proof of credential attributes
53///
54/// # Example
55///
56/// ```rust,ignore
57/// use lib_q_zkp::ip::credential::{prove_credential_attributes, IpCredential};
58///
59/// let credential = IpCredential {
60///     attributes: vec![
61///         b"name".to_vec(),
62///         b"age".to_vec(),
63///         b"ssn".to_vec(),
64///     ],
65/// };
66/// let reveal_mask = vec![true, true, false]; // Reveal name and age, hide SSN
67/// let proof = prove_credential_attributes(&credential, &reveal_mask)?;
68/// ```
69pub fn prove_credential_attributes(
70    credential: &IpCredential,
71    reveal_mask: &[bool],
72) -> Result<ZkpProof> {
73    if credential.attributes.is_empty() {
74        return Err(lib_q_core::Error::InvalidState {
75            operation: "prove_credential_attributes".to_string(),
76            reason: "Credential must have at least one attribute".to_string(),
77        });
78    }
79
80    if reveal_mask.len() != credential.attributes.len() {
81        return Err(lib_q_core::Error::InvalidState {
82            operation: "prove_credential_attributes".to_string(),
83            reason: format!(
84                "Reveal mask length {} must match credential attributes {}",
85                reveal_mask.len(),
86                credential.attributes.len()
87            ),
88        });
89    }
90
91    // Create schema from attribute sizes
92    let attribute_sizes: Vec<usize> = credential
93        .attributes
94        .iter()
95        .map(|attr| attr.len())
96        .collect();
97
98    let schema =
99        CredentialSchema::new(attribute_sizes).map_err(|e| lib_q_core::Error::InternalError {
100            operation: "prove_credential_attributes".to_string(),
101            details: e.to_string(),
102        })?;
103
104    // Create credential AIR
105    let air = CredentialAir::new(schema, reveal_mask.to_vec()).map_err(|e| {
106        lib_q_core::Error::InternalError {
107            operation: "prove_credential_attributes".to_string(),
108            details: e.to_string(),
109        }
110    })?;
111
112    // Create input
113    let input = CredentialInput {
114        attributes: credential.attributes.clone(),
115    };
116
117    // Generate trace
118    let trace = air
119        .generate_trace(&input)
120        .map_err(|e| lib_q_core::Error::InternalError {
121            operation: "prove_credential_attributes".to_string(),
122            details: e.to_string(),
123        })?;
124
125    // Get public values (commitment + revealed attributes)
126    let public_values = air.public_values(&input);
127
128    // Generate STARK proof
129    let config = default_config();
130    let prover = StarkProver::new(config);
131    let stark_proof = prover.prove(&air, trace, &public_values).map_err(|e| {
132        lib_q_core::Error::InternalError {
133            operation: "STARK proof generation".to_string(),
134            details: e.to_string(),
135        }
136    })?;
137
138    // Create ZkpProof with credential-specific metadata
139    let metadata = crate::ProofMetadata::Credential {
140        attribute_sizes: credential
141            .attributes
142            .iter()
143            .map(|a| a.len().min(u16::MAX as usize) as u16)
144            .collect(),
145        reveal_mask: reveal_mask.to_vec(),
146    };
147    ZkpProof::from_stark_proof(&stark_proof, metadata)
148}
149
150/// Compute the credential commitment from all attributes.
151///
152/// This produces the same commitment that the prover embeds in the proof,
153/// allowing a verifier (or issuer) to derive `expected_commitment` from the
154/// full attribute set without needing the proof itself.
155///
156/// The commitment is computed by Poseidon-hashing each attribute individually,
157/// then aggregating all per-attribute hashes via an iterated Poseidon chain.
158///
159/// Returns 8 bytes: 4 LE bytes for the real part, 4 LE bytes for the imaginary
160/// part of the `Complex<Mersenne31>` commitment field element. Pass these bytes
161/// to `verify_credential_proof` as `expected_commitment`.
162pub fn compute_credential_commitment(attributes: &[Vec<u8>]) -> Result<Vec<u8>> {
163    if attributes.is_empty() {
164        return Err(lib_q_core::Error::InvalidState {
165            operation: "compute_credential_commitment".to_string(),
166            reason: "Credential must have at least one attribute".to_string(),
167        });
168    }
169
170    use lib_q_poseidon::PoseidonField;
171
172    use crate::air::merkle_inclusion::compute_poseidon_with_intermediates;
173
174    let n = attributes.len();
175    let mut attr_hashes: Vec<PoseidonField> = Vec::with_capacity(n);
176    for attr in attributes {
177        let (left, right) = attr_to_left_right(attr);
178        let input_vec = vec![left, right];
179        let (hash_out, _) = compute_poseidon_with_intermediates(&input_vec);
180        attr_hashes.push(hash_out);
181    }
182
183    let commitment_field = if n == 1 {
184        attr_hashes[0]
185    } else {
186        let mut running = attr_hashes[0];
187        for right in attr_hashes.iter().take(n).skip(1) {
188            let input_vec = vec![running, *right];
189            let (hash_out, _) = compute_poseidon_with_intermediates(&input_vec);
190            running = hash_out;
191        }
192        running
193    };
194
195    Ok(commitment_field_to_bytes(commitment_field))
196}
197
198/// Serialize a `Complex<Mersenne31>` to 8 LE bytes (4 real + 4 imag).
199fn commitment_field_to_bytes(f: lib_q_poseidon::PoseidonField) -> Vec<u8> {
200    use lib_q_stark_field::{
201        BasedVectorSpace,
202        PrimeField32,
203    };
204    use lib_q_stark_mersenne31::Mersenne31;
205    let coords: &[Mersenne31] = f.as_basis_coefficients_slice();
206    let mut bytes = Vec::with_capacity(8);
207    bytes.extend_from_slice(&coords[0].as_canonical_u32().to_le_bytes());
208    bytes.extend_from_slice(&coords[1].as_canonical_u32().to_le_bytes());
209    bytes
210}
211
212/// Deserialize 8 LE bytes into a `Complex<Mersenne31>`.
213fn commitment_field_from_bytes(bytes: &[u8]) -> Option<lib_q_poseidon::PoseidonField> {
214    use lib_q_stark_field::extension::Complex;
215    use lib_q_stark_mersenne31::Mersenne31;
216    if bytes.len() < 8 {
217        return None;
218    }
219    let real = Mersenne31::new(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]));
220    let imag = Mersenne31::new(u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]));
221    Some(Complex::new_complex(real, imag))
222}
223
224/// Verify a credential proof
225///
226/// This verifies that a proof demonstrates knowledge of a credential
227/// with the expected commitment and revealed attributes.
228///
229/// # Arguments
230///
231/// * `proof` - The proof to verify
232/// * `expected_commitment` - The expected credential commitment
233/// * `revealed_attributes` - The revealed attribute values
234///
235/// # Returns
236///
237/// `Ok(true)` if the proof is valid, `Ok(false)` or `Err` otherwise
238pub fn verify_credential_proof(
239    proof: &ZkpProof,
240    expected_commitment: &[u8],
241    revealed_attributes: &[Vec<u8>],
242) -> Result<bool> {
243    if proof.proof_type != crate::ProofType::Stark {
244        return Ok(false);
245    }
246
247    if proof.data.is_empty() {
248        return Ok(false);
249    }
250
251    // Extract credential metadata
252    let crate::ProofMetadata::Credential {
253        attribute_sizes,
254        reveal_mask,
255    } = &proof.metadata
256    else {
257        return Ok(false);
258    };
259
260    // Validate metadata consistency
261    if attribute_sizes.len() != reveal_mask.len() {
262        return Ok(false);
263    }
264
265    if revealed_attributes.len() != reveal_mask.iter().filter(|&&r| r).count() {
266        return Ok(false);
267    }
268
269    // Reconstruct credential schema
270    let schema = CredentialSchema::new(attribute_sizes.iter().map(|&s| s as usize).collect())
271        .map_err(|e| lib_q_core::Error::InternalError {
272            operation: "verify_credential_proof".to_string(),
273            details: e.to_string(),
274        })?;
275
276    // Reconstruct AIR with the same reveal mask used during proving
277    let air = CredentialAir::new(schema, reveal_mask.clone()).map_err(|e| {
278        lib_q_core::Error::InternalError {
279            operation: "verify_credential_proof".to_string(),
280            details: e.to_string(),
281        }
282    })?;
283
284    use lib_q_stark_field::extension::Complex;
285    use lib_q_stark_mersenne31::Mersenne31;
286
287    use crate::air::poseidon_to_field;
288    type Val = Complex<Mersenne31>;
289
290    // Deserialize commitment from bytes (8 LE bytes → single Complex<Mersenne31>)
291    let commitment_poseidon =
292        commitment_field_from_bytes(expected_commitment).ok_or_else(|| {
293            lib_q_core::Error::InvalidState {
294                operation: "verify_credential_proof".to_string(),
295                reason: "expected_commitment must be at least 8 bytes".to_string(),
296            }
297        })?;
298    let mut public_values: Vec<Val> = vec![poseidon_to_field::<Val>(&commitment_poseidon)];
299
300    // Add revealed attribute hashes (same order as during proving)
301    let mut revealed_idx = 0;
302    for (attr_size, &revealed) in attribute_sizes.iter().zip(reveal_mask.iter()) {
303        if revealed {
304            if revealed_idx >= revealed_attributes.len() {
305                return Ok(false);
306            }
307            let attr = &revealed_attributes[revealed_idx];
308            if attr.len() > *attr_size as usize {
309                return Ok(false);
310            }
311            let (left, right) = attr_to_left_right(attr);
312            let input_vec = vec![left, right];
313            let (hash_out, _) =
314                crate::air::merkle_inclusion::compute_poseidon_with_intermediates(&input_vec);
315            public_values.push(poseidon_to_field::<Val>(&hash_out));
316            revealed_idx += 1;
317        }
318    }
319
320    // Deserialize STARK proof
321    let stark_proof = proof
322        .to_stark_proof()
323        .map_err(|_| lib_q_core::Error::InternalError {
324            operation: "verify_credential_proof".to_string(),
325            details: "Failed to deserialize STARK proof".to_string(),
326        })?;
327
328    // Verify the proof using the same AIR and public values
329    let config = default_config();
330    let verifier = StarkVerifier::new(config);
331
332    match verifier.verify(&air, &stark_proof, &public_values) {
333        Ok(()) => Ok(true),
334        Err(_) => Ok(false),
335    }
336}
337
338#[cfg(test)]
339mod tests {
340    use alloc::vec;
341
342    use super::*;
343    use crate::{
344        ProofMetadata,
345        ProofType,
346        ZkpProof,
347    };
348
349    #[test]
350    fn test_prove_credential_attributes() {
351        let credential = IpCredential {
352            attributes: vec![b"John Doe".to_vec(), b"30".to_vec()],
353        };
354        let reveal_mask = vec![true, false];
355        let result = prove_credential_attributes(&credential, &reveal_mask);
356        assert!(result.is_ok());
357    }
358
359    #[test]
360    fn test_compute_credential_commitment_empty_attributes() {
361        let result = compute_credential_commitment(&[]);
362        assert!(result.is_err());
363    }
364
365    #[test]
366    fn test_compute_credential_commitment_deterministic() {
367        let attrs = vec![b"Alice".to_vec(), b"42".to_vec()];
368        let c1 = compute_credential_commitment(&attrs).unwrap();
369        let c2 = compute_credential_commitment(&attrs).unwrap();
370        assert_eq!(c1, c2, "commitment must be deterministic");
371        assert_eq!(c1.len(), 8, "commitment is 8 bytes (Complex<Mersenne31>)");
372    }
373
374    #[test]
375    fn test_credential_prove_verify_roundtrip() {
376        let credential = IpCredential {
377            attributes: vec![b"Alice".to_vec(), b"42".to_vec(), b"secret".to_vec()],
378        };
379        let reveal_mask = vec![true, true, false];
380
381        let commitment = compute_credential_commitment(&credential.attributes).expect("commitment");
382        let proof = prove_credential_attributes(&credential, &reveal_mask).expect("prove");
383
384        let revealed = vec![b"Alice".to_vec(), b"42".to_vec()];
385        let result = verify_credential_proof(&proof, &commitment, &revealed)
386            .expect("verify should not error");
387        assert!(result, "valid credential proof must verify");
388    }
389
390    #[test]
391    fn test_credential_soundness_wrong_commitment() {
392        let credential = IpCredential {
393            attributes: vec![b"Alice".to_vec(), b"42".to_vec()],
394        };
395        let reveal_mask = vec![true, false];
396
397        let proof = prove_credential_attributes(&credential, &reveal_mask).expect("prove");
398
399        let wrong_commitment = vec![0u8; 8];
400        let revealed = vec![b"Alice".to_vec()];
401        let result = verify_credential_proof(&proof, &wrong_commitment, &revealed)
402            .expect("verify should not error");
403        assert!(!result, "wrong commitment must fail verification");
404    }
405
406    #[test]
407    fn test_credential_soundness_wrong_revealed_attribute() {
408        let credential = IpCredential {
409            attributes: vec![b"Alice".to_vec(), b"42".to_vec()],
410        };
411        let reveal_mask = vec![true, false];
412
413        let commitment = compute_credential_commitment(&credential.attributes).expect("commitment");
414        let proof = prove_credential_attributes(&credential, &reveal_mask).expect("prove");
415
416        let wrong_revealed = vec![b"Bob".to_vec()];
417        let result = verify_credential_proof(&proof, &commitment, &wrong_revealed)
418            .expect("verify should not error");
419        assert!(!result, "wrong revealed attribute must fail verification");
420    }
421
422    #[test]
423    fn test_prove_credential_attributes_rejects_empty_credential() {
424        let credential = IpCredential { attributes: vec![] };
425        let result = prove_credential_attributes(&credential, &[]);
426        assert!(result.is_err());
427    }
428
429    #[test]
430    fn test_prove_credential_attributes_rejects_reveal_mask_length_mismatch() {
431        let credential = IpCredential {
432            attributes: vec![b"A".to_vec(), b"B".to_vec()],
433        };
434        let result = prove_credential_attributes(&credential, &[true]);
435        assert!(result.is_err());
436    }
437
438    #[test]
439    fn test_verify_credential_proof_rejects_empty_data() {
440        let proof = ZkpProof {
441            data: vec![],
442            proof_type: ProofType::Stark,
443            security_level: 1,
444            metadata: ProofMetadata::Credential {
445                attribute_sizes: vec![1],
446                reveal_mask: vec![true],
447            },
448        };
449        let result = verify_credential_proof(&proof, &[0u8; 8], &[b"A".to_vec()]).unwrap();
450        assert!(!result);
451    }
452
453    #[test]
454    fn test_verify_credential_proof_rejects_non_credential_metadata() {
455        let proof = ZkpProof {
456            data: vec![1u8; 16],
457            proof_type: ProofType::Stark,
458            security_level: 1,
459            metadata: ProofMetadata::None,
460        };
461        let result = verify_credential_proof(&proof, &[0u8; 8], &[]).unwrap();
462        assert!(!result);
463    }
464
465    #[test]
466    fn test_verify_credential_proof_rejects_inconsistent_metadata_lengths() {
467        let proof = ZkpProof {
468            data: vec![1u8; 16],
469            proof_type: ProofType::Stark,
470            security_level: 1,
471            metadata: ProofMetadata::Credential {
472                attribute_sizes: vec![4, 4],
473                reveal_mask: vec![true],
474            },
475        };
476        let result = verify_credential_proof(&proof, &[0u8; 8], &[b"A".to_vec()]).unwrap();
477        assert!(!result);
478    }
479
480    #[test]
481    fn test_verify_credential_proof_rejects_revealed_count_mismatch() {
482        let proof = ZkpProof {
483            data: vec![1u8; 16],
484            proof_type: ProofType::Stark,
485            security_level: 1,
486            metadata: ProofMetadata::Credential {
487                attribute_sizes: vec![4, 4],
488                reveal_mask: vec![true, false],
489            },
490        };
491        let result = verify_credential_proof(&proof, &[0u8; 8], &[]).unwrap();
492        assert!(!result);
493    }
494
495    #[test]
496    fn test_verify_credential_proof_rejects_short_expected_commitment() {
497        let credential = IpCredential {
498            attributes: vec![b"Alice".to_vec(), b"42".to_vec()],
499        };
500        let reveal_mask = vec![true, false];
501        let proof = prove_credential_attributes(&credential, &reveal_mask).expect("proof");
502        let result = verify_credential_proof(&proof, &[1u8; 7], &[b"Alice".to_vec()]);
503        assert!(result.is_err());
504    }
505
506    #[test]
507    fn test_verify_credential_proof_rejects_revealed_attribute_too_long() {
508        let credential = IpCredential {
509            attributes: vec![b"A".to_vec(), b"42".to_vec()],
510        };
511        let reveal_mask = vec![true, false];
512        let commitment = compute_credential_commitment(&credential.attributes).expect("commitment");
513        let proof = prove_credential_attributes(&credential, &reveal_mask).expect("proof");
514        let result = verify_credential_proof(&proof, &commitment, &[b"TOO-LONG".to_vec()]).unwrap();
515        assert!(!result);
516    }
517}