Skip to main content

identity_kit/vc/
v2.rs

1//! Verifiable Credentials Data Model 2.0: [`CredentialV2`] and [`PresentationV2`] secured as
2//! JWTs per VC-JOSE-COSE, and the [`EnvelopedVc`] and [`EnvelopedVp`] objects that carry a
3//! secured credential or presentation in a `data:` URL.
4
5use 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/// A verifiable credential of the Verifiable Credentials Data Model 2.0, as data.
17///
18/// Secured per VC-JOSE-COSE, the credential itself is the JWT payload: `@context` and the other
19/// properties are claims next to the registered ones, with `typ: vc+jwt` in the header. See
20/// [`to_jwt_claims`](Self::to_jwt_claims).
21///
22/// Deserialization checks that the first `@context` is the base context, `type` includes
23/// `VerifiableCredential`, there is a subject, and `validUntil` is not before `validFrom`.
24///
25/// # Examples
26///
27/// ```
28/// use chrono::{TimeZone as _, Utc};
29/// use identity_kit::did::Uri;
30/// use identity_kit::vc::{CredentialV2, Subject};
31/// use serde_json::json;
32///
33/// let credential = CredentialV2::new(
34///     Uri::parse("https://university.example/issuers/14")?,
35///     Subject::new(Uri::parse("did:example:ebfeb1f712ebc6f1c276e12ec21")?)
36///         .with_property("degree", json!({ "type": "ExampleBachelorDegree" })),
37/// )
38/// .with_type("ExampleDegreeCredential")
39/// .with_valid_from(Utc.with_ymd_and_hms(2010, 1, 1, 19, 23, 24).unwrap());
40///
41/// let claims = credential.to_jwt_claims();
42/// assert_eq!(claims.additional["validFrom"], "2010-01-01T19:23:24Z");
43/// assert_eq!(CredentialV2::from_jwt_claims(&claims)?, credential);
44/// # Ok::<(), Box<dyn std::error::Error>>(())
45/// ```
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47#[non_exhaustive]
48#[serde(remote = "Self")]
49pub struct CredentialV2 {
50    /// `@context`, starting with [`CredentialV2::CONTEXT`].
51    #[serde(rename = "@context")]
52    pub context: Context,
53    /// The identifier of the credential.
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub id: Option<Uri>,
56    /// `type`, including [`CredentialV2::TYPE`].
57    #[serde(rename = "type")]
58    pub types: OneOrMany<String>,
59    /// `name`: a string or language-tagged values, kept as JSON.
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub name: Option<Value>,
62    /// `description`: a string or language-tagged values, kept as JSON.
63    #[serde(default, skip_serializing_if = "Option::is_none")]
64    pub description: Option<Value>,
65    /// The issuer.
66    pub issuer: Party,
67    /// When the credential becomes valid.
68    #[serde(rename = "validFrom", default, skip_serializing_if = "Option::is_none")]
69    pub valid_from: Option<DateTime<Utc>>,
70    /// When the credential ceases to be valid.
71    #[serde(
72        rename = "validUntil",
73        default,
74        skip_serializing_if = "Option::is_none"
75    )]
76    pub valid_until: Option<DateTime<Utc>>,
77    /// `credentialStatus`: where to learn whether the credential is revoked or suspended.
78    #[serde(
79        rename = "credentialStatus",
80        default,
81        skip_serializing_if = "Option::is_none"
82    )]
83    pub credential_status: Option<OneOrMany<TypedObject>>,
84    /// The subjects and the claims about them.
85    #[serde(rename = "credentialSubject")]
86    pub credential_subject: OneOrMany<Subject>,
87    /// `credentialSchema`.
88    #[serde(
89        rename = "credentialSchema",
90        default,
91        skip_serializing_if = "Option::is_none"
92    )]
93    pub credential_schema: Option<OneOrMany<TypedObject>>,
94    /// Every other property (`relatedResource`, `termsOfUse`, `evidence`, `refreshService`,
95    /// …), in insertion order.
96    #[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    /// Reads the credential and checks it with [`CredentialV2::check`].
108    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    /// The base context, `https://www.w3.org/ns/credentials/v2`.
117    pub const CONTEXT: &'static str = CONTEXT_V2;
118
119    /// The type every credential has, `VerifiableCredential`.
120    pub const TYPE: &'static str = "VerifiableCredential";
121
122    /// The `typ` of a JWT securing a credential, `vc+jwt`.
123    pub const JWT_TYP: &'static str = "vc+jwt";
124
125    /// The media type of a JWT securing a credential, `application/vc+jwt`.
126    pub const JWT_MEDIA_TYPE: &'static str = "application/vc+jwt";
127
128    /// A credential of type `VerifiableCredential` in the base context, issued by `issuer`
129    /// about `subject`, valid indefinitely.
130    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    /// This credential with `context` appended to its `@context`.
148    #[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    /// This credential with its `id` set.
155    #[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    /// This credential with `credential_type` appended to its `type`.
162    #[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    /// This credential with its `name` set.
169    #[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    /// This credential with its `description` set.
176    #[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    /// This credential with its `validFrom` set.
183    #[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    /// This credential with its `validUntil` set.
190    #[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    /// This credential with another subject.
197    #[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    /// This credential with a `credentialStatus` added.
208    #[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    /// This credential with a `credentialSchema` added.
215    #[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    /// This credential with the property `name` set to `value`.
222    #[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    /// The one subject, when there is exactly one.
229    pub fn subject(&self) -> Option<&Subject> {
230        (self.credential_subject.len() == 1).then(|| self.credential_subject.first())?
231    }
232
233    /// Whether `credential_type` is among the types.
234    pub fn has_type(&self, credential_type: &str) -> bool {
235        self.types.iter().any(|t| t == credential_type)
236    }
237
238    /// The status entries, in order; none when `credentialStatus` is absent.
239    pub fn statuses(&self) -> impl Iterator<Item = &TypedObject> {
240        self.credential_status.iter().flat_map(OneOrMany::iter)
241    }
242
243    /// Checks the structural requirements of the data model: the first `@context` is the base
244    /// context, `type` includes `VerifiableCredential`, there is a subject, and `validUntil`
245    /// is not before `validFrom`.
246    ///
247    /// # Errors
248    ///
249    /// The first [`CredentialError`] found.
250    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    /// The JWT claims set securing the credential (VC-JOSE-COSE §3.1.1): the credential's own
267    /// properties, nothing else. `iss`, `jti`, `sub`, `iat` and `exp` are the caller's to add
268    /// and must agree with the credential when they are.
269    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    /// The credential a JWT claims set secures: its non-registered claims, with `iss`, `jti`
277    /// and `sub`, when present, checked against `issuer`, `id` and the subject.
278    ///
279    /// # Errors
280    ///
281    /// [`CredentialError::InvalidClaim`] when the claims hold a `vc` claim or do not form a
282    /// credential, [`CredentialError::ClaimMismatch`] when a registered claim disagrees with
283    /// the property it duplicates, and what [`check`](Self::check) reports.
284    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/// A verifiable presentation of the Verifiable Credentials Data Model 2.0 whose credentials
297/// are [`EnvelopedVc`] objects, as data.
298///
299/// Secured per VC-JOSE-COSE, the presentation itself is the JWT payload, with `typ: vp+jwt`
300/// in the header; the holder signs it.
301#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
302#[non_exhaustive]
303#[serde(remote = "Self")]
304pub struct PresentationV2 {
305    /// `@context`, starting with [`PresentationV2::CONTEXT`].
306    #[serde(rename = "@context")]
307    pub context: Context,
308    /// The identifier of the presentation.
309    #[serde(default, skip_serializing_if = "Option::is_none")]
310    pub id: Option<Uri>,
311    /// `type`, including [`PresentationV2::TYPE`].
312    #[serde(rename = "type")]
313    pub types: OneOrMany<String>,
314    /// The holder presenting the credentials.
315    #[serde(default, skip_serializing_if = "Option::is_none")]
316    pub holder: Option<Party>,
317    /// The presented credentials, each enveloped.
318    #[serde(
319        rename = "verifiableCredential",
320        default,
321        skip_serializing_if = "Vec::is_empty"
322    )]
323    pub verifiable_credential: Vec<EnvelopedVc>,
324    /// Every other property, in insertion order.
325    #[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    /// Reads the presentation and checks it with [`PresentationV2::check`].
337    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    /// The base context, `https://www.w3.org/ns/credentials/v2`.
352    pub const CONTEXT: &'static str = CONTEXT_V2;
353
354    /// The type every presentation has, `VerifiablePresentation`.
355    pub const TYPE: &'static str = "VerifiablePresentation";
356
357    /// The `typ` of a JWT securing a presentation, `vp+jwt`.
358    pub const JWT_TYP: &'static str = "vp+jwt";
359
360    /// The media type of a JWT securing a presentation, `application/vp+jwt`.
361    pub const JWT_MEDIA_TYPE: &'static str = "application/vp+jwt";
362
363    /// An empty presentation of type `VerifiablePresentation` in the base context.
364    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    /// This presentation with `context` appended to its `@context`.
376    #[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    /// This presentation with its `id` set.
383    #[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    /// This presentation with `presentation_type` appended to its `type`.
390    #[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    /// This presentation with its `holder` set.
397    #[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    /// This presentation with `credential` added.
404    #[must_use]
405    pub fn with_credential(mut self, credential: EnvelopedVc) -> Self {
406        self.verifiable_credential.push(credential);
407        self
408    }
409
410    /// This presentation with the property `name` set to `value`.
411    #[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    /// Whether `presentation_type` is among the types.
418    pub fn has_type(&self, presentation_type: &str) -> bool {
419        self.types.iter().any(|t| t == presentation_type)
420    }
421
422    /// Checks that the first `@context` is the base context and `type` includes
423    /// `VerifiablePresentation`.
424    ///
425    /// # Errors
426    ///
427    /// The first [`CredentialError`] found.
428    pub fn check(&self) -> Result<(), CredentialError> {
429        check_context_and_type(&self.context, Self::CONTEXT, &self.types, Self::TYPE)
430    }
431
432    /// The JWT claims set securing the presentation (VC-JOSE-COSE §3.1.2): the presentation's
433    /// own properties. `iss`, `aud`, `nonce` and the rest are the caller's to add.
434    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    /// The presentation a JWT claims set secures, with `iss` and `jti`, when present, checked
442    /// against `holder` and `id`.
443    ///
444    /// # Errors
445    ///
446    /// As [`CredentialV2::from_jwt_claims`].
447    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
458/// The non-registered claims as the object of a VCDM 2.0 payload, which must not hold the
459/// VCDM 1.1 claim `forbidden`.
460fn 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
477/// The parts of an RFC 2397 `data:` URL: media type, whether `;base64` is set, and the data.
478fn 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            /// `@context`, which must define the type; the base context does.
498            #[serde(rename = "@context")]
499            pub context: Context,
500            /// The `data:` URL holding the secured form.
501            pub id: Uri,
502            /// `type`, including the enveloped type.
503            #[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            /// Reads the object and checks its `type` and that `id` is a `data:` URL.
515            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            /// The `type`.
524            pub const TYPE: &'static str = $object_type;
525
526            /// The media type of the JWT form this crate reads.
527            pub const JWT_MEDIA_TYPE: &'static str = $media_type;
528
529            /// An envelope for `data` of `media_type`, in the base context.
530            ///
531            /// `data` is placed in the URL as is; a JWT needs no encoding, other media types
532            /// may.
533            ///
534            /// # Errors
535            ///
536            /// [`CredentialError::InvalidClaim`] for `id` when `media_type` or `data` holds a
537            /// character a URI cannot.
538            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            /// An envelope for `jwt`.
553            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            /// The media type of the `data:` URL.
559            pub fn media_type(&self) -> &str {
560                parse_data_url(self.id.as_str()).map_or("", |(media_type, _, _)| media_type)
561            }
562
563            /// The data of the `data:` URL, as written.
564            pub fn data(&self) -> &str {
565                parse_data_url(self.id.as_str()).map_or("", |(_, _, data)| data)
566            }
567
568            /// The JWT the envelope holds.
569            ///
570            /// # Errors
571            ///
572            /// [`CredentialError::UnsupportedMediaType`] when the media type is not the JWT
573            /// one, and [`CredentialError::InvalidClaim`] when the data is not a JWT.
574            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            /// Checks that `type` includes the enveloped type and `id` is a `data:` URL.
595            ///
596            /// # Errors
597            ///
598            /// [`CredentialError::MissingType`] or [`CredentialError::NotDataUrl`].
599            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    /// An `EnvelopedVerifiableCredential` (VCDM 2.0 §4.12.1): a secured credential carried in
614    /// a `data:` URL, the form a credential takes inside a [`PresentationV2`].
615    ///
616    /// # Examples
617    ///
618    /// ```
619    /// use identity_kit::vc::{EnvelopedVc, Jwt};
620    ///
621    /// let jwt: Jwt = "eyJhbGciOiJFZERTQSJ9.RXhhbXBsZSBvZiBFZDI1NTE5IHNpZ25pbmc.\
622    ///     hgyY0il_MGCjP0JzlnLWG1PPOt7-09PGcvMg3AIbQR6dWbhijcNR4ki4iylGjg5BhVsPt9g7sVvpAr_MuM0KAg"
623    ///     .parse()?;
624    /// let enveloped = EnvelopedVc::from_jwt(&jwt);
625    /// assert_eq!(enveloped.media_type(), "application/vc+jwt");
626    /// assert_eq!(enveloped.to_jwt()?, jwt);
627    /// assert_eq!(
628    ///     serde_json::to_value(&enveloped)?["type"],
629    ///     "EnvelopedVerifiableCredential"
630    /// );
631    /// # Ok::<(), Box<dyn std::error::Error>>(())
632    /// ```
633    EnvelopedVc,
634    "credential",
635    "EnvelopedVerifiableCredential",
636    CredentialV2::JWT_MEDIA_TYPE
637);
638
639enveloped!(
640    /// An `EnvelopedVerifiablePresentation` (VCDM 2.0 §4.12.2): a secured presentation carried
641    /// in a `data:` URL.
642    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    /// The media type of a credential secured with SD-JWT (VC-JOSE-COSE §3.2.1),
652    /// `application/vc+sd-jwt`.
653    pub const SD_JWT_MEDIA_TYPE: &'static str = crate::sd_jwt_vc::SdJwtCredentialV2::MEDIA_TYPE;
654
655    /// An envelope for `sd_jwt`, in its compact serialization.
656    ///
657    /// # Panics
658    ///
659    /// Never: a compact SD-JWT holds only base64url characters, dots and tildes, all of them
660    /// allowed in a URI.
661    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    /// The SD-JWT the envelope holds, parsed but not processed; see
667    /// [`SdJwtCredentialV2::from_sd_jwt`](crate::sd_jwt_vc::SdJwtCredentialV2::from_sd_jwt)
668    /// for the credential it secures.
669    ///
670    /// # Errors
671    ///
672    /// [`CredentialError::UnsupportedMediaType`] when the media type is not
673    /// `application/vc+sd-jwt`, and [`CredentialError::InvalidClaim`] when the data is not
674    /// an SD-JWT.
675    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    /// Whether the envelope holds a credential secured with SD-JWT: its media type is
696    /// `application/vc+sd-jwt`.
697    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}