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