identity_kit/vc/validation/
credential.rs1use crate::did::{Did, DidUrl, Uri, VerificationRelationship};
4use crate::jose::{CompactJws, DefaultVerifier, JwsHeader, JwsVerifier};
5use crate::vc::{Credential, CredentialV2, Jwt, JwtClaims, Party, TypedObject};
6
7use super::{
8 CredentialValidationOptions, KeyResolver, StatusCheck, StatusError, StatusPolicy,
9 ValidationError,
10};
11
12#[derive(Debug, Clone)]
15#[non_exhaustive]
16pub struct DecodedCredential {
17 pub header: JwsHeader,
19 pub claims: JwtClaims,
21 pub credential: Credential,
23}
24
25#[derive(Debug, Clone)]
27#[non_exhaustive]
28pub struct DecodedCredentialV2 {
29 pub header: JwsHeader,
31 pub claims: JwtClaims,
33 pub credential: CredentialV2,
35}
36
37pub struct JwtCredentialValidator<R, V = DefaultVerifier> {
98 resolver: R,
99 verifier: V,
100 status: Option<Box<dyn StatusCheck>>,
101}
102
103impl<R: KeyResolver, V: JwsVerifier> JwtCredentialValidator<R, V> {
104 pub fn new(resolver: R, verifier: V) -> Self {
107 Self {
108 resolver,
109 verifier,
110 status: None,
111 }
112 }
113
114 #[must_use]
116 pub fn with_status_check(mut self, check: impl StatusCheck + 'static) -> Self {
117 self.status = Some(Box::new(check));
118 self
119 }
120
121 pub fn resolver(&self) -> &R {
123 &self.resolver
124 }
125
126 pub fn verifier(&self) -> &V {
128 &self.verifier
129 }
130
131 pub fn has_status_check(&self) -> bool {
133 self.status.is_some()
134 }
135
136 pub async fn validate(
143 &self,
144 jwt: &Jwt,
145 options: &CredentialValidationOptions,
146 ) -> Result<DecodedCredential, ValidationError> {
147 let jws = jwt.decode()?;
148 let decoded = decode(&jws)?;
149 let kid = self
150 .verify(&jws, VerificationRelationship::AssertionMethod)
151 .await?;
152 check_controller(&decoded.credential.issuer, &kid, "issuer")?;
153 options.time.check_claims(&decoded.claims)?;
154 options.time.check(
155 Some(decoded.credential.issuance_date),
156 decoded.credential.expiration_date,
157 None,
158 )?;
159 self.check_status(decoded.credential.credential_status.iter(), options.status)
160 .await?;
161 Ok(decoded)
162 }
163
164 pub async fn validate_v2(
170 &self,
171 jwt: &Jwt,
172 options: &CredentialValidationOptions,
173 ) -> Result<DecodedCredentialV2, ValidationError> {
174 let jws = jwt.decode()?;
175 let decoded = decode_v2(&jws)?;
176 let kid = self
177 .verify(&jws, VerificationRelationship::AssertionMethod)
178 .await?;
179 check_controller(&decoded.credential.issuer, &kid, "issuer")?;
180 options.time.check_claims(&decoded.claims)?;
181 options.time.check(
182 decoded.credential.valid_from,
183 decoded.credential.valid_until,
184 None,
185 )?;
186 self.check_status(decoded.credential.statuses(), options.status)
187 .await?;
188 Ok(decoded)
189 }
190
191 pub(super) async fn verify(
193 &self,
194 jws: &CompactJws,
195 relationship: VerificationRelationship,
196 ) -> Result<DidUrl, ValidationError> {
197 let kid = jws.kid().ok_or(ValidationError::MissingKid)?;
198 let kid = DidUrl::parse(kid).map_err(|source| ValidationError::InvalidKid {
199 kid: kid.to_owned(),
200 source,
201 })?;
202 if kid.path().is_some() || kid.query().is_some() {
203 return Err(ValidationError::KidWithParameters(kid));
204 }
205 let key = self
206 .resolver
207 .resolve_key(&kid, relationship)
208 .await
209 .map_err(|source| ValidationError::KeyResolution {
210 kid: kid.clone(),
211 source,
212 })?;
213 jws.verify(&self.verifier, &key)?;
214 Ok(kid)
215 }
216
217 async fn check_status<'a>(
219 &self,
220 statuses: impl Iterator<Item = &'a TypedObject>,
221 policy: StatusPolicy,
222 ) -> Result<(), ValidationError> {
223 if policy == StatusPolicy::SkipAll {
224 return Ok(());
225 }
226 for status in statuses {
227 let result = match &self.status {
228 Some(check) => check.check(status).await,
229 None => Err(StatusError::Unsupported(status.object_type().to_owned())),
230 };
231 match result {
232 Ok(()) => {}
233 Err(StatusError::Unsupported(_)) if policy == StatusPolicy::SkipUnsupported => {}
234 Err(StatusError::Unsupported(status_type)) => {
235 return Err(ValidationError::StatusUnchecked(status_type));
236 }
237 Err(error) => return Err(ValidationError::Status(error)),
238 }
239 }
240 Ok(())
241 }
242}
243
244impl<R: core::fmt::Debug, V: core::fmt::Debug> core::fmt::Debug for JwtCredentialValidator<R, V> {
245 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
246 f.debug_struct("JwtCredentialValidator")
247 .field("resolver", &self.resolver)
248 .field("verifier", &self.verifier)
249 .field("status", &self.status.as_ref().map(|_| "StatusCheck"))
250 .finish()
251 }
252}
253
254pub fn decode_credential(jwt: &Jwt) -> Result<DecodedCredential, ValidationError> {
261 decode(&jwt.decode()?)
262}
263
264pub fn decode_credential_v2(jwt: &Jwt) -> Result<DecodedCredentialV2, ValidationError> {
270 decode_v2(&jwt.decode()?)
271}
272
273pub(super) fn decode(jws: &CompactJws) -> Result<DecodedCredential, ValidationError> {
275 check_typ(jws.header(), "JWT")?;
276 let claims: JwtClaims = serde_json::from_slice(jws.payload())
277 .map_err(|e| ValidationError::Payload(e.to_string()))?;
278 let credential = Credential::from_jwt_claims(&claims)?;
279 Ok(DecodedCredential {
280 header: jws.header().clone(),
281 claims,
282 credential,
283 })
284}
285
286fn decode_v2(jws: &CompactJws) -> Result<DecodedCredentialV2, ValidationError> {
289 check_typ(jws.header(), CredentialV2::JWT_TYP)?;
290 let claims: JwtClaims = serde_json::from_slice(jws.payload())
291 .map_err(|e| ValidationError::Payload(e.to_string()))?;
292 let credential = CredentialV2::from_jwt_claims(&claims)?;
293 Ok(DecodedCredentialV2 {
294 header: jws.header().clone(),
295 claims,
296 credential,
297 })
298}
299
300pub(super) fn check_typ(header: &JwsHeader, expected: &'static str) -> Result<(), ValidationError> {
302 match header.typ.as_deref() {
303 Some(found) if found != expected => Err(ValidationError::UnexpectedTyp {
304 expected,
305 found: found.to_owned(),
306 }),
307 _ => Ok(()),
308 }
309}
310
311pub(super) fn check_controller(
313 party: &Party,
314 kid: &DidUrl,
315 role: &'static str,
316) -> Result<(), ValidationError> {
317 let id = party.id();
318 let did = Did::parse(id.as_str()).map_err(|_| ValidationError::NotDid {
319 role,
320 id: id.to_string(),
321 })?;
322 if did != *kid.did() {
323 return Err(mismatch(role, id.clone(), kid.clone()));
324 }
325 Ok(())
326}
327
328fn mismatch(role: &'static str, id: Uri, kid: DidUrl) -> ValidationError {
330 if role == "holder" {
331 ValidationError::HolderMismatch { holder: id, kid }
332 } else {
333 ValidationError::IssuerMismatch { issuer: id, kid }
334 }
335}