identity-kit 0.1.0

DID, key management and verifiable credential primitives
Documentation
//! `JwtCredentialValidator`.

use crate::did::{Did, DidUrl, Uri, VerificationRelationship};
use crate::jose::{CompactJws, DefaultVerifier, JwsHeader, JwsVerifier};
use crate::vc::{Credential, CredentialV2, Jwt, JwtClaims, Party, TypedObject};

use super::{
    CredentialValidationOptions, KeyResolver, StatusCheck, StatusError, StatusPolicy,
    ValidationError,
};

/// A validated or decoded VCDM 1.1 credential JWT: its header, its claims (custom ones
/// included) and the credential they map to.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct DecodedCredential {
    /// The JWS header.
    pub header: JwsHeader,
    /// The claims set, `vc` included.
    pub claims: JwtClaims,
    /// The credential, with the registered claims mapped back into it.
    pub credential: Credential,
}

/// A validated or decoded VCDM 2.0 credential JWT: its header, its claims and the credential.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct DecodedCredentialV2 {
    /// The JWS header.
    pub header: JwsHeader,
    /// The claims set, which is the credential plus any registered claims.
    pub claims: JwtClaims,
    /// The credential.
    pub credential: CredentialV2,
}

/// Validates credential JWTs, see the [module documentation](super) for the checks.
///
/// # Examples
///
/// A `did:key` issuer signs a credential about a subject, and the verifier validates it with a
/// resolver that knows `did:key`.
///
/// ```
/// # #[cfg(feature = "did-key")]
/// # {
/// use chrono::{TimeZone as _, Utc};
/// use identity_kit::did::Uri;
/// use identity_kit::jose::{DefaultVerifier, Jwk, JwsAlgorithm, JwsHeader, SignError, Signer};
/// use identity_kit::methods::DidKey;
/// use identity_kit::resolver::Resolver;
/// use identity_kit::vc::validation::{CredentialValidationOptions, JwtCredentialValidator, TimeOptions};
/// use identity_kit::vc::{Credential, Jwt, Subject};
///
/// // The Ed25519 key of RFC 8037 Appendix A.1 behind a `Signer`.
/// struct Issuer(ed25519_dalek::SigningKey);
///
/// #[async_trait::async_trait]
/// impl Signer for Issuer {
///     fn alg(&self) -> JwsAlgorithm { JwsAlgorithm::EdDSA }
///     fn kid(&self) -> Option<&str> { None }
///     async fn sign(&self, input: &[u8]) -> Result<Vec<u8>, SignError> {
///         use ed25519_dalek::Signer as _;
///         Ok(self.0.sign(input).to_bytes().to_vec())
///     }
/// }
///
/// # tokio::runtime::Builder::new_current_thread().build()?.block_on(async {
/// let secret: [u8; 32] = identity_kit::jose::Jwk::from_ed25519_public(&[0; 32]).map(|_| [
///     0x9d, 0x61, 0xb1, 0x9d, 0xef, 0xfd, 0x5a, 0x60, 0xba, 0x84, 0x4a, 0xf4, 0x92, 0xec, 0x2c, 0xc4,
///     0x44, 0x49, 0xc5, 0x69, 0x7b, 0x32, 0x69, 0x19, 0x70, 0x3b, 0xac, 0x03, 0x1c, 0xae, 0x7f, 0x60,
/// ])?;
/// let signing_key = ed25519_dalek::SigningKey::from_bytes(&secret);
/// let jwk = Jwk::from_ed25519_public(signing_key.verifying_key().as_bytes())?;
/// let issuer = DidKey::did(&jwk)?;
/// let kid = issuer.join(&format!("#{}", issuer.method_specific_id()))?;
///
/// let credential = Credential::new(
///     &issuer,
///     Utc.with_ymd_and_hms(2010, 1, 1, 0, 0, 0).unwrap(),
///     Subject::new(Uri::parse("did:example:ebfeb1f712ebc6f1c276e12ec21")?),
/// );
/// let header = JwsHeader::new(JwsAlgorithm::EdDSA).with_typ("JWT").with_kid(kid.as_str());
/// let jwt = Jwt::sign(&header, &credential.to_jwt_claims()?, &Issuer(signing_key)).await?;
///
/// let resolver = Resolver::builder().with(DidKey::default()).build();
/// let validator = JwtCredentialValidator::new(resolver, DefaultVerifier);
/// let options = CredentialValidationOptions::new()
///     .with_time(TimeOptions::at(Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap()));
/// let decoded = validator.validate(&jwt, &options).await?;
/// assert_eq!(decoded.credential, credential);
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// # })?;
/// # }
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
pub struct JwtCredentialValidator<R, V = DefaultVerifier> {
    resolver: R,
    verifier: V,
    status: Option<Box<dyn StatusCheck>>,
}

impl<R: KeyResolver, V: JwsVerifier> JwtCredentialValidator<R, V> {
    /// A validator resolving keys with `resolver` and verifying signatures with `verifier`,
    /// without a status check.
    pub fn new(resolver: R, verifier: V) -> Self {
        Self {
            resolver,
            verifier,
            status: None,
        }
    }

    /// This validator checking `credentialStatus` with `check`.
    #[must_use]
    pub fn with_status_check(mut self, check: impl StatusCheck + 'static) -> Self {
        self.status = Some(Box::new(check));
        self
    }

    /// The key resolver.
    pub fn resolver(&self) -> &R {
        &self.resolver
    }

    /// The signature verifier.
    pub fn verifier(&self) -> &V {
        &self.verifier
    }

    /// Whether a status check is configured.
    pub fn has_status_check(&self) -> bool {
        self.status.is_some()
    }

    /// Validates a VCDM 1.1 credential JWT.
    ///
    /// # Errors
    ///
    /// The first [`ValidationError`] found, in the order the [module documentation](super)
    /// lists the checks.
    pub async fn validate(
        &self,
        jwt: &Jwt,
        options: &CredentialValidationOptions,
    ) -> Result<DecodedCredential, ValidationError> {
        let jws = jwt.decode()?;
        let decoded = decode(&jws)?;
        let kid = self
            .verify(&jws, VerificationRelationship::AssertionMethod)
            .await?;
        check_controller(&decoded.credential.issuer, &kid, "issuer")?;
        options.time.check_claims(&decoded.claims)?;
        options.time.check(
            Some(decoded.credential.issuance_date),
            decoded.credential.expiration_date,
            None,
        )?;
        self.check_status(decoded.credential.credential_status.iter(), options.status)
            .await?;
        Ok(decoded)
    }

    /// Validates a VCDM 2.0 credential JWT, whose `typ` is `vc+jwt` when present.
    ///
    /// # Errors
    ///
    /// As [`validate`](Self::validate).
    pub async fn validate_v2(
        &self,
        jwt: &Jwt,
        options: &CredentialValidationOptions,
    ) -> Result<DecodedCredentialV2, ValidationError> {
        let jws = jwt.decode()?;
        let decoded = decode_v2(&jws)?;
        let kid = self
            .verify(&jws, VerificationRelationship::AssertionMethod)
            .await?;
        check_controller(&decoded.credential.issuer, &kid, "issuer")?;
        options.time.check_claims(&decoded.claims)?;
        options.time.check(
            decoded.credential.valid_from,
            decoded.credential.valid_until,
            None,
        )?;
        self.check_status(decoded.credential.statuses(), options.status)
            .await?;
        Ok(decoded)
    }

    /// Resolves the key of `jws` under `relationship` and verifies the signature; the `kid`.
    pub(super) async fn verify(
        &self,
        jws: &CompactJws,
        relationship: VerificationRelationship,
    ) -> Result<DidUrl, ValidationError> {
        let kid = jws.kid().ok_or(ValidationError::MissingKid)?;
        let kid = DidUrl::parse(kid).map_err(|source| ValidationError::InvalidKid {
            kid: kid.to_owned(),
            source,
        })?;
        if kid.path().is_some() || kid.query().is_some() {
            return Err(ValidationError::KidWithParameters(kid));
        }
        let key = self
            .resolver
            .resolve_key(&kid, relationship)
            .await
            .map_err(|source| ValidationError::KeyResolution {
                kid: kid.clone(),
                source,
            })?;
        jws.verify(&self.verifier, &key)?;
        Ok(kid)
    }

    /// Checks every status entry per `policy`.
    async fn check_status<'a>(
        &self,
        statuses: impl Iterator<Item = &'a TypedObject>,
        policy: StatusPolicy,
    ) -> Result<(), ValidationError> {
        if policy == StatusPolicy::SkipAll {
            return Ok(());
        }
        for status in statuses {
            let result = match &self.status {
                Some(check) => check.check(status).await,
                None => Err(StatusError::Unsupported(status.object_type().to_owned())),
            };
            match result {
                Ok(()) => {}
                Err(StatusError::Unsupported(_)) if policy == StatusPolicy::SkipUnsupported => {}
                Err(StatusError::Unsupported(status_type)) => {
                    return Err(ValidationError::StatusUnchecked(status_type));
                }
                Err(error) => return Err(ValidationError::Status(error)),
            }
        }
        Ok(())
    }
}

impl<R: core::fmt::Debug, V: core::fmt::Debug> core::fmt::Debug for JwtCredentialValidator<R, V> {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("JwtCredentialValidator")
            .field("resolver", &self.resolver)
            .field("verifier", &self.verifier)
            .field("status", &self.status.as_ref().map(|_| "StatusCheck"))
            .finish()
    }
}

/// Decodes a VCDM 1.1 credential JWT without verifying or validating anything.
///
/// # Errors
///
/// [`ValidationError::Jws`], [`ValidationError::Payload`], [`ValidationError::UnexpectedTyp`]
/// or [`ValidationError::Credential`] when the token does not decode to a credential.
pub fn decode_credential(jwt: &Jwt) -> Result<DecodedCredential, ValidationError> {
    decode(&jwt.decode()?)
}

/// Decodes a VCDM 2.0 credential JWT without verifying or validating anything.
///
/// # Errors
///
/// As [`decode_credential`].
pub fn decode_credential_v2(jwt: &Jwt) -> Result<DecodedCredentialV2, ValidationError> {
    decode_v2(&jwt.decode()?)
}

/// Decodes a VCDM 1.1 credential JWT.
pub(super) fn decode(jws: &CompactJws) -> Result<DecodedCredential, ValidationError> {
    check_typ(jws.header(), "JWT")?;
    let claims: JwtClaims = serde_json::from_slice(jws.payload())
        .map_err(|e| ValidationError::Payload(e.to_string()))?;
    let credential = Credential::from_jwt_claims(&claims)?;
    Ok(DecodedCredential {
        header: jws.header().clone(),
        claims,
        credential,
    })
}

/// Decodes a VCDM 2.0 credential JWT: `typ` is `vc+jwt` when present (VC-JOSE-COSE ยง3.1.1),
/// so a token typed for another purpose is never read as a credential.
fn decode_v2(jws: &CompactJws) -> Result<DecodedCredentialV2, ValidationError> {
    check_typ(jws.header(), CredentialV2::JWT_TYP)?;
    let claims: JwtClaims = serde_json::from_slice(jws.payload())
        .map_err(|e| ValidationError::Payload(e.to_string()))?;
    let credential = CredentialV2::from_jwt_claims(&claims)?;
    Ok(DecodedCredentialV2 {
        header: jws.header().clone(),
        claims,
        credential,
    })
}

/// Rejects a header `typ` other than `expected`, when one is present.
pub(super) fn check_typ(header: &JwsHeader, expected: &'static str) -> Result<(), ValidationError> {
    match header.typ.as_deref() {
        Some(found) if found != expected => Err(ValidationError::UnexpectedTyp {
            expected,
            found: found.to_owned(),
        }),
        _ => Ok(()),
    }
}

/// Checks that `party` is the DID of `kid`, the controller of the signing key.
pub(super) fn check_controller(
    party: &Party,
    kid: &DidUrl,
    role: &'static str,
) -> Result<(), ValidationError> {
    let id = party.id();
    let did = Did::parse(id.as_str()).map_err(|_| ValidationError::NotDid {
        role,
        id: id.to_string(),
    })?;
    if did != *kid.did() {
        return Err(mismatch(role, id.clone(), kid.clone()));
    }
    Ok(())
}

/// The mismatch error for `role`.
fn mismatch(role: &'static str, id: Uri, kid: DidUrl) -> ValidationError {
    if role == "holder" {
        ValidationError::HolderMismatch { holder: id, kid }
    } else {
        ValidationError::IssuerMismatch { issuer: id, kid }
    }
}