diff --git a/Cargo.lock b/Cargo.lock index 47bbd2c1b1..ebc41ede42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1904,6 +1904,7 @@ dependencies = [ "reqwest 0.11.27", "ring 0.17.14", "router_env", + "rsa", "rust_decimal", "rustc-hash 1.1.0", "semver 1.0.26", @@ -6973,6 +6974,7 @@ dependencies = [ "pkcs1", "pkcs8 0.10.2", "rand_core 0.6.4", + "sha2", "signature 2.2.0", "spki 0.7.3", "subtle", diff --git a/crates/common_utils/Cargo.toml b/crates/common_utils/Cargo.toml index 75fc14b01f..d61b91c513 100644 --- a/crates/common_utils/Cargo.toml +++ b/crates/common_utils/Cargo.toml @@ -47,6 +47,7 @@ rand = "0.8.5" regex = "1.11.1" reqwest = { version = "0.11.27", features = ["json", "native-tls", "gzip", "multipart"] } ring = { version = "0.17.14", features = ["std", "wasm32_unknown_unknown_js"] } +rsa = {version = "0.9.8", features = ["sha2"]} rust_decimal = "1.37" rustc-hash = "1.1.0" semver = { version = "1.0.26", features = ["serde"] } diff --git a/crates/common_utils/src/crypto.rs b/crates/common_utils/src/crypto.rs index 2672fcd1c7..c76777eb05 100644 --- a/crates/common_utils/src/crypto.rs +++ b/crates/common_utils/src/crypto.rs @@ -1,6 +1,7 @@ //! Utilities for cryptographic algorithms use std::ops::Deref; +use base64::Engine; use error_stack::ResultExt; use masking::{ExposeInterface, Secret}; use md5; @@ -12,8 +13,10 @@ use ring::{ }; #[cfg(feature = "logs")] use router_env::logger; +use rsa::{pkcs8::DecodePublicKey, signature::Verifier}; use crate::{ + consts::BASE64_ENGINE, errors::{self, CustomResult}, pii::{self, EncryptionStrategy}, }; @@ -501,6 +504,52 @@ impl VerifySignature for Sha256 { } } +/// Secure Hash Algorithm 256 with RSA public-key cryptosystem +#[derive(Debug)] +pub struct RsaSha256; + +impl VerifySignature for RsaSha256 { + fn verify_signature( + &self, + secret: &[u8], + signature: &[u8], + msg: &[u8], + ) -> CustomResult { + // create verifying key + let decoded_public_key = BASE64_ENGINE + .decode(secret) + .change_context(errors::CryptoError::SignatureVerificationFailed) + .attach_printable("base64 decoding failed")?; + + let string_public_key = String::from_utf8(decoded_public_key) + .change_context(errors::CryptoError::SignatureVerificationFailed) + .attach_printable("utf8 to string parsing failed")?; + + let rsa_public_key = rsa::RsaPublicKey::from_public_key_pem(&string_public_key) + .change_context(errors::CryptoError::SignatureVerificationFailed) + .attach_printable("rsa public key transformation failed")?; + + let verifying_key = rsa::pkcs1v15::VerifyingKey::::new(rsa_public_key); + + // transfrom the signature + let decoded_signature = BASE64_ENGINE + .decode(signature) + .change_context(errors::CryptoError::SignatureVerificationFailed) + .attach_printable("base64 decoding failed")?; + + let rsa_signature = rsa::pkcs1v15::Signature::try_from(&decoded_signature[..]) + .change_context(errors::CryptoError::SignatureVerificationFailed) + .attach_printable("rsa signature transformation failed")?; + + // signature verification + verifying_key + .verify(msg, &rsa_signature) + .map(|_| true) + .change_context(errors::CryptoError::SignatureVerificationFailed) + .attach_printable("signature verification step failed") + } +} + /// TripleDesEde3 hash function #[derive(Debug)] #[cfg(feature = "crypto_openssl")] @@ -951,4 +1000,43 @@ mod crypto_tests { "Wrong signature should not verify" ); } + + #[test] + fn test_rsasha256_verify_signature() { + use base64::Engine; + use rand::rngs::OsRng; + use rsa::{ + pkcs8::EncodePublicKey, + signature::{RandomizedSigner, SignatureEncoding}, + }; + + use crate::consts::BASE64_ENGINE; + + let mut rng = OsRng; + + let bits = 2048; + let private_key = rsa::RsaPrivateKey::new(&mut rng, bits).expect("keygen failed"); + let signing_key = rsa::pkcs1v15::SigningKey::::new(private_key.clone()); + + let message = "{ This is a test message :) }".as_bytes(); + + let signature = signing_key.sign_with_rng(&mut rng, message); + let encoded_signature = BASE64_ENGINE.encode(signature.to_bytes()); + + let rsa_public_key = private_key.to_public_key(); + let pem_format_public_key = rsa_public_key + .to_public_key_pem(rsa::pkcs8::LineEnding::LF) + .expect("transformation failed"); + let encoded_pub_key = BASE64_ENGINE.encode(&pem_format_public_key[..]); + + let right_verified = super::RsaSha256 + .verify_signature( + encoded_pub_key.as_bytes(), + encoded_signature.as_bytes(), + message, + ) + .expect("Right signature verification result"); + + assert!(right_verified); + } } diff --git a/crates/hyperswitch_connectors/src/connectors/wise.rs b/crates/hyperswitch_connectors/src/connectors/wise.rs index 68f1b0536c..e3c2e8f5ad 100644 --- a/crates/hyperswitch_connectors/src/connectors/wise.rs +++ b/crates/hyperswitch_connectors/src/connectors/wise.rs @@ -758,6 +758,50 @@ impl ConnectorIntegration for Wise {} #[async_trait::async_trait] impl IncomingWebhook for Wise { + fn get_webhook_source_verification_algorithm( + &self, + _request: &IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, ConnectorError> { + Ok(Box::new(common_utils::crypto::RsaSha256)) + } + + fn get_webhook_source_verification_signature( + &self, + #[cfg(feature = "payouts")] request: &IncomingWebhookRequestDetails<'_>, + #[cfg(not(feature = "payouts"))] _request: &IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, + ) -> CustomResult, ConnectorError> { + #[cfg(feature = "payouts")] + { + request + .headers + .get("X-Signature-SHA256") + .map(|header_value| header_value.as_bytes().to_vec()) + .ok_or(ConnectorError::WebhookSignatureNotFound.into()) + } + #[cfg(not(feature = "payouts"))] + { + Err(report!(ConnectorError::WebhooksNotImplemented)) + } + } + + fn get_webhook_source_verification_message( + &self, + #[cfg(feature = "payouts")] request: &IncomingWebhookRequestDetails<'_>, + #[cfg(not(feature = "payouts"))] _request: &IncomingWebhookRequestDetails<'_>, + _merchant_id: &common_utils::id_type::MerchantId, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, + ) -> CustomResult, ConnectorError> { + #[cfg(feature = "payouts")] + { + Ok(request.body.to_vec()) + } + #[cfg(not(feature = "payouts"))] + { + Err(report!(ConnectorError::WebhooksNotImplemented)) + } + } + fn get_webhook_object_reference_id( &self, #[cfg(feature = "payouts")] request: &IncomingWebhookRequestDetails<'_>,