use cheers_core::{Claims, CodecError, McpClaims, TokenVerifier};
use pasetors::claims::ClaimsValidationRules;
use pasetors::keys::AsymmetricPublicKey;
use pasetors::public;
use pasetors::token::UntrustedToken;
use pasetors::version4::{PublicToken, V4};
pub fn codec_err(e: pasetors::errors::Error) -> CodecError {
use pasetors::errors::Error as P;
match e {
P::TokenValidation => CodecError::SignatureMismatch,
P::ClaimValidation(_) => CodecError::Expired,
other => CodecError::Crypto(format!("{other:?}")),
}
}
pub struct PasetoV4PublicVerifier {
public: AsymmetricPublicKey<V4>,
}
impl PasetoV4PublicVerifier {
pub fn from_public_key(bytes: &[u8; 32]) -> Result<Self, CodecError> {
let public = AsymmetricPublicKey::<V4>::from(bytes)
.map_err(|e| CodecError::Crypto(format!("{e:?}")))?;
Ok(Self { public })
}
pub fn from_key(public: AsymmetricPublicKey<V4>) -> Self {
Self { public }
}
pub fn public_key(&self) -> &AsymmetricPublicKey<V4> {
&self.public
}
pub fn verify_mcp_at(
&self,
token: &str,
now: i64,
expected_kid: &str,
) -> Result<McpClaims, CodecError> {
let untrusted = UntrustedToken::<pasetors::token::Public, V4>::try_from(token)
.map_err(|_| CodecError::Malformed)?;
let footer_bytes = untrusted.untrusted_footer();
if footer_bytes.is_empty() {
return Err(CodecError::MissingKid);
}
let footer: serde_json::Value =
serde_json::from_slice(footer_bytes).map_err(|_| CodecError::Malformed)?;
let kid = footer
.get("kid")
.and_then(|v| v.as_str())
.ok_or(CodecError::Malformed)?;
if kid != expected_kid {
return Err(CodecError::UnknownKid(kid.to_string()));
}
let trusted = PublicToken::verify(&self.public, &untrusted, None, None).map_err(codec_err)?;
let out: McpClaims = serde_json::from_str(trusted.payload())?;
if out.is_expired_at(now) {
return Err(CodecError::Expired);
}
Ok(out)
}
}
impl TokenVerifier for PasetoV4PublicVerifier {
fn verify_at(&self, token: &str, now: i64) -> Result<Claims, CodecError> {
let untrusted = UntrustedToken::<pasetors::token::Public, V4>::try_from(token)
.map_err(|_| CodecError::Malformed)?;
let mut rules = ClaimsValidationRules::new();
rules.allow_non_expiring();
let trusted = public::verify(&self.public, &untrusted, &rules, None, None)
.map_err(codec_err)?;
let pclaims = trusted.payload_claims().ok_or(CodecError::Malformed)?;
let v = pclaims
.get_claim("cheers")
.ok_or(CodecError::Malformed)?
.clone();
let out: Claims = serde_json::from_value(v)?;
if out.is_expired_at(now) {
return Err(CodecError::Expired);
}
Ok(out)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_malformed() {
let verifier = PasetoV4PublicVerifier::from_public_key(&[0u8; 32]).unwrap();
assert!(matches!(
verifier.verify_at("not-a-paseto", 0).unwrap_err(),
CodecError::Malformed
));
}
#[test]
fn codec_err_maps_token_validation_to_signature_mismatch() {
let e = codec_err(pasetors::errors::Error::TokenValidation);
assert!(matches!(e, CodecError::SignatureMismatch));
}
}