Independent confidentiality and verification review — 2026-09-09, round 2
Result and scope
No verified authentication bypass, direct plaintext disclosure to the parent/front end, or cross-client plaintext disclosure was found in the reviewed v2 paths. This is a bounded review result, not a finding that the complete system is secure. RandomX native implementation, full delay composition, kernel isolation, dependency memory safety and the production deployment are not certified by this report.
The frozen source is a10323dede4413fbf295916b8ad12e3dbad7514e; checkout HEAD during this review was 85c61e96590b52ded80a995ef5d1e379b2287761. A byte comparison of 249 tracked files under crates, python, config, vendor, Cargo/toolchain inputs, and the Graviton5 Docker/build/library-collection inputs found every checked current file identical to the frozen version. Evidence: .local/astra-confidentiality-round2/source-identity.json. The modified threatmodel.md is review data, not runtime code or evidence of security. No prior model review reports were read.
I traced the deployed binary selection in build/Dockerfile.graviton5:32–35: the final image copies and executes /attested-relay-enclave. crates/enclave/Cargo.toml:11 selects src/v2_main.rs. Its serving path is v2_main → v2_proxy → net/dns/transport; boot also calls attest and hardware, and publication calls v2_epoch. The supported client path reviewed is Python Relay → InnerTLS → GetTransport, with verify_document for live verification and the separate archive APIs for history. The old Rust CLI verifier, ACME, old request proxy, Mullvad provisioning and drand are not reachable v2 request paths. Although wg is compiled, v2 constructs Net::new(..., false, Vec::new()) and never switches it from Mode::Direct (v2_main.rs:156, relay.rs:58). Legacy paths do not supply findings here.
Executed evidence
All tests were local or synthetic. No production connections, deployment changes, credential reads, external messages, file deletion or runtime edits were performed.
Run from the repository root:
PYTHONDONTWRITEBYTECODE=1 /private/tmp/attested-relay-venv/bin/python .local/astra-confidentiality-round2/run_checks.py
The independent driver exercised the real Relay.get/verify decision path and real COSE/X.509 verifier using a synthetic trusted issuer, while replacing only the transport/TLS observation with an attacker-controlled fixture. It verified that the secret-bearing application path never reached the stream in six attacks. A seventh case distinguished historical from live acceptance. Results are in .local/astra-confidentiality-round2/independent-checks.json and checks.log; the test source and generated synthetic certificate fixtures are retained alongside them.
The same invocation ran the existing Python verification, archive, transport and end-to-end tests: 64 passed in 9.04 seconds. End-to-end tests used a local synthetic TLS server and the existing local target/debug/tlproxy-host; this is functional test evidence, not independent provenance for that preexisting executable. No Nitro hardware test was run locally. The source/build equality check is reproducible with .local/astra-confidentiality-round2/source_identity.py.
Attacks investigated and why they were blocked
1. Front end terminates inner TLS and relays fresh evidence from a real enclave
Capability: Parent/front end can terminate the outer connection, present its own inner certificate, and relay the victim's nonce to an honest enclave. Target property: Only the measured enclave may receive the application URL and plaintext.
client.py:143–164 obtains the actual completed TLS peer certificate, makes the nonce request, verifies the returned document and assigns _stream only after success. verify.py:110–114 extracts that peer certificate's SPKI and compares it to the NSM-signed public_key. The production enclave signs its own identity.spki_der, never a requester-supplied key (v2_main.rs:98–111). A copied genuine certificate without its private key cannot complete authenticated TLS possession; a fresh certificate for an attacker's key fails the SPKI comparison.
Executed: The synthetic attacker returned correctly signed, fresh, right-PCR evidence while the observed TLS peer used a different key. Relay.get("https://victim.example/private?token=SYNTHETIC_SECRET") raised AttestationError: attested TLS key differs from actual inner TLS peer; only /v1/attestation?... had been transmitted. This explicitly covers the otherwise suspicious CERT_NONE at client.py:50: trust is deferred, and application data is gated on the later proof.
2. Replay, unsigned policy substitution and historical/live confusion
Capability: Attacker controls every byte of the returned JSON and can replay authentic archived attestations. Target property: Fresh live authentication and measured ready/hardware policy.
verify.py:52–106 bounds the attestation, pins a nonzero exact PCR0, rejects missing/zero PCR0/1/2/4, enforces the five-minute timestamp window in live mode, validates the certificate chain against only the bundled AWS root with a pinned fingerprint, and verifies ES384 over the original protected-header and payload bytes. verify.py:131–139 compares the fresh 32-byte nonce and checks signed policy/SPKI. The unsigned top-level display policy is not used by the live verifier. A top-level mode:dev causes default production clients to reject; accepting dev additionally requires two explicit development options and a loopback endpoint (client.py:128–158).
Executed: Wrong nonce, signed warming state, signed graviton5_verified:false despite an unsigned ready/true display policy, corrupted COSE signature, and unsigned mode:dev each blocked before application GET. A correctly signed ten-minute-old document was accepted by verify_historical_attestation with historical:true and rejected by verify_document for stale time. Existing tests additionally reject wrong PCR, wrong root, rogue certificate chains and trailing attestation data.
archive.py:55–82 deliberately validates archived chain validity at signed historical time and never installs a live stream. Its bundle verifier checks exact artifact fields, content hashes, signer equality with the attested service key, strict Ed25519 signatures, work-parameter agreement, manifest ID and epoch (archive.py:119–163). These establish historical identity/manifest binding; they do not establish present readiness or a public release time. The report does not treat archived evidence as a live proof.
3. Session retry, stream splicing and cross-client attacks
Capability: Front end can replay, drop, duplicate, reorder or swap transport packets and session identifiers, including after learning all outer request IDs. Target property: No application replay/injection or plaintext transfer between independently authenticated clients.
GetTransport.exchange retries the exact same request and advances its sequence only after validating the response reqid, integer sequence, bounded base64 payload and boolean EOF (transport.py:122–179). The host caches the full prior packet/reply, accepts only exact retries, checks acknowledgements, and treats partial-write failures as terminal rather than opening a replacement stream (crates/host/src/http_relay.rs:89–163). Detached host workers preserve the cache after outer HTTP disconnect (http_relay.rs:258–263).
The host rules are reliability controls, not a trust anchor: a malicious host can ignore all of them. Inner TLS authenticates the ordered byte stream, so duplication/reordering or swapping bytes from another TLS session is rejected by the cryptographic channel rather than becoming an authenticated request. A request-ID holder can deny service or consume ciphertext, but the ID is not an application decryption key. Relay.get drops a failed stream (client.py:176–180); the next call must perform fresh verification. No automatic application-level retry onto a replacement connection was found. The existing local end-to-end lost-outer-response test returned the correct body with exactly one /f/ request at the inner server.
I found no shared upstream connection, cookie jar, authorization header store or response cache that lets one client influence another's application stream. fetch opens a fresh upstream connection per hop, constructs only fixed request headers plus the validated host, and keeps response/history state local to the request (v2_proxy.rs:161–197). This observation does not promise that sharing a single Python Relay object concurrently between application threads is supported.
4. Parent/DNS substitution, SSRF and redirects
Capability: Parent supplies arbitrary TCP bytes and may redirect its physical connections; resolver and destination may supply malicious but authenticated DNS/HTTP content. Target property: Upstream plaintext reaches the authenticated intended HTTPS name and redirects respect the implemented destination policy.
Production uses Rustls with bundled WebPKI roots, the original destination name, and certificate verification before constructing/sending the HTTP request (net.rs:57–63, net.rs:110–151, v2_proxy.rs:166–173). The DoH TLS connection verifies the configured resolver IP address (dns.rs:188–193). Forging a parent OK or redirecting a CONNECT to a different machine does not supply a certificate for the required identity. ECH fallback retains ordinary authenticated TLS; it can expose SNI but is not a plaintext/TLS-verification fallback.
validate_target requires HTTPS, no credentials/fragment, port 443, a valid hostname and nonlocal literal destinations (v2_proxy.rs:127–135). Every redirect is joined and then revalidated at the next loop entry. The actual resolved IPv4 address is checked again for public eligibility immediately before connecting (net.rs:127–132, net.rs:240–253). IPv6 is rejected by this policy. This blocks ordinary DNS rebinding to loopback/link-local/private addresses, HTTP downgrades and private-IP redirects. A malicious parent can still physically route a nominally public IP to a private endpoint; the remaining authentication barrier is its certificate for the original hostname. This is not a proof of physical network location.
DNS parsing does not check all DNS response semantics, including matching questions/answer ownership, before collecting A/ECH records (dns.rs:110–142, dns.rs:208–210). With a malicious resolver this can select an unintended IP/ECH configuration, but I did not find an application confidentiality bypass because final target-name TLS verification and public-IP checks remain in force. No DNS parser memory-safety claim is made. Redirects may name a different public HTTPS recipient; that is the implemented redirect contract and an already-authenticated destination can disclose its own request. It is not evidence of access to a different client's plaintext.
5. Hardware evidence substitution
Capability: Parent chooses candidate instance ID, credentials and all transport replies. Target property: Production readiness must correspond to the local required hardware, not another instance or a historical identity document.
hardware::verify first reads local CPU MIDRs, obtains local NSM PCR4/time, checks SHA384(48 zero bytes || candidate instance ID), then requests DescribeInstances through enclave-verified TLS to the fixed EC2 hostname. Exact XML namespace/direct-child uniqueness, instance ID, type, architecture, running state and enclave enablement are checked (hardware.rs:179–208). A second local NSM read requires unchanged PCR4 and elapsed signed time within 90 seconds (hardware.rs:246–269). The local COSE extraction helper is used only for this process's /dev/nsm return, not for host-supplied attestation documents. Under the stated trust in AWS/NSM, absence of a separate signature check on this local ioctl result is not a remote attestation bypass.
The production TLS listener/state is built after this gate (v2_main.rs:159–181). Malicious credential/XML errors fail initialization; they do not set graviton5_verified. I inspected but did not execute the Linux/aarch64 hardware path or make EC2 calls.
6. Sensitive errors, parsers and audit egress
V2 infrastructure logging drops supplied dynamic messages and the diagnostics queue accepts only printable bounded &'static str messages (v2_main.rs:25–26, v2_diagnostics.rs:27–41). Initialization errors and per-request nested upstream/parser errors become static messages (v2_main.rs:119–134, v2_proxy.rs:42–50, v2_proxy.rs:93–96). The richer error strings in shared net.rs::json are not used on the v2 request/hardware paths. I found no path that sends upstream bodies, query strings, credential values or TLS secrets to parent diagnostics.
The v2 server rejects request transfer-encoding and nonzero content-length and uses bounded HTTP headers/connections (v2_proxy.rs:53–58, v2_main.rs:179–196). Host acknowledgement parsing accepts only exact bounded OK lines and preserves any coalesced TLS bytes (transport.rs:18–40, transport.rs:124–134). Upstream response/history state is request-local, capture is bounded, and response bodies are not delivered before encrypted record publication acknowledgement (v2_proxy.rs:104–119). Plaintext JSON/history copies are not all zeroized; under the specified Nitro isolation this is not by itself a path for parent or another client to read enclave memory. It remains relevant if a separate memory-disclosure defect is found.
Additional trust and limits
- WebPKI is an additional dependency. Ordinary WebPKI roots authenticate upstream servers, DoH resolver IPs and the EC2 API inside the enclave. They are not SPKI-pinned. A valid attacker-held certificate for the target name can defeat upstream identity even if Nitro itself is sound. No CA-compromise attack was simulated or claimed. Client outer HTTPS also uses its ordinary trust store, but confidentiality of correctly verified inner application traffic does not rely on the front end being honest.
- The exact independent PCR0 pin remains essential. It pins the measured behavior, not an abstract guarantee that every program with a valid Nitro signature is safe. The live client does not independently prove generation speed, minimum wall-clock delay, or all public-release conditions from policy fields.
- Historical bundle authentication is narrower than record origin. This report does not upgrade post-recovery AEAD verification into a service-origin signature, infer archive completeness/durability from a parent ACK, or infer readiness from historical signatures.
- Known traffic/length/recipient disclosures remain possible. No padding or anonymity proof was found or assumed; those boundaries do not replace the attack-specific analysis above.
- No production call or independent EIF execution occurred. Native RandomX races, cryptographic-library/kernel memory corruption, hardware side channels and a production full-duration rollover need their own evidence. The 64 passing functional tests and seven independent checks do not establish their absence.