1use bh_jws_utils::{jwt, JwkPublic, JwtSigner, JwtVerifier, SigningAlgorithm};
17use iref::Uri;
18use serde::{Deserialize, Serialize};
19
20use crate::{
21 token,
22 utils::jwt::{sign_jwt_token, verify_jwt_token},
23 Error, Result, StatusList, UriBuf,
24};
25
26const STATUS_LIST_TOKEN_TYP: &str = "statuslist+jwt";
27
28pub struct StatusListToken<S>(jwt::Token<StatusListTokenHeader, StatusListTokenClaims, S>);
34
35impl StatusListToken<token::Signed> {
36 pub fn new(claims: StatusListTokenClaims, kid: String, key: &impl JwtSigner) -> Result<Self> {
49 let alg = key.algorithm();
50
51 let header = StatusListTokenHeader {
52 alg,
53 kid,
54 typ: STATUS_LIST_TOKEN_TYP.to_owned(),
55 };
56
57 let signed_token = sign_jwt_token(header, claims, key)?;
58
59 Ok(Self(signed_token))
60 }
61
62 pub fn as_str(&self) -> &str {
64 self.0.as_str()
65 }
66}
67
68impl std::fmt::Display for StatusListToken<token::Signed> {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 f.write_str(self.as_str())
71 }
72}
73
74impl From<StatusListToken<token::Verified>> for (StatusListTokenHeader, StatusListTokenClaims) {
75 fn from(token: StatusListToken<token::Verified>) -> Self {
76 token.0.into()
77 }
78}
79
80impl StatusListToken<token::Verified> {
81 pub fn verify(
109 token: &str,
110 verifier: &(impl JwtVerifier + ?Sized),
111 public_key: &JwkPublic,
112 current_time: u64,
113 iss: &Uri,
114 sub: &Uri,
115 ) -> Result<Self> {
116 let verified_token = verify_jwt_token(token, verifier, public_key)?;
118
119 verified_token.header().verify()?;
121
122 verified_token.claims().verify(current_time, iss, sub)?;
124
125 Ok(Self(verified_token))
126 }
127
128 pub fn header(&self) -> &StatusListTokenHeader {
130 self.0.header()
131 }
132
133 pub fn claims(&self) -> &StatusListTokenClaims {
135 self.0.claims()
136 }
137}
138
139#[derive(Serialize, Deserialize)]
141#[non_exhaustive]
142pub struct StatusListTokenHeader {
143 pub alg: SigningAlgorithm,
145
146 pub kid: String,
148
149 pub typ: String,
151}
152
153impl StatusListTokenHeader {
154 fn verify(&self) -> Result<()> {
159 if self.typ != STATUS_LIST_TOKEN_TYP {
160 return Err(bherror::Error::root(Error::InvalidTokenHeaderTyp(
161 self.typ.to_owned(),
162 )));
163 }
164
165 Ok(())
166 }
167}
168
169impl jwt::JoseHeader for StatusListTokenHeader {
170 fn algorithm_type(&self) -> jwt::AlgorithmType {
171 self.alg.into()
172 }
173}
174
175#[derive(Serialize, Deserialize)]
177#[non_exhaustive]
178pub struct StatusListTokenClaims {
179 pub iss: UriBuf,
184
185 pub sub: UriBuf,
190
191 pub iat: u64,
195
196 #[serde(skip_serializing_if = "Option::is_none")]
201 pub exp: Option<u64>,
202
203 #[serde(skip_serializing_if = "Option::is_none")]
207 pub ttl: Option<u64>,
208
209 pub status_list: StatusList,
211}
212
213impl StatusListTokenClaims {
214 pub fn new(
236 iss: UriBuf,
237 sub: UriBuf,
238 iat: u64,
239 exp: Option<u64>,
240 ttl: Option<u64>,
241 status_list: StatusList,
242 ) -> Self {
243 Self {
244 iss,
245 sub,
246 iat,
247 exp,
248 ttl,
249 status_list,
250 }
251 }
252
253 fn verify(&self, current_time: u64, iss: &Uri, sub: &Uri) -> Result<()> {
263 if self.iss != iss {
264 return Err(bherror::Error::root(Error::InvalidTokenIss(
265 iss.to_owned(),
266 self.iss.clone(),
267 )));
268 }
269
270 if self.sub != sub {
271 return Err(bherror::Error::root(Error::InvalidTokenSub(
272 sub.to_owned(),
273 self.sub.clone(),
274 )));
275 }
276
277 if let Some(exp) = self.exp {
278 if exp < current_time {
279 return Err(bherror::Error::root(Error::TokenExpired(current_time, exp)));
280 }
281 }
282
283 Ok(())
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use std::str::FromStr as _;
290
291 use bh_jws_utils::{
292 json_object, BoxError,
293 SigningAlgorithm::{self, *},
294 };
295
296 use super::*;
297 use crate::{JwtError, StatusBits, StatusListInternal};
298
299 fn str_to_uri(s: &str) -> UriBuf {
300 UriBuf::from_str(s).unwrap()
301 }
302
303 struct DummySigner(SigningAlgorithm, bool);
304
305 impl bh_jws_utils::Signer for DummySigner {
306 fn algorithm(&self) -> SigningAlgorithm {
307 self.0
308 }
309
310 fn sign(&self, _message: &[u8]) -> std::result::Result<Vec<u8>, BoxError> {
311 self.1
312 .then(|| b"signatureb64".to_vec())
313 .ok_or(Box::new(JwtError::InvalidSignature))
314 }
315
316 fn public_jwk(&self) -> std::result::Result<JwkPublic, BoxError> {
317 unimplemented!("this is currently not needed")
318 }
319 }
320
321 struct DummyVerifier(SigningAlgorithm, std::result::Result<bool, ()>);
322
323 impl bh_jws_utils::SignatureVerifier for DummyVerifier {
324 fn algorithm(&self) -> SigningAlgorithm {
325 self.0
326 }
327
328 fn verify(&self, _: &[u8], _: &[u8], _: &JwkPublic) -> std::result::Result<bool, BoxError> {
329 self.1.map_err(|_| Box::new(JwtError::Format) as _)
330 }
331 }
332
333 fn dummy_jwk() -> JwkPublic {
334 json_object!({})
335 }
336
337 fn get_valid_jwt(alg: SigningAlgorithm, iss: UriBuf, sub: UriBuf, exp: Option<u64>) -> String {
338 let status_list = StatusListInternal::new(StatusBits::Eight, Option::None);
339
340 let claims =
341 StatusListTokenClaims::new(iss, sub, 100, exp, Option::None, status_list.into());
342
343 let token_signed =
344 StatusListToken::new(claims, "kid".to_owned(), &DummySigner(alg, true)).unwrap();
345
346 token_signed.to_string()
347 }
348
349 #[test]
350 fn test_status_list_token_signing_fails() {
351 let status_list = StatusListInternal::new(StatusBits::Eight, Option::None);
352
353 let claims = StatusListTokenClaims::new(
354 str_to_uri("http://iss"),
355 str_to_uri("http://sub"),
356 100,
357 Option::None,
358 Option::None,
359 status_list.into(),
360 );
361
362 let err = StatusListToken::new(claims, "kid".to_owned(), &DummySigner(Es512, false))
363 .err()
364 .unwrap();
365
366 assert!(matches!(err.error, Error::TokenSigningFailed));
367 }
368
369 #[test]
370 fn test_status_list_token_new_success() {
371 let status_list = StatusListInternal::new(StatusBits::Two, Option::None);
372
373 let claims = StatusListTokenClaims::new(
374 str_to_uri("http://iss"),
375 str_to_uri("http://sub"),
376 100,
377 Option::None,
378 Option::None,
379 status_list.into(),
380 );
381
382 let _token =
383 StatusListToken::new(claims, "kid".to_owned(), &DummySigner(Es256, true)).unwrap();
384 }
385
386 #[test]
387 fn test_status_list_token_verify_success() {
388 let status_list = StatusListInternal::new(StatusBits::One, Option::None);
389
390 let iss = str_to_uri("http://iss");
391 let sub = str_to_uri("http://sub");
392 let iat = 100;
393
394 let claims = StatusListTokenClaims::new(
395 iss.clone(),
396 sub.clone(),
397 iat,
398 Option::None,
399 Option::None,
400 status_list.into(),
401 );
402
403 let token_signed =
404 StatusListToken::new(claims, "kid".to_owned(), &DummySigner(Es512, true)).unwrap();
405
406 let token_verified = StatusListToken::verify(
407 token_signed.as_str(),
408 &DummyVerifier(Es512, Ok(true)),
409 &dummy_jwk(),
410 100,
411 &iss,
412 &sub,
413 )
414 .unwrap();
415
416 let (header, claims) = token_verified.into();
417
418 assert_eq!(header.alg, Es512);
419 assert_eq!(header.kid, "kid");
420 assert_eq!(header.typ, STATUS_LIST_TOKEN_TYP);
421
422 assert_eq!(claims.iss, iss);
423 assert_eq!(claims.sub, sub);
424 assert_eq!(claims.iat, iat);
425 }
426
427 #[test]
428 fn test_status_list_token_verify_parse_fails() {
429 let err = StatusListToken::verify(
430 "invalid-token",
431 &DummyVerifier(Ps384, Ok(true)),
432 &dummy_jwk(),
433 100,
434 &str_to_uri("http://iss"),
435 &str_to_uri("http://sub"),
436 )
437 .err()
438 .unwrap();
439
440 assert!(matches!(err.error, Error::TokenParsingFailed));
441 }
442
443 #[test]
444 fn test_status_list_token_verify_invalid_alg_fails() {
445 let iss = str_to_uri("http://iss");
446 let sub = str_to_uri("http://sub");
447
448 let jwt = get_valid_jwt(Ps512, iss.clone(), sub.clone(), Option::None);
449
450 let err = StatusListToken::verify(
451 &jwt,
452 &DummyVerifier(Ps256, Ok(true)),
453 &dummy_jwk(),
454 100,
455 &iss,
456 &sub,
457 )
458 .err()
459 .unwrap();
460
461 assert!(matches!(err.error, Error::TokenSignatureVerificationFailed));
462 }
463
464 #[test]
465 fn test_status_list_token_verify_signature_mismatch_fails() {
466 let iss = str_to_uri("http://iss");
467 let sub = str_to_uri("http://sub");
468
469 let jwt = get_valid_jwt(Ps384, iss.clone(), sub.clone(), Option::None);
470
471 let err = StatusListToken::verify(
472 &jwt,
473 &DummyVerifier(Ps384, Ok(false)),
474 &dummy_jwk(),
475 100,
476 &iss,
477 &sub,
478 )
479 .err()
480 .unwrap();
481
482 assert!(matches!(err.error, Error::TokenSignatureVerificationFailed));
483 }
484
485 #[test]
486 fn test_status_list_token_verify_signature_verification_fails() {
487 let iss = str_to_uri("http://iss");
488 let sub = str_to_uri("http://sub");
489
490 let jwt = get_valid_jwt(Ps384, iss.clone(), sub.clone(), Option::None);
491
492 let err = StatusListToken::verify(
493 &jwt,
494 &DummyVerifier(Ps384, Err(())),
495 &dummy_jwk(),
496 100,
497 &iss,
498 &sub,
499 )
500 .err()
501 .unwrap();
502
503 assert!(matches!(err.error, Error::TokenSignatureVerificationFailed));
504 }
505
506 #[test]
507 fn test_status_list_token_verify_iss_mismatch_fails() {
508 let iss = str_to_uri("http://iss");
509 let sub = str_to_uri("http://sub");
510
511 let iss_invalid = str_to_uri("http://iss-invalid");
512
513 let jwt = get_valid_jwt(Es512, iss_invalid.clone(), sub.clone(), Option::None);
514
515 let err = StatusListToken::verify(
516 &jwt,
517 &DummyVerifier(Es512, Ok(true)),
518 &dummy_jwk(),
519 100,
520 &iss,
521 &sub,
522 )
523 .err()
524 .unwrap();
525
526 assert!(
527 matches!(err.error, Error::InvalidTokenIss(iss_vc, iss_rec) if iss_vc == iss && iss_rec == iss_invalid)
528 );
529 }
530
531 #[test]
532 fn test_status_list_token_verify_sub_mismatch_fails() {
533 let iss = str_to_uri("http://iss");
534 let sub = str_to_uri("http://sub");
535
536 let sub_invalid = str_to_uri("http://sub-invalid");
537
538 let jwt = get_valid_jwt(Es256, iss.clone(), sub_invalid.clone(), Option::None);
539
540 let err = StatusListToken::verify(
541 &jwt,
542 &DummyVerifier(Es256, Ok(true)),
543 &dummy_jwk(),
544 100,
545 &iss,
546 &sub,
547 )
548 .err()
549 .unwrap();
550
551 assert!(
552 matches!(err.error, Error::InvalidTokenSub(sub_vc, sub_rec) if sub_vc == sub && sub_rec == sub_invalid)
553 );
554 }
555
556 #[test]
557 fn test_status_list_token_verify_token_expired_fails() {
558 let iss = str_to_uri("http://iss");
559 let sub = str_to_uri("http://sub");
560
561 let jwt = get_valid_jwt(Es256, iss.clone(), sub.clone(), Some(200));
562
563 let err = StatusListToken::verify(
564 &jwt,
565 &DummyVerifier(Es256, Ok(true)),
566 &dummy_jwk(),
567 300,
568 &iss,
569 &sub,
570 )
571 .err()
572 .unwrap();
573
574 assert!(matches!(err.error, Error::TokenExpired(curr, exp) if curr == 300 && exp == 200));
575 }
576}