1use std::cell::OnceCell;
3
4use anyhow::Context as _;
5use serde::{
6 self, Deserialize as _, Serialize as _,
7 de::{Error as _, IntoDeserializer as _},
8 ser::Error as _,
9};
10
11use crate::api;
12use crate::misc::jwt;
13use crate::misc::serde_ext::bytes_wrapper::B64UU;
14
15#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
20pub struct ExtendedSessionRequest {
21 request: SessionRequest,
23
24 #[serde(rename = "nextSession")]
27 #[serde(skip_serializing_if = "Option::is_none")]
28 pub next_session: Option<NextSessionData>,
29}
30
31#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
33pub struct NextSessionData {
34 pub url: url::Url,
35}
36
37#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
39pub struct SessionRequest {
40 #[serde(rename = "@context")]
41 context: LdContext,
42
43 disclose: Option<AttributeConDisCon>,
45
46 credentials: Option<Vec<CredentialToBeIssued>>,
48
49 #[serde(default)]
51 #[serde(skip_serializing_if = "Vec::is_empty")]
52 #[serde(rename = "skipExpiryCheck")]
53 skip_expiry_check: Vec<CredentialTypeIdentifier>,
54}
55
56impl ExtendedSessionRequest {
57 pub fn disclosure(cdc: AttributeConDisCon) -> Self {
58 Self {
59 request: SessionRequest {
60 context: LdContext::Disclosure,
61 disclose: Some(cdc),
62 credentials: None,
63 skip_expiry_check: vec![],
64 },
65 next_session: None,
66 }
67 }
68
69 pub fn issuance(credentials: Vec<CredentialToBeIssued>) -> Self {
70 Self {
71 request: SessionRequest {
72 context: LdContext::Issuance,
73 disclose: None,
74 credentials: Some(credentials),
75 skip_expiry_check: vec![],
76 },
77 next_session: None,
78 }
79 }
80
81 pub fn next_session(self, url: url::Url) -> Self {
83 Self {
84 next_session: Some(NextSessionData { url }),
85 ..self
86 }
87 }
88
89 pub fn sign(self, creds: &Credentials<SigningKey>) -> anyhow::Result<jwt::JWT> {
95 creds
96 .key
97 .sign(
98 &jwt::Claims::new()
99 .iat_now()?
100 .claim("iss", &creds.name)? .claim("sub", self.request.context.jwt_sub())?
102 .claim(self.request.context.jwt_key(), self)?,
103 )
104 .context("signing session request")
105
106 }
109
110 pub fn open_signed(
112 jwt: &jwt::JWT,
113 requestor_credentials: &Credentials<VerifyingKey>,
114 ) -> anyhow::Result<Self> {
115 let mut session_type_perhaps: Option<String> = None;
116
117 let mut claims = requestor_credentials
118 .key
119 .open(jwt)
120 .context("invalid jwt")?
121 .check_iss(jwt::expecting::exactly(&requestor_credentials.name))?
122 .check_sub(
123 |claim_name: &'static str, sub: Option<String>| -> Result<(), jwt::Error> {
124 let sub = sub.ok_or_else(|| jwt::Error::MissingClaim(claim_name))?;
125
126 assert!(
127 session_type_perhaps.replace(sub.to_string()).is_none(),
128 "bug: did not expect to set session_type twice"
129 );
130
131 Ok(())
132 },
133 )?;
134
135 let session_type = LdContext::from_jwt_sub(
136 &session_type_perhaps.expect("bug: expected session_type to be set here"),
137 )
138 .context("unknown subject in signed extended session request")?;
139
140 let session_request: Self = claims
141 .extract(session_type.jwt_key())?
142 .with_context(|| format!("missing claim {}", session_type.jwt_key()))?;
143
144 anyhow::ensure!(
145 session_request.request.context == session_type,
146 "session request jwt subject's, {}, does not align with session request context, {}",
147 session_type.jwt_sub(),
148 session_request.request.context.jwt_sub()
149 );
150
151 Ok(session_request)
152 }
153
154 pub fn mock_disclosure_response(
163 &self,
164 df: impl Fn(&AttributeTypeIdentifier) -> String,
165 ) -> SessionResult {
166 assert_eq!(self.request.context, LdContext::Disclosure);
167
168 let disclosed: Vec<Vec<DisclosedAttribute>> = self
169 .request
170 .disclose
171 .as_ref()
172 .expect("missing `disclose` field in disclosure session request")
173 .iter()
174 .map(|dc: &Vec<Vec<AttributeRequest>>| {
175 assert_eq!(dc.len(), 1, "'discon's not supported by this mock function");
176
177 let con_req: &Vec<AttributeRequest> = &dc[0];
178
179 let con_resp: Vec<DisclosedAttribute> = con_req
180 .iter()
181 .map(|ar: &AttributeRequest| {
182 DisclosedAttribute::mock(df(&ar.ty), ar.ty.clone())
183 })
184 .collect();
185
186 con_resp
187 })
188 .collect();
189
190 SessionResult::mock_disclosure(disclosed)
191 }
192}
193
194#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
199pub enum LdContext {
200 #[serde(rename = "https://irma.app/ld/request/disclosure/v2")]
201 Disclosure,
202
203 #[serde(rename = "https://irma.app/ld/request/signature/v2")]
204 Signature,
205
206 #[serde(rename = "https://irma.app/ld/request/issuance/v2")]
207 Issuance,
208}
209
210impl LdContext {
211 pub fn from_jwt_sub(jwt_sub: &str) -> Option<LdContext> {
212 match jwt_sub {
213 "verification_request" => Some(LdContext::Disclosure),
214 "signature_request" => Some(LdContext::Signature),
215 "issue_request" => Some(LdContext::Issuance),
216 _ => None,
217 }
218 }
219
220 pub const fn jwt_sub(&self) -> &'static str {
222 match self {
223 LdContext::Disclosure => "verification_request",
224 LdContext::Signature => "signature_request",
225 LdContext::Issuance => "issue_request",
226 }
227 }
228
229 pub const fn jwt_key(&self) -> &'static str {
231 match self {
232 LdContext::Disclosure => "sprequest",
233 LdContext::Signature => "absrequest",
234 LdContext::Issuance => "iprequest",
235 }
236 }
237}
238
239pub type AttributeConDisCon = Vec<Vec<Vec<AttributeRequest>>>;
241
242#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
244pub struct AttributeRequest {
245 #[serde(rename = "type")] pub ty: AttributeTypeIdentifier,
247
248 #[serde(default)]
249 #[serde(skip_serializing_if = "Option::is_none")]
250 pub value: Option<String>,
251}
252
253#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
256pub struct CredentialToBeIssued {
257 validity: Option<api::NumericDate>,
258
259 #[serde(rename = "credential")]
260 type_id: CredentialTypeIdentifier,
261
262 attributes: std::collections::HashMap<String, String>,
263}
264
265impl CredentialToBeIssued {
266 pub fn new(type_id: CredentialTypeIdentifier) -> Self {
267 Self {
268 validity: None,
269 type_id,
270 attributes: Default::default(),
271 }
272 }
273
274 pub fn valid_for(self, duration: std::time::Duration) -> Self {
275 Self {
276 validity: Some(api::NumericDate::now() + duration),
277 ..self
278 }
279 }
280
281 pub fn attribute(mut self, key: String, value: String) -> Self {
282 self.attributes.insert(key, value);
283 self
284 }
285}
286
287#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
291#[serde(deny_unknown_fields)]
292pub struct Credentials<K> {
293 pub name: String,
294 pub key: K,
295}
296
297impl Credentials<SigningKey> {
298 pub fn to_verifying_credentials(&self) -> Credentials<VerifyingKey> {
300 Credentials {
301 name: self.name.clone(),
302 key: self.key.to_verifying_key(),
303 }
304 }
305}
306
307#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
309#[serde(deny_unknown_fields)]
310pub enum SigningKey {
311 #[serde(rename = "hs256")]
312 HS256(B64UU<jwt::HS256>),
313
314 #[serde(rename = "rs256")]
317 RS256(#[serde(with = "rs256sk_encoding")] Box<jwt::RS256Sk>),
318}
319
320mod rs256sk_encoding {
322 use super::*;
323
324 pub fn deserialize<'de, D>(d: D) -> Result<Box<jwt::RS256Sk>, D::Error>
325 where
326 D: serde::Deserializer<'de>,
327 {
328 use std::borrow::Cow;
329
330 let s: Cow<'de, str> = Cow::<'de, str>::deserialize(d)?;
331
332 Ok(Box::new(
333 jwt::RS256Sk::from_pkcs8_pem(&s).map_err(D::Error::custom)?,
334 ))
335 }
336
337 #[expect(clippy::borrowed_box)]
339 pub fn serialize<S>(pk: &Box<jwt::RS256Sk>, s: S) -> Result<S::Ok, S::Error>
340 where
341 S: serde::ser::Serializer,
342 {
343 s.serialize_str(&pk.to_pkcs8_pem().map_err(S::Error::custom)?)
344 }
345}
346
347impl SigningKey {
348 fn sign<C: serde::Serialize>(&self, claims: &C) -> Result<jwt::JWT, jwt::Error> {
353 match self {
354 SigningKey::HS256(key) => jwt::JWT::create(claims, &**key),
355 SigningKey::RS256(key) => jwt::JWT::create(claims, &**key),
356 }
357 }
358
359 pub fn to_verifying_key(&self) -> VerifyingKey {
360 match self {
361 SigningKey::HS256(key) => VerifyingKey::HS256(key.clone()),
362 SigningKey::RS256(key) => {
363 VerifyingKey::RS256(jwt::RS256Vk::new(key.as_rsa_pub().clone()))
364 }
365 }
366 }
367}
368
369#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
371#[serde(deny_unknown_fields)]
372pub enum VerifyingKey {
373 #[serde(rename = "hs256")]
374 HS256(B64UU<jwt::HS256>),
375
376 #[serde(rename = "rs256")]
377 RS256(#[serde(with = "rs256vk_encoding")] jwt::RS256Vk),
378 }
381
382mod rs256vk_encoding {
384 use super::*;
385
386 pub fn deserialize<'de, D>(d: D) -> Result<jwt::RS256Vk, D::Error>
387 where
388 D: serde::Deserializer<'de>,
389 {
390 let s: &'de str = <&'de str>::deserialize(d)?;
391
392 jwt::RS256Vk::from_public_key_pem(s).map_err(D::Error::custom)
393 }
394
395 pub fn serialize<S>(pk: &jwt::RS256Vk, s: S) -> Result<S::Ok, S::Error>
396 where
397 S: serde::ser::Serializer,
398 {
399 s.serialize_str(&pk.to_public_key_pem().map_err(S::Error::custom)?)
400 }
401}
402
403impl VerifyingKey {
404 fn open(&self, jwt: &jwt::JWT) -> Result<jwt::Claims, jwt::Error> {
406 match self {
407 VerifyingKey::HS256(key) => jwt::JWT::open(jwt, &**key),
408 VerifyingKey::RS256(key) => jwt::JWT::open(jwt, key),
409 }
410 }
411}
412
413#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
417pub struct SessionResult {
418 pub token: RequestorToken,
419 pub status: Status,
420
421 #[serde(rename = "type")]
422 pub session_type: SessionType,
423
424 #[serde(rename = "proofStatus")]
425 pub proof_status: Option<ProofStatus>,
426
427 pub disclosed: Option<Vec<Vec<DisclosedAttribute>>>,
428
429 pub error: Option<RemoteError>,
431
432 #[serde(rename = "nextSession")]
433 pub next_session: Option<RequestorToken>,
434}
435
436impl SessionResult {
437 fn mock_disclosure(disclosed: Vec<Vec<DisclosedAttribute>>) -> SessionResult {
439 SessionResult {
440 token: "1234567890abcdefghij".parse().unwrap(),
441 status: Status::Done,
442 session_type: SessionType::Disclosing,
443 proof_status: Some(ProofStatus::Valid),
444 disclosed: Some(disclosed),
445 error: None,
446 next_session: None,
447 }
448 }
449}
450
451impl SessionResult {
452 pub fn sign(
456 self,
457 creds: &Credentials<SigningKey>,
458 validity: std::time::Duration,
459 ) -> anyhow::Result<jwt::JWT> {
460 creds
461 .key
462 .sign(
463 &jwt::Claims::from_custom(&self)?
464 .iat_now()?
465 .exp_after(validity)?
466 .claim("iss", &creds.name)? .claim("sub", self.session_type.to_result_sub())?,
468 )
469 .context("signing session result")
470 }
471
472 pub fn open_signed(
474 jwt: &jwt::JWT,
475 server_credentials: &Credentials<VerifyingKey>,
476 ) -> anyhow::Result<Self> {
477 let mut session_type_perhaps: Option<SessionType> = None;
478
479 let session_result: Self = server_credentials
480 .key
481 .open(jwt)
482 .context("invalid jwt")?
483 .check_iss(jwt::expecting::exactly(&server_credentials.name))?
484 .check_sub(
485 |claim_name: &'static str, sub: Option<String>| -> Result<(), jwt::Error> {
486 let sub = sub.ok_or_else(|| jwt::Error::MissingClaim(claim_name))?;
487
488 assert!(
489 session_type_perhaps
490 .replace(SessionType::from_result_sub(&sub).map_err(|err| {
491 jwt::Error::InvalidClaim {
492 claim_name,
493 source: err,
494 }
495 })?)
496 .is_none(),
497 "bug: did not expect to set session_type twice"
498 );
499
500 Ok(())
501 },
502 )?
503 .into_custom()?;
504
505 let session_type = session_type_perhaps.expect("bug: expected session_type to be set here");
506
507 anyhow::ensure!(
508 session_result.session_type == session_type,
509 "session result jwt subject, {}, does not align with session result type, {}",
510 session_type,
511 session_result.session_type
512 );
513
514 Ok(session_result)
515 }
516
517 fn validate_except_disclosed(&self) -> anyhow::Result<()> {
519 anyhow::ensure!(
520 self.status == Status::Done || self.status == Status::Connected,
521 "session status is not 'done' (or 'connected'), but {}",
524 self.status
525 );
526
527 if let Some(proof_status) = self.proof_status {
528 anyhow::ensure!(
529 proof_status == ProofStatus::Valid,
530 "session proof status is not 'valid', but {}",
531 proof_status
532 );
533 }
534
535 if let Some(error) = &self.error {
536 anyhow::bail!(
537 "session result error field set: {}",
538 serde_json::to_string(&error).unwrap_or_else(|_| "<ERROR SERIALIZING>".to_string()),
539 );
540 }
541
542 Ok(())
543 }
544
545 pub fn validate_and_extract_raw_singles(
548 &self,
549 ) -> anyhow::Result<impl Iterator<Item = anyhow::Result<(&AttributeTypeIdentifier, &str)>>>
550 {
551 self.validate_except_disclosed()?;
552
553 Ok(self
554 .disclosed
555 .iter()
556 .flatten()
557 .map(|inner_con: &Vec<DisclosedAttribute>| {
558 let da: &DisclosedAttribute = inner_con
559 .first()
560 .context("inner conjunction has no attribute")?;
561
562 da.validate()?;
563
564 Ok((&da.id, da.raw_value.as_str()))
565 }))
566 }
567}
568
569#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
573pub struct DisclosedAttribute {
574 #[serde(rename = "rawvalue")]
575 pub raw_value: String,
576
577 pub id: AttributeTypeIdentifier,
580
581 pub status: AttributeProofStatus,
582
583 #[serde(rename = "issuancetime")]
584 pub issuance_time: api::NumericDate,
585
586 #[serde(rename = "notrevoked")]
587 pub not_revoked: Option<bool>,
588
589 #[serde(rename = "notrevokedbefore")]
590 pub not_revoked_before: Option<api::NumericDate>,
591}
592
593impl DisclosedAttribute {
594 fn mock(raw_value: String, id: AttributeTypeIdentifier) -> Self {
595 Self {
596 raw_value,
597 id,
598 status: AttributeProofStatus::Present,
599 issuance_time: api::NumericDate::now(),
600 not_revoked: None,
601 not_revoked_before: None,
602 }
603 }
604
605 fn validate(&self) -> anyhow::Result<()> {
607 anyhow::ensure!(
608 self.status == AttributeProofStatus::Present,
609 "proof status is not 'present'"
610 );
611
612 if self.not_revoked == Some(false) {
613 anyhow::bail!("attribute is revoked");
614 }
615
616 if let Some(not_revoked_before) = self.not_revoked_before
617 && api::NumericDate::now() > not_revoked_before
618 {
619 anyhow::bail!("attribute is (presumably) revoked after {not_revoked_before}");
620 }
621
622 Ok(())
623 }
624}
625
626#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Eq, PartialEq)]
630#[serde(transparent)]
631pub struct RequestorToken {
632 #[serde(deserialize_with = "RequestorToken::deserialize_inner")]
633 inner: String,
634}
635
636const REQUESTOR_TOKEN_REGEX: &str = r"^[a-z0-9A-Z]{20}$";
640
641thread_local! {
642 static REQUESTOR_TOKEN_REGEX_TLK: OnceCell<regex::Regex> = const { OnceCell::new() };
644}
645
646fn with_requestor_token_regex<R>(f: impl FnOnce(®ex::Regex) -> R) -> R {
649 REQUESTOR_TOKEN_REGEX_TLK.with(|oc: &OnceCell<regex::Regex>| {
650 f(oc.get_or_init(|| regex::Regex::new(REQUESTOR_TOKEN_REGEX).unwrap()))
651 })
652}
653
654impl RequestorToken {
655 fn deserialize_inner<'de, D>(d: D) -> Result<String, D::Error>
656 where
657 D: serde::de::Deserializer<'de>,
658 {
659 let inner: String = String::deserialize(d)?;
660
661 Self::validate_inner(&inner).map_err(D::Error::custom)?;
662
663 Ok(inner)
664 }
665
666 fn validate_inner(inner: &str) -> anyhow::Result<()> {
668 if !with_requestor_token_regex(|r: ®ex::Regex| r.is_match(inner)) {
669 anyhow::bail!(
670 "invalid yivi requestor token: did not match regex {}",
671 REQUESTOR_TOKEN_REGEX
672 );
673 }
674 Ok(())
675 }
676}
677
678impl std::str::FromStr for RequestorToken {
679 type Err = anyhow::Error;
680
681 fn from_str(inner: &str) -> Result<Self, Self::Err> {
682 Self::validate_inner(inner)?;
683 Ok(Self {
684 inner: inner.to_string(),
685 })
686 }
687}
688
689#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Eq, PartialEq, Hash)]
701#[serde(transparent)]
702pub struct AttributeTypeIdentifier {
703 #[serde(deserialize_with = "AttributeTypeIdentifier::deserialize_inner")]
704 inner: String,
705}
706
707impl AttributeTypeIdentifier {
708 fn deserialize_inner<'de, D>(d: D) -> Result<String, D::Error>
709 where
710 D: serde::de::Deserializer<'de>,
711 {
712 let inner: String = String::deserialize(d)?;
713
714 Self::validate_inner(&inner).map_err(D::Error::custom)?;
715
716 Ok(inner)
717 }
718
719 fn validate_inner(inner: &str) -> anyhow::Result<()> {
721 let dot_count: usize = inner.chars().filter(|c: &char| *c == '.').count();
722
723 if dot_count == 2 {
724 anyhow::bail!("we do not support yivi attribute identifiers with two dots");
725 }
726
727 anyhow::ensure!(
728 dot_count == 3,
729 "invalid yivi attribute type identifier: does not contain three dots"
730 );
731
732 Ok(())
733 }
734
735 pub fn as_str(&self) -> &str {
737 &self.inner
738 }
739}
740
741impl std::str::FromStr for AttributeTypeIdentifier {
742 type Err = anyhow::Error;
743
744 fn from_str(inner: &str) -> Result<Self, Self::Err> {
745 Self::validate_inner(inner)?;
746 Ok(Self {
747 inner: inner.to_string(),
748 })
749 }
750}
751
752impl std::fmt::Display for AttributeTypeIdentifier {
753 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
754 self.inner.fmt(f)
755 }
756}
757
758#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Eq, PartialEq)]
760#[serde(transparent)]
761pub struct CredentialTypeIdentifier {
762 #[serde(deserialize_with = "CredentialTypeIdentifier::deserialize_inner")]
763 inner: String,
764}
765
766impl CredentialTypeIdentifier {
767 fn deserialize_inner<'de, D>(d: D) -> Result<String, D::Error>
768 where
769 D: serde::de::Deserializer<'de>,
770 {
771 let inner: String = String::deserialize(d)?;
772
773 Self::validate_inner(&inner).map_err(D::Error::custom)?;
774
775 Ok(inner)
776 }
777
778 fn validate_inner(inner: &str) -> anyhow::Result<()> {
780 let dot_count: usize = inner.chars().filter(|c: &char| *c == '.').count();
781
782 anyhow::ensure!(
783 dot_count == 2,
784 "invalid yivi credential type identifier: does not contain two dots"
785 );
786
787 Ok(())
788 }
789
790 pub fn as_str(&self) -> &str {
792 &self.inner
793 }
794}
795
796impl std::str::FromStr for CredentialTypeIdentifier {
797 type Err = anyhow::Error;
798
799 fn from_str(inner: &str) -> Result<Self, Self::Err> {
800 Self::validate_inner(inner)?;
801 Ok(Self {
802 inner: inner.to_string(),
803 })
804 }
805}
806
807impl std::fmt::Display for CredentialTypeIdentifier {
808 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
809 self.inner.fmt(f)
810 }
811}
812
813#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Clone)]
817pub struct RemoteError {
818 pub status: Option<u64>,
819 pub error: Option<String>,
820 pub description: Option<String>,
821 pub message: Option<String>,
822 pub stacktrace: Option<String>,
823}
824
825#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
829#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
830pub enum ProofStatus {
831 Valid,
832 Invalid,
833 InvalidTimestamp,
834 UnmatchedRequest,
835 MissingAttributes,
836 Expired,
837}
838
839impl std::fmt::Display for ProofStatus {
840 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
841 self.serialize(f)
842 }
843}
844
845#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
849#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
850pub enum Status {
851 Done,
852 Pairing,
853 Connected,
854 Cancelled,
855 Timeout,
856 Initialized,
857}
858
859impl std::fmt::Display for Status {
860 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
861 self.serialize(f)
862 }
863}
864
865#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
869#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
870pub enum AttributeProofStatus {
871 Present,
872 Extra,
873 Null,
874}
875
876#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
880#[serde(rename_all = "lowercase")]
881pub enum SessionType {
882 Disclosing,
883 Signing,
884 Issuing,
885 Redirect,
886 Revoking,
887 Unknown,
888}
889
890impl std::fmt::Display for SessionType {
891 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
892 self.serialize(f)
893 }
894}
895
896impl std::str::FromStr for SessionType {
897 type Err = serde::de::value::Error;
898
899 fn from_str(s: &str) -> Result<Self, Self::Err> {
900 Self::deserialize(s.into_deserializer())
901 }
902}
903
904impl SessionType {
905 fn from_result_sub(sub: &str) -> anyhow::Result<Self> {
907 let stripped_sub = sub
908 .strip_suffix("_result")
909 .ok_or_else(|| anyhow::anyhow!("subject did not end with '_result'"))?;
910
911 stripped_sub.parse().context("unknown session type")
912 }
913
914 fn to_result_sub(self) -> String {
916 format!("{self}_result")
917 }
918}
919
920#[derive(
923 Debug, Clone, Copy, PartialOrd, Ord, Eq, PartialEq, serde::Serialize, serde::Deserialize,
924)]
925#[serde(transparent)]
926pub struct Epoch {
927 seqnr: u64,
928}
929
930impl Epoch {
931 pub const fn seconds() -> u64 {
933 60 * 60 * 24 * 7
934 }
935
936 pub fn current() -> Epoch {
938 api::NumericDate::now().into()
939 }
940
941 pub fn with_seqnr(seqnr: u64) -> Self {
943 Self { seqnr }
944 }
945
946 pub fn starts(&self) -> api::NumericDate {
948 api::NumericDate::new(self.seqnr * Self::seconds())
949 }
950
951 pub fn ends(&self) -> api::NumericDate {
953 api::NumericDate::new((self.seqnr + 1) * Self::seconds())
954 }
955}
956
957impl From<api::NumericDate> for Epoch {
958 fn from(nd: api::NumericDate) -> Self {
959 Self {
960 seqnr: nd.timestamp() / Self::seconds(),
961 }
962 }
963}
964
965impl std::fmt::Display for Epoch {
966 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
967 write!(
968 f,
969 "Yivi epoch {} that starts {} and ends {}",
970 self.seqnr,
971 self.starts(),
972 self.ends()
973 )
974 }
975}
976
977#[cfg(test)]
978mod test {
979 use super::*;
980 use std::str::FromStr as _;
981
982 #[test]
983 fn requestor_token() {
984 let r1: RequestorToken = "1234567890abcdefghij".parse().unwrap();
985 let r2: RequestorToken = serde_json::from_str("\"1234567890abcdefghij\"").unwrap();
986 assert_eq!(r1, r2);
987
988 assert!(RequestorToken::from_str("1234567890 abcdefghij").is_err());
989 }
990
991 #[test]
992 fn attribute_type_identifier() {
993 let ati = serde_json::from_str::<AttributeTypeIdentifier>("\"a.b.c.d\"").unwrap();
994 assert_eq!(serde_json::to_string(&ati).unwrap(), "\"a.b.c.d\"");
995 assert!(serde_json::from_str::<AttributeTypeIdentifier>("\"a.b.c\"").is_err());
996 }
997
998 #[test]
999 fn test_session_result_validation() {
1000 let sr: SessionResult = serde_json::from_value(serde_json::json!({
1001 "disclosed": [
1002 [
1003 {
1004 "id": "irma-demo.sidn-pbdf.email.email",
1005 "issuancetime": 1735776000,
1006 "rawvalue": "test@test.com",
1007 "status": "PRESENT",
1008 "value": {
1009 "": "test@test.com",
1010 "en": "test@test.com",
1011 "nl": "test@test.com"
1012 }
1013 }
1014 ],
1015 [
1016 {
1017 "id": "irma-demo.sidn-pbdf.mobilenumber.mobilenumber",
1018 "issuancetime": 1735776000,
1019 "rawvalue": "0612345678",
1020 "status": "PRESENT",
1021 "value": {
1022 "": "0612345678",
1023 "en": "0612345678",
1024 "nl": "0612345678"
1025 }
1026 }
1027 ]
1028 ],
1029 "proofStatus": "VALID",
1030 "status": "DONE",
1031 "token": "KDRkE7LE0jIPhIBNdoBb",
1032 "type": "disclosing"
1033 }))
1034 .unwrap();
1035
1036 let mut results: Vec<(&AttributeTypeIdentifier, &str)> = sr
1037 .validate_and_extract_raw_singles()
1038 .unwrap()
1039 .map(|r| r.unwrap())
1040 .collect();
1041
1042 results.sort_by_key(|(_, v)| v.to_string());
1044
1045 assert_eq!(
1046 results,
1047 vec![
1048 (
1049 &AttributeTypeIdentifier::from_str(
1050 "irma-demo.sidn-pbdf.mobilenumber.mobilenumber"
1051 )
1052 .unwrap(),
1053 "0612345678"
1054 ),
1055 (
1056 &AttributeTypeIdentifier::from_str("irma-demo.sidn-pbdf.email.email").unwrap(),
1057 "test@test.com"
1058 ),
1059 ]
1060 );
1061 }
1062}