1use baseid_core::error::{CredentialError, SerializationError};
8use baseid_crypto::jwt::{self, alg_to_str, JwtHeader};
9use baseid_crypto::signer::{Signer, Verifier};
10use serde_json::Value;
11
12use crate::credential::{Issuer, VerifiableCredential};
13use crate::presentation::VerifiablePresentation;
14
15const TIME_LEEWAY_SECS: i64 = 60;
17
18pub fn issuer_id(issuer: &Issuer) -> &str {
20 match issuer {
21 Issuer::Uri(uri) => uri.as_str(),
22 Issuer::Object { id, .. } => id.as_str(),
23 }
24}
25
26pub fn sign_credential_jwt(
34 vc: &VerifiableCredential,
35 signer: &dyn Signer,
36 kid: &str,
37) -> baseid_core::Result<String> {
38 let header = JwtHeader {
39 alg: alg_to_str(signer.algorithm()).to_string(),
40 typ: Some("JWT".to_string()),
41 kid: Some(kid.to_string()),
42 additional: serde_json::Map::new(),
43 };
44
45 let vc_value = serde_json::to_value(vc).map_err(SerializationError::Json)?;
46
47 let mut claims = serde_json::Map::new();
48 claims.insert(
49 "iss".to_string(),
50 Value::String(issuer_id(&vc.issuer).to_string()),
51 );
52 if let Some(id) = &vc.id {
53 claims.insert("jti".to_string(), Value::String(id.clone()));
54 }
55 if let Some(sub) = vc.credential_subject.get("id").and_then(|v| v.as_str()) {
57 claims.insert("sub".to_string(), Value::String(sub.to_string()));
58 }
59
60 if let Some(ref valid_from) = vc.valid_from {
62 if let Some(epoch) = parse_rfc3339_to_epoch(valid_from) {
63 claims.insert("nbf".to_string(), Value::Number(epoch.into()));
64 }
65 }
66 if let Some(ref valid_until) = vc.valid_until {
67 if let Some(epoch) = parse_rfc3339_to_epoch(valid_until) {
68 claims.insert("exp".to_string(), Value::Number(epoch.into()));
69 }
70 }
71 claims.insert("iat".to_string(), Value::Number(current_epoch().into()));
72
73 claims.insert("vc".to_string(), vc_value);
74
75 jwt::encode_jwt(&header, &Value::Object(claims), signer)
76}
77
78pub fn verify_credential_jwt(
87 jwt_str: &str,
88 verifier: &dyn Verifier,
89) -> baseid_core::Result<VerifiableCredential> {
90 let (_header, claims) = jwt::decode_jwt(jwt_str, verifier)?;
91
92 let now = current_epoch();
94 if let Some(nbf) = claims.get("nbf").and_then(|v| v.as_i64()) {
95 if now < nbf - TIME_LEEWAY_SECS {
96 return Err(CredentialError::InvalidCredential.into());
97 }
98 }
99 if let Some(exp) = claims.get("exp").and_then(|v| v.as_i64()) {
100 if now > exp + TIME_LEEWAY_SECS {
101 return Err(CredentialError::Expired.into());
102 }
103 }
104
105 let vc_value = claims.get("vc").ok_or(CredentialError::InvalidCredential)?;
106
107 let vc: VerifiableCredential =
108 serde_json::from_value(vc_value.clone()).map_err(SerializationError::Json)?;
109
110 Ok(vc)
111}
112
113pub fn sign_presentation_jwt(
118 vp: &VerifiablePresentation,
119 signer: &dyn Signer,
120 kid: &str,
121 nonce: &str,
122 audience: &str,
123) -> baseid_core::Result<String> {
124 let header = JwtHeader {
125 alg: alg_to_str(signer.algorithm()).to_string(),
126 typ: Some("JWT".to_string()),
127 kid: Some(kid.to_string()),
128 additional: serde_json::Map::new(),
129 };
130
131 let vp_value = serde_json::to_value(vp).map_err(SerializationError::Json)?;
132
133 let mut claims = serde_json::Map::new();
134 if let Some(holder) = &vp.holder {
135 claims.insert("iss".to_string(), Value::String(holder.clone()));
136 }
137 claims.insert("vp".to_string(), vp_value);
138 claims.insert("nonce".to_string(), Value::String(nonce.to_string()));
139 claims.insert("aud".to_string(), Value::String(audience.to_string()));
140
141 jwt::encode_jwt(&header, &Value::Object(claims), signer)
142}
143
144pub fn verify_presentation_jwt(
150 jwt_str: &str,
151 verifier: &dyn Verifier,
152 expected_nonce: Option<&str>,
153 expected_audience: Option<&str>,
154) -> baseid_core::Result<VerifiablePresentation> {
155 let (_header, claims) = jwt::decode_jwt(jwt_str, verifier)?;
156
157 if let Some(expected) = expected_nonce {
159 let actual = claims
160 .get("nonce")
161 .and_then(|v| v.as_str())
162 .ok_or(CredentialError::InvalidCredential)?;
163 if actual != expected {
164 return Err(CredentialError::InvalidCredential.into());
165 }
166 }
167
168 if let Some(expected) = expected_audience {
170 let actual = claims
171 .get("aud")
172 .and_then(|v| v.as_str())
173 .ok_or(CredentialError::InvalidCredential)?;
174 if actual != expected {
175 return Err(CredentialError::InvalidCredential.into());
176 }
177 }
178
179 let vp_value = claims.get("vp").ok_or(CredentialError::InvalidCredential)?;
180
181 let vp: VerifiablePresentation =
182 serde_json::from_value(vp_value.clone()).map_err(SerializationError::Json)?;
183
184 Ok(vp)
185}
186
187fn current_epoch() -> i64 {
189 std::time::SystemTime::now()
190 .duration_since(std::time::UNIX_EPOCH)
191 .unwrap_or_default()
192 .as_secs() as i64
193}
194
195fn parse_rfc3339_to_epoch(s: &str) -> Option<i64> {
200 if s.len() < 19 {
202 return None;
203 }
204 let b = s.as_bytes();
205 if b[4] != b'-'
207 || b[7] != b'-'
208 || (b[10] != b'T' && b[10] != b't')
209 || b[13] != b':'
210 || b[16] != b':'
211 {
212 return None;
213 }
214 let year = parse_u32(&b[0..4])? as i64;
215 let month = parse_u32(&b[5..7])? as i64;
216 let day = parse_u32(&b[8..10])? as i64;
217 let hour = parse_u32(&b[11..13])? as i64;
218 let minute = parse_u32(&b[14..16])? as i64;
219 let second = parse_u32(&b[17..19])? as i64;
220
221 let rest = &s[19..];
223 let tz_part = if let Some(rest) = rest.strip_prefix('.') {
224 let end = rest
226 .find(|c: char| !c.is_ascii_digit())
227 .unwrap_or(rest.len());
228 &rest[end..]
229 } else {
230 rest
231 };
232
233 let offset_seconds = match tz_part {
234 "" | "Z" | "z" => 0i64,
235 s if s.len() >= 6 && (s.starts_with('+') || s.starts_with('-')) => {
236 let sign: i64 = if s.starts_with('-') { -1 } else { 1 };
237 let oh = parse_u32(&s.as_bytes()[1..3])? as i64;
238 let om = parse_u32(&s.as_bytes()[4..6])? as i64;
239 sign * (oh * 3600 + om * 60)
240 }
241 _ => return None,
242 };
243
244 let days = days_from_civil(year, month, day)?;
245 Some(days * 86400 + hour * 3600 + minute * 60 + second - offset_seconds)
246}
247
248fn parse_u32(b: &[u8]) -> Option<u32> {
249 std::str::from_utf8(b).ok()?.parse().ok()
250}
251
252fn days_from_civil(y: i64, m: i64, d: i64) -> Option<i64> {
255 if !(1..=12).contains(&m) || !(1..=31).contains(&d) {
256 return None;
257 }
258 let y = if m <= 2 { y - 1 } else { y };
259 let era = if y >= 0 { y } else { y - 399 } / 400;
260 let yoe = (y - era * 400) as u64;
261 let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) as u64 + 2) / 5 + d as u64 - 1;
262 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
263 Some(era * 146097 + doe as i64 - 719468)
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269 use baseid_core::types::KeyType;
270 use baseid_crypto::KeyPair;
271
272 fn sample_vc(issuer_did: &str) -> VerifiableCredential {
273 VerifiableCredential {
274 context: vec!["https://www.w3.org/ns/credentials/v2".to_string()],
275 id: Some("urn:uuid:12345678-1234-1234-1234-123456789abc".to_string()),
276 r#type: vec![
277 "VerifiableCredential".to_string(),
278 "ExampleCredential".to_string(),
279 ],
280 issuer: Issuer::Uri(issuer_did.to_string()),
281 valid_from: Some("2024-01-01T00:00:00Z".to_string()),
282 valid_until: None,
283 credential_subject: serde_json::json!({
284 "id": "did:key:z6MkHolder...",
285 "name": "Alice",
286 }),
287 credential_status: None,
288 proof: None,
289 }
290 }
291
292 fn vc_sign_verify_roundtrip(key_type: KeyType) {
293 let kp = KeyPair::generate(key_type).unwrap();
294 let kid = "did:key:test#key-0";
295 let vc = sample_vc("did:key:test");
296
297 let jwt = sign_credential_jwt(&vc, &kp, kid).unwrap();
298 let decoded = verify_credential_jwt(&jwt, &kp.public).unwrap();
299
300 assert_eq!(decoded.id, vc.id);
301 assert_eq!(decoded.r#type, vc.r#type);
302 assert_eq!(decoded.issuer, vc.issuer);
303 assert_eq!(decoded.credential_subject, vc.credential_subject);
304 }
305
306 #[test]
307 fn vc_roundtrip_ed25519() {
308 vc_sign_verify_roundtrip(KeyType::Ed25519);
309 }
310
311 #[test]
312 fn vc_roundtrip_p256() {
313 vc_sign_verify_roundtrip(KeyType::P256);
314 }
315
316 #[test]
317 fn vc_roundtrip_p384() {
318 vc_sign_verify_roundtrip(KeyType::P384);
319 }
320
321 #[test]
322 fn vc_roundtrip_secp256k1() {
323 vc_sign_verify_roundtrip(KeyType::Secp256k1);
324 }
325
326 #[test]
327 fn vc_wrong_key_rejected() {
328 let kp1 = KeyPair::generate(KeyType::Ed25519).unwrap();
329 let kp2 = KeyPair::generate(KeyType::Ed25519).unwrap();
330 let vc = sample_vc("did:key:test");
331 let jwt = sign_credential_jwt(&vc, &kp1, "did:key:test#key-0").unwrap();
332
333 assert!(verify_credential_jwt(&jwt, &kp2.public).is_err());
334 }
335
336 #[test]
337 fn vp_roundtrip() {
338 let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
339 let vc = sample_vc("did:key:issuer");
340 let vp = VerifiablePresentation {
341 context: vec!["https://www.w3.org/ns/credentials/v2".to_string()],
342 r#type: vec!["VerifiablePresentation".to_string()],
343 verifiable_credential: vec![vc],
344 holder: Some("did:key:holder".to_string()),
345 proof: None,
346 };
347
348 let jwt = sign_presentation_jwt(
349 &vp,
350 &kp,
351 "did:key:holder#key-0",
352 "nonce123",
353 "did:key:verifier",
354 )
355 .unwrap();
356 let decoded =
357 verify_presentation_jwt(&jwt, &kp.public, Some("nonce123"), Some("did:key:verifier"))
358 .unwrap();
359
360 assert_eq!(decoded.holder, vp.holder);
361 assert_eq!(decoded.verifiable_credential.len(), 1);
362 }
363
364 #[test]
365 fn jwt_claims_structure() {
366 let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
367 let vc = sample_vc("did:key:issuer123");
368 let jwt = sign_credential_jwt(&vc, &kp, "did:key:issuer123#key-0").unwrap();
369
370 let (_header, claims) = baseid_crypto::decode_jwt_unverified(&jwt).unwrap();
371 assert_eq!(claims["iss"], "did:key:issuer123");
372 assert_eq!(
373 claims["jti"],
374 "urn:uuid:12345678-1234-1234-1234-123456789abc"
375 );
376 assert_eq!(claims["sub"], "did:key:z6MkHolder...");
377 assert!(claims.get("vc").is_some());
378 assert!(claims.get("nbf").is_some(), "nbf claim should be present");
380 assert!(claims.get("iat").is_some(), "iat claim should be present");
381 }
382
383 #[test]
384 fn jwt_includes_exp_when_valid_until_set() {
385 let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
386 let mut vc = sample_vc("did:key:issuer");
387 vc.valid_until = Some("2030-12-31T23:59:59Z".to_string());
388
389 let jwt = sign_credential_jwt(&vc, &kp, "kid").unwrap();
390 let (_header, claims) = baseid_crypto::decode_jwt_unverified(&jwt).unwrap();
391
392 let exp = claims["exp"].as_i64().expect("exp should be a number");
393 assert_eq!(exp, 1924991999);
395 }
396
397 #[test]
398 fn expired_credential_rejected() {
399 let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
400 let mut vc = sample_vc("did:key:issuer");
401 vc.valid_until = Some("2020-01-01T00:00:00Z".to_string());
402
403 let jwt = sign_credential_jwt(&vc, &kp, "kid").unwrap();
404 let result = verify_credential_jwt(&jwt, &kp.public);
405
406 assert!(result.is_err());
407 let err = format!("{}", result.unwrap_err());
408 assert!(
409 err.contains("expired") || err.contains("Expired"),
410 "Expected expired error, got: {err}"
411 );
412 }
413
414 #[test]
415 fn future_credential_rejected() {
416 let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
417 let mut vc = sample_vc("did:key:issuer");
418 vc.valid_from = Some("2099-01-01T00:00:00Z".to_string());
419
420 let jwt = sign_credential_jwt(&vc, &kp, "kid").unwrap();
421 let result = verify_credential_jwt(&jwt, &kp.public);
422
423 assert!(
424 result.is_err(),
425 "Credential not yet valid should be rejected"
426 );
427 }
428
429 #[test]
430 fn parse_rfc3339_basic() {
431 assert_eq!(
432 parse_rfc3339_to_epoch("2024-01-01T00:00:00Z"),
433 Some(1704067200)
434 );
435 assert_eq!(parse_rfc3339_to_epoch("1970-01-01T00:00:00Z"), Some(0));
436 }
437
438 #[test]
439 fn parse_rfc3339_with_offset() {
440 let utc = parse_rfc3339_to_epoch("2024-01-01T00:00:00Z").unwrap();
442 let plus5 = parse_rfc3339_to_epoch("2024-01-01T05:00:00+05:00").unwrap();
443 assert_eq!(utc, plus5);
444 }
445
446 #[test]
447 fn parse_rfc3339_with_fractional_seconds() {
448 let without = parse_rfc3339_to_epoch("2024-01-01T00:00:00Z").unwrap();
449 let with = parse_rfc3339_to_epoch("2024-01-01T00:00:00.123456Z").unwrap();
450 assert_eq!(without, with);
451 }
452
453 #[test]
454 fn vp_wrong_nonce_rejected() {
455 let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
456 let vc = sample_vc("did:key:issuer");
457 let vp = VerifiablePresentation {
458 context: vec!["https://www.w3.org/ns/credentials/v2".to_string()],
459 r#type: vec!["VerifiablePresentation".to_string()],
460 verifiable_credential: vec![vc],
461 holder: Some("did:key:holder".to_string()),
462 proof: None,
463 };
464
465 let jwt = sign_presentation_jwt(&vp, &kp, "kid", "real-nonce", "aud").unwrap();
466 let result = verify_presentation_jwt(&jwt, &kp.public, Some("wrong-nonce"), None);
467 assert!(result.is_err(), "Wrong nonce should be rejected");
468 }
469
470 #[test]
471 fn vp_wrong_audience_rejected() {
472 let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
473 let vc = sample_vc("did:key:issuer");
474 let vp = VerifiablePresentation {
475 context: vec!["https://www.w3.org/ns/credentials/v2".to_string()],
476 r#type: vec!["VerifiablePresentation".to_string()],
477 verifiable_credential: vec![vc],
478 holder: Some("did:key:holder".to_string()),
479 proof: None,
480 };
481
482 let jwt = sign_presentation_jwt(&vp, &kp, "kid", "nonce", "did:key:real-verifier").unwrap();
483 let result =
484 verify_presentation_jwt(&jwt, &kp.public, None, Some("did:key:wrong-verifier"));
485 assert!(result.is_err(), "Wrong audience should be rejected");
486 }
487
488 #[test]
489 fn vp_nonce_and_audience_accepted_when_correct() {
490 let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
491 let vc = sample_vc("did:key:issuer");
492 let vp = VerifiablePresentation {
493 context: vec!["https://www.w3.org/ns/credentials/v2".to_string()],
494 r#type: vec!["VerifiablePresentation".to_string()],
495 verifiable_credential: vec![vc],
496 holder: Some("did:key:holder".to_string()),
497 proof: None,
498 };
499
500 let jwt =
501 sign_presentation_jwt(&vp, &kp, "kid", "correct-nonce", "did:key:verifier").unwrap();
502 let result = verify_presentation_jwt(
503 &jwt,
504 &kp.public,
505 Some("correct-nonce"),
506 Some("did:key:verifier"),
507 );
508 assert!(result.is_ok());
509 }
510
511 #[test]
512 fn vp_no_validation_when_none_expected() {
513 let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
514 let vc = sample_vc("did:key:issuer");
515 let vp = VerifiablePresentation {
516 context: vec!["https://www.w3.org/ns/credentials/v2".to_string()],
517 r#type: vec!["VerifiablePresentation".to_string()],
518 verifiable_credential: vec![vc],
519 holder: Some("did:key:holder".to_string()),
520 proof: None,
521 };
522
523 let jwt = sign_presentation_jwt(&vp, &kp, "kid", "any-nonce", "any-aud").unwrap();
524 let result = verify_presentation_jwt(&jwt, &kp.public, None, None);
526 assert!(result.is_ok());
527 }
528}