use hmac::{Hmac, Mac};
use sha2::Sha256;
use time::format_description::well_known::Rfc3339;
use time::OffsetDateTime;
use crate::message::{hex_decode, hex_encode, SesameError};
type HmacSha256 = Hmac<Sha256>;
pub fn sign(key: &[u8], canonical: &str) -> String {
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts any key length");
mac.update(canonical.as_bytes());
hex_encode(&mac.finalize().into_bytes())
}
pub fn verify(key: &[u8], canonical: &str, provided_hex: &str) -> Result<(), SesameError> {
let provided = hex_decode(provided_hex).ok_or(SesameError::SignatureMismatch)?;
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts any key length");
mac.update(canonical.as_bytes());
mac.verify_slice(&provided)
.map_err(|_| SesameError::SignatureMismatch)
}
pub fn verify_any(
keys: &[Vec<u8>],
canonical: &str,
provided_hex: &str,
) -> Result<(), SesameError> {
let provided = match hex_decode(provided_hex) {
Some(p) => p,
None => return Err(SesameError::SignatureMismatch),
};
let mut ok = false;
for key in keys {
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts any key length");
mac.update(canonical.as_bytes());
if mac.verify_slice(&provided).is_ok() {
ok = true;
}
}
if ok {
Ok(())
} else {
Err(SesameError::SignatureMismatch)
}
}
pub fn check_freshness(
timestamp_iso: &str,
now: OffsetDateTime,
window_secs: i64,
) -> Result<(), SesameError> {
let ts = OffsetDateTime::parse(timestamp_iso, &Rfc3339)
.map_err(|_| SesameError::ExpiredTimestamp)?;
let delta = now.unix_timestamp() - ts.unix_timestamp();
if delta.abs() <= window_secs {
Ok(())
} else {
Err(SesameError::ExpiredTimestamp)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sign_then_verify_roundtrip() {
let key = b"shared-secret";
let sig = sign(key, "canonical-string");
assert_eq!(sig.len(), 64); assert!(verify(key, "canonical-string", &sig).is_ok());
}
#[test]
fn wrong_key_rejected() {
let sig = sign(b"k1", "c");
assert_eq!(
verify(b"k2", "c", &sig),
Err(SesameError::SignatureMismatch)
);
}
#[test]
fn tampered_canonical_rejected() {
let key = b"k";
let sig = sign(key, "original");
assert_eq!(
verify(key, "tampered", &sig),
Err(SesameError::SignatureMismatch)
);
}
#[test]
fn verify_any_accepts_old_or_new_key() {
let old = b"old-key".to_vec();
let new = b"new-key".to_vec();
let sig_old = sign(&old, "c");
assert!(verify_any(&[old.clone(), new.clone()], "c", &sig_old).is_ok());
let sig_new = sign(&new, "c");
assert!(verify_any(&[old, new], "c", &sig_new).is_ok());
}
#[test]
fn rfc2104_known_answer() {
let sig = sign(b"Jefe", "what do ya want for nothing?");
assert_eq!(
sig,
"5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"
);
}
#[test]
fn freshness_window() {
let now = OffsetDateTime::parse("2026-02-24T18:05:00Z", &Rfc3339).unwrap();
assert!(check_freshness("2026-02-24T18:00:00Z", now, 300).is_ok());
assert_eq!(
check_freshness("2026-02-24T17:59:59Z", now, 300),
Err(SesameError::ExpiredTimestamp)
);
assert_eq!(
check_freshness("2026-02-24T18:10:01Z", now, 300),
Err(SesameError::ExpiredTimestamp)
);
}
#[test]
fn unparseable_timestamp_rejected() {
let now = OffsetDateTime::parse("2026-02-24T18:00:00Z", &Rfc3339).unwrap();
assert_eq!(
check_freshness("not-a-date", now, 300),
Err(SesameError::ExpiredTimestamp)
);
}
}