Skip to content

Commit

Permalink
feat: validate SD-JWTs
Browse files Browse the repository at this point in the history
  • Loading branch information
nanderstabel committed Oct 22, 2024
1 parent 8785ec3 commit 39d9056
Show file tree
Hide file tree
Showing 11 changed files with 449 additions and 121 deletions.
349 changes: 279 additions & 70 deletions Cargo.lock

Large diffs are not rendered by default.

31 changes: 18 additions & 13 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,39 +19,44 @@ edition = "2021"
rust-version = "1.76.0"

[workspace.dependencies]
did_manager = { git = "https://[email protected]/impierce/did-manager.git", tag = "v1.0.0-beta.3" }
siopv2 = { git = "https://[email protected]/impierce/openid4vc.git", rev = "b4b7a56" }
oid4vci = { git = "https://[email protected]/impierce/openid4vc.git", rev = "b4b7a56" }
oid4vc-core = { git = "https://[email protected]/impierce/openid4vc.git", rev = "b4b7a56" }
oid4vc-manager = { git = "https://[email protected]/impierce/openid4vc.git", rev = "b4b7a56" }
oid4vp = { git = "https://[email protected]/impierce/openid4vc.git", rev = "b4b7a56" }
# did_manager = { git = "https://[email protected]/impierce/did-manager.git", tag = "v1.0.0-beta.3" }
did_manager = { git = "https://[email protected]/impierce/did-manager.git", rev = "c1cfda0" }
# siopv2 = { path = "../openid4vc/siopv2" }
# oid4vci = { path = "../openid4vc/oid4vci"}
# oid4vc-core = { path = "../openid4vc/oid4vc-core"}
# oid4vci = { path = "../openid4vc/oid4vci" }
# oid4vc-core = { path = "../openid4vc/oid4vc-core" }
# oid4vc-manager = { path = "../openid4vc/oid4vc-manager" }
# oid4vp = { path = "../openid4vc/oid4vp" }
siopv2 = { git = "https://[email protected]/impierce/openid4vc.git", rev = "bc4d6d2" }
oid4vci = { git = "https://[email protected]/impierce/openid4vc.git", rev = "bc4d6d2" }
oid4vc-core = { git = "https://[email protected]/impierce/openid4vc.git", rev = "bc4d6d2" }
oid4vc-manager = { git = "https://[email protected]/impierce/openid4vc.git", rev = "bc4d6d2" }
oid4vp = { git = "https://[email protected]/impierce/openid4vc.git", rev = "bc4d6d2" }

async-trait = "0.1"
axum = { version = "0.7", features = ["tracing"] }
base64 = "0.22"
cqrs-es = "0.4.2"
futures = "0.3"
identity_core = "1.3"
identity_credential = { version = "1.3", default-features = false, features = [
identity_core = { git = "https://github.com/impierce/identity.rs", branch = "fix/compile-fixes" }
identity_credential = { git = "https://github.com/impierce/identity.rs", branch = "fix/compile-fixes", default-features = false, features = [
"validator",
"credential",
"presentation",
"domain-linkage",
"sd-jwt-vc"
] }
identity_did = { version = "1.3" }
identity_iota = { version = "1.3" }
identity_verification = { version = "1.3", default-features = false }

identity_did = { git = "https://github.com/impierce/identity.rs", branch = "fix/compile-fixes" }
identity_document = { git = "https://github.com/impierce/identity.rs", branch = "fix/compile-fixes" }
identity_iota = { git = "https://github.com/impierce/identity.rs", branch = "fix/compile-fixes" }
identity_verification = { git = "https://github.com/impierce/identity.rs", branch = "fix/compile-fixes", default-features = false }
jsonwebtoken = "9.3"
lazy_static = "1.4"
mime = { version = "0.3" }
once_cell = { version = "1.19" }
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
rstest = "0.22"
sd-jwt-payload-rework = { package = "sd-jwt-payload", git = "https://github.com/iotaledger/sd-jwt-payload.git", rev = "0300fc5", default-features = false, features = ["sha"] }
serde = { version = "1.0", default-features = false, features = ["derive"] }
serde_json = { version = "1.0" }
serde_with = "3.7"
Expand Down
2 changes: 1 addition & 1 deletion agent_identity/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ did_manager.workspace = true
identity_credential.workspace = true
identity_core.workspace = true
identity_did.workspace = true
identity_document = { version = "1.3" }
identity_document.workspace = true
jsonwebtoken.workspace = true
oid4vc-core.workspace = true
serde.workspace = true
Expand Down
1 change: 1 addition & 0 deletions agent_shared/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ rust-version.workspace = true

[dependencies]
async-trait.workspace = true
base64.workspace = true
config = { version = "0.14" }
cqrs-es.workspace = true
dotenvy = { version = "0.15" }
Expand Down
1 change: 1 addition & 0 deletions agent_shared/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod error;
pub mod generic_query;
pub mod handlers;
pub mod url_utils;
pub mod verifier;

pub use ::config::ConfigError;
use identity_iota::verification::jws::JwsAlgorithm;
Expand Down
43 changes: 24 additions & 19 deletions agent_shared/src/verifier.rs
Original file line number Diff line number Diff line change
@@ -1,45 +1,50 @@
use std::str::FromStr as _;

use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use identity_iota::{
core::{FromJson as _, ToJson as _},
verification::{
jwk::Jwk,
jws::{JwsVerifier, SignatureVerificationError, VerificationInput},
},
use identity_iota::core::{FromJson as _, ToJson as _};
use identity_iota::verification;
use identity_iota::verification::jws::{
JwsVerifier, SignatureVerificationError, SignatureVerificationErrorKind, VerificationInput,
};
use jsonwebtoken::{crypto::verify, Algorithm, DecodingKey, Validation};
use jsonwebtoken::crypto::verify;
use jsonwebtoken::{Algorithm, DecodingKey, Validation};

/// This `Verifier` uses `jsonwebtoken` under the hood to verify verification input.
pub struct Verifier;
impl JwsVerifier for Verifier {
fn verify(&self, input: VerificationInput, public_key: &Jwk) -> Result<(), SignatureVerificationError> {
let algorithm = Algorithm::from_str(&input.alg.to_string()).unwrap();
fn verify(
&self,
input: VerificationInput,
public_key: &verification::jwk::Jwk,
) -> Result<(), SignatureVerificationError> {
use SignatureVerificationErrorKind::*;

println!("public_key: {:?}", public_key);
let algorithm =
Algorithm::from_str(&input.alg.to_string()).map_err(|_| SignatureVerificationError::new(UnsupportedAlg))?;

// Convert the `Jwk` first into a `jsonwebtoken::jwk::Jwk` and then into a `DecodingKey`.
// Convert the `IotaIdentityJwk` first into a `jsonwebtoken::Jwk` and then into a `DecodingKey`.
let decoding_key = public_key
.to_json()
.ok()
.and_then(|public_key| jsonwebtoken::jwk::Jwk::from_json(&public_key).ok())
.and_then(|jwk| DecodingKey::from_jwk(&jwk).ok())
.unwrap();
.ok_or(SignatureVerificationError::new(KeyDecodingFailure))?;

let mut validation = Validation::new(algorithm);
validation.validate_aud = false;
validation.required_spec_claims.clear();

println!("validation: {:?}", validation);

verify(
match verify(
&URL_SAFE_NO_PAD.encode(input.decoded_signature),
&input.signing_input,
&decoding_key,
algorithm,
)
.unwrap();

Ok(())
) {
Ok(_) => Ok(()),
Err(_) => Err(SignatureVerificationError::new(
// TODO: more fine-grained error handling?
InvalidSignature,
)),
}
}
}
6 changes: 6 additions & 0 deletions agent_verification/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ agent_shared = { path = "../agent_shared" }
anyhow = "1.0"
async-trait.workspace = true
cqrs-es.workspace = true
did_manager.workspace = true
futures.workspace = true
identity_credential.workspace = true
jsonwebtoken.workspace = true
oid4vc-core.workspace = true
oid4vc-manager.workspace = true
Expand All @@ -24,6 +26,10 @@ tracing.workspace = true
url.workspace = true
tokio.workspace = true

identity_iota.workspace = true
sd-jwt-payload-rework.workspace = true
base64.workspace = true

[dev-dependencies]
agent_shared = { path = "../agent_shared", features = ["test_utils"] }
agent_verification = { path = ".", features = ["test_utils"] }
Expand Down
115 changes: 100 additions & 15 deletions agent_verification/src/authorization_request/aggregate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,25 @@ use crate::{
},
services::VerificationServices,
};
use agent_shared::config::{config, get_preferred_signing_algorithm};
use agent_shared::{
config::{config, get_preferred_signing_algorithm},
verifier::Verifier,
};
use async_trait::async_trait;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use cqrs_es::Aggregate;
use did_manager::Resolver;
use identity_credential::sd_jwt_vc::SdJwtVc;
use identity_iota::{
core::ToJson as _,
credential::KeyBindingJWTValidationOptions,
did::DID as _,
document::DIDUrlQuery,
verification::jwk::{Jwk, JwkParams},
};
use oid4vc_core::{authorization_request::ByReference, scope::Scope};
use oid4vp::{authorization_request::ClientIdScheme, Oid4vpParams};
use oid4vp::{authorization_request::ClientIdScheme, oid4vp_params::OneOrManyVpToken, Oid4vpParams};
use sd_jwt_payload_rework::{RequiredKeyBinding, Sha256Hasher};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tracing::info;
Expand All @@ -21,10 +35,12 @@ pub struct AuthorizationRequest {
pub form_url_encoded_authorization_request: Option<String>,
pub signed_authorization_request_object: Option<String>,
pub id_token: Option<String>,
pub vp_token: Option<String>,
pub vp_tokens: Option<Vec<String>>,
pub state: Option<String>,
}

// fn validate_sd_jwt_vc(sd_jwt_vc: &SdJwtVc) -> String {}

#[async_trait]
impl Aggregate for AuthorizationRequest {
type Command = AuthorizationRequestCommand;
Expand Down Expand Up @@ -155,18 +171,84 @@ impl Aggregate for AuthorizationRequest {
}])
}
GenericAuthorizationResponse::OID4VP(oid4vp_authorization_response) => {
let _ = relying_party
.validate_response(&oid4vp_authorization_response)
.await
.map_err(InvalidOID4VPAuthorizationResponse)?;

let vp_token = match oid4vp_authorization_response.extension.oid4vp_parameters {
Oid4vpParams::Params { vp_token, .. } => vp_token,
let mut vp_tokens = match &oid4vp_authorization_response.extension.oid4vp_parameters {
Oid4vpParams::Params {
vp_token: OneOrManyVpToken::One(vp_token),
..
} => vec![vp_token.clone()],
Oid4vpParams::Params {
vp_token: OneOrManyVpToken::Many(vp_token),
..
} => vp_token.clone(),
Oid4vpParams::Jwt { .. } => return Err(UnsupportedJwtParameterError),
};

for vp_token in &mut vp_tokens {
if let Ok(sd_jwt_vc) = vp_token.parse::<SdJwtVc>() {
info!("VC SD-JWT: {}", sd_jwt_vc);

if let Some(cnf) = &sd_jwt_vc.claims().cnf {
let jwk = match cnf {
RequiredKeyBinding::Jwk(jwk) => Jwk::from_params(
serde_json::from_value::<JwkParams>(serde_json::json!(jwk))
.map_err(|e| InvalidCnfParameterError(e.to_string()))?,
),
RequiredKeyBinding::Kid(kid) => {
info!("Cnf `kid` value: {kid}");

let did_url = identity_iota::did::DIDUrl::parse(kid)
.map_err(|e| InvalidDidUrlError(format!("Invalid DID URL: {}", e)))?;

let resolver = Resolver::new().await;

let document = resolver
.resolve(did_url.did().as_str())
.await
.map_err(|e| UnsupportedDidMethodError(e.to_string()))?;

let verification_method = document
.resolve_method(
DIDUrlQuery::from(&did_url),
Some(identity_iota::verification::MethodScope::VerificationMethod),
)
.ok_or(MissingVerificationMethodError)?;

verification_method
.data()
.public_key_jwk()
.ok_or(MissingVerificationMethodKeyError)?
.clone()
}
_ => return Err(UnsupportedCnfParameterError),
};

sd_jwt_vc
.validate_key_binding(
&Verifier,
&jwk,
&Sha256Hasher::new(),
&KeyBindingJWTValidationOptions::default(),
)
.map_err(|_| InvalidKeyBindingError)?;
}
let disclosed_object = sd_jwt_vc.into_disclosed_object(&Sha256Hasher::new()).unwrap();

info!("Disclosed object: {:?}", disclosed_object);

*vp_token = URL_SAFE_NO_PAD.encode(
disclosed_object
.to_json_vec()
.map_err(|e| InvalidDisclosedObjectError(e.to_string()))?,
);
} else {
let _ = relying_party
.validate_response(&oid4vp_authorization_response)
.await
.map_err(InvalidOID4VPAuthorizationResponse)?;
}
}
Ok(vec![OID4VPAuthorizationResponseVerified {
vp_token,
vp_tokens,
state: oid4vp_authorization_response.state,
}])
}
Expand Down Expand Up @@ -200,8 +282,8 @@ impl Aggregate for AuthorizationRequest {
self.id_token.replace(id_token);
self.state = state;
}
OID4VPAuthorizationResponseVerified { vp_token, state } => {
self.vp_token.replace(vp_token);
OID4VPAuthorizationResponseVerified { vp_tokens, state } => {
self.vp_tokens.replace(vp_tokens);
self.state = state;
}
}
Expand All @@ -228,6 +310,7 @@ pub mod tests {
use oid4vc_manager::ProviderManager;
use oid4vci::VerifiableCredentialJwt;
use oid4vp::oid4vp::AuthorizationResponseInput;
use oid4vp::oid4vp::PresentationInputType;
use oid4vp::PresentationDefinition;
use rstest::rstest;
use serde_json::json;
Expand Down Expand Up @@ -355,7 +438,7 @@ pub mod tests {
state: Some("state".to_string()),
},
"vp_token" => AuthorizationRequestEvent::OID4VPAuthorizationResponseVerified {
vp_token: token,
vp_tokens: vec![token],
state: Some("state".to_string()),
},
_ => unreachable!("Invalid response type."),
Expand Down Expand Up @@ -442,7 +525,9 @@ pub mod tests {
.generate_response(
oid4vp_authorization_request,
AuthorizationResponseInput {
verifiable_presentation,
verifiable_presentation_input: vec![PresentationInputType::Unsigned(
verifiable_presentation,
)],
presentation_submission,
},
)
Expand Down
16 changes: 16 additions & 0 deletions agent_verification/src/authorization_request/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,20 @@ pub enum AuthorizationRequestError {
InvalidOID4VPAuthorizationResponse(#[source] anyhow::Error),
#[error("`jwt` parameter is not supported yet")]
UnsupportedJwtParameterError,
#[error("`cnf` parameter must be a JWK or a `kid` string")]
UnsupportedCnfParameterError,
#[error("Invalid `cnf` parameter: {0}")]
InvalidCnfParameterError(String),
#[error("Invalid key binding")]
InvalidKeyBindingError,
#[error("Invalid DID URL: {0}")]
InvalidDidUrlError(String),
#[error("Unsupported DID method: {0}")]
UnsupportedDidMethodError(String),
#[error("Unable to find verification method")]
MissingVerificationMethodError,
#[error("No verification method key found")]
MissingVerificationMethodKeyError,
#[error("Invalid disclosed object: {0}")]
InvalidDisclosedObjectError(String),
}
2 changes: 1 addition & 1 deletion agent_verification/src/authorization_request/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub enum AuthorizationRequestEvent {
state: Option<String>,
},
OID4VPAuthorizationResponseVerified {
vp_token: String,
vp_tokens: Vec<String>,
state: Option<String>,
},
}
Expand Down
4 changes: 2 additions & 2 deletions agent_verification/src/authorization_request/views/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ impl View<AuthorizationRequest> for AuthorizationRequest {
self.id_token.replace(id_token.clone());
self.state.clone_from(state);
}
OID4VPAuthorizationResponseVerified { vp_token, state } => {
self.vp_token.replace(vp_token.clone());
OID4VPAuthorizationResponseVerified { vp_tokens, state } => {
self.vp_tokens.replace(vp_tokens.clone());
self.state.clone_from(state);
}
}
Expand Down

0 comments on commit 39d9056

Please sign in to comment.