use std::time::{SystemTime, UNIX_EPOCH};
use hmac::{Hmac, Mac};
use sha2::Sha256;
pub fn verify_signature(
secret_key: &str,
body: &str,
timestamp: &str,
signature: &str,
max_age_secs: Option<u64>,
) -> bool {
if let Some(max_age) = max_age_secs {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let ts = match timestamp.parse::<u64>() {
Ok(v) => v,
Err(_) => return false,
};
if now > ts && now - ts > max_age {
return false;
}
if ts > now && ts - now > max_age {
return false;
}
}
let mut mac = match Hmac::<Sha256>::new_from_slice(secret_key.as_bytes()) {
Ok(m) => m,
Err(_) => return false,
};
mac.update(body.as_bytes());
mac.update(timestamp.as_bytes());
match hex::decode(signature) {
Ok(sig_bytes) => mac.verify_slice(&sig_bytes).is_ok(),
Err(_) => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_verify_valid_signature() {
let secret_key = "my-secret-key";
let body = r#"{"event":"sms:received","payload":{"message":"Hello"}}"#;
let timestamp = "1700000000";
let mut mac = Hmac::<Sha256>::new_from_slice(secret_key.as_bytes()).unwrap();
let message = format!("{}{}", body, timestamp);
mac.update(message.as_bytes());
let signature = hex::encode(mac.finalize().into_bytes());
assert!(verify_signature(
secret_key, body, timestamp, &signature, None
));
}
#[test]
fn test_verify_invalid_signature() {
let secret_key = "my-secret-key";
let body = r#"{"event":"sms:received","payload":{"message":"Hello"}}"#;
let timestamp = "1700000000";
assert!(!verify_signature(
secret_key, body, timestamp, "deadbeef", None
));
}
#[test]
fn test_verify_wrong_key() {
let secret_key = "my-secret-key";
let body = r#"{"event":"sms:received"}"#;
let timestamp = "1700000000";
let mut mac = Hmac::<Sha256>::new_from_slice(secret_key.as_bytes()).unwrap();
let message = format!("{}{}", body, timestamp);
mac.update(message.as_bytes());
let signature = hex::encode(mac.finalize().into_bytes());
assert!(!verify_signature(
"wrong-key",
body,
timestamp,
&signature,
None
));
}
#[test]
fn test_verify_wrong_timestamp() {
let secret_key = "my-secret-key";
let body = r#"{"event":"sms:received"}"#;
let mut mac = Hmac::<Sha256>::new_from_slice(secret_key.as_bytes()).unwrap();
let message = format!("{}{}", body, "1700000000");
mac.update(message.as_bytes());
let signature = hex::encode(mac.finalize().into_bytes());
assert!(!verify_signature(
secret_key,
body,
"1700000001",
&signature,
None
));
}
#[test]
fn test_verify_empty_values() {
assert!(!verify_signature("", "", "", "", None));
assert!(!verify_signature("key", "", "", "sig", None));
}
#[test]
fn test_verify_replay_detection() {
let secret_key = "test-key";
let body = "payload";
let old_timestamp = "1000";
let new_timestamp = "2000";
let mut mac = Hmac::<Sha256>::new_from_slice(secret_key.as_bytes()).unwrap();
let message = format!("{}{}", body, old_timestamp);
mac.update(message.as_bytes());
let old_sig = hex::encode(mac.finalize().into_bytes());
assert!(verify_signature(
secret_key,
body,
old_timestamp,
&old_sig,
None
));
assert!(!verify_signature(
secret_key,
body,
new_timestamp,
&old_sig,
None
));
}
#[test]
fn test_verify_max_age_rejects_old_timestamp() {
let secret_key = "test-key";
let body = "payload";
let old_timestamp = "1000";
let mut mac = Hmac::<Sha256>::new_from_slice(secret_key.as_bytes()).unwrap();
let message = format!("{}{}", body, old_timestamp);
mac.update(message.as_bytes());
let sig = hex::encode(mac.finalize().into_bytes());
assert!(!verify_signature(
secret_key,
body,
old_timestamp,
&sig,
Some(300)
));
}
#[test]
fn test_verify_max_age_none_skips_check() {
let secret_key = "test-key";
let body = "payload";
let timestamp = "1000";
let mut mac = Hmac::<Sha256>::new_from_slice(secret_key.as_bytes()).unwrap();
let message = format!("{}{}", body, timestamp);
mac.update(message.as_bytes());
let sig = hex::encode(mac.finalize().into_bytes());
assert!(verify_signature(secret_key, body, timestamp, &sig, None));
}
}