Independent Pi/OpenRouter review: moonshotai/kimi-k3
Source: a10323dede4413fbf295916b8ad12e3dbad7514e. Status: completed.
Independent Security Review: timelock-proxy v2 (attested-relay-enclave)
Source reviewed: frozen runtime at a10323dede4413fbf295916b8ad12e3dbad7514e, v2 reachability via crates/enclave/src/v2_main.rs (bin target attested-relay-enclave, sole ENTRYPOINT in build/Dockerfile.graviton5 lines 32–35). The legacy tlproxy-enclave binary is built into /out but is not copied into the EIF stage. Documentation/CI material was treated as claims, not evidence.
Method note: I have no execution environment; every test in the tree is unexecuted by me and treated as a claim, not evidence.
Missing source that limits this review: the full vendor/randomx/ tree (only randomx.h and the build hash-checker were provided), config/enclave.toml, crates/enclave/src/main.rs (legacy), crates/cli/, the parent-side port-8005 credential bridge, and measurements/. Where these matter, I say so explicitly.
Verdicts
| Property | Verdict |
|---|---|
| A. Pre-delay confidentiality of request/response contents (URL path+query, response body/headers) from in-scope adversaries | SUPPORTED CONDITIONALLY |
| A. Confidentiality of destination identity (hostname/IP) | VIOLATED by design (acknowledged in threatmodel §6; must not be confused with contents) |
| B. Early recovery of epoch keys/seeds/intermediates; pre-solved-puzzle injection | SUPPORTED CONDITIONALLY (no injection path found; rests entirely on Nitro memory isolation + NSM entropy + measured code) |
| B. Delay magnitude "≈ one week" | SUPPORTED CONDITIONALLY — publication-relative (≈6–7 days), reference-hardware-calibrated, explicitly not hardware-independent |
| B. Fail-closed behavior under delayed/replayed/withheld host messages | SUPPORTED CONDITIONALLY (availability only; a malicious parent can always halt service) |
| Side-channel / microarchitectural resistance | INSUFFICIENT EVIDENCE |
FFI/native correctness of the vendored RandomX fork (RANDOMX_FLAG_V2) |
INSUFFICIENT EVIDENCE |
| Post-release record provenance / archive completeness | LIMITED (acknowledged): records are not individually signed; host ACK ≠ durability |
| Dependency supply-chain hygiene (rustls/aws-lc-rs/hyper/hickory/etc. at locked versions) | INSUFFICIENT EVIDENCE (not audited here) |
A. Can an in-scope adversary recover or infer contents before the intended delay?
Analysis of the content path, byte by byte:
-
Client → enclave. The Python client (
client.py:126–180) performs inner TLS 1.3 (ssl.MemoryBIO,minimum_version = TLSv1_3,CERT_NONE+check_hostname=False) and authenticates only viaverify_document(verify.py:131–142): fresh 32-byte nonce, ±5-minute timestamp, AWS-root chain validity, exact nonzero pinned PCR0,public_key == SPKI of the actual TLS peer on that connection(verify.py:110–114), policy checks includingstate == "ready"for live use (verify.py:126). No application data precedes verification — the only pre-verification payload is the nonce GET.Relay.get()auto-verifies if the stream is absent and never reuses a broken stream (client.py:167–180). This binding chain is the strongest part of the design: relaying a valid attestation cannot authenticate an attacker's TLS endpoint, because the document binds the connection's SPKI. -
Outer transport. Inner TLS bytes are base64'd into GET query parameters (
transport.py:122–174; hosthttp_relay.rs:166–219). Cloudflare/host see ciphertext plus sizes/timing. The stop-and-wait session protocol with exact-replay caching (http_relay.rs:89–163) handles adversarial reorder/replay/truncation without duplicating inner bytes; conflicting retries fail closed. Session hijack by other clients requires the 128-bitreqid; the in-scope transport parties who can seereqidcan disrupt but not decrypt. -
Enclave handling. GET-only, bodyless requests (
v2_proxy.rs:54–58); target validation against SSRF/credential/port tricks (v2_proxy.rs:127–137), rechecked after DNS resolution per address (net.rs:129–131, 240–251) and on every redirect hop (v2_proxy.rs:161–199). No client headers are forwarded upstream; upstream responses are capped at 10 MiB total including redirect history. The sealed record is built (canonical CBOR), encrypted (v2_proxy.rs:104–105), published and ACK-gated before the response is returned (v2_proxy.rs:107–119). Errors never echo request-derived content to clients or logs (static messages only;v2_diagnostics.rs,v2_main.rs:128–132). -
Upstream TLS terminates in the enclave with WebPKI roots (
net.rs:56–71); parent carries ciphertext.
Findings on A:
- A-F1 (Accepted disclosure — destination identity):
v2_main.rs:156constructsrelay::Net::new(transport, false, Vec::new())→Mode::Direct; all upstream connections traverse the parent's CONNECT tunnel, so the parent observes every destination IP. The resolvers are hardcoded to1.1.1.1/8.8.8.8(v2_main.rs:157) — the resolver sees every queried hostname, and 1.1.1.1 is Cloudflare, an explicitly in-scope adversary. ECH (net.rs:77–92) is opportunistic with an explicit non-ECH fallback (net.rs:97–108, brittle string-matching inis_ech_error,net.rs:253–256), and is moot against the resolver anyway. Contents are protected; destination identity is not. This matches the stated objective ("contents") and threatmodel §6, but the documentation's emphasis on ECH/SNI-hiding should not be read as destination confidentiality — against the stated colluding adversary set, there is none. - A-F2 (Traffic analysis): outer request/response sizes and timing are visible (acknowledged §6). No padding. Contents are not inferable from this in general; small fixed corpus responses (e.g., distinguishing a 3-byte vs 1 MiB upstream body) are.
- I found no plaintext echo path: no client-controlled header forwarding, no error-message reflection, static-only diagnostics (
v2_diagnostics.rs:33–41enforces&'static str), host logs static-only (main.rs:74–76), and the boot-failure path replaces the error (v2_main.rs:123–132).
Verdict A: SUPPORTED CONDITIONALLY — conditioned on (i) the client actually executing the full verification path with an independently pinned PCR0, (ii) Nitro/NSM trust, (iii) ordinary primitive strength, and (iv) B below (epoch-key secrecy until solve). Metadata exceptions: destination identity, sizes, timing — real, but not concealing a contents-inference path I could find.
B. Early key/puzzle-capability recovery without breaking RandomX
Working through the enumerated vectors:
-
Generation vs solving parallelism. Generation runs 7 workers in parallel over independent per-segment chains (
timelock/lib.rs:327–351), ≈1 segment-time (~25.14 h measured). Solving is forced serial:x = unwrap(puzzle, segment, &x)gates segment s+1 on the final chain value of segment s (lib.rs:476–492), each wrap AEAD-authenticated with context binding epoch/segment/iterations/dataset_key/key_commitment (lib.rs:246–284), and the final value must matchkey_commitment(lib.rs:493–496). Cross-segment parallelism is structurally impossible; within-segment parallelism is excluded only by the assumed no-shortcut property of the iterated RandomX chain (domain-separated input including epoch/segment/iteration,lib.rs:238–245). The composition itself looks sound; the 7:1 asymmetry is the entire delay mechanism — there is no margin beyond it. -
Shared work / pre-computation.
dataset_keyandseed_1are only disclosed at publication (v2_epoch.rs:88); dataset construction (~2 GiB) is one-time shared cost, not chain progress. External solvers sharing checkpoints can at most split the serial timeline — checkpoints are advisory and a malicious checkpoint is caught at the next segment-boundary AEAD (lib.rs:438–455, 488), wasting ≤1 segment of work (solver-side DoS only). -
Publication ordering. Activation is anchored before puzzle disclosure: attestation →
activated_monotonic/activated_at_ms→ puzzle publish → bundle →ensure!(active.accepts(now))(v2_epoch.rs:76–102). A parent delaying the puzzle ACK shrinks or kills the epoch; it cannot rebase a disclosed puzzle as fresh. The unexecuted e2e testtest_delayed_publication_ack_never_rebases_expired_puzzle_as_fresh_epochclaims coverage of exactly this. The next puzzle is never published before the prior epoch's expiry (publication_not_before,v2_epoch.rs:107–140), gated on NSM time. -
Acknowledged storage.
publishrequires the parent'sOK\n(v2_epoch.rs:57–74) before activation or response delivery — but the ACK is a lie-able statement (threatmodel §5 acknowledges this). Consequence is provenance/recovery, not pre-delay confidentiality. -
Delayed/replayed host messages. Parent protocol parsers are bounded (
transport.rs:18–39, 119–129); DNS is TLS-protected DoH with bounded bodies (dns.rs:33–42); publish ACKs are exact-match; failures are uniformly fail-closed (service closes, requests 503). -
Time. Requests use uncached NSM time (
attest.rs:86–98;v2_proxy.rs:81), cross-checked against a monotonic lifetime (v2_epoch.rs:26–35), and the watchdog erases the active key on monotonic expiry alone (v2_epoch.rs:40–52) — NSM stall cannot prolong key life. -
Restart. Keys and epoch numbering are per-boot NSM-random (
v2_main.rs:169–172;v2_epoch.rs:105); no persistence; old puzzles remain publicly solvable by design. -
Expiration / request leases. Admitted requests hold the epoch
Arcpast rotation, bounded by fetch (30 s) + publish (30 s) deadlines (v2_proxy.rs:89–107) — the expired key survives only inside those leases. -
Key reuse. Per-epoch keys; per-record random 192-bit XChaCha nonces (
lib.rs:103–104); AAD binds epoch/sequence/manifest_id (lib.rs:73–80); sequence allocation is pre-reserved and wrap-proof (v2_proxy.rs:85–87, 122–125). -
Checkpoints / attestations / chosen-input. Checkpoints are solver-local. Attestations are fresh-nonce-bound; historical verification is a separate API that requires only signature-valid evidence at signed time (
archive.py:55–83) — correctly distinguished. There is no decryption oracle anywhere in the enclave (encrypt-only), and request bytes never enter RandomX inputs. There is no path by which an external party supplies or substitutes a puzzle — manifests are self-generated from NSM entropy (all entropy drawn before expensive work,lib.rs:312–325) and self-signed; clients and archive verification bind the signer to the attested policy key (archive.py:139–147).
Findings on B:
- B-F1 (INSUFFICIENT EVIDENCE — custom RandomX fork). The vendored header defines a non-upstream flag
RANDOMX_FLAG_V2 = 128(vendor/randomx/src/randomx.h:52), unconditionally OR'd into VM flags (randomx.rs:59), with the fork pinned to commitaaafe71…andrandomx_algorithm = "v2". The build checks file hashes againstSHA256SUMS(crates/timelock/build.rs:6–13) — that proves integrity against tampering, not correctness of the fork. The entire delay claim rests on this C++ code's hash semantics, memory behavior, and freedom from shortcuts; I was given only the header. The FFI wrapper itself is small, ownership-checked, and plausibly sound (randomx.rs:39–118), includingunsafe impl Sync for Datasetwhose justification (read-only shared cache/dataset, per-worker VMs) matches upstream's documented contract. Request: the fullvendor/randomxtree, itsSHA256SUMS, and the fork's diff vs upstream. - B-F2 (Inherent design reliance — next puzzle lives in memory). Because generation (≈25.14 h) overlaps the active epoch (24 h), the enclave holds the complete solution of the next epoch (seeds, chain finals, epoch key) in memory for essentially the whole current epoch (
v2_epoch.rs:104–150). This is inherent to the design and is why the guarantee collapses to Nitro memory isolation. Rust-side secrets areZeroizing; the RandomX VM scratchpads and C++ allocations are not zeroized on destroy (randomx.rs:112–118), and chain intermediates (which skip work from that point) are sensitive until publication. Under the stated "AWS trusted" assumption this is in-scope-acceptable; it is the correct place to say the assumption is load-bearing. - B-F3 (Conditional — kernel-RNG dependence for nonces). Record AEAD nonces and attestation nonces come from
OsRng/rand::random(kernel getrandom), not NSM directly (lib.rs:103–104,v2_epoch.rs:77). The kernel pool is NSM-seeded once at boot with a tick-spaced double reseed (attest.rs:197–214, 250–270) before any TLS/key generation — correct ordering — but a long-running enclave on the pinned 4.14.256 kernel thereafter relies on that kernel's CRNG. Fail-closed on error. Conditional on AWS/kernel correctness. - B-F4 (Availability edge — poisoned content-addressed name). An interrupted first write leaves a partial file under its digest name;
storerefuses overwrite and refuses identical-byte repair because the existing bytes differ (artifacts.rs:83–119);publishretries identical bytes and fails permanently (v2_epoch.rs:60–73), and for a puzzle this kills the epoch task (v2_epoch.rs:144–147). Requires a host crash/kill mid-write; a malicious host can halt service anyway. Repro sketch (not executed): pause the host betweencreate_newandsync_allfor*.puzzle.json, kill it, restart; enclave republish → "artifact already exists with different contents" → service closed. - B-F5 (Minor).
is_ech_errorstring-matching can force a non-ECH retry, exposing SNI (already exposed to the resolver; metadata only). Legacyconfig/enclave.tomlis compiled into the binary viatlproxy-commonbut inert in v2;build.rs:8reruns on the wrong config file (cosmetic —include_str!still tracksrelay-v2.toml). Signing/TLS private keys are not zeroized on drop (ed25519-dalek without the zeroize feature; rcgenKeyPair) — they live inStateuntil process exit regardless; Nitro teardown is the real erasure. Host CONNECT/UDP line parsers use unboundedread_line(main.rs:234, 379) on a channel only the measured enclave can reach — defense-in-depth nit. - B-F6 (Unverified test claim, not security-relevant). The "canonical CBOR" assertion (
v2_proxy.rs:29–33, 205–211) depends on serde_cborValuemap ordering being length-first; I could not confirm this from source and could not execute the test. Record confidentiality does not depend on it.
Verdict B: SUPPORTED CONDITIONALLY for no-early-key/no-pre-solved-puzzle, with the explicit conditions: Nitro memory isolation, NSM entropy quality, measured-code correctness (this review covers the Rust; the RandomX fork is not covered), and the calibration assumption that a reference sequential solver is ≈506 hashes/s on Graviton5-class hardware. The delay is publication-relative: records sealed near epoch end get ≈6 days, not 7 (acknowledged §6). An adversary with faster sequential RandomX throughput shortens recovery proportionally — this is expressly outside the assumed guarantee.
Strongest code-grounded reasons attacks are blocked
- Attestation binds policy + connection SPKI + fresh nonce; clients pin PCR0 and reject non-ready or mismatched states (
verify.py:52–142;attest.rs:140–168;v2_main.rs:94–114). - Secrets come only from NSM, fail-closed, all drawn before expensive work; kernel reseed precedes TLS/keygen (
attest.rs:43–82, 197–214;v2_main.rs:150–155;lib.rs:312–325). - Activation is anchored before first disclosure; delayed ACKs shrink/kill rather than rebase; rotation fails closed (
v2_epoch.rs:76–150). - Records are sealed and ACK-published before response delivery; every admitted request (including failed upstream fetches) yields a record attempt (
v2_proxy.rs:85–119). - The 7-segment serial composition has no cross-segment shortcut: AEAD-chained wraps + commitment + strict Ed25519 with canonical encoding (
lib.rs:174–236, 458–498;archive.py:97–158). - Host store is content-addressed, digest-verified, never-overwrite (
artifacts.rs:67–123); public serving re-verifies hashes (artifacts.rs:208–233). - Bounded resources at every adversary-controlled input (sessions, pending, frames, headers, bodies, connections, permits).
Unresolved questions / requested source
- Full
vendor/randomxsource +SHA256SUMS+ theRANDOMX_FLAG_V2fork diff (highest priority — the delay primitive itself). config/enclave.toml,crates/enclave/src/main.rs,crates/cli/(completeness/reachability confirmation).- The parent-side port-8005 credential bridge implementation (
hardware.rs:130–147connects to it; it is not in the frozen tree). measurements/graviton5-calibration-*and evidence for any completed full-duration production generation/rollover/recovery (docs claim these remain unverified).- Deployment facts: nitro-cli invocation (debug mode off, CPU/memory), outer Cloudflare config.
- Whether serde_cbor
Valuemap ordering is actually RFC 7049 canonical (test unexecuted). - Dependency-advisory status of the locked tree (not audited).
Would I rely on this for sensitive content under the explicit assumptions?
Conditionally, for contents only: the Rust-side construction is careful, consistently fail-closed, and I found no pre-delay contents-inference path. I would not rely on it to hide which sites are contacted (resolver + parent + IP exposure is structural). I would treat the one-week delay as a ≈6–7-day, publication-relative, hardware-speed-calibrated estimate — not a proven lower bound — and I would not place high-value reliance on the delay until the custom RandomX fork is audited and at least one full-duration production epoch rollover and independent recovery has been observed. The docs themselves state these remain unverified.