Responses to the independent reviews
Six models reviewed commit 67eb6e6 via OpenRouter (reviews/run-reviews.py);
their reports are in this directory. This file records what was done about
each substantive finding. "Fixed in" refers to the commit following 67eb6e6.
Time / lock length
- Finish-time fallback can shorten the post-completion lock (GPT-5.5, GPT-5.6, Kimi; all High). Correct as stated against the old README wording. Fixed: the enclave now retries the NSM for 30 s before falling back, and the README states precisely what is guaranteed: at least seven days after the trusted start timestamp always, plus a best-effort extension to seven days after the trusted finish timestamp. Discarding records instead was considered and rejected by the operator (that would lose the operator's property for a marginal gain). Verifiers should treat the start-based bound as the guarantee.
- Lock minimum not re-checked at sealing / dev override (Kimi). Fixed:
outside dev mode the sealing path uses
max(configured, seven days). - Record id derived from the local clock (Kimi, Low). Fixed: ids now use the trusted start timestamp.
Verification
tlproxy verifypasses without--pcrs, printing unverified policy (GPT-5.5, GPT-5.6, Medium). Fixed:--pcrsis mandatory unless--insecure-skip-measurementis given; measurements files now carry the expectedage_recipientandlock_seconds, whichverifychecks; policy fields are labelled "(measured)" or "(UNVERIFIED)".Cargo.lock/rust-toolchain.tomlabsent (GPT-5.5, Medium). They were omitted from the review bundle, not from the repository; the bundle now includes them.- nitro-cli version not pinned (GPT-5.6, Low). Fixed:
build-eif.shrefuses any version other than the pinned one. - No binding of TLS key to a session / multiple enclaves with the same PCRs (Kimi, Medium). Not a vulnerability: the handshake proves possession of the attested key, and every enclave with the same PCRs runs the same measured code and enforces the same policy.
Availability / resource exhaustion
- Record queue bounded by count, not bytes; sealing backlog unbounded
(GPT-5.5 High, GPT-5.6 High). Fixed: byte budget on the queue; at most 4
concurrent seal jobs; an exchange stays counted against
max_in_flightand the capture budget until its record is sealed and queued. - Connections not bounded before
max_in_flight; no header timeout; slowloris (GPT-5.5, GPT-5.6, High). Fixed:max_connectionssemaphore before TLS, 15 s header read timeout,max_connection_secondslifetime cap. - Userspace TCP: unbounded per-connection outbound queue (GPT-5.6, High). Fixed: 256 KiB per-connection limit with backpressure to the writer.
- Userspace TCP: data consumed from the socket before it is queued to the user (Gemini, Medium). Only the stack task sends on that channel, so the race cannot occur; the code now consumes exactly what it hands over anyway.
- Headers captured without a cap (GPT-5.5, Medium). Fixed:
max_header_bytesper side,headers_truncatedin the record. - Attestation endpoint unbounded NSM use (Kimi, Medium). Fixed: at most 4 concurrent attestation requests; 503 beyond.
- "Never lose a record" wording vs drop behaviour (GPT-5.5, Low). Fixed in the README: records can be lost if the parent is unreachable long enough to exhaust the queue; senders are never blocked.
Record completeness
- Finalization on response EOF can miss trailing request-body frames when the upstream answers early (GPT-5.6, Medium). Fixed: both bodies hold the finalizer; the record is sealed only when both are finished or abandoned.
- Upstream error text returned to the sender (GPT-5.5, Low). Fixed: the sender gets a fixed category; the detail stays in the sealed record.
Upstream privacy
x-timelock-proxy-echleaks upstream identity (Kimi, Medium). Not a leak: response headers travel inside the sender's TLS session; the parent cannot see them, and the sender already knows the upstream.- Hostnames with underscores accepted (GPT-5.5, Medium). Fixed: strict DNS label validation.
- DNS cache TTLs from resolvers (Kimi, Low). Already floored at 30 s; unchanged.
Relay
- Newest-first Mullvad eviction can delete the owner's newest device (GPT-5.6, Medium). Correct. Fixed: the owner lists protected device names in a host-provided file; those are never deleted, and if room cannot be made without them registration fails rather than deleting.
- Account number from the parent is unauthenticated (Kimi, Info). By design and documented; a wrong account only breaks connectivity.
- Parent can delay/reorder WireGuard datagrams (Kimi, Medium). WireGuard tolerates it; the effect is availability only, which the parent controls anyway. Documented.
Claude Opus 5 (second run)
- F1, High: the
user_data== SPKI binding is only checked inside a third-party crate, with no test. The crate does compareuser_datato the certificate's SPKI (verified by reading it), but the point stands. Fixed:check_key_bindingin the CLI does the comparison itself, also requires the NSMpublic_keyfield to be unused, and has fixture tests against a captured live attestation document (positive, different certificate, missinguser_data). - F5, Medium: relay API error text served through the attestation endpoint
and unescaped into the landing page. Fixed:
RelayStatus.stateis a fixed word; reasons go to the journal only; landing-page substitutions are HTML-escaped. - F8, High: memory accounting gaps (sealing amplification, queue bounded by count, connection count unbounded, tunnel buffers). The queue, connection and accounting-after-sealing parts were fixed for the GPT reports; in addition the capture budget is now 96 MiB with 2 concurrent seals, the tunnel per-connection user queue is 128 KiB, and the enclave gets 2 GiB.
- F9, Medium: one undeliverable record blocks delivery forever. Fixed: after ~5 minutes of retries a record is abandoned (and logged) so the queue moves on.
- F10, Medium: ECH rejection surfaces as a 502 and a stale config is cached for up to an hour. Fixed: on an ECH handshake error the cached answer is forgotten and the connection retried once without ECH.
- F11, Medium: parent-fetched beacon JSON parsed under
panic = "abort". Fixed by switching the profile to unwind (see below); a parse panic now fails only the refresh task. - F12, Medium: anonymous DoS; NSM starvation via the attestation endpoint. Partly fixed: trusted timestamps are cached for one second (a stale lower bound only lengthens a lock) and attestation requests are capped at 4. Per-source limiting would need the parent to pass client addresses into the enclave; not done, since the parent can rate-limit at TCP level itself and the enclave does not otherwise learn sender addresses.
- F13, Low: tunnel details (pending data dropped when the user drops the stream; local port reuse). Fixed: pending bytes are flushed then the socket closed gracefully; local ports in use are tracked.
- F14, Low: same as GPT-5.6's early-response finding; fixed.
- F15, Info: ALPN order now prefers
http/1.1; the ACME account is kept across retries within a boot. - F7, Info: production diagnostics go nowhere. Fixed: scrubbed lines are shipped to the parent daemon's journal over vsock.
- Design:
panic = "abort"is wrong for this enclave. Agreed; changed to unwind. - Design: the browser-valid certificate is a net negative. Noted. It is cosmetic and documented as such; the operator asked for it. The hand-rolled DER walk it needs is confined to checking that the issued leaf carries the enclave key, where a mis-parse can only break handshakes.
Kimi K3 and Qwen 3.8 Max (reviewing 765b671)
- Kimi H1: the nonce check compares base64 and always fails. Incorrect:
validate_expected_noncebase64-encodes the document's nonce before comparing, which is what the CLI passes; live verification succeeds. No change. - Kimi M2: self-referential upstream amplifies one request into many.
Correct. Fixed: targets matching the proxy's own names are refused, and
forwarded requests carry a per-boot random
x-timelock-proxy-viamarker; a request arriving with our own marker is refused with 508. - Kimi M3: smoltcp 120 s timeout cuts idle exchanges. The timeout counts from the last acknowledgement including keepalive replies, so an idle live peer is not cut; raised to 300 s with 25 s keepalives anyway.
- Kimi M4 / Gemini: bytes consumed before the user channel accepts them. Only reachable once the user is gone (single sender, capacity checked first); documented in code.
- Kimi M5 / Opus F8: memory budgets exceed the enclave. Fixed (2 GiB enclave, 96 MiB capture budget, 2 concurrent seals, byte-bounded queue).
- Kimi M6: one sender can hold all in-flight slots. Known; per-source fairness needs client addresses the enclave deliberately does not receive. The parent can rate-limit at TCP level.
- Kimi L7: nonce-less attestation documents. Fixed: a nonce is required.
- Kimi L8: counters leak when no runtime is available. Fixed.
- Kimi L9: unparsable ECH config silently downgrades to plain SNI. Kept:
the sender sees
x-timelock-proxy-ech: not-offered; refusing the request would let a broken DNS record deny service. - Kimi L11: trailers not captured. Fixed: trailers are recorded as
trailer:-prefixed header entries. - Kimi I12 / I13: added to the README's trust assumptions.
- Qwen M-1: same as Opus F5; fixed. Qwen L-1: README now says the unlock-round header is a minimum. Qwen L-2: accepted (see Opus F3). Qwen L-3: the ACME client's retry policy is bounded; the task is single and enclave-initiated.
DeepSeek V4 Pro and GLM 5.3 Flash (reviewing 765b671)
- DeepSeek, "Critical": the operator sets the parent clock back and blocks beacons. The premise is wrong: the attestation timestamp comes from the Nitro Security Module, not the parent's OS clock, and the parent cannot set it. The suggested posture is still worth having, and GLM's L-1 asks for the same at boot: requests are now refused unless a verified beacon is newer than 15 minutes by trusted time, so the lock never rests on NSM time alone.
- DeepSeek, Medium: slow request bodies hold in-flight slots for an hour. Fixed by lowering the connection lifetime cap to 20 minutes; per-source limits remain out of scope (see Kimi M6).
- DeepSeek, Low:
unwrapon static response builders. Static inputs; with the profile now unwinding, a panic would fail only that task. - GLM H-1 / M-1 / L-2 / L-4 / I-5 / I-6: already fixed (memory, delivery head-of-line, relay status, counter leak, both-body finalization, explicit key binding).
- GLM L-3: error classification by substring can be fooled by host names. Fixed: failures carry a typed kind as context; no text is inspected.
- GLM I-1: README now lists all
x-timelock-proxy-echvalues. - GLM I-3: NSM ioctls are blocking calls on runtime workers. Bounded by the semaphore and the one-second cache; millisecond-scale; accepted.
Not addressed
- Including PCRs in each sealed record for auditability (Kimi, Info): a reasonable future addition, not a security property of the current design.
- Entropy source in the enclave (Kimi, Low): the Nitro kernel seeds the CSPRNG
from the NSM;
getrandomis used throughout.