feat(crypto): add RSA-SHA-256 signature verification (#9080)

This commit is contained in:
Sakil Mostak
2025-09-03 19:23:06 +05:30
committed by GitHub
parent 0543e51555
commit 3fcbe40e5c
4 changed files with 135 additions and 0 deletions

2
Cargo.lock generated
View File

@ -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",

View File

@ -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"] }

View File

@ -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<bool, errors::CryptoError> {
// 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::<rsa::sha2::Sha256>::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::<rsa::sha2::Sha256>::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);
}
}

View File

@ -758,6 +758,50 @@ impl ConnectorIntegration<RSync, RefundsData, RefundsResponseData> for Wise {}
#[async_trait::async_trait]
impl IncomingWebhook for Wise {
fn get_webhook_source_verification_algorithm(
&self,
_request: &IncomingWebhookRequestDetails<'_>,
) -> CustomResult<Box<dyn common_utils::crypto::VerifySignature + Send>, 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<Vec<u8>, 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<Vec<u8>, 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<'_>,