Independent Pi/OpenRouter review: minimax/minimax-m2.7
Source: a10323dede4413fbf295916b8ad12e3dbad7514e. Status: completed.
Security Review: timelock-proxy v2 (commit a10323dede4413fbf295916b8ad12e3dbad7514e)
Summary Assessment
| Property | Verdict | Primary Basis |
|---|---|---|
| Record encryption (enclave confidentiality) | SUPPORTED CONDITIONALLY | XChaCha20Poly1305, HKDF per-record key derivation, epoch key from RandomX chain; depends on NSM entropy and RandomX chain security |
| RandomX chain integrity | SUPPORTED CONDITIONALLY | ChaCha20Poly1305 wrapping of chained seeds; final commitment check; depends on RandomX not having shortcuts |
| Generation timing vs epoch duration | VIOLATED | Measured 25.14h generation exceeds 24h epoch window; code has no overlap/pipeline; confirmed fail-closed gap |
| Epoch key freshness | SUPPORTED CONDITIONALLY | Sequential activation, monotonic expiration, no reuse; depends on NSM time and generation completing before activation |
| NSM entropy reliability | SUPPORTED | Direct NSM ioctl, no OS fallback, bounded retries with zeroize on failure; tested failure modes |
| Parent cannot extract epoch key early | SUPPORTED CONDITIONALLY | Key never transmitted; parent only receives ciphertext; depends on enclave isolation and RandomX |
| Parallel generation vs serial solving | VIOLATED | Generation: 7 threads in std::thread::scope, ~25h wall clock. Solving: strictly sequential segments. A faster attacker machine cannot accelerate solving relative to generation (same work), but the epoch window may expire before generation completes. |
CRITICAL: Generation Exceeds Epoch Window
Finding: Service fail-closed gap from timing miscalibration
File/line: crates/enclave/src/v2_epoch.rs:104–151, config/relay-v2.toml:6
Attacker capability: None required (configuration/infrastructure issue)
Execution trace:
v2_epoch::run():
1. epoch = random()
2. generate() // blocks ~25.14 hours (7-worker parallel)
3. publication_not_before = published candidate's expires_at_ms
4. loop:
active = None // old key released
activate(generated) // timeout 60s
// if generation took >24h, we're now in a gap
Impact: After the first epoch expires, no puzzle is available. All requests return 503 until generation finishes. The gap continues indefinitely if generation always exceeds the epoch.
Evidence:
config/relay-v2.toml:4: "Seven-worker generation measured ~25.14h; a 24h epoch expires closed if late."crates/enclave/src/v2_epoch.rs:112–115: No pre-generation; generation is blocking and runs after old epoch activationcrates/enclave/src/v2_epoch.rs:144:activatehas a 60-second timeout, but generation itself is unbounded- No pipelining: the next puzzle is generated only after the current one activates
Code-grounded reason blocking is not expected:
The comment in config/relay-v2.toml calls this "a calibration estimate, not a hardware speed bound." However, the code has no mechanism to recover from this: there is no pipeline, no overlap, and no catch-up mode.
Missing evidence: No observed complete generation run. The calibration estimate is from measurements/graviton5-calibration-20260909/ (not provided for review).
Test sketch (not executed):
# Simulate generation taking 25.14h with 24h epoch
# 1. Start with epoch 0 active
# 2. After 24h: epoch 0 expires
# 3. Generation still running (1.14h remaining)
# 4. All requests fail 503
# 5. After 25.14h: epoch 1 activates, requests succeed
# 6. Repeat: epoch 1 expires after 24h, epoch 2 generation still running
Recommendation: Increase epoch_seconds to at least max_generation_time + 1h (e.g., 27h) or implement puzzle pre-generation/pipelining.
A. Request/Response Content Confidentiality
A1. Can the parent/host recover plaintext before the timelock?
SUPPORTED CONDITIONALLY
Code-grounded reasons blocking this:
crates/enclave/src/v2_proxy.rs:104–105: Encryption happens in the enclave before sending:
let plaintext = zeroize::Zeroizing::new(canonical_record(&record)?);
let sealed = relay_timelock::encrypt_record(&epoch.manifest, &epoch.key, sequence, &plaintext)?;
crates/enclave/src/v2_proxy.rs:107: Waits for persistence ACK before returning:
tokio::time::timeout(Duration::from_secs(30), v2_epoch::publish(&state, "record.json", &sealed_bytes)).await??
crates/timelock/src/lib.rs:92–124: Per-record key derivation uses HKDF-SHA256 with unique nonce per record; no epoch key reuse
Residual risk not blocked by the code:
- Memory exposure during encryption (compiler/allocator/copy behavior not verified)
- Side channels in XChaCha20Poly1305 or HKDF implementation
- NSM entropy compromise (outside threat model)
A2. Can the parent publish an already-solved puzzle?
SUPPORTED — blocked by code
File/line: crates/enclave/src/v2_epoch.rs:76–88
Protection mechanism:
// Anchor the epoch before its first possible puzzle disclosure. A parent
// withholding publication ACKs must not turn an already solvable puzzle
// into a fresh full-lifetime epoch when it finally releases those ACKs.
let activated_monotonic = Instant::now();
let activated_at_ms = state.attester.trusted_time_ms_uncached()?;
let expires_at_ms = activated_at_ms + lifetime_ms;
The puzzle is published, then activated at activated_at_ms. Even if the parent withholds the ACK, the puzzle is already disclosed. The parent's delay cannot extend the epoch's lifetime.
A3. Can a faster attacker recover records earlier than intended?
VIOLATED — timing uncertainty
The threat model explicitly states: "approximate delay starts at puzzle publication, not at each request" and "faster hardware, improvements or shared progress can shorten recovery time."
This is acknowledged disclosure, not a vulnerability. However:
Concrete risk: If an attacker uses a faster machine than the generation hardware (Graviton5), they could recover records from the beginning of an epoch significantly earlier than the calibration estimate.
Code evidence: crates/timelock/src/lib.rs:476–492 — solving is strictly sequential across all 7 segments with no checkpoint sharing or parallelization possible:
for segment in cp.segment as usize..SEGMENTS {
for iteration in start..puzzle.iterations {
*x = vm.hash(&input(puzzle.epoch, segment, iteration, &x));
}
x = unwrap(puzzle, segment, &x)?; // chain enforces sequential
}
B. Epoch Key / Seed / Intermediates Early Recovery
B1. Parallel generation vs. serial solving asymmetry
VIOLATED — architectural consequence, not a cryptographic break
File/line: crates/timelock/src/lib.rs:327–351
The asymmetry:
// Generation: all 7 segments run in parallel threads
let workers: Vec<_> = seeds.iter().enumerate().map(|(segment, seed)| {
scope.spawn(move || {
let mut vm = dataset.vm()?;
let mut x = seed.clone();
for iteration in 0..iterations {
*x = vm.hash(&input(epoch, segment, iteration, &x));
}
Ok(x) // each segment independent
})
}).collect();
let ys = workers.into_iter().map(|w| w.join()).collect::<Result<Vec<_>>>()?;
// ys[6] (last output) wraps epoch_key
Generation: 7 threads, ~25.14h wall clock (segment with slowest individual worker). Solving: 7 segments must be solved sequentially; each segment takes ~25h / 7 ≈ 3.6h.
Attack implication: An attacker with the same 7-worker machine as the generator cannot accelerate solving. However, if the attacker has significantly more cores or a faster architecture, they could solve faster than the generation hardware. The RandomX primitive does not prevent this.
Missing evidence: No analysis of whether a sophisticated attacker could use partial information about the chain structure (e.g., timing side channels during generation vs. solving) to accelerate solving.
B2. Can the parent make the enclave use an already-solved puzzle?
SUPPORTED — blocked
File/line: crates/enclave/src/v2_epoch.rs:130–140
The generation task produces one puzzle at a time. The parent cannot:
- Cause the enclave to regenerate a puzzle with known solution
- Submit an externally-generated puzzle
- Cause key reuse between epochs
Code confirmation:
let generation = tokio::task::spawn_blocking(move || {
relay_timelock::generate_with_rng(epoch, iterations, &signer, mode,
|bytes| entropy_state.attester.fill_random(bytes))
}).await;
// epoch is derived from enclave memory (rand::random) and NSM entropy
// no parent-controllable seed
B3. Checkpoint safety
SUPPORTED — not exploitable
File/line: crates/timelock/src/lib.rs:458–498
Checkpoints are a solver-side optimization only. They:
- Are generated by the solver during solving, never transmitted to enclave
- Resume from a given segment/iteration with validated state
- Include SHA256 checksum validated before use
- Cannot bypass the RandomX chain
The enclave never receives or acts on external checkpoints.
B4. Epoch key lifecycle and memory safety
SUPPORTED CONDITIONALLY
File/line: crates/enclave/src/v2_epoch.rs:94
let active = Arc::new(Epoch {
manifest: generated.manifest,
key: generated.epoch_key, // Zeroizing<[u8; 32]>
...
});
Protections:
- Key type is
Zeroizing<[u8;32]>— zeroized on drop - Key held in
Arc<Epoch>— multiple in-flight requests share the epoch - Epoch reference released by watchdog when monotonic time expires
- In-flight requests retain their own
Arcclone, keeping the key alive until sealing completes
Code evidence for leak-free expiration: crates/enclave/src/v2_epoch.rs:196–210 (test):
async fn expired_key_survives_only_until_existing_request_lease_is_released() {
let request_lease = epoch.clone();
let weak = Arc::downgrade(&epoch);
expire_active(&active, activated + Duration::from_millis(100)).await;
assert_eq!(Arc::strong_count(&request_lease), 1); // only request holds it
drop(request_lease);
assert!(weak.upgrade().is_none()); // key truly dropped
}
Residual risk: Zeroization in Zeroizing depends on correct drop implementation. This is standard practice but not formally verified against compiler optimizations.
B5. Signing key never leaves enclave
SUPPORTED
File/line: crates/enclave/src/v2_main.rs:169–172
let mut signing_seed = zeroize::Zeroizing::new([0u8; 32]);
attester.fill_random(signing_seed.as_mut())?;
let signer = relay_timelock::SigningKey::from_bytes(&signing_seed);
drop(signing_seed);
The signing key is derived from NSM entropy at boot and never transmitted. Manifests are signed in-enclave during generation. The parent never sees the private signing key.
B6. Seed and intermediate exposure
SUPPORTED — not in scope for early recovery
File/line: crates/timelock/src/lib.rs:365–386
Only seed_1 is published (as the first seed, in plaintext). All subsequent seeds and the epoch key are wrapped with ChaCha20Poly1305 using the previous segment's output as the key. The chain is:
seed[0] (published)
↓ wraps seed[1]
output[0] → key for wrapping
↓ wraps seed[2]
output[1] → key for wrapping
...
↓ wraps epoch_key
output[6]
Missing evidence: No formal analysis of the wrapping construction's security properties. The comment in crates/timelock/src/lib.rs:1–2 states: "calibrated sequential-work assumption, not a proven VDF." This is honest but means the construction's security depends on empirical timing rather than cryptographic proof.
C. Side Channels and Metadata
C1. Request timing and size leakage
VIOLATED — acknowledged disclosure
The parent observes:
- Connection timing and duration
- Request/response sizes (encrypted but length-visible)
- Number of concurrent connections
This is documented in threatmodel.md:6.1: "Parent/front-end observers can see IP addresses, timing, sizes and connection behavior."
Not a vulnerability under the stated threat model, but worth noting for users who expect stronger anonymity.
C2. DNS resolution visibility
ACKNOWLEDGED DISCLOSURE
The enclave resolves DNS internally via DoH over TLS (with ECH when available). The parent sees only the DoH resolver's IP, not the target domain, when ECH is in use.
Code evidence: crates/enclave/src/dns.rs:182–211 — enclave opens TLS connection to resolver IP, performs DoH query.
Limitation: ECH is opportunistic. If the upstream doesn't advertise ECH configs, SNI is visible to the parent/Cloudflare.
C3. Content-length header forwarding
VIOLATED — metadata leakage
File/line: crates/enclave/src/v2_proxy.rs:110–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);
}
}
The content-length header is forwarded. Combined with chunked transfer, an observer can estimate response body size even without decryption.
Impact: Minor metadata leakage. Not a confidentiality breach of content, but could aid traffic analysis.
D. Concrete Cryptographic Properties
D1. Record encryption (XChaCha20Poly1305)
SUPPORTED CONDITIONALLY
File/line: crates/timelock/src/lib.rs:92–124
Each record is encrypted with:
- Per-record key:
HKDF-SHA256(epoch_key, "relay-record-key-v1", manifest.id() || sequence) - Fresh nonce:
OsRng(or NSM in production) - AAD: epoch||sequence||manifest_id (binds record to specific puzzle/position)
Code evidence:
let key = record_key(manifest, epoch_key)?; // HKDF-derived
let mut nonce = [0; 24];
OsRng.fill_bytes(&mut nonce); // fresh per record
let ciphertext = XChaCha20Poly1305::new_from_slice(key.as_ref())
.encrypt(XNonce::from_slice(&nonce), Payload { msg: plaintext, aad: &context })
Strongest code-grounded reason blocking early recovery: The epoch key is produced by the RandomX chain (output of segment 6). To recover the epoch key, the attacker must either:
- Break RandomX (outside threat model)
- Obtain the wrapped key material from the manifest (protected by ChaCha20Poly1305 wrapping, requiring all 7 prior segment outputs)
Missing evidence: No formal proof that the wrapping construction is as strong as the underlying primitives. The construction chains ChaCha20Poly1305 outputs as keys, which is non-standard.
D2. Key commitment integrity
SUPPORTED — integrity check, not confidentiality
File/line: crates/timelock/src/lib.rs:363, crates/timelock/src/lib.rs:493–496
// Generation
key_commitment: hex::encode(Sha256::digest(*epoch_key)),
// Solving (final check)
ensure!(hex::encode(Sha256::digest(*x)) == puzzle.key_commitment, "epoch key commitment mismatch");
The commitment check verifies that the solver's computed final output matches the committed hash. This prevents:
- Submitting a wrong key
- Tampering with intermediate chain results
It does not strengthen the timelock (the chain itself enforces sequential work).
D3. tlock layer (timelock)
SUPPORTED CONDITIONALLY
File/line: crates/common/src/lib.rs:339–366
Records are sealed with: age(operator_key, tlock(drand_round, plaintext)). The tlock encryption uses the drand BLS signature as the decryption key, which is only available after the drand round is published (approximately every 30 seconds).
Missing evidence: The tlock_age crate implementation was not provided for review. The security of the timelock depends on the drand network's unpredictability, which is assumed but not verified in this review.
E. Specific Attack Scenarios
E1. Malicious parent: withhold record ACK to observe timing
NO IMPACT on confidentiality
File/line: crates/enclave/src/v2_proxy.rs:107
The parent ACK is awaited for availability, but the record is encrypted before being sent. The parent cannot recover plaintext even if it withholds the ACK indefinitely.
Impact: Only availability (request timeout) is affected.
E2. Malicious parent: provide wrong/missing credential data during hardware verification
BLOCKED
File/line: crates/enclave/src/hardware.rs:239–267
Hardware verification combines:
- Local CPU MIDR check (Neoverse V3)
- NSM PCR4 binding to parent instance ID
- EC2 API response authenticated by enclave TLS
A malicious parent cannot pass step 2 or 3 without controlling the actual AWS resources bound to the enclave's PCR4.
E3. Malicious parent: replay old attestation or puzzle
BLOCKED
The client's verify() call uses a fresh nonce and verifies the attestation timestamp freshness (±5 minutes). Old attestations are rejected by the timestamp check.
File/line: python/attested-relay/src/attested_relay/verify.py:87–88:
if abs(now.timestamp()*1000 - timestamp) > 300000:
raise AttestationError("attestation timestamp outside five-minute freshness window")
E4. Archive host: withhold puzzles or provide modified records
NO CONFIDENTIALITY IMPACT
Archive integrity is verified by the ed25519 signature over the manifest. Modified records fail AEAD authentication.
Post-release provenance limitation (acknowledged): Records are not individually signed. After the epoch key is published, anyone can create a valid AEAD record. Proving original relay interaction requires an independent receipt.
F. Most Important Missing Evidence
-
Generation timing: No observed complete production generation run. The 25.14h estimate is a calibration measurement, not a production observation. If generation consistently exceeds the 24h epoch, the service has chronic fail-closed gaps.
-
tlock_age implementation: The tlock decryption implementation was not provided. The timelock security depends on this crate's correctness.
-
RandomX v2 security: The threat model assumes "no cryptanalytic shortcut to evaluating a specified dependent chain." This is not verified in this review. RandomX was designed for proof-of-work, not cryptographic timelock constructions.
-
Vendor RandomX source integrity:
vendor/randomx/SHA256SUMSprovides a checksum, but no reproducible build verification was provided for the RandomX C library itself. -
Formal wrapping construction analysis: The ChaCha20Poly1305 wrapping chain is non-standard. While plausible, it lacks the scrutiny of established timelock constructions.
Would I Rely on This for Sensitive Content?
Under the explicit assumptions, with the documented gaps:
Yes, with reservations, specifically for confidentiality of content in transit, given:
- Encryption is performed in-enclave before any transmission
- Keys are NSM-anchored and epoch-key isolation is enforced
- The RandomX chain construction appears sound (though not formally proven)
- No cryptographic primitives have known practical breaks
No, due to:
-
Generation timing gap (VIOLATED): If the 25.14h estimate is accurate, the service fails closed indefinitely after the first epoch. This is a denial of service, not a confidentiality breach, but it means the service is not continuously available.
-
No independent record provenance: Records are not individually signed. After key release, a malicious archive could substitute records or claim false interactions.
-
Post-release provenance gap (acknowledged in threat model): Fresh receipts are not implemented. Proving an original relay interaction requires trust in the archive or client-held receipts.
Recommendation for the generation timing issue: Either:
- Increase
epoch_secondsto 27h+ (accounting for generation variance) - Implement puzzle pre-generation during the active epoch
- Accept that epochs will have gaps and clients must retry
The source commit reviewed is a10323dede4413fbf295916b8ad12e3dbad7514e. The enclave binary and PCRs from the CI run (5d47234e2c7b0f0c77051c6fffe6bed363e69658) should be verified before production deployment.