diff --git a/Cargo.lock b/Cargo.lock index f791a121..1f42cbae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -193,8 +193,11 @@ dependencies = [ "futures", "identity_iota", "jsonwebtoken", + "lazy_static", "log", "oid4vc-core", + "p256 0.13.2", + "ring", "serde", "serde_json", "tokio", diff --git a/agent_issuance/src/credential/aggregate.rs b/agent_issuance/src/credential/aggregate.rs index c8be9a85..9a9a5533 100644 --- a/agent_issuance/src/credential/aggregate.rs +++ b/agent_issuance/src/credential/aggregate.rs @@ -1,4 +1,4 @@ -use agent_shared::config::{config, get_preferred_did_method}; +use agent_shared::config::{config, get_preferred_did_method, get_preferred_signing_algorithm}; use async_trait::async_trait; use cqrs_es::Aggregate; use derivative::Derivative; @@ -6,7 +6,7 @@ use identity_core::convert::FromJson; use identity_credential::credential::{ Credential as W3CVerifiableCredential, CredentialBuilder as W3CVerifiableCredentialBuilder, Issuer, }; -use jsonwebtoken::{Algorithm, Header}; +use jsonwebtoken::Header; use oid4vc_core::jwt; use oid4vci::credential_format_profiles::w3c_verifiable_credentials::jwt_vc_json::{ CredentialDefinition, JwtVcJson, JwtVcJsonParameters, @@ -179,7 +179,7 @@ impl Aggregate for Credential { let issuer_did = services .issuer - .identifier(&default_did_method.to_string(), Algorithm::EdDSA) + .identifier(&default_did_method.to_string(), get_preferred_signing_algorithm()) .await .unwrap(); let signed_credential = { @@ -211,7 +211,7 @@ impl Aggregate for Credential { json!(jwt::encode( services.issuer.clone(), - Header::new(Algorithm::EdDSA), + Header::new(get_preferred_signing_algorithm()), VerifiableCredentialJwt::builder() .sub(subject_id) .iss(issuer_did) @@ -261,6 +261,7 @@ pub mod credential_tests { use super::*; + use jsonwebtoken::Algorithm; use lazy_static::lazy_static; use oid4vci::proof::KeyProofMetadata; use oid4vci::ProofType; diff --git a/agent_secret_manager/Cargo.toml b/agent_secret_manager/Cargo.toml index 632f9522..e257bc3e 100644 --- a/agent_secret_manager/Cargo.toml +++ b/agent_secret_manager/Cargo.toml @@ -16,6 +16,7 @@ identity_iota.workspace = true jsonwebtoken = "9.3" log = "0.4" oid4vc-core.workspace = true +p256 = { version = "0.13", features = ["jwk"] } serde.workspace = true serde_json = "1.0" tokio.workspace = true @@ -25,3 +26,5 @@ url.workspace = true agent_shared = { path = "../agent_shared", features = ["test_utils"] } futures.workspace = true +lazy_static.workspace = true +ring = "0.17.8" diff --git a/agent_secret_manager/src/subject.rs b/agent_secret_manager/src/subject.rs index 755e0087..649fbd92 100644 --- a/agent_secret_manager/src/subject.rs +++ b/agent_secret_manager/src/subject.rs @@ -29,21 +29,31 @@ impl Verify for Subject { .unwrap(); // Try decode from `MethodData` directly, else use public JWK params. - verification_method - .data() - .try_decode() - .or_else(|_| { - verification_method - .data() - .public_key_jwk() - .and_then(|public_key_jwk| match public_key_jwk.params() { - JwkParams::Okp(okp_params) => Some(okp_params.x.as_bytes().to_vec()), - JwkParams::Ec(ec_params) => Some(ec_params.x.as_bytes().to_vec()), - _ => None, - }) - .ok_or(anyhow::anyhow!("Failed to decode public key for DID URL: {}", did_url)) - }) - .and_then(|encoded_public_key| URL_SAFE_NO_PAD.decode(encoded_public_key).map_err(Into::into)) + verification_method.data().try_decode().or_else(|_| { + verification_method + .data() + .public_key_jwk() + .and_then(|public_key_jwk| match public_key_jwk.params() { + JwkParams::Okp(okp_params) => URL_SAFE_NO_PAD.decode(&okp_params.x).ok(), + JwkParams::Ec(ec_params) => { + let x_bytes = URL_SAFE_NO_PAD.decode(&ec_params.x).ok()?; + let y_bytes = URL_SAFE_NO_PAD.decode(&ec_params.y).ok()?; + + let encoded_point = p256::EncodedPoint::from_affine_coordinates( + p256::FieldBytes::from_slice(&x_bytes), + p256::FieldBytes::from_slice(&y_bytes), + false, // false for uncompressed point + ); + + let verifying_key = p256::ecdsa::VerifyingKey::from_encoded_point(&encoded_point) + .expect("Failed to create verifying key from encoded point"); + + Some(verifying_key.to_encoded_point(false).as_bytes().to_vec()) + } + _ => None, + }) + .ok_or(anyhow::anyhow!("Failed to decode public key for DID URL: {}", did_url)) + }) } } @@ -131,3 +141,69 @@ impl oid4vc_core::Subject for Subject { fn origin() -> url::Origin { config().url.parse::().unwrap().origin() } + +#[cfg(test)] +mod tests { + use super::*; + use agent_shared::config::{set_config, SecretManagerConfig}; + use ring::signature::{UnparsedPublicKey, ECDSA_P256_SHA256_FIXED, ED25519}; + + const ES256_SIGNED_JWT: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGVXpJMU5pSXNJbU55ZGlJNklsQXRNalUySWl3aWEybGtJam9pTkVGMVdXaFNRMk5HYkc0eWJuUm5VMTlxT1hCRlFtUkxkekl3VUhRdGJHRnFXVWh0V1RkQk1FMUdUU0lzSW10MGVTSTZJa1ZESWl3aWVDSTZJakpNV0dwT1JFOTZWM1J3WlZOWk0ydGlUbEkyWm14YVRVUjRZV2gxYXpKMlVXMWpkWFprUVRodk5EUWlMQ0o1SWpvaVpFRjJSVlpzV0UxSFVFdGFjMnRXV1RSWlZ6QnpPRUk0UzNZM2Myc3hZemt5VDA1WVJFcHZlRjlJY3lKOSMwIn0.eyJpc3MiOiJkaWQ6andrOmV5SmhiR2NpT2lKRlV6STFOaUlzSW1OeWRpSTZJbEF0TWpVMklpd2lhMmxrSWpvaU5FRjFXV2hTUTJOR2JHNHliblJuVTE5cU9YQkZRbVJMZHpJd1VIUXRiR0ZxV1VodFdUZEJNRTFHVFNJc0ltdDBlU0k2SWtWRElpd2llQ0k2SWpKTVdHcE9SRTk2VjNSd1pWTlpNMnRpVGxJMlpteGFUVVI0WVdoMWF6SjJVVzFqZFhaa1FUaHZORFFpTENKNUlqb2laRUYyUlZac1dFMUhVRXRhYzJ0V1dUUlpWekJ6T0VJNFMzWTNjMnN4WXpreVQwNVlSRXB2ZUY5SWN5SjkiLCJzdWIiOiJkaWQ6andrOmV5SmhiR2NpT2lKRlV6STFOaUlzSW1OeWRpSTZJbEF0TWpVMklpd2lhMmxrSWpvaU5FRjFXV2hTUTJOR2JHNHliblJuVTE5cU9YQkZRbVJMZHpJd1VIUXRiR0ZxV1VodFdUZEJNRTFHVFNJc0ltdDBlU0k2SWtWRElpd2llQ0k2SWpKTVdHcE9SRTk2VjNSd1pWTlpNMnRpVGxJMlpteGFUVVI0WVdoMWF6SjJVVzFqZFhaa1FUaHZORFFpTENKNUlqb2laRUYyUlZac1dFMUhVRXRhYzJ0V1dUUlpWekJ6T0VJNFMzWTNjMnN4WXpreVQwNVlSRXB2ZUY5SWN5SjkiLCJhdWQiOiJkaWQ6andrOmV5SmhiR2NpT2lKRlV6STFOaUlzSW1OeWRpSTZJbEF0TWpVMklpd2lhMmxrSWpvaVlrNDNiSEpaWVhOUlZrNDNMVUpZY0MxMFdFVldTR1l0YVhkTWRsVnRiWHByVUZsc2VHWlRWRkZvVlNJc0ltdDBlU0k2SWtWRElpd2llQ0k2SW1odVkyNU5UM2sxU0dGWGJ6SmFTbmhCWW5sWU1GOW1NVTFHU1dsMlRrRmtUMjFXYjNSWGVWZG9ielFpTENKNUlqb2libE5wYkhwMllsTmFYMUp1VWpOU2RreHdkRWxITmpkVWJWVkVhR1ZQWVZGNlltczJhVFJmWDBkeVFTSjkiLCJleHAiOjE3MjMwMjkyMjUsImlhdCI6MTcyMzAyODYyNSwibm9uY2UiOiJ0aGlzIGlzIGEgbm9uY2UifQ.w202CZKOeGM9k35tysJylksBUGI3fvkOgsPPVrfXYZzurns7KF5plMiR_KHH4H_GpYg57Nf2JWa3YEcXGDTVdw"; + const EDDSA_SIGNED_JWT: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkVSVFFTSXNJbU55ZGlJNklrVmtNalUxTVRraUxDSnJhV1FpT2lKSmJWOVpNRkZQTm05SFgyczVNbTlzY1RWTWRIUTJZVkE0YzE5QmJFRmhWVUl6UzBkelVFY3RlR0kwSWl3aWEzUjVJam9pVDB0UUlpd2llQ0k2SWxaUGFrUjBRblozY0daalNraHlUelpMVjFOUGRYTlZVR1ptUWt3eVIxOUtjWFp0VVRZNFMzaDRWalFpZlEjMCJ9.eyJpc3MiOiJkaWQ6andrOmV5SmhiR2NpT2lKRlpFUlRRU0lzSW1OeWRpSTZJa1ZrTWpVMU1Ua2lMQ0pyYVdRaU9pSkpiVjlaTUZGUE5tOUhYMnM1TW05c2NUVk1kSFEyWVZBNGMxOUJiRUZoVlVJelMwZHpVRWN0ZUdJMElpd2lhM1I1SWpvaVQwdFFJaXdpZUNJNklsWlBha1IwUW5aM2NHWmpTa2h5VHpaTFYxTlBkWE5WVUdabVFrd3lSMTlLY1hadFVUWTRTM2g0VmpRaWZRIiwic3ViIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaRVJUUVNJc0ltTnlkaUk2SWtWa01qVTFNVGtpTENKcmFXUWlPaUpKYlY5Wk1GRlBObTlIWDJzNU1tOXNjVFZNZEhRMllWQTRjMTlCYkVGaFZVSXpTMGR6VUVjdGVHSTBJaXdpYTNSNUlqb2lUMHRRSWl3aWVDSTZJbFpQYWtSMFFuWjNjR1pqU2toeVR6WkxWMU5QZFhOVlVHWm1Ra3d5UjE5S2NYWnRVVFk0UzNoNFZqUWlmUSIsImF1ZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkVSVFFTSXNJbU55ZGlJNklrVmtNalUxTVRraUxDSnJhV1FpT2lKdFFqSXhUV2t5Y1V0WVZtTTFOREpVWWt0U09UZ3lUelpUWjFKWVZrWlFaVzV3TTNGWWRIRlRla3R2SWl3aWEzUjVJam9pVDB0UUlpd2llQ0k2SWprM1JVRXpSSE5vUmpONlIwSllTVjlVYnpObVJrUnJNVTFxV1VaYVV6bFZiMUpVYmxCT1NIUlpVV01pZlEiLCJleHAiOjE3MjMwMzE3MTQsImlhdCI6MTcyMzAzMTExNCwibm9uY2UiOiJ0aGlzIGlzIGEgbm9uY2UifQ.oGRYpwH4QvWZs0bZkgAuxq6MqNYdoX44KxNfRl7GzXCnv_0D_c19rhYMwzn04R7udNCthFDr7GUhXLQgROlUDw"; + + lazy_static::lazy_static! { + static ref SECRET_MANAGER_CONFIG: SecretManagerConfig = SecretManagerConfig { + generate_stronghold: false, + stronghold_path: "../agent_secret_manager/tests/res/all_slots.stronghold".to_string(), + stronghold_password: "sup3rSecr3t".to_string(), + issuer_eddsa_key_id: Some("ed25519-0".to_string()), + issuer_es256_key_id: Some("es256-0".to_string()), + issuer_did: None, + issuer_fragment: None, + }; + } + + #[tokio::test] + async fn es256_signed_jwt_successfully_verified() { + set_config().set_secret_manager_config(SECRET_MANAGER_CONFIG.clone()); + + let subject = Arc::new(Subject { + secret_manager: crate::secret_manager().await, + }); + + let mut split = ES256_SIGNED_JWT.rsplitn(2, '.'); + let (signature, message) = (split.next().unwrap(), split.next().unwrap()); + + // Decode the signature. + let signature_bytes = URL_SAFE_NO_PAD.decode(signature).unwrap(); + + // Resolve the public key from the DID Document + let public_key_bytes = subject.public_key("did:jwk:eyJhbGciOiJFUzI1NiIsImNydiI6IlAtMjU2Iiwia2lkIjoiNEF1WWhSQ2NGbG4ybnRnU19qOXBFQmRLdzIwUHQtbGFqWUhtWTdBME1GTSIsImt0eSI6IkVDIiwieCI6IjJMWGpORE96V3RwZVNZM2tiTlI2ZmxaTUR4YWh1azJ2UW1jdXZkQThvNDQiLCJ5IjoiZEF2RVZsWE1HUEtac2tWWTRZVzBzOEI4S3Y3c2sxYzkyT05YREpveF9IcyJ9#0").await.unwrap(); + + // Verify the signature + let public_key = UnparsedPublicKey::new(&ECDSA_P256_SHA256_FIXED, public_key_bytes); + assert!(public_key.verify(message.as_bytes(), &signature_bytes).is_ok()); + } + + #[tokio::test] + async fn eddsa_signed_jwt_successfully_verified() { + set_config().set_secret_manager_config(SECRET_MANAGER_CONFIG.clone()); + + let subject = Arc::new(Subject { + secret_manager: crate::secret_manager().await, + }); + + let mut split = EDDSA_SIGNED_JWT.rsplitn(2, '.'); + let (signature, message) = (split.next().unwrap(), split.next().unwrap()); + + // Decode the signature. + let signature_bytes = URL_SAFE_NO_PAD.decode(signature).unwrap(); + + // Resolve the public key from the DID Document + let public_key_bytes = subject.public_key("did:jwk:eyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJJbV9ZMFFPNm9HX2s5Mm9scTVMdHQ2YVA4c19BbEFhVUIzS0dzUEcteGI0Iiwia3R5IjoiT0tQIiwieCI6IlZPakR0QnZ3cGZjSkhyTzZLV1NPdXNVUGZmQkwyR19KcXZtUTY4S3h4VjQifQ#0").await.unwrap(); + + // Verify the signature + let public_key = UnparsedPublicKey::new(&ED25519, public_key_bytes); + assert!(public_key.verify(message.as_bytes(), &signature_bytes).is_ok()); + } +} diff --git a/agent_secret_manager/tests/res/all_slots.stronghold b/agent_secret_manager/tests/res/all_slots.stronghold new file mode 100644 index 00000000..7d3ca871 Binary files /dev/null and b/agent_secret_manager/tests/res/all_slots.stronghold differ diff --git a/agent_shared/src/config.rs b/agent_shared/src/config.rs index 28459864..d1eb7db5 100644 --- a/agent_shared/src/config.rs +++ b/agent_shared/src/config.rs @@ -270,6 +270,10 @@ impl ApplicationConfiguration { } } } + + pub fn set_secret_manager_config(&mut self, config: SecretManagerConfig) { + self.secret_manager = config; + } } /// Returns the application configuration or loads it, if it hasn't been loaded already. diff --git a/agent_shared/src/domain_linkage/mod.rs b/agent_shared/src/domain_linkage/mod.rs index f0af2625..7bd5d82c 100644 --- a/agent_shared/src/domain_linkage/mod.rs +++ b/agent_shared/src/domain_linkage/mod.rs @@ -3,6 +3,7 @@ pub mod verifiable_credential_jwt; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use std::time::{SystemTime, UNIX_EPOCH}; +use crate::config::get_preferred_signing_algorithm; use crate::error::SharedError; use crate::from_jsonwebtoken_algorithm_to_jwsalgorithm; use did_manager::SecretManager; @@ -12,7 +13,7 @@ use identity_credential::domain_linkage::{DomainLinkageConfiguration, DomainLink use identity_did::DID; use identity_document::document::CoreDocument; use identity_storage::{JwkDocumentExt, JwsSignatureOptions, Storage}; -use jsonwebtoken::{Algorithm, Header}; +use jsonwebtoken::Header; use tracing::info; use verifiable_credential_jwt::VerifiableCredentialJwt; @@ -86,7 +87,7 @@ pub async fn create_did_configuration_resource( // Compose JWT let header = Header { - alg: Algorithm::EdDSA, + alg: get_preferred_signing_algorithm(), typ: Some("JWT".to_string()), kid: Some(format!("{subject_did}#key-0")), ..Default::default() diff --git a/agent_verification/src/authorization_request/aggregate.rs b/agent_verification/src/authorization_request/aggregate.rs index 9f96d835..cd7692a4 100644 --- a/agent_verification/src/authorization_request/aggregate.rs +++ b/agent_verification/src/authorization_request/aggregate.rs @@ -3,10 +3,9 @@ use crate::{ generic_oid4vc::{GenericAuthorizationRequest, OID4VPAuthorizationRequest, SIOPv2AuthorizationRequest}, services::VerificationServices, }; -use agent_shared::config::config; +use agent_shared::config::{config, get_preferred_signing_algorithm}; use async_trait::async_trait; use cqrs_es::Aggregate; -use jsonwebtoken::Algorithm; use oid4vc_core::{authorization_request::ByReference, scope::Scope}; use oid4vp::authorization_request::ClientIdScheme; use serde::{Deserialize, Serialize}; @@ -47,7 +46,7 @@ impl Aggregate for AuthorizationRequest { let default_subject_syntax_type = services.relying_party.default_subject_syntax_type().to_string(); let verifier = &services.verifier; let verifier_did = verifier - .identifier(&default_subject_syntax_type, Algorithm::EdDSA) + .identifier(&default_subject_syntax_type, get_preferred_signing_algorithm()) .await .unwrap(); @@ -165,6 +164,7 @@ pub mod tests { use agent_shared::config::set_config; use agent_shared::config::SupportedDidMethod; use cqrs_es::test::TestFramework; + use jsonwebtoken::Algorithm; use lazy_static::lazy_static; use oid4vc_core::Subject as _; use oid4vc_core::{client_metadata::ClientMetadataResource, SubjectSyntaxType};