use base64::Engine as _;
use base64::alphabet;
use base64::engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig};
use super::{GrantError, MAX_GRANT_HEADER_BYTES};
const B64: GeneralPurpose = GeneralPurpose::new(
&alphabet::URL_SAFE,
GeneralPurposeConfig::new()
.with_encode_padding(false)
.with_decode_padding_mode(DecodePaddingMode::RequireNone)
.with_decode_allow_trailing_bits(false),
);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum OwnerScheme {
Ed25519,
Secp256k1Eip191,
WebAuthnP256,
}
impl OwnerScheme {
pub const ALL: [Self; 3] = [Self::Ed25519, Self::Secp256k1Eip191, Self::WebAuthnP256];
#[must_use]
pub fn token(self) -> &'static str {
match self {
Self::Ed25519 => "ed25519",
Self::Secp256k1Eip191 => "secp256k1-eip191",
Self::WebAuthnP256 => "webauthn-p256",
}
}
#[must_use]
pub fn from_token(s: &str) -> Option<Self> {
Self::ALL.into_iter().find(|scheme| scheme.token() == s)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SignedHeader {
pub statement: Vec<u8>,
pub scheme: OwnerScheme,
pub blob: Vec<u8>,
}
impl SignedHeader {
pub fn parse(value: &str) -> Result<Self, GrantError> {
if value.len() > MAX_GRANT_HEADER_BYTES {
return Err(GrantError::HeaderTooLong);
}
let mut parts = value.split('.');
let (Some(statement), Some(scheme), Some(blob), None) =
(parts.next(), parts.next(), parts.next(), parts.next())
else {
return Err(GrantError::HeaderFormat);
};
if statement.is_empty() || scheme.is_empty() || blob.is_empty() {
return Err(GrantError::HeaderFormat);
}
let scheme = OwnerScheme::from_token(scheme).ok_or(GrantError::UnknownScheme)?;
let decode = |s: &str| B64.decode(s).map_err(|_| GrantError::HeaderBase64);
Ok(Self {
statement: decode(statement)?,
scheme,
blob: decode(blob)?,
})
}
pub fn encode(&self) -> Result<String, GrantError> {
if self.statement.is_empty() || self.blob.is_empty() {
return Err(GrantError::HeaderFormat);
}
let value = format!(
"{}.{}.{}",
B64.encode(&self.statement),
self.scheme.token(),
B64.encode(&self.blob)
);
if value.len() > MAX_GRANT_HEADER_BYTES {
return Err(GrantError::HeaderTooLong);
}
Ok(value)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn header(statement: &[u8], blob: &[u8]) -> SignedHeader {
SignedHeader {
statement: statement.to_vec(),
scheme: OwnerScheme::Ed25519,
blob: blob.to_vec(),
}
}
#[test]
fn scheme_tokens() {
for scheme in OwnerScheme::ALL {
assert_eq!(OwnerScheme::from_token(scheme.token()), Some(scheme));
}
for bad in [
"",
"Ed25519",
"ed25519 ",
"secp256k1",
"webauthn-p256k",
"p256",
] {
assert_eq!(OwnerScheme::from_token(bad), None, "{bad:?}");
}
}
#[test]
fn roundtrip() {
for scheme in OwnerScheme::ALL {
for (s, b) in [
(&b"a"[..], &b"b"[..]),
(b"ab", b"abc"),
(b"\xff\xfe\xfd", &[0u8; 65][..]),
] {
let h = SignedHeader {
statement: s.to_vec(),
scheme,
blob: b.to_vec(),
};
let text = h.encode().unwrap();
assert!(!text.contains('='));
assert_eq!(SignedHeader::parse(&text), Ok(h));
}
}
assert_eq!(
header(b"\xfb\xff", b"a").encode().unwrap(),
"-_8.ed25519.YQ"
);
}
#[test]
fn rejects_non_canonical_base64() {
assert_eq!(
SignedHeader::parse("YQ==.ed25519.YQ"),
Err(GrantError::HeaderBase64)
);
assert_eq!(
SignedHeader::parse("YQ.ed25519.YQ="),
Err(GrantError::HeaderBase64)
);
assert_eq!(
SignedHeader::parse("+_8.ed25519.YQ"),
Err(GrantError::HeaderBase64)
);
assert_eq!(
SignedHeader::parse("-/8.ed25519.YQ"),
Err(GrantError::HeaderBase64)
);
assert!(SignedHeader::parse("QQ.ed25519.YQ").is_ok());
assert_eq!(
SignedHeader::parse("QR.ed25519.YQ"),
Err(GrantError::HeaderBase64)
);
assert_eq!(
SignedHeader::parse("YQ.ed25519.YR"),
Err(GrantError::HeaderBase64)
);
assert!(SignedHeader::parse("AA.ed25519.YQ").is_ok());
assert_eq!(
SignedHeader::parse("AB.ed25519.YQ"),
Err(GrantError::HeaderBase64)
);
assert_eq!(
SignedHeader::parse("QUJDR.ed25519.YQ"),
Err(GrantError::HeaderBase64)
);
assert_eq!(
SignedHeader::parse("Y Q.ed25519.YQ"),
Err(GrantError::HeaderBase64)
);
}
#[test]
fn rejects_bad_structure() {
for bad in [
"YQ.ed25519",
"YQ",
"YQ.ed25519.YQ.YQ",
"YQ..ed25519.YQ",
".ed25519.YQ",
"YQ.ed25519.",
"YQ..YQ",
"",
] {
assert_eq!(
SignedHeader::parse(bad),
Err(GrantError::HeaderFormat),
"{bad:?}"
);
}
assert_eq!(
SignedHeader::parse("YQ.rsa.YQ"),
Err(GrantError::UnknownScheme)
);
assert_eq!(
SignedHeader::parse("YQ.ED25519.YQ"),
Err(GrantError::UnknownScheme)
);
assert_eq!(header(b"", b"a").encode(), Err(GrantError::HeaderFormat));
assert_eq!(header(b"a", b"").encode(), Err(GrantError::HeaderFormat));
}
#[test]
fn header_length_bound() {
let exact = header(&[b'x'; 6135], b"ab").encode().unwrap();
assert_eq!(exact.len(), MAX_GRANT_HEADER_BYTES);
assert!(SignedHeader::parse(&exact).is_ok());
let over = format!("{exact}A");
assert_eq!(over.len(), MAX_GRANT_HEADER_BYTES + 1);
assert_eq!(SignedHeader::parse(&over), Err(GrantError::HeaderTooLong));
assert_eq!(
header(&vec![b'x'; MAX_GRANT_HEADER_BYTES], b"a").encode(),
Err(GrantError::HeaderTooLong)
);
}
}