1use chrono::{DateTime, Utc};
6use indexmap::IndexMap;
7use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
8use serde_json::Value;
9
10use crate::did::{Context, OneOrMany, Uri};
11
12use super::credential::push;
13use super::model::{CONTEXT_V2, append_context, check_context_and_type, push_type};
14use super::{CredentialError, Jwt, JwtClaims, Party, Subject, TypedObject, claims};
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47#[non_exhaustive]
48#[serde(remote = "Self")]
49pub struct CredentialV2 {
50 #[serde(rename = "@context")]
52 pub context: Context,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub id: Option<Uri>,
56 #[serde(rename = "type")]
58 pub types: OneOrMany<String>,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub name: Option<Value>,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub description: Option<Value>,
65 pub issuer: Party,
67 #[serde(rename = "validFrom", default, skip_serializing_if = "Option::is_none")]
69 pub valid_from: Option<DateTime<Utc>>,
70 #[serde(
72 rename = "validUntil",
73 default,
74 skip_serializing_if = "Option::is_none"
75 )]
76 pub valid_until: Option<DateTime<Utc>>,
77 #[serde(
79 rename = "credentialStatus",
80 default,
81 skip_serializing_if = "Option::is_none"
82 )]
83 pub credential_status: Option<OneOrMany<TypedObject>>,
84 #[serde(rename = "credentialSubject")]
86 pub credential_subject: OneOrMany<Subject>,
87 #[serde(
89 rename = "credentialSchema",
90 default,
91 skip_serializing_if = "Option::is_none"
92 )]
93 pub credential_schema: Option<OneOrMany<TypedObject>>,
94 #[serde(flatten)]
97 pub properties: IndexMap<String, Value>,
98}
99
100impl Serialize for CredentialV2 {
101 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
102 Self::serialize(self, serializer)
103 }
104}
105
106impl<'de> Deserialize<'de> for CredentialV2 {
107 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
109 let credential = Self::deserialize(deserializer)?;
110 credential.check().map_err(de::Error::custom)?;
111 Ok(credential)
112 }
113}
114
115impl CredentialV2 {
116 pub const CONTEXT: &'static str = CONTEXT_V2;
118
119 pub const TYPE: &'static str = "VerifiableCredential";
121
122 pub const JWT_TYP: &'static str = "vc+jwt";
124
125 pub const JWT_MEDIA_TYPE: &'static str = "application/vc+jwt";
127
128 pub fn new(issuer: impl Into<Party>, subject: Subject) -> Self {
131 Self {
132 context: Context::uri(Self::CONTEXT),
133 id: None,
134 types: OneOrMany::One(Self::TYPE.to_owned()),
135 name: None,
136 description: None,
137 issuer: issuer.into(),
138 valid_from: None,
139 valid_until: None,
140 credential_status: None,
141 credential_subject: OneOrMany::One(subject),
142 credential_schema: None,
143 properties: IndexMap::new(),
144 }
145 }
146
147 #[must_use]
149 pub fn with_context(mut self, context: impl Into<String>) -> Self {
150 self.context = append_context(self.context, context.into());
151 self
152 }
153
154 #[must_use]
156 pub fn with_id(mut self, id: impl Into<Uri>) -> Self {
157 self.id = Some(id.into());
158 self
159 }
160
161 #[must_use]
163 pub fn with_type(mut self, credential_type: impl Into<String>) -> Self {
164 push_type(&mut self.types, credential_type.into());
165 self
166 }
167
168 #[must_use]
170 pub fn with_name(mut self, name: impl Into<Value>) -> Self {
171 self.name = Some(name.into());
172 self
173 }
174
175 #[must_use]
177 pub fn with_description(mut self, description: impl Into<Value>) -> Self {
178 self.description = Some(description.into());
179 self
180 }
181
182 #[must_use]
184 pub fn with_valid_from(mut self, valid_from: DateTime<Utc>) -> Self {
185 self.valid_from = Some(valid_from);
186 self
187 }
188
189 #[must_use]
191 pub fn with_valid_until(mut self, valid_until: DateTime<Utc>) -> Self {
192 self.valid_until = Some(valid_until);
193 self
194 }
195
196 #[must_use]
198 pub fn with_subject(mut self, subject: Subject) -> Self {
199 let mut subjects =
200 core::mem::replace(&mut self.credential_subject, OneOrMany::Many(Vec::new()))
201 .into_vec();
202 subjects.push(subject);
203 self.credential_subject = OneOrMany::Many(subjects);
204 self
205 }
206
207 #[must_use]
209 pub fn with_status(mut self, status: TypedObject) -> Self {
210 self.credential_status = Some(push(self.credential_status.take(), status));
211 self
212 }
213
214 #[must_use]
216 pub fn with_schema(mut self, schema: TypedObject) -> Self {
217 self.credential_schema = Some(push(self.credential_schema.take(), schema));
218 self
219 }
220
221 #[must_use]
223 pub fn with_property(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
224 self.properties.insert(name.into(), value.into());
225 self
226 }
227
228 pub fn subject(&self) -> Option<&Subject> {
230 (self.credential_subject.len() == 1).then(|| self.credential_subject.first())?
231 }
232
233 pub fn has_type(&self, credential_type: &str) -> bool {
235 self.types.iter().any(|t| t == credential_type)
236 }
237
238 pub fn statuses(&self) -> impl Iterator<Item = &TypedObject> {
240 self.credential_status.iter().flat_map(OneOrMany::iter)
241 }
242
243 pub fn check(&self) -> Result<(), CredentialError> {
251 check_context_and_type(&self.context, Self::CONTEXT, &self.types, Self::TYPE)?;
252 if self.credential_subject.is_empty() {
253 return Err(CredentialError::NoSubject);
254 }
255 if let (Some(from), Some(until)) = (self.valid_from, self.valid_until)
256 && until < from
257 {
258 return Err(CredentialError::InvalidValidity {
259 from: "validFrom",
260 until: "validUntil",
261 });
262 }
263 Ok(())
264 }
265
266 pub fn to_jwt_claims(&self) -> JwtClaims {
270 JwtClaims {
271 additional: claims::to_object(self).into_iter().collect(),
272 ..JwtClaims::default()
273 }
274 }
275
276 pub fn from_jwt_claims(jwt_claims: &JwtClaims) -> Result<Self, CredentialError> {
285 let mut vc = payload_object(jwt_claims, "vc")?;
286 claims::merge_id(&mut vc, "id", "jti", jwt_claims.jti.as_deref())?;
287 claims::merge_id(&mut vc, "issuer", "iss", jwt_claims.iss.as_deref())?;
288 claims::merge_subject(&mut vc, jwt_claims.sub.as_deref())?;
289 serde_json::from_value(Value::Object(vc)).map_err(|e| CredentialError::InvalidClaim {
290 claim: "@context",
291 reason: e.to_string(),
292 })
293 }
294}
295
296#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
302#[non_exhaustive]
303#[serde(remote = "Self")]
304pub struct PresentationV2 {
305 #[serde(rename = "@context")]
307 pub context: Context,
308 #[serde(default, skip_serializing_if = "Option::is_none")]
310 pub id: Option<Uri>,
311 #[serde(rename = "type")]
313 pub types: OneOrMany<String>,
314 #[serde(default, skip_serializing_if = "Option::is_none")]
316 pub holder: Option<Party>,
317 #[serde(
319 rename = "verifiableCredential",
320 default,
321 skip_serializing_if = "Vec::is_empty"
322 )]
323 pub verifiable_credential: Vec<EnvelopedVc>,
324 #[serde(flatten)]
326 pub properties: IndexMap<String, Value>,
327}
328
329impl Serialize for PresentationV2 {
330 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
331 Self::serialize(self, serializer)
332 }
333}
334
335impl<'de> Deserialize<'de> for PresentationV2 {
336 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
338 let presentation = Self::deserialize(deserializer)?;
339 presentation.check().map_err(de::Error::custom)?;
340 Ok(presentation)
341 }
342}
343
344impl Default for PresentationV2 {
345 fn default() -> Self {
346 Self::new()
347 }
348}
349
350impl PresentationV2 {
351 pub const CONTEXT: &'static str = CONTEXT_V2;
353
354 pub const TYPE: &'static str = "VerifiablePresentation";
356
357 pub const JWT_TYP: &'static str = "vp+jwt";
359
360 pub const JWT_MEDIA_TYPE: &'static str = "application/vp+jwt";
362
363 pub fn new() -> Self {
365 Self {
366 context: Context::uri(Self::CONTEXT),
367 id: None,
368 types: OneOrMany::One(Self::TYPE.to_owned()),
369 holder: None,
370 verifiable_credential: Vec::new(),
371 properties: IndexMap::new(),
372 }
373 }
374
375 #[must_use]
377 pub fn with_context(mut self, context: impl Into<String>) -> Self {
378 self.context = append_context(self.context, context.into());
379 self
380 }
381
382 #[must_use]
384 pub fn with_id(mut self, id: impl Into<Uri>) -> Self {
385 self.id = Some(id.into());
386 self
387 }
388
389 #[must_use]
391 pub fn with_type(mut self, presentation_type: impl Into<String>) -> Self {
392 push_type(&mut self.types, presentation_type.into());
393 self
394 }
395
396 #[must_use]
398 pub fn with_holder(mut self, holder: impl Into<Party>) -> Self {
399 self.holder = Some(holder.into());
400 self
401 }
402
403 #[must_use]
405 pub fn with_credential(mut self, credential: EnvelopedVc) -> Self {
406 self.verifiable_credential.push(credential);
407 self
408 }
409
410 #[must_use]
412 pub fn with_property(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
413 self.properties.insert(name.into(), value.into());
414 self
415 }
416
417 pub fn has_type(&self, presentation_type: &str) -> bool {
419 self.types.iter().any(|t| t == presentation_type)
420 }
421
422 pub fn check(&self) -> Result<(), CredentialError> {
429 check_context_and_type(&self.context, Self::CONTEXT, &self.types, Self::TYPE)
430 }
431
432 pub fn to_jwt_claims(&self) -> JwtClaims {
435 JwtClaims {
436 additional: claims::to_object(self).into_iter().collect(),
437 ..JwtClaims::default()
438 }
439 }
440
441 pub fn from_jwt_claims(jwt_claims: &JwtClaims) -> Result<Self, CredentialError> {
448 let mut vp = payload_object(jwt_claims, "vp")?;
449 claims::merge_id(&mut vp, "id", "jti", jwt_claims.jti.as_deref())?;
450 claims::merge_id(&mut vp, "holder", "iss", jwt_claims.iss.as_deref())?;
451 serde_json::from_value(Value::Object(vp)).map_err(|e| CredentialError::InvalidClaim {
452 claim: "@context",
453 reason: e.to_string(),
454 })
455 }
456}
457
458fn payload_object(
461 jwt_claims: &JwtClaims,
462 forbidden: &'static str,
463) -> Result<serde_json::Map<String, Value>, CredentialError> {
464 if jwt_claims.additional.contains_key(forbidden) {
465 return Err(CredentialError::InvalidClaim {
466 claim: forbidden,
467 reason: "must not be present in a VC-JOSE-COSE payload".to_owned(),
468 });
469 }
470 Ok(jwt_claims
471 .additional
472 .iter()
473 .map(|(name, value)| (name.clone(), value.clone()))
474 .collect())
475}
476
477fn parse_data_url(uri: &str) -> Option<(&str, bool, &str)> {
479 let (meta, data) = uri.strip_prefix("data:")?.split_once(',')?;
480 let (media_type, base64) = match meta.strip_suffix(";base64") {
481 Some(media_type) => (media_type, true),
482 None => (meta, false),
483 };
484 Some((media_type, base64, data))
485}
486
487macro_rules! enveloped {
488 (
489 $(#[$doc:meta])*
490 $name:ident, $kind:literal, $object_type:literal, $media_type:expr
491 ) => {
492 $(#[$doc])*
493 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
494 #[non_exhaustive]
495 #[serde(remote = "Self")]
496 pub struct $name {
497 #[serde(rename = "@context")]
499 pub context: Context,
500 pub id: Uri,
502 #[serde(rename = "type")]
504 pub types: OneOrMany<String>,
505 }
506
507 impl Serialize for $name {
508 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
509 Self::serialize(self, serializer)
510 }
511 }
512
513 impl<'de> Deserialize<'de> for $name {
514 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
516 let enveloped = Self::deserialize(deserializer)?;
517 enveloped.check().map_err(de::Error::custom)?;
518 Ok(enveloped)
519 }
520 }
521
522 impl $name {
523 pub const TYPE: &'static str = $object_type;
525
526 pub const JWT_MEDIA_TYPE: &'static str = $media_type;
528
529 pub fn new(media_type: &str, data: &str) -> Result<Self, CredentialError> {
539 let id = Uri::parse(&format!("data:{media_type},{data}")).map_err(|error| {
540 CredentialError::InvalidClaim {
541 claim: "id",
542 reason: format!("not a `data:` URL: {error}"),
543 }
544 })?;
545 Ok(Self {
546 context: Context::uri(CONTEXT_V2),
547 id,
548 types: OneOrMany::One(Self::TYPE.to_owned()),
549 })
550 }
551
552 pub fn from_jwt(jwt: &Jwt) -> Self {
554 Self::new(Self::JWT_MEDIA_TYPE, jwt.as_str())
555 .expect("a compact JWS is base64url and dots, all URI characters")
556 }
557
558 pub fn media_type(&self) -> &str {
560 parse_data_url(self.id.as_str()).map_or("", |(media_type, _, _)| media_type)
561 }
562
563 pub fn data(&self) -> &str {
565 parse_data_url(self.id.as_str()).map_or("", |(_, _, data)| data)
566 }
567
568 pub fn to_jwt(&self) -> Result<Jwt, CredentialError> {
575 let (media_type, base64, data) = parse_data_url(self.id.as_str())
576 .ok_or_else(|| CredentialError::NotDataUrl(self.id.to_string()))?;
577 if media_type != Self::JWT_MEDIA_TYPE || base64 {
578 return Err(CredentialError::UnsupportedMediaType {
579 kind: $kind,
580 media_type: if base64 {
581 format!("{media_type};base64")
582 } else {
583 media_type.to_owned()
584 },
585 expected: Self::JWT_MEDIA_TYPE,
586 });
587 }
588 Jwt::new(data).map_err(|e| CredentialError::InvalidClaim {
589 claim: "id",
590 reason: e.to_string(),
591 })
592 }
593
594 pub fn check(&self) -> Result<(), CredentialError> {
600 if !self.types.iter().any(|t| t == Self::TYPE) {
601 return Err(CredentialError::MissingType(Self::TYPE));
602 }
603 if parse_data_url(self.id.as_str()).is_none() {
604 return Err(CredentialError::NotDataUrl(self.id.to_string()));
605 }
606 Ok(())
607 }
608 }
609 };
610}
611
612enveloped!(
613 EnvelopedVc,
634 "credential",
635 "EnvelopedVerifiableCredential",
636 CredentialV2::JWT_MEDIA_TYPE
637);
638
639enveloped!(
640 EnvelopedVp,
643 "presentation",
644 "EnvelopedVerifiablePresentation",
645 PresentationV2::JWT_MEDIA_TYPE
646);
647
648#[cfg(feature = "sd-jwt")]
649#[cfg_attr(docsrs, doc(cfg(feature = "sd-jwt")))]
650impl EnvelopedVc {
651 pub const SD_JWT_MEDIA_TYPE: &'static str = crate::sd_jwt_vc::SdJwtCredentialV2::MEDIA_TYPE;
654
655 pub fn from_sd_jwt(sd_jwt: &crate::sd_jwt_vc::SdJwt) -> Self {
662 Self::new(Self::SD_JWT_MEDIA_TYPE, &sd_jwt.to_string())
663 .expect("a compact SD-JWT is base64url, dots and tildes, all URI characters")
664 }
665
666 pub fn to_sd_jwt(&self) -> Result<crate::sd_jwt_vc::SdJwt, CredentialError> {
676 let (media_type, base64, data) = parse_data_url(self.id.as_str())
677 .ok_or_else(|| CredentialError::NotDataUrl(self.id.to_string()))?;
678 if media_type != Self::SD_JWT_MEDIA_TYPE || base64 {
679 return Err(CredentialError::UnsupportedMediaType {
680 kind: "credential",
681 media_type: if base64 {
682 format!("{media_type};base64")
683 } else {
684 media_type.to_owned()
685 },
686 expected: Self::SD_JWT_MEDIA_TYPE,
687 });
688 }
689 crate::sd_jwt_vc::SdJwt::parse(data).map_err(|e| CredentialError::InvalidClaim {
690 claim: "id",
691 reason: e.to_string(),
692 })
693 }
694
695 pub fn is_sd_jwt(&self) -> bool {
698 parse_data_url(self.id.as_str())
699 .is_some_and(|(media_type, base64, _)| media_type == Self::SD_JWT_MEDIA_TYPE && !base64)
700 }
701}