use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
use crate::agent::AgentManifest;
use crate::error::{Error, Result};
use crate::identity::{normalize_key, verify_signature, AgentIdentity};
fn unix_now() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct RotationProof {
pub previous_key: String,
pub new_key: String,
pub rotated_at: u64,
pub signature: String,
}
impl RotationProof {
fn message(previous_key: &str, new_key: &str, rotated_at: u64) -> Vec<u8> {
format!("corrosive-rotation:{previous_key}:{new_key}:{rotated_at}").into_bytes()
}
pub fn create(old: &AgentIdentity, new: &AgentIdentity) -> Self {
let previous_key = old.public_key_base64();
let new_key = new.public_key_base64();
let rotated_at = unix_now();
let signature = old.sign(&Self::message(&previous_key, &new_key, rotated_at));
Self {
previous_key,
new_key,
rotated_at,
signature,
}
}
pub fn verify(&self) -> Result<()> {
verify_signature(
&self.previous_key,
&Self::message(&self.previous_key, &self.new_key, self.rotated_at),
&self.signature,
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Revocation {
pub public_key: String,
pub revoked_at: u64,
pub reason: String,
pub signature: String,
}
impl Revocation {
fn message(public_key: &str, revoked_at: u64, reason: &str) -> Vec<u8> {
format!("corrosive-revocation:{public_key}:{revoked_at}:{reason}").into_bytes()
}
pub fn create(identity: &AgentIdentity, reason: impl Into<String>) -> Self {
let public_key = identity.public_key_base64();
let revoked_at = unix_now();
let reason = reason.into();
let signature = identity.sign(&Self::message(&public_key, revoked_at, &reason));
Self {
public_key,
revoked_at,
reason,
signature,
}
}
pub fn verify(&self) -> Result<()> {
verify_signature(
&self.public_key,
&Self::message(&self.public_key, self.revoked_at, &self.reason),
&self.signature,
)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TrustStore {
trusted_keys: BTreeSet<String>,
revocations: BTreeMap<String, Revocation>,
}
impl TrustStore {
pub fn new() -> Self {
Self::default()
}
pub fn trust(&mut self, key_or_did: &str) -> Result<()> {
self.trusted_keys.insert(normalize_key(key_or_did)?);
Ok(())
}
pub fn revoke(&mut self, revocation: Revocation) -> Result<()> {
revocation.verify()?;
self.revocations
.insert(revocation.public_key.clone(), revocation);
Ok(())
}
pub fn is_trusted(&self, key_or_did: &str) -> bool {
normalize_key(key_or_did)
.map(|k| self.trusted_keys.contains(&k))
.unwrap_or(false)
}
pub fn revocation(&self, key_or_did: &str) -> Option<&Revocation> {
normalize_key(key_or_did)
.ok()
.and_then(|k| self.revocations.get(&k))
}
pub fn to_json(&self) -> Result<String> {
Ok(serde_json::to_string_pretty(self)?)
}
pub fn from_json(json: &str) -> Result<Self> {
let store: Self = serde_json::from_str(json)?;
for revocation in store.revocations.values() {
revocation.verify()?;
}
Ok(store)
}
pub fn verify_manifest(&self, manifest: &AgentManifest) -> Result<()> {
manifest.verify()?;
let current = manifest
.public_key
.as_deref()
.ok_or_else(|| Error::Verification("manifest has no public key".into()))?;
if let Some(revocation) = self.revocations.get(current) {
return Err(Error::Verification(format!(
"agent key is revoked ({})",
revocation.reason
)));
}
if self.trusted_keys.contains(current) {
return Ok(());
}
let history = &manifest.key_history;
if history.is_empty() {
return Err(Error::Verification(
"agent key is not trusted and manifest has no key history".into(),
));
}
let last = history.last().expect("history is non-empty");
if last.new_key != current {
return Err(Error::Verification(
"key history does not end at the manifest's current key".into(),
));
}
let Some(start) = history
.iter()
.position(|proof| self.trusted_keys.contains(&proof.previous_key))
else {
return Err(Error::Verification(
"no trusted key found in the rotation chain".into(),
));
};
for (i, proof) in history[start..].iter().enumerate() {
proof.verify()?;
if let Some(next) = history[start..].get(i + 1) {
if next.previous_key != proof.new_key {
return Err(Error::Verification(
"rotation chain is not contiguous".into(),
));
}
}
if let Some(revocation) = self.revocations.get(&proof.previous_key) {
if proof.rotated_at >= revocation.revoked_at {
return Err(Error::Verification(format!(
"rotation was made after key revocation ({})",
revocation.reason
)));
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn signed_manifest(identity: &AgentIdentity) -> AgentManifest {
let mut manifest = AgentManifest::new("t", "1.0.0");
manifest.sign(identity).unwrap();
manifest
}
#[test]
fn rotation_proof_roundtrip() {
let old = AgentIdentity::generate();
let new = AgentIdentity::generate();
let proof = RotationProof::create(&old, &new);
proof.verify().unwrap();
let mut forged = proof.clone();
forged.new_key = AgentIdentity::generate().public_key_base64();
assert!(forged.verify().is_err());
}
#[test]
fn directly_trusted_key_passes() {
let identity = AgentIdentity::generate();
let manifest = signed_manifest(&identity);
let mut trust = TrustStore::new();
trust.trust(&identity.public_key_base64()).unwrap();
trust.verify_manifest(&manifest).unwrap();
}
#[test]
fn untrusted_key_fails() {
let manifest = signed_manifest(&AgentIdentity::generate());
let trust = TrustStore::new();
assert!(trust.verify_manifest(&manifest).is_err());
}
#[test]
fn rotation_chain_extends_trust() {
let k1 = AgentIdentity::generate();
let k2 = AgentIdentity::generate();
let k3 = AgentIdentity::generate();
let mut manifest = signed_manifest(&k1);
manifest.rotate_identity(&k1, &k2).unwrap();
manifest.rotate_identity(&k2, &k3).unwrap();
assert_eq!(manifest.public_key, Some(k3.public_key_base64()));
let mut trust = TrustStore::new();
trust.trust(&k1.public_key_base64()).unwrap();
trust.verify_manifest(&manifest).unwrap();
}
#[test]
fn revoked_current_key_fails() {
let identity = AgentIdentity::generate();
let manifest = signed_manifest(&identity);
let mut trust = TrustStore::new();
trust.trust(&identity.public_key_base64()).unwrap();
trust
.revoke(Revocation::create(&identity, "compromised"))
.unwrap();
let err = trust.verify_manifest(&manifest).unwrap_err();
assert!(err.to_string().contains("revoked"));
}
#[test]
fn rotation_before_revocation_still_valid() {
let old = AgentIdentity::generate();
let new = AgentIdentity::generate();
let mut manifest = signed_manifest(&old);
manifest.rotate_identity(&old, &new).unwrap();
let mut trust = TrustStore::new();
trust.trust(&old.public_key_base64()).unwrap();
let mut revocation = Revocation::create(&old, "retired");
revocation.revoked_at = manifest.key_history[0].rotated_at + 10;
revocation.signature = old.sign(&Revocation::message(
&revocation.public_key,
revocation.revoked_at,
&revocation.reason,
));
trust.revoke(revocation).unwrap();
trust.verify_manifest(&manifest).unwrap();
}
#[test]
fn rotation_after_revocation_rejected() {
let old = AgentIdentity::generate();
let new = AgentIdentity::generate();
let mut manifest = signed_manifest(&old);
manifest.rotate_identity(&old, &new).unwrap();
let mut trust = TrustStore::new();
trust.trust(&old.public_key_base64()).unwrap();
let mut revocation = Revocation::create(&old, "compromised");
revocation.revoked_at = manifest.key_history[0].rotated_at.saturating_sub(10);
revocation.signature = old.sign(&Revocation::message(
&revocation.public_key,
revocation.revoked_at,
&revocation.reason,
));
trust.revoke(revocation).unwrap();
assert!(trust.verify_manifest(&manifest).is_err());
}
#[test]
fn trust_store_json_roundtrip() {
let identity = AgentIdentity::generate();
let mut trust = TrustStore::new();
trust.trust(&identity.public_key_base64()).unwrap();
trust
.revoke(Revocation::create(&AgentIdentity::generate(), "test"))
.unwrap();
let restored = TrustStore::from_json(&trust.to_json().unwrap()).unwrap();
assert!(restored.is_trusted(&identity.public_key_base64()));
}
}