use chrono::{DateTime, Utc};
use http::Request;
use crate::cavage::{CavageVerified, cavage_verify, cavage_verify_with_policy};
use crate::error::Error;
use crate::key::VerifyingKey;
use crate::policy::VerifyPolicy;
use crate::rfc9421::{
Rfc9421Verified, SIGNATURE_INPUT_HEADER, rfc9421_verify, rfc9421_verify_with_policy,
};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum Verified {
Cavage(CavageVerified),
Rfc9421(Rfc9421Verified),
}
impl Verified {
#[must_use]
pub fn key_id(&self) -> &str {
match self {
Self::Cavage(c) => &c.key_id,
Self::Rfc9421(r) => r.input.keyid.as_deref().unwrap_or_default(),
}
}
#[must_use]
pub fn signature_base(&self) -> &str {
match self {
Self::Cavage(c) => &c.signature_base,
Self::Rfc9421(r) => &r.signature_base,
}
}
}
pub fn verify<B, F>(req: &Request<B>, mut resolve_key: F) -> Result<Verified, Error>
where
F: FnMut(&str) -> Result<VerifyingKey, Error>,
{
if req.headers().contains_key(SIGNATURE_INPUT_HEADER) {
return rfc9421_verify(req, &mut resolve_key).map(Verified::Rfc9421);
}
cavage_verify(req, |kid| resolve_key(kid)).map(Verified::Cavage)
}
pub fn verify_with_policy<B, F>(
req: &Request<B>,
policy: &VerifyPolicy,
now: DateTime<Utc>,
mut resolve_key: F,
) -> Result<Verified, Error>
where
F: FnMut(&str) -> Result<VerifyingKey, Error>,
{
if req.headers().contains_key(SIGNATURE_INPUT_HEADER) {
return rfc9421_verify_with_policy(req, policy, now, &mut resolve_key)
.map(Verified::Rfc9421);
}
cavage_verify_with_policy(req, policy, now, |kid| resolve_key(kid)).map(Verified::Cavage)
}
#[cfg(test)]
mod tests {
use http::{Method, Request};
use pretty_assertions::assert_eq;
use super::*;
use crate::cavage::CavageSigner;
use crate::digest::sha256_digest_header;
use crate::key::SigningKey;
use crate::rfc9421::Rfc9421Signer;
fn base_request(body: &[u8]) -> Request<Vec<u8>> {
Request::builder()
.method(Method::POST)
.uri("https://example.com/inbox")
.header("host", "example.com")
.header("date", "Sun, 05 Jan 2014 21:31:40 GMT")
.header("digest", sha256_digest_header(body))
.header("content-type", "application/activity+json")
.body(body.to_vec())
.expect("valid")
}
#[test]
fn cavage_signed_request_is_dispatched_to_cavage_verifier() {
let key = SigningKey::generate_ed25519();
let public = key.verifying_key();
let mut req = base_request(b"{}");
CavageSigner::new(&key, "https://example.com/actor#kid")
.sign(&mut req)
.expect("sign");
let report = verify(&req, |_| Ok(public.clone())).expect("verify");
assert!(matches!(report, Verified::Cavage(_)));
assert_eq!(report.key_id(), "https://example.com/actor#kid");
}
#[test]
fn rfc9421_signed_request_is_dispatched_to_rfc9421_verifier() {
let key = SigningKey::generate_ed25519();
let public = key.verifying_key();
let mut req = base_request(b"{}");
Rfc9421Signer::new(&key, "https://example.com/actor#kid")
.sign(&mut req)
.expect("sign");
let report = verify(&req, |_| Ok(public.clone())).expect("verify");
assert!(matches!(report, Verified::Rfc9421(_)));
assert_eq!(report.key_id(), "https://example.com/actor#kid");
}
#[test]
fn rfc9421_takes_precedence_over_cavage_when_both_are_present() {
let key = SigningKey::generate_ed25519();
let public = key.verifying_key();
let mut req = base_request(b"{}");
CavageSigner::new(&key, "cavage-kid")
.sign(&mut req)
.expect("sign cavage");
Rfc9421Signer::new(&key, "rfc9421-kid")
.sign(&mut req)
.expect("sign 9421");
let report = verify(&req, |_| Ok(public.clone())).expect("verify");
assert!(matches!(report, Verified::Rfc9421(_)));
assert_eq!(report.key_id(), "rfc9421-kid");
}
#[test]
fn unsigned_request_returns_missing_header_error() {
let req = base_request(b"{}");
let err =
verify(&req, |_| panic!("resolver must not be called")).expect_err("unsigned request");
assert!(matches!(err, Error::MissingHeader(_)));
}
#[test]
fn policy_rejects_cavage_signature_older_than_max_age() {
let key = SigningKey::generate_ed25519();
let public = key.verifying_key();
let mut req = base_request(b"{}");
CavageSigner::new(&key, "kid")
.with_created(1_700_000_000)
.sign(&mut req)
.expect("sign");
let now = DateTime::<Utc>::from_timestamp(1_700_000_000 + 20 * 3600, 0).expect("valid");
let err = verify_with_policy(&req, &VerifyPolicy::mastodon(), now, |_| Ok(public.clone()))
.expect_err("stale signature must be rejected");
assert!(matches!(err, Error::TimestampTooOld { .. }));
}
#[test]
fn policy_rejects_rfc9421_signature_in_the_future() {
let key = SigningKey::generate_ed25519();
let public = key.verifying_key();
let mut req = base_request(b"{}");
Rfc9421Signer::new(&key, "kid")
.with_created(1_700_000_000 + 15 * 60)
.sign(&mut req)
.expect("sign");
let now = DateTime::<Utc>::from_timestamp(1_700_000_000, 0).expect("valid");
let err = verify_with_policy(&req, &VerifyPolicy::mastodon(), now, |_| Ok(public.clone()))
.expect_err("future-dated signature must be rejected");
assert!(matches!(err, Error::TimestampInFuture { .. }));
}
#[test]
fn policy_accepts_signature_within_skew_tolerance() {
let key = SigningKey::generate_ed25519();
let public = key.verifying_key();
let mut req = base_request(b"{}");
Rfc9421Signer::new(&key, "kid")
.with_created(1_700_000_000 + 60)
.sign(&mut req)
.expect("sign");
let now = DateTime::<Utc>::from_timestamp(1_700_000_000, 0).expect("valid");
verify_with_policy(&req, &VerifyPolicy::mastodon(), now, |_| Ok(public.clone()))
.expect("signature within skew tolerance must verify");
}
}