Independent Pi/OpenRouter review: qwen/qwen3.8-max-0902
Source: a10323dede4413fbf295916b8ad12e3dbad7514e. Status: completed.
Independent Security Review: Attested Relay v2 (Timelock Proxy)
Source commit: a10323dede4413fbf295916b8ad12e3dbad7514e
Reviewer scope: Frozen source as supplied. No tools executed; no tests run. Documentation at 85c61e9 treated as claims to evaluate, not evidence.
I. Executive Summary
The design is architecturally sound for its stated objective: an AWS Nitro Enclave terminates inner TLS, proxies GET requests upstream, encrypts the full audit record with an epoch key, and publishes that key only inside a RandomX puzzle requiring approximately seven serial CPU-days to solve. The host/parent is treated as hostile throughout and never receives plaintext request/response content or the epoch key.
I found no concrete code path by which an in-scope adversary recovers request/response plaintext or the epoch key before the intended delay, without breaking the assumed RandomX primitive or AWS Nitro isolation. However, several properties rest on conditions that are not fully verifiable from source alone, and the delay guarantee is publication-relative rather than per-request, with a meaningful gap for late-epoch records.
I would rely on this for sensitive content under the stated assumptions conditionally, with the caveats enumerated below.
II. Property-by-Property Assessment
A. Confidentiality of request/response contents from intermediaries and operator
SUPPORTED CONDITIONALLY.
Reasons supported:
-
Inner TLS terminates inside the enclave. The host's
http_relay.rstreats the inner TLS stream as an opaque byte sequence (crates/host/src/http_relay.rs:1–6: "Nothing in this module interprets or authenticates inner TLS bytes"). The enclave accepts TLS on vsock port 8443 (v2_main.rs:180–197), and the host merely copies bytes bidirectionally (host/src/main.rs:199). The host never possesses the enclave's TLS private key, which is generated at boot inside the enclave (tls.rs:69–70) and never serialized outside it. -
Records are encrypted before leaving the enclave. In
v2_proxy.rs:104–107, the canonical CBOR record is encrypted viarelay_timelock::encrypt_recordusing the epoch key, and only the resultingEncryptedRecordJSON (containing only ciphertext, nonce, and identifiers) is published to the host. The plaintext is wrapped inZeroizingand dropped after encryption. -
The epoch key never crosses the enclave boundary. It is generated from NSM randomness (
timelock/src/lib.rs:320–321), stored asZeroizing<[u8;32]>inEpoch.key(v2_epoch.rs:12), and used only withinencrypt_recordinside the enclave. The host receives only the wrapped-key puzzle manifest, where the epoch key is encrypted under the output of the seventh RandomX segment. -
The parent's CONNECT tunnel carries only IP:port, and upstream TLS is verified in the enclave. DNS resolution happens inside the enclave via DoH (
dns.rs:89–168), the TCP connection is tunneled through the parent (transport.rs:135–141), and TLS verification happens in the enclave (net.rs:96–153). The parent sees the destination IP but not the HTTP content. When a WireGuard relay is configured (relay.rs:85–110), even the destination IP is hidden from the parent. -
Diagnostics emit only static strings.
v2_diagnostics.rs:33–41accepts only&'static str, rejects non-printable content, and drops when the queue is full. The test attests/test_v2_e2e.py:338–339verifies that upstream hostnames, paths, and response payloads do not appear in host-visible logs. The boot-failure test at line 413–420 confirms untrusted configuration values do not leak.
Conditions limiting this conclusion:
- C1: The parent can observe inner-TLS record sizes, connection timing, and count. It can see the outbound destination IP (and SNI, unless ECH is active). These are acknowledged metadata disclosures (
threatmodel.md§6) but could enable traffic-analysis inference of content in some threat scenarios (e.g., a unique response size for a known endpoint). This is metadata, not content decryption, but the distinction matters for high-value targets. - C2: The kernel RNG seeding (
attest.rs:67–82) seeds the Linux CRNG from NSM before TLS initialization. I cannot verify from source that every subsequentgetrandom()call in every library (aws-lc-rs, rustls internals, tokio) actually draws from the reseeded CRNG rather than a secondary or fallback source. The two-reseed-with-tick-wait approach (seed_kernel_from,attest.rs:198–214) is well-reasoned for the pinned 4.14.256 kernel, but I cannot confirm the kernel's CRNG propagation behavior without executing on that exact kernel. - C3: The
OsRngused for record nonces inencrypt_record(timelock/src/lib.rs:104) draws from the kernel RNG. In the enclave, this should be the NSM-seeded CRNG. In the standalone CLI, it uses the host OS RNG. This is acceptable for the stated use cases but means the nonce quality depends on the kernel seeding path being correct.
B. Epoch key / seed confidentiality and puzzle integrity
SUPPORTED CONDITIONALLY.
Reasons supported:
-
Generation entropy is NSM-sourced and fail-closed.
generate_with_rng(timelock/src/lib.rs:301–398) obtains all entropy (dataset key, 7 seeds, epoch key, 7 wrap nonces = 16 fills) before any expensive work. If any fill fails, generation aborts. The enclave suppliesattester.fill_randomas the entropy source (v2_epoch.rs:123–124). The test attimelock/src/lib.rs:580–610verifies that every entropy call is required and failure at any point aborts generation. -
Only seed_1 and dataset_key are published; seeds 2–7 and the epoch key are wrapped. The manifest (
timelock/src/lib.rs:352–364) publishesdataset_key,seed_1, andwrapped_keys[0..6]. Eachwrapped_keys[i]is a ChaCha20Poly1305 ciphertext ofseeds[i+1](orepoch_keyfori=6), keyed by HKDF of segmenti's RandomX output. Without computing the full segment chain, the wrapped keys are unrecoverable. -
The signing key is generated from NSM at boot and never exported.
v2_main.rs:169–172: the seed is generated viaattester.fill_random, wrapped inZeroizing, used to create the Ed25519SigningKey, then explicitly dropped. The signing key lives inState.signerfor the process lifetime, which is necessary for signing each epoch's puzzle. It is never serialized to the host. -
The puzzle is signed and the signature is verified by solvers.
SignedManifest::verify(timelock/src/lib.rs:216–226) checks the Ed25519 signature over the canonical binary encoding usingverify_strict. The solver (solve, line 466) calls this before any computation. The Python client's_strict_signature(archive.py:107–116) additionally rejects small-order points and noncanonical scalars, matchinged25519-dalek's strict verification. -
The key commitment binds the epoch key to the puzzle.
key_commitment = SHA256(epoch_key)is published in the manifest. Bothrecord_key(timelock/src/lib.rs:82–84) and the solver's final check (timelock/src/lib.rs:493–496) verify this commitment. A wrong key fails before any record decryption or after solving.
Conditions:
- C4: The
dataset_keyis published in the manifest. This is necessary for the solver to initialize the RandomX dataset. It does not shortcut the hash chain computation, but it does mean the dataset initialization cost (which is parallelizable) is borne by the solver, not the generator. This is by design and does not weaken the serial work assumption. - C5: I cannot verify from source alone that the vendored RandomX C code at
vendor/randomxis unmodified beyond the SHA256SUMS check incrates/timelock/build.rs:4–13. The build script hashes each file against the manifest, which is a compile-time integrity check. However, I do not have the actualvendor/randomxsource files or their SHA256SUMS to verify independently. The header (vendor/randomx/src/randomx.h) appears to be standard RandomX with aRANDOMX_FLAG_V2 = 128addition.
C. Resistance to pre-solved or substituted puzzles
SUPPORTED CONDITIONALLY.
-
The host cannot inject a puzzle. Puzzles are generated inside the enclave (
v2_epoch.rs:122–125), signed with the enclave's attested key, and published viapublish(). The host stores the puzzle as a content-addressed file. A substituted puzzle would either lack the enclave's signature or have a different content hash. The Python client'sverify_bundle(archive.py:119–158) cross-binds the puzzle signature to the attested service key and verifies all fields match the attested policy. -
The host cannot replay an old puzzle for a new epoch. Each puzzle has a unique epoch number, random seeds, and a fresh signature. The attestation document includes
current_epoch(v2_main.rs:89), and the client checks this. Thepublication_not_beforemechanism (v2_epoch.rs:110) prevents the next puzzle from being published before the current epoch expires. -
Delayed publication ACKs cannot extend an epoch. The
activated_monotonicis captured atv2_epoch.rs:82, before the puzzle is published at line 88. The expiration is computed fromactivated_at_ms + epoch_seconds * 1000. The monotonic watchdog (v2_epoch.rs:40–52) independently expires the epoch. The testtest_delayed_publication_ack_never_rebases_expired_puzzle_as_fresh_epoch(tests/test_v2_e2e.py:345–361) verifies that a 6-second ACK delay on a 5-second dev epoch causes publication failure, not extension. -
The
--devflag cannot be injected in production. The enclave binary is measured by PCR0. The EIF's entrypoint is["/attested-relay-enclave"](Dockerfile line 35), without--dev. The host cannot modify the command line without changing the EIF measurement. Production enforcesrequire_graviton5,iterations > 0, andepoch_seconds == 86400(v2_main.rs:142–145).
Condition:
- C6: The host controls the storage layer and could theoretically withhold the puzzle publication ACK, causing the 60-second activation timeout (
v2_epoch.rs:144) to expire. This would close the service (fail-closed), not leak information. The host could also delete stored puzzles after publication, affecting future recovery but not current confidentiality.
D. Record encryption and nonce management
SUPPORTED CONDITIONALLY.
-
XChaCha20Poly1305 with random 24-byte nonce.
encrypt_record(timelock/src/lib.rs:103–104) generates a fresh random nonce per record. With 192-bit random nonces, the collision probability is negligible for any practical number of records per epoch. -
AAD binds record to epoch and sequence. The
record_context(timelock/src/lib.rs:73–80) includes the protocol version, epoch, sequence, and manifest ID. This prevents record swapping between epochs or sequence positions. The test attimelock/src/lib.rs:630–635verifies that tampering with sequence or epoch causes decryption failure. -
Sequence numbers cannot wrap.
next_sequence(v2_proxy.rs:122–125) usesfetch_updatewithchecked_add(1), failing atu64::MAX. The test atv2_proxy.rs:213–219confirms exhaustion is detected and does not wrap. -
The record key is derived per-puzzle.
record_key(timelock/src/lib.rs:81–91) uses HKDF with the manifest ID as info, so different epochs produce different record keys even if the epoch key were somehow reused (which it is not, since each epoch generates a fresh key).
Condition:
- C7: Individual record envelopes are not service-signed. After the epoch key becomes public, anyone can create a valid AEAD record for that epoch. This is acknowledged in
threatmodel.md§5: "Individual record envelopes are not service-signed." Provenance requires a previously trusted ciphertext digest. This is a post-release provenance limitation, not a pre-release confidentiality issue.
E. Authentication and attestation binding
SUPPORTED CONDITIONALLY.
-
The attestation document binds the TLS SPKI.
attestation_for_epoch(v2_main.rs:102–114) includes the TLS certificate's SPKI DER in the attestation'spublic_keyfield. The Python verifier checksdoc.get("public_key") != spki(verify.py:113). This prevents a MITM from presenting a valid attestation for a different TLS key. -
The client verifies a fresh nonce. The client generates a 32-byte nonce (
client.py:151), includes it in the attestation request, and the verifier checksdoc.get("nonce", b"") == nonce(verify.py:137). This prevents replay of old attestation documents. -
PCR0 is pinned by the client. The client supplies
expected_pcr0and the verifier checkspcrs[0] == expected(verify.py:76–77). The threat model correctly states that a PCR supplied by the server is not a trust anchor. -
The policy includes the signing public key. The
service_signing_public_keyin the attested policy (v2_main.rs:85) allows the client to verify that subsequent puzzles are signed by the same key.
Conditions:
- C8: The attestation timestamp check allows a 5-minute window (
verify.py:87–88). Within this window, a replayed attestation from the same enclave would pass. However, the nonce check prevents cross-session replay. - C9: The
trusted_time_ms(cached) vstrusted_time_ms_uncacheddistinction (attest.rs:86–98vs104–132) is correctly applied: epoch admission uses the uncached version (v2_epoch.rs:83,v2_proxy.rs:81), preventing a stale cached timestamp from extending an epoch's acceptance window.
III. Concrete Findings
Finding 1: Publication-relative delay creates a variable per-request window
Severity: Design limitation, not a defect.
Location: v2_epoch.rs:82–88, config/relay-v2.toml:7
Description: The epoch lifetime is 86400 seconds (24 hours). All records within an epoch are encrypted with the same epoch key. The puzzle is published at epoch activation. The last request in an epoch has its record published just before the epoch expires, meaning the puzzle for that record has been public for nearly 24 hours by the time the next epoch's puzzle is published. The effective delay for the last record is approximately 7 days minus up to 24 hours, not a full 7 days from the request.
More precisely: the delay for any record is measured from the puzzle's publication (which happens at epoch activation), not from the record's creation. A record created 23 hours into an epoch has only ~6 days + 1 hour of remaining delay after the epoch ends, because the puzzle was already public for 23 hours.
Impact: The minimum delay for any record is approximately 6 days (7 days minus the 24-hour epoch), not 7 days. The threat model acknowledges this: "Approximate delay starts at puzzle publication, not at each request."
Not a violation of the stated objective ("approximately one week"), but the worst case is meaningfully shorter than 7 × 24 hours.
Finding 2: Generation time may exceed epoch duration
Severity: Availability concern, not confidentiality.
Location: config/relay-v2.toml:4–6, v2_epoch.rs:104–151
Description: The configuration comment states "Seven-worker generation measured ~25.14h; a 24h epoch expires closed if late." With epoch_seconds = 86400 (24h) and generation taking ~25.14h, the next puzzle cannot be generated and published before the current epoch expires. The code handles this by deactivating the old epoch and waiting for the publication deadline, but the service enters a "warming" state during the gap.
Impact: There will be periodic windows where the service is unavailable (state = "warming"). During these windows, requests receive 503 ("waiting for a published epoch"). This is fail-closed and does not leak content, but it means the service cannot sustain continuous operation with the current parameters.
Attestation: I did not execute the generation benchmark. The 25.14h figure is from the configuration comment and the threat model. The code correctly handles this by failing closed.
Finding 3: Host can observe and manipulate publication ACKs
Severity: Acknowledged design limitation.
Location: v2_epoch.rs:57–74, host/src/main.rs:330–350
Description: The publish function sends data to the host and waits for an "OK\n" acknowledgement. The host controls whether and when this ACK arrives. A malicious host can:
- Delay the ACK (mitigated by
activated_monotonicandpublication_not_before) - Withhold the ACK entirely (causes publication failure, service closes)
- Acknowledge but not persist (enclave cannot verify durability)
- Delete files after acknowledging (affects future recovery)
Impact: Availability and durability, not confidentiality. The enclave's cryptographic operations are complete before publication. The host cannot learn plaintext from the publication process.
Code evidence: v2_epoch.rs:56: "An untrusted host can still lie about durability; independent replicas and client-held receipts are required for the availability promise."
Finding 4: Upstream response is returned to client before record persistence is confirmed
Severity: Not a confidentiality issue, but an ordering concern.
Location: v2_proxy.rs:105–119
Description: The flow is: encrypt record → publish record → construct response → return response. The response is returned only after publish succeeds (line 107 uses ??). If publication fails, the client receives a 502 error, not the upstream response. This means the client does not receive the response unless the encrypted record is persisted.
However, the upstream has already processed the request by this point. If the host kills the enclave after the upstream responds but before publication completes, the upstream interaction occurred but no record was stored. The threat model acknowledges this: "A host can also kill the enclave after an upstream has observed a request but before capture finishes."
Impact: Not a content leak. The upstream already saw the request. The concern is audit completeness, not confidentiality.
Finding 5: validate_target does not block all DNS rebinding scenarios
Severity: Low, mitigated by design.
Location: v2_proxy.rs:127–137, net.rs:129–131
Description: validate_target checks the URL structure and rejects private IPs if the host is an IP literal. However, for hostname targets, the DNS resolution happens later in connect_tls_once (net.rs:111), and the resolved IP is checked against public_destination only for Purpose::Upstream (net.rs:129). This is correct: the IP check happens after resolution, not before.
The redirect handling (v2_proxy.rs:163–164) re-validates the target on each redirect, preventing redirect-based SSRF. The test at v2_proxy.rs:221–226 covers private IPs, credentials, wrong ports, and localhost.
Residual concern: A DNS name could resolve to a public IP during validation but a private IP during connection (DNS rebinding). However, the enclave resolves DNS itself via DoH (dns.rs), and the resolved IP is checked in connect_tls_once before connecting. The window for rebinding within a single request is very small, and the attacker would need to control both the DNS response and the timing.
Assessment: The mitigation is adequate for the stated threat model. The enclave's own DNS resolution and IP validation provide defense-in-depth.
Finding 6: log_infra accepts dynamic strings but routes to static-only diagnostics
Severity: Informational, no vulnerability found.
Location: v2_main.rs:26, relay.rs:71–76
Description: log_infra is defined as fn log_infra(_message: impl AsRef<str>) { log("relay infrastructure state changed"); }. The dynamic message is discarded (parameter is _message), and only the static string "relay infrastructure state changed" is emitted. This is correct: the relay mode transitions in relay.rs:72–75 format dynamic strings but they are swallowed by log_infra.
Assessment: No information leak. The design intentionally discards dynamic content.
IV. Strongest Code-Grounded Reasons Attacks Are Blocked
-
Epoch key isolation. The epoch key is generated from NSM randomness (
timelock/src/lib.rs:320–321), stored inZeroizing<[u8;32]>(v2_epoch.rs:12), used only for HKDF derivation insideencrypt_record, and never serialized. The host receives only the wrapped-key manifest where the key is encrypted under the seventh segment's RandomX output. There is no code path that sends the raw epoch key across the vsock boundary. -
Content-addressed immutable storage.
artifacts.rs:83–122usescreate_new(true)and verifies SHA256 digest against the filename. Existing files are never overwritten or deleted. A malicious host cannot substitute a different puzzle or record without changing the content-addressed name, which would be detected by any client that verifies the digest. -
Monotonic expiration independent of NSM. The
expiration_watchdog(v2_epoch.rs:40–52) usesInstant::now()(monotonic clock) and does not depend on NSM availability. Even if the NSM becomes unresponsive, the watchdog expires the epoch. The test atv2_epoch.rs:178–193verifies that the watchdog releases the key without any NSM or generation call. -
Fail-closed entropy. All production entropy comes from
attester.fill_random, which uses the NSM directly (attest.rs:52–56). If the NSM fails, the destination is zeroized and an error is returned. There is no fallback to a weaker source. The test atattest.rs:317–340verifies bounded behavior and output clearing on failure. -
Signed puzzle with strict verification. The Ed25519 signature covers a fixed-width canonical binary encoding (
Manifest::canonical_bytes,timelock/src/lib.rs:175–202). The solver verifies the signature before any computation (solve, line 466). The Python verifier additionally rejects small-order points (archive.py:100–116). A forged or modified puzzle fails verification. -
Inner TLS prevents host inspection of HTTP content. The host's
http_relay.rsimplements a stop-and-wait byte relay. It never parses, logs, or inspects the payload content. Thelogfunction (host/src/main.rs:74) accepts only&'static str.
V. Missing Evidence Limiting Conclusions
-
I did not execute any tests. All test references are to code I read but did not run. The integration tests (
tests/test_v2_e2e.py) are comprehensive but require a built Rust toolchain and specific dev-mode setup. -
I do not have the vendored RandomX source. The
vendor/randomxdirectory is referenced but not included in the snapshot. Thebuild.rsSHA256 check provides compile-time integrity, but I cannot verify the actual C implementation matches upstream or is free of modifications. -
I cannot verify the Nitro Enclave isolation properties. The security model depends on AWS Nitro preventing the parent from reading enclave memory. This is an AWS trust assumption, not something verifiable from source.
-
I cannot verify the kernel configuration. The
seed_kernel_fromfunction references specific kernel behavior (HZ=250, per-NUMA CRNGs, RNDRESEEDCRNG ioctl). I cannot confirm these properties hold on the actual deployed kernel without access to the kernel configuration. -
The production measurement evidence (
measurements/graviton5-production-a10323d-20260909/,reviews/current/) is referenced but not included. I cannot verify that the production PCR0 matches the CI-reproduced measurement. -
I did not review the
vendor/i18n-embed-flpatch referenced inCargo.toml:37–38. This is a build dependency ofageand is unlikely to affect security, but it is a supply-chain modification. -
The
config/enclave.tomlfile is referenced bycrates/common/src/lib.rs:13(include_str!("../../../config/enclave.toml")) but is not included in the snapshot. This is the legacy v1 configuration. The v2 binary usesconfig/relay-v2.tomlinstead. I cannot verify that the legacy config is not accidentally included in the v2 build path.
VI. Unresolved Questions
-
Q1: Does the EIF configuration fix the command-line arguments such that
--devcannot be injected? The Dockerfile showsENTRYPOINT ["/attested-relay-enclave"]without--dev, but I cannot verify the EIF packaging without the Nitro tooling. -
Q2: Is the
config/enclave.tomlfile (legacy v1) included in the v2 enclave image? Theinclude_str!incrates/common/src/lib.rs:13compiles it into thetlproxy-commoncrate, which is a dependency oftlproxy-enclave. If the v2 binary linkstlproxy-common, the legacy config string is in the binary. This does not appear to be a security issue (it's a different protocol), but it increases the attack surface for parser bugs. -
Q3: What is the actual solver fleet configuration? The threat model mentions "nine-worker solver fleet" but the source only implements single-process solving with optional checkpointing. The parallelism of the solver fleet affects the actual delay.
-
Q4: Are the S3 uploads authenticated by the enclave? The threat model says "Enclave-authenticated S3 storage/retention verification is not implemented." This means the archive provider could substitute or delete artifacts without detection by the enclave.
-
Q5: The
require_graviton5flag is in the compiled config. If a future deployment targets a different instance type, does the config change require a new PCR0? (Yes, since it'sinclude_str!'d into the binary.)
VII. Verdict on Stated Objective
"Can an in-scope adversary recover or infer request/response contents before the intended delay?"
No concrete attack path found. The host sees only encrypted inner-TLS bytes and encrypted records. The epoch key is generated, used, and destroyed within the enclave. The puzzle construction requires serial RandomX work to unwrap the key. The attestation binding prevents puzzle substitution. However, the delay is publication-relative (not per-request), metadata leakage (timing, size, destination IP) is acknowledged, and the guarantee depends on the RandomX primitive, AWS Nitro isolation, and correct kernel RNG seeding—all stated assumptions.
"Can an adversary obtain epoch keys, seeds, or equivalent decryption capability early, or force use of a pre-solved puzzle, without breaking RandomX?"
No concrete attack path found. The generation-solve asymmetry (7 parallel workers vs. serial chain) is correctly implemented. The host cannot inject, substitute, or replay puzzles because they are signed by the enclave's NSM-generated key. The activated_monotonic and publication_not_before mechanisms prevent delayed-ACK extension. The --dev flag cannot be injected without changing the measured image. However, I cannot verify the vendored RandomX C code, the Nitro isolation, or the kernel RNG behavior from source alone.
Would I rely on this for sensitive content under the explicit assumptions?
Conditionally, yes, with the following reservations:
- The minimum effective delay is ~6 days, not 7, due to publication-relative timing.
- The service has availability gaps due to generation time exceeding epoch duration.
- Post-release provenance is limited (no per-record signatures).
- Storage durability depends on the host's honesty and independent replication.
- I could not verify the vendored RandomX source, the kernel configuration, or the production PCR measurements.
- The guarantee is "no known shortcut" for RandomX, not a proven VDF lower bound.
For content where a 6-day delay is acceptable and the stated trust assumptions (AWS, RandomX, WebPKI) are deemed sufficient, the implementation provides a well-engineered defense against the stated adversaries. For content requiring stronger guarantees (permanent confidentiality, provenance, or availability), additional mechanisms beyond this system are needed.