1use std::fs::{self, File, OpenOptions};
9use std::io::{Read, Write};
10use std::path::{Path, PathBuf};
11
12use ed25519_dalek::SigningKey;
13use clawft_types::project::cert::{key_id, ProjectCert};
14use serde::{Deserialize, Serialize};
15use weftos_leaf_types::link::{LeafCertificate, ParentAdvertisement, PublishAck, SignedPublish};
16
17pub const MAX_LEAF_FRAME: usize = 32 * 1024;
18pub const MAX_LEAF_PAYLOAD: usize = 16 * 1024;
19pub const DISCOVERY_MAGIC: &[u8; 4] = b"WLD1";
20const MAX_ENROLLMENT_FILE: usize = 4096;
21const MAX_FLOOR_FILE: usize = 512;
22
23fn private_dir(path: &Path) -> Result<(), LeafError> {
24 match fs::symlink_metadata(path) {
25 Ok(meta) if !meta.is_dir() => return Err(LeafError::Artifact(format!("{} is not a directory", path.display()))),
26 Ok(_) => {},
27 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
28 let mut builder = fs::DirBuilder::new();
29 #[cfg(unix)] {
30 use std::os::unix::fs::DirBuilderExt;
31 builder.mode(0o700);
32 }
33 builder.create(path)?;
34 },
35 Err(e) => return Err(e.into()),
36 }
37 let meta = fs::symlink_metadata(path)?;
38 #[cfg(unix)] {
39 use std::os::unix::fs::{MetadataExt, PermissionsExt};
40 if meta.uid() != unsafe { libc::geteuid() } || meta.permissions().mode() & 0o077 != 0 {
41 return Err(LeafError::Artifact(format!("{} must be owned by the service and mode 0700", path.display())));
42 }
43 }
44 Ok(())
45}
46
47fn read_bounded(path: &Path, max: usize) -> Result<Option<Vec<u8>>, LeafError> {
48 let mut opts = OpenOptions::new(); opts.read(true);
49 #[cfg(unix)] {
50 use std::os::unix::fs::OpenOptionsExt;
51 opts.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK);
52 }
53 let f = match opts.open(path) {
54 Ok(f) => f,
55 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
56 Err(e) => return Err(e.into()),
57 };
58 let meta = f.metadata()?;
59 if !meta.is_file() || meta.len() > max as u64 {
60 return Err(LeafError::Artifact(format!("{} is not a bounded regular file", path.display())));
61 }
62 #[cfg(unix)] {
63 use std::os::unix::fs::MetadataExt;
64 if meta.uid() != unsafe { libc::geteuid() } {
65 return Err(LeafError::Artifact(format!("{} is not owned by the service", path.display())));
66 }
67 }
68 let mut bytes = Vec::new();
69 f.take(max as u64 + 1).read_to_end(&mut bytes)?;
70 if bytes.len() > max { return Err(LeafError::Artifact(format!("{} exceeds {} bytes", path.display(), max))); }
71 Ok(Some(bytes))
72}
73
74#[derive(Debug, thiserror::Error)]
75pub enum LeafError {
76 #[error("leaf io: {0}")]
77 Io(#[from] std::io::Error),
78 #[error("leaf artifact: {0}")]
79 Artifact(String),
80 #[error("leaf certificate is not enrolled or has been revoked")]
81 NotEnrolled,
82 #[error("leaf certificate/signature: {0:?}")]
83 Signature(weftos_leaf_types::link::LinkError),
84 #[error("leaf sequence gap or regression: expected {expected}, got {got}")]
85 Sequence { expected: u64, got: u64 },
86 #[error("leaf sequence reused with different content")]
87 Equivocation,
88 #[error("leaf payload exceeds {MAX_LEAF_PAYLOAD} bytes")]
89 TooLarge,
90 #[error("leaf parent does not own the certified tenant scope: {0}")]
91 WrongScope(String),
92}
93
94#[derive(Deserialize)]
95struct Enrollment {
96 node_id: String,
97 cert: LeafCertificate,
98 #[serde(default)]
99 project_cert: Option<ProjectCert>,
100}
101
102fn hex_key(s: &str) -> Option<[u8; 32]> {
103 let raw = hex::decode(s).ok()?;
104 raw.try_into().ok()
105}
106
107pub fn verify_scope_chain(cert: &LeafCertificate, project_cert: Option<&ProjectCert>, now: u64) -> Result<[u8; 32], LeafError> {
111 let (user_id, project_id) = weftos_leaf_types::link::parse_parent_scope(&cert.parent_scope)
112 .map_err(|e| LeafError::WrongScope(format!("scope syntax: {e:?}")))?;
113 match project_id {
114 None => {
115 if project_cert.is_some() || key_id(&cert.parent_pubkey) != user_id {
116 return Err(LeafError::WrongScope("user issuer key ID does not match user scope".into()));
117 }
118 Ok(cert.parent_pubkey)
119 }
120 Some(project_id) => {
121 let pc = project_cert.ok_or_else(|| LeafError::WrongScope("project certificate missing".into()))?;
122 let user_key = hex_key(&pc.user_pubkey).ok_or_else(|| LeafError::WrongScope("project user key malformed".into()))?;
123 let at = i64::try_from(now).ok().and_then(|s| chrono::DateTime::<chrono::Utc>::from_timestamp(s, 0))
124 .ok_or_else(|| LeafError::WrongScope("invalid verification time".into()))?;
125 pc.verify(&user_key, at).map_err(|e| LeafError::WrongScope(format!("project certificate: {e}")))?;
126 if pc.project_id != project_id || pc.project_key_id != key_id(&cert.parent_pubkey)
127 || pc.user_key_id != user_id || hex_key(&pc.project_pubkey) != Some(cert.parent_pubkey) {
128 return Err(LeafError::WrongScope("project certificate does not bind issuer and tenant".into()));
129 }
130 Ok(user_key)
131 }
132 }
133}
134
135#[derive(Serialize, Deserialize)]
136struct Floor {
137 seq: u64,
138 digest: [u8; 32],
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142pub enum Prepare { New, Duplicate }
143
144type ScopeAuthority = std::sync::Arc<dyn Fn(&str, &[u8; 32], Option<&ProjectCert>) -> bool + Send + Sync>;
145
146pub struct LeafIngress {
147 registry: PathBuf,
148 floors: PathBuf,
149 machine_key: SigningKey,
150 scope_authority: Option<ScopeAuthority>,
151 pub lock: tokio::sync::Mutex<()>,
154}
155
156impl LeafIngress {
157 pub fn open(root: &Path, machine_key: SigningKey) -> Result<Self, LeafError> {
160 let registry = root.join("registry");
161 let floors = root.join("floors");
162 private_dir(root)?;
163 private_dir(®istry)?;
164 private_dir(&floors)?;
165 Ok(Self { registry, floors, machine_key, scope_authority: None, lock: tokio::sync::Mutex::new(()) })
166 }
167
168 pub fn with_scope_authority(mut self, authority: ScopeAuthority) -> Self {
171 self.scope_authority = Some(authority);
172 self
173 }
174
175 fn authorized(&self, enrolled: &Enrollment, cert: &LeafCertificate, now: u64) -> bool {
176 if enrolled.node_id != cert.leaf_id() || enrolled.cert != *cert
177 || cert.mesh_pubkey != self.machine_key.verifying_key().to_bytes()
178 || cert.verify(&cert.parent_pubkey, now).is_err() { return false; }
179 let Ok(user_key) = verify_scope_chain(cert, enrolled.project_cert.as_ref(), now) else { return false };
180 let (user_id, _) = weftos_leaf_types::link::parse_parent_scope(&cert.parent_scope).expect("verified scope");
181 self.scope_authority.as_ref().is_none_or(|authority| authority(user_id, &user_key, enrolled.project_cert.as_ref()))
182 }
183
184 fn floor_path(&self, id: &str) -> PathBuf { self.floors.join(format!("{id}.json")) }
185
186 fn read_floor(&self, id: &str) -> Result<Option<Floor>, LeafError> {
187 read_bounded(&self.floor_path(id), MAX_FLOOR_FILE)?
188 .map(|bytes| serde_json::from_slice(&bytes).map_err(|e| LeafError::Artifact(format!("invalid replay floor: {e}"))))
189 .transpose()
190 }
191
192 pub fn still_enrolled(&self, cert: &LeafCertificate, now: u64) -> bool {
195 let id = cert.leaf_id();
196 let Ok(Some(bytes)) = read_bounded(&self.registry.join(format!("{id}.json")), MAX_ENROLLMENT_FILE) else { return false };
197 let Ok(enrolled) = serde_json::from_slice::<Enrollment>(&bytes) else { return false };
198 self.authorized(&enrolled, cert, now)
199 }
200
201 pub fn prepare(&self, frame: &SignedPublish, now: u64) -> Result<Prepare, LeafError> {
204 if frame.payload.len() > MAX_LEAF_PAYLOAD { return Err(LeafError::TooLarge); }
205 let id = frame.cert.leaf_id();
206 let bytes = read_bounded(&self.registry.join(format!("{id}.json")), MAX_ENROLLMENT_FILE)?.ok_or(LeafError::NotEnrolled)?;
207 let enrolled: Enrollment = serde_json::from_slice(&bytes).map_err(|e| LeafError::Artifact(format!("invalid enrollment: {e}")))?;
208 if !self.authorized(&enrolled, &frame.cert, now) {
209 return Err(LeafError::NotEnrolled);
210 }
211 frame.verify(&enrolled.cert.parent_pubkey, now).map_err(LeafError::Signature)?;
212 match self.read_floor(&id)? {
213 None if frame.seq == 1 => Ok(Prepare::New),
214 None => Err(LeafError::Sequence { expected: 1, got: frame.seq }),
215 Some(floor) if frame.seq == floor.seq => {
216 if frame.digest() == floor.digest { Ok(Prepare::Duplicate) } else { Err(LeafError::Equivocation) }
217 }
218 Some(floor) if frame.seq == floor.seq.saturating_add(1) => Ok(Prepare::New),
219 Some(floor) => Err(LeafError::Sequence { expected: floor.seq.saturating_add(1), got: frame.seq }),
220 }
221 }
222
223 pub fn commit(&self, frame: &SignedPublish) -> Result<(), LeafError> {
227 let id = frame.cert.leaf_id();
228 let path = self.floor_path(&id);
229 let tmp = self.floors.join(format!(".{id}.{:016x}.tmp", rand::random::<u64>()));
230 let bytes = serde_json::to_vec(&Floor { seq: frame.seq, digest: frame.digest() })
231 .map_err(|e| LeafError::Artifact(e.to_string()))?;
232 let mut opts = OpenOptions::new(); opts.write(true).create_new(true);
233 #[cfg(unix)] {
234 use std::os::unix::fs::OpenOptionsExt;
235 opts.mode(0o600);
236 }
237 let mut f = opts.open(&tmp)?;
238 let result = (|| -> Result<(), LeafError> {
239 f.write_all(&bytes)?;
240 f.sync_all()?;
241 fs::rename(&tmp, &path)?;
242 File::open(&self.floors)?.sync_all()?;
243 Ok(())
244 })();
245 if result.is_err() { let _ = fs::remove_file(&tmp); }
246 result
247 }
248
249 pub fn ack(&self, frame: &SignedPublish) -> PublishAck { PublishAck::sign(&self.machine_key, frame) }
250
251 fn advertisement(&self, id: &str, endpoint: String, now: u64, nonce: [u8; 16]) -> Option<ParentAdvertisement> {
252 if id.len() != 32 || !id.bytes().all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase()) { return None; }
253 let bytes = read_bounded(&self.registry.join(format!("{id}.json")), MAX_ENROLLMENT_FILE).ok()??;
254 let enrolled: Enrollment = serde_json::from_slice(&bytes).ok()?;
255 if !self.authorized(&enrolled, &enrolled.cert, now) { return None; }
256 Some(ParentAdvertisement::sign(&self.machine_key, enrolled.cert.parent_scope, endpoint, now.saturating_add(30), nonce))
257 }
258
259 pub async fn serve_discovery(&self, mesh_addr: std::net::SocketAddr, leaf_addr: std::net::SocketAddr) -> Result<(), LeafError> {
263 let port = mesh_addr.port().checked_add(1).ok_or_else(|| LeafError::Artifact("mesh port has no discovery successor".into()))?;
264 let bind = std::net::SocketAddr::new(mesh_addr.ip(), port);
265 let sock = tokio::net::UdpSocket::bind(bind).await?;
266 let mut request = [0u8; 64];
267 loop {
268 let (n, peer) = sock.recv_from(&mut request).await?;
269 if n != 52 || &request[..4] != DISCOVERY_MAGIC { continue; }
270 let Ok(id) = std::str::from_utf8(&request[4..36]) else { continue };
271 let mut nonce = [0u8; 16]; nonce.copy_from_slice(&request[36..52]);
272 let ip = if mesh_addr.ip().is_unspecified() {
273 let probe = std::net::UdpSocket::bind(std::net::SocketAddr::new(mesh_addr.ip(), 0))?;
274 if probe.connect(peer).is_err() { continue; }
275 match probe.local_addr() { Ok(a) => a.ip(), Err(_) => continue }
276 } else { mesh_addr.ip() };
277 let endpoint = std::net::SocketAddr::new(ip, leaf_addr.port()).to_string();
278 let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
279 if let Some(ad) = self.advertisement(id, endpoint, now, nonce)
280 && let Ok(bytes) = weftos_leaf_types::encode(&ad)
281 && bytes.len() <= 1024 {
282 let _ = sock.send_to(&bytes, peer).await;
283 }
284 }
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use clawft_types::project::cert::{CertRequest, ProjectCert};
292 use ed25519_dalek::SigningKey;
293 use weftos_leaf_types::link::node_id;
294
295 #[test]
296 fn scope_chain_rejects_another_users_scope_and_requires_project_issuer_proof() {
297 let user_a = SigningKey::from_bytes(&[51; 32]);
298 let user_b = SigningKey::from_bytes(&[52; 32]);
299 let project = SigningKey::from_bytes(&[53; 32]);
300 let leaf = SigningKey::from_bytes(&[54; 32]);
301 let machine = SigningKey::from_bytes(&[55; 32]);
302 let id_a = key_id(&user_a.verifying_key().to_bytes());
303 let id_b = key_id(&user_b.verifying_key().to_bytes());
304 let now = chrono::Utc::now();
305 let secs = now.timestamp() as u64;
306 let wrong_user = LeafCertificate::issue(&user_a, format!("user:{id_b}"), machine.verifying_key().to_bytes(),
307 leaf.verifying_key().to_bytes(), 1, secs - 1, secs + 60, vec!["input.publish".into()]).unwrap();
308 assert!(verify_scope_chain(&wrong_user, None, secs).is_err());
309
310 let project_id = "01HZY7Y9M3MZQ5RSY3MTEP6F3C";
311 let chain = ProjectCert::sign(&user_a, &CertRequest {
312 project_id: project_id.into(), project_pubkey: project.verifying_key().to_bytes(),
313 serial: 1, issued_at: now, expires_at: None,
314 });
315 let good = LeafCertificate::issue(&project, format!("project:{id_a}:{project_id}"), machine.verifying_key().to_bytes(),
316 leaf.verifying_key().to_bytes(), 2, secs - 1, secs + 60, vec!["input.publish".into()]).unwrap();
317 assert_eq!(verify_scope_chain(&good, Some(&chain), secs).unwrap(), user_a.verifying_key().to_bytes());
318 assert!(verify_scope_chain(&good, None, secs).is_err());
319 let forged_scope = LeafCertificate::issue(&project, format!("project:{id_b}:{project_id}"), machine.verifying_key().to_bytes(),
320 leaf.verifying_key().to_bytes(), 3, secs - 1, secs + 60, vec!["input.publish".into()]).unwrap();
321 assert!(verify_scope_chain(&forged_scope, Some(&chain), secs).is_err());
322 let other_project = SigningKey::from_bytes(&[56; 32]);
323 let wrong_issuer = LeafCertificate::issue(&other_project, format!("project:{id_a}:{project_id}"), machine.verifying_key().to_bytes(),
324 leaf.verifying_key().to_bytes(), 4, secs - 1, secs + 60, vec!["input.publish".into()]).unwrap();
325 assert!(verify_scope_chain(&wrong_issuer, Some(&chain), secs).is_err());
326
327 let dir = tempfile::tempdir().unwrap();
328 #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; fs::set_permissions(dir.path(), fs::Permissions::from_mode(0o700)).unwrap(); }
329 let expected_user = id_a.clone();
330 let expected_key = user_a.verifying_key().to_bytes();
331 let ingress = LeafIngress::open(dir.path(), machine).unwrap().with_scope_authority(std::sync::Arc::new(move |id, key, _| {
332 id == expected_user && *key == expected_key
333 }));
334 let leaf_id = good.leaf_id();
335 fs::write(ingress.registry.join(format!("{leaf_id}.json")),
336 serde_json::json!({"node_id": leaf_id, "cert": good.clone(), "project_cert": chain.clone()}).to_string()).unwrap();
337 let frame = SignedPublish::sign(good.clone(), &leaf, 1, format!("mesh.leaf.{leaf_id}.input"), vec![1]).unwrap();
338 assert_eq!(ingress.prepare(&frame, secs).unwrap(), Prepare::New);
339 let denied = LeafIngress::open(dir.path(), ingress.machine_key.clone()).unwrap()
340 .with_scope_authority(std::sync::Arc::new(|_, _, _| false));
341 assert!(matches!(denied.prepare(&frame, secs), Err(LeafError::NotEnrolled)));
342 }
343
344 #[test]
345 fn enrolled_frame_replays_exactly_once_across_restart() {
346 let dir = tempfile::tempdir().unwrap();
347 #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; fs::set_permissions(dir.path(), fs::Permissions::from_mode(0o700)).unwrap(); }
348 let parent = SigningKey::from_bytes(&[3;32]);
349 let user_id = clawft_types::project::cert::key_id(&parent.verifying_key().to_bytes());
350 let machine = SigningKey::from_bytes(&[4;32]);
351 let leaf = SigningKey::from_bytes(&[5;32]);
352 let cert = LeafCertificate::issue(&parent, format!("user:{user_id}"), machine.verifying_key().to_bytes(), leaf.verifying_key().to_bytes(), 1, 1, 1000, vec!["input.publish".into()]).unwrap();
353 let id = node_id(&cert.leaf_pubkey);
354 let frame = SignedPublish::sign(cert.clone(), &leaf, 1, format!("mesh.leaf.{id}.input"), vec![1]).unwrap();
355 let ingress = LeafIngress::open(dir.path(), machine.clone()).unwrap();
356 fs::write(ingress.registry.join(format!("{id}.json")), serde_json::json!({"node_id":id,"cert":cert}).to_string()).unwrap();
357 assert_eq!(ingress.prepare(&frame, 10).unwrap(), Prepare::New);
358 ingress.commit(&frame).unwrap();
359 let ingress = LeafIngress::open(dir.path(), machine).unwrap();
360 assert_eq!(ingress.prepare(&frame, 10).unwrap(), Prepare::Duplicate);
361 assert!(ingress.still_enrolled(&frame.cert, 10));
362 assert!(!ingress.still_enrolled(&frame.cert, 1000));
363 ingress.ack(&frame).verify(&frame.cert.mesh_pubkey, &frame).unwrap();
364 let mut altered = frame.clone(); altered.payload.push(2);
365 assert!(matches!(ingress.prepare(&altered, 10), Err(LeafError::Signature(_))));
366 fs::remove_file(ingress.registry.join(format!("{id}.json"))).unwrap();
367 assert!(!ingress.still_enrolled(&frame.cert, 10));
368 assert!(matches!(ingress.prepare(&frame, 10), Err(LeafError::NotEnrolled)));
369 }
370
371 #[test]
372 fn discovery_only_answers_enrolled_leaf_with_signed_endpoint_and_nonce() {
373 let dir = tempfile::tempdir().unwrap();
374 #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; fs::set_permissions(dir.path(), fs::Permissions::from_mode(0o700)).unwrap(); }
375 let parent = SigningKey::from_bytes(&[13; 32]);
376 let user_id = clawft_types::project::cert::key_id(&parent.verifying_key().to_bytes());
377 let machine = SigningKey::from_bytes(&[14; 32]);
378 let leaf = SigningKey::from_bytes(&[15; 32]);
379 let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
380 let cert = LeafCertificate::issue(&parent, format!("user:{user_id}"), machine.verifying_key().to_bytes(), leaf.verifying_key().to_bytes(), 1, now - 1, now + 60, vec!["input.publish".into()]).unwrap();
381 let id = cert.leaf_id();
382 let ingress = LeafIngress::open(dir.path(), machine.clone()).unwrap();
383 fs::write(ingress.registry.join(format!("{id}.json")), serde_json::json!({"node_id":id,"cert":cert}).to_string()).unwrap();
384 let endpoint = "127.0.0.1:9489".to_string();
385 let ad = ingress.advertisement(&id, endpoint.clone(), now, [9; 16]).unwrap();
386 ad.verify_for(&machine.verifying_key().to_bytes(), &cert.parent_scope, now, &[9; 16]).unwrap();
387 assert_eq!(ad.endpoint, endpoint);
388 assert!(ad.verify_for(&machine.verifying_key().to_bytes(), &cert.parent_scope, now, &[8; 16]).is_err());
389 assert!(ingress.advertisement(&"f".repeat(32), endpoint, now, [9; 16]).is_none());
390 }
391}