Skip to main content

identity_kit/vc/validation/
credential.rs

1//! `JwtCredentialValidator`.
2
3use std::collections::BTreeSet;
4
5use crate::did::Uri;
6use crate::jose::{CompactJws, DefaultVerifier, Jwk, JwsHeader, JwsVerifier};
7use crate::vc::{Credential, CredentialV2, Jwt, JwtClaims, Party};
8
9use chrono::{DateTime, Utc};
10use serde_json::Value;
11
12use super::{
13    CredentialStatus, CredentialValidationOptions, KeyReference, KeyResolver, KeyRole, StatusCheck,
14    StatusPolicy, ValidationError, check_statuses, status_claim, status_claim_entry,
15};
16
17/// A validated or decoded VCDM 1.1 credential JWT: its header, its claims (custom ones
18/// included) and the credential they map to.
19#[derive(Debug, Clone)]
20#[non_exhaustive]
21pub struct DecodedCredential {
22    /// The JWS header.
23    pub header: JwsHeader,
24    /// The claims set, `vc` included.
25    pub claims: JwtClaims,
26    /// The credential, with the registered claims mapped back into it.
27    pub credential: Credential,
28}
29
30/// A validated or decoded VCDM 2.0 credential JWT: its header, its claims and the credential.
31#[derive(Debug, Clone)]
32#[non_exhaustive]
33pub struct DecodedCredentialV2 {
34    /// The JWS header.
35    pub header: JwsHeader,
36    /// The claims set, which is the credential plus any registered claims.
37    pub claims: JwtClaims,
38    /// The credential.
39    pub credential: CredentialV2,
40}
41
42/// Validates credential JWTs, see the [module documentation](super) for the checks.
43///
44/// # Examples
45///
46/// A `did:key` issuer signs a credential about a subject, and the verifier validates it with a
47/// resolver that knows `did:key`.
48///
49/// ```
50/// # #[cfg(feature = "did-key")]
51/// # {
52/// use chrono::{TimeZone as _, Utc};
53/// use identity_kit::did::Uri;
54/// use identity_kit::jose::{DefaultVerifier, Jwk, JwsAlgorithm, JwsHeader, SignError, Signer};
55/// use identity_kit::methods::DidKey;
56/// use identity_kit::resolver::Resolver;
57/// use identity_kit::vc::validation::{CredentialValidationOptions, JwtCredentialValidator, TimeOptions};
58/// use identity_kit::vc::{Credential, Jwt, Subject};
59///
60/// // The Ed25519 key of RFC 8037 Appendix A.1 behind a `Signer`.
61/// struct Issuer(ed25519_dalek::SigningKey);
62///
63/// #[async_trait::async_trait]
64/// impl Signer for Issuer {
65///     fn alg(&self) -> JwsAlgorithm { JwsAlgorithm::EdDSA }
66///     fn kid(&self) -> Option<&str> { None }
67///     async fn sign(&self, input: &[u8]) -> Result<Vec<u8>, SignError> {
68///         use ed25519_dalek::Signer as _;
69///         Ok(self.0.sign(input).to_bytes().to_vec())
70///     }
71/// }
72///
73/// # tokio::runtime::Builder::new_current_thread().build()?.block_on(async {
74/// let secret: [u8; 32] = identity_kit::jose::Jwk::from_ed25519_public(&[0; 32]).map(|_| [
75///     0x9d, 0x61, 0xb1, 0x9d, 0xef, 0xfd, 0x5a, 0x60, 0xba, 0x84, 0x4a, 0xf4, 0x92, 0xec, 0x2c, 0xc4,
76///     0x44, 0x49, 0xc5, 0x69, 0x7b, 0x32, 0x69, 0x19, 0x70, 0x3b, 0xac, 0x03, 0x1c, 0xae, 0x7f, 0x60,
77/// ])?;
78/// let signing_key = ed25519_dalek::SigningKey::from_bytes(&secret);
79/// let jwk = Jwk::from_ed25519_public(signing_key.verifying_key().as_bytes())?;
80/// let issuer = DidKey::did(&jwk)?;
81/// let kid = issuer.join(&format!("#{}", issuer.method_specific_id()))?;
82///
83/// let credential = Credential::new(
84///     &issuer,
85///     Utc.with_ymd_and_hms(2010, 1, 1, 0, 0, 0).unwrap(),
86///     Subject::new(Uri::parse("did:example:ebfeb1f712ebc6f1c276e12ec21")?),
87/// );
88/// let header = JwsHeader::new(JwsAlgorithm::EdDSA).with_typ("JWT").with_kid(kid.as_str());
89/// let jwt = Jwt::sign(&header, &credential.to_jwt_claims()?, &Issuer(signing_key)).await?;
90///
91/// let resolver = Resolver::builder().with(DidKey::default()).build();
92/// let validator = JwtCredentialValidator::new(resolver, DefaultVerifier);
93/// let options = CredentialValidationOptions::new()
94///     .with_time(TimeOptions::at(Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap()));
95/// let decoded = validator.validate(&jwt, &options).await?;
96/// assert_eq!(decoded.credential, credential);
97/// # Ok::<(), Box<dyn std::error::Error>>(())
98/// # })?;
99/// # }
100/// # Ok::<(), Box<dyn std::error::Error>>(())
101/// ```
102pub struct JwtCredentialValidator<R, V = DefaultVerifier> {
103    resolver: R,
104    verifier: V,
105    status: Option<Box<dyn StatusCheck>>,
106}
107
108impl<R: KeyResolver, V: JwsVerifier> JwtCredentialValidator<R, V> {
109    /// A validator resolving keys with `resolver` and verifying signatures with `verifier`,
110    /// without a status check.
111    pub fn new(resolver: R, verifier: V) -> Self {
112        Self {
113            resolver,
114            verifier,
115            status: None,
116        }
117    }
118
119    /// This validator checking `credentialStatus` and the `status` claim with `check`.
120    #[must_use]
121    pub fn with_status_check(mut self, check: impl StatusCheck + 'static) -> Self {
122        self.status = Some(Box::new(check));
123        self
124    }
125
126    /// The key resolver.
127    pub fn resolver(&self) -> &R {
128        &self.resolver
129    }
130
131    /// The signature verifier.
132    pub fn verifier(&self) -> &V {
133        &self.verifier
134    }
135
136    /// Whether a status check is configured.
137    pub fn has_status_check(&self) -> bool {
138        self.status.is_some()
139    }
140
141    /// Validates a VCDM 1.1 credential JWT.
142    ///
143    /// # Errors
144    ///
145    /// The first [`ValidationError`] found, in the order the [module documentation](super)
146    /// lists the checks.
147    pub async fn validate(
148        &self,
149        jwt: &Jwt,
150        options: &CredentialValidationOptions,
151    ) -> Result<DecodedCredential, ValidationError> {
152        let jws = jwt.decode()?;
153        let decoded = decode(&jws, &options.additional_typ)?;
154        let issuer = &decoded.credential.issuer;
155        let (reference, key) = self
156            .verify(&jws, KeyRole::Issuer, Some(issuer.id().as_str()))
157            .await?;
158        check_controller(issuer, &reference, &key, "issuer")?;
159        options.time.check_claims(&decoded.claims)?;
160        options.time.check(
161            Some(decoded.credential.issuance_date),
162            decoded.credential.expiration_date,
163            None,
164        )?;
165        let status = status_claim(&decoded.claims, options.status)?;
166        self.check_status(
167            decoded
168                .credential
169                .credential_status
170                .iter()
171                .map(CredentialStatus::Vcdm)
172                .chain(status.iter().map(status_claim_entry))
173                .collect(),
174            options.status,
175            options.time.now(),
176        )
177        .await?;
178        Ok(decoded)
179    }
180
181    /// Validates a VCDM 2.0 credential JWT, whose `typ` is `vc+jwt` when present.
182    ///
183    /// # Errors
184    ///
185    /// As [`validate`](Self::validate).
186    pub async fn validate_v2(
187        &self,
188        jwt: &Jwt,
189        options: &CredentialValidationOptions,
190    ) -> Result<DecodedCredentialV2, ValidationError> {
191        let jws = jwt.decode()?;
192        let decoded = decode_v2(&jws, &options.additional_typ)?;
193        let issuer = &decoded.credential.issuer;
194        let (reference, key) = self
195            .verify(&jws, KeyRole::Issuer, Some(issuer.id().as_str()))
196            .await?;
197        check_controller(issuer, &reference, &key, "issuer")?;
198        options.time.check_claims(&decoded.claims)?;
199        options.time.check(
200            decoded.credential.valid_from,
201            decoded.credential.valid_until,
202            None,
203        )?;
204        let status = status_claim(&decoded.claims, options.status)?;
205        self.check_status(
206            decoded
207                .credential
208                .statuses()
209                .map(CredentialStatus::Vcdm)
210                .chain(status.iter().map(status_claim_entry))
211                .collect(),
212            options.status,
213            options.time.now(),
214        )
215        .await?;
216        Ok(decoded)
217    }
218
219    /// Resolves the key `jws` names, `iss` being its issuer, for `role` and verifies the
220    /// signature; the reference and the key.
221    pub(super) async fn verify(
222        &self,
223        jws: &CompactJws,
224        role: KeyRole,
225        iss: Option<&str>,
226    ) -> Result<(KeyReference, Jwk), ValidationError> {
227        let reference = KeyReference::from_jws(jws, iss)?;
228        let key = self
229            .resolver
230            .resolve_key(&reference, role)
231            .await
232            .map_err(|source| ValidationError::KeyResolution {
233                key: Box::new(reference.clone()),
234                source,
235            })?;
236        jws.verify(&self.verifier, &key)?;
237        Ok((reference, key))
238    }
239
240    /// Checks every status entry with this validator's check, per `policy`, at `now`.
241    pub(super) async fn check_status(
242        &self,
243        statuses: Vec<CredentialStatus<'_>>,
244        policy: StatusPolicy,
245        now: DateTime<Utc>,
246    ) -> Result<(), ValidationError> {
247        check_statuses(self.status.as_deref(), statuses, policy, now).await
248    }
249}
250
251impl<R: core::fmt::Debug, V: core::fmt::Debug> core::fmt::Debug for JwtCredentialValidator<R, V> {
252    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
253        f.debug_struct("JwtCredentialValidator")
254            .field("resolver", &self.resolver)
255            .field("verifier", &self.verifier)
256            .field("status", &self.status.as_ref().map(|_| "StatusCheck"))
257            .finish()
258    }
259}
260
261/// Decodes a VCDM 1.1 credential JWT without verifying or validating anything.
262///
263/// # Errors
264///
265/// [`ValidationError::Jws`], [`ValidationError::Payload`], [`ValidationError::UnexpectedTyp`]
266/// or [`ValidationError::Credential`] when the token does not decode to a credential, and
267/// [`ValidationError::SdJwtPayload`] when it is the issuer-signed JWT of an SD-JWT.
268pub fn decode_credential(jwt: &Jwt) -> Result<DecodedCredential, ValidationError> {
269    decode(&jwt.decode()?, &NO_TYP)
270}
271
272/// Decodes a VCDM 2.0 credential JWT without verifying or validating anything.
273///
274/// # Errors
275///
276/// As [`decode_credential`].
277pub fn decode_credential_v2(jwt: &Jwt) -> Result<DecodedCredentialV2, ValidationError> {
278    decode_v2(&jwt.decode()?, &NO_TYP)
279}
280
281/// No `typ` besides the profile's own.
282pub(super) static NO_TYP: BTreeSet<String> = BTreeSet::new();
283
284/// Decodes a VCDM 1.1 credential JWT whose `typ`, when present, is `JWT` or one of
285/// `additional_typ`.
286pub(super) fn decode(
287    jws: &CompactJws,
288    additional_typ: &BTreeSet<String>,
289) -> Result<DecodedCredential, ValidationError> {
290    check_typ(jws.header(), "JWT", additional_typ)?;
291    let claims = plain_claims(jws)?;
292    let credential = Credential::from_jwt_claims(&claims)?;
293    Ok(DecodedCredential {
294        header: jws.header().clone(),
295        claims,
296        credential,
297    })
298}
299
300/// Decodes a VCDM 2.0 credential JWT: `typ` is `vc+jwt` when present (VC-JOSE-COSE §3.1.1),
301/// or one of `additional_typ`, so a token typed for another purpose is never read as a
302/// credential.
303pub(super) fn decode_v2(
304    jws: &CompactJws,
305    additional_typ: &BTreeSet<String>,
306) -> Result<DecodedCredentialV2, ValidationError> {
307    check_typ(jws.header(), CredentialV2::JWT_TYP, additional_typ)?;
308    let claims = plain_claims(jws)?;
309    let credential = CredentialV2::from_jwt_claims(&claims)?;
310    Ok(DecodedCredentialV2 {
311        header: jws.header().clone(),
312        claims,
313        credential,
314    })
315}
316
317/// The claims set of `jws`, refused when it is the issuer-signed JWT of an SD-JWT: see
318/// [`ValidationError::SdJwtPayload`].
319pub(super) fn plain_claims(jws: &CompactJws) -> Result<JwtClaims, ValidationError> {
320    let claims: JwtClaims =
321        serde_json::from_slice(jws.payload()).map_err(ValidationError::Payload)?;
322    match claims
323        .additional
324        .iter()
325        .find_map(|(name, value)| sd_jwt_member(name, value))
326    {
327        Some(claim) => Err(ValidationError::SdJwtPayload { claim }),
328        None => Ok(claims),
329    }
330}
331
332/// The names RFC 9901 §4.2 reserves in the payload of an SD-JWT: `_sd` and `_sd_alg` as
333/// member names, and `...` as the one member of an array element standing for a digest.
334const SD_JWT_NAMES: [&str; 2] = ["_sd", "_sd_alg"];
335const ELLIPSIS: &str = "...";
336
337/// The SD-JWT name the member `name` with `value` is, or holds anywhere below it. `serde_json`
338/// limits the nesting of a parsed payload, so the recursion is bounded.
339fn sd_jwt_member(name: &str, value: &Value) -> Option<&'static str> {
340    SD_JWT_NAMES
341        .into_iter()
342        .find(|reserved| *reserved == name)
343        .or_else(|| sd_jwt_name_in(value))
344}
345
346/// As [`sd_jwt_member`], for the members and elements of `value`.
347fn sd_jwt_name_in(value: &Value) -> Option<&'static str> {
348    match value {
349        Value::Object(object) => object
350            .iter()
351            .find_map(|(name, value)| sd_jwt_member(name, value)),
352        Value::Array(array) => array.iter().find_map(|element| match element {
353            Value::Object(object)
354                if object.len() == 1 && object.get(ELLIPSIS).is_some_and(Value::is_string) =>
355            {
356                Some(ELLIPSIS)
357            }
358            element => sd_jwt_name_in(element),
359        }),
360        _ => None,
361    }
362}
363
364/// Rejects a header `typ` other than `expected` and `additional`, when one is present.
365pub(super) fn check_typ(
366    header: &JwsHeader,
367    expected: &'static str,
368    additional: &BTreeSet<String>,
369) -> Result<(), ValidationError> {
370    match header.typ.as_deref() {
371        Some(found) if found != expected && !additional.contains(found) => {
372            Err(ValidationError::UnexpectedTyp {
373                expected,
374                found: found.to_owned(),
375            })
376        }
377        _ => Ok(()),
378    }
379}
380
381/// Checks that `key`, which `reference` resolved to, belongs to `party`
382/// ([`KeyReference::binds_to`]).
383pub(super) fn check_controller(
384    party: &Party,
385    reference: &KeyReference,
386    key: &Jwk,
387    role: &'static str,
388) -> Result<(), ValidationError> {
389    let id = party.id();
390    if !reference.binds_to(Some(id.as_str()), key) {
391        return Err(mismatch(role, id.clone(), reference.clone()));
392    }
393    Ok(())
394}
395
396/// The mismatch error for `role`.
397fn mismatch(role: &'static str, id: Uri, key: KeyReference) -> ValidationError {
398    if role == "holder" {
399        ValidationError::HolderMismatch {
400            holder: id,
401            key: Box::new(key),
402        }
403    } else {
404        ValidationError::IssuerMismatch {
405            issuer: id,
406            key: Box::new(key),
407        }
408    }
409}