1pub mod hmac {
12 use ::hmac::{Hmac, Mac};
19 use sha2::Sha256;
20
21 pub fn hmac_sign(secret: &[u8], kid: &str, message: &[u8]) -> String {
36 let mut mac = <Hmac<Sha256> as Mac>::new_from_slice(secret)
37 .expect("HMAC-SHA256 accepts any key length");
38 mac.update(kid.as_bytes());
39 mac.update(b":");
40 mac.update(message);
41 let out = mac.finalize().into_bytes();
42 format!("{kid}:{}", hex::encode(out))
43 }
44
45 pub fn hmac_verify(
48 secret: &[u8],
49 kid: &str,
50 message: &[u8],
51 token: &str,
52 ) -> Result<(), HmacVerifyError> {
53 let (tok_kid, tok_hex) =
54 token.split_once(':').ok_or(HmacVerifyError::Malformed)?;
55 if tok_kid != kid {
56 return Err(HmacVerifyError::WrongKid {
57 expected: kid.to_owned(),
58 actual: tok_kid.to_owned(),
59 });
60 }
61 let expected = hex::decode(tok_hex).map_err(|_| HmacVerifyError::Malformed)?;
62 let mut mac = <Hmac<Sha256> as Mac>::new_from_slice(secret)
63 .map_err(|_| HmacVerifyError::Malformed)?;
64 mac.update(kid.as_bytes());
65 mac.update(b":");
66 mac.update(message);
67 mac.verify_slice(&expected)
68 .map_err(|_| HmacVerifyError::SignatureMismatch)
69 }
70
71 #[derive(Debug, thiserror::Error)]
74 pub enum HmacVerifyError {
75 #[error("token malformed; expected kid:hex shape")]
76 Malformed,
77 #[error("token kid mismatch; expected {expected}, got {actual}")]
78 WrongKid { expected: String, actual: String },
79 #[error("HMAC signature mismatch")]
80 SignatureMismatch,
81 }
82
83 #[cfg(test)]
84 mod tests {
85 use super::*;
86
87 #[test]
88 fn sign_then_verify_round_trip() {
89 let secret = b"super-secret-key";
90 let tok = hmac_sign(secret, "kid1", b"exec-id:wp-id");
91 assert!(tok.starts_with("kid1:"));
92 hmac_verify(secret, "kid1", b"exec-id:wp-id", &tok).expect("verify ok");
93 }
94
95 #[test]
96 fn verify_rejects_tampered_message() {
97 let secret = b"s";
98 let tok = hmac_sign(secret, "k", b"msg");
99 let err = hmac_verify(secret, "k", b"tampered", &tok).unwrap_err();
100 assert!(matches!(err, HmacVerifyError::SignatureMismatch));
101 }
102
103 #[test]
104 fn verify_rejects_wrong_kid() {
105 let secret = b"s";
106 let tok = hmac_sign(secret, "k1", b"msg");
107 let err = hmac_verify(secret, "k2", b"msg", &tok).unwrap_err();
108 assert!(matches!(err, HmacVerifyError::WrongKid { .. }));
109 }
110
111 #[test]
112 fn verify_rejects_malformed() {
113 assert!(matches!(
114 hmac_verify(b"s", "k", b"msg", "no-colon-token"),
115 Err(HmacVerifyError::Malformed)
116 ));
117 assert!(matches!(
118 hmac_verify(b"s", "k", b"msg", "k:not-hex-zzzz"),
119 Err(HmacVerifyError::Malformed)
120 ));
121 }
122
123 #[test]
124 fn sign_is_deterministic() {
125 let a = hmac_sign(b"k", "kid", b"msg");
127 let b = hmac_sign(b"k", "kid", b"msg");
128 assert_eq!(a, b);
129 }
130 }
131}