logicaffeine_compile/concurrency/
channel.rs1pub(crate) const CHAN_MAGIC: u8 = 0xC0;
24
25pub(crate) const CHAN_VER: u8 = 1;
27
28pub(crate) const CHAN_HEADER_LEN: usize = 4;
30
31pub const SUITE_NULL: u16 = 0;
33
34pub const SUITE_PQ: u16 = 1;
37
38pub const SUITE_PNP: u16 = 2;
44
45pub trait Suite: Sync {
50 fn id(&self) -> u16;
52 fn seal_body(&self, blob: &[u8]) -> Vec<u8>;
55 fn open_body(&self, body: &[u8]) -> Option<Vec<u8>>;
57}
58
59pub struct NullSuite;
62
63impl Suite for NullSuite {
64 fn id(&self) -> u16 {
65 SUITE_NULL
66 }
67 fn seal_body(&self, blob: &[u8]) -> Vec<u8> {
68 blob.to_vec()
69 }
70 fn open_body(&self, body: &[u8]) -> Option<Vec<u8>> {
71 Some(body.to_vec())
72 }
73}
74
75pub fn suite_for(id: u16) -> Option<&'static dyn Suite> {
79 static NULL: NullSuite = NullSuite;
80 match id {
81 SUITE_NULL => Some(&NULL),
82 _ => None,
83 }
84}
85
86pub struct PqSuite {
92 key: [u8; 32],
93 seq: std::sync::atomic::AtomicU64,
94}
95
96impl PqSuite {
97 pub fn new(key: [u8; 32]) -> Self {
99 Self { key, seq: std::sync::atomic::AtomicU64::new(0) }
100 }
101}
102
103impl Suite for PqSuite {
104 fn id(&self) -> u16 {
105 SUITE_PQ
106 }
107 fn seal_body(&self, blob: &[u8]) -> Vec<u8> {
108 let seq = self.seq.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
109 let mut nonce = [0u8; 12];
110 nonce[..8].copy_from_slice(&seq.to_le_bytes());
111 let sealed =
112 logicaffeine_system::aead::chacha20poly1305_seal(&self.key, &nonce, &SUITE_PQ.to_le_bytes(), blob);
113 let mut body = Vec::with_capacity(12 + sealed.len());
114 body.extend_from_slice(&nonce);
115 body.extend_from_slice(&sealed);
116 body
117 }
118 fn open_body(&self, body: &[u8]) -> Option<Vec<u8>> {
119 if body.len() < 12 {
120 return None;
121 }
122 let nonce: [u8; 12] = body[..12].try_into().ok()?;
123 logicaffeine_system::aead::chacha20poly1305_open(&self.key, &nonce, &SUITE_PQ.to_le_bytes(), &body[12..])
124 }
125}
126
127pub fn derive_aead_key(shared_secret: &[u8], label: &[u8]) -> [u8; 32] {
131 let mut input = Vec::with_capacity(shared_secret.len() + label.len() + 24);
132 input.extend_from_slice(b"logos-pq-channel-v1\x00");
133 input.extend_from_slice(label);
134 input.extend_from_slice(shared_secret);
135 let mut k = [0u8; 32];
136 k.copy_from_slice(&logicaffeine_system::keccak::shake256_bytes(&input, 32));
137 k
138}
139
140pub struct PqHandshake {
142 pub ek: Vec<u8>,
144 dk: Vec<u8>,
146}
147
148pub fn pq_handshake_initiator(seed_d: &[u8; 32], seed_z: &[u8; 32]) -> PqHandshake {
150 let (ek, dk) = logicaffeine_system::mlkem::keygen(seed_d, seed_z);
151 PqHandshake { ek, dk }
152}
153
154pub fn pq_handshake_responder(ek: &[u8], msg: &[u8; 32]) -> (Vec<u8>, PqSuite, PqSuite) {
157 let (ct, ss) = logicaffeine_system::mlkem::encaps(ek, msg);
158 (ct, PqSuite::new(derive_aead_key(&ss, b"i2r")), PqSuite::new(derive_aead_key(&ss, b"r2i")))
159}
160
161pub fn pq_handshake_finish(hs: &PqHandshake, ct: &[u8]) -> (PqSuite, PqSuite) {
164 let ss = logicaffeine_system::mlkem::decaps(&hs.dk, ct);
165 (PqSuite::new(derive_aead_key(&ss, b"i2r")), PqSuite::new(derive_aead_key(&ss, b"r2i")))
166}
167
168pub fn seal_with(suite: &dyn Suite, blob: &[u8]) -> Vec<u8> {
171 let body = suite.seal_body(blob);
172 let mut out = Vec::with_capacity(CHAN_HEADER_LEN + body.len());
173 out.push(CHAN_MAGIC);
174 out.push(CHAN_VER);
175 out.extend_from_slice(&suite.id().to_le_bytes());
176 out.extend_from_slice(&body);
177 out
178}
179
180pub fn open_with(suite: &dyn Suite, bytes: &[u8]) -> Option<Vec<u8>> {
183 if bytes.len() < CHAN_HEADER_LEN || bytes[0] != CHAN_MAGIC || bytes[1] != CHAN_VER {
184 return None;
185 }
186 if u16::from_le_bytes([bytes[2], bytes[3]]) != suite.id() {
187 return None;
188 }
189 suite.open_body(&bytes[CHAN_HEADER_LEN..])
190}
191
192pub fn seal(suite_id: u16, blob: &[u8]) -> Vec<u8> {
196 let suite = suite_for(suite_id).expect("seal with a registered suite");
197 let body = suite.seal_body(blob);
198 let mut out = Vec::with_capacity(CHAN_HEADER_LEN + body.len());
199 out.push(CHAN_MAGIC);
200 out.push(CHAN_VER);
201 out.extend_from_slice(&suite_id.to_le_bytes());
202 out.extend_from_slice(&body);
203 out
204}
205
206pub fn open(bytes: &[u8]) -> Option<Vec<u8>> {
210 if bytes.len() < CHAN_HEADER_LEN || bytes[0] != CHAN_MAGIC || bytes[1] != CHAN_VER {
211 return None;
212 }
213 let suite_id = u16::from_le_bytes([bytes[2], bytes[3]]);
214 let suite = suite_for(suite_id)?;
215 suite.open_body(&bytes[CHAN_HEADER_LEN..])
216}
217
218thread_local! {
219 static ACTIVE_SUITE: std::cell::Cell<Option<u16>> = const { std::cell::Cell::new(None) };
223}
224
225pub fn active_suite() -> Option<u16> {
227 ACTIVE_SUITE.with(|s| s.get())
228}
229
230pub fn with_suite<T>(suite: Option<u16>, f: impl FnOnce() -> T) -> T {
233 let prev = ACTIVE_SUITE.with(|s| s.replace(suite));
234 let out = f();
235 ACTIVE_SUITE.with(|s| s.set(prev));
236 out
237}
238
239pub trait ActiveSession {
244 fn seal(&self, bytes: &[u8]) -> Option<Vec<u8>>;
247 fn open(&self, bytes: &[u8]) -> Option<Vec<u8>>;
249}
250
251thread_local! {
252 static ACTIVE_SESSION: std::cell::RefCell<Option<std::rc::Rc<dyn ActiveSession>>> =
256 const { std::cell::RefCell::new(None) };
257}
258
259pub fn active_session() -> Option<std::rc::Rc<dyn ActiveSession>> {
261 ACTIVE_SESSION.with(|s| s.borrow().clone())
262}
263
264pub fn install_session(
267 session: Option<std::rc::Rc<dyn ActiveSession>>,
268) -> Option<std::rc::Rc<dyn ActiveSession>> {
269 ACTIVE_SESSION.with(|s| std::mem::replace(&mut *s.borrow_mut(), session))
270}
271
272pub fn with_session<T>(session: Option<std::rc::Rc<dyn ActiveSession>>, f: impl FnOnce() -> T) -> T {
275 let prev = install_session(session);
276 let out = f();
277 install_session(prev);
278 out
279}
280
281pub fn seal_active(bytes: Vec<u8>) -> Vec<u8> {
285 match active_suite() {
286 Some(id) => seal(id, &bytes),
287 None => bytes,
288 }
289}
290
291pub fn seal_active_checked(bytes: Vec<u8>) -> Option<Vec<u8>> {
296 match active_session() {
297 Some(session) => session.seal(&bytes),
298 None => Some(seal_active(bytes)),
299 }
300}
301
302pub fn open_active(bytes: Vec<u8>) -> Option<Vec<u8>> {
306 if let Some(session) = active_session() {
307 return session.open(&bytes);
308 }
309 match active_suite() {
310 Some(_) => open(&bytes),
311 None => Some(bytes),
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318 use super::super::marshal::{message_to_wire_with, WireCodec, WireIntegrity};
319 use crate::interpreter::RuntimeValue;
320
321 #[test]
322 fn null_envelope_round_trips_and_rejects_malformed() {
323 let blob = message_to_wire_with("alice", &RuntimeValue::Int(7), WireCodec::Native, WireIntegrity::Checked)
324 .expect("encode");
325
326 let sealed = seal(SUITE_NULL, &blob);
327
328 assert_eq!(open(&sealed).as_deref(), Some(blob.as_slice()), "null suite round-trip");
330
331 assert!(open(&sealed[..2]).is_none(), "truncated envelope rejected");
333
334 let mut bad = sealed.clone();
336 bad[2] = 0xFF;
337 bad[3] = 0xFF;
338 assert!(open(&bad).is_none(), "unknown suite rejected");
339 }
340
341 fn splitmix64(state: &mut u64) -> u64 {
343 *state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
344 let mut z = *state;
345 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
346 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
347 z ^ (z >> 31)
348 }
349
350 #[test]
351 fn null_round_trips_arbitrary_blobs() {
352 let mut s = 0x1234_5678_9ABC_DEF0u64;
353 for _ in 0..2000 {
354 let len = (splitmix64(&mut s) % 257) as usize; let blob: Vec<u8> = (0..len).map(|_| splitmix64(&mut s) as u8).collect();
356 let sealed = seal(SUITE_NULL, &blob);
357 assert_eq!(open(&sealed), Some(blob), "null suite round-trip, len {len}");
358 }
359 }
360
361 #[test]
362 fn rejects_bad_magic_version_and_empty() {
363 let sealed = seal(SUITE_NULL, b"payload");
364 assert!(open(&[]).is_none(), "empty input rejected");
365 assert!(open(&sealed[..CHAN_HEADER_LEN - 1]).is_none(), "header-short rejected");
366
367 let mut bad_magic = sealed.clone();
368 bad_magic[0] ^= 0xFF;
369 assert!(open(&bad_magic).is_none(), "bad magic rejected");
370
371 let mut bad_ver = sealed.clone();
372 bad_ver[1] = bad_ver[1].wrapping_add(1);
373 assert!(open(&bad_ver).is_none(), "bad version rejected");
374 }
375
376 #[test]
377 fn seal_is_framed_not_raw() {
378 let blob = b"the relay sees only the envelope";
379 let sealed = seal(SUITE_NULL, blob);
380 assert_eq!(&sealed[..2], &[CHAN_MAGIC, CHAN_VER], "header magic+version present");
381 assert_eq!(&sealed[2..4], &SUITE_NULL.to_le_bytes(), "suite id bound little-endian");
382 assert_eq!(sealed.len(), CHAN_HEADER_LEN + blob.len(), "null body length-exact");
383 assert_ne!(sealed.as_slice(), blob.as_slice(), "sealed bytes differ from the raw blob");
384 }
385
386 #[test]
387 fn null_suite_trait_dispatch() {
388 let suite = suite_for(SUITE_NULL).expect("null suite registered");
389 assert_eq!(suite.id(), SUITE_NULL);
390 let body = suite.seal_body(b"abc");
391 assert_eq!(suite.open_body(&body).as_deref(), Some(&b"abc"[..]), "identity body");
392 assert!(suite_for(0xFFFF).is_none(), "unknown suite id is not registered");
393 }
394
395 #[test]
396 fn no_active_suite_is_passthrough() {
397 assert_eq!(active_suite(), None, "default: channel disengaged");
398 let blob = b"plain".to_vec();
399 assert_eq!(seal_active(blob.clone()), blob, "seal is identity when off");
400 assert_eq!(open_active(blob.clone()), Some(blob), "open is identity when off");
401 }
402
403 #[test]
404 fn with_suite_scopes_and_restores() {
405 assert_eq!(active_suite(), None);
406 with_suite(Some(SUITE_NULL), || {
407 assert_eq!(active_suite(), Some(SUITE_NULL), "suite active inside scope");
408 let blob = b"secret payload".to_vec();
409 let sealed = seal_active(blob.clone());
410 assert_ne!(sealed, blob, "sealed under an active suite is framed");
411 assert_eq!(open_active(sealed), Some(blob), "round-trip under the active suite");
412 });
413 assert_eq!(active_suite(), None, "prior suite restored after scope");
414 }
415
416 #[test]
417 fn open_active_drops_foreign_frame_under_active_suite() {
418 with_suite(Some(SUITE_NULL), || {
419 assert!(open_active(b"not an envelope".to_vec()).is_none(), "foreign frame dropped");
420 });
421 }
422
423 #[test]
424 fn pq_channel_seals_a_real_wire_message_end_to_end() {
425 let hs = pq_handshake_initiator(&[0xA1; 32], &[0xA2; 32]);
429 let (ct, resp_i2r, resp_r2i) = pq_handshake_responder(&hs.ek, &[0xB0; 32]);
430 let (init_i2r, init_r2i) = pq_handshake_finish(&hs, &ct);
431
432 let blob = message_to_wire_with(
434 "responder",
435 &RuntimeValue::Int(42),
436 WireCodec::Native,
437 WireIntegrity::Checked,
438 )
439 .expect("encode");
440
441 let sealed = seal_with(&resp_r2i, &blob);
443 assert_eq!(&sealed[..2], &[CHAN_MAGIC, CHAN_VER], "channel header present");
444 assert_eq!(&sealed[2..4], &SUITE_PQ.to_le_bytes(), "PQ suite id bound in the envelope");
445 assert_ne!(sealed[CHAN_HEADER_LEN..].to_vec(), blob, "body is ciphertext, not the raw blob");
446 assert_eq!(
447 open_with(&init_r2i, &sealed).as_deref(),
448 Some(blob.as_slice()),
449 "initiator opens the responder's PQ-sealed wire message"
450 );
451
452 let s2 = seal_with(&init_i2r, b"ack");
454 assert_eq!(open_with(&resp_i2r, &s2).as_deref(), Some(&b"ack"[..]), "i2r direction opens");
455
456 let sealed_again = seal_with(&resp_r2i, &blob);
458 assert_ne!(sealed, sealed_again, "fresh counter nonce ⇒ a distinct frame each time");
459 assert_eq!(open_with(&init_r2i, &sealed_again).as_deref(), Some(blob.as_slice()));
460
461 let mut tampered = sealed.clone();
463 let last = tampered.len() - 1;
464 tampered[last] ^= 1;
465 assert!(open_with(&init_r2i, &tampered).is_none(), "tampered PQ frame rejected");
466 let eve = PqSuite::new(derive_aead_key(&[0u8; 32], b"r2i"));
467 assert!(open_with(&eve, &sealed).is_none(), "wrong session key cannot open");
468 }
469
470 #[test]
471 fn pq_handshake_disagrees_on_a_corrupted_ciphertext() {
472 let hs = pq_handshake_initiator(&[1; 32], &[2; 32]);
475 let (ct, _resp_i2r, resp_r2i) = pq_handshake_responder(&hs.ek, &[3; 32]);
476 let mut bad_ct = ct.clone();
477 bad_ct[0] ^= 1;
478 let (_init_i2r, init_r2i) = pq_handshake_finish(&hs, &bad_ct);
479 let sealed = seal_with(&resp_r2i, b"top secret");
480 assert!(
481 open_with(&init_r2i, &sealed).is_none(),
482 "a corrupted handshake yields divergent keys ⇒ frames don't open"
483 );
484 }
485}