1use std::collections::BTreeSet;
12
13use content_addressable::{ContentId, MerkleNode};
14use serde::{Deserialize, Serialize};
15
16use crate::agent_key::{CertChain, Issuer, SerdeSig};
17use crate::{Fingerprint, MeshError, Result, UserKey, UserPublic};
18
19const REVOCATION_DOMAIN: &[u8] = b"agent-mesh/revocation/v1\0";
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24pub enum RevocationEntry {
25 Genesis {
27 user: UserPublic,
29 },
30 Revoke {
32 agent: Fingerprint,
34 },
35 Generation {
37 current: u64,
39 },
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44pub struct SignedEntry {
45 node: MerkleNode<RevocationEntry>,
46 sig: SerdeSig,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51pub struct RevocationLog {
52 entries: Vec<SignedEntry>,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct RevocationState {
58 pub user: Fingerprint,
60 pub head: ContentId,
62 pub len: u64,
64 pub generation: u64,
66 pub revoked: BTreeSet<Fingerprint>,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
72pub struct Seen {
73 pub user: Fingerprint,
75 pub len: u64,
77 pub head: ContentId,
79}
80
81fn bad(msg: &str) -> MeshError {
82 MeshError::RevocationState(msg.into())
83}
84
85fn message(cid: &ContentId) -> Vec<u8> {
86 let mut m = REVOCATION_DOMAIN.to_vec();
87 m.extend_from_slice(&cid.to_bytes());
88 m
89}
90
91fn sign_node(user: &UserKey, node: MerkleNode<RevocationEntry>) -> Result<SignedEntry> {
92 let id = node.id().map_err(|e| MeshError::Encoding(e.to_string()))?;
93 Ok(SignedEntry {
94 node,
95 sig: SerdeSig(user.sign(&message(&id))),
96 })
97}
98
99impl RevocationState {
100 #[must_use]
102 pub fn seen(&self) -> Seen {
103 Seen {
104 user: self.user,
105 len: self.len,
106 head: self.head,
107 }
108 }
109}
110
111impl RevocationLog {
112 pub fn genesis(user: &UserKey) -> Result<Self> {
117 let node = MerkleNode::genesis(RevocationEntry::Genesis {
118 user: user.public(),
119 });
120 Ok(Self {
121 entries: vec![sign_node(user, node)?],
122 })
123 }
124
125 pub fn append(&mut self, user: &UserKey, entry: RevocationEntry) -> Result<()> {
132 let state = self.verify()?;
133 if user.fingerprint() != state.user {
134 return Err(bad("appending key is not the log's user"));
135 }
136 match &entry {
137 RevocationEntry::Genesis { .. } => return Err(bad("genesis only starts a log")),
138 RevocationEntry::Generation { current } if *current <= state.generation => {
139 return Err(bad("generation must strictly increase"))
140 }
141 _ => {}
142 }
143 self.entries
144 .push(sign_node(user, MerkleNode::new(entry, [state.head]))?);
145 Ok(())
146 }
147
148 pub fn verify(&self) -> Result<RevocationState> {
157 let first = self.entries.first().ok_or_else(|| bad("missing genesis"))?;
158 let RevocationEntry::Genesis { user } = first.node.payload() else {
159 return Err(bad("first node is not a genesis"));
160 };
161 let mut state = RevocationState {
162 user: user.fingerprint(),
163 head: Self::check_link(user, first, None)?,
164 len: 1,
165 generation: 0,
166 revoked: BTreeSet::new(),
167 };
168 for e in &self.entries[1..] {
169 state.head = Self::check_link(user, e, Some(state.head))?;
170 state.len += 1;
171 match e.node.payload() {
172 RevocationEntry::Genesis { .. } => return Err(bad("second genesis")),
173 RevocationEntry::Revoke { agent } => {
174 state.revoked.insert(*agent);
175 }
176 RevocationEntry::Generation { current } => {
177 if *current <= state.generation {
178 return Err(bad("generation does not increase"));
179 }
180 state.generation = *current;
181 }
182 }
183 }
184 Ok(state)
185 }
186
187 fn check_link(
190 user: &UserPublic,
191 e: &SignedEntry,
192 prev: Option<ContentId>,
193 ) -> Result<ContentId> {
194 if !e.node.parents().iter().copied().eq(prev) {
195 return Err(bad("broken parent link"));
196 }
197 let id = e
198 .node
199 .id()
200 .map_err(|err| MeshError::Encoding(err.to_string()))?;
201 user.verify(&message(&id), &e.sig.0)?;
202 Ok(id)
203 }
204
205 pub fn extends(&self, seen: &Seen) -> Result<()> {
212 let state = self.verify()?;
213 if state.user != seen.user {
214 return Err(bad("log belongs to a different user"));
215 }
216 if seen.len == 0 || state.len < seen.len {
217 return Err(bad("log is shorter than what was seen (rollback)"));
218 }
219 let at = &self.entries[usize::try_from(seen.len - 1).map_err(|_| bad("seen too long"))?];
220 let id = at
221 .node
222 .id()
223 .map_err(|e| MeshError::Encoding(e.to_string()))?;
224 if id != seen.head {
225 return Err(bad("log forks from what was seen"));
226 }
227 Ok(())
228 }
229}
230
231impl CertChain {
232 pub fn verify_with(&self, state: &RevocationState) -> Result<()> {
242 if self.user_fingerprint() != state.user {
243 return Err(bad("chain belongs to a different user"));
244 }
245 let mut link = self;
246 loop {
247 let agent = link.agent_fingerprint();
248 if state.revoked.contains(&agent) {
249 return Err(MeshError::Revoked { agent });
250 }
251 match &link.issuer {
252 Issuer::User(_) => break,
253 Issuer::Agent { parent, .. } => link = parent,
254 }
255 }
256 self.verify_at(state.generation)
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263 use crate::{AgentKey, AgentMetadata, Caveats, Scope};
264
265 fn meta(caveats: Caveats) -> AgentMetadata {
266 AgentMetadata {
267 role: "r".into(),
268 host: "h".into(),
269 capabilities: vec![],
270 issued_at: "2026-10-26T00:00:00Z".into(),
271 expires_at: None,
272 caveats,
273 }
274 }
275
276 fn scoped(gen: u64) -> Caveats {
277 let mut c = Caveats::top();
278 c.valid_for_generation = Scope::Only([gen].into());
279 c
280 }
281
282 fn revoke(user: &UserKey, log: &mut RevocationLog, k: &AgentKey) {
283 let agent = k.fingerprint();
284 log.append(user, RevocationEntry::Revoke { agent }).unwrap();
285 }
286
287 fn is_state_err(r: Result<impl std::fmt::Debug>) -> bool {
288 matches!(r, Err(MeshError::RevocationState(_)))
289 }
290
291 #[test]
292 fn fresh_log_verifies_and_accepts_chain() {
293 let user = UserKey::generate();
294 let log = RevocationLog::genesis(&user).unwrap();
295 let st = log.verify().unwrap();
296 assert_eq!((st.len, st.generation), (1, 0));
297 let a = AgentKey::issue(&user, meta(Caveats::top()));
298 a.cert().verify_with(&st).unwrap();
299 }
300
301 #[test]
302 fn revoked_leaf_refused() {
303 let user = UserKey::generate();
304 let a = AgentKey::issue(&user, meta(Caveats::top()));
305 let mut log = RevocationLog::genesis(&user).unwrap();
306 revoke(&user, &mut log, &a);
307 let err = a.cert().verify_with(&log.verify().unwrap()).unwrap_err();
308 assert!(matches!(err, MeshError::Revoked { agent } if agent == a.fingerprint()));
309 }
310
311 #[test]
312 fn revoked_parent_refuses_delegated_child() {
313 let user = UserKey::generate();
314 let parent = AgentKey::issue(&user, meta(Caveats::top()));
315 let child = parent.delegate(meta(Caveats::top())).unwrap();
316 let mut log = RevocationLog::genesis(&user).unwrap();
317 revoke(&user, &mut log, &parent);
318 let err = child
319 .cert()
320 .verify_with(&log.verify().unwrap())
321 .unwrap_err();
322 assert!(matches!(err, MeshError::Revoked { agent } if agent == parent.fingerprint()));
323 }
324
325 #[test]
326 fn generation_bump_refuses_old_scope_but_not_all() {
327 let user = UserKey::generate();
328 let old = AgentKey::issue(&user, meta(scoped(0)));
329 let all = AgentKey::issue(&user, meta(Caveats::top()));
330 let mut log = RevocationLog::genesis(&user).unwrap();
331 log.append(&user, RevocationEntry::Generation { current: 1 })
332 .unwrap();
333 let st = log.verify().unwrap();
334 assert!(matches!(
335 old.cert().verify_with(&st),
336 Err(MeshError::Generation(_))
337 ));
338 all.cert().verify_with(&st).unwrap();
339 AgentKey::issue(&user, meta(scoped(1)))
340 .cert()
341 .verify_with(&st)
342 .unwrap();
343 }
344
345 #[test]
346 fn chain_of_another_user_refused() {
347 let user = UserKey::generate();
348 let a = AgentKey::issue(&UserKey::generate(), meta(Caveats::top()));
349 let st = RevocationLog::genesis(&user).unwrap().verify().unwrap();
350 assert!(is_state_err(a.cert().verify_with(&st)));
351 }
352
353 #[test]
354 fn log_signed_by_different_user_refused() {
355 let user = UserKey::generate();
356 let mut log = RevocationLog::genesis(&user).unwrap();
357 let other = UserKey::generate();
358 assert!(is_state_err(
359 log.append(&other, RevocationEntry::Generation { current: 1 })
360 ));
361 let node = MerkleNode::new(
363 RevocationEntry::Generation { current: 1 },
364 [log.verify().unwrap().head],
365 );
366 log.entries.push(sign_node(&other, node).unwrap());
367 assert!(matches!(log.verify(), Err(MeshError::BadSignature)));
368 }
369
370 #[test]
371 fn tampered_node_refused() {
372 let user = UserKey::generate();
373 let a = AgentKey::issue(&user, meta(Caveats::top()));
374 let mut log = RevocationLog::genesis(&user).unwrap();
375 revoke(&user, &mut log, &a);
376 let b = AgentKey::issue(&user, meta(Caveats::top()));
377 log.entries[1].node.payload = RevocationEntry::Revoke {
378 agent: b.fingerprint(),
379 };
380 assert!(matches!(log.verify(), Err(MeshError::BadSignature)));
381 }
382
383 #[test]
384 fn reordered_log_refused() {
385 let user = UserKey::generate();
386 let mut log = RevocationLog::genesis(&user).unwrap();
387 for g in 1..=2 {
388 log.append(&user, RevocationEntry::Generation { current: g })
389 .unwrap();
390 }
391 log.entries.swap(1, 2);
392 assert!(is_state_err(log.verify()));
393 }
394
395 #[test]
396 fn non_increasing_generation_refused() {
397 let user = UserKey::generate();
398 let mut log = RevocationLog::genesis(&user).unwrap();
399 log.append(&user, RevocationEntry::Generation { current: 2 })
400 .unwrap();
401 assert!(is_state_err(
402 log.append(&user, RevocationEntry::Generation { current: 2 })
403 ));
404 let node = MerkleNode::new(
406 RevocationEntry::Generation { current: 1 },
407 [log.verify().unwrap().head],
408 );
409 log.entries.push(sign_node(&user, node).unwrap());
410 assert!(is_state_err(log.verify()));
411 }
412
413 #[test]
414 fn missing_genesis_refused() {
415 let user = UserKey::generate();
416 assert!(is_state_err(RevocationLog { entries: vec![] }.verify()));
417 let mut log = RevocationLog::genesis(&user).unwrap();
418 log.append(&user, RevocationEntry::Generation { current: 1 })
419 .unwrap();
420 log.entries.remove(0);
421 assert!(is_state_err(log.verify()));
422 }
423
424 #[test]
425 fn extends_refuses_shorter_and_forked_accepts_extension() {
426 let user = UserKey::generate();
427 let mut log = RevocationLog::genesis(&user).unwrap();
428 let base = log.clone();
429 log.append(&user, RevocationEntry::Generation { current: 1 })
430 .unwrap();
431 let seen = log.verify().unwrap().seen();
432 assert!(is_state_err(base.extends(&seen)));
433 let mut fork = base.clone();
434 fork.append(&user, RevocationEntry::Generation { current: 7 })
435 .unwrap();
436 assert!(is_state_err(fork.extends(&seen)));
437 let mut longer = log.clone();
438 longer
439 .append(&user, RevocationEntry::Generation { current: 2 })
440 .unwrap();
441 longer.extends(&seen).unwrap();
442 log.extends(&seen).unwrap();
443 }
444
445 #[test]
446 fn extends_refuses_other_user_and_json_roundtrips() {
447 let user = UserKey::generate();
448 let mut log = RevocationLog::genesis(&user).unwrap();
449 log.append(&user, RevocationEntry::Generation { current: 1 })
450 .unwrap();
451 let mut seen = log.verify().unwrap().seen();
452 let back: RevocationLog =
453 serde_json::from_str(&serde_json::to_string(&log).unwrap()).unwrap();
454 assert_eq!(back, log);
455 back.verify().unwrap();
456 seen.user = UserKey::generate().fingerprint();
457 assert!(is_state_err(log.extends(&seen)));
458 }
459
460 #[test]
464 fn signature_without_the_revocation_domain_is_refused() {
465 let user = UserKey::generate();
466 let base = RevocationLog::genesis(&user).unwrap();
467 let head = base.verify().unwrap().head;
468 let node = MerkleNode::new(RevocationEntry::Generation { current: 1 }, [head]);
469 let cid = node.id().unwrap().to_bytes();
470 let assertion = [b"agent-mesh/provenance/assertion/v1\0".as_slice(), &cid].concat();
471 for msg in [cid.clone(), assertion] {
472 let mut log = base.clone();
473 log.entries.push(SignedEntry {
474 node: node.clone(),
475 sig: SerdeSig(user.sign(&msg)),
476 });
477 assert!(matches!(log.verify(), Err(MeshError::BadSignature)));
478 }
479 let good = [REVOCATION_DOMAIN, &cid].concat();
481 let mut log = base;
482 log.entries.push(SignedEntry {
483 node,
484 sig: SerdeSig(user.sign(&good)),
485 });
486 log.verify().unwrap();
487 }
488}