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,
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` 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        self.check_status(
166            decoded
167                .credential
168                .credential_status
169                .iter()
170                .map(CredentialStatus::Vcdm),
171            options.status,
172            options.time.now(),
173        )
174        .await?;
175        Ok(decoded)
176    }
177
178    /// Validates a VCDM 2.0 credential JWT, whose `typ` is `vc+jwt` when present.
179    ///
180    /// # Errors
181    ///
182    /// As [`validate`](Self::validate).
183    pub async fn validate_v2(
184        &self,
185        jwt: &Jwt,
186        options: &CredentialValidationOptions,
187    ) -> Result<DecodedCredentialV2, ValidationError> {
188        let jws = jwt.decode()?;
189        let decoded = decode_v2(&jws, &options.additional_typ)?;
190        let issuer = &decoded.credential.issuer;
191        let (reference, key) = self
192            .verify(&jws, KeyRole::Issuer, Some(issuer.id().as_str()))
193            .await?;
194        check_controller(issuer, &reference, &key, "issuer")?;
195        options.time.check_claims(&decoded.claims)?;
196        options.time.check(
197            decoded.credential.valid_from,
198            decoded.credential.valid_until,
199            None,
200        )?;
201        self.check_status(
202            decoded.credential.statuses().map(CredentialStatus::Vcdm),
203            options.status,
204            options.time.now(),
205        )
206        .await?;
207        Ok(decoded)
208    }
209
210    /// Resolves the key `jws` names, `iss` being its issuer, for `role` and verifies the
211    /// signature; the reference and the key.
212    pub(super) async fn verify(
213        &self,
214        jws: &CompactJws,
215        role: KeyRole,
216        iss: Option<&str>,
217    ) -> Result<(KeyReference, Jwk), ValidationError> {
218        let reference = KeyReference::from_jws(jws, iss)?;
219        let key = self
220            .resolver
221            .resolve_key(&reference, role)
222            .await
223            .map_err(|source| ValidationError::KeyResolution {
224                key: Box::new(reference.clone()),
225                source,
226            })?;
227        jws.verify(&self.verifier, &key)?;
228        Ok((reference, key))
229    }
230
231    /// Checks every status entry with this validator's check, per `policy`, at `now`.
232    pub(super) async fn check_status<'a>(
233        &self,
234        statuses: impl IntoIterator<Item = CredentialStatus<'a>>,
235        policy: StatusPolicy,
236        now: DateTime<Utc>,
237    ) -> Result<(), ValidationError> {
238        check_statuses(self.status.as_deref(), statuses, policy, now).await
239    }
240}
241
242impl<R: core::fmt::Debug, V: core::fmt::Debug> core::fmt::Debug for JwtCredentialValidator<R, V> {
243    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
244        f.debug_struct("JwtCredentialValidator")
245            .field("resolver", &self.resolver)
246            .field("verifier", &self.verifier)
247            .field("status", &self.status.as_ref().map(|_| "StatusCheck"))
248            .finish()
249    }
250}
251
252/// Decodes a VCDM 1.1 credential JWT without verifying or validating anything.
253///
254/// # Errors
255///
256/// [`ValidationError::Jws`], [`ValidationError::Payload`], [`ValidationError::UnexpectedTyp`]
257/// or [`ValidationError::Credential`] when the token does not decode to a credential, and
258/// [`ValidationError::SdJwtPayload`] when it is the issuer-signed JWT of an SD-JWT.
259pub fn decode_credential(jwt: &Jwt) -> Result<DecodedCredential, ValidationError> {
260    decode(&jwt.decode()?, &NO_TYP)
261}
262
263/// Decodes a VCDM 2.0 credential JWT without verifying or validating anything.
264///
265/// # Errors
266///
267/// As [`decode_credential`].
268pub fn decode_credential_v2(jwt: &Jwt) -> Result<DecodedCredentialV2, ValidationError> {
269    decode_v2(&jwt.decode()?, &NO_TYP)
270}
271
272/// No `typ` besides the profile's own.
273pub(super) static NO_TYP: BTreeSet<String> = BTreeSet::new();
274
275/// Decodes a VCDM 1.1 credential JWT whose `typ`, when present, is `JWT` or one of
276/// `additional_typ`.
277pub(super) fn decode(
278    jws: &CompactJws,
279    additional_typ: &BTreeSet<String>,
280) -> Result<DecodedCredential, ValidationError> {
281    check_typ(jws.header(), "JWT", additional_typ)?;
282    let claims = plain_claims(jws)?;
283    let credential = Credential::from_jwt_claims(&claims)?;
284    Ok(DecodedCredential {
285        header: jws.header().clone(),
286        claims,
287        credential,
288    })
289}
290
291/// Decodes a VCDM 2.0 credential JWT: `typ` is `vc+jwt` when present (VC-JOSE-COSE §3.1.1),
292/// or one of `additional_typ`, so a token typed for another purpose is never read as a
293/// credential.
294pub(super) fn decode_v2(
295    jws: &CompactJws,
296    additional_typ: &BTreeSet<String>,
297) -> Result<DecodedCredentialV2, ValidationError> {
298    check_typ(jws.header(), CredentialV2::JWT_TYP, additional_typ)?;
299    let claims = plain_claims(jws)?;
300    let credential = CredentialV2::from_jwt_claims(&claims)?;
301    Ok(DecodedCredentialV2 {
302        header: jws.header().clone(),
303        claims,
304        credential,
305    })
306}
307
308/// The claims set of `jws`, refused when it is the issuer-signed JWT of an SD-JWT: see
309/// [`ValidationError::SdJwtPayload`].
310pub(super) fn plain_claims(jws: &CompactJws) -> Result<JwtClaims, ValidationError> {
311    let claims: JwtClaims = serde_json::from_slice(jws.payload())
312        .map_err(|e| ValidationError::Payload(e.to_string()))?;
313    match claims
314        .additional
315        .iter()
316        .find_map(|(name, value)| sd_jwt_member(name, value))
317    {
318        Some(claim) => Err(ValidationError::SdJwtPayload { claim }),
319        None => Ok(claims),
320    }
321}
322
323/// The names RFC 9901 §4.2 reserves in the payload of an SD-JWT: `_sd` and `_sd_alg` as
324/// member names, and `...` as the one member of an array element standing for a digest.
325const SD_JWT_NAMES: [&str; 2] = ["_sd", "_sd_alg"];
326const ELLIPSIS: &str = "...";
327
328/// The SD-JWT name the member `name` with `value` is, or holds anywhere below it. serde_json
329/// limits the nesting of a parsed payload, so the recursion is bounded.
330fn sd_jwt_member(name: &str, value: &Value) -> Option<&'static str> {
331    SD_JWT_NAMES
332        .into_iter()
333        .find(|reserved| *reserved == name)
334        .or_else(|| sd_jwt_name_in(value))
335}
336
337/// As [`sd_jwt_member`], for the members and elements of `value`.
338fn sd_jwt_name_in(value: &Value) -> Option<&'static str> {
339    match value {
340        Value::Object(object) => object
341            .iter()
342            .find_map(|(name, value)| sd_jwt_member(name, value)),
343        Value::Array(array) => array.iter().find_map(|element| match element {
344            Value::Object(object)
345                if object.len() == 1 && object.get(ELLIPSIS).is_some_and(Value::is_string) =>
346            {
347                Some(ELLIPSIS)
348            }
349            element => sd_jwt_name_in(element),
350        }),
351        _ => None,
352    }
353}
354
355/// Rejects a header `typ` other than `expected` and `additional`, when one is present.
356pub(super) fn check_typ(
357    header: &JwsHeader,
358    expected: &'static str,
359    additional: &BTreeSet<String>,
360) -> Result<(), ValidationError> {
361    match header.typ.as_deref() {
362        Some(found) if found != expected && !additional.contains(found) => {
363            Err(ValidationError::UnexpectedTyp {
364                expected,
365                found: found.to_owned(),
366            })
367        }
368        _ => Ok(()),
369    }
370}
371
372/// Checks that `key`, which `reference` resolved to, belongs to `party`
373/// ([`KeyReference::binds_to`]).
374pub(super) fn check_controller(
375    party: &Party,
376    reference: &KeyReference,
377    key: &Jwk,
378    role: &'static str,
379) -> Result<(), ValidationError> {
380    let id = party.id();
381    if !reference.binds_to(Some(id.as_str()), key) {
382        return Err(mismatch(role, id.clone(), reference.clone()));
383    }
384    Ok(())
385}
386
387/// The mismatch error for `role`.
388fn mismatch(role: &'static str, id: Uri, key: KeyReference) -> ValidationError {
389    if role == "holder" {
390        ValidationError::HolderMismatch {
391            holder: id,
392            key: Box::new(key),
393        }
394    } else {
395        ValidationError::IssuerMismatch {
396            issuer: id,
397            key: Box::new(key),
398        }
399    }
400}