use crate::auth::SecretProvider;
use crate::error::ValidationError;
use std::sync::Arc;
#[derive(Clone)]
pub struct SignatureValidator {
secrets: Arc<dyn SecretProvider>,
}
impl SignatureValidator {
pub fn new(secrets: Arc<dyn SecretProvider>) -> Self {
Self { secrets }
}
pub async fn validate(&self, payload: &[u8], signature: &str) -> Result<bool, ValidationError> {
let signature_bytes = self.parse_signature(signature)?;
let secret = self.secrets.get_webhook_secret().await.map_err(|e| {
ValidationError::InvalidSignatureFormat {
message: format!("Failed to retrieve webhook secret: {}", e),
}
})?;
let expected_hmac = self.compute_hmac(payload, &secret)?;
let is_valid = self.constant_time_compare(&signature_bytes, &expected_hmac);
Ok(is_valid)
}
fn parse_signature(&self, signature: &str) -> Result<Vec<u8>, ValidationError> {
const PREFIX: &str = "sha256=";
if !signature.starts_with(PREFIX) {
return Err(ValidationError::InvalidSignatureFormat {
message: format!(
"Signature must start with '{}', got: '{}'",
PREFIX,
signature.chars().take(10).collect::<String>()
),
});
}
let hex_signature = &signature[PREFIX.len()..];
hex::decode(hex_signature).map_err(|e| ValidationError::InvalidSignatureFormat {
message: format!("Invalid hex encoding in signature: {}", e),
})
}
fn compute_hmac(&self, payload: &[u8], secret: &str) -> Result<Vec<u8>, ValidationError> {
use hmac::{Hmac, KeyInit, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).map_err(|e| {
ValidationError::HmacError {
message: format!("Failed to create HMAC instance: {}", e),
}
})?;
mac.update(payload);
Ok(mac.finalize().into_bytes().to_vec())
}
fn constant_time_compare(&self, a: &[u8], b: &[u8]) -> bool {
use subtle::ConstantTimeEq;
if a.len() != b.len() {
return false;
}
a.ct_eq(b).into()
}
}
impl std::fmt::Debug for SignatureValidator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SignatureValidator")
.field("secrets", &"<REDACTED>")
.finish()
}
}
#[cfg(test)]
#[path = "validation_tests.rs"]
mod tests;