Skip to content

Commit de46da8

Browse files
docs: reflect real in-place secret zeroization (Track 1) + updated test counts
SECURITY.md item 4 and AUDIT.md issue #2 now describe the reference-shared Word8Array-backed Secret cell wiped in place via SecureZero.zero, with honest residual caveats (string-typed key-schedule/AEAD boundaries, zeroizeConfig rebind, no sodium_malloc/mlock pinning). Test counts updated to 317 (pure) / 357 (FFI) on both compilers. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 4208eef commit de46da8

2 files changed

Lines changed: 50 additions & 17 deletions

File tree

AUDIT.md

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,10 @@ libsodium for constant-time execution. The audit should cover:
3939
(libsodium) and AES-128/256-GCM + RSA-PSS-SHA256 (OpenSSL). The default
4040
(no-FFI) build retains the pure-SML primitives as the portable fallback and
4141
proof oracle; those are **not** constant-time.
42-
5. **Key lifecycle** (`SecureZero`, `zeroize` / `zeroizeConfig`): whether key
43-
material is erased on teardown and the documented best-effort limits.
42+
5. **Key lifecycle** (`SecureZero`, `Secret`, `zeroize` / `zeroizeConfig`):
43+
per-connection secrets are erased in place on teardown via a reference-shared
44+
`Word8Array`-backed `Secret` cell; review the documented residual limits
45+
(`string`-typed key-schedule/AEAD boundaries, config rebind, no pinning).
4446

4547
### Explicitly out of scope (already documented as trusted/assumed)
4648
- The cryptographic *strength* of SHA-256/HMAC/AEAD/X25519/RSA primitives
@@ -82,10 +84,11 @@ libsodium for constant-time execution. The audit should cover:
8284
Prior run: 33 000+ inputs crash-free on the parser entry points.
8385

8486
### 3.2 Test suite
85-
- 306 passing tests on **both** MLton and Poly/ML (`make test && make test-poly`),
87+
- 317 passing tests on **both** MLton and Poly/ML (`make test && make test-poly`),
88+
357 in the FFI build (`make test-ffi && make test-ffi-poly`),
8689
including RFC 8448 vectors, round-trip and tamper/negative tests, the PSK
8790
resumption accept + negative-binder tests, the FFI cross-implementation
88-
byte-equality tests, and the zeroize tests.
91+
byte-equality tests, and the in-place zeroize tests.
8992

9093
### 3.3 Formal-verification artifacts
9194
- `proof/` — 5 HOL4 theories, `Holmake cleanAll && Holmake` clean,
@@ -137,9 +140,16 @@ bash scripts/run_afl.sh
137140
these in the FFI build; byte-identity (NIST GCM) and pure↔OpenSSL RSA-PSS
138141
cross-verification are proved in `test/ffi.sml` on both compilers. A
139142
deployment with a timing adversary must use the FFI build, not the default.
140-
2. **Memory zeroing is best-effort**: SML strings are immutable and the GC may
141-
have copied secrets; `zeroize` overwrites the currently-referenced buffers
142-
only (documented in `SECURITY.md`).
143+
2. **Memory zeroing — real in-place erasure (with bounded residuals)**:
144+
per-connection secrets (traffic keys/IVs, derived secrets, ephemeral + RSA
145+
private keys, PSK ticket store) live in a mutable, reference-shared
146+
`Word8Array`-backed `Secret` cell that `zeroize` wipes in place via
147+
`SecureZero.zero`, so erasure is observable through the original state handle
148+
on both compilers (asserted in `test/zeroize.sml`). Residuals: key-schedule /
149+
AEAD boundaries are still `string`-typed (transient GC-reclaimed copies via
150+
`Secret.toBytes`), `zeroizeConfig` still rebinds remaining `string` config
151+
fields, and there is no `sodium_malloc`/`mlock` pinning (`Secret.pinned =
152+
false`) — all documented in `SECURITY.md` item 4.
143153
3. **Formal refinement gaps** (do not affect the running pure-SML/FFI library,
144154
relevant only to the "provably correct to machine code" claim): the Track 2c
145155
refinement is hand-mirrored (not yet `ml_translatorLib`-certified), and the

SECURITY.md

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,11 @@ A full TLS 1.3 (RFC 8446) client and server state machine in pure Standard ML:
4242
catch-all backstop maps any unexpected exception to `decode_error`, so
4343
untrusted input cannot crash `step`.
4444

45-
**Test coverage:** 306 passing tests on both MLton and Poly/ML, including RFC
45+
**Test coverage:** 317 passing tests on both MLton and Poly/ML (357 in the FFI
46+
build), including RFC
4647
8448 vector tests, round-trip tests, negative / tamper tests, PSK-resumption
4748
accept + negative-binder tests, FFI cross-implementation byte-equality tests,
48-
and key-zeroization tests.
49+
and in-place key-zeroization tests.
4950

5051
### Interoperability (J2)
5152

@@ -170,13 +171,31 @@ to compile faithfully rather than an in-logic `compile` evaluation (GAP B).
170171
The remaining hello/cert/ticket codecs and the full crypto-bearing
171172
mid-handshake simulation are not yet refined.
172173

173-
### 4. Memory zeroing (partially addressed, best-effort)
174-
175-
`TlsClient.zeroize` / `TlsServer.zeroize` / `zeroizeConfig` overwrite traffic
176-
keys, derived secrets, and `serverConfig.rsaPrivateKeyDer` via `sodium_memzero`
177-
(an FFI call the optimizer cannot elide). **Best-effort caveat:** SML strings
178-
are immutable and the GC may have copied secrets before zeroization; only the
179-
currently-referenced buffers are overwritten, so prior copies may survive.
174+
### 4. Memory zeroing (real in-place erasure for per-connection secrets)
175+
176+
Per-connection secret material (traffic keys/IVs, derived handshake/application
177+
secrets, ephemeral private keys, the server's RSA private key, and the PSK
178+
ticket store) now lives in a mutable, reference-shared `Secret` cell backed by a
179+
`Word8Array`. `TlsClient.zeroize` / `TlsServer.zeroize` wipe these buffers **in
180+
place** via `SecureZero.zero` (`sodium_memzero` in the FFI build, an un-elidable
181+
`Word8Array.modify` in the pure build), so the erasure is observable through the
182+
**original** state handle — not merely a rebind on the returned value. This is
183+
verified by `test/zeroize.sml`, which captures secrets, calls `zeroize` on the
184+
original handle, then re-reads the same handle and asserts all-zero, on both
185+
compilers.
186+
187+
**Residual caveats (honest):**
188+
- `TlsKeySchedule` outputs and the AEAD interface are still `string`-typed;
189+
secrets are converted to bytes only at those boundaries via `Secret.toBytes`,
190+
leaving a short-lived transient copy that the GC — not `sodium_memzero`
191+
reclaims.
192+
- `zeroizeConfig` (client and server) still rebinds the remaining `string`-typed
193+
config fields to zeros (prior semantics); only the per-connection state
194+
secrets are wiped truly in place.
195+
- No memory pinning (`sodium_malloc`/`mlock`): holding a persistent C pointer
196+
across the purely-functional state threading isn't clean on both compilers,
197+
and since the crypto APIs copy `string`s anyway it buys little over the
198+
reference-shared `Word8Array` mechanism (`Secret.pinned = false`).
180199

181200
### 5. No audit
182201

@@ -224,7 +243,11 @@ Completed (see `AUDIT.md`, `proof/PROOF_STATUS.md`, `proof/PROOF_CHAIN.md`):
224243
5.**Constant-time crypto** (FFI build): X25519 + ChaCha20-Poly1305 via
225244
libsodium; AES-128/256-GCM + RSA-PSS-SHA256 via OpenSSL libcrypto, with the
226245
live handshake routed through them in the `sources-ffi.mlb` build.
227-
6.**Memory zeroing**: `zeroize` API via `sodium_memzero` (best-effort).
246+
6.**Memory zeroing**: `zeroize` wipes per-connection secrets (traffic
247+
keys/IVs, derived secrets, ephemeral + RSA private keys, PSK ticket store)
248+
**in place** through a reference-shared `Word8Array`-backed `Secret` cell, so
249+
erasure is observable on the original state handle on both compilers (see
250+
item 4 for residual `string`-boundary / config caveats).
228251
7.**PSK resumption (accept path)**: server ticket store, binder
229252
verification, PSK key schedule.
230253

0 commit comments

Comments
 (0)