1use async_trait::async_trait;
23use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
24use serde::{Deserialize, Serialize};
25
26use crate::node_id::node_id_from_pubkey;
27
28pub use crate::mesh_admit_gate::{
29 AdmissionRecord, CryptoGate, OpenVerdicts, Verdict, VerdictRequest, VerdictSource,
30 VERDICT_TTL,
31};
32#[cfg(feature = "exochain")]
33pub use crate::mesh_admit_gate::GateVerdictSource;
34
35pub const HELLO_KIND: &str = "mesh.admit.hello";
37pub const HELLO_DOMAIN: &[u8] = b"weftos/mesh-admit/v1\0";
39pub const MAX_SKEW_SECS: u64 = 60;
41pub const CAP_LEAF: &str = "leaf";
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct AdmitHello {
47 pub kind: String,
49 pub v: u32,
51 pub node_id: String,
53 pub pubkey: String,
55 #[serde(default)]
57 pub platform: String,
58 #[serde(default)]
60 pub capabilities: Vec<String>,
61 pub genesis_hash: String,
63 pub ts: u64,
65 #[serde(default)]
67 pub noise_static_pub: String,
68 pub sig: String,
70}
71
72#[derive(Debug, Clone, Copy)]
74pub struct ChannelBinding<'a> {
75 pub handshake_hash: &'a [u8],
77 pub remote_static: Option<&'a [u8]>,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct VerifiedHello {
84 pub node_id: String,
86 pub pubkey: [u8; 32],
88 pub platform: String,
90 pub capabilities: Vec<String>,
92 pub genesis_hash: [u8; 32],
94 pub ts: u64,
96}
97
98impl VerifiedHello {
99 pub fn class(&self) -> PeerClass {
106 if self.capabilities.iter().any(|c| c == CAP_LEAF) {
107 PeerClass::Leaf
108 } else {
109 PeerClass::Node
110 }
111 }
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
116pub enum HelloFailure {
117 Missing,
119 Malformed(String),
121 NodeIdMismatch,
123 NoHandshakeBinding,
125 StaticMismatch,
127 BadSignature,
129 ClockSkew,
131}
132
133impl HelloFailure {
134 pub fn code(&self) -> &'static str {
136 match self {
137 Self::Missing => "hello_missing",
138 Self::Malformed(_) => "hello_malformed",
139 Self::NodeIdMismatch => "node_id_mismatch",
140 Self::NoHandshakeBinding => "no_handshake_binding",
141 Self::StaticMismatch => "static_mismatch",
142 Self::BadSignature => "bad_signature",
143 Self::ClockSkew => "clock_skew",
144 }
145 }
146}
147
148pub fn signed_bytes(
157 handshake_hash: &[u8],
158 genesis: &[u8; 32],
159 ts: u64,
160 node_id: &str,
161 platform: &str,
162 capabilities: &[String],
163) -> Vec<u8> {
164 fn put(b: &mut Vec<u8>, s: &str) {
165 b.extend_from_slice(&(s.len() as u32).to_be_bytes());
166 b.extend_from_slice(s.as_bytes());
167 }
168 let mut b = Vec::with_capacity(HELLO_DOMAIN.len() + handshake_hash.len() + 128);
169 b.extend_from_slice(HELLO_DOMAIN);
170 b.extend_from_slice(handshake_hash);
171 b.extend_from_slice(genesis);
172 b.extend_from_slice(&ts.to_be_bytes());
173 put(&mut b, node_id);
174 put(&mut b, platform);
175 b.extend_from_slice(&(capabilities.len() as u32).to_be_bytes());
176 for c in capabilities {
177 put(&mut b, c);
178 }
179 b
180}
181
182impl AdmitHello {
183 pub fn sign(
187 key: &SigningKey,
188 handshake_hash: &[u8],
189 noise_static_pub: &[u8],
190 genesis: &[u8; 32],
191 ts: u64,
192 platform: &str,
193 capabilities: Vec<String>,
194 ) -> Self {
195 let pk = key.verifying_key().to_bytes();
196 let node_id = node_id_from_pubkey(&pk);
197 let sig = key.sign(&signed_bytes(
198 handshake_hash, genesis, ts, &node_id, platform, &capabilities,
199 ));
200 Self {
201 kind: HELLO_KIND.to_owned(),
202 v: 1,
203 node_id,
204 pubkey: hex_encode(&pk),
205 platform: platform.to_owned(),
206 capabilities,
207 genesis_hash: hex_encode(genesis),
208 ts,
209 noise_static_pub: hex_encode(noise_static_pub),
210 sig: hex_encode(&sig.to_bytes()),
211 }
212 }
213
214 pub fn to_bytes(&self) -> Vec<u8> {
216 serde_json::to_vec(self).unwrap_or_default()
217 }
218
219 pub fn parse_frame(data: &[u8]) -> Option<Result<Self, HelloFailure>> {
222 if data.first() != Some(&b'{') {
223 return None;
224 }
225 let v: serde_json::Value = serde_json::from_slice(data).ok()?;
226 if v.get("kind").and_then(|k| k.as_str()) != Some(HELLO_KIND) {
227 return None;
228 }
229 Some(serde_json::from_value(v).map_err(|e| HelloFailure::Malformed(e.to_string())))
230 }
231
232 pub fn verify(
235 &self,
236 binding: Option<&ChannelBinding<'_>>,
237 now: u64,
238 ) -> Result<VerifiedHello, HelloFailure> {
239 if self.v != 1 {
240 return Err(HelloFailure::Malformed(format!("unsupported version {}", self.v)));
241 }
242 let pubkey: [u8; 32] = hex_decode_fixed(&self.pubkey)
243 .ok_or_else(|| HelloFailure::Malformed("pubkey".into()))?;
244 let genesis: [u8; 32] = hex_decode_fixed(&self.genesis_hash)
245 .ok_or_else(|| HelloFailure::Malformed("genesis_hash".into()))?;
246 let sig: [u8; 64] =
247 hex_decode_fixed(&self.sig).ok_or_else(|| HelloFailure::Malformed("sig".into()))?;
248 if node_id_from_pubkey(&pubkey) != self.node_id {
249 return Err(HelloFailure::NodeIdMismatch);
250 }
251 let binding = binding.ok_or(HelloFailure::NoHandshakeBinding)?;
252 let claimed_static = hex_decode(&self.noise_static_pub)
253 .ok_or_else(|| HelloFailure::Malformed("noise_static_pub".into()))?;
254 if binding.remote_static != Some(claimed_static.as_slice()) {
255 return Err(HelloFailure::StaticMismatch);
256 }
257 let vk = VerifyingKey::from_bytes(&pubkey)
258 .map_err(|_| HelloFailure::Malformed("pubkey not on curve".into()))?;
259 if now.abs_diff(self.ts) > MAX_SKEW_SECS {
261 return Err(HelloFailure::ClockSkew);
262 }
263 vk.verify_strict(
264 &signed_bytes(
265 binding.handshake_hash,
266 &genesis,
267 self.ts,
268 &self.node_id,
269 &self.platform,
270 &self.capabilities,
271 ),
272 &Signature::from_bytes(&sig),
273 )
274 .map_err(|_| HelloFailure::BadSignature)?;
275 Ok(VerifiedHello {
276 node_id: self.node_id.clone(),
277 pubkey,
278 platform: self.platform.clone(),
279 capabilities: self.capabilities.clone(),
280 genesis_hash: genesis,
281 ts: self.ts,
282 })
283 }
284}
285
286#[derive(Clone)]
288pub struct DialIdentity {
289 pub key: SigningKey,
291 pub genesis: [u8; 32],
293 pub platform: String,
295 pub capabilities: Vec<String>,
297}
298
299impl DialIdentity {
300 pub fn hello(&self, handshake_hash: &[u8], noise_static: &[u8], ts: u64) -> AdmitHello {
303 AdmitHello::sign(
304 &self.key,
305 handshake_hash,
306 noise_static,
307 &self.genesis,
308 ts,
309 &self.platform,
310 self.capabilities.clone(),
311 )
312 }
313}
314
315#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
317pub enum PeerClass {
318 Node,
320 Leaf,
322 Legacy,
324}
325
326impl PeerClass {
327 pub fn as_str(self) -> &'static str {
329 match self {
330 Self::Node => "node",
331 Self::Leaf => "leaf",
332 Self::Legacy => "legacy",
333 }
334 }
335}
336
337#[derive(Debug, Clone, Copy, PartialEq, Eq)]
339pub enum ChannelKind {
340 Noise,
342 Passthrough,
344}
345
346#[derive(Debug, Clone, Copy)]
348pub struct AdmitContext {
349 pub class: PeerClass,
351 pub channel: ChannelKind,
353}
354
355#[derive(Debug, Clone, Copy, PartialEq, Eq)]
357pub enum PeerLimits {
358 None,
360 Leaf,
362}
363
364#[derive(Debug, Clone, PartialEq, Eq)]
366pub struct Refusal {
367 pub code: &'static str,
369 pub detail: String,
371}
372
373#[derive(Debug, Clone, PartialEq, Eq)]
375pub struct Grant {
376 pub limits: PeerLimits,
378 pub class: PeerClass,
381 pub admitted: bool,
386 pub trust_scope: bool,
388 pub observed: Option<Refusal>,
390}
391
392impl Grant {
393 pub fn open(class: PeerClass) -> Self {
395 Self { limits: PeerLimits::None, class, admitted: false, trust_scope: false, observed: None }
396 }
397}
398
399#[derive(Debug, Clone, PartialEq, Eq)]
401pub enum Admission {
402 Admit(Grant),
404 Refuse(Refusal),
406}
407
408#[async_trait]
410pub trait AdmissionGate: Send + Sync + 'static {
411 fn strict(&self) -> bool {
416 false
417 }
418
419 fn is_revoked(&self, _node_id: &str) -> bool {
423 false
424 }
425
426 async fn admit(&self, hello: &VerifiedHello, ctx: &AdmitContext) -> Admission;
428
429 async fn admit_unverified(&self, failure: &HelloFailure, ctx: &AdmitContext) -> Admission;
431}
432
433#[derive(Debug, Default, Clone, Copy)]
436pub struct AllowAll;
437
438#[async_trait]
439impl AdmissionGate for AllowAll {
440 async fn admit(&self, _: &VerifiedHello, ctx: &AdmitContext) -> Admission {
441 Admission::Admit(Grant::open(ctx.class))
442 }
443 async fn admit_unverified(&self, _: &HelloFailure, _: &AdmitContext) -> Admission {
444 Admission::Admit(Grant::open(PeerClass::Legacy))
445 }
446}
447
448pub fn hex_encode(b: &[u8]) -> String {
450 use std::fmt::Write;
451 b.iter().fold(String::with_capacity(b.len() * 2), |mut s, x| {
452 let _ = write!(s, "{x:02x}");
453 s
454 })
455}
456
457pub fn hex_decode(s: &str) -> Option<Vec<u8>> {
459 if !s.len().is_multiple_of(2) || !s.is_ascii() {
460 return None;
461 }
462 (0..s.len())
463 .step_by(2)
464 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok())
465 .collect()
466}
467
468fn hex_decode_fixed<const N: usize>(s: &str) -> Option<[u8; N]> {
469 hex_decode(s)?.try_into().ok()
470}
471
472#[cfg(test)]
473#[path = "mesh_admit_tests.rs"]
474mod tests;
475
476#[cfg(test)]
477#[path = "mesh_admit_dial_tests.rs"]
478mod dial_tests;
479
480pub use crate::mesh_admit_gate::install_nested_peer_ceiling;