identity_credential/credential/
credential_v2.rs1use std::fmt::Display;
5
6use identity_core::common::Context;
7use identity_core::common::Object;
8use identity_core::common::OneOrMany;
9use identity_core::common::Timestamp;
10use identity_core::common::Url;
11use identity_core::convert::FmtJson as _;
12use identity_core::convert::ToJson as _;
13use once_cell::sync::Lazy;
14use serde::de::DeserializeOwned;
15use serde::de::Error as _;
16use serde::Deserialize;
17use serde::Deserializer;
18use serde::Serialize;
19
20use crate::credential::CredentialBuilder;
21use crate::credential::CredentialSealed;
22use crate::credential::CredentialT;
23use crate::credential::Evidence;
24use crate::credential::Issuer;
25use crate::credential::Policy;
26use crate::credential::Proof;
27use crate::credential::RefreshService;
28use crate::credential::Schema;
29use crate::credential::Status;
30use crate::credential::Subject;
31use crate::error::Error;
32use crate::error::Result;
33
34pub(crate) static BASE_CONTEXT: Lazy<Context> =
35 Lazy::new(|| Context::Url(Url::parse("https://www.w3.org/ns/credentials/v2").unwrap()));
36
37pub(crate) fn deserialize_vc2_0_context<'de, D>(deserializer: D) -> Result<OneOrMany<Context>, D::Error>
38where
39 D: Deserializer<'de>,
40{
41 let ctx = OneOrMany::<Context>::deserialize(deserializer)?;
42 if ctx.contains(&BASE_CONTEXT) {
43 Ok(ctx)
44 } else {
45 Err(D::Error::custom("Missing base context"))
46 }
47}
48
49#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
51pub struct Credential<T = Object> {
52 #[serde(rename = "@context", deserialize_with = "deserialize_vc2_0_context")]
54 pub context: OneOrMany<Context>,
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub id: Option<Url>,
58 #[serde(rename = "type")]
60 pub types: OneOrMany<String>,
61 #[serde(rename = "credentialSubject")]
63 pub credential_subject: OneOrMany<Subject>,
64 pub issuer: Issuer,
66 #[serde(rename = "validFrom")]
68 pub valid_from: Timestamp,
69 #[serde(rename = "validUntil", skip_serializing_if = "Option::is_none")]
71 pub valid_until: Option<Timestamp>,
72 #[serde(default, rename = "credentialStatus", skip_serializing_if = "Option::is_none")]
74 pub credential_status: Option<Status>,
75 #[serde(default, rename = "credentialSchema", skip_serializing_if = "OneOrMany::is_empty")]
77 pub credential_schema: OneOrMany<Schema>,
78 #[serde(default, rename = "refreshService", skip_serializing_if = "OneOrMany::is_empty")]
80 pub refresh_service: OneOrMany<RefreshService>,
81 #[serde(default, rename = "termsOfUse", skip_serializing_if = "OneOrMany::is_empty")]
83 pub terms_of_use: OneOrMany<Policy>,
84 #[serde(default, skip_serializing_if = "OneOrMany::is_empty")]
86 pub evidence: OneOrMany<Evidence>,
87 #[serde(rename = "nonTransferable", skip_serializing_if = "Option::is_none")]
90 pub non_transferable: Option<bool>,
91 #[serde(flatten)]
93 pub properties: T,
94 #[serde(skip_serializing_if = "Option::is_none")]
96 pub proof: Option<Proof>,
97}
98
99impl<T> Credential<T> {
100 pub fn base_context() -> &'static Context {
102 &BASE_CONTEXT
103 }
104
105 pub fn base_type() -> &'static str {
107 "VerifiableCredential"
108 }
109
110 pub fn from_builder(mut builder: CredentialBuilder<T>) -> Result<Self> {
112 if builder.context.first() != Some(Self::base_context()) {
113 builder.context.insert(0, Self::base_context().clone());
114 }
115
116 if builder.types.first().map(String::as_str) != Some(Self::base_type()) {
117 builder.types.insert(0, Self::base_type().to_owned());
118 }
119
120 let this = Self {
121 context: OneOrMany::Many(builder.context),
122 id: builder.id,
123 types: builder.types.into(),
124 credential_subject: builder.subject.into(),
125 issuer: builder.issuer.ok_or(Error::MissingIssuer)?,
126 valid_from: builder.issuance_date.unwrap_or_default(),
127 valid_until: builder.expiration_date,
128 credential_status: builder.status,
129 credential_schema: builder.schema.into(),
130 refresh_service: builder.refresh_service.into(),
131 terms_of_use: builder.terms_of_use.into(),
132 evidence: builder.evidence.into(),
133 non_transferable: builder.non_transferable,
134 properties: builder.properties,
135 proof: builder.proof,
136 };
137
138 this.check_structure()?;
139
140 Ok(this)
141 }
142
143 pub(crate) fn check_structure(&self) -> Result<()> {
145 match self.context.get(0) {
147 Some(context) if context == Self::base_context() => {}
148 Some(_) | None => return Err(Error::MissingBaseContext),
149 }
150
151 if !self.types.iter().any(|type_| type_ == Self::base_type()) {
153 return Err(Error::MissingBaseType);
154 }
155
156 if self.credential_subject.is_empty() {
158 return Err(Error::MissingSubject);
159 }
160
161 for subject in self.credential_subject.iter() {
163 if subject.id.is_none() && subject.properties.is_empty() {
164 return Err(Error::InvalidSubject);
165 }
166 }
167
168 Ok(())
169 }
170}
171
172impl<T> Display for Credential<T>
173where
174 T: Serialize,
175{
176 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> core::fmt::Result {
177 self.fmt_json(f)
178 }
179}
180
181impl<T> CredentialSealed for Credential<T> {}
182
183impl<T> CredentialT for Credential<T>
184where
185 T: Clone + Serialize + DeserializeOwned,
186{
187 type Properties = T;
188
189 fn base_context(&self) -> &'static Context {
190 Self::base_context()
191 }
192
193 fn type_(&self) -> &OneOrMany<String> {
194 &self.types
195 }
196
197 fn context(&self) -> &OneOrMany<Context> {
198 &self.context
199 }
200
201 fn subject(&self) -> &OneOrMany<Subject> {
202 &self.credential_subject
203 }
204
205 fn issuer(&self) -> &Issuer {
206 &self.issuer
207 }
208
209 fn valid_from(&self) -> Timestamp {
210 self.valid_from
211 }
212
213 fn valid_until(&self) -> Option<Timestamp> {
214 self.valid_until
215 }
216
217 fn properties(&self) -> &Self::Properties {
218 &self.properties
219 }
220
221 fn status(&self) -> Option<&Status> {
222 self.credential_status.as_ref()
223 }
224
225 fn non_transferable(&self) -> bool {
226 self.non_transferable.unwrap_or_default()
227 }
228
229 fn serialize_jwt(&self, custom_claims: Option<Object>) -> Result<String> {
230 self.serialize_jwt(custom_claims)
231 }
232}
233
234impl<T> Credential<T>
235where
236 T: Serialize,
237{
238 pub fn serialize_jwt(&self, _custom_claims: Option<Object>) -> Result<String> {
243 self
244 .to_json()
245 .map_err(|err| Error::JwtClaimsSetSerializationError(err.into()))
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use identity_verification::jws::Decoder;
252
253 use super::*;
254
255 #[test]
256 fn valid_from_json_str() {
257 let json_credential = r#"
258{
259 "@context": [
260 "https://www.w3.org/ns/credentials/v2",
261 "https://www.w3.org/ns/credentials/examples/v2"
262 ],
263 "id": "http://university.example/credentials/3732",
264 "type": [
265 "VerifiableCredential",
266 "ExampleDegreeCredential"
267 ],
268 "issuer": "https://university.example/issuers/565049",
269 "validFrom": "2010-01-01T00:00:00Z",
270 "credentialSubject": {
271 "id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
272 "degree": {
273 "type": "ExampleBachelorDegree",
274 "name": "Bachelor of Science and Arts"
275 }
276 }
277}
278 "#;
279 serde_json::from_str::<Credential>(json_credential).expect("valid VC using Data Model 2.0");
280 }
281
282 #[test]
283 fn invalid_from_json_str() {
284 let json_credential = include_str!("../../tests/fixtures/credential-1.json");
285 let _error = serde_json::from_str::<Credential>(json_credential).unwrap_err();
286 }
287
288 #[test]
289 fn parsed_from_jwt_payload() {
290 let jwt = "eyJraWQiOiJFeEhrQk1XOWZtYmt2VjI2Nm1ScHVQMnNVWV9OX0VXSU4xbGFwVXpPOHJvIiwiYWxnIjoiRVMyNTYifQ.eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLCJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvZXhhbXBsZXMvdjIiXSwiaWQiOiJodHRwOi8vdW5pdmVyc2l0eS5leGFtcGxlL2NyZWRlbnRpYWxzLzM3MzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiRXhhbXBsZURlZ3JlZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiaHR0cHM6Ly91bml2ZXJzaXR5LmV4YW1wbGUvaXNzdWVycy81NjUwNDkiLCJ2YWxpZEZyb20iOiIyMDEwLTAxLTAxVDAwOjAwOjAwWiIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmV4YW1wbGU6ZWJmZWIxZjcxMmViYzZmMWMyNzZlMTJlYzIxIiwiZGVncmVlIjp7InR5cGUiOiJFeGFtcGxlQmFjaGVsb3JEZWdyZWUiLCJuYW1lIjoiQmFjaGVsb3Igb2YgU2NpZW5jZSBhbmQgQXJ0cyJ9fX0.YEsG9at9Hnt_j-UykCrnl494fcYMTjzpgvlK0KzzjvfmZmSg-sNVJqMZWizYhWv_eRUvAoZohvSJWeagwj_Ajw";
291 let decoded_jwt = Decoder::new()
292 .decode_compact_serialization(jwt.as_bytes(), None)
293 .expect("valid JWT");
294
295 let _credential: Credential<Object> = serde_json::from_slice(decoded_jwt.claims()).expect("valid JWT payload");
296 }
297}