The Message Envelope and Integrity
July 16, 2026 ยท View on GitHub
A routed message is carried in one structure, IpcMessage, and each one carries a keyed MAC
computed over its own fields. This page documents the envelope, the boot-time key that keys
the MAC, and the integrity check. The code is under src/ipc/nonos_channel/, whose live
surface is exactly the envelope (message), the MAC (hash), and the error type.
The envelope
IpcMessage (src/ipc/nonos_channel/message.rs:26) is the on-the-wire form every routed
message takes:
struct IpcMessage {
from: String, // "proc.<pid>", stamped by the kernel
to: String, // destination inbox name
data: Vec<u8>,
timestamp_ms: u64,
correlation: u64, // request/reply matching
checksum64: u64, // keyed MAC, private field
}
new (message.rs:36) rejects a payload larger than MAX_MESSAGE_SIZE = 1 MiB, stamps
the current millisecond timestamp, and computes the MAC before the message exists; there is
no way to construct an IpcMessage without a valid MAC over its contents, because
checksum64 is private and set only in the constructors. from and to are set by the
routing layer, not by the sending capsule, so the sender identity in the envelope is the
kernel's attestation of who sent it.
The MAC key
Integrity is keyed, not a bare hash, so it is seeded once at boot. init_ipc_secret
(src/ipc/nonos_channel/hash.rs:25) draws 32 bytes from the secure RNG, derives a key from
them with a domain-separated BLAKE3 derive-key, and stores it in a Once:
init_ipc_secret():
bytes = get_bytes_secure(32) else "rng failed to seed IPC MAC key"
key = blake3::new_derive_key("NONOS:IPC:SECRET:v1").update(bytes).finalize()
IPC_SECRET.call_once(|| key)
This runs during kernel init (src/kernel_core/init/entry.rs:36) and is fatal on
failure: if the RNG cannot seed the MAC key, the kernel does not continue with an
unkeyed IPC path. Every later MAC pulls the key through get_ipc_secret, which errors if
the key was never initialized, so a MAC can never be computed against a zero or absent key.
The MAC
compute_checksum (hash.rs:63) is a keyed BLAKE3 MAC over the message's own fields, with
a domain-separation tag and a field separator so distinct messages cannot collide by
concatenation:
mac = blake3::new_keyed(secret)
.update("NONOS:IPC:MAC:v1")
.update(from) .update(0xF0) .update(to)
.update(timestamp_ms.le_bytes())
.update(data)
.finalize()
checksum64 = last 8 bytes of mac, little-endian
The MAC binds the sender, the destination, the timestamp, and the payload together under
the boot secret. validate_integrity (message.rs:72) recomputes the MAC over the
current fields and compares; the comparison folds the difference into a single word and
tests it against zero in one step rather than short-circuiting byte by byte
(hash.rs:83), so the check does not leak where a forged MAC first diverges.
Honest scope
The MAC is a sixty-four-bit tag, a deliberate size for an in-kernel integrity and
anti-corruption check rather than a full-width authentication tag against an adversary with
online forgery attempts; its job is to detect a corrupted or malformed envelope, and it is
keyed under the per-boot secret so the tag cannot be precomputed across boots. The envelope
travels inside the kernel between kernel-managed inboxes, so the transport itself is not an
untrusted channel; the MAC is defense in depth on the message structure, and the sender
identity guarantee comes from the kernel stamping from, not from the MAC alone.
Security analysis
The envelope is the one structure every routed message becomes, so its guarantees are the guarantees of the message layer. Three properties hold it, and one boundary is worth stating plainly.
The sender field is the kernel's, not the sender's. from is written by the routing layer as
proc.<caller_pid> when it builds the message (src/ipc/kernel_ipc.rs:74), never taken from the
sending capsule. A capsule cannot construct an IpcMessage at all with a from of its choosing,
because the constructor is the only way to make one and the routing path is the only
caller that supplies the fields. So the receiver reads the sender identity as the kernel's attestation
of who sent the message, and a capsule cannot forge a message that appears to come from another.
No message exists without a valid MAC. checksum64 is a private field set only inside new
(message.rs:36), which computes the keyed MAC before the value is returned. There is no path that
builds an IpcMessage and skips the MAC, and there is no setter, so a message on any inbox carries a
tag over its own from, to, timestamp_ms, and data. validate_integrity (message.rs:72)
recomputes and compares by folding the difference into one word and testing it against zero in a single
step (hash.rs:83), so the check does not leak where a forged tag first diverges.
The key is per-boot and fatal to miss. init_ipc_secret (hash.rs:25) derives the MAC key from
32 secure-RNG bytes through a domain-separated BLAKE3 derive-key and is fatal on RNG failure at init
(src/kernel_core/init/entry.rs:36): the kernel does not continue with an unkeyed IPC path. Every MAC
pulls the key through get_ipc_secret, which errors if it was never set, so a tag can never be computed
against a zero or absent key, and a tag captured on one boot cannot be replayed as valid on the next.
The honest boundary is the tag width and what the MAC does not do. checksum64 is sixty-four bits, a
deliberate size for an in-kernel integrity and anti-corruption check rather than a full-width
authentication tag against an adversary with online forgery attempts. The envelope travels inside the
kernel between kernel-managed inboxes, so the transport is not itself an untrusted channel; the MAC is
defense in depth on the structure. The load-bearing sender guarantee is the kernel stamping from, not
the MAC. And the MAC binds the fields to each other and to the boot secret, but it does not by itself
authorise the route: whether a caller may reach a destination at all is the capability check on the
routing path, not anything the envelope carries.
Debugging the envelope
The envelope has two failure surfaces, and they surface very differently. Construction fails when the
payload is too large: new (message.rs:36) rejects a data longer than MAX_MESSAGE_SIZE = 1 MiB,
which propagates as the ChannelError (error.rs) that the routing path maps to ENOMEM (-12) at
kernel_route_ipc_corr (kernel_ipc.rs:79). If a send returns -12 and the buffer was not oversize
at the syscall's own MAX_MESSAGE_SIZE check (send.rs:55), the construction was the thing that
refused it, which in practice means the transient kernel allocation for the message failed. Integrity
fails when validate_integrity returns false on a dequeued message: because from, to,
timestamp_ms, and data are all covered, a mismatch means one of them changed after the MAC was
computed, which inside a single boot is memory corruption rather than an ordinary bad message, since the
key cannot have changed. A tag that verifies under this boot's key but not a prior capture is the
per-boot re-key working as intended, not a fault. There is no separate errno for an integrity failure at
the send path because the MAC is checked on the receiving side, so the tell is a message that routed
cleanly but a drainer rejected.
Source map
src/ipc/nonos_channel/message.rs IpcMessage, MAX_MESSAGE_SIZE, new, validate_integrity
src/ipc/nonos_channel/hash.rs init_ipc_secret, compute_checksum, the constant-time compare
src/ipc/nonos_channel/error.rs ChannelError
src/kernel_core/init/entry.rs the fatal boot-time seed of the MAC key
src/ipc/kernel_ipc.rs where new is called and from is stamped, ChannelError -> ENOMEM
Every reference above is verified against those trees. The from stamp and the capability check that
actually authorises a route are on the routing page, the inboxes these messages land in
are on the inbox page, and the MAX_MESSAGE_SIZE bound is enforced a second time at the
send syscall documented on routing.