use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64;
use base64::Engine as _;
use hmac::{Hmac, Mac};
use rainier_support::{Error, Result};
use sha2::Sha256;
use subtle::ConstantTimeEq;
use crate::key::{Key, KeyRing};
type HmacSha256 = Hmac<Sha256>;
pub trait Signer: Send + Sync + 'static {
fn sign(&self, value: &str) -> Result<String>;
fn verify(&self, signed: &str) -> Result<String>;
}
pub trait SignerExt: Signer {
fn is_valid(&self, signed: &str) -> bool {
self.verify(signed).is_ok()
}
}
impl<S: Signer + ?Sized> SignerExt for S {}
pub struct HmacSigner {
keys: KeyRing,
}
impl HmacSigner {
pub fn new(keys: KeyRing) -> Self {
Self { keys }
}
pub fn keys(&self) -> &KeyRing {
&self.keys
}
pub fn detached_tag(&self, value: &str) -> Result<String> {
let key = self.keys.current();
Ok(format!("{}.{}", key.id(), B64.encode(Self::tag(key, value)?)))
}
pub fn verify_detached(&self, value: &str, tag: &str) -> bool {
let Some((key_id, tag)) = tag.split_once('.') else {
return false;
};
let Some(key) = self.keys.find(key_id) else {
return false;
};
let (Ok(expected), Ok(presented)) = (Self::tag(key, value), B64.decode(tag)) else {
return false;
};
presented.ct_eq(&expected).into()
}
fn tag(key: &Key, value: &str) -> Result<Vec<u8>> {
let mut mac = HmacSha256::new_from_slice(key.bytes())
.map_err(|_| Error::internal("the key is the wrong length for HMAC-SHA256"))?;
mac.update(key.id().as_bytes());
mac.update(b".");
mac.update(value.as_bytes());
Ok(mac.finalize().into_bytes().to_vec())
}
}
impl Signer for HmacSigner {
fn sign(&self, value: &str) -> Result<String> {
if value.contains('.') {
return Err(Error::internal(
"a signed value must not contain `.` — encode it first (base64, or JSON \
through the encrypter)",
));
}
let key = self.keys.current();
Ok(format!("{value}.{}.{}", key.id(), B64.encode(Self::tag(key, value)?)))
}
fn verify(&self, signed: &str) -> Result<String> {
let invalid = || Error::bad_request("the signature is not valid");
let mut parts = signed.split('.');
let (Some(value), Some(key_id), Some(tag), None) =
(parts.next(), parts.next(), parts.next(), parts.next())
else {
return Err(invalid());
};
let key = self.keys.find(key_id).ok_or_else(invalid)?;
let expected = Self::tag(key, value)?;
let presented = B64.decode(tag).map_err(|_| invalid())?;
if presented.ct_eq(&expected).into() {
Ok(value.to_string())
} else {
Err(invalid())
}
}
}
impl std::fmt::Debug for HmacSigner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HmacSigner").field("keys", &self.keys.ids()).finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn signer() -> HmacSigner {
HmacSigner::new(KeyRing::new(Key::generate()))
}
#[test]
fn a_value_round_trips() {
let signer = signer();
let signed = signer.sign("user-42").unwrap();
assert_eq!(signer.verify(&signed).unwrap(), "user-42");
}
#[test]
fn the_value_stays_readable() {
assert!(signer().sign("unsubscribe-42").unwrap().starts_with("unsubscribe-42."));
}
#[test]
fn altering_the_value_invalidates_it() {
let signer = signer();
let signed = signer.sign("user-42").unwrap();
let forged = signed.replacen("user-42", "user-43", 1);
assert!(signer.verify(&forged).is_err());
}
#[test]
fn altering_the_tag_invalidates_it() {
let signer = signer();
let signed = signer.sign("user-42").unwrap();
let mut parts: Vec<&str> = signed.split('.').collect();
let other = B64.encode([0u8; 32]);
parts[2] = &other;
assert!(signer.verify(&parts.join(".")).is_err());
}
#[test]
fn another_key_cannot_verify_it() {
let signed = signer().sign("user-42").unwrap();
assert!(signer().verify(&signed).is_err());
}
#[test]
fn a_retired_key_still_verifies_what_it_signed() {
let old = Key::generate();
let signed = HmacSigner::new(KeyRing::new(old.clone())).sign("user-42").unwrap();
let rotated = HmacSigner::new(KeyRing::new(Key::generate()).with_previous(old));
assert_eq!(rotated.verify(&signed).unwrap(), "user-42");
}
#[test]
fn a_signature_cannot_be_replayed_under_another_key_id() {
let old = Key::generate();
let signer = HmacSigner::new(KeyRing::new(Key::generate()).with_previous(old.clone()));
let signed = signer.sign("user-42").unwrap();
let mut parts: Vec<&str> = signed.split('.').collect();
parts[1] = old.id();
assert!(signer.verify(&parts.join(".")).is_err(), "the id is folded into the tag");
}
#[test]
fn a_malformed_signature_is_a_400() {
let signer = signer();
for rubbish in ["", "nope", "a.b", "a.b.c.d"] {
assert_eq!(signer.verify(rubbish).unwrap_err().status(), 400, "{rubbish}");
}
}
#[test]
fn a_value_containing_a_dot_is_refused_rather_than_mangled() {
let err = signer().sign("a.b").unwrap_err();
assert!(err.message().contains("encode it first"), "{}", err.message());
}
#[test]
fn is_valid_answers_without_the_value() {
let signer = signer();
let signed = signer.sign("x").unwrap();
assert!(signer.is_valid(&signed));
assert!(!signer.is_valid("x.y.z"));
}
#[test]
fn the_port_is_object_safe() {
let signer: std::sync::Arc<dyn Signer> = std::sync::Arc::new(signer());
assert_eq!(signer.verify(&signer.sign("via dyn").unwrap()).unwrap(), "via dyn");
}
}