reviews/current/objective-20260909T223027Z/z-ai__glm-5.3.md
On this page

Independent Pi/OpenRouter review: z-ai/glm-5.3

Source: a10323dede4413fbf295916b8ad12e3dbad7514e. Status: completed.

Independent security review — attested-relay v2 (SOURCE_COMMIT a10323d)

Method note: Static review of the frozen snapshot only. I executed no tests, builds, or measurements; every test referenced below is marked not executed. The deployed binary is attested-relay-enclave (crates/enclave/src/v2_main.rs per the Dockerfile entrypoint); I treated the legacy tlproxy-enclave/main.rs path and the tlock/age/drand sealing in crates/common as unreachable in the deployed EIF (only /attested-relay-enclave is copied into the enclave image stage).

Verdict summary

Property Verdict
A. Content confidentiality vs intermediaries/operator before the intended delay SUPPORTED CONDITIONALLY (cryptography + Nitro isolation + RandomX-primitive assumption; metadata excluded, see F1/F7)
B. Early epoch-key/plaintext recovery or use of a pre-solved puzzle without breaking RandomX SUPPORTED CONDITIONALLY — no code path found; delay magnitude is calibration-relative, not a proven bound
Availability / retention under malicious parent VIOLATED in the documented sense (parent can permanently close the service and lie about durability) — not a confidentiality claim, but in scope as a defect of the deployment
Post-release provenance of individual records INSUFFICIENT EVIDENCE / known gap (documented; confirmed by code)
Session integrity vs malicious front end VIOLATED (availability/integrity only, not content confidentiality) — see F4

Question A — early content recovery

SUPPORTED CONDITIONALLY. The path from request to disclosure is:

  1. Inner TLS (client→enclave, v2_proxy.rs:42): outer actors see only ciphertext chunks; the enclave is the TLS endpoint. The client verifies a fresh, nonce-bound, PCR0-pinned Nitro document binding the actual peer SPKI (verify.py:131–142, 52–107) before any application data — replaying someone else's attestation does not authenticate an attacker's endpoint (_bound_policy, verify.py:110–128).
  2. Upstream fetch inside the enclave, TLS-verified there (v2_proxy.rs:139–159); the parent carries only bytes to IPs (transport.rs:135–141).
  3. Audit record: canonical CBOR, sealed with XChaCha20Poly1305 under HKDF(epoch_key, manifest.id) with AAD binding epoch, sequence, and full signed-manifest digest (timelock/src/lib.rs:73–124), then persisted as ciphertext only (v2_proxy.rs:104–107).
  4. epoch_key exists only in enclave memory and in AEAD-wrapped form in the public puzzle.

I found no path by which an in-scope intermediary (excluding AWS, excluding side channels) obtains request/response plaintext before the epoch key is recovered by puzzle computation. The conditionality is dominated by three things the code cannot fix: (i) the RandomX-primitive assumption itself, (ii) the absence of any hardware-independent lower bound (F1), and (iii) unproven absence of microarchitectural or native-library memory disclosure (F6).

Content vs metadata: the parent and front end learn destination IPs, timing, sizes, connection counts, and SNI whenever ECH is unavailable or falls back (net.rs:96–124). Note that in the deployed v2 the Mullvad/WireGuard relay is not configured at all — v2_main.rs:156 constructs Net::new(transport, /*relay_required=*/ false, vec![]), and relay.rs:89–98 therefore routes every upstream directly through parent CONNECT tunnels. The parent thus sees every upstream IP today, not only "while the relay is down." This is disclosed in threatmodel §6 but is worth stating as a code-level fact: hostname hiding rests entirely on ECH uptake.

Question B — early key recovery / pre-solved puzzle (review emphasis)

B1. Wrapping construction (timelock/src/lib.rs)

The construction is sound under the primitive assumptions:

B2. Seeds and entropy

All secret material comes from direct NSM fills with zeroized destinations and no fallback (attest.rs:43–62, lib.rs:301–325; the test at lib.rs:580–609 shows a failing entropy call aborts before any expensive work). The operator cannot bias seed selection, cannot replay NSM entropy, and cannot inject a chosen manifest: there is no external input anywhere into puzzle materialgenerate_with_rng consumes only epoch counter, measured iterations, signer, mode, and NSM bytes.

B3. Checkpoints

Checkpoint (lib.rs:402–456) is a progress hint, not a trust object: validate() checks only self-consistency (its own checksum) plus bounds; a forged checkpoint with a self-consistent digest but wrong x produces a wrong y_i and fails the segment AEAD (lib.rs:262–284) or the final commitment. Two properties follow:

B4. Epoch transitions and malicious parent scheduling

This is where I looked hardest. The relevant ordering (v2_epoch.rs):

Remaining scheduling vectors and their outcomes:

Vector Outcome
Parent withholds record/puzzle ACKs Activation fails → generator task returns permanently (v2_epoch.rs:146) → service closed until operator restart. Availability only (F3).
Parent ACKs without storing Client receives success headers; artifacts may never exist publicly. Retention/provenance lie, documented (F2). Not early confidentiality.
Parent kills/restarts enclave Keys lost (memory only); nothing reused; epoch counter restarts at a random 63-bit value (v2_epoch.rs:105) — no replay surface.
Parent throttles enclave CPUs / chooses CPU count at launch Generation slower → longer delay, fail-closed gaps. Cannot shorten anything; generation parallelism is capped at 7 threads regardless of vCPU count.
Parent manipulates time Time comes from NSM attestation timestamps (attest.rs:86–98), trusted via AWS assumption; no parent-reachable clock input exists.
Parent replays old host messages publish opens a fresh connection per attempt and requires exact OK\n (v2_epoch.rs:57–74, transport.rs:35–39); frames are fresh bytes each time. No replay surface found.
Operator runs dev mode (--dev) Attested policy reports mode: "dev", graviton5_verified: false; _bound_policy and client.py:155–158 reject. Only defeats non-verifying clients, who have no guarantee anyway.
Pre-solved puzzle induction No input channel exists into manifest contents; the serving epoch is always the one just generated internally. No path found.

B5. Leases, reuse, sequence numbers

Conclusion B: SUPPORTED CONDITIONALLY. Under the RandomX-primitive assumption and AWS/Nitro trust, the earliest any party — including the operator, who has no informational advantage — can decrypt an epoch's records is ~7 reference-CPU-days of serial work starting at puzzle publication.


Concrete findings

F1 — Delay is calibration-relative and publication-relative (design; disclosed but load-bearing). relay-v2.toml:2–6 calibrates 43,768,124 iterations ≈ 1 reference-CPU-day per segment on Graviton5. Nothing in the code bounds an adversary's hash rate. An adversary with 10× the reference machine recovers keys ~17 h after publication. Additionally, delay runs from puzzle publication, not per request: with 24 h epochs, records sealed in the epoch's last hour become decryptable ~6 days after sealing (v2_epoch.rs:82–101 order: anchor → publish → serve for 24 h). If "approximately one week" is a hard requirement, the late-epoch shortfall (~14%) should be treated as a parameter, not a rounding error. Not a violation of the documented objective; it is the boundary of the objective itself.

F2 — Parent persistence ACK is a lie-able channel (documented; code-confirmed). v2_epoch.rs:57–74 accepts a 3-byte OK\n from the untrusted parent as "publication acknowledged," and v2_proxy.rs:107–116 returns success headers (x-attested-relay-record) on the same basis. A malicious parent can ACK and discard, so the ciphertext, puzzle, and evidence may never be publicly retrievable even though the client was told a record name. Impact: retention/availability/provenance, never early confidentiality. The code comment and threatmodel §5 acknowledge this; the client (client.py) does not itself fetch-and-retain the puzzle artifact as a receipt. Suggested hardening: have the verifying client immediately fetch the named puzzle/record from the public archive and keep it.

F3 — Any activation/publication failure permanently kills the epoch generator (availability). v2_epoch.rs:128 and :146 both return on a single failed generation or activation; nothing retries. A parent that withholds one ACK (or one that simply lets a transient occur) converts the service into a permanently closed relay until an operator restart — and the operator is assumed malicious, so "the operator restarts it" is not a mitigation. Impact: liveness DoS fully within parent capability; fail-closed, no confidentiality impact.

F4 — Relay session is a bearer token visible to the front end (integrity/availability). The /relay transport (http_relay.rs:166–219) authenticates sessions solely by reqid (32–64 hex chars) carried in the URL. Cloudflare — in scope as malicious — observes every client's reqid, seq, ack and can therefore inject chosen bytes into a victim's inner TLS stream or race the victim's polls to consume responses. Impact boundary: the injected data is still inside the victim's inner TLS, so nothing about the victim's chosen content is revealed; the attacker gains DoS, request forgery attributed to the victim's audit trail, and response theft for requests it itself injected. This does not breach content confidentiality, but it is a concrete in-scope integrity defect. A per-session secret established inside inner TLS (or an HMAC over packets keyed by a TLS-derived secret) would close it.

F5 — Record nonces come from the kernel RNG, not directly from NSM. lib.rs:103–104 uses rand::0.8 OsRng. The enclave mitigates this by double-reseeding the pinned 4.14.256 kernel CRNG from NSM before TLS/serving (attest.rs:67–82, 197–214), which is a thoughtful treatment, but it makes the kernel a link in the nonce chain that direct NSM fills would avoid. With XChaCha's 192-bit nonce, even a partially predictable nonce is not exploitable; severity: informational, conditional on kernel trust already implied by the seeding design.

F6 — Future epoch key resident in memory up to ~a day before publication (memory-handling exposure window). v2_epoch.rs:118–151 generates epoch N+1 (including its epoch_key, GeneratedPuzzle) immediately after activating epoch N and holds it until N's expiry. This roughly doubles the window during which a side channel or memory-disclosure bug in native RandomX/TLS/aws-lc hands the adversary a key whose puzzle is not yet public — and worse, recovering the future key early would break the delay property for that epoch's records entirely. The Zeroizing wrappers cover Rust-side buffers only; RandomX scratchpads and aws-lc allocations are not zeroized. Within the stated assumptions (no side channels, Nitro isolation) this is not an attack; it is explicitly flagged because "memory handling" is a review target and this is the largest exposure surface the design itself creates.

F7 — No outbound relay configured: parent sees all upstream IPs today. v2_main.rs:156 passes relay_required=false, empty relay list; relay.rs:85–109 then routes upstreams directly through parent CONNECT tunnels. Combined with SNI visibility on ECH fallback (net.rs:96–124), destination identification by the parent is the practical default. Documented (threatmodel §6), but the deployment currently does not exercise the WireGuard machinery at all; treat hostname-privacy as best-effort.

F8 — is_ech_error matches error strings (net.rs:253–256). Behavior (fallback to plaintext SNI) depends on error-text fragments across library versions. Correctness/fragility nit; worst case is ECH retry logic misfiring (metadata only).

F9 — Vendored i18n-embed-fl patch is a supply-chain input to the measured binary. Cargo.toml:35–38 patches a proc-macro crate from vendor/. It is justified (HashMap-order determinism) and ultimately measured by PCR0, but reproduction evidence proves only self-consistency of this source, not its safety. It deserves the same review scrutiny as application code; I was not given vendor/i18n-embed-fl/PATCHED.md.

F10 — NSM amplification via /v1/status (availability nit). policy() calls trusted_time_ms_uncached() — a full NSM attestation — per status request, outside the attestations semaphore (v2_main.rs:73–98; the semaphore guards only /v1/attestation). Up to max_connections (128) concurrent unauthenticated inner-TLS callers can contend on the NSM. The deliberate uncached design is correct for admission; the missing bound is minor.


Strongest code-grounded reasons the attacks are blocked

  1. Serial wrap chain with domain-separated inputs (lib.rs:238–284, 365–387): each seed is only obtainable through the previous segment's full chain; AEAD under y_i means no partial-information shortcut exists short of breaking ChaCha20Poly1305/HKDF/RandomX.
  2. No external input into puzzle material: the serving manifest is constructed exclusively from NSM entropy and internal counters; there is no code path by which a parent, client, or network input influences seeds, wraps, iterations (config is include_str!-measured, v2_main.rs:137), or the epoch key.
  3. Publication anchoring and fail-closed activation (v2_epoch.rs:76–102, 132–147): puzzles become solvable no earlier than the previous epoch's NSM-time expiry, lifetimes are anchored before first disclosure, and withheld ACKs discard the puzzle rather than rebase it.
  4. Attestation binding: fresh nonce + 5-minute window + PCR0 pin + SPKI-to-actual-peer binding (verify.py:52–142) defeats replay and mis-termination of the inner TLS; signed policy pins the work parameters so a low-work puzzle requires a new PCR0, which clients reject.
  5. Record binding: per-epoch HKDF key, AAD over epoch/sequence/manifest-id, checked-add sequence counter — no cross-epoch or replay decryption.

Missing evidence limiting the conclusions

Would I rely on this for sensitive content under the explicit assumptions?

Conditionally yes for content confidentiality, with eyes open. Against the enumerated intermediaries (parent, front end, network, DNS, archive, other clients, external solvers), and trusting AWS/Nitro plus the RandomX primitive, I found no route to request/response plaintext before the puzzle is computable, and no route to make the service serve under a pre-solved puzzle. The reliance is bounded by: (a) the delay is only as strong as the slowest serious solver's hardware — treat "one week" as roughly 6 days for late-epoch records and as shrinking with hardware improvements; (b) the parent can silently destroy the archive story (F2) and permanently kill service (F3); (c) destination-level metadata is currently exposed in full (F7); (d) any memory-disclosure or side-channel bug in the sizeable native surface defeats everything, and the future-key residency window (F6) makes that the sharpest unproven edge. For content whose disclosure a week early would be catastrophic, the residual risks (b)–(d) argue for not relying on this alone; for content whose disclosure a week early is merely harmful, the construction, as far as the supplied source shows, does what the threat model honestly says it does — no more.