From 1ec2d70c6ed9595bbd579644596e56f7a1743d24 Mon Sep 17 00:00:00 2001 From: Chris Smith <1979423+chris13524@users.noreply.github.com> Date: Thu, 22 Feb 2024 16:28:38 -0500 Subject: [PATCH] fix: attribute Keys Server 404 as client error (#372) * fix: attribute Keys Server 404 as client error * chore: test keys server request --- Cargo.toml | 2 +- src/auth.rs | 475 ++++++++++++++---- src/model/types/account_id/eip155.rs | 36 +- src/model/types/account_id/mod.rs | 4 +- .../handlers/relay_webhook/error.rs | 26 +- .../relay_webhook/handlers/notify_delete.rs | 3 +- .../handlers/notify_get_notifications.rs | 3 +- .../handlers/notify_subscribe.rs | 3 +- .../relay_webhook/handlers/notify_update.rs | 3 +- .../handlers/notify_watch_subscriptions.rs | 3 +- tests/deployment.rs | 10 +- tests/integration.rs | 61 +-- tests/utils/mod.rs | 109 +--- tests/utils/notify_relay_api.rs | 10 +- tests/utils/relay_api.rs | 4 +- 15 files changed, 484 insertions(+), 268 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e08bb61f..16aeee42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,9 +93,9 @@ wiremock = "0.5.19" itertools = "0.11.0" sha3 = "0.10.8" validator = { version = "0.16.1", features = ["derive"] } +k256 = "0.13.1" [dev-dependencies] -k256 = "0.13.1" test-context = "0.1" [build-dependencies] diff --git a/src/auth.rs b/src/auth.rs index 9009d519..129ec061 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -6,7 +6,7 @@ use { helpers::{GetNotificationsParams, GetNotificationsResult}, types::{AccountId, AccountIdParseError}, }, - registry::storage::{redis::Redis, KeyValueStorage}, + registry::storage::{error::StorageError, redis::Redis, KeyValueStorage}, BlockchainApiProvider, }, base64::{DecodeError, Engine}, @@ -504,54 +504,9 @@ pub enum AuthError { #[error("Invalid algorithm")] Algorithm, - #[error("Keyserver returned non-success status code. status:{status} response:{response:?}")] - KeyserverUnsuccessfulResponse { - status: StatusCode, - response: Response, - }, - - #[error("Keyserver returned non-success response. status:{status} error:{error:?}")] - KeyserverNotSuccess { - status: String, - error: Option, - }, - - #[error("Keyserver returned successful response, but without a value")] - KeyserverResponseMissingValue, - #[error("JWT iss not did:key: {0}")] JwtIssNotDidKey(ClientIdDecodingError), - #[error("CACAO verification failed: {0}")] - CacaoValidation(CacaoError), - - #[error("CACAO account doesn't match")] - CacaoAccountMismatch, - - #[error("CACAO doesn't contain matching iss: {0}")] - CacaoMissingIdentityKey(CacaoError), - - #[error("CACAO iss is not a did:pkh: {0}")] - CacaoIssNotDidPkh(AccountIdParseError), - - #[error("Account namespace not supported.")] - AccountNamespaceNotSupported, - - #[error("CACAO has wrong iss")] - CacaoWrongIdentityKey, - - #[error("CACAO expired")] - CacaoExpired, - - #[error("CACAO not yet valid")] - CacaoNotYetValid, - - #[error("CACAO missing statement")] - CacaoStatementMissing, - - #[error("CACAO invalid statement")] - CacaoStatementInvalid, - #[error("JWT expired")] JwtExpired, @@ -575,47 +530,135 @@ pub enum AuthorizedApp { Unlimited, } +#[derive(Debug, Error)] +pub enum IdentityVerificationError { + #[error("Client: {0}")] + Client(#[from] IdentityVerificationClientError), + + #[error("Internal: {0}")] + Internal(#[from] IdentityVerificationInternalError), +} + +#[derive(Debug, Error)] +pub enum IdentityVerificationClientError { + #[error("CACAO not registered")] + NotRegistered, + + #[error("ksu could not be parsed as URL: {0}")] + KsuNotUrl(url::ParseError), + + #[error("iss could not be parsed as an account ID: {0}")] + CacaoAccountId(AccountIdParseError), + + #[error("CACAO failed verification: {0}")] + CacaoVerification(CacaoError), + + #[error("CACAO doesn't contain matching iss: {0}")] + CacaoMissingIdentityKey(CacaoError), + + #[error("CACAO has wrong iss")] + CacaoWrongIdentityKey, + + #[error("CACAO missing statement")] + CacaoStatementMissing, + + #[error("CACAO invalid statement")] + CacaoStatementInvalid, + + #[error("CACAO account doesn't match")] + CacaoAccountMismatch, + + #[error("CACAO not yet valid")] + CacaoNotYetValid, + + #[error("CACAO nbf parse error: {0}")] + CacaoNbfParse(chrono::ParseError), + + #[error("CACAO expired")] + CacaoExpired, + + #[error("CACAO exp parse error: {0}")] + CacaoExpParse(chrono::ParseError), +} + +#[derive(Debug, Error)] +pub enum IdentityVerificationInternalError { + #[error("HTTP: {0}")] + Http(reqwest::Error), + + #[error("JSON: {0}")] + Json(reqwest::Error), + + #[error("Keys Server returned non-success response. status:{status} error:{error:?}")] + KeyServerNotSuccess { + status: String, + error: Option, + }, + + #[error("Keys Server returned successful response, but without a value")] + KeyServerResponseMissingValue, + + #[error("Keys Server returned non-success status code. status:{status} response:{response:?}")] + KeyServerUnsuccessfulResponse { + status: StatusCode, + response: Response, + }, + + #[error("Cache lookup: {0}")] + CacheLookup(StorageError), + + #[error("Could not construct Keys Server request URL: {0}")] + KeysServerRequestUrlConstructionError(url::ParseError), +} + pub const KEYS_SERVER_STATUS_SUCCESS: &str = "SUCCESS"; -async fn keys_server_request(url: Url) -> Result { +async fn keys_server_request(url: Url) -> Result { info!("Timing: Requesting to keys server"); - let response = reqwest::get(url).await?; + let response = reqwest::get(url) + .await + .map_err(IdentityVerificationInternalError::Http)?; info!("Timing: Keys server response"); - if !response.status().is_success() { - return Err(NotifyServerError::JwtVerificationError( - AuthError::KeyserverUnsuccessfulResponse { - status: response.status(), - response, - }, - )); - } + match response.status() { + StatusCode::NOT_FOUND => Err(IdentityVerificationClientError::NotRegistered)?, + status if status.is_success() => { + let keyserver_response = response + .json::() + .await + .map_err(IdentityVerificationInternalError::Json)?; + + if keyserver_response.status != KEYS_SERVER_STATUS_SUCCESS { + Err(IdentityVerificationInternalError::KeyServerNotSuccess { + status: keyserver_response.status, + error: keyserver_response.error, + })?; + } - let keyserver_response = response.json::().await?; + let Some(cacao) = keyserver_response.value else { + // Keys server should never do this since it already returned SUCCESS above + return Err(IdentityVerificationInternalError::KeyServerResponseMissingValue)?; + }; - if keyserver_response.status != KEYS_SERVER_STATUS_SUCCESS { - Err(AuthError::KeyserverNotSuccess { - status: keyserver_response.status, - error: keyserver_response.error, - })?; + Ok(cacao.cacao) + } + status => Err( + IdentityVerificationInternalError::KeyServerUnsuccessfulResponse { status, response }, + )?, } - - let Some(cacao) = keyserver_response.value else { - // Keys server should never do this since it already returned SUCCESS above - return Err(AuthError::KeyserverResponseMissingValue)?; - }; - - Ok(cacao.cacao) } async fn keys_server_request_cached( url: Url, redis: Option<&Arc>, -) -> Result<(Cacao, KeysServerResponseSource), NotifyServerError> { +) -> Result<(Cacao, KeysServerResponseSource), IdentityVerificationError> { let cache_key = format!("keys-server-{}", url); if let Some(redis) = redis { - let value = redis.get(&cache_key).await?; + let value = redis + .get(&cache_key) + .await + .map_err(IdentityVerificationInternalError::CacheLookup)?; if let Some(cacao) = value { return Ok((cacao, KeysServerResponseSource::Cache)); } @@ -669,8 +712,12 @@ pub async fn verify_identity( redis: Option<&Arc>, provider: &BlockchainApiProvider, metrics: Option<&Metrics>, -) -> Result { - let mut url = Url::parse(ksu)?.join(KEYS_SERVER_IDENTITY_ENDPOINT)?; +) -> Result { + let mut url = Url::parse(ksu) + .map_err(IdentityVerificationClientError::KsuNotUrl)? + .join(KEYS_SERVER_IDENTITY_ENDPOINT) + // This probably shouldn't error, but catching just in-case + .map_err(IdentityVerificationInternalError::KeysServerRequestUrlConstructionError)?; let pubkey = iss_client_id.to_string(); url.query_pairs_mut() .append_pair(KEYS_SERVER_IDENTITY_ENDPOINT_PUBLIC_KEY_QUERY, &pubkey); @@ -681,12 +728,13 @@ pub async fn verify_identity( metrics.keys_server_request(start, &source); } - let account = AccountId::from_did_pkh(&cacao.p.iss).map_err(AuthError::CacaoIssNotDidPkh)?; + let account = AccountId::from_did_pkh(&cacao.p.iss) + .map_err(IdentityVerificationClientError::CacaoAccountId)?; let always_true = cacao .verify(provider) .await - .map_err(AuthError::CacaoValidation)?; + .map_err(IdentityVerificationClientError::CacaoVerification)?; assert!(always_true); // TODO verify `cacao.p.aud`. Blocked by at least https://github.com/WalletConnect/walletconnect-utils/issues/128 @@ -694,34 +742,38 @@ pub async fn verify_identity( let cacao_identity_key = cacao .p .identity_key() - .map_err(AuthError::CacaoMissingIdentityKey)?; + .map_err(IdentityVerificationClientError::CacaoMissingIdentityKey)?; if cacao_identity_key != pubkey { - Err(AuthError::CacaoWrongIdentityKey)?; + Err(IdentityVerificationClientError::CacaoWrongIdentityKey)?; } let app = { - let statement = cacao.p.statement.ok_or(AuthError::CacaoStatementMissing)?; + let statement = cacao + .p + .statement + .ok_or(IdentityVerificationClientError::CacaoStatementMissing)?; info!("CACAO statement: {statement}"); - parse_cacao_statement(&statement, &cacao.p.domain)? + parse_cacao_statement(&statement, &cacao.p.domain) + .map_err(|_| IdentityVerificationClientError::CacaoStatementInvalid)? }; if cacao.p.iss != sub { - Err(AuthError::CacaoAccountMismatch)?; + Err(IdentityVerificationClientError::CacaoAccountMismatch)?; } if let Some(nbf) = cacao.p.nbf { - let nbf = DateTime::parse_from_rfc3339(&nbf)?; - + let nbf = DateTime::parse_from_rfc3339(&nbf) + .map_err(IdentityVerificationClientError::CacaoNbfParse)?; if Utc::now().timestamp() <= nbf.timestamp() { - Err(AuthError::CacaoNotYetValid)?; + return Err(IdentityVerificationClientError::CacaoNotYetValid)?; } } if let Some(exp) = cacao.p.exp { - let exp = DateTime::parse_from_rfc3339(&exp)?; - + let exp = DateTime::parse_from_rfc3339(&exp) + .map_err(IdentityVerificationClientError::CacaoExpParse)?; if exp.timestamp() <= Utc::now().timestamp() { - Err(AuthError::CacaoExpired)?; + return Err(IdentityVerificationClientError::CacaoExpired)?; } } @@ -732,10 +784,7 @@ pub async fn verify_identity( }) } -fn parse_cacao_statement( - statement: &str, - domain: &str, -) -> Result { +fn parse_cacao_statement(statement: &str, domain: &str) -> Result { if statement.contains("DAPP") || statement == STATEMENT_THIS_DOMAIN_IDENTITY || statement == STATEMENT_THIS_DOMAIN @@ -749,7 +798,7 @@ fn parse_cacao_statement( { Ok(AuthorizedApp::Unlimited) } else { - return Err(AuthError::CacaoStatementInvalid)?; + Err(()) } } @@ -835,9 +884,142 @@ impl<'a> Deserialize<'a> for DidWeb { } } +pub mod test_utils { + use { + super::{ + AccountId, CacaoValue, DidWeb, KeyServerResponse, KEYS_SERVER_IDENTITY_ENDPOINT, + KEYS_SERVER_IDENTITY_ENDPOINT_PUBLIC_KEY_QUERY, KEYS_SERVER_STATUS_SUCCESS, + }, + chrono::Utc, + hyper::StatusCode, + k256::ecdsa::SigningKey as EcdsaSigningKey, + relay_rpc::{ + auth::{ + cacao::{ + self, + header::EIP4361, + signature::{ + eip1271::get_rpc_url::GetRpcUrl, + eip191::{eip191_bytes, EIP191}, + }, + Cacao, + }, + ed25519_dalek::SigningKey as Ed25519SigningKey, + }, + domain::DecodedClientId, + }, + sha2::Digest, + sha3::Keccak256, + url::Url, + wiremock::{ + http::Method, + matchers::{method, path, query_param}, + Mock, MockServer, ResponseTemplate, + }, + }; + + #[derive(Clone)] + pub struct IdentityKeyDetails { + pub keys_server_url: Url, + pub signing_key: Ed25519SigningKey, + pub client_id: DecodedClientId, + } + + pub fn generate_identity_key() -> (Ed25519SigningKey, DecodedClientId) { + let signing_key = Ed25519SigningKey::generate(&mut rand::thread_rng()); + let client_id = DecodedClientId::from_key(&signing_key.verifying_key()); + (signing_key, client_id) + } + + pub async fn sign_cacao( + app_domain: &DidWeb, + account: &AccountId, + statement: String, + identity_public_key: DecodedClientId, + keys_server_url: String, + account_signing_key: &EcdsaSigningKey, + ) -> cacao::Cacao { + let mut cacao = cacao::Cacao { + h: cacao::header::Header { + t: EIP4361.to_owned(), + }, + p: cacao::payload::Payload { + domain: app_domain.domain().to_owned(), + iss: account.to_did_pkh(), + statement: Some(statement), + aud: identity_public_key.to_did_key(), + version: cacao::Version::V1, + nonce: hex::encode(rand::Rng::gen::<[u8; 10]>(&mut rand::thread_rng())), + iat: Utc::now().to_rfc3339(), + exp: None, + nbf: None, + request_id: None, + resources: Some(vec![keys_server_url]), + }, + s: cacao::signature::Signature { + t: "".to_owned(), + s: "".to_owned(), + }, + }; + let (signature, recovery): (k256::ecdsa::Signature, _) = account_signing_key + .sign_digest_recoverable(Keccak256::new_with_prefix(eip191_bytes( + &cacao.siwe_message().unwrap(), + ))) + .unwrap(); + let cacao_signature = [&signature.to_bytes()[..], &[recovery.to_byte()]].concat(); + cacao.s.t = EIP191.to_owned(); + cacao.s.s = hex::encode(cacao_signature); + cacao.verify(&MockGetRpcUrl).await.unwrap(); + cacao + } + + pub struct MockGetRpcUrl; + impl GetRpcUrl for MockGetRpcUrl { + fn get_rpc_url(&self, _: String) -> Option { + None + } + } + + pub async fn register_mocked_identity_key( + mock_keys_server: &MockServer, + identity_public_key: DecodedClientId, + cacao: Cacao, + ) { + Mock::given(method(Method::Get)) + .and(path(KEYS_SERVER_IDENTITY_ENDPOINT)) + .and(query_param( + KEYS_SERVER_IDENTITY_ENDPOINT_PUBLIC_KEY_QUERY, + identity_public_key.to_string(), + )) + .respond_with( + ResponseTemplate::new(StatusCode::OK).set_body_json(KeyServerResponse { + status: KEYS_SERVER_STATUS_SUCCESS.to_owned(), + error: None, + value: Some(CacaoValue { cacao }), + }), + ) + .mount(mock_keys_server) + .await; + } +} + #[cfg(test)] -mod test { - use super::*; +pub mod test { + use { + super::*, + crate::{ + auth::test_utils::{ + generate_identity_key, register_mocked_identity_key, sign_cacao, IdentityKeyDetails, + }, + model::types::eip155::test_utils::generate_account, + }, + relay_rpc::domain::ProjectId, + wiremock::{ + http::Method, + matchers::{method, path, query_param}, + Mock, MockServer, ResponseTemplate, + }, + }; #[test] fn notify_all_domains() { @@ -899,4 +1081,111 @@ mod test { AuthorizedApp::Limited("app.example.com".to_owned()) ); } + + #[tokio::test] + async fn test_keys_server_request() { + let (account_signing_key, account) = generate_account(); + + let keys_server = MockServer::start().await; + let keys_server_url = keys_server.uri().parse::().unwrap(); + + let (identity_signing_key, identity_public_key) = generate_identity_key(); + let identity_key_details = IdentityKeyDetails { + keys_server_url: keys_server_url.clone(), + signing_key: identity_signing_key, + client_id: identity_public_key.clone(), + }; + + let project_id = ProjectId::generate(); + let app_domain = DidWeb::from_domain(format!("{project_id}.walletconnect.com")); + + let cacao = sign_cacao( + &app_domain, + &account, + STATEMENT_THIS_DOMAIN.to_owned(), + identity_public_key.clone(), + identity_key_details.keys_server_url.to_string(), + &account_signing_key, + ) + .await; + + register_mocked_identity_key(&keys_server, identity_public_key.clone(), cacao.clone()) + .await; + + let mut keys_server_request_url = + keys_server_url.join(KEYS_SERVER_IDENTITY_ENDPOINT).unwrap(); + keys_server_request_url.query_pairs_mut().append_pair( + KEYS_SERVER_IDENTITY_ENDPOINT_PUBLIC_KEY_QUERY, + &identity_public_key.to_string(), + ); + assert_eq!( + cacao, + keys_server_request(keys_server_request_url).await.unwrap() + ); + } + + #[tokio::test] + async fn test_keys_server_request_404() { + let identity_public_key = Uuid::new_v4(); + + let keys_server = MockServer::start().await; + let keys_server_url = keys_server.uri().parse::().unwrap(); + + Mock::given(method(Method::Get)) + .and(path(KEYS_SERVER_IDENTITY_ENDPOINT)) + .and(query_param( + KEYS_SERVER_IDENTITY_ENDPOINT_PUBLIC_KEY_QUERY, + identity_public_key.to_string(), + )) + .respond_with(ResponseTemplate::new(StatusCode::NOT_FOUND)) + .mount(&keys_server) + .await; + + let mut keys_server_request_url = + keys_server_url.join(KEYS_SERVER_IDENTITY_ENDPOINT).unwrap(); + keys_server_request_url.query_pairs_mut().append_pair( + KEYS_SERVER_IDENTITY_ENDPOINT_PUBLIC_KEY_QUERY, + &identity_public_key.to_string(), + ); + assert!(matches!( + keys_server_request(keys_server_request_url).await, + Err(IdentityVerificationError::Client( + IdentityVerificationClientError::NotRegistered + )) + )); + } + + #[tokio::test] + async fn test_keys_server_request_500() { + let identity_public_key = Uuid::new_v4(); + + let keys_server = MockServer::start().await; + let keys_server_url = keys_server.uri().parse::().unwrap(); + + Mock::given(method(Method::Get)) + .and(path(KEYS_SERVER_IDENTITY_ENDPOINT)) + .and(query_param( + KEYS_SERVER_IDENTITY_ENDPOINT_PUBLIC_KEY_QUERY, + identity_public_key.to_string(), + )) + .respond_with(ResponseTemplate::new(StatusCode::INTERNAL_SERVER_ERROR)) + .mount(&keys_server) + .await; + + let mut keys_server_request_url = + keys_server_url.join(KEYS_SERVER_IDENTITY_ENDPOINT).unwrap(); + keys_server_request_url.query_pairs_mut().append_pair( + KEYS_SERVER_IDENTITY_ENDPOINT_PUBLIC_KEY_QUERY, + &identity_public_key.to_string(), + ); + assert!(matches!( + keys_server_request(keys_server_request_url).await, + Err(IdentityVerificationError::Internal( + IdentityVerificationInternalError::KeyServerUnsuccessfulResponse { + status: StatusCode::INTERNAL_SERVER_ERROR, + response: _ + } + )) + )); + } } diff --git a/src/model/types/account_id/eip155.rs b/src/model/types/account_id/eip155.rs index 03b2629b..553cde93 100644 --- a/src/model/types/account_id/eip155.rs +++ b/src/model/types/account_id/eip155.rs @@ -68,8 +68,42 @@ fn validate_eip155_address(address: &str) -> Result<(), Eip155AddressError> { } } +pub mod test_utils { + use { + super::erc_55_checksum_encode, crate::model::types::AccountId, k256::ecdsa::SigningKey, + rand::rngs::OsRng, sha2::Digest, sha3::Keccak256, + }; + + pub fn generate_eoa() -> (SigningKey, String) { + let account_signing_key = SigningKey::random(&mut OsRng); + let address = &Keccak256::default() + .chain_update( + &account_signing_key + .verifying_key() + .to_encoded_point(false) + .as_bytes()[1..], + ) + .finalize()[12..]; + let address = format!( + "0x{}", + erc_55_checksum_encode(&hex::encode(address)).collect::() + ); + (account_signing_key, address) + } + + pub fn format_eip155_account(chain_id: u32, address: &str) -> AccountId { + AccountId::try_from(format!("eip155:{chain_id}:{address}")).unwrap() + } + + pub fn generate_account() -> (SigningKey, AccountId) { + let (account_signing_key, address) = generate_eoa(); + let account = format_eip155_account(1, &address); + (account_signing_key, account) + } +} + #[cfg(test)] -mod tests { +pub mod tests { use {super::*, crate::model::types::account_id::caip10::validate_caip_10}; #[test] diff --git a/src/model/types/account_id/mod.rs b/src/model/types/account_id/mod.rs index 64c9adaa..52925696 100644 --- a/src/model/types/account_id/mod.rs +++ b/src/model/types/account_id/mod.rs @@ -5,8 +5,8 @@ use { std::sync::Arc, }; -mod caip10; -mod eip155; +pub mod caip10; +pub mod eip155; pub mod erc55; #[derive( diff --git a/src/services/public_http_server/handlers/relay_webhook/error.rs b/src/services/public_http_server/handlers/relay_webhook/error.rs index fe0965f9..1547e36a 100644 --- a/src/services/public_http_server/handlers/relay_webhook/error.rs +++ b/src/services/public_http_server/handlers/relay_webhook/error.rs @@ -1,6 +1,11 @@ use { crate::{ - auth::JwtError, error::NotifyServerError, rate_limit::RateLimitExceeded, + auth::{ + IdentityVerificationClientError, IdentityVerificationError, + IdentityVerificationInternalError, JwtError, + }, + error::NotifyServerError, + rate_limit::RateLimitExceeded, types::EnvelopeParseError, }, relay_rpc::domain::Topic, @@ -25,12 +30,18 @@ pub enum RelayMessageClientError { #[error("JWT parse/verification error: {0}")] JwtError(JwtError), + + #[error(transparent)] + IdentityVerification(IdentityVerificationClientError), } #[derive(Debug, thiserror::Error)] pub enum RelayMessageServerError { #[error(transparent)] NotifyServerError(#[from] NotifyServerError), + + #[error(transparent)] + IdentityVerification(IdentityVerificationInternalError), } #[derive(Debug, thiserror::Error)] @@ -41,3 +52,16 @@ pub enum RelayMessageError { #[error("Relay message server error: {0}")] Server(#[from] RelayMessageServerError), } + +impl From for RelayMessageError { + fn from(err: IdentityVerificationError) -> Self { + match err { + IdentityVerificationError::Client(err) => { + RelayMessageError::Client(RelayMessageClientError::IdentityVerification(err)) + } + IdentityVerificationError::Internal(err) => { + RelayMessageError::Server(RelayMessageServerError::IdentityVerification(err)) + } + } + } +} diff --git a/src/services/public_http_server/handlers/relay_webhook/handlers/notify_delete.rs b/src/services/public_http_server/handlers/relay_webhook/handlers/notify_delete.rs index 8a5be488..7d4c48af 100644 --- a/src/services/public_http_server/handlers/relay_webhook/handlers/notify_delete.rs +++ b/src/services/public_http_server/handlers/relay_webhook/handlers/notify_delete.rs @@ -113,8 +113,7 @@ pub async fn handle(msg: RelayIncomingMessage, state: &AppState) -> Result<(), R &state.provider, state.metrics.as_ref(), ) - .await - .map_err(RelayMessageServerError::NotifyServerError)?; // TODO change to client error? + .await?; // TODO verify `sub_auth.aud` matches `project_data.identity_keypair` diff --git a/src/services/public_http_server/handlers/relay_webhook/handlers/notify_get_notifications.rs b/src/services/public_http_server/handlers/relay_webhook/handlers/notify_get_notifications.rs index d5a653cf..d4ee6d0b 100644 --- a/src/services/public_http_server/handlers/relay_webhook/handlers/notify_get_notifications.rs +++ b/src/services/public_http_server/handlers/relay_webhook/handlers/notify_get_notifications.rs @@ -112,8 +112,7 @@ pub async fn handle(msg: RelayIncomingMessage, state: &AppState) -> Result<(), R &state.provider, state.metrics.as_ref(), ) - .await - .map_err(RelayMessageServerError::NotifyServerError)?; // TODO change to client error? + .await?; // TODO verify `sub_auth.aud` matches `project_data.identity_keypair` diff --git a/src/services/public_http_server/handlers/relay_webhook/handlers/notify_subscribe.rs b/src/services/public_http_server/handlers/relay_webhook/handlers/notify_subscribe.rs index b8362785..94a5f608 100644 --- a/src/services/public_http_server/handlers/relay_webhook/handlers/notify_subscribe.rs +++ b/src/services/public_http_server/handlers/relay_webhook/handlers/notify_subscribe.rs @@ -133,8 +133,7 @@ pub async fn handle(msg: RelayIncomingMessage, state: &AppState) -> Result<(), R &state.provider, state.metrics.as_ref(), ) - .await - .map_err(RelayMessageServerError::NotifyServerError)?; // TODO change to client error? + .await?; // TODO verify `sub_auth.aud` matches `project_data.identity_keypair` diff --git a/src/services/public_http_server/handlers/relay_webhook/handlers/notify_update.rs b/src/services/public_http_server/handlers/relay_webhook/handlers/notify_update.rs index 1c1e0e9d..15c918e3 100644 --- a/src/services/public_http_server/handlers/relay_webhook/handlers/notify_update.rs +++ b/src/services/public_http_server/handlers/relay_webhook/handlers/notify_update.rs @@ -111,8 +111,7 @@ pub async fn handle(msg: RelayIncomingMessage, state: &AppState) -> Result<(), R &state.provider, state.metrics.as_ref(), ) - .await - .map_err(RelayMessageServerError::NotifyServerError)?; // TODO change to client error? + .await?; // TODO verify `sub_auth.aud` matches `project_data.identity_keypair` diff --git a/src/services/public_http_server/handlers/relay_webhook/handlers/notify_watch_subscriptions.rs b/src/services/public_http_server/handlers/relay_webhook/handlers/notify_watch_subscriptions.rs index eb440e6f..abba0e9d 100644 --- a/src/services/public_http_server/handlers/relay_webhook/handlers/notify_watch_subscriptions.rs +++ b/src/services/public_http_server/handlers/relay_webhook/handlers/notify_watch_subscriptions.rs @@ -104,8 +104,7 @@ pub async fn handle(msg: RelayIncomingMessage, state: &AppState) -> Result<(), R &state.provider, state.metrics.as_ref(), ) - .await - .map_err(RelayMessageServerError::NotifyServerError)? // TODO change to client error? + .await? // TODO verify `sub_auth.aud` matches `notify-server.identity_keypair` diff --git a/tests/deployment.rs b/tests/deployment.rs index 5d49d245..c96f246c 100644 --- a/tests/deployment.rs +++ b/tests/deployment.rs @@ -1,15 +1,19 @@ use { crate::utils::{ - assert_successful_response, generate_account, generate_identity_key, + assert_successful_response, http_api::subscribe_topic, notify_relay_api::{ accept_notify_message, accept_watch_subscriptions_changed, subscribe, watch_subscriptions, }, - sign_cacao, unregister_identity_key, IdentityKeyDetails, RelayClient, + unregister_identity_key, RelayClient, }, notify_server::{ - auth::{CacaoValue, DidWeb, STATEMENT_THIS_DOMAIN}, + auth::{ + test_utils::{generate_identity_key, sign_cacao, IdentityKeyDetails}, + CacaoValue, DidWeb, STATEMENT_THIS_DOMAIN, + }, + model::types::eip155::test_utils::generate_account, rpc::decode_key, services::public_http_server::handlers::notify_v0::NotifyBody, types::Notification, diff --git a/tests/integration.rs b/tests/integration.rs index a111b400..dbeba5b3 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1,10 +1,9 @@ use { crate::utils::{ - assert_successful_response, encode_auth, format_eip155_account, generate_eoa, - generate_identity_key, + assert_successful_response, encode_auth, notify_relay_api::{accept_watch_subscriptions_changed, subscribe, watch_subscriptions}, relay_api::{decode_message, decode_response_message}, - sign_cacao, IdentityKeyDetails, RelayClient, RELAY_MESSAGE_DELIVERY_TIMEOUT, + RelayClient, RELAY_MESSAGE_DELIVERY_TIMEOUT, }, async_trait::async_trait, chrono::{DateTime, Duration, TimeZone, Utc}, @@ -14,14 +13,15 @@ use { notify_server::{ auth::{ encode_authentication_private_key, encode_authentication_public_key, - encode_subscribe_private_key, encode_subscribe_public_key, from_jwt, CacaoValue, - DidWeb, GetSharedClaims, KeyServerResponse, MessageResponseAuth, - NotifyServerSubscription, SubscriptionDeleteRequestAuth, - SubscriptionDeleteResponseAuth, SubscriptionGetNotificationsRequestAuth, - SubscriptionGetNotificationsResponseAuth, SubscriptionUpdateRequestAuth, - SubscriptionUpdateResponseAuth, KEYS_SERVER_IDENTITY_ENDPOINT, - KEYS_SERVER_IDENTITY_ENDPOINT_PUBLIC_KEY_QUERY, KEYS_SERVER_STATUS_SUCCESS, - STATEMENT_ALL_DOMAINS, STATEMENT_THIS_DOMAIN, + encode_subscribe_private_key, encode_subscribe_public_key, from_jwt, + test_utils::{ + generate_identity_key, register_mocked_identity_key, sign_cacao, IdentityKeyDetails, + }, + CacaoValue, DidWeb, GetSharedClaims, MessageResponseAuth, NotifyServerSubscription, + SubscriptionDeleteRequestAuth, SubscriptionDeleteResponseAuth, + SubscriptionGetNotificationsRequestAuth, SubscriptionGetNotificationsResponseAuth, + SubscriptionUpdateRequestAuth, SubscriptionUpdateResponseAuth, STATEMENT_ALL_DOMAINS, + STATEMENT_THIS_DOMAIN, }, config::Configuration, model::{ @@ -36,7 +36,10 @@ use { GetNotificationsParams, GetNotificationsResult, SubscribeResponse, SubscriberAccountAndScopes, WelcomeNotification, }, - types::AccountId, + types::{ + eip155::test_utils::{format_eip155_account, generate_account, generate_eoa}, + AccountId, + }, }, notify_message::NotifyMessage, rate_limit::{self, ClockImpl}, @@ -84,10 +87,7 @@ use { rand::{rngs::StdRng, SeedableRng}, rand_chacha::rand_core::OsRng, relay_rpc::{ - auth::{ - cacao::Cacao, - ed25519_dalek::{Signer, SigningKey, VerifyingKey}, - }, + auth::ed25519_dalek::{Signer, SigningKey, VerifyingKey}, domain::{DecodedClientId, DidKey, MessageId, ProjectId, Topic}, jwt::{JwtBasicClaims, JwtHeader, VerifyableClaims}, rpc::{ @@ -119,17 +119,12 @@ use { tracing_subscriber::fmt::format::FmtSpan, url::Url, utils::{ - generate_account, notify_relay_api::{accept_notify_message, subscribe_with_mjv}, relay_api::{publish_jwt_message, TopicEncrptionScheme}, unregister_identity_key, }, uuid::Uuid, - wiremock::{ - http::Method, - matchers::{method, path, query_param}, - Mock, MockServer, ResponseTemplate, - }, + wiremock::MockServer, x25519_dalek::PublicKey, }; @@ -3490,28 +3485,6 @@ async fn delete_subscription(notify_server: &NotifyServerContext) { assert_eq!(resp.not_found.len(), 1); } -async fn register_mocked_identity_key( - mock_keys_server: &MockServer, - identity_public_key: DecodedClientId, - cacao: Cacao, -) { - Mock::given(method(Method::Get)) - .and(path(KEYS_SERVER_IDENTITY_ENDPOINT)) - .and(query_param( - KEYS_SERVER_IDENTITY_ENDPOINT_PUBLIC_KEY_QUERY, - identity_public_key.to_string(), - )) - .respond_with( - ResponseTemplate::new(StatusCode::OK).set_body_json(KeyServerResponse { - status: KEYS_SERVER_STATUS_SUCCESS.to_owned(), - error: None, - value: Some(CacaoValue { cacao }), - }), - ) - .mount(mock_keys_server) - .await; -} - #[test_context(NotifyServerContext)] #[tokio::test] async fn all_domains_works(notify_server: &NotifyServerContext) { diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs index 78dd6fbc..7648a832 100644 --- a/tests/utils/mod.rs +++ b/tests/utils/mod.rs @@ -1,28 +1,16 @@ use { base64::Engine, chrono::Utc, - k256::ecdsa::SigningKey as EcdsaSigningKey, notify_server::{ - auth::{AuthError, DidWeb, GetSharedClaims, SharedClaims}, + auth::{AuthError, GetSharedClaims, SharedClaims}, error::NotifyServerError, - model::types::{erc55::erc_55_checksum_encode, AccountId}, + model::types::AccountId, notify_message::NotifyMessage, relay_client_helpers::create_http_client, }, - rand_chacha::rand_core::OsRng, relay_client::http::Client, relay_rpc::{ - auth::{ - cacao::{ - self, - header::EIP4361, - signature::{ - eip1271::get_rpc_url::GetRpcUrl, - eip191::{eip191_bytes, EIP191}, - }, - }, - ed25519_dalek::{Signer, SigningKey as Ed25519SigningKey, VerifyingKey}, - }, + auth::ed25519_dalek::{Signer, SigningKey as Ed25519SigningKey, VerifyingKey}, domain::{DecodedClientId, ProjectId, Topic}, jwt::{JwtHeader, JWT_HEADER_ALG, JWT_HEADER_TYP}, rpc::SubscriptionData, @@ -30,8 +18,6 @@ use { reqwest::Response, serde::Serialize, serde_json::json, - sha2::Digest, - sha3::Keccak256, std::{sync::Arc, time::Duration}, tokio::sync::{ broadcast::{error::RecvError, Receiver}, @@ -224,33 +210,6 @@ pub fn verify_jwt(jwt: &str, key: &VerifyingKey) -> Result (EcdsaSigningKey, String) { - let account_signing_key = EcdsaSigningKey::random(&mut OsRng); - let address = &Keccak256::default() - .chain_update( - &account_signing_key - .verifying_key() - .to_encoded_point(false) - .as_bytes()[1..], - ) - .finalize()[12..]; - let address = format!( - "0x{}", - erc_55_checksum_encode(&hex::encode(address)).collect::() - ); - (account_signing_key, address) -} - -pub fn format_eip155_account(chain_id: u32, address: &str) -> AccountId { - AccountId::try_from(format!("eip155:{chain_id}:{address}")).unwrap() -} - -pub fn generate_account() -> (EcdsaSigningKey, AccountId) { - let (account_signing_key, address) = generate_eoa(); - let account = format_eip155_account(1, &address); - (account_signing_key, account) -} - pub fn encode_auth(auth: &T, signing_key: &Ed25519SigningKey) -> String { let data = JwtHeader { typ: JWT_HEADER_TYP, @@ -322,65 +281,3 @@ pub async fn assert_successful_response(response: Response) -> Response { } response } - -#[derive(Clone)] -pub struct IdentityKeyDetails { - pub keys_server_url: Url, - pub signing_key: Ed25519SigningKey, - pub client_id: DecodedClientId, -} - -pub fn generate_identity_key() -> (Ed25519SigningKey, DecodedClientId) { - let signing_key = Ed25519SigningKey::generate(&mut rand::thread_rng()); - let client_id = DecodedClientId::from_key(&signing_key.verifying_key()); - (signing_key, client_id) -} - -pub async fn sign_cacao( - app_domain: &DidWeb, - account: &AccountId, - statement: String, - identity_public_key: DecodedClientId, - keys_server_url: String, - account_signing_key: &EcdsaSigningKey, -) -> cacao::Cacao { - let mut cacao = cacao::Cacao { - h: cacao::header::Header { - t: EIP4361.to_owned(), - }, - p: cacao::payload::Payload { - domain: app_domain.domain().to_owned(), - iss: account.to_did_pkh(), - statement: Some(statement), - aud: identity_public_key.to_did_key(), - version: cacao::Version::V1, - nonce: hex::encode(rand::Rng::gen::<[u8; 10]>(&mut rand::thread_rng())), - iat: Utc::now().to_rfc3339(), - exp: None, - nbf: None, - request_id: None, - resources: Some(vec![keys_server_url]), - }, - s: cacao::signature::Signature { - t: "".to_owned(), - s: "".to_owned(), - }, - }; - let (signature, recovery): (k256::ecdsa::Signature, _) = account_signing_key - .sign_digest_recoverable(Keccak256::new_with_prefix(eip191_bytes( - &cacao.siwe_message().unwrap(), - ))) - .unwrap(); - let cacao_signature = [&signature.to_bytes()[..], &[recovery.to_byte()]].concat(); - cacao.s.t = EIP191.to_owned(); - cacao.s.s = hex::encode(cacao_signature); - cacao.verify(&MockGetRpcUrl).await.unwrap(); - cacao -} - -pub struct MockGetRpcUrl; -impl GetRpcUrl for MockGetRpcUrl { - fn get_rpc_url(&self, _: String) -> Option { - None - } -} diff --git a/tests/utils/notify_relay_api.rs b/tests/utils/notify_relay_api.rs index e2efed28..4d11d0ec 100644 --- a/tests/utils/notify_relay_api.rs +++ b/tests/utils/notify_relay_api.rs @@ -2,7 +2,7 @@ use { super::{ encode_auth, relay_api::{publish_jwt_message, TopicEncrptionScheme, TopicEncryptionSchemeAsymetric}, - IdentityKeyDetails, RelayClient, + RelayClient, }, crate::utils::{ http_api::get_notify_did_json, @@ -11,10 +11,10 @@ use { }, notify_server::{ auth::{ - from_jwt, DidWeb, NotifyServerSubscription, SubscriptionRequestAuth, - SubscriptionResponseAuth, WatchSubscriptionsChangedRequestAuth, - WatchSubscriptionsChangedResponseAuth, WatchSubscriptionsRequestAuth, - WatchSubscriptionsResponseAuth, + from_jwt, test_utils::IdentityKeyDetails, DidWeb, NotifyServerSubscription, + SubscriptionRequestAuth, SubscriptionResponseAuth, + WatchSubscriptionsChangedRequestAuth, WatchSubscriptionsChangedResponseAuth, + WatchSubscriptionsRequestAuth, WatchSubscriptionsResponseAuth, }, model::types::AccountId, notify_message::NotifyMessage, diff --git a/tests/utils/relay_api.rs b/tests/utils/relay_api.rs index 68e55e74..7a422e32 100644 --- a/tests/utils/relay_api.rs +++ b/tests/utils/relay_api.rs @@ -1,10 +1,10 @@ use { - super::{IdentityKeyDetails, RelayClient}, + super::RelayClient, chacha20poly1305::{aead::Aead, ChaCha20Poly1305, KeyInit}, chrono::{DateTime, Utc}, data_encoding::BASE64, notify_server::{ - auth::{add_ttl, from_jwt, GetSharedClaims, SharedClaims}, + auth::{add_ttl, from_jwt, test_utils::IdentityKeyDetails, GetSharedClaims, SharedClaims}, rpc::{derive_key, JsonRpcResponse, ResponseAuth}, types::{Envelope, EnvelopeType0, EnvelopeType1}, utils::topic_from_key,