identity_kit/vc/presentation.rs
1//! `Presentation`: a Verifiable Credentials Data Model 1.1 presentation of JWT credentials.
2
3use indexmap::IndexMap;
4use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
5use serde_json::Value;
6
7use crate::did::{Context, OneOrMany, Uri};
8
9use super::model::{CONTEXT_V1, append_context, check_context_and_type, push_type};
10use super::{CredentialError, Jwt, JwtClaims, Party, claims};
11
12/// A verifiable presentation of the Verifiable Credentials Data Model 1.1 whose credentials are
13/// JWTs, as data.
14///
15/// Deserialization checks that the first `@context` is the base context and `type` includes
16/// `VerifiablePresentation`. A presentation is secured by signing its JWT form, see
17/// [`to_jwt_claims`](Self::to_jwt_claims); the holder signs it, so the holder is the `iss`.
18///
19/// # Examples
20///
21/// ```
22/// use identity_kit::did::{Did, Uri};
23/// use identity_kit::vc::{Jwt, Presentation};
24///
25/// let credential: Jwt = "eyJhbGciOiJFZERTQSJ9.RXhhbXBsZSBvZiBFZDI1NTE5IHNpZ25pbmc.\
26/// hgyY0il_MGCjP0JzlnLWG1PPOt7-09PGcvMg3AIbQR6dWbhijcNR4ki4iylGjg5BhVsPt9g7sVvpAr_MuM0KAg"
27/// .parse()?;
28/// let presentation = Presentation::new()
29/// .with_id(Uri::parse("urn:uuid:3978344f-8596-4c3a-a978-8fcaba3903c5")?)
30/// .with_holder(Did::parse("did:example:ebfeb1f712ebc6f1c276e12ec21")?)
31/// .with_credential(credential);
32///
33/// let claims = presentation.to_jwt_claims()?.with_aud("did:example:4a57546973436f6f6c4a4a57573");
34/// assert_eq!(claims.iss.as_deref(), Some("did:example:ebfeb1f712ebc6f1c276e12ec21"));
35/// assert_eq!(claims.jti.as_deref(), Some("urn:uuid:3978344f-8596-4c3a-a978-8fcaba3903c5"));
36/// assert_eq!(claims.additional["vp"]["verifiableCredential"].as_array().map(Vec::len), Some(1));
37/// assert_eq!(Presentation::from_jwt_claims(&claims)?, presentation);
38/// # Ok::<(), Box<dyn std::error::Error>>(())
39/// ```
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41#[non_exhaustive]
42#[serde(remote = "Self")]
43pub struct Presentation {
44 /// `@context`, starting with [`Presentation::CONTEXT`].
45 #[serde(rename = "@context")]
46 pub context: Context,
47 /// The identifier of the presentation.
48 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub id: Option<Uri>,
50 /// `type`, including [`Presentation::TYPE`].
51 #[serde(rename = "type")]
52 pub types: OneOrMany<String>,
53 /// The presented credentials, as JWTs.
54 #[serde(
55 rename = "verifiableCredential",
56 default,
57 skip_serializing_if = "Vec::is_empty"
58 )]
59 pub verifiable_credential: Vec<Jwt>,
60 /// The holder presenting the credentials.
61 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub holder: Option<Party>,
63 /// Every other property, in insertion order.
64 #[serde(flatten)]
65 pub properties: IndexMap<String, Value>,
66}
67
68impl Serialize for Presentation {
69 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
70 Self::serialize(self, serializer)
71 }
72}
73
74impl<'de> Deserialize<'de> for Presentation {
75 /// Reads the presentation and checks it with [`Presentation::check`].
76 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
77 let presentation = Self::deserialize(deserializer)?;
78 presentation.check().map_err(de::Error::custom)?;
79 Ok(presentation)
80 }
81}
82
83impl Default for Presentation {
84 fn default() -> Self {
85 Self::new()
86 }
87}
88
89impl Presentation {
90 /// The base context, `https://www.w3.org/2018/credentials/v1`.
91 pub const CONTEXT: &'static str = CONTEXT_V1;
92
93 /// The type every presentation has, `VerifiablePresentation`.
94 pub const TYPE: &'static str = "VerifiablePresentation";
95
96 /// An empty presentation of type `VerifiablePresentation` in the base context.
97 pub fn new() -> Self {
98 Self {
99 context: Context::uri(Self::CONTEXT),
100 id: None,
101 types: OneOrMany::One(Self::TYPE.to_owned()),
102 verifiable_credential: Vec::new(),
103 holder: None,
104 properties: IndexMap::new(),
105 }
106 }
107
108 /// This presentation with `context` appended to its `@context`.
109 #[must_use]
110 pub fn with_context(mut self, context: impl Into<String>) -> Self {
111 self.context = append_context(self.context, context.into());
112 self
113 }
114
115 /// This presentation with its `id` set.
116 #[must_use]
117 pub fn with_id(mut self, id: impl Into<Uri>) -> Self {
118 self.id = Some(id.into());
119 self
120 }
121
122 /// This presentation with `presentation_type` appended to its `type`.
123 #[must_use]
124 pub fn with_type(mut self, presentation_type: impl Into<String>) -> Self {
125 push_type(&mut self.types, presentation_type.into());
126 self
127 }
128
129 /// This presentation with its `holder` set.
130 #[must_use]
131 pub fn with_holder(mut self, holder: impl Into<Party>) -> Self {
132 self.holder = Some(holder.into());
133 self
134 }
135
136 /// This presentation with `credential` added.
137 #[must_use]
138 pub fn with_credential(mut self, credential: Jwt) -> Self {
139 self.verifiable_credential.push(credential);
140 self
141 }
142
143 /// This presentation with the property `name` set to `value`.
144 #[must_use]
145 pub fn with_property(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
146 self.properties.insert(name.into(), value.into());
147 self
148 }
149
150 /// Whether `presentation_type` is among the types.
151 pub fn has_type(&self, presentation_type: &str) -> bool {
152 self.types.iter().any(|t| t == presentation_type)
153 }
154
155 /// Checks that the first `@context` is the base context and `type` includes
156 /// `VerifiablePresentation`.
157 ///
158 /// # Errors
159 ///
160 /// The first [`CredentialError`] found.
161 pub fn check(&self) -> Result<(), CredentialError> {
162 check_context_and_type(&self.context, Self::CONTEXT, &self.types, Self::TYPE)
163 }
164
165 /// The JWT claims set of the presentation (VCDM 1.1 §6.3.1 "JWT Encoding"): `iss` is the
166 /// holder, `jti` the id, and the presentation without those properties is the `vp` claim.
167 /// `aud`, `nbf`, `exp` and `nonce` are the caller's to add.
168 ///
169 /// # Errors
170 ///
171 /// What [`check`](Self::check) reports.
172 pub fn to_jwt_claims(&self) -> Result<JwtClaims, CredentialError> {
173 self.check()?;
174 let mut vp = claims::to_object(self);
175 let jti = claims::take_id(&mut vp, "id");
176 let iss = claims::take_id(&mut vp, "holder");
177 Ok(JwtClaims {
178 iss,
179 jti,
180 additional: IndexMap::from([("vp".to_owned(), Value::Object(vp))]),
181 ..JwtClaims::default()
182 })
183 }
184
185 /// The presentation of a JWT claims set (VCDM 1.1 §6.3.1 "JWT Decoding"): the `vp` claim
186 /// with `iss` and `jti` mapped back to `holder` and `id`.
187 ///
188 /// # Errors
189 ///
190 /// As [`Credential::from_jwt_claims`](super::Credential::from_jwt_claims), for `vp`.
191 pub fn from_jwt_claims(jwt_claims: &JwtClaims) -> Result<Self, CredentialError> {
192 let mut vp = claims::claim_object(jwt_claims, "vp")?;
193 claims::merge_id(&mut vp, "id", "jti", jwt_claims.jti.as_deref())?;
194 claims::merge_id(&mut vp, "holder", "iss", jwt_claims.iss.as_deref())?;
195 serde_json::from_value(Value::Object(vp)).map_err(|e| CredentialError::InvalidClaim {
196 claim: "vp",
197 reason: e.to_string(),
198 })
199 }
200}