use std::sync::Arc;
use std::time::Duration;
use base64::Engine;
use chrono::{DateTime, Utc};
use moka::future::Cache;
use ring::signature::{UnparsedPublicKey, ED25519};
use serde::{Deserialize, Serialize};
use thiserror::Error;
pub const DEFAULT_SIG_REPLAY_TTL: Duration = Duration::from_secs(300);
pub const DEFAULT_REPLAY_CACHE_CAPACITY: u64 = 100_000;
#[derive(Debug, Error)]
pub enum Sig9421Error {
#[error("envelope missing required `signature` field")]
MissingSignature,
#[error("malformed signature object: {0}")]
MalformedSignature(String),
#[error("unsupported signature algorithm: {0}")]
UnsupportedAlgorithm(String),
#[error("signature keyid `{0}` does not resolve to a registered public key")]
UnknownKeyid(String),
#[error("signature verification failed")]
InvalidSignature,
#[error("envelope replay detected: id `{0}` already seen")]
ReplayDetected(String),
#[error("envelope timestamp `{0}` is older than the replay window")]
StaleTimestamp(String),
#[error("envelope is not a JSON object")]
MalformedEnvelope,
#[error("envelope `id` field missing or not a string")]
MissingEnvelopeId,
#[error("envelope `ts` field missing or not a valid RFC 3339 timestamp")]
InvalidEnvelopeTimestamp,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EnvelopeSignature {
pub keyid: String,
pub alg: String,
pub sig: String,
}
pub trait KeyResolver: Send + Sync {
fn resolve(&self, keyid: &str) -> Option<Vec<u8>>;
}
pub struct StaticKeyResolver {
keys: std::collections::HashMap<String, Vec<u8>>,
}
impl StaticKeyResolver {
pub fn new() -> Self {
Self {
keys: std::collections::HashMap::new(),
}
}
pub fn insert(&mut self, keyid: impl Into<String>, public_key: Vec<u8>) {
self.keys.insert(keyid.into(), public_key);
}
}
impl Default for StaticKeyResolver {
fn default() -> Self {
Self::new()
}
}
impl KeyResolver for StaticKeyResolver {
fn resolve(&self, keyid: &str) -> Option<Vec<u8>> {
self.keys.get(keyid).cloned()
}
}
pub struct Sig9421Verifier {
resolver: Arc<dyn KeyResolver>,
replay_cache: Cache<String, ()>,
ttl: Duration,
}
impl Sig9421Verifier {
pub fn new(resolver: Arc<dyn KeyResolver>) -> Self {
Self::with_ttl(resolver, DEFAULT_SIG_REPLAY_TTL)
}
pub fn with_ttl(resolver: Arc<dyn KeyResolver>, ttl: Duration) -> Self {
Self {
resolver,
replay_cache: Cache::builder()
.max_capacity(DEFAULT_REPLAY_CACHE_CAPACITY)
.time_to_live(ttl)
.build(),
ttl,
}
}
pub async fn verify(&self, envelope: &serde_json::Value) -> Result<(), Sig9421Error> {
let obj = envelope
.as_object()
.ok_or(Sig9421Error::MalformedEnvelope)?;
let sig_value = obj.get("signature").ok_or(Sig9421Error::MissingSignature)?;
let signature: EnvelopeSignature = serde_json::from_value(sig_value.clone())
.map_err(|e| Sig9421Error::MalformedSignature(e.to_string()))?;
let env_id = obj
.get("id")
.and_then(|v| v.as_str())
.ok_or(Sig9421Error::MissingEnvelopeId)?
.to_string();
let env_ts_str = obj
.get("ts")
.and_then(|v| v.as_str())
.ok_or(Sig9421Error::InvalidEnvelopeTimestamp)?;
let env_ts: DateTime<Utc> = DateTime::parse_from_rfc3339(env_ts_str)
.map_err(|_| Sig9421Error::InvalidEnvelopeTimestamp)?
.with_timezone(&Utc);
let age = Utc::now().signed_duration_since(env_ts);
if age > chrono::Duration::from_std(self.ttl).unwrap_or(chrono::Duration::seconds(300)) {
return Err(Sig9421Error::StaleTimestamp(env_ts_str.to_string()));
}
let mut bare = obj.clone();
bare.remove("signature");
let canonical = jcs_canonicalize(&serde_json::Value::Object(bare));
let pubkey = self
.resolver
.resolve(&signature.keyid)
.ok_or_else(|| Sig9421Error::UnknownKeyid(signature.keyid.clone()))?;
let sig_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(signature.sig.as_bytes())
.map_err(|_| Sig9421Error::InvalidSignature)?;
match signature.alg.as_str() {
"EdDSA" => {
let key = UnparsedPublicKey::new(&ED25519, &pubkey);
key.verify(canonical.as_bytes(), &sig_bytes)
.map_err(|_| Sig9421Error::InvalidSignature)?;
}
other => return Err(Sig9421Error::UnsupportedAlgorithm(other.to_string())),
}
if self.replay_cache.get(&env_id).await.is_some() {
return Err(Sig9421Error::ReplayDetected(env_id));
}
self.replay_cache.insert(env_id, ()).await;
Ok(())
}
}
pub fn jcs_canonicalize(value: &serde_json::Value) -> String {
let mut out = String::new();
jcs_write(value, &mut out);
out
}
fn jcs_write(value: &serde_json::Value, out: &mut String) {
match value {
serde_json::Value::Null => out.push_str("null"),
serde_json::Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
serde_json::Value::Number(n) => {
out.push_str(&n.to_string());
}
serde_json::Value::String(s) => {
out.push('"');
for ch in s.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\u{08}' => out.push_str("\\b"),
'\u{0c}' => out.push_str("\\f"),
c if (c as u32) < 0x20 => {
out.push_str(&format!("\\u{:04x}", c as u32));
}
c => out.push(c),
}
}
out.push('"');
}
serde_json::Value::Array(items) => {
out.push('[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
jcs_write(item, out);
}
out.push(']');
}
serde_json::Value::Object(map) => {
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
out.push('{');
for (i, key) in keys.iter().enumerate() {
if i > 0 {
out.push(',');
}
jcs_write(&serde_json::Value::String((*key).clone()), out);
out.push(':');
jcs_write(&map[*key], out);
}
out.push('}');
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use ring::rand::{SecureRandom, SystemRandom};
use ring::signature::{Ed25519KeyPair, KeyPair};
fn signing_keypair() -> (Ed25519KeyPair, Vec<u8>) {
let rng = SystemRandom::new();
let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
let kp = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
let pub_bytes = kp.public_key().as_ref().to_vec();
(kp, pub_bytes)
}
fn build_envelope() -> serde_json::Value {
serde_json::json!({
"v": 1,
"type": "session.invite",
"id": "env_sig_test_1",
"ts": Utc::now().to_rfc3339(),
"sid": "sess_abc",
"cid": "conv_abc",
"payload": {
"from": "part_alice",
"to": ["part_bob"],
"medium": "voice",
}
})
}
fn sign_envelope(envelope: &mut serde_json::Value, keyid: &str, kp: &Ed25519KeyPair) {
let obj = envelope.as_object_mut().unwrap();
obj.remove("signature");
let canonical = jcs_canonicalize(envelope);
let sig = kp.sign(canonical.as_bytes());
let sig_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(sig.as_ref());
let obj = envelope.as_object_mut().unwrap();
obj.insert(
"signature".to_string(),
serde_json::json!({
"keyid": keyid,
"alg": "EdDSA",
"sig": sig_b64,
}),
);
}
#[tokio::test]
async fn round_trip_signed_envelope_verifies() {
let (kp, pubkey) = signing_keypair();
let mut resolver = StaticKeyResolver::new();
resolver.insert("key:agent-1", pubkey);
let verifier = Sig9421Verifier::new(Arc::new(resolver));
let mut env = build_envelope();
sign_envelope(&mut env, "key:agent-1", &kp);
verifier.verify(&env).await.expect("verify");
}
#[tokio::test]
async fn tampered_payload_fails_verification() {
let (kp, pubkey) = signing_keypair();
let mut resolver = StaticKeyResolver::new();
resolver.insert("key:agent-1", pubkey);
let verifier = Sig9421Verifier::new(Arc::new(resolver));
let mut env = build_envelope();
sign_envelope(&mut env, "key:agent-1", &kp);
env["payload"]["from"] = serde_json::json!("part_mallory");
let err = verifier.verify(&env).await.unwrap_err();
assert!(matches!(err, Sig9421Error::InvalidSignature));
}
#[tokio::test]
async fn replay_rejected_on_second_call() {
let (kp, pubkey) = signing_keypair();
let mut resolver = StaticKeyResolver::new();
resolver.insert("key:agent-1", pubkey);
let verifier = Sig9421Verifier::new(Arc::new(resolver));
let mut env = build_envelope();
sign_envelope(&mut env, "key:agent-1", &kp);
verifier.verify(&env).await.expect("first verify");
let err = verifier.verify(&env).await.unwrap_err();
assert!(matches!(err, Sig9421Error::ReplayDetected(_)));
}
#[tokio::test]
async fn unknown_keyid_rejected() {
let (kp, _pubkey) = signing_keypair();
let verifier = Sig9421Verifier::new(Arc::new(StaticKeyResolver::new()));
let mut env = build_envelope();
sign_envelope(&mut env, "key:agent-unknown", &kp);
let err = verifier.verify(&env).await.unwrap_err();
assert!(matches!(err, Sig9421Error::UnknownKeyid(_)));
}
#[tokio::test]
async fn cross_key_tampering_rejected() {
let (kp_a, pubkey_a) = signing_keypair();
let (_kp_b, pubkey_b) = signing_keypair();
let mut resolver = StaticKeyResolver::new();
resolver.insert("key:agent-1", pubkey_b);
let _ = pubkey_a;
let verifier = Sig9421Verifier::new(Arc::new(resolver));
let mut env = build_envelope();
sign_envelope(&mut env, "key:agent-1", &kp_a);
let err = verifier.verify(&env).await.unwrap_err();
assert!(matches!(err, Sig9421Error::InvalidSignature));
}
#[tokio::test]
async fn stale_timestamp_rejected() {
let (kp, pubkey) = signing_keypair();
let mut resolver = StaticKeyResolver::new();
resolver.insert("key:agent-1", pubkey);
let verifier = Sig9421Verifier::with_ttl(Arc::new(resolver), Duration::from_secs(2));
let mut env = build_envelope();
env["ts"] = serde_json::json!((Utc::now() - chrono::Duration::hours(1)).to_rfc3339());
sign_envelope(&mut env, "key:agent-1", &kp);
let err = verifier.verify(&env).await.unwrap_err();
assert!(matches!(err, Sig9421Error::StaleTimestamp(_)));
}
#[tokio::test]
async fn missing_signature_field_returns_typed_error() {
let verifier = Sig9421Verifier::new(Arc::new(StaticKeyResolver::new()));
let env = build_envelope();
let err = verifier.verify(&env).await.unwrap_err();
assert!(matches!(err, Sig9421Error::MissingSignature));
}
#[test]
fn jcs_sorts_object_keys() {
let v = serde_json::json!({ "z": 1, "a": 2, "m": 3 });
assert_eq!(jcs_canonicalize(&v), r#"{"a":2,"m":3,"z":1}"#);
}
#[test]
fn jcs_escapes_strings() {
let v = serde_json::json!("a\"b\\c\n");
assert_eq!(jcs_canonicalize(&v), r#""a\"b\\c\n""#);
}
#[test]
fn jcs_handles_nested() {
let v = serde_json::json!({ "b": [1, 2, { "y": "z", "x": "w" }], "a": null });
assert_eq!(
jcs_canonicalize(&v),
r#"{"a":null,"b":[1,2,{"x":"w","y":"z"}]}"#
);
}
#[test]
fn rng_bound_exists() {
let rng = SystemRandom::new();
let mut buf = [0u8; 4];
rng.fill(&mut buf).unwrap();
assert_ne!(buf, [0u8; 4]);
}
}