crypto: constant-time pure-OCaml AES + Node.js differential test
Two follow-up changes on top of 98046dc8:
1. Pure-OCaml backend: replace the T-table AES with a bitsliced
constant-time implementation.
src/ocaml/aes_pure.ml was a direct port of mirage-crypto's old
aes_generic.c -- Philip J. Erdelsky's public-domain T-table AES,
which uses sbox.(secret_byte) lookups and is exposed to
cache-timing attacks on the host. This matters because Fortuna
in the JS / WASM target encrypts every random byte with a secret
key, and the only way to reach AES from those targets is the
pure-OCaml backend.
The replacement is a direct port of BearSSL's aes_ct.c +
aes_ct_enc.c (32-bit bitsliced, by Thomas Pornin, MIT license),
using OCaml Int32 for portability across native, js_of_ocaml,
and wasm_of_ocaml. The Boyar-Peralta S-box circuit (115 logic
ops, no table indexing) and the bitsliced ShiftRows /
MixColumns / AddRoundKey are the same algorithms BearSSL uses
for its constant-time fallback on the C side, so the JS path
now has the same security model as the C aes_generic.c path.
Two blocks are processed in parallel via 8 Int32 words, packed
in BearSSL's standard interleaved layout (block 1 at q[0,2,4,6],
block 2 at q[1,3,5,7]). Single-block calls pad with zero in
the second slot. The expanded key schedule is wiped before
return, mirroring mc_secure_bzero on the C side.
Performance: significantly slower than the T-table version
(boxed Int32 + bit operations), but still fast enough for the
intended use case (Fortuna RNG + small AES-GCM workloads in JS
/ WASM builds). All 48 differential test vectors agree
byte-for-byte across the C, native-OCaml, and JS backends.
2. Three-way differential test: capture stdout from
test_pure_c.exe (C), test_pure.exe (OCaml native), and
test_pure.bc.js (OCaml under Node.js) and dune-diff them in
the runtest alias. Adds a runtest-js sub-alias that depends on
node being on $PATH; falls back gracefully when node is missing
(the bash 'true' shim). CI pipelines that include Node.js
exercise the JS path automatically.
Reverts (per directive to minimize the diff with mirage-crypto
upstream where the change is not security-related):
- crypto.h: revert the do { } while (0) wrapping of
_mc_switch_accel. Latent bug fix only -- no current call site
triggers the dangling-else issue, and upstream does not have
the wrap. Add it again only when a real call-site triggers it.
- src/c/aes_aesni.c, ghash_pclmul.c, misc_sse.c: revert the
matching trailing-semicolon additions.
- bitfn.h: revert the __builtin_bswap modernization. Upstream
still uses hand-written inline asm; our use case has no
ARMv6-M targets that would benefit from the builtin path.
The security/correctness fixes from 98046dc8 (auxv.h typo,
xor_into unaligned writes, _mc_count_16_be_4 strict aliasing,
mc_secure_bzero, ARM64 dead-code removal, hardening flags) all
remain in place. All 4068 tests still pass.