1use 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
22pub struct VcJwtLifecycle<'a> {
28 signer: &'a dyn Signer,
29 verifier: &'a dyn Verifier,
30 kid: String,
31}
32
33impl<'a> VcJwtLifecycle<'a> {
34 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 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 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 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 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 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}