baseid_bbs/
lifecycle.rs

1use baseid_core::claims::{ClaimDisclosure, ClaimPath, ClaimSet, DisclosureSelection};
2use baseid_core::lifecycle::*;
3use baseid_core::types::CredentialFormat;
4
5use crate::credential::{BbsClaim, BbsCredential, BbsDerivedProof};
6use crate::keys::BbsKeyPair;
7use crate::proof::{bbs_proof_gen, bbs_proof_verify};
8use crate::signing::{bbs_sign, bbs_verify};
9
10/// BBS+ credential lifecycle -- issue, verify, and present with unlinkable selective disclosure.
11pub struct BbsLifecycle {
12    key_pair: BbsKeyPair,
13}
14
15impl BbsLifecycle {
16    pub fn new(key_pair: BbsKeyPair) -> Self {
17        Self { key_pair }
18    }
19
20    pub fn public_key_bytes(&self) -> &[u8] {
21        &self.key_pair.public_key
22    }
23}
24
25impl CredentialIssuer for BbsLifecycle {
26    fn format(&self) -> CredentialFormat {
27        CredentialFormat::Bbs
28    }
29
30    fn issue(
31        &self,
32        issuer_did: &str,
33        subject_did: Option<&str>,
34        claims: &ClaimSet,
35        options: &IssuanceOptions,
36    ) -> baseid_core::Result<IssuedCredential> {
37        // Convert ClaimSet to ordered BbsClaims
38        let mut bbs_claims: Vec<BbsClaim> = Vec::new();
39        for (ns, claims_map) in claims.namespaces() {
40            for (name, value) in claims_map {
41                bbs_claims.push(BbsClaim {
42                    namespace: ns.to_string(),
43                    name: name.clone(),
44                    value: value.clone(),
45                });
46            }
47        }
48
49        let cred = BbsCredential {
50            issuer: issuer_did.to_string(),
51            subject: subject_did.map(|s| s.to_string()),
52            types: if options.types.is_empty() {
53                vec!["VerifiableCredential".to_string()]
54            } else {
55                options.types.clone()
56            },
57            claims: bbs_claims,
58            signature: vec![], // placeholder
59            issuer_public_key: self.key_pair.public_key.clone(),
60        };
61
62        // Encode and sign
63        let messages = cred.encode_messages();
64        let signature = bbs_sign(&self.key_pair, &messages, None)
65            .map_err(|e| -> baseid_core::Error { e.into() })?;
66
67        let mut signed_cred = cred;
68        signed_cred.signature = signature;
69
70        let data = signed_cred.to_bytes()?;
71
72        Ok(IssuedCredential {
73            data,
74            format: CredentialFormat::Bbs,
75            id: options.credential_id.clone(),
76            issuer: issuer_did.to_string(),
77            subject: subject_did.map(|s| s.to_string()),
78        })
79    }
80}
81
82impl CredentialVerifier for BbsLifecycle {
83    fn format(&self) -> CredentialFormat {
84        CredentialFormat::Bbs
85    }
86
87    fn verify(&self, credential_data: &[u8]) -> baseid_core::Result<VerificationOutcome> {
88        let cred = BbsCredential::from_bytes(credential_data)?;
89        let messages = cred.encode_messages();
90
91        let valid = bbs_verify(&cred.issuer_public_key, &cred.signature, &messages, None)
92            .map_err(|e| -> baseid_core::Error { e.into() })?;
93
94        // Reconstruct ClaimSet
95        let mut claim_set = ClaimSet::new();
96        for claim in &cred.claims {
97            claim_set.insert(&claim.namespace, &claim.name, claim.value.clone());
98        }
99
100        Ok(VerificationOutcome {
101            valid,
102            format: CredentialFormat::Bbs,
103            issuer: cred.issuer,
104            subject: cred.subject,
105            claims: claim_set,
106            unlinkable: false, // full credential verification is NOT unlinkable
107            predicates_verified: vec![],
108            valid_until: None,
109            revocation_status: RevocationStatus::NotChecked,
110        })
111    }
112}
113
114impl CredentialPresenter for BbsLifecycle {
115    fn format(&self) -> CredentialFormat {
116        CredentialFormat::Bbs
117    }
118
119    fn present(
120        &self,
121        credential_data: &[u8],
122        disclosure: &DisclosureSelection,
123        options: &PresentationOptions,
124    ) -> baseid_core::Result<PresentedCredential> {
125        let cred = BbsCredential::from_bytes(credential_data)?;
126        let messages = cred.encode_messages();
127
128        // Determine disclosed indices from DisclosureSelection
129        let mut disclosed_indices = Vec::new();
130        let mut disclosed_claims = Vec::new();
131        let mut predicate_results = Vec::new();
132
133        for (i, claim) in cred.claims.iter().enumerate() {
134            match disclosure.get(&claim.namespace, &claim.name) {
135                Some(ClaimDisclosure::Reveal) | None => {
136                    // Default to reveal if not specified
137                    disclosed_indices.push(i);
138                    disclosed_claims.push((i, claim.clone()));
139                }
140                Some(ClaimDisclosure::Hide) => {
141                    // Not disclosed -- hidden in ZK proof
142                }
143                Some(ClaimDisclosure::Predicate(predicate)) => {
144                    // For predicates, the claim is hidden but we evaluate the predicate
145                    let satisfied = crate::predicates::evaluate_predicate(&claim.value, predicate);
146                    predicate_results.push((claim.name.clone(), satisfied));
147                    // Claim is NOT disclosed (hidden in ZK)
148                }
149            }
150        }
151
152        // Generate BBS+ proof
153        let nonce = options.nonce.as_deref().map(|n| n.as_bytes());
154        let proof = bbs_proof_gen(
155            &cred.issuer_public_key,
156            &cred.signature,
157            &messages,
158            &disclosed_indices,
159            None,
160            nonce,
161        )
162        .map_err(|e| -> baseid_core::Error { e.into() })?;
163
164        let derived = BbsDerivedProof {
165            issuer: cred.issuer,
166            subject: cred.subject,
167            types: cred.types,
168            proof,
169            disclosed_claims,
170            total_claims: cred.claims.len(),
171            issuer_public_key: cred.issuer_public_key,
172            predicate_results,
173            nonce: nonce.map(|n| n.to_vec()),
174        };
175
176        let data = derived.to_bytes()?;
177
178        Ok(PresentedCredential {
179            data,
180            format: CredentialFormat::Bbs,
181            unlinkable: true,
182        })
183    }
184}
185
186/// Verify a BBS+ derived proof (presented credential).
187pub fn verify_derived_proof(proof_data: &[u8]) -> baseid_core::Result<VerificationOutcome> {
188    let derived = BbsDerivedProof::from_bytes(proof_data)?;
189
190    let disclosed_msgs: Vec<(usize, Vec<u8>)> = derived
191        .disclosed_claims
192        .iter()
193        .map(|(i, claim)| {
194            let canonical = serde_json::json!({
195                "ns": claim.namespace,
196                "name": claim.name,
197                "value": claim.value,
198            });
199            (*i, serde_json::to_vec(&canonical).unwrap_or_default())
200        })
201        .collect();
202
203    let nonce_ref = derived.nonce.as_deref();
204    let valid = bbs_proof_verify(
205        &derived.issuer_public_key,
206        &derived.proof,
207        &disclosed_msgs,
208        None,
209        nonce_ref,
210    )
211    .map_err(|e| -> baseid_core::Error { e.into() })?;
212
213    // Reconstruct ClaimSet from disclosed claims only
214    let mut claim_set = ClaimSet::new();
215    for (_, claim) in &derived.disclosed_claims {
216        claim_set.insert(&claim.namespace, &claim.name, claim.value.clone());
217    }
218
219    let predicates_verified: Vec<(ClaimPath, baseid_core::claims::PredicateType)> = vec![];
220
221    Ok(VerificationOutcome {
222        valid,
223        format: CredentialFormat::Bbs,
224        issuer: derived.issuer,
225        subject: derived.subject,
226        claims: claim_set,
227        unlinkable: true,
228        predicates_verified,
229        valid_until: None,
230        revocation_status: RevocationStatus::NotChecked,
231    })
232}