use hmac::{KeyInit, Mac, SimpleHmac};
use sha2::Sha256;
use crate::core::Secret;
fn hmac_sha256(key: &[u8], message: &[u8]) -> [u8; 32] {
let mut mac = <SimpleHmac<Sha256> as KeyInit>::new_from_slice(key)
.expect("HMAC accepts a key of any length");
mac.update(message);
mac.finalize().into_bytes().into()
}
#[derive(Debug, Clone)]
pub struct BodySigning {
header: reqwest::header::HeaderName,
secret: Secret,
}
impl BodySigning {
#[must_use]
pub fn new(header: impl Into<String>, secret: Secret) -> Self {
let header = header.into();
let name = reqwest::header::HeaderName::try_from(header.as_str()).unwrap_or_else(|_| {
panic!(
"push body-signing header '{header}' is not an HTTP field name, so \
every delivery to this destination would fail at the POST"
)
});
assert!(
!secret.is_empty(),
"a push body-signing secret is empty: the receiver would verify a MAC \
anybody can compute, and a check that always passes reads exactly \
like one that means something"
);
Self {
header: name,
secret,
}
}
#[must_use]
pub fn header(&self) -> &str {
self.header.as_str()
}
pub(super) const fn header_name(&self) -> &reqwest::header::HeaderName {
&self.header
}
pub(super) fn value_for(&self, body: &[u8]) -> String {
let mac = hmac_sha256(self.secret.expose().as_bytes(), body);
format!("sha256={}", hex::encode(mac))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_construction_matches_rfc_4231() {
assert_eq!(
hex::encode(hmac_sha256(&[0x0b; 20], b"Hi There")),
"b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7",
"RFC 4231 test case 1"
);
assert_eq!(
hex::encode(hmac_sha256(b"Jefe", b"what do ya want for nothing?")),
"5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843",
"RFC 4231 test case 2"
);
assert_eq!(
hex::encode(hmac_sha256(
&[0xaa; 131],
b"Test Using Larger Than Block-Size Key - Hash Key First"
)),
"60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54",
"RFC 4231 test case 6: a key longer than the block must be hashed first"
);
}
#[test]
fn the_signature_follows_the_body() {
let signing = BodySigning::new("X-Mako-Signature", Secret::new("shared"));
let value = signing.value_for(br#"{"a":1}"#);
assert!(
value.starts_with("sha256="),
"the algorithm prefix is how a receiver dispatches: {value}"
);
assert_eq!(value.len(), "sha256=".len() + 64, "{value}");
assert!(
value[7..]
.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_uppercase()),
"lowercase hex is the convention receivers compare against: {value}"
);
assert_ne!(
value,
signing.value_for(br#"{"a":2}"#),
"one byte of the body changed and the signature did not"
);
assert_ne!(
value,
BodySigning::new("X-Mako-Signature", Secret::new("other")).value_for(br#"{"a":1}"#),
"a different secret produced the same signature"
);
}
#[test]
fn a_signing_secret_is_redacted() {
let signing = BodySigning::new("X-Mako-Signature", Secret::new("sk-live-abcdef"));
let shown = format!("{signing:#?}");
assert!(!shown.contains("abcdef"), "{shown}");
}
#[test]
#[should_panic(expected = "is not an HTTP field name")]
fn a_header_name_that_cannot_be_sent_is_refused_at_configuration() {
let _ = BodySigning::new("X Mako Signature", Secret::new("shared"));
}
#[test]
#[should_panic(expected = "is empty")]
fn an_empty_signing_secret_is_refused() {
let _ = BodySigning::new("X-Mako-Signature", Secret::new(""));
}
}