1extern 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#[derive(Debug, Clone)]
34pub struct IpCredential {
35 pub attributes: Vec<Vec<u8>>,
37}
38
39pub 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 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 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 let input = CredentialInput {
114 attributes: credential.attributes.clone(),
115 };
116
117 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 let public_values = air.public_values(&input);
127
128 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 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
150pub 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
198fn 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
212fn 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
224pub 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 let crate::ProofMetadata::Credential {
253 attribute_sizes,
254 reveal_mask,
255 } = &proof.metadata
256 else {
257 return Ok(false);
258 };
259
260 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 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 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 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 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 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 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}