Skip to main content

identity_kit/vc/validation/
credential.rs

1//! `JwtCredentialValidator`.
2
3use 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/// A validated or decoded VCDM 1.1 credential JWT: its header, its claims (custom ones
13/// included) and the credential they map to.
14#[derive(Debug, Clone)]
15#[non_exhaustive]
16pub struct DecodedCredential {
17    /// The JWS header.
18    pub header: JwsHeader,
19    /// The claims set, `vc` included.
20    pub claims: JwtClaims,
21    /// The credential, with the registered claims mapped back into it.
22    pub credential: Credential,
23}
24
25/// A validated or decoded VCDM 2.0 credential JWT: its header, its claims and the credential.
26#[derive(Debug, Clone)]
27#[non_exhaustive]
28pub struct DecodedCredentialV2 {
29    /// The JWS header.
30    pub header: JwsHeader,
31    /// The claims set, which is the credential plus any registered claims.
32    pub claims: JwtClaims,
33    /// The credential.
34    pub credential: CredentialV2,
35}
36
37/// Validates credential JWTs, see the [module documentation](super) for the checks.
38///
39/// # Examples
40///
41/// A `did:key` issuer signs a credential about a subject, and the verifier validates it with a
42/// resolver that knows `did:key`.
43///
44/// ```
45/// # #[cfg(feature = "did-key")]
46/// # {
47/// use chrono::{TimeZone as _, Utc};
48/// use identity_kit::did::Uri;
49/// use identity_kit::jose::{DefaultVerifier, Jwk, JwsAlgorithm, JwsHeader, SignError, Signer};
50/// use identity_kit::methods::DidKey;
51/// use identity_kit::resolver::Resolver;
52/// use identity_kit::vc::validation::{CredentialValidationOptions, JwtCredentialValidator, TimeOptions};
53/// use identity_kit::vc::{Credential, Jwt, Subject};
54///
55/// // The Ed25519 key of RFC 8037 Appendix A.1 behind a `Signer`.
56/// struct Issuer(ed25519_dalek::SigningKey);
57///
58/// #[async_trait::async_trait]
59/// impl Signer for Issuer {
60///     fn alg(&self) -> JwsAlgorithm { JwsAlgorithm::EdDSA }
61///     fn kid(&self) -> Option<&str> { None }
62///     async fn sign(&self, input: &[u8]) -> Result<Vec<u8>, SignError> {
63///         use ed25519_dalek::Signer as _;
64///         Ok(self.0.sign(input).to_bytes().to_vec())
65///     }
66/// }
67///
68/// # tokio::runtime::Builder::new_current_thread().build()?.block_on(async {
69/// let secret: [u8; 32] = identity_kit::jose::Jwk::from_ed25519_public(&[0; 32]).map(|_| [
70///     0x9d, 0x61, 0xb1, 0x9d, 0xef, 0xfd, 0x5a, 0x60, 0xba, 0x84, 0x4a, 0xf4, 0x92, 0xec, 0x2c, 0xc4,
71///     0x44, 0x49, 0xc5, 0x69, 0x7b, 0x32, 0x69, 0x19, 0x70, 0x3b, 0xac, 0x03, 0x1c, 0xae, 0x7f, 0x60,
72/// ])?;
73/// let signing_key = ed25519_dalek::SigningKey::from_bytes(&secret);
74/// let jwk = Jwk::from_ed25519_public(signing_key.verifying_key().as_bytes())?;
75/// let issuer = DidKey::did(&jwk)?;
76/// let kid = issuer.join(&format!("#{}", issuer.method_specific_id()))?;
77///
78/// let credential = Credential::new(
79///     &issuer,
80///     Utc.with_ymd_and_hms(2010, 1, 1, 0, 0, 0).unwrap(),
81///     Subject::new(Uri::parse("did:example:ebfeb1f712ebc6f1c276e12ec21")?),
82/// );
83/// let header = JwsHeader::new(JwsAlgorithm::EdDSA).with_typ("JWT").with_kid(kid.as_str());
84/// let jwt = Jwt::sign(&header, &credential.to_jwt_claims()?, &Issuer(signing_key)).await?;
85///
86/// let resolver = Resolver::builder().with(DidKey::default()).build();
87/// let validator = JwtCredentialValidator::new(resolver, DefaultVerifier);
88/// let options = CredentialValidationOptions::new()
89///     .with_time(TimeOptions::at(Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap()));
90/// let decoded = validator.validate(&jwt, &options).await?;
91/// assert_eq!(decoded.credential, credential);
92/// # Ok::<(), Box<dyn std::error::Error>>(())
93/// # })?;
94/// # }
95/// # Ok::<(), Box<dyn std::error::Error>>(())
96/// ```
97pub 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    /// A validator resolving keys with `resolver` and verifying signatures with `verifier`,
105    /// without a status check.
106    pub fn new(resolver: R, verifier: V) -> Self {
107        Self {
108            resolver,
109            verifier,
110            status: None,
111        }
112    }
113
114    /// This validator checking `credentialStatus` with `check`.
115    #[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    /// The key resolver.
122    pub fn resolver(&self) -> &R {
123        &self.resolver
124    }
125
126    /// The signature verifier.
127    pub fn verifier(&self) -> &V {
128        &self.verifier
129    }
130
131    /// Whether a status check is configured.
132    pub fn has_status_check(&self) -> bool {
133        self.status.is_some()
134    }
135
136    /// Validates a VCDM 1.1 credential JWT.
137    ///
138    /// # Errors
139    ///
140    /// The first [`ValidationError`] found, in the order the [module documentation](super)
141    /// lists the checks.
142    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    /// Validates a VCDM 2.0 credential JWT, whose `typ` is `vc+jwt` when present.
165    ///
166    /// # Errors
167    ///
168    /// As [`validate`](Self::validate).
169    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    /// Resolves the key of `jws` under `relationship` and verifies the signature; the `kid`.
192    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    /// Checks every status entry per `policy`.
218    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
254/// Decodes a VCDM 1.1 credential JWT without verifying or validating anything.
255///
256/// # Errors
257///
258/// [`ValidationError::Jws`], [`ValidationError::Payload`], [`ValidationError::UnexpectedTyp`]
259/// or [`ValidationError::Credential`] when the token does not decode to a credential.
260pub fn decode_credential(jwt: &Jwt) -> Result<DecodedCredential, ValidationError> {
261    decode(&jwt.decode()?)
262}
263
264/// Decodes a VCDM 2.0 credential JWT without verifying or validating anything.
265///
266/// # Errors
267///
268/// As [`decode_credential`].
269pub fn decode_credential_v2(jwt: &Jwt) -> Result<DecodedCredentialV2, ValidationError> {
270    decode_v2(&jwt.decode()?)
271}
272
273/// Decodes a VCDM 1.1 credential JWT.
274pub(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
286/// Decodes a VCDM 2.0 credential JWT: `typ` is `vc+jwt` when present (VC-JOSE-COSE ยง3.1.1),
287/// so a token typed for another purpose is never read as a credential.
288fn 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
300/// Rejects a header `typ` other than `expected`, when one is present.
301pub(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
311/// Checks that `party` is the DID of `kid`, the controller of the signing key.
312pub(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
328/// The mismatch error for `role`.
329fn 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}