reviews/current/astra-security-review.md
On this page

Independent Astra security review — 2026-09-09

Reviewed the working tree based on 04d67264ce55d140d42518390fe5b5a855eddf5a, including the uncommitted NSM entropy, canonical CBOR and high-level Puzzle API changes. The running image remains the separately measured 474083e build; this review does not establish that the replacement source is deployed.

Scope: v2 enclave initialization, NSM randomness/time, epoch activation/erasure, RandomX generation/wrapping, GET relay, outbound TLS and hardware verification, live Python TLS/COSE validation, archive binding, and the new Puzzle wrapper. Read REQUIREMENTS-AUDIT.md, NSM-RNG-AUDIT.md and the final shared design/transport corrections. AWS/Nitro/PKI are trusted; parent and Cloudflare are adversarial. No credentials or private deployment state were read. No network/cloud actions or source changes were made. Tests used local synthetic certificate fixtures.

Outcome

No new high/critical confidentiality or authentication bypass was established. Two concrete availability defects violated the intended memory bounds. Both were sent to the parent agent and corrected during this review. I independently inspected the final source fixes: M1 now bounds tunnel/config lines before allocation grows, and M2 limits DoH bytes before collection. The corrections are local source changes; they have not been measured/deployed. Findings below retain the original locations and triggers for review history.

Findings

M1: parent tunnel acknowledgement allocates without a byte limit — fixed in source

Location: crates/enclave/src/transport.rs:91–104, especially read_line at 98.

Every outbound TCP connection first reads the untrusted parent's textual tunnel acknowledgement. A parent responding with a long ASCII stream without a newline causes read_line(&mut String) to keep growing the buffer. The 30-second timeout limits elapsed time, not allocation. This path runs before outbound TLS, and is reachable during hardware initialization as well as normal DNS/upstream traffic. The request concurrency cap does not bound bytes allocated by one such stream.

Impact: allocation exhaustion can terminate the enclave or cancel in-flight capture work. It does not reveal a key or authenticate a fake upstream. The parent can already terminate its enclave, so this is a bounded-resource defect, not an additional availability guarantee against the account owner.

Recommended correction: a bounded line reader, for example a 64-byte maximum for the tunnel acknowledgement, that requires a terminated accepted OK line and retains bytes buffered after the newline for TLS. Reject an unterminated line at the bound. Test a long no-newline stream, EOF without newline, and an OK line coalesced with trailing TLS bytes using a bounded in-memory duplex stream.

The same primitive at transport.rs:134 reads the legacy host-config line without a pre-allocation cap; its 64-KiB check runs only after read_line returns. That legacy path is not used by the reviewed v2 bootstrap, but a shared bounded helper should also enforce its 64-KiB limit before allocation grows further.

M2: authenticated DoH response body is unbounded — fixed in source

Location: crates/enclave/src/dns.rs:194, resp.into_body().collect().

The enclave's first configured DoH provider is Cloudflare at 1.1.1.1. TLS proves the provider's identity, not that its response is safely sized. Under the stated malicious-Cloudflare model, that provider can return HTTP 200 with an arbitrarily large chunked body. The implementation collects it completely before asking the DNS parser to validate it. The query's timeout does not impose a byte cap. A body exceeding 65,535 bytes is already a bounded trigger for this gap; no memory-exhaustion test is needed to demonstrate the missing check.

Impact: a malicious/compromised resolver can exhaust enclave memory through DNS, bypassing the relay's 10-MiB response limit. Ordinary relay clients cannot forge the resolver's TLS certificate; controlling a queried domain is not by itself shown to permit this attack. The scope expressly includes malicious Cloudflare.

Recommended correction: incrementally bound the decoded HTTP body to the DNS wire-message maximum of 65,535 bytes before collection/parsing. A Content-Length check alone is insufficient for chunked responses. Test the exact boundary, one byte above it, and a chunked response that crosses it. Retain the total query timeout as well.

Verified boundaries and accepted limitations

Validation

Executed:

PYTHONPATH=python/attested-relay/src /tmp/attested-relay-venv/bin/python -m pytest -q -p no:cacheprovider python/attested-relay/tests/test_verify.py python/attested-relay/tests/test_archive.py -k 'not fetch_historical_bundle_and_discover_via_real_host' --basetemp=/tmp/astra-security-pytest-20260909
46 passed, 1 deselected in 0.38s

This exercises synthetic-chain verification, nonce/PCR/TLS-key rejection, invalid signed policy, historical certificate validity at signed time, unsigned policy substitution, puzzle/bundle/content-address binding, native manifest encoding, and small-order/noncanonical Ed25519 rejection. The deselected test requires the local relay host integration fixture. No replacement-image Nitro run or production-duration puzzle generation was performed in this review. The memory-bound findings are source-established; no unbounded allocation or live denial-of-service experiment was run.

Correction review

Inspected the final transport diff: bounded_parent_line limits allocation before extending the output, rejects a full unterminated line immediately, retains buffered bytes after a newline, and parent_ok accepts only OK LF/CRLF. The legacy config reader uses the same helper with a 64-KiB cap. Its tests cover fragmented oversized streams that stay open, exact acknowledgement syntax, coalesced TLS preservation and the exact config limit.

Inspected the final DNS diff: Limited::new(body, 65535) wraps the body before collect. Tests cover multiple frames with no eventual EOF, the exact bound, and one oversized frame. Both corrections address the findings without changing the TLS authentication trust boundary. Implementing agents reported the following successful targeted executions:

cargo test --locked -p tlproxy-enclave transport::tests -- --test-threads=1
4/4 v2 tests passed; 4/4 legacy tests passed.

cargo test -p tlproxy-enclave --bin attested-relay-enclave dns::tests --locked
2/2 DNS tests passed. Log: /tmp/relay-dns-bound-tests.log

The parent also reported 76 passing Python tests in 15.29 seconds after the final API corrections. My independent execution remains the 46-test verifier/ archive run above. At report freeze, the parent was running broader workspace Rust checks and rebuilt-binary end-to-end tests; those are not represented as completed evidence here.