reviews/current/objective-20260909T223027Z/minimax__minimax-m2.7.md
On this page

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:

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:

let plaintext = zeroize::Zeroizing::new(canonical_record(&record)?);
let sealed = relay_timelock::encrypt_record(&epoch.manifest, &epoch.key, sequence, &plaintext)?;
tokio::time::timeout(Duration::from_secs(30), v2_epoch::publish(&state, "record.json", &sealed_bytes)).await??

Residual risk not blocked by the code:

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:

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:

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:

  1. Key type is Zeroizing<[u8;32]> — zeroized on drop
  2. Key held in Arc<Epoch> — multiple in-flight requests share the epoch
  3. Epoch reference released by watchdog when monotonic time expires
  4. In-flight requests retain their own Arc clone, 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:

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:

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:

  1. Break RandomX (outside threat model)
  2. 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:

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:

  1. Local CPU MIDR check (Neoverse V3)
  2. NSM PCR4 binding to parent instance ID
  3. 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

  1. 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.

  2. tlock_age implementation: The tlock decryption implementation was not provided. The timelock security depends on this crate's correctness.

  3. 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.

  4. Vendor RandomX source integrity: vendor/randomx/SHA256SUMS provides a checksum, but no reproducible build verification was provided for the RandomX C library itself.

  5. 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:

No, due to:

  1. 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.

  2. No independent record provenance: Records are not individually signed. After key release, a malicious archive could substitute records or claim false interactions.

  3. 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:

The source commit reviewed is a10323dede4413fbf295916b8ad12e3dbad7514e. The enclave binary and PCRs from the CI run (5d47234e2c7b0f0c77051c6fffe6bed363e69658) should be verified before production deployment.