baseid_vc/
lifecycle.rs

1//! W3C VC JWT implementation of the unified credential lifecycle traits.
2//!
3//! `VcJwtLifecycle` implements `CredentialIssuer`, `CredentialVerifier`, and
4//! `CredentialPresenter` for the W3C VC 2.0 JWT format. Predicates are not
5//! supported and return `UnsupportedPredicate`. Selective disclosure is not
6//! natively supported by JWT-VC — `Reveal` includes all claims, `Hide` is
7//! a no-op (the full JWT is always included in the presentation).
8
9use baseid_core::claims::{ClaimSet, DisclosureSelection};
10use baseid_core::error::CredentialError;
11use baseid_core::lifecycle::{
12    CredentialIssuer, CredentialPresenter, CredentialVerifier, IssuanceOptions, IssuedCredential,
13    PresentationOptions, PresentedCredential, RevocationStatus, VerificationOutcome,
14};
15use baseid_core::types::CredentialFormat;
16use baseid_crypto::signer::{Signer, Verifier};
17
18use crate::credential::{Issuer, VerifiableCredential};
19use crate::presentation::VerifiablePresentation;
20use crate::signing;
21
22/// W3C VC JWT credential lifecycle implementation.
23///
24/// Issues, verifies, and presents W3C Verifiable Credentials as JWTs.
25/// JWT-VC does not support selective disclosure — the full credential is
26/// always included. For selective disclosure, use SD-JWT or BBS+.
27pub struct VcJwtLifecycle<'a> {
28    signer: &'a dyn Signer,
29    verifier: &'a dyn Verifier,
30    kid: String,
31}
32
33impl<'a> VcJwtLifecycle<'a> {
34    /// Create a new VC-JWT lifecycle handler.
35    pub fn new(signer: &'a dyn Signer, verifier: &'a dyn Verifier, kid: &str) -> Self {
36        Self {
37            signer,
38            verifier,
39            kid: kid.to_string(),
40        }
41    }
42}
43
44impl CredentialIssuer for VcJwtLifecycle<'_> {
45    fn format(&self) -> CredentialFormat {
46        CredentialFormat::W3cVc
47    }
48
49    fn issue(
50        &self,
51        issuer_did: &str,
52        subject_did: Option<&str>,
53        claims: &ClaimSet,
54        options: &IssuanceOptions,
55    ) -> baseid_core::Result<IssuedCredential> {
56        // Build credential subject from claims
57        let mut subject = serde_json::Map::new();
58        if let Some(sub) = subject_did {
59            subject.insert("id".to_string(), serde_json::json!(sub));
60        }
61        if let Some(ns_claims) = claims.namespace("") {
62            for (name, value) in ns_claims {
63                subject.insert(name.clone(), value.clone());
64            }
65        }
66
67        let mut types = vec!["VerifiableCredential".to_string()];
68        types.extend(options.types.iter().cloned());
69
70        let vc = VerifiableCredential {
71            context: vec!["https://www.w3.org/ns/credentials/v2".to_string()],
72            id: options.credential_id.clone(),
73            r#type: types,
74            issuer: Issuer::Uri(issuer_did.to_string()),
75            valid_from: options.valid_from.clone(),
76            valid_until: options.valid_until.clone(),
77            credential_subject: serde_json::Value::Object(subject),
78            credential_status: options.status.clone(),
79            proof: None,
80        };
81
82        let jwt = signing::sign_credential_jwt(&vc, self.signer, &self.kid)?;
83
84        Ok(IssuedCredential {
85            data: jwt.into_bytes(),
86            format: CredentialFormat::W3cVc,
87            id: options.credential_id.clone(),
88            issuer: issuer_did.to_string(),
89            subject: subject_did.map(|s| s.to_string()),
90        })
91    }
92}
93
94impl CredentialVerifier for VcJwtLifecycle<'_> {
95    fn format(&self) -> CredentialFormat {
96        CredentialFormat::W3cVc
97    }
98
99    fn verify(&self, credential_data: &[u8]) -> baseid_core::Result<VerificationOutcome> {
100        let jwt_str =
101            std::str::from_utf8(credential_data).map_err(|_| CredentialError::InvalidCredential)?;
102
103        let vc = signing::verify_credential_jwt(jwt_str, self.verifier)?;
104
105        let issuer = match &vc.issuer {
106            Issuer::Uri(uri) => uri.clone(),
107            Issuer::Object { id, .. } => id.clone(),
108        };
109
110        let subject = vc
111            .credential_subject
112            .get("id")
113            .and_then(|v| v.as_str())
114            .map(|s| s.to_string());
115
116        // Build ClaimSet from credential subject (excluding "id")
117        let mut claim_set = ClaimSet::new();
118        if let Some(obj) = vc.credential_subject.as_object() {
119            for (k, v) in obj {
120                if k != "id" {
121                    claim_set.insert("", k, v.clone());
122                }
123            }
124        }
125
126        Ok(VerificationOutcome {
127            valid: true,
128            format: CredentialFormat::W3cVc,
129            issuer,
130            subject,
131            claims: claim_set,
132            unlinkable: false,
133            predicates_verified: vec![],
134            valid_until: vc.valid_until,
135            revocation_status: RevocationStatus::NotChecked,
136        })
137    }
138}
139
140impl CredentialPresenter for VcJwtLifecycle<'_> {
141    fn format(&self) -> CredentialFormat {
142        CredentialFormat::W3cVc
143    }
144
145    fn present(
146        &self,
147        credential_data: &[u8],
148        disclosure: &DisclosureSelection,
149        options: &PresentationOptions,
150    ) -> baseid_core::Result<PresentedCredential> {
151        // JWT-VC does not support predicates
152        if disclosure.has_predicates() {
153            return Err(CredentialError::UnsupportedPredicate.into());
154        }
155
156        let jwt_str =
157            std::str::from_utf8(credential_data).map_err(|_| CredentialError::InvalidCredential)?;
158
159        // JWT-VC doesn't support selective disclosure — the full credential
160        // JWT is wrapped in a Verifiable Presentation JWT.
161        let vc = signing::verify_credential_jwt(jwt_str, self.verifier)?;
162
163        let vp = VerifiablePresentation {
164            context: vec!["https://www.w3.org/ns/credentials/v2".to_string()],
165            r#type: vec!["VerifiablePresentation".to_string()],
166            verifiable_credential: vec![vc],
167            holder: options.holder_did.clone(),
168            proof: None,
169        };
170
171        let nonce = options.nonce.as_deref().unwrap_or("default-nonce");
172        let audience = options.audience.as_deref().unwrap_or("");
173
174        let vp_jwt = signing::sign_presentation_jwt(&vp, self.signer, &self.kid, nonce, audience)?;
175
176        Ok(PresentedCredential {
177            data: vp_jwt.into_bytes(),
178            format: CredentialFormat::W3cVc,
179            unlinkable: false,
180        })
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use baseid_core::claims::PredicateType;
188    use baseid_core::types::KeyType;
189    use baseid_crypto::KeyPair;
190    use serde_json::json;
191
192    fn setup() -> (KeyPair, ClaimSet) {
193        let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
194        let mut claims = ClaimSet::new();
195        claims.insert("", "given_name", json!("Alice"));
196        claims.insert("", "family_name", json!("Smith"));
197        claims.insert("", "birth_date", json!("1990-01-15"));
198        (kp, claims)
199    }
200
201    #[test]
202    fn issue_and_verify() {
203        let (kp, claims) = setup();
204        let lifecycle = VcJwtLifecycle::new(&kp, &kp.public, "did:key:issuer#key-0");
205
206        let opts = IssuanceOptions {
207            types: vec!["IdentityCredential".to_string()],
208            ..Default::default()
209        };
210
211        let issued = lifecycle
212            .issue("did:key:issuer", Some("did:key:holder"), &claims, &opts)
213            .unwrap();
214
215        assert_eq!(issued.format, CredentialFormat::W3cVc);
216        assert_eq!(issued.issuer, "did:key:issuer");
217
218        let outcome = lifecycle.verify(&issued.data).unwrap();
219        assert!(outcome.valid);
220        assert_eq!(outcome.issuer, "did:key:issuer");
221        assert_eq!(outcome.subject, Some("did:key:holder".to_string()));
222        assert_eq!(outcome.claims.get("", "given_name"), Some(&json!("Alice")));
223        assert_eq!(outcome.claims.get("", "family_name"), Some(&json!("Smith")));
224        assert!(!outcome.unlinkable);
225    }
226
227    #[test]
228    fn present_creates_vp() {
229        let (kp, claims) = setup();
230        let lifecycle = VcJwtLifecycle::new(&kp, &kp.public, "did:key:issuer#key-0");
231
232        let issued = lifecycle
233            .issue(
234                "did:key:issuer",
235                Some("did:key:holder"),
236                &claims,
237                &IssuanceOptions::default(),
238            )
239            .unwrap();
240
241        let disclosure = DisclosureSelection::new()
242            .reveal("given_name")
243            .reveal("family_name");
244
245        let opts = PresentationOptions {
246            nonce: Some("nonce-123".to_string()),
247            audience: Some("did:key:verifier".to_string()),
248            holder_did: Some("did:key:holder".to_string()),
249        };
250
251        let presented = lifecycle.present(&issued.data, &disclosure, &opts).unwrap();
252        assert_eq!(presented.format, CredentialFormat::W3cVc);
253        assert!(!presented.unlinkable);
254
255        // The presentation is a VP-JWT — verify it decodes
256        let vp_jwt = std::str::from_utf8(&presented.data).unwrap();
257        let vp = signing::verify_presentation_jwt(
258            vp_jwt,
259            &kp.public,
260            Some("nonce-123"),
261            Some("did:key:verifier"),
262        )
263        .unwrap();
264        assert_eq!(vp.holder, Some("did:key:holder".to_string()));
265        assert_eq!(vp.verifiable_credential.len(), 1);
266    }
267
268    #[test]
269    fn predicate_returns_unsupported() {
270        let (kp, claims) = setup();
271        let lifecycle = VcJwtLifecycle::new(&kp, &kp.public, "did:key:issuer#key-0");
272
273        let issued = lifecycle
274            .issue("did:key:issuer", None, &claims, &IssuanceOptions::default())
275            .unwrap();
276
277        let disclosure = DisclosureSelection::new()
278            .reveal("given_name")
279            .predicate("birth_date", PredicateType::LessThan(json!("2008-03-01")));
280
281        let result = lifecycle.present(&issued.data, &disclosure, &PresentationOptions::default());
282        assert!(result.is_err());
283
284        let err = result.unwrap_err();
285        assert!(
286            format!("{err}").contains("Predicate"),
287            "Error should mention predicate: {err}"
288        );
289    }
290
291    #[test]
292    fn format_method() {
293        let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
294        let lifecycle = VcJwtLifecycle::new(&kp, &kp.public, "kid");
295
296        assert_eq!(
297            <VcJwtLifecycle as CredentialIssuer>::format(&lifecycle),
298            CredentialFormat::W3cVc
299        );
300        assert_eq!(
301            <VcJwtLifecycle as CredentialVerifier>::format(&lifecycle),
302            CredentialFormat::W3cVc
303        );
304        assert_eq!(
305            <VcJwtLifecycle as CredentialPresenter>::format(&lifecycle),
306            CredentialFormat::W3cVc
307        );
308    }
309
310    #[test]
311    fn issuance_with_options() {
312        let (kp, claims) = setup();
313        let lifecycle = VcJwtLifecycle::new(&kp, &kp.public, "kid");
314
315        let opts = IssuanceOptions {
316            credential_id: Some("urn:uuid:1234".to_string()),
317            types: vec!["PersonCredential".to_string()],
318            valid_from: Some("2024-01-01T00:00:00Z".to_string()),
319            valid_until: Some("2030-01-01T00:00:00Z".to_string()),
320            status: None,
321        };
322
323        let issued = lifecycle
324            .issue("did:key:issuer", Some("did:key:holder"), &claims, &opts)
325            .unwrap();
326
327        let outcome = lifecycle.verify(&issued.data).unwrap();
328        assert_eq!(
329            outcome.valid_until,
330            Some("2030-01-01T00:00:00Z".to_string())
331        );
332    }
333}