1use std::sync::Arc;
31
32use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
33use serde::{Deserialize, Serialize};
34
35use crate::ipc::{KernelMessage, MessagePayload, MessageTarget};
36use crate::mesh_artifact::ArtifactExchange;
37use crate::mesh_delivery::PeerCtx;
38use crate::mesh_runtime::{MeshRuntime, PeerControlSink, REVOKE_TOPIC};
39use crate::revocation::{RevocationKind, RevocationList};
40use crate::workload_pkg::{KeyOrigin, TrustAnchors};
41
42pub const REVOKE_DOMAIN: &[u8] = b"weftos.artifact_revocation.v1\0";
44pub const MAX_NOTICE_BYTES: usize = 4096;
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49pub struct RevocationNotice {
50 pub kind: RevocationKind,
52 pub id: String,
54 pub reason: String,
56 pub issued_at: u64,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62pub struct SignedRevocation {
63 pub payload: String,
65 pub public_key: Vec<u8>,
67 pub signature: Vec<u8>,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
73pub enum NoticeError {
74 #[error("malformed revocation notice: {0}")]
76 Malformed(String),
77 #[error("revocation signature does not verify")]
79 BadSignature,
80 #[error("revocation signer is not a pinned operator key")]
82 UnauthorizedSigner,
83 #[error("revocation signer key is revoked")]
85 SignerRevoked,
86 #[error("revocation notices are arriving too fast")]
88 RateLimited,
89}
90
91fn signed_bytes(payload: &str) -> Vec<u8> {
92 let mut v = REVOKE_DOMAIN.to_vec();
93 v.extend_from_slice(payload.as_bytes());
94 v
95}
96
97pub fn sign_revocation(
99 kind: RevocationKind,
100 id: &str,
101 reason: &str,
102 now: u64,
103 key: &SigningKey,
104) -> Result<SignedRevocation, NoticeError> {
105 let id = kind
106 .normalize(id)
107 .map_err(|e| NoticeError::Malformed(e.to_string()))?;
108 let payload = serde_json::to_string(&RevocationNotice {
109 kind,
110 id,
111 reason: reason.chars().take(256).collect(),
112 issued_at: now,
113 })
114 .map_err(|e| NoticeError::Malformed(e.to_string()))?;
115 let sig = key.sign(&signed_bytes(&payload));
116 Ok(SignedRevocation {
117 payload,
118 public_key: key.verifying_key().to_bytes().to_vec(),
119 signature: sig.to_bytes().to_vec(),
120 })
121}
122
123pub fn verify_revocation(
125 signed: &SignedRevocation,
126 anchors: &TrustAnchors,
127) -> Result<RevocationNotice, NoticeError> {
128 if signed.payload.len() > MAX_NOTICE_BYTES {
129 return Err(NoticeError::Malformed("payload too large".into()));
130 }
131 let pk: [u8; 32] = signed
132 .public_key
133 .as_slice()
134 .try_into()
135 .map_err(|_| NoticeError::Malformed("public key length".into()))?;
136 let sig: [u8; 64] = signed
137 .signature
138 .as_slice()
139 .try_into()
140 .map_err(|_| NoticeError::Malformed("signature length".into()))?;
141 let vk = VerifyingKey::from_bytes(&pk).map_err(|_| NoticeError::BadSignature)?;
142 vk.verify_strict(&signed_bytes(&signed.payload), &Signature::from_bytes(&sig))
143 .map_err(|_| NoticeError::BadSignature)?;
144 let pinned = anchors.signer(&pk).ok_or(NoticeError::UnauthorizedSigner)?;
145 if !matches!(pinned.origin, KeyOrigin::Operator | KeyOrigin::Weftos) {
146 return Err(NoticeError::UnauthorizedSigner);
147 }
148 let notice: RevocationNotice = serde_json::from_str(&signed.payload)
149 .map_err(|e| NoticeError::Malformed(e.to_string()))?;
150 let canonical = notice
151 .kind
152 .normalize(¬ice.id)
153 .map_err(|e| NoticeError::Malformed(e.to_string()))?;
154 if canonical != notice.id {
155 return Err(NoticeError::Malformed("id is not canonical".into()));
156 }
157 Ok(notice)
158}
159
160pub struct RevocationExchange {
162 ex: Arc<ArtifactExchange>,
163 list: Arc<RevocationList>,
164 anchors: TrustAnchors,
165 runtime: Arc<MeshRuntime>,
166 me: std::sync::Weak<Self>,
167 buckets: dashmap::DashMap<u64, (f64, std::time::Instant)>,
170 seen: std::sync::Mutex<Seen>,
172 on_applied: std::sync::OnceLock<AppliedHook>,
174 log: std::sync::Mutex<log::NoticeLog>,
176 replays: std::sync::Mutex<log::Replays>,
178 replays_started: std::sync::atomic::AtomicU64,
179}
180
181pub type AppliedHook = Arc<dyn Fn(&RevocationNotice) + Send + Sync>;
185
186type Seen = (std::collections::HashSet<[u8; 32]>, std::collections::VecDeque<[u8; 32]>);
188
189const MAX_TRACKED_CONNS: usize = 1024;
191const MAX_SEEN: usize = 4096;
193
194pub const NOTICES_PER_SEC: f64 = 2.0;
196pub const NOTICE_BURST: f64 = 10.0;
198
199impl RevocationExchange {
200 pub fn start(
204 ex: Arc<ArtifactExchange>,
205 list: Arc<RevocationList>,
206 anchors: TrustAnchors,
207 runtime: Arc<MeshRuntime>,
208 ) -> Arc<Self> {
209 ex.set_revocations(list.clone());
210 let me = Arc::new_cyclic(|w| Self {
211 ex,
212 list,
213 anchors,
214 runtime: runtime.clone(),
215 me: w.clone(),
216 buckets: dashmap::DashMap::new(),
217 seen: Default::default(),
218 on_applied: std::sync::OnceLock::new(),
219 log: Default::default(),
220 replays: Default::default(),
221 replays_started: Default::default(),
222 });
223 runtime.set_control_sink(REVOKE_TOPIC, me.clone());
224 Self::spawn_rejoin_replay(&me);
225 me
226 }
227
228 pub fn set_on_applied(&self, hook: AppliedHook) -> bool {
232 self.on_applied.set(hook).is_ok()
233 }
234
235 fn take_token(&self, conn: u64) -> bool {
237 if self.buckets.len() >= MAX_TRACKED_CONNS && !self.buckets.contains_key(&conn) {
238 let oldest = self.buckets.iter().min_by_key(|b| b.1).map(|b| *b.key());
239 if let Some(k) = oldest {
240 self.buckets.remove(&k);
241 }
242 }
243 let now = std::time::Instant::now();
244 let mut b = self.buckets.entry(conn).or_insert((NOTICE_BURST, now));
245 let dt = now.duration_since(b.1).as_secs_f64();
246 b.1 = now;
247 b.0 = (b.0 + dt * NOTICES_PER_SEC).min(NOTICE_BURST);
248 if b.0 < 1.0 {
249 return false;
250 }
251 b.0 -= 1.0;
252 true
253 }
254
255 fn notice_key(signed: &SignedRevocation) -> [u8; 32] {
256 let mut h = blake3::Hasher::new();
257 h.update(signed.payload.as_bytes());
258 h.update(&signed.public_key);
259 h.update(&signed.signature);
260 *h.finalize().as_bytes()
261 }
262
263 pub fn accept(&self, signed: &SignedRevocation) -> Result<bool, NoticeError> {
267 self.accept_from(signed, Some(0))
268 }
269
270 fn accept_from(
277 &self,
278 signed: &SignedRevocation,
279 conn: Option<u64>,
280 ) -> Result<bool, NoticeError> {
281 if signed.payload.len() > MAX_NOTICE_BYTES {
282 return Err(NoticeError::Malformed("payload too large".into()));
283 }
284 let pk: [u8; 32] = signed
285 .public_key
286 .as_slice()
287 .try_into()
288 .map_err(|_| NoticeError::Malformed("public key length".into()))?;
289 match self.anchors.signer(&pk) {
290 Some(k) if matches!(k.origin, KeyOrigin::Operator | KeyOrigin::Weftos) => {}
291 _ => return Err(NoticeError::UnauthorizedSigner),
292 }
293 if self
294 .list
295 .is_subject_revoked(RevocationKind::SignerKey, &crate::workload_pkg::codec::hex_encode(&pk))
296 {
297 return Err(NoticeError::SignerRevoked);
298 }
299 let key = Self::notice_key(signed);
300 if self.seen.lock().unwrap_or_else(|p| p.into_inner()).0.contains(&key) {
301 return Ok(false);
302 }
303 if let Some(conn) = conn
304 && !self.take_token(conn)
305 {
306 return Err(NoticeError::RateLimited);
307 }
308 let n = verify_revocation(signed, &self.anchors)?;
309 let by = format!(
310 "mesh:{}",
311 crate::workload_pkg::codec::hex_encode(&pk[..4])
312 );
313 let chain = self.ex.chain.clone();
314 let sink = chain.as_ref().map(|cm| {
315 let cm = cm.clone();
316 move |k: &str, p: serde_json::Value| {
317 cm.append(crate::workload_governance::gate::CHAIN_SOURCE, k, Some(p));
318 }
319 });
320 let new = match self.list.revoke_audited(
321 n.kind,
322 &n.id,
323 &n.reason,
324 &by,
325 sink.as_ref().map(|f| f as &dyn Fn(&str, serde_json::Value)),
326 ) {
327 Ok(new) => new,
328 Err(crate::revocation::RevocationError::Persist(e)) => {
332 tracing::error!(error = %e, "revocation applied but not persisted");
333 true
334 }
335 Err(e) => return Err(NoticeError::Malformed(e.to_string())),
336 };
337 {
338 let mut g = self.seen.lock().unwrap_or_else(|p| p.into_inner());
339 if g.0.insert(key) {
340 g.1.push_back(key);
341 if g.1.len() > MAX_SEEN
342 && let Some(old) = g.1.pop_front()
343 {
344 g.0.remove(&old);
345 }
346 }
347 }
348 self.log_notice(signed);
349 if new {
350 self.ex.apply_revocations();
351 if let Some(hook) = self.on_applied.get() {
352 hook(&n);
353 }
354 }
355 Ok(new)
356 }
357
358 pub async fn issue(&self, signed: SignedRevocation) -> Result<bool, NoticeError> {
362 let new = self.accept_from(&signed, None)?;
363 self.flood(&signed, None).await;
364 Ok(new)
365 }
366
367 async fn flood(&self, signed: &SignedRevocation, except: Option<&str>) {
368 let Ok(value) = serde_json::to_value(signed) else {
369 return;
370 };
371 for peer in self.runtime.peer_ids() {
372 if Some(peer.as_str()) == except {
373 continue;
374 }
375 let msg = KernelMessage::new(
376 0,
377 MessageTarget::Topic(REVOKE_TOPIC.to_string()),
378 MessagePayload::Json(value.clone()),
379 );
380 if let Err(e) = self.runtime.route_to_remote(&peer, msg).await {
381 tracing::debug!(peer, error = %e, "revocation forward failed");
382 }
383 }
384 }
385}
386
387impl PeerControlSink for RevocationExchange {
388 fn on_peer_control(
389 &self,
390 ctx: &PeerCtx,
391 conn: u64,
392 payload: &serde_json::Value,
393 ) -> Vec<serde_json::Value> {
394 let signed: SignedRevocation = match serde_json::from_value(payload.clone()) {
395 Ok(s) => s,
396 Err(e) => {
397 tracing::warn!(peer = %ctx.peer_id, error = %e, "malformed revocation message");
398 return Vec::new();
399 }
400 };
401 match self.accept_from(&signed, Some(conn)) {
402 Ok(true) => {
403 if let Some(me) = self.me.upgrade() {
406 let from = ctx.peer_id.clone();
407 tokio::spawn(async move { me.flood(&signed, Some(&from)).await });
408 }
409 }
410 Ok(false) => {}
411 Err(e) => {
412 tracing::warn!(peer = %ctx.peer_id, verified = ctx.node_verified, error = %e,
413 "revocation notice refused");
414 }
415 }
416 Vec::new()
417 }
418}
419
420#[path = "mesh_swarm_revoke_log.rs"]
421mod log;
422pub use log::{MAX_LOGGED, REPLAY_COOLDOWN};
423
424#[cfg(test)]
425#[path = "mesh_swarm_revoke_tests.rs"]
426mod tests;