Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 3 additions & 16 deletions agent_holder/src/credential/aggregate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use crate::credential::error::CredentialError::{self};
use crate::credential::event::CredentialEvent;
use crate::services::HolderServices;
use agent_shared::credential_status_checker::CredentialStatusChecker;
use agent_shared::get_unverified_jwt_claims;
use async_trait::async_trait;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use cqrs_es::Aggregate;
use identity_credential::credential::Jwt;
use oid4vc_core::credential_status_verifier::CredentialStatusVerifier;
Expand Down Expand Up @@ -52,7 +52,8 @@ impl Aggregate for Credential {
received_offer_id,
credential,
} => {
let raw = get_unverified_jwt_claims(&serde_json::json!(credential))?;
let raw = get_unverified_jwt_claims(&serde_json::json!(credential))
.ok_or(CredentialError::CredentialDecodingError)?;

if let Some(status_claim) = raw.get("status") {
let credential_status_checker = CredentialStatusChecker {
Expand Down Expand Up @@ -98,20 +99,6 @@ impl Aggregate for Credential {
}
}

// TODO: actually validate the JWT!
/// Get the claims from a JWT without performing validation.
pub fn get_unverified_jwt_claims(jwt: &serde_json::Value) -> Result<serde_json::Value, CredentialError> {
jwt.as_str()
.and_then(|string| string.splitn(3, '.').collect::<Vec<&str>>().get(1).cloned())
.and_then(|payload| {
URL_SAFE_NO_PAD
.decode(payload)
.ok()
.and_then(|payload_bytes| serde_json::from_slice::<serde_json::Value>(&payload_bytes).ok())
})
.ok_or(CredentialError::CredentialDecodingError)
}

#[cfg(test)]
pub mod credential_tests {
use super::test_utils::*;
Expand Down
29 changes: 8 additions & 21 deletions agent_identity/src/services.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use crate::connection::error::ConnectionError;
use agent_secret_manager::subject::Subject;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use agent_shared::get_unverified_jwt_claims;
use chrono::{DateTime, Utc};
use identity_credential::domain_linkage::{DomainLinkageConfiguration, JwtDomainLinkageValidator};
use identity_did::DIDUrl;
use identity_did::DID;
use identity_iota::{core::FromJson, credential::JwtCredentialValidationOptions};
use identity_iota::{
core::{FromJson, ToJson},
credential::JwtCredentialValidationOptions,
};
use oid4vc_core::verifier::SignatureVerifier;
use oid4vci::credential_issuer::credential_issuer_metadata::CredentialIssuerMetadata;
use reqwest::Client;
Expand Down Expand Up @@ -70,7 +73,8 @@ impl IdentityServices {
.linked_dids()
.iter()
.filter_map(|jwt| {
let claims = get_unverified_jwt_claims(jwt.as_str()).ok()?;
let jwt_value = jwt.to_json_value().ok()?;
let claims = get_unverified_jwt_claims(&jwt_value)?;
let did_str = claims
.get("sub")
.or_else(|| claims.get("iss"))
Expand Down Expand Up @@ -149,23 +153,6 @@ impl IdentityServices {
}
}

/// Get the claims from a jwt string without performing validation.
fn get_unverified_jwt_claims(jwt: &str) -> Result<serde_json::Value, ConnectionError> {
jwt.splitn(3, '.')
.collect::<Vec<&str>>()
.get(1)
.cloned()
.and_then(|payload| {
URL_SAFE_NO_PAD
.decode(payload)
.ok()
.and_then(|payload_bytes| serde_json::from_slice::<serde_json::Value>(&payload_bytes).ok())
})
.ok_or(ConnectionError::DIDResolutionFailed(
"Failed to decode JWT claims".to_string(),
))
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -180,7 +167,7 @@ mod tests {
#[test]
fn test_decode_linked_did_jwt() {
let jwt = serde_json::json!(LINKED_DID_JWT);
let claims = get_unverified_jwt_claims(&jwt.to_string()).unwrap();
let claims = get_unverified_jwt_claims(&jwt).unwrap();
assert_eq!(
claims["sub"],
"did:key:z6MkoTHsgNNrby8JzCNQ1iRLyW5QQ6R8Xuu6AA8igGrMVPUM"
Expand Down
14 changes: 4 additions & 10 deletions agent_issuance/src/application/access_token_validation_service.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use crate::state::IssuanceState;
use agent_shared::config::config;
use identity_core::convert::{FromJson as _, ToJson as _};
use jsonwebtoken::{decode, jwk::Jwk as JsonWebTokenJwk, DecodingKey, Validation};
use agent_shared::{config::config, convert_iota_jwk_to_decoding_key};
use jsonwebtoken::{decode, Validation};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use thiserror::Error;
Expand Down Expand Up @@ -53,13 +52,8 @@ impl AccessTokenValidationService {
.await
.map_err(|_err| AccessTokenValidationError::KidResolutionError)?;

// Convert the `IotaIdentityJwk` first into a `JsonWebTokenJwk` and then into a `DecodingKey`.
let decoding_key = public_key_jwk
.to_json()
.ok()
.and_then(|public_key| JsonWebTokenJwk::from_json(&public_key).ok())
.and_then(|jwk| DecodingKey::from_jwk(&jwk).ok())
.ok_or(AccessTokenValidationError::KidResolutionError)?;
let decoding_key =
convert_iota_jwk_to_decoding_key(&public_key_jwk).ok_or(AccessTokenValidationError::KidResolutionError)?;

let public_url = config().public_url.to_string();

Expand Down
3 changes: 2 additions & 1 deletion agent_shared/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ cqrs-es.workspace = true
dotenvy = { version = "0.15" }
http-serde = "2.1"
# TODO: replace all identity_* with identity_iota?
identity_iota.workspace = true
identity_core.workspace = true
identity_iota.workspace = true
identity_jose.workspace = true
jsonwebtoken.workspace = true
oauth_tsl.workspace = true
oid4vc-core.workspace = true
Expand Down
17 changes: 6 additions & 11 deletions agent_shared/src/credential_status_checker.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use async_trait::async_trait;
use identity_core::convert::{FromJson as _, ToJson as _};
use jsonwebtoken::{decode_header, jwk::Jwk as JsonWebTokenJwk, DecodingKey};
use jsonwebtoken::decode_header;
use oauth_tsl::{
relying_party::{decompress_gzip, decrypt_status_list_token, StatusListTokenResponseType},
status_list::{StatusList, StatusType},
Expand All @@ -15,6 +14,8 @@ use thiserror::Error;
use tracing::{info, warn};
use url::Url;

use crate::convert_iota_jwk_to_decoding_key;

#[derive(Error, Debug)]
pub enum CredentialStatusCheckerError {
#[error("Failed to get credential status: {0}")]
Expand Down Expand Up @@ -110,15 +111,9 @@ impl CredentialStatusChecker {
.await
.map_err(|e| CredentialStatusCheckerError::FailedToGetCredentialStatus(e.to_string()))?;

// Convert the `IotaIdentityJwk` first into a `JsonWebTokenJwk` and then into a `DecodingKey`.
let decoding_key = public_key_jwk
.to_json()
.ok()
.and_then(|public_key| JsonWebTokenJwk::from_json(&public_key).ok())
.and_then(|jwk| DecodingKey::from_jwk(&jwk).ok())
.ok_or(CredentialStatusCheckerError::FailedToGetCredentialStatus(
"Failed to create decoding key".to_string(),
))?;
let decoding_key = convert_iota_jwk_to_decoding_key(&public_key_jwk).ok_or(
CredentialStatusCheckerError::FailedToGetCredentialStatus("Failed to create decoding key".to_string()),
)?;

// TODO: move this logic to the OAuth TSL library.
let decoded_jwt = decrypt_status_list_token(&status_list_jwt, decoding_key)
Expand Down
24 changes: 24 additions & 0 deletions agent_shared/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ pub mod serde_json_value_ext;
pub mod url_utils;

pub use ::config::ConfigError;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use identity_core::convert::{FromJson as _, ToJson as _};
use identity_iota::verification::jws::JwsAlgorithm;
use jsonwebtoken::{jwk::Jwk as JsonWebTokenJwk, DecodingKey};
use rand::Rng;
pub use url_utils::UrlAppendHelpers;

Expand Down Expand Up @@ -46,3 +49,24 @@ pub fn from_jsonwebtoken_algorithm_to_jwsalgorithm(algorithm: &jsonwebtoken::Alg
jsonwebtoken::Algorithm::EdDSA => JwsAlgorithm::EdDSA,
}
}

/// Get the claims from a JWT without performing validation.
pub fn get_unverified_jwt_claims(jwt: &serde_json::Value) -> Option<serde_json::Value> {
jwt.as_str()
.and_then(|string| string.splitn(3, '.').collect::<Vec<&str>>().get(1).cloned())
.and_then(|payload| {
URL_SAFE_NO_PAD
.decode(payload)
.ok()
.and_then(|payload_bytes| serde_json::from_slice::<serde_json::Value>(&payload_bytes).ok())
})
}
Comment on lines +53 to +63

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this relates to your comment here right @Oran-Dan ?


/// Convert the `IotaIdentityJwk` first into a `JsonWebTokenJwk` and then into a `DecodingKey`.
pub fn convert_iota_jwk_to_decoding_key(public_key: &identity_jose::jwk::Jwk) -> Option<DecodingKey> {
public_key
.to_json()
.ok()
.and_then(|public_key| JsonWebTokenJwk::from_json(&public_key).ok())
.and_then(|jwk| DecodingKey::from_jwk(&jwk).ok())
}
Loading