Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ chrono = "0.4"
did-key = "0.2"
getset = "0.1"
http = "1.3.1"
identity_core = { git = "https://github.com/iotaledger/identity", tag = "v1.9.4-beta.1" }
identity_did = { git = "https://github.com/iotaledger/identity", tag = "v1.9.4-beta.1" }
identity_document = { git = "https://github.com/iotaledger/identity", tag = "v1.9.4-beta.1" }
identity_ecdsa_verifier = { git = "https://github.com/iotaledger/identity", tag = "v1.9.4-beta.1" }
Expand Down
2 changes: 2 additions & 0 deletions oid4vc-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ did-key.workspace = true
did_url = "0.1.0"
ed25519-dalek = { version = "2.0.0", features = ["rand_core"] }
getset = "0.1.2"
identity_core.workspace = true
identity_credential.workspace = true
identity_did.workspace = true
identity_document.workspace = true
identity_ecdsa_verifier.workspace = true
Expand Down
72 changes: 70 additions & 2 deletions oid4vc-core/src/jwt.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
use crate::Sign;
use crate::{
credential_status_verifier::CredentialStatusVerifier, verification_material_resolver::VerificationMaterialResolver,
Sign,
};
use anyhow::{anyhow, Result};
use getset::Getters;
use jsonwebtoken::{Algorithm, DecodingKey, Header, Validation};
use jsonwebtoken::{decode_header, jwk::Jwk as JsonWebTokenJwk, Algorithm, DecodingKey, Header, Validation};
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::sync::Arc;

use identity_core::convert::{FromJson as _, ToJson as _};
use identity_credential::credential::Jwt;
use identity_verification::jws::Decoder;
use serde_json::Value;

#[derive(Debug, Serialize, Getters)]
pub struct JsonWebToken<C>
where
Expand Down Expand Up @@ -84,6 +92,66 @@ where
Ok(base64_url::encode(serde_json::to_vec(value)?.as_slice()))
}

/// Validate a credential JWT: resolve the issuer's public key, verify the
/// signature, extract the `vc` claim, and optionally check credential status.
/// This fn expects the JWT to have the credential in the `vc` claim as prescribed by the jwt_vc_json format as defined here: https://www.w3.org/TR/vc-data-model-1.1/#jwt-encoding
pub async fn validate_credential_jwt(
resolver: &impl VerificationMaterialResolver,
credential_status_verifier: &impl CredentialStatusVerifier,
credential_jwt: &Jwt,
) -> Result<Value> {
let validation_item = Decoder::new()
.decode_compact_serialization(credential_jwt.as_str().as_bytes(), None)
.map_err(|e| anyhow!("JWS decoding error: {e}"))?;

let kid_str = validation_item
Comment thread
coplat marked this conversation as resolved.
.kid()
.ok_or_else(|| anyhow!("Missing KID in JWT header"))?;

// TODO: verify whether issuer is trusted (through `trusted_authorities`).

let public_key_jwk = resolver
.resolve_public_key(kid_str)
.await
.map_err(|e| anyhow!("Verification material resolution error: {e}"))?;
Comment thread
coplat marked this conversation as resolved.
let decoding_key = convert_iota_jwk_to_decoding_key(&public_key_jwk)
.ok_or_else(|| anyhow!("Failed to convert JWK to DecodingKey"))?;

let jwt_header = decode_header(credential_jwt.as_str()).map_err(|e| anyhow!("JWT header decoding error: {e}"))?;

// The below validation settings are disabled because since different specs require different claims and this fn needs to be agnostic of those specs.
let mut validation = Validation::new(jwt_header.alg);
validation.validate_aud = false;
validation.required_spec_claims.clear();

let jwt_data = jsonwebtoken::decode::<Value>(credential_jwt.as_str(), &decoding_key, &validation)
.map_err(|e| anyhow!("JWT validation error: {e}"))?;

let credential = jwt_data
.claims
.get("vc")
.ok_or_else(|| anyhow!("JWT is missing the `vc` claim"))?
.clone();

if let Some(status_value) = jwt_data.claims.get("status").cloned() {
credential_status_verifier
.check_credential_status(status_value)
.await
.map_err(|_| anyhow!("Credential status is invalid"))?;
}

Ok(credential)
}

/// Convert an `identity_jose` JWK into a `jsonwebtoken` [`DecodingKey`].
fn convert_iota_jwk_to_decoding_key(public_key: &identity_jose::jwk::Jwk) -> Option<DecodingKey> {
public_key
.to_json()
.ok()
.and_then(|json| JsonWebTokenJwk::from_json(&json).ok())
.and_then(|jwk| DecodingKey::from_jwk(&jwk).ok())
}

#[cfg(feature = "test-utils")]
#[cfg(test)]
mod tests {
Expand Down
81 changes: 27 additions & 54 deletions oid4vp/src/token/vp_token_validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ use identity_credential::{
sd_jwt_payload::{SdJwt, Sha256Hasher},
sd_jwt_vc::SdJwtVc,
validator::{
DecodedJwtCredential, DecodedJwtPresentation, FailFast, JwtCredentialValidationOptions, JwtCredentialValidator,
JwtPresentationValidator, SdJwtCredentialValidator, StatusCheck,
DecodedJwtPresentation, JwtCredentialValidationOptions, JwtPresentationValidator, SdJwtCredentialValidator,
StatusCheck,
},
};
use identity_did::DIDUrl;
use identity_verification::jws::{Decoder, JwsVerifier};
use nutype::nutype;
use oid4vc_core::{credential_status_verifier::CredentialStatusVerifier, utils::predicates::not_empty};
use oid4vc_core::{
credential_status_verifier::CredentialStatusVerifier, jwt::validate_credential_jwt, utils::predicates::not_empty,
};
use oid4vc_core::{
types::string_or_object::StringOrObject, verification_material_resolver::VerificationMaterialResolver, JsonObject,
};
Expand All @@ -38,13 +40,11 @@ pub enum VpTokenValidationError {
#[error("Verification material resolution error: {0}")]
VerificationMaterialResolutionError(String),
#[error("JWT validation error: {0}")]
JwtValidation(#[from] identity_credential::validator::JwtValidationError),
#[error("Credential validation error: {0}")]
CredentialValidation(#[from] identity_credential::validator::CompoundCredentialValidationError),
JwtValidation(String),
#[error("Presentation validation error: {0}")]
PresentationValidation(#[from] identity_credential::validator::CompoundJwtPresentationValidationError),
#[error("Serialization error: {0}")]
SerializationError(#[from] serde_json::Error),
SerializationError(String),
#[error("SD-JWT parsing error: {0}")]
SdJwtParsingError(String),
#[error("SD-JWT validation error: {0}")]
Expand Down Expand Up @@ -84,7 +84,6 @@ pub enum VpTokenValidationError {
/// A type validating [`VpToken`]s.
pub struct VpTokenValidator<'a, V: JwsVerifier, VMR: VerificationMaterialResolver, CSV: CredentialStatusVerifier> {
jwt_presentation_validator: JwtPresentationValidator<V>,
jwt_credential_validator: JwtCredentialValidator<V>,
sd_jwt_credential_validator: SdJwtCredentialValidator<V>,
signature_verifier: &'a V,
verification_material_resolver: &'a VMR,
Expand All @@ -103,7 +102,6 @@ impl<'a, SV: JwsVerifier + Clone, VMR: VerificationMaterialResolver, CSV: Creden
) -> Self {
Self {
jwt_presentation_validator: JwtPresentationValidator::with_signature_verifier(signature_verifier.clone()),
jwt_credential_validator: JwtCredentialValidator::with_signature_verifier(signature_verifier.clone()),
sd_jwt_credential_validator: SdJwtCredentialValidator::new(signature_verifier.clone(), Sha256Hasher),
signature_verifier,
verification_material_resolver,
Expand Down Expand Up @@ -215,9 +213,15 @@ impl<'a, SV: JwsVerifier + Clone, VMR: VerificationMaterialResolver, CSV: Creden
};

for credential_jwt in credential_jwts {
let decoded_credential = self.validate_credential_jwt(&credential_jwt).await?;
let decoded_credential = validate_credential_jwt(
self.verification_material_resolver,
self.credential_status_verifier,
&credential_jwt,
)
.await
.map_err(|e| VpTokenValidationError::JwtValidation(e.to_string()))?;

let obj = serde_json::to_value(decoded_credential.credential)?
let obj = decoded_credential
.as_object()
.cloned()
.ok_or(VpTokenValidationError::InvalidDecodedCredentialType)?;
Expand Down Expand Up @@ -248,7 +252,12 @@ impl<'a, SV: JwsVerifier + Clone, VMR: VerificationMaterialResolver, CSV: Creden
.validate_sd_jwt_vc(&sd_jwt_vc, client_id, nonce, require_holder_binding)
.await?;

let obj = serde_json::to_value(decoded_sd_jwt_vc)?
let obj = serde_json::to_value(decoded_sd_jwt_vc)
.map_err(|e| {
VpTokenValidationError::SerializationError(format!(
"Failed to serialize decoded SD-JWT VC (dc+sd-jwt): {e}"
))
})?
.as_object()
.cloned()
.ok_or(VpTokenValidationError::InvalidDecodedCredentialType)?;
Expand Down Expand Up @@ -299,7 +308,12 @@ impl<'a, SV: JwsVerifier + Clone, VMR: VerificationMaterialResolver, CSV: Creden
for sd_jwt in sd_jwts {
let decoded_vc_sd_jwt = self.validate_vcdm2_sd_jwt(&sd_jwt).await?;

let obj = serde_json::to_value(decoded_vc_sd_jwt)?
let obj = serde_json::to_value(decoded_vc_sd_jwt)
.map_err(|e| {
VpTokenValidationError::SerializationError(format!(
"Failed to serialize decoded VC SD-JWT (vc+sd-jwt): {e}"
))
})?
.as_object()
.cloned()
.ok_or(VpTokenValidationError::InvalidDecodedCredentialType)?;
Expand Down Expand Up @@ -373,47 +387,6 @@ impl<'a, SV: JwsVerifier + Clone, VMR: VerificationMaterialResolver, CSV: Creden
Ok(decoded_jwt_presentation)
}

/// Internal helper to validate a credential JWT.
async fn validate_credential_jwt(
&self,
credential_jwt: &Jwt,
) -> Result<DecodedJwtCredential<JsonObject>, VpTokenValidationError> {
let validation_item = Decoder::new()
.decode_compact_serialization(credential_jwt.as_str().as_bytes(), None)
.map_err(VpTokenValidationError::JwsDecodingError)?;

let kid_str = validation_item.kid().ok_or(VpTokenValidationError::MissingKid)?;
let kid: DIDUrl = kid_str
.parse()
.map_err(|e: identity_did::Error| VpTokenValidationError::InvalidKid(e.to_string()))?;

let resolver = &self.verification_material_resolver;

// TODO: verify whether issuer is trusted (through `trusted_authorities`).
let issuer = resolver
.resolve_did_document(kid.did())
.await
.map_err(|e| VpTokenValidationError::VerificationMaterialResolutionError(e.to_string()))?;

// `SkipUnsupported` allows for custom credential types, such as the StatusList2021Entry (https://www.w3.org/TR/2023/WD-vc-status-list-20230427/#statuslist2021entry)
let options = &JwtCredentialValidationOptions::new().status_check(StatusCheck::SkipUnsupported);
let fail_fast = FailFast::FirstError;

let jwt_data = self
.jwt_credential_validator
.validate(credential_jwt, &issuer, options, fail_fast)
.map_err(VpTokenValidationError::CredentialValidation)?;

if let Some(status_value) = jwt_data.custom_claims.as_ref().and_then(|v| v.get("status").cloned()) {
self.credential_status_verifier
.check_credential_status(status_value)
.await
.map_err(|_| VpTokenValidationError::CredentialStatusInvalid)?;
}

Ok(jwt_data)
}

/// Internal helper to validate a generic SD-JWT VC (signature, key binding, disclosures).
async fn validate_sd_jwt_vc(
&self,
Expand Down
Loading