1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
// SPDX-License-Identifier: Apache-2.0
//! 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` and the proxy enforces that scope, so an
//! authenticated connection already binds to VM identity **as attested by AWS**. A caller who
//! trusts the proxy needs nothing here. This module is for the caller who wants that binding
//! without trusting the proxy, and it works because we control both endpoints: the proof
//! terminates in the daemon rather than at the proxy.
//!
//! # 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.** `KK` means 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 honest limit, restated here because this is where a reader will look
//!
//! This proves the far end is **`agentd` in the VM the caller launched**. 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.
/// The Noise handshake pattern, spelled once for both sides.
///
/// `KK` because both static keys are known in advance, which is the whole shape of this
/// problem: the host *generated* the daemon's key, so pre-knowledge is not an assumption to
/// arrange but the starting condition. The alternatives are worse fits — `XX` transmits the
/// statics during the handshake, which is what you want when the peers have never met, and it
/// would let a responder present *any* key and be believed until a later check caught it.
/// Under `KK` a wrong key fails the handshake itself.
///
/// `25519_ChaChaPoly_BLAKE2s`: all three have pure-Rust implementations in snow's default
/// resolver, which is what keeps the daemon's build free of a C compiler. AES-GCM would pull
/// that constraint back in on some targets for no security gain here.
pub const NOISE_PATTERN: &str = "Noise_KK_25519_ChaChaPoly_BLAKE2s";
/// The length of a seed, a static secret, and a public key. All 32 bytes, on x25519.
///
/// One constant rather than three, because they are the same number for the same reason: an
/// x25519 static secret *is* 32 bytes of seed, so the derivation is a type change rather than
/// a KDF. That is why this design needs no HKDF step and no rejection loop — the thing the
/// platform can carry is already the thing the handshake needs.
pub const SEED_BYTES: usize = 32;
/// The payload key carrying the VM's own seed.
///
/// Named beside the pattern so a reader finds the whole wire contract in one file, and
/// spelled as a constant because it appears in the host's composition and the daemon's parse
/// — a literal in each place is a typo that fails as "the VM has no identity" rather than as
/// a compile error.
pub const SEED_KEY: &str = "identity_seed";
/// The payload key carrying the host's public key, which the daemon pins in return.
///
/// The other half of mutual authentication. Without it the daemon would accept a handshake
/// from anyone holding the agent token, and the token is a bearer credential the proxy
/// transports on every request — so "knows the token" is a weaker claim than "holds the
/// launching host's private key".
pub const HOST_PUBLIC_KEY_KEY: &str = "identity_host_public_key";
/// How many bytes the identity material adds to the run-hook payload.
///
/// Two 32-byte values, each base64'd to 44 characters, plus the JSON keys, quotes, colons,
/// and commas. Stated as a constant because the payload shares one measured 4096-byte ceiling
/// with the agent token and the launch environment, and a caller near that ceiling needs the
/// number rather than a warning. `microvms-core` asserts the real composition against it.
pub const IDENTITY_PAYLOAD_BYTES: usize = 137;
/// Why some bytes are not a usable identity seed.
///
/// Named variants rather than a string for the same reason [`super::hook::RunHookError`] has
/// them: this material is secret, and a message that quoted what it rejected would put key
/// material into a log line and a response body.
/// Checks decoded bytes into a seed, refusing the two shapes that are not one.
///
/// Takes bytes rather than a base64 string on purpose: this crate carries no encoder (crate
/// docs), so each side decodes with the shared `base64` crate and hands the result here. That
/// keeps one *validation* for both sides, which is the half that has to agree — a host that
/// accepted a seed the daemon refuses would report a launch as identity-capable and then fail
/// every handshake against it.