From 8d00263860e42756578617075b172fd380ab2e8e Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Fri, 20 Sep 2024 18:06:16 +0200 Subject: [PATCH] fetch issuer's JWK (to ease verification) --- Cargo.toml | 2 +- identity_credential/Cargo.toml | 74 ++++++------------- identity_credential/src/sd_jwt_vc/error.rs | 3 + .../src/sd_jwt_vc/metadata/issuer.rs | 7 +- .../src/sd_jwt_vc/metadata/vc_type.rs | 8 ++ identity_credential/src/sd_jwt_vc/mod.rs | 2 + identity_credential/src/sd_jwt_vc/token.rs | 70 ++++++++++++++++++ .../src/sd_jwt_vc/validation.rs | 11 +++ identity_resolver/Cargo.toml | 4 +- 9 files changed, 123 insertions(+), 58 deletions(-) create mode 100644 identity_credential/src/sd_jwt_vc/validation.rs diff --git a/Cargo.toml b/Cargo.toml index d005618aa..6f35f6713 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ members = [ "identity_ecdsa_verifier", "identity_eddsa_verifier", "examples", - "compound_resolver", + "compound_resolver", ] exclude = ["bindings/wasm", "bindings/grpc"] diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index e69fcf001..b847e5b56 100644 --- a/identity_credential/Cargo.toml +++ b/identity_credential/Cargo.toml @@ -12,39 +12,24 @@ rust-version.workspace = true description = "An implementation of the Verifiable Credentials standard." [dependencies] +anyhow = { version = "1" } async-trait = { version = "0.1.64", default-features = false } bls12_381_plus = { workspace = true, optional = true } -flate2 = { version = "1.0.28", default-features = false, features = [ - "rust_backend", -], optional = true } +flate2 = { version = "1.0.28", default-features = false, features = ["rust_backend"], optional = true } futures = { version = "0.3", default-features = false, optional = true, features = ["alloc"] } identity_core = { version = "=1.3.1", path = "../identity_core", default-features = false } identity_did = { version = "=1.3.1", path = "../identity_did", default-features = false } identity_document = { version = "=1.3.1", path = "../identity_document", default-features = false } identity_verification = { version = "=1.3.1", path = "../identity_verification", default-features = false } -indexmap = { version = "2.0", default-features = false, features = [ - "std", - "serde", -] } -itertools = { version = "0.11", default-features = false, features = [ - "use_std", -], optional = true } +indexmap = { version = "2.0", default-features = false, features = ["std", "serde"] } +itertools = { version = "0.11", default-features = false, features = ["use_std"], optional = true } json-proof-token = { workspace = true, optional = true } +jsonschema = { version = "0.19", optional = true, default-features = false } once_cell = { version = "1.18", default-features = false, features = ["std"] } -reqwest = { version = "0.11", default-features = false, features = [ - "default-tls", - "json", - "stream", -], optional = true } -roaring = { version = "0.10.2", default-features = false, features = [ - "serde", -], optional = true } -sd-jwt-payload = { version = "0.2.1", default-features = false, features = [ - "sha", -], optional = true } -sd-jwt-payload-rework = { package = "sd-jwt-payload", git = "https://github.com/iotaledger/sd-jwt-payload.git", branch = "feat/sd-jwt-v11", default-features = false, features = [ - "sha", -], optional = true } +reqwest = { version = "0.11", default-features = false, features = ["default-tls", "json", "stream"], optional = true } +roaring = { version = "0.10.2", default-features = false, features = ["serde"], optional = true } +sd-jwt-payload = { version = "0.2.1", default-features = false, features = ["sha"], optional = true } +sd-jwt-payload-rework = { package = "sd-jwt-payload", git = "https://github.com/iotaledger/sd-jwt-payload.git", branch = "feat/sd-jwt-v11", default-features = false, features = ["sha"], optional = true } serde.workspace = true serde-aux = { version = "4.3.1", default-features = false } serde_json.workspace = true @@ -53,24 +38,13 @@ strum.workspace = true thiserror.workspace = true url = { version = "2.5", default-features = false } zkryptium = { workspace = true, optional = true } -anyhow = { version = "1" } -jsonschema = { version = "0.19", optional = true, default-features = false } [dev-dependencies] anyhow = "1.0.62" -identity_eddsa_verifier = { path = "../identity_eddsa_verifier", default-features = false, features = [ - "ed25519", -] } -iota-crypto = { version = "0.23.2", default-features = false, features = [ - "ed25519", - "std", - "random", -] } +identity_eddsa_verifier = { path = "../identity_eddsa_verifier", default-features = false, features = ["ed25519"] } +iota-crypto = { version = "0.23.2", default-features = false, features = ["ed25519", "std", "random"] } proptest = { version = "1.4.0", default-features = false, features = ["std"] } -tokio = { version = "1.35.0", default-features = false, features = [ - "rt-multi-thread", - "macros", -] } +tokio = { version = "1.35.0", default-features = false, features = ["rt-multi-thread", "macros"] } [package.metadata.docs.rs] # To build locally: @@ -80,13 +54,13 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = [ - "revocation-bitmap", - "validator", - "credential", - "presentation", - "domain-linkage-fetch", - "sd-jwt", - "sd-jwt-vc", + "revocation-bitmap", + "validator", + "credential", + "presentation", + "domain-linkage-fetch", + "sd-jwt", + "sd-jwt-vc", ] credential = [] presentation = ["credential"] @@ -98,11 +72,11 @@ domain-linkage-fetch = ["domain-linkage", "dep:reqwest", "dep:futures"] sd-jwt = ["credential", "validator", "dep:sd-jwt-payload"] sd-jwt-vc = ["sd-jwt", "dep:sd-jwt-payload-rework", "dep:jsonschema"] jpt-bbs-plus = [ - "credential", - "validator", - "dep:zkryptium", - "dep:bls12_381_plus", - "dep:json-proof-token", + "credential", + "validator", + "dep:zkryptium", + "dep:bls12_381_plus", + "dep:json-proof-token", ] [lints] diff --git a/identity_credential/src/sd_jwt_vc/error.rs b/identity_credential/src/sd_jwt_vc/error.rs index 21246bba9..13af8911a 100644 --- a/identity_credential/src/sd_jwt_vc/error.rs +++ b/identity_credential/src/sd_jwt_vc/error.rs @@ -48,6 +48,9 @@ pub enum Error { /// Credential validation failed. #[error("credential validation failed: {0}")] Validation(#[source] anyhow::Error), + /// SD-JWT VC signature verification failed. + #[error("verification failed: {0}")] + Verification(#[source] anyhow::Error), } /// Either a value of type `T` or an [`Error`]. diff --git a/identity_credential/src/sd_jwt_vc/metadata/issuer.rs b/identity_credential/src/sd_jwt_vc/metadata/issuer.rs index 676200d90..7af0effd6 100644 --- a/identity_credential/src/sd_jwt_vc/metadata/issuer.rs +++ b/identity_credential/src/sd_jwt_vc/metadata/issuer.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use identity_core::common::Url; -use identity_verification::jwk::Jwk; +use identity_verification::jwk::JwkSet; use serde::Deserialize; use serde::Serialize; @@ -51,10 +51,7 @@ pub enum Jwks { Uri(Url), /// An embedded JWK set. #[serde(rename = "jwks")] - Object { - /// List of JWKs. - keys: Vec, - }, + Object(JwkSet), } #[cfg(test)] diff --git a/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs b/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs index dd4389aca..280f001d4 100644 --- a/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs +++ b/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs @@ -57,6 +57,14 @@ impl TypeMetadata { pub fn extends_integrity(&self) -> Option<&str> { self.extends_integrity.as_ref().map(|meta| meta.as_ref()) } + /// Returns the [`ClaimMetadata`]s associated with this credential type. + pub fn claim_metadata(&self) -> &[ClaimMetadata] { + &self.claims + } + /// Returns the [`DisplayMetadata`]s associated with this credential type. + pub fn display_metadata(&self) -> &[DisplayMetadata] { + &self.display + } /// Uses this [`TypeMetadata`] to validate JSON object `credential`. This method fails /// if the schema is referenced instead of embedded. /// Use [`TypeMetadata::validate_credential_with_resolver`] for such cases. diff --git a/identity_credential/src/sd_jwt_vc/mod.rs b/identity_credential/src/sd_jwt_vc/mod.rs index f576b4ecd..0b6aa746d 100644 --- a/identity_credential/src/sd_jwt_vc/mod.rs +++ b/identity_credential/src/sd_jwt_vc/mod.rs @@ -10,6 +10,7 @@ mod presentation; mod resolver; mod status; mod token; +mod validation; pub use claims::*; pub use error::Error; @@ -18,3 +19,4 @@ pub use presentation::*; pub use resolver::*; pub use status::*; pub use token::*; +pub use validation::*; diff --git a/identity_credential/src/sd_jwt_vc/token.rs b/identity_credential/src/sd_jwt_vc/token.rs index dd6fc1ab1..5ee310441 100644 --- a/identity_credential/src/sd_jwt_vc/token.rs +++ b/identity_credential/src/sd_jwt_vc/token.rs @@ -7,6 +7,7 @@ use std::str::FromStr; use super::claims::SdJwtVcClaims; use super::metadata::IssuerMetadata; +use super::metadata::Jwks; use super::metadata::TypeMetadata; #[allow(unused_imports)] use super::metadata::WELL_KNOWN_VCT; @@ -16,8 +17,11 @@ use super::Error; use super::Resolver; use super::Result; use super::SdJwtVcPresentationBuilder; +use anyhow::anyhow; use identity_core::common::StringOrUrl; use identity_core::common::Url; +use identity_verification::jwk::Jwk; +use identity_verification::jwk::JwkSet; use sd_jwt_payload_rework::Hasher; use sd_jwt_payload_rework::JsonObject; use sd_jwt_payload_rework::SdJwt; @@ -121,7 +125,73 @@ impl SdJwtVc { Ok((metadata, raw)) } + + /// Resolves the issuer's public key in JWK format. + pub async fn issuer_jwk(&self, resolver: &R) -> Result + where + R: Resolver>, + { + let kid = self + .header() + .get("kid") + .and_then(|value| value.as_str()) + .ok_or_else(|| Error::Verification(anyhow!("missing header claim `kid`")))?; + + // Try to find the key among issuer metadata jwk set. + if let jwk @ Ok(_) = self.issuer_jwk_from_iss_metadata(resolver, kid).await { + jwk + } else { + // Issuer has no metadata that can lead to its JWK. Let's see if it can be resolved directly. + let jwk_uri = kid.parse::().map_err(|e| Error::Verification(e.into()))?; + resolver + .resolve(&jwk_uri) + .await + .map_err(|e| Error::Resolution { + input: jwk_uri.to_string(), + source: e, + }) + .and_then(|bytes| { + serde_json::from_slice(&bytes).map_err(|e| Error::Verification(anyhow!("invalid JWK: {}", e))) + }) + } + } + + async fn issuer_jwk_from_iss_metadata(&self, resolver: &R, kid: &str) -> Result + where + R: Resolver>, + { + let metadata = self + .issuer_metadata(resolver) + .await? + .ok_or_else(|| Error::Verification(anyhow!("missing issuer metadata")))?; + metadata.validate(self)?; + + let jwks = match metadata.jwks { + Jwks::Object(jwks) => jwks, + Jwks::Uri(jwks_uri) => resolver + .resolve(&jwks_uri) + .await + .map_err(|e| Error::Resolution { + input: jwks_uri.into_string(), + source: e, + }) + .and_then(|bytes| serde_json::from_slice::(&bytes).map_err(|e| Error::Verification(e.into())))?, + }; + jwks + .iter() + .find(|jwk| jwk.kid() == Some(kid)) + .cloned() + .ok_or_else(|| Error::Verification(anyhow!("missing key \"{kid}\" in issuer JWK set"))) + } } +// Verifies SD-JWT signature. +// pub async fn verify(&self, resolver: &R, jws_verifier: &V) -> Result<(), SignatureVerificationError> +// where +// R: Resolver, +// V: JwsVerifier, +// { +// // Fetch the issuer's public key. +// } /// Converts `vct` claim's URI value into the appropriate well-known URL. /// ## Warnings diff --git a/identity_credential/src/sd_jwt_vc/validation.rs b/identity_credential/src/sd_jwt_vc/validation.rs new file mode 100644 index 000000000..adce6ed4c --- /dev/null +++ b/identity_credential/src/sd_jwt_vc/validation.rs @@ -0,0 +1,11 @@ +use crate::validator::JwtCredentialValidationOptions; + +/// Options to decide which operations should be performed during SD-JWT VC validation. +#[derive(Debug, Clone)] +pub struct ValidationOptions { + /// Credential validation options. + pub credential_validation_options: JwtCredentialValidationOptions, + /// The credential will be checked using the credential type + /// specified through the `vct` claim. + pub vct: bool, +} diff --git a/identity_resolver/Cargo.toml b/identity_resolver/Cargo.toml index c5025fb52..5bfdf7ccd 100644 --- a/identity_resolver/Cargo.toml +++ b/identity_resolver/Cargo.toml @@ -10,16 +10,16 @@ rust-version.workspace = true [dependencies] anyhow = "1.0.86" -thiserror.workspace = true -iota-sdk = { version = "1.1.5" } # This is currently necessary for the ResolutionHandler trait. This can be made an optional dependency if alternative ways of attaching handlers are introduced. async-trait = { version = "0.1", default-features = false } futures = { version = "0.3" } identity_core = { version = "=1.3.1", path = "../identity_core", default-features = false } identity_did = { version = "=1.3.1", path = "../identity_did", default-features = false } identity_document = { version = "=1.3.1", path = "../identity_document", default-features = false } +iota-sdk = { version = "1.1.5" } serde = { version = "1.0", default-features = false, features = ["std", "derive"] } strum.workspace = true +thiserror.workspace = true [dependencies.identity_iota_core] version = "=1.3.1"