use crate::utils::crypto::is_sha256_checksum;
use subtle::ConstantTimeEq;
#[derive(Debug)]
pub struct Signature(Box<str>);
impl Signature {
#[must_use]
pub fn from_hex(value: &str) -> Option<Self> {
is_sha256_checksum(value).then(|| Self(value.into()))
}
#[must_use]
pub fn from_base64(value: &str) -> Option<Self> {
if value.len() != 28 {
return None;
}
base64_simd::STANDARD.check(value.as_bytes()).ok()?;
(base64_simd::STANDARD.decoded_length(value.as_bytes()).ok()? == 20).then(|| Self(value.into()))
}
#[must_use]
pub(crate) fn from_computed(value: String) -> Self {
debug_assert!(
Self::from_hex(&value).is_some() || Self::from_base64(&value).is_some(),
"internal error: computed signature is not canonical"
);
Self(value.into_boxed_str())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn compare(left: &Self, right: &Self) -> bool {
bool::from(left.ct_eq(right))
}
}
impl ConstantTimeEq for Signature {
fn ct_eq(&self, other: &Self) -> subtle::Choice {
self.0.as_bytes().ct_eq(other.0.as_bytes())
}
}
#[cfg(test)]
mod tests {
use super::*;
const HEX_64: &str = "aeeed9bbccd4d02ee5c0109b86d86835f995330da4c265957d157751f604d404";
const B64_20: &str = "1No4mq5ETf02z8aet9voy6gui6E=";
const B64_20_PLUS: &str = "++++AAAAAAAAAAAAAAAAAAAAAAA=";
#[test]
fn from_hex_accepts_canonical() {
let sig = Signature::from_hex(HEX_64).expect("canonical hex should parse");
assert_eq!(sig.as_str(), HEX_64);
}
#[test]
fn from_hex_rejects_uppercase() {
assert!(Signature::from_hex(&HEX_64.to_uppercase()).is_none());
}
#[test]
fn from_hex_rejects_wrong_length() {
assert!(Signature::from_hex(&HEX_64[..63]).is_none());
}
#[test]
fn from_hex_rejects_non_hex_chars() {
assert!(Signature::from_hex(&format!("z{}", &HEX_64[1..])).is_none());
}
#[test]
fn from_base64_accepts_canonical() {
let sig = Signature::from_base64(B64_20).expect("canonical base64 should parse");
assert_eq!(sig.as_str(), B64_20);
}
#[test]
fn from_base64_accepts_plus_and_slash() {
let sig = Signature::from_base64(B64_20_PLUS).expect("base64 with '+' should parse");
assert_eq!(sig.as_str(), B64_20_PLUS);
}
#[test]
fn from_base64_rejects_non_base64_alphabet() {
assert!(Signature::from_base64("!!!!!!!!!!!!!!!!!!!!!!!=").is_none());
}
#[test]
fn from_base64_rejects_missing_padding() {
assert!(Signature::from_base64(&B64_20[..27]).is_none());
}
#[test]
fn from_base64_rejects_non_20_byte_payload() {
assert!(Signature::from_base64("AAAAAAAAAAAAAAAAAAAAAAAA").is_none());
assert!(Signature::from_base64("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA").is_none());
assert!(Signature::from_base64("AAAAAAAAAAAAAAAAAAAAAAAAAA==").is_none());
}
#[test]
fn from_computed_round_trip() {
let sig = Signature::from_computed(HEX_64.to_owned());
assert_eq!(sig.as_str(), HEX_64);
}
#[test]
fn ct_eq_matches_equal_values() {
let a = Signature::from_hex(HEX_64).unwrap();
let b = Signature::from_hex(HEX_64).unwrap();
assert!(bool::from(a.ct_eq(&b)));
}
#[test]
fn ct_eq_reports_mismatch() {
let a = Signature::from_hex(HEX_64).unwrap();
let b = Signature::from_hex(&format!("0{}", &HEX_64[1..])).unwrap();
assert!(!bool::from(a.ct_eq(&b)));
}
#[test]
fn ct_eq_cross_encoding_is_always_unequal() {
let hex_sig = Signature::from_hex(HEX_64).unwrap();
let b64_sig = Signature::from_base64(B64_20).unwrap();
assert!(!bool::from(hex_sig.ct_eq(&b64_sig)));
}
#[test]
fn compare_reports_match_and_mismatch() {
let a = Signature::from_hex(HEX_64).unwrap();
let same = Signature::from_hex(HEX_64).unwrap();
let different = Signature::from_hex(&format!("0{}", &HEX_64[1..])).unwrap();
let b64 = Signature::from_base64(B64_20).unwrap();
assert!(Signature::compare(&a, &same));
assert!(!Signature::compare(&a, &different));
assert!(!Signature::compare(&a, &b64));
}
#[test]
fn debug_prints_plaintext_value() {
let sig = Signature::from_hex(HEX_64).unwrap();
assert!(format!("{sig:?}").contains(HEX_64));
}
}