1use baseid_core::claims::{ClaimSet, DisclosureSelection};
8use baseid_core::error::CredentialError;
9use baseid_core::lifecycle::{
10 CredentialIssuer, CredentialPresenter, CredentialVerifier, IssuanceOptions, IssuedCredential,
11 PresentationOptions, PresentedCredential, RevocationStatus, VerificationOutcome,
12};
13use baseid_core::types::CredentialFormat;
14use baseid_crypto::signer::{Signer, Verifier};
15
16use crate::issuer::SdJwtIssuer;
17use crate::verifier::SdJwtVerifier;
18use crate::SdJwt;
19
20pub struct SdJwtLifecycle<'a> {
27 signer: &'a dyn Signer,
28 verifier: &'a dyn Verifier,
29 kid: String,
30}
31
32impl<'a> SdJwtLifecycle<'a> {
33 pub fn new(signer: &'a dyn Signer, verifier: &'a dyn Verifier, kid: &str) -> Self {
40 Self {
41 signer,
42 verifier,
43 kid: kid.to_string(),
44 }
45 }
46}
47
48impl CredentialIssuer for SdJwtLifecycle<'_> {
49 fn format(&self) -> CredentialFormat {
50 CredentialFormat::SdJwtVc
51 }
52
53 fn issue(
54 &self,
55 issuer_did: &str,
56 subject_did: Option<&str>,
57 claims: &ClaimSet,
58 options: &IssuanceOptions,
59 ) -> baseid_core::Result<IssuedCredential> {
60 let mut builder = SdJwtIssuer::new(self.signer, &self.kid)
61 .add_plain_claim("iss", serde_json::json!(issuer_did));
62
63 if let Some(sub) = subject_did {
64 builder = builder.add_plain_claim("sub", serde_json::json!(sub));
65 }
66
67 if let Some(ref vf) = options.valid_from {
68 builder = builder.add_plain_claim("iat", serde_json::json!(vf));
69 }
70
71 if let Some(ref vu) = options.valid_until {
72 builder = builder.add_plain_claim("exp", serde_json::json!(vu));
73 }
74
75 if let Some(ref id) = options.credential_id {
76 builder = builder.add_plain_claim("jti", serde_json::json!(id));
77 }
78
79 if let Some(ns_claims) = claims.namespace("") {
81 for (name, value) in ns_claims {
82 builder = builder.add_sd_claim(name, value.clone());
83 }
84 }
85
86 let sd_jwt = builder.build()?;
87 let serialized = sd_jwt.serialize();
88
89 Ok(IssuedCredential {
90 data: serialized.into_bytes(),
91 format: CredentialFormat::SdJwtVc,
92 id: options.credential_id.clone(),
93 issuer: issuer_did.to_string(),
94 subject: subject_did.map(|s| s.to_string()),
95 })
96 }
97}
98
99impl CredentialVerifier for SdJwtLifecycle<'_> {
100 fn format(&self) -> CredentialFormat {
101 CredentialFormat::SdJwtVc
102 }
103
104 fn verify(&self, credential_data: &[u8]) -> baseid_core::Result<VerificationOutcome> {
105 let compact =
106 std::str::from_utf8(credential_data).map_err(|_| CredentialError::InvalidCredential)?;
107
108 let sd_jwt = SdJwt::parse(compact)?;
109 let verifier = SdJwtVerifier::new(self.verifier);
110 let claims_value = verifier.verify(&sd_jwt)?;
111
112 let issuer = claims_value
114 .get("iss")
115 .and_then(|v| v.as_str())
116 .unwrap_or("")
117 .to_string();
118
119 let subject = claims_value
120 .get("sub")
121 .and_then(|v| v.as_str())
122 .map(|s| s.to_string());
123
124 let valid_until = claims_value
125 .get("exp")
126 .and_then(|v| v.as_str())
127 .map(|s| s.to_string());
128
129 let mut claim_set = ClaimSet::new();
131 if let Some(obj) = claims_value.as_object() {
132 for (k, v) in obj {
133 match k.as_str() {
135 "iss" | "sub" | "iat" | "exp" | "jti" | "nbf" | "aud" => continue,
136 _ => claim_set.insert("", k, v.clone()),
137 }
138 }
139 }
140
141 Ok(VerificationOutcome {
142 valid: true,
143 format: CredentialFormat::SdJwtVc,
144 issuer,
145 subject,
146 claims: claim_set,
147 unlinkable: false,
148 predicates_verified: vec![],
149 valid_until,
150 revocation_status: RevocationStatus::NotChecked,
151 })
152 }
153}
154
155impl CredentialPresenter for SdJwtLifecycle<'_> {
156 fn format(&self) -> CredentialFormat {
157 CredentialFormat::SdJwtVc
158 }
159
160 fn present(
161 &self,
162 credential_data: &[u8],
163 disclosure: &DisclosureSelection,
164 _options: &PresentationOptions,
165 ) -> baseid_core::Result<PresentedCredential> {
166 if disclosure.has_predicates() {
168 return Err(CredentialError::UnsupportedPredicate.into());
169 }
170
171 let compact =
172 std::str::from_utf8(credential_data).map_err(|_| CredentialError::InvalidCredential)?;
173
174 let sd_jwt = SdJwt::parse(compact)?;
175
176 let revealed = disclosure.revealed_claims();
179 let mut selected_disclosures = Vec::new();
180
181 for encoded_disc in &sd_jwt.disclosures {
182 let disc = crate::disclosure::Disclosure::decode(encoded_disc)?;
183 if let Some(ref name) = disc.claim_name {
184 let should_include =
187 revealed.contains(&name.as_str()) || disclosure.get("", name).is_none();
188 if should_include {
189 selected_disclosures.push(encoded_disc.clone());
190 }
191 } else {
192 selected_disclosures.push(encoded_disc.clone());
194 }
195 }
196
197 let presented = SdJwt {
198 jwt: sd_jwt.jwt,
199 disclosures: selected_disclosures,
200 key_binding_jwt: sd_jwt.key_binding_jwt,
201 };
202
203 Ok(PresentedCredential {
204 data: presented.serialize().into_bytes(),
205 format: CredentialFormat::SdJwtVc,
206 unlinkable: false,
207 })
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use baseid_core::claims::PredicateType;
215 use baseid_core::types::KeyType;
216 use baseid_crypto::KeyPair;
217 use serde_json::json;
218
219 fn setup() -> (KeyPair, ClaimSet) {
220 let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
221 let mut claims = ClaimSet::new();
222 claims.insert("", "given_name", json!("Alice"));
223 claims.insert("", "family_name", json!("Smith"));
224 claims.insert("", "birth_date", json!("1990-01-15"));
225 claims.insert("", "email", json!("alice@example.com"));
226 (kp, claims)
227 }
228
229 #[test]
230 fn issue_and_verify() {
231 let (kp, claims) = setup();
232 let lifecycle = SdJwtLifecycle::new(&kp, &kp.public, "kid-1");
233
234 let issued = lifecycle
235 .issue(
236 "did:key:issuer",
237 Some("did:key:holder"),
238 &claims,
239 &IssuanceOptions::default(),
240 )
241 .unwrap();
242
243 assert_eq!(issued.format, CredentialFormat::SdJwtVc);
244 assert_eq!(issued.issuer, "did:key:issuer");
245 assert_eq!(issued.subject, Some("did:key:holder".to_string()));
246
247 let outcome = lifecycle.verify(&issued.data).unwrap();
248 assert!(outcome.valid);
249 assert_eq!(outcome.issuer, "did:key:issuer");
250 assert_eq!(outcome.subject, Some("did:key:holder".to_string()));
251 assert_eq!(outcome.claims.get("", "given_name"), Some(&json!("Alice")));
252 assert_eq!(outcome.claims.get("", "family_name"), Some(&json!("Smith")));
253 assert!(!outcome.unlinkable);
254 }
255
256 #[test]
257 fn present_selective_disclosure() {
258 let (kp, claims) = setup();
259 let lifecycle = SdJwtLifecycle::new(&kp, &kp.public, "kid-1");
260
261 let issued = lifecycle
262 .issue(
263 "did:key:issuer",
264 Some("did:key:holder"),
265 &claims,
266 &IssuanceOptions::default(),
267 )
268 .unwrap();
269
270 let disclosure = DisclosureSelection::new()
272 .reveal("given_name")
273 .reveal("family_name")
274 .hide("birth_date")
275 .hide("email");
276
277 let presented = lifecycle
278 .present(&issued.data, &disclosure, &PresentationOptions::default())
279 .unwrap();
280
281 assert_eq!(presented.format, CredentialFormat::SdJwtVc);
282 assert!(!presented.unlinkable);
283
284 let outcome = lifecycle.verify(&presented.data).unwrap();
286 assert!(outcome.valid);
287 assert_eq!(outcome.claims.get("", "given_name"), Some(&json!("Alice")));
288 assert_eq!(outcome.claims.get("", "family_name"), Some(&json!("Smith")));
289 assert_eq!(outcome.claims.get("", "birth_date"), None);
290 assert_eq!(outcome.claims.get("", "email"), None);
291 }
292
293 #[test]
294 fn predicate_returns_unsupported() {
295 let (kp, claims) = setup();
296 let lifecycle = SdJwtLifecycle::new(&kp, &kp.public, "kid-1");
297
298 let issued = lifecycle
299 .issue("did:key:issuer", None, &claims, &IssuanceOptions::default())
300 .unwrap();
301
302 let disclosure = DisclosureSelection::new()
303 .reveal("given_name")
304 .predicate("birth_date", PredicateType::LessThan(json!("2008-03-01")));
305
306 let result = lifecycle.present(&issued.data, &disclosure, &PresentationOptions::default());
307 assert!(result.is_err());
308
309 let err = result.unwrap_err();
310 assert!(
311 format!("{err}").contains("Predicate"),
312 "Error should mention predicate: {err}"
313 );
314 }
315
316 #[test]
317 fn issuance_with_options() {
318 let (kp, claims) = setup();
319 let lifecycle = SdJwtLifecycle::new(&kp, &kp.public, "kid-1");
320
321 let opts = IssuanceOptions {
322 credential_id: Some("urn:uuid:1234".to_string()),
323 types: vec!["IdentityCredential".to_string()],
324 valid_from: Some("2024-01-01T00:00:00Z".to_string()),
325 valid_until: Some("2025-01-01T00:00:00Z".to_string()),
326 status: None,
327 };
328
329 let issued = lifecycle
330 .issue("did:key:issuer", Some("did:key:holder"), &claims, &opts)
331 .unwrap();
332
333 let outcome = lifecycle.verify(&issued.data).unwrap();
334 assert_eq!(
335 outcome.valid_until,
336 Some("2025-01-01T00:00:00Z".to_string())
337 );
338 }
339}