use std::time::{SystemTime, UNIX_EPOCH};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use subtle::ConstantTimeEq;
type HmacSha256 = Hmac<Sha256>;
pub const DEFAULT_TOLERANCE_SECONDS: i64 = 300;
pub fn verify_rerout_signature(
raw_body: &str,
signature_header: &str,
secret: &str,
tolerance_seconds: i64,
now: Option<i64>,
) -> bool {
if signature_header.is_empty() || secret.is_empty() {
return false;
}
let parsed = match parse_signature_header(signature_header) {
Some(parts) => parts,
None => return false,
};
if tolerance_seconds > 0 {
let now_seconds = now.unwrap_or_else(current_unix_seconds);
let drift = now_seconds.saturating_sub(parsed.timestamp).abs();
if drift > tolerance_seconds {
return false;
}
}
let mut mac = match HmacSha256::new_from_slice(secret.as_bytes()) {
Ok(m) => m,
Err(_) => return false,
};
let signed_payload = format!("{}.{}", parsed.timestamp, raw_body);
mac.update(signed_payload.as_bytes());
let expected = mac.finalize().into_bytes();
let actual = match hex_decode(&parsed.v1) {
Some(bytes) => bytes,
None => return false,
};
if expected.len() != actual.len() {
return false;
}
expected.ct_eq(&actual).into()
}
struct ParsedSignature {
timestamp: i64,
v1: String,
}
fn parse_signature_header(header: &str) -> Option<ParsedSignature> {
let mut timestamp: Option<i64> = None;
let mut v1: Option<String> = None;
for segment in header.split(',') {
let Some(eq) = segment.find('=') else {
continue;
};
if eq == 0 {
continue;
}
let key = segment[..eq].trim().to_ascii_lowercase();
let value = segment[eq + 1..].trim();
if value.is_empty() {
continue;
}
match key.as_str() {
"t" => {
if let Ok(parsed) = value.parse::<i64>()
&& parsed > 0
{
timestamp = Some(parsed);
}
}
"v1" => {
v1 = Some(value.to_string());
}
_ => {}
}
}
Some(ParsedSignature {
timestamp: timestamp?,
v1: v1?,
})
}
fn hex_decode(s: &str) -> Option<Vec<u8>> {
if s.is_empty() || s.len() % 2 != 0 {
return None;
}
if !s.bytes().all(|b| b.is_ascii_hexdigit()) {
return None;
}
hex::decode(s).ok()
}
fn current_unix_seconds() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}