Expand description
The tunnel’s identity proof: the seed’s wire contract, the Noise pattern, and the pin.
Here rather than on either side for ARCH-2’s reason, and the stakes are higher than for the close codes: the host derives a public key from a seed and the daemon derives what must be the same public key from the same seed. A derivation spelled twice is a derivation that can disagree, and a disagreement surfaces as a handshake that fails with no way to tell a wrong pin from a wrong derivation.
Following this crate’s own rule (see the crate docs), the base64 encoding is not here:
the seed travels as a String field and each side encodes with the shared base64 crate,
exactly as the exec payloads do. What is here is the part that must agree — the key names,
the length, the pattern, and which seeds are refused.
§What this proves, and what platform-layer mTLS cannot
TLS terminates at the AWS-managed endpoint proxy and the service model has no
client-certificate parameter anywhere: CreateMicrovmAuthToken takes
microvmIdentifier, allowedPorts, and expirationInMinutes, and nothing else. So there
is no way to present a certificate to the VM through the platform, and endpoint auth is
the JWE alone.
The JWE is scoped to one microvmId by the endpoint proxy. The additional
Noise proof terminates in the daemon and checks possession of the launch-pinned key.
Its trust root includes the AWS control plane and run-hook delivery: the host supplies
the VM seed through RunMicrovm. The proxy carries the agent token used for root
control, so key protection also assumes trusted authorized control and guest-root access.
This is an application-layer proof under those assumptions, not independence from them.
§Noise KK rather than TLS, on a measured constraint
Issue #70’s layer 3 called for rustls with a per-VM certificate pinned by SPKI. That is
not available to the daemon, and the reason is the shipping target rather than a
preference. agentd ships as a static aarch64-unknown-linux-musl binary, and both
rustls crypto providers compile C: aws-lc-sys and ring each drive cc-rs, which looks
for aarch64-linux-musl-gcc. Measured 2026-08-29 on the devbox, and .github/workflows/ci.yml
installs gcc-aarch64-linux-gnu — a gnu cross compiler, not the musl one cc-rs asks
for. Both providers fail the daemon’s release build with error occurred in cc-rs: failed to find tool "aarch64-linux-musl-gcc", so putting rustls in the daemon would trade a
working shipping artifact for an identity feature.
Noise KK gives the same three properties with no PKI and no C:
- Mutual authentication.
KKmeans both statics are known to both sides before the handshake, which is exactly the situation here — the host generated the seed, so it knows the daemon’s key, and the daemon is told the host’s key in the same payload. Each side proves possession of its own static, so the daemon learns the peer is the launching host rather than another token holder. - Pinning by construction. There is no name, no CA, and no chain to get wrong: the remote static key is the pin. A wrong pin cannot be “accepted anyway” by a verifier that forgot a check, because the key is mixed into the handshake hash and a mismatch fails decryption. That is a stronger position than a custom certificate verifier, which is code that can be written wrong.
- Confidentiality past the proxy. ChaCha20-Poly1305 over the whole stream, so the bytes the relay carries are opaque to the endpoint that carries them.
snow is the crate, at 26M downloads the most-used Noise implementation in the ecosystem,
and it cross-compiles to the shipping target with no C compiler — measured, with
default-features = false, which is load-bearing: snow’s std feature reads ring/std
rather than ring?/std, so enabling it force-enables the very C dependency the daemon
cannot build.
§The end of stream (#342)
Every relayed chunk rides one Noise transport message, and so does the end of a stream: a side that has sent its last byte sends one more message with an empty plaintext. A relayed chunk is never empty, so the empty message is unambiguous, and it authenticates under the session’s keys like any other. It also counts: each transport message decrypts only at its own position (snow’s nonce is a counter), so the end of stream proves every message before it arrived, in order, with none added. That’s why it carries no byte counts.
The WebSocket close frame that follows it stays plaintext, since it has to reach a caller whose handshake failed. After the handshake it’s a diagnosis and never the verdict: anything on the path can send a close frame or drop the connection, and only the end of stream says a stream finished rather than being cut short.
Each side offers the end of stream in its handshake message’s payload
(HANDSHAKE_PAYLOAD), and a side holds its peer to it only when the peer offered it. The
payloads are inside the handshake’s encryption and its hash, so an on-path party can’t strip
the offer the way it could strip a query flag. An older peer sends an empty payload and no
end of stream, and it reads the newer peer’s end of stream as a message with nothing in it,
so a release that predates the end of stream and one that sends it still work together in
either order within the same handshake version. Protocol 2 requires a prologue
binding the route and guest port, then a separate transport confirmation before
the guest dial; protocol 1 verified tunnels are refused. docs/PROTOCOL.md
has the complete version and end-of-stream rules.
§The honest limit, restated here because this is where a reader will look
Under the launch-material and privileged-access assumptions above, this proves
possession of the key pinned for the VM. It does not prove the VM is uncompromised: the workload runs as uid 0 and can in principle ptrace the
daemon and read the derived key out of its memory. Same class as the other entries on
docs/TRUST.md’s unenforced list. It is documented rather than overclaimed.
Enums§
- Seed
Error - Why some bytes are not a usable identity seed.
Constants§
- END_
OF_ STREAM - The handshake payload’s flag for the end of stream: the sender sends one, and checks the peer’s when the peer offered it too.
- HANDSHAKE_
PAYLOAD - The payload each side writes into its handshake message: the flags it offers.
- HOST_
PUBLIC_ KEY_ KEY - The payload key carrying the host’s public key, which the daemon pins in return.
- IDENTITY_
PAYLOAD_ BYTES - How many bytes the identity material adds to the run-hook payload.
- NOISE_
PATTERN - The Noise handshake pattern, spelled once for both sides.
- SEED_
BYTES - The length of a seed, a static secret, and a public key. All 32 bytes, on x25519.
- SEED_
KEY - The payload key carrying the VM’s own seed.
- TUNNEL_
CONFIRMATION - The first transport message, consumed before the daemon connects to the guest.
- TUNNEL_
ROUTE - The route bound into every verified tunnel handshake.
Functions§
- is_
end_ of_ stream - Whether a transport message’s plaintext is the end of stream.
- offers_
end_ of_ stream - Whether a peer’s handshake payload offers the end of stream.
- seed_
from_ bytes - Checks decoded bytes into a seed, refusing the two shapes that are not one.
- tunnel_
prologue - The context both peers authenticate alongside their pinned keys.