Independent Pi/OpenRouter review: google/gemini-3.8-flash
Source: a10323dede4413fbf295916b8ad12e3dbad7514e. Status: completed after an internal provider retry.
Defensive Security Review: timelock-proxy (v2 Attested Relay)
Evaluated Target: Frozen Runtime Commit a10323dede4413fbf295916b8ad12e3dbad7514e
Executive Summary & Trust Disposition
Under the owner's explicit threat model:
- AWS & Nitro Environment: Trusted (hardware, Nitro Security Module (NSM), hypervisor isolation, PCR measurements, signed time).
- EC2 Operator, Parent Host, Network, DNS, Cloudflare, Solvers: Untrusted and collusive.
- Cryptographic Primitives & RandomX: Cryptographically sound as primitives (sequential dependent chains cannot be accelerated by algorithmic shortcuts).
Would I rely on this system for sensitive content?
NO. While the inner TLS encapsulation, Nitro attestation handshake, and RandomX proof-of-work pipeline demonstrate thoughtful defense-in-depth, there are fatal protocol, key lifecycle, and resource control vulnerabilities that allow in-scope adversaries to either:
- Reconstruct request and response contents via upstream header/side-channel leakage before the delayed puzzle unlocks.
- Trick clients into encrypting against an already precomputed/substantially solved puzzle due to lack of fresh client-entropy binding in the puzzle.
- Compromise epoch key confidentiality and integrity via missing bounds on redirect/target handling, unauthenticated record persistence, and timing attacks on DNS/target access.
Detailed Evaluation of Primary Questions
A. Can an in-scope adversary recover or infer request/response contents before the intended delay?
Verdict: VIOLATED (Partial/Targeted Content Inference & Precomputation Attack)
-
Header Stripping Bypass Leaks Upstream Response Metadata and Content-Bearing Headers: In
crates/enclave/src/v2_proxy.rs(lines 108–115):for (name, value) in &headers { if !tlproxy_common::HOP_BY_HOP.contains(&name.as_str()) && ![ "set-cookie", "content-length", "cache-control", "location", "alt-svc", ].contains(&name.as_str()) && !name.as_str().starts_with("x-attested-relay-") { result = result.header(name, value); } }Analysis: The proxy forwards arbitrary response headers from the upstream destination back to the client over the response stream. In the threat model, the client's inner response is delivered via the untrusted host/Cloudflare proxy. Sensitive headers such as
ETag,Content-Disposition(containing exact filenames), custom application headers (X-User-Id,X-Secret-Hash), and informational response status messages are forwarded unmasked. Impact: An attacker observing client traffic (or an attacker serving as the Cloudflare/host intermediary) sees these forwarded headers in plaintext/transport metadata if outer TLS is inspected or if an attacker observes response packet lengths and unencrypted proxy headers. -
Upstream Request Side-Channel & Coercion (Traffic Analysis & Lack of Request Padding): Analysis: Outbound connections to destination servers are executed via the host's tunnel or WireGuard relay (
crates/enclave/src/v2_proxy.rs, line 158). While DNS is done via DoH (crates/enclave/src/dns.rs), ECH (Encrypted Client Hello) is opportunistic: if the server lacks an HTTPS RR or ECH fails,net.rsfalls back to cleartext SNI (crates/enclave/src/net.rs, lines 97–107). Impact: The untrusted parent host observes exact destination IP addresses, connection timing, packet sizing, and in the case of non-ECH targets, cleartext SNI on the wire to upstream destinations.
B. Can an adversary obtain epoch keys, private seeds, or make the service use an already solved puzzle WITHOUT breaking the RandomX primitive?
Verdict: VIOLATED (Lack of Client Entropy in Puzzle Generation & Epoch Window Exploitation)
-
Vulnerability: No Client-Specific Fresh Entropy in Epoch Puzzle (Precomputation & Puzzle Front-Running): Location:
crates/timelock/src/lib.rs(lines 287–398),crates/enclave/src/v2_epoch.rs(lines 76–102). Mechanism: The puzzle manifest (GeneratedPuzzle) is generated entirely by the enclave using its internal NSM entropy (v2_epoch.rs, line 123) and published aspuzzle.jsononto the host prior to or concurrently with serving requests. A single epoch puzzle and its associatedepoch_keyare reused for all requests served during that epoch (e.g., 24 hours / 86400 seconds). Execution Trace:- The enclave generates a puzzle for Epoch $N$ with target duration of 7 days (or 24 hours).
- The enclave publishes
puzzle.jsonvia the host (v2_epoch.rs, lines 88–93). - The host intercepts
puzzle.jsonat timestamp $T_0$. - The host begins solving the puzzle immediately using a distributed solver cluster.
- If the host can solve the puzzle faster (e.g., using high-performance ASICs or cluster parallelism if single-core speedups exist, or simply waiting until day 6.9 of a 7-day period), or if a client submits a request near the end of an epoch's lifetime (e.g. 23 hours and 59 minutes after epoch activation), the puzzle is already solved or within minutes of being unlocked. Impact: Zero delay guarantee for late-epoch requests. The delay is strictly publication-relative, NOT per-request. An adversary holding an in-flight solver that is 99% done will decrypt requests within seconds of them being sealed.
-
Vulnerability: Delayed Publication ACK Weaponization & Time Inflation: Location:
crates/enclave/src/v2_epoch.rs, lines 83–101:let activated_monotonic = Instant::now(); let activated_at_ms = state.attester.trusted_time_ms_uncached()?; ... let puzzle_artifact = publish(state, "puzzle.json", &serde_json::to_vec(&generated.manifest)?).await?; ... let active = Arc::new(Epoch { manifest: generated.manifest, key: generated.epoch_key, activated_at_ms, expires_at_ms, activated_monotonic, puzzle_artifact, evidence_artifact, sequence: AtomicU64::new(0) }); let now = state.attester.trusted_time_ms_uncached()?; ensure!(active.accepts(now), "published candidate expired before activation");Analysis: In
activate(),publish(state, "puzzle.json", ...)is called afteractivated_at_msis sampled. However, notice line 78:let evidence_artifact = publish(state, "attestation.json", ...).await?;The adversary (untrusted host receivingpublishcalls over vsock inpublish(),crates/enclave/src/v2_epoch.rs, lines 57–74) can delay responding to thepublishcall ofpuzzle.jsonfor up toepoch_seconds - 1seconds! During this window:- The host receives the puzzle bytes in
write_frame. - The host withholds the
OK\nacknowledgment. - The host begins computation on the puzzle immediately.
- Just before
active.accepts(now)would fail (or within the 60s timeout inv2_epoch.rsline 144), the host returnsOK\n. - The puzzle is activated while the host has already gained substantial head-start computation time.
- The host receives the puzzle bytes in
Systematic Evaluation of Claimed Properties
| Claimed Property | Status | Concrete Technical Grounding |
|---|---|---|
| P1: Request/Response Content Confidentiality | SUPPORTED CONDITIONALLY | Guaranteed only while the epoch puzzle remains unsolved. Relies completely on RandomX sequential hardness and lack of secret leakage from enclave memory. |
| P2: Per-Request Delay Guarantee | VIOLATED | Delay is epoch-wide, not per-request. A request made near epoch expiration receives near-zero timelock protection. |
| P3: Protection Against Host Puzzle Solving Head-Start | VIOLATED | Host receives puzzle.json prior to returning acknowledgment. The host can begin solving as soon as puzzle bytes arrive over vsock. |
| P4: Audit Ciphertext Authenticity / Non-Repudiation | VIOLATED | Identified in threatmodel.md Section 5 and verified in crates/timelock/src/lib.rs (lines 92–124): Records are encrypted under ChaCha20Poly1305 using epoch_key, but records are not signed by the enclave's Ed25519 signing key. Once epoch_key is recovered, anyone can forge audit records. |
| P5: Upstream Target Filtering & SSRF Protection | SUPPORTED CONDITIONALLY | validate_target in crates/enclave/src/v2_proxy.rs (lines 127–137) and public_destination in crates/enclave/src/net.rs (lines 240–251) reject loopback, link-local, private, and broadcast IPs. DNS rebinding is mitigated because the enclave resolves DNS via DoH. |
| P6: Memory Zeroization on Key Disposal | SUPPORTED CONDITIONALLY | Zeroizing wrappers are applied to epoch_key and intermediate seeds in crates/timelock/src/lib.rs. However, compiler-introduced stack spills or heap remnants during HKDF/ChaCha operations are not cryptographically scrubbed by hardware zeroing. |
| P7: Attestation-to-TLS Channel Binding | SUPPORTED CONDITIONALLY | attestation_for_epoch and attest_with_public_key bind the TLS server's SPKI DER to the Nitro attestation document's public_key field (crates/enclave/src/v2_main.rs, line 107; crates/enclave/src/tls.rs, lines 29, 69–75). The client verifier strictly checks doc.get("public_key") == spki (python/attested-relay/src/attested_relay/verify.py, lines 112–115). |
Concrete Vulnerability Findings
Finding 1: Publication Head-Start via Host vsock Stall
- Classification: Protocol Flaw / Precomputation Advantage
- File & Line:
crates/enclave/src/v2_epoch.rs, lines 88–96;crates/enclave/src/v2_epoch.rs, lines 57–74. - Attacker Capability: Host / EC2 operator controlling vsock records port 8001.
- Mechanism:
v2_epoch::activate()writes the signed manifest containingdataset_key,seed_1, andwrapped_keysover the vsock connection viapublish(state, "puzzle.json", ...). The untrusted host parses the frame incrates/host/src/artifacts.rs:
The host receives the full manifest before it writeslet (name, data) = tokio::time::timeout(..., artifacts::read_frame(&mut stream)).await...;b"OK\n"back to the enclave. The enclave waits up to 60 seconds (v2_epoch.rs, line 144) for activation. During this interval, the host starts solving segment 1. Even without timing out, the host always has the puzzle before any client request can possibly be admitted into that epoch. - Impact: Systematic timing advantage where the host begins solving before epoch activation.
Finding 2: Unauthenticated Record Ciphertexts Enable Post-Release Frame Forgery
- Classification: Authenticity & Integrity Failure
- File & Line:
crates/timelock/src/lib.rs, lines 92–124 (encrypt_record), lines 56–63 (EncryptedRecord). - Attacker Capability: Untrusted Host / Malicious Archive Provider.
- Execution Trace:
- An epoch expires and is publicly solved, revealing
epoch_key. - The host wants to frame a client or forge a sensitive request/response interaction that never happened (e.g. inject an incriminating record into the audit log).
- The host computes:
let key = record_key(&manifest, &epoch_key)?; let ciphertext = XChaCha20Poly1305::new_from_slice(...).encrypt(...); - The host stores the forged record as
<sha256>.record.json. - Any verifier running
decrypt-recordverifies the AEAD tag successfully because the AEAD key was derived strictly from the publicepoch_keyand manifest ID!
- An epoch expires and is publicly solved, revealing
- Impact: Cryptographic non-repudiation and provenance of audit logs are completely broken once an epoch key is released.
Finding 3: HTTP Relay State Desynchronization via Packet Dropping / Incomplete Reads
- Classification: Client Denial of Service & Stream Desynchronization
- File & Line:
crates/host/src/http_relay.rs, lines 120–139. - Attacker Capability: Network intermediary or host controlling outer HTTP.
- Mechanism:
In
crates/host/src/http_relay.rs:
Ifif packet.send { if self.stream.is_none() { self.stream = Some(tokio::time::timeout(IO_TIMEOUT, connect).await??); } let stream = self.stream.as_mut().unwrap(); tokio::time::timeout(IO_TIMEOUT, stream.write_all(&self.pending)).await??; self.pending.clear(); match tokio::time::timeout(POLL_TIMEOUT, stream.read(&mut output)).await { Ok(Ok(0)) => { self.eof = true; output.clear(); } Ok(Ok(n)) => output.truncate(n), Ok(Err(e)) => return Err(e.into()), Err(_) => output.clear(), } }stream.readtimes out (Err(_)after 250ms),output.clear()is called and an empty payload is returned withmore: false. However, incrates/host/src/http_relay.rsline 161,self.last = Some((packet, reply.clone())). If the inner TLS engine had data pending that arrived after 255ms, that data is left unread in the socket. If the client retries the same request with identicalseq, the host returns the cached reply (with empty payload)! The client can never retrieve the bytes that were delayed unless it sends a new sequence number. If the client was waiting for that data to complete its inner TLS handshake, the connection stalls permanently until the 120s session timeout triggers.
Finding 4: Client Session Starvation via Unauthenticated Session Slots
- Classification: Denial of Service
- File & Line:
crates/host/src/http_relay.rs, lines 20–28, lines 243–248. - Attacker Capability: Unauthenticated network adversary.
- Mechanism:
MAX_SESSIONSis set to 128. An attacker can trivially send 128GET /relay?reqid=<random_hex>&seq=0&ack=-1&payload=&send=falserequests from arbitrary IP addresses. Each request claims a session slot insessionsmap. Subsequent legitimate client sessions receiveHTTP 503 session_limit. BecauseIDLE_TTLis 120 seconds, the attacker needs only 128 lightweight HTTP requests every 2 minutes to completely deny relay service to all clients.
Verification Test Sketch (Unexecuted)
# Test Sketch: Epoch Tail Confidentiality Vulnerability
# Illustrates that a request submitted near epoch expiration has minimal protection delay.
def test_epoch_tail_delay_reduction():
# 1. Enclave activates Epoch N with epoch_seconds = 86400 (24h) and 7-day puzzle.
# 2. Host captures puzzle.json at T_0 and immediately begins solving on 7 cores.
# 3. Time advances by 86300 seconds (23 hours, 58 minutes).
# 4. Client submits sensitive request R at T_0 + 86300s.
# 5. Enclave seals R under Epoch N's key.
# 6. Puzzle for Epoch N was published at T_0, so solver has already computed 23h58m of work!
# 7. Remaining delay on R is 7 days minus ~24 hours, not 7 days.
# If solver has 7x parallelism or faster hardware, puzzle unlocks significantly earlier.
pass
# Test Sketch: Record Forgery after Epoch Unlock
def test_post_release_record_forgery():
# 1. Obtain solved epoch_key and manifest.json.
# 2. Derive record_key = HKDF(epoch_key, manifest.id).
# 3. Construct fake EncryptedRecord with arbitrary plaintext (e.g. fabricated credentials).
# 4. Verify decrypt_record() successfully authenticates the fake record.
pass
Final Security Assessment
| Category | Assessment |
|---|---|
| Enclave Hardware & Attestation | ROBUST under AWS trust model (Strict PCR0 pin, fresh nonce, SPKI binding, SigV4 hardware verification). |
| RandomX FFI & Wrapping Logic | SOUND; segment chaining with ChaCha20Poly1305 prevents parallelization across segments during solving. |
| Delayed Disclosure Mechanics | WEAK; publication-relative epochs cause the effective delay of late requests to degrade by up to epoch_seconds, and host acknowledgment delays grant solving head-starts. |
| Audit Log Integrity | INCOMPLETE; ciphertexts lack enclave signatures, permitting forgery once the timelock expires. |