reviews/current/astra-20260909-round2/epoch-and-early-recovery.md
On this page

Independent review: epoch lifecycle and early recovery

Reviewed 2026-09-09. Scope: the deployed-source revision a10323dede4413fbf295916b8ad12e3dbad7514e, particularly private generation, serial wrapping, publication/admission, restart, checkpoints and record encryption. This report was written without reading other review reports. threatmodel.md was read as claims to challenge. No production services, credentials or external communications were used; runtime files were not edited.

Verdict

No demonstrated early epoch-key recovery or direct plaintext-disclosure exploit was found in this scope. Under trusted Nitro isolation, direct NSM randomness/time, ordinary cryptography and the stipulated dependent RandomX cost, the inspected composition does not expose a way for a parent, client or solver to skip the serial chain. This is a bounded review conclusion, not a proof of whole-program confidentiality or a hardware-independent recovery deadline.

The strongest surviving conditional early-recovery counterexample is a later enclave-memory disclosure that reads residual native VM registers: the final register file is sufficient to cheaply recompute a segment output, and those bytes are not erased at VM destruction. Reading the final segment output permits immediate epoch-key recovery. I did not find the memory disclosure primitive needed to make that an attack available to the stipulated actors. The distinction matters: an untrusted parent can read its own memory, not arbitrary protected enclave memory under the accepted Nitro assumption.

A separate, source-confirmed native race remains in concurrent VM creation. Its existence does not demonstrate secret extraction. No novel exploitable confidentiality defect was established here. Metadata inference, intended-recipient disclosure, post-release record forgery and imprecise wall-clock delay remain real boundaries already explicit in the supplied model; they are not counted as new findings.

Source identity and reachability

Working HEAD was 85c61e96590b52ded80a995ef5d1e379b2287761. git diff a10323dede4413fbf295916b8ad12e3dbad7514e -- crates Cargo.toml Cargo.lock vendor config python tests was empty. The Graviton5 Dockerfile and runtime library collector also have no changes from that revision. Ancillary deployment/CI/evidence/docs have changed, so this does not assert the entire current checkout equals the deployed commit.

build/Dockerfile.graviton5:32-35 installs and enters /attested-relay-enclave; crates/enclave/Cargo.toml:11-13 maps that binary to v2_main.rs. That entry point imports v2_epoch, v2_proxy and the shared transport/attestation code, and starts exactly one generation loop plus an expiration watchdog (v2_main.rs:178-179). Legacy key-release/record modules are not imported into this v2 binary. The native generator/solver CLI is built as an artifact but is not the runtime entry point. The v2 HTTP dispatcher exposes attestation, status and proxy GET paths, not generator/solver/checkpoint APIs (v2_proxy.rs:53-84).

Attacks investigated and why they stop

Serial composition and chosen inputs — blocked in the inspected path

crates/timelock/src/lib.rs:312-325 obtains a fresh dataset key, seven independently sampled seeds, an independent epoch key and seven nonces through the caller's fallible RNG. Production supplies Attester::fill_random directly (v2_epoch.rs:118-125); its NSM implementation clears destinations on failure and has no OS fallback (attest.rs:41-60,172-194). TLS/OS entropy is seeded before provider initialization (v2_main.rs:149-155), while the service signing seed uses direct NSM randomness (169-172).

Generation computes each output y_i using the dependent hash input containing the domain, epoch, segment, iteration and preceding value (lib.rs:238-245,327-351). The published manifest contains s_1 but no other seed/output (352-364). Wrap i encrypts s_(i+1), or the epoch key for the last segment, under HKDF of y_i (365-386). The context binds epoch, segment index, iteration count, dataset key and epoch-key commitment (246-260). Therefore knowing a public initial seed, dataset key, one record plaintext, or another epoch's solved key does not furnish the next private seed or the final wrap key. The last wrap cannot be independently attacked using the public commitment without breaking the assumed KDF/AEAD/hash security.

The manifest signature covers canonical fixed-width fields including all wraps, initial seed and work parameters (175-235,388-393). Swapping wraps, reducing iterations, substituting another dataset or relabeling segments changes the signed bytes and/or AEAD context. Clients do not supply generation parameters or seeds to this runtime. A chosen attestation nonce requests an NSM signature over a structured attestation, not a service-key signature over attacker-chosen puzzle bytes (v2_main.rs:94-113). No cross-protocol signing oracle was found.

Parent starts solving, then withholds ACK — blocked

Concrete attempted trace: parent reads a puzzle on the records vsock, starts solving it privately, withholds its persistence ACK until recovery is nearly finished, then tries to make the enclave serve a fresh 24-hour epoch under that already solved key.

v2_epoch.rs:82-88 anchors the monotonic and uncached trusted-time lifetime before the first possible puzzle write. It obtains puzzle and bundle ACKs before activation, then samples trusted time again and rejects an expired candidate (88-99). A 60-second outer timeout bounds asynchronous publication (144-146). Retrying the same puzzle does not reset the anchor. Forging OK\n can defeat durability but cannot change the locally computed lifetime or choose the generated key. Withholding the earlier attestation ACK merely postpones the as-yet undisclosed puzzle.

The publication function itself has no per-call timeout, but both v2 callers wrap it: activation at 60 seconds and record publication at 30 seconds (v2_epoch.rs:144; v2_proxy.rs:107). Timeouts require executor progress; synchronous NSM/native operations and enclave suspension are not hard real-time bounded. This limits an exact erasure-deadline claim, but does not give the parent key bytes or activate an expired candidate on resumption.

Overlapping generation, rollover and restart — blocked

After one epoch is activated, a single spawn_blocking task computes its successor. The completed successor remains internal until uncached trusted NSM time reaches publication_not_before (v2_epoch.rs:110-140). That deadline is stored separately from the active Arc, so watchdog removal does not accidentally authorize early successor publication. Slow/failed generation leaves expired epochs closed, rather than extending their serving life (126-129,141-146).

An enclave restart initializes active to None, obtains a fresh direct-NSM signing seed and begins fresh generation (v2_main.rs:169-178). There is no v2 restore API that trusts host-supplied old epoch secrets or puzzles. Even a repeated epoch number after restart does not reuse seeds, dataset key, signer or record-key context. Launching many genuine instances samples unrelated puzzles rather than sharing the victim's private future seeds. No rollback of protected state was assumed available under the accepted AWS/Nitro trust model.

Admission, disconnect and sequence manipulation — blocked for key reuse

v2_proxy.rs:78-92 acquires capacity, obtains uncached trusted time, clones only an epoch that accepts that time and current monotonic time, reserves a checked sequence, then starts upstream work. Epoch::accepts_at rejects times before activation, at/after expiry, and monotonic expiry (v2_epoch.rs:22-34). The monotonic test is performed at admission, so a stale earlier NSM sample cannot by itself keep an expired epoch open. Sequence exhaustion fails before outbound traffic and does not wrap (v2_proxy.rs:85-87,122-125).

Disconnecting the outer client does not release the admitted worker early: handle spawns dispatch, while the permit and epoch lease remain in the worker through fetch, serialization/encryption and publication (42-46,78-119). Fetch and asynchronous record publication each have 30-second deadlines in the pinned config. The watchdog drops the global epoch reference independently every 100 ms (v2_epoch.rs:37-51); existing admitted workers retain their old key only for completion. CPU serialization/encryption between those timeouts is bounded by input sizes but does not have a separate hard-time deadline. Thus strict wall-clock erasure at expiry plus exactly 60 seconds is not established; this is not evidence that expired epochs accept new traffic or that the key escapes.

Malicious solver checkpoints — blocked unless the attacker already has useful secret progress

The checksum is public, so an attacker can fabricate a checkpoint claiming segment 6 is finished (lib.rs:400-455). That claim alone is not evidence of work. The solver still unwraps that segment using the supplied value and checks the final epoch-key commitment (458-497). Arbitrary progress can waste/redirect computation or deny recovery, but cannot produce a victim key without the needed segment output. Enclave generation does not call solve or emit checkpoint journals; checkpoint output belongs to external solver execution (crates/timelock/src/main.rs:202-240). Sharing a legitimate solver checkpoint shares work already completed after disclosure, which is inherent to this construction.

Record chosen-plaintext interactions — blocked under ordinary AEAD/KDF assumptions

Record keys are derived separately using relay-record-key-v1, the epoch secret and full signed manifest ID (lib.rs:81-90). XChaCha20-Poly1305 receives a fresh 192-bit OS nonce and AAD containing version, epoch, sequence and manifest ID (73-79,98-123). A client can cause chosen records in the same epoch, but cannot choose the nonce, raw epoch key or wrap context. Sequence reuse is rejected and is not needed to provide nonce uniqueness. Altering the ciphertext envelope or supplying a wrong recovered key fails context/AEAD or commitment validation (125-155). This reasoning does not claim length hiding or permanent authenticity after the epoch key is public.

Residual-secret and native correctness findings

Confirmed erasure gap; exploitation conditional. Rust-owned long-lived seeds/output/key buffers use Zeroizing, but native and transient copies do not all do so. crates/timelock/src/randomx.rs:99-110 returns a plain 32-byte output; vendor/randomx/src/randomx.cpp:392-403 has an unwiped stack tempHash. More significantly, vendor/randomx/src/virtual_machine.cpp:119-123 computes the final segment hash as BLAKE2b of the VM register file after updating reg.a. randomx_destroy_vm only deletes the VM (randomx.cpp:375-378); its base destructor is empty (virtual_machine.cpp:39-41) and the scratchpad is freed without a wipe (101-103, allocator.cpp:45-48). Consequently, a retained copy of the final register file yields y_i with cheap BLAKE2b; retaining final-segment state is as dangerous as retaining its output. Erasing the Rust ys vector does not establish erasure of all material sufficient for early recovery.

Attacker trace requires an additional enclave memory-read/use-after-free exploit or successful applicable side channel: obtain final-segment residual registers after generation; derive y_7; derive the manifest-bound wrap key; decrypt the seventh wrap to get the epoch key; decrypt victim records immediately. No such reachable read/side-channel exploit was demonstrated. This finding therefore cannot be promoted to an in-scope confidentiality break on its own. Hardening should erase native VM/register/scratch/temp material and inspect compiler/library copies; that reduces consequences of later disclosure but is not a substitute for preventing it.

Confirmed source race; exploit not demonstrated. Seven generation workers call dataset.vm() concurrently (lib.rs:327-341); hard-AES VM allocation accesses shared non-atomic aesDummy (virtual_machine.cpp:98,110-113). volatile is not synchronization. The static value is an AES feature probe, not a seed/output buffer. I did not run ThreadSanitizer here or establish a way to turn the race into key extraction. This remains a native correctness defect deserving a fix even though the composition analysis does not supply an exploit.

Local verification and limits

All artifacts were retained under .local/astra-epoch-round2; runtime source was unchanged. The source-built relay-timelock library tests passed: 3 passed, 0 failed, 1 ignored. These cover the native light-mode known vector, fallible entropy, real generation/solve, restart and tampering. The ignored test allocates a full 2080 MiB dataset; I did not use it as evidence. The source-built v2 epoch tests also passed (3 passed, 0 failed): backward-clock/expiry admission, watchdog release without generation/NSM, and an old epoch surviving only while an admitted lease exists.

A separate source-linked probe in .local/astra-epoch-round2/src/main.rs generated a real short-work puzzle and tested four final-checkpoint substitutions: all-zero bytes, public initial seed, public key commitment, and even the actual epoch key used in place of the needed final intermediate. Their recomputed public checksums validated, but all failed recovery. A genuine final checkpoint recovered the epoch key without additional RandomX work, confirming the security importance of retaining that intermediate. The probe also rejected record changes to sequence, epoch, nonce and puzzle ID.

Reproduction commands:

cargo test --locked --offline -p relay-timelock --lib --target-dir .local/astra-epoch-round2/target
cargo run --offline --manifest-path .local/astra-epoch-round2/Cargo.toml --target-dir .local/astra-epoch-round2/target
cargo test --locked --offline -p tlproxy-enclave --bin attested-relay-enclave v2_epoch::tests --target-dir .local/astra-epoch-round2/target

The tests run locally on the development host, not the production Nitro image. The review does not establish production-duration generation/rollover/recovery, full-mode hardware equivalence beyond the source, general native memory safety, side-channel resistance, or a week-long wall-clock lower bound. Its positive conclusion is specifically that the examined v2 secret-generation, serial-wrap and lifecycle paths yielded no demonstrated early recovery attack under the stated trust assumptions.