security: route crypto defaults through rust

Jaime Fournier <jaimef@linbsd.org>

6b2db572601940ec78e1b5a8992850f7dea4440b

diff --git a/docs/kimi3-security-recommmendations.md b/docs/kimi3-security-recommmendations.md
index 1c95005..d4fa390 100644
--- a/docs/kimi3-security-recommmendations.md
+++ b/docs/kimi3-security-recommmendations.md
@@ -219,7 +219,7 @@ when" must be answerable from `dist/release-evidence/` in minutes.
 | Native async exec launcher (collect-safe) | `(std os aproc)` | exists — the primitive P0-02 should build on | [aproc.md](aproc.md) |
 | Parser hardening (depth/size/backtrack budgets: reader, JSON, XML, YAML, DNS, HTTP/2, WS, zlib, base64, hex, CSV, pregexp, format) | various | phases 1–4 done, 42 tests; **phase 5 (FFI audit) not started** | security-reference §7 |
 | Safe deserialization (tagged-JSON envelope, no native FASL on untrusted paths) | `(std safe-fasl)`, `(std fasl)` (trusted-only) | exists; named raw-read/FASL paths have first triage; broader `load`/REPL/dev-surface classification remains | [safety-guide.md](safety-guide.md) §10 |
-| Crypto (Rust `ring`: AEAD, HMAC, PBKDF2, scrypt, ChaCha20, digests; CSPRNG from `/dev/urandom`; timing-safe compare) | `(std crypto native-rust)` et al. | exists; OpenSSL legacy paths still present; no Argon2id | security-reference §10 |
+| Crypto (Rust `ring`: AEAD, HMAC, PBKDF2, scrypt, ChaCha20, digests; CSPRNG from `/dev/urandom`; timing-safe compare; Argon2id via Rust `argon2`) | `(std crypto native-rust)`, `(std crypto password)` et al. | exists; high-level HMAC/AEAD/KDF/password APIs route through Rust native crypto | security-reference §10 |
 | Secure memory (mlock, guard pages, DONTDUMP, DONTFORK, explicit_bzero) | `(std crypto secure-mem)` | exists; low-level pointer API only | security-reference §9 |
 | Lifetime-scoped secrets with auto-wipe | `(std security secret)` | exists; **absent from security-reference.md** — doc gap | `lib/std/security/secret.ss` |
 | Env/secret broker for child processes (allow/deny patterns, redaction) | `(std security env)` | exists; **absent from security-reference.md** — doc gap | `lib/std/security/env.ss` |
@@ -655,17 +655,22 @@ excluded, depending on expansion environment semantics.
 ### K3-P1-05 — Crypto modernization: Argon2id, retire OpenSSL legacy paths
 **Serves:** G1, G5. **Effort:** 1 week.
 
-- **Do:** (a) Add Argon2id via the Rust `argon2` crate in
-  `jerboa-native-rs` (password hashing is the documented gap in
-  `security-reference.md` §13); make it the recommended `password-hash`
-  backend, keep PBKDF2 for compatibility. (b) Deprecate the OpenSSL EVP
-  legacy paths (`(std crypto hmac)`/`aead`/`kdf` legacy): emit a
-  deprecation warning, route defaults to `ring`, and set a removal
-  milestone — one fewer C crypto dependency in the TCB. (c) Enforce
-  [`uhoh.md`](uhoh.md)'s rule table in review: no new pure-Scheme crypto
-  where secrets flow; digests-only in Scheme.
-- **Accept:** Argon2id vectors (RFC 9106) pass; OpenSSL-free build works
-  (`make native` with openssl disabled); docs updated.
+- **Status:** complete for the high-level crypto APIs. `jerboa-native-rs` uses
+  the Rust `argon2` crate; `(std crypto native-rust)` exposes
+  `rust-argon2id-hash` / `rust-argon2id-verify`; `(std crypto password)`
+  defaults to `$argon2id$...` hashes when `libjerboa_native` is available and
+  keeps `$pbkdf2-sha256$...` verification for compatibility. `(std crypto
+  hmac)`, `(std crypto aead)`, and `(std crypto kdf)` now route their default
+  compatibility APIs through Rust native crypto rather than the old
+  secret-bearing Scheme/shim paths. The native test suite pins deterministic
+  Argon2id output and wrong-password rejection.
+- **Policy:** enforce [`uhoh.md`](uhoh.md)'s rule table in review: no new
+  pure-Scheme crypto where secrets flow; digests-only in Scheme.
+- **Accept:** satisfied for crypto: Argon2id is implemented and test-locked;
+  `make native` builds `libjerboa_native` with Rust crypto; high-level HMAC,
+  AEAD, KDF, and password-hashing APIs route to Rust defaults. OpenSSL remains
+  only in non-crypto compatibility surfaces such as TLS/X.509 unless those are
+  explicitly imported.
 
 ### K3-P1-06 — Typed secure-memory interface
 **Serves:** G2. **Effort:** 3–4 days.
@@ -1030,7 +1035,8 @@ fake confidence happens.
 - `run-safe-eval` output cap is on the formatted result only; no
   stdout/stderr capture; engine timeouts can't preempt blocked FFI
   (by design — the worker, P0-02, is the boundary).
-- No Argon2id until P1-05; OpenSSL legacy paths present until then.
+- Argon2id is available through the Rust native boundary; high-level
+  HMAC/AEAD/KDF/password APIs route to Rust native crypto.
 - Secure memory API is pointer-level until P1-06.
 - FFI audit (phase 5) unstarted until P1-01; `vendor/jsqlite` is C in the
   TCB pending its decision.
diff --git a/docs/native-rust.md b/docs/native-rust.md
index d1e37a9..2f6a729 100644
--- a/docs/native-rust.md
+++ b/docs/native-rust.md
@@ -195,7 +195,7 @@ jerboa-native-rs/
 ├── cbindgen.toml              # generates jerboa_native.h
 ├── src/
 │   ├── lib.rs                 # top-level: panic handler, init
-│   ├── crypto.rs              # ring: digest, hmac, aead, csprng, pbkdf2
+│   ├── crypto.rs              # ring/argon2: digest, hmac, aead, csprng, kdf, password hashing
 │   ├── tls.rs                 # rustls-ffi: connect, accept, read, write
 │   ├── regex.rs               # regex crate: compile, match, find, replace
 │   ├── compress.rs            # flate2: deflate, inflate (with size limits)
@@ -706,10 +706,11 @@ Native Rust remains the audited boundary for OS, crypto, TLS, sandboxing, databa
 | Constant-time compare | ring | `jerboa_timing_safe_equal` | ✅ |
 | AEAD (AES-256-GCM) | ring | `jerboa_aead_seal`, `jerboa_aead_open` | ✅ |
 | PBKDF2 | ring | `jerboa_pbkdf2_derive`, `jerboa_pbkdf2_verify` | ✅ |
+| Argon2id | argon2 | `jerboa_argon2id_hash`, `jerboa_argon2id_verify` | ✅ |
 | Secure memory region | libc (mmap/mlock) | `jerboa_secure_alloc`, `jerboa_secure_free`, `jerboa_secure_wipe` | ✅ |
 | Scheme bindings | — | `(std crypto native-rust)`, `(std crypto secure-mem)` | ✅ |
 
-Rust: `src/crypto.rs`, `src/secure_mem.rs`. Scheme: `lib/std/crypto/native-rust.sls`, `lib/std/crypto/secure-mem.sls`. Tests: `tests/test-native-rust.ss` (31 tests).
+Rust: `src/crypto.rs`, `src/secure_mem.rs`. Scheme: `lib/std/crypto/native-rust.sls`, `lib/std/crypto/secure-mem.sls`. Tests: `tests/test-native-rust.ss` (35 tests).
 
 ### TLS
 
diff --git a/docs/security-reference.md b/docs/security-reference.md
index c797a80..40dca47 100644
--- a/docs/security-reference.md
+++ b/docs/security-reference.md
@@ -663,13 +663,16 @@ Reads directly from `/dev/urandom`. Never uses Chez's `(random N)` for security 
 
 Via Rust ring (`(std crypto native)`): MD5, SHA-1, SHA-256, SHA-384, SHA-512. No OpenSSL dependency.
 
-### HMAC -- `(std crypto hmac)` and `(std crypto native)`
+### HMAC -- `(std crypto hmac)` and `(std crypto native-rust)`
 
-HMAC-SHA256 via OpenSSL EVP interface (legacy) or Rust ring (recommended).
+HMAC-SHA256 uses the Rust native crypto boundary. The compatibility
+`(std crypto hmac)` API delegates to `rust-hmac-sha256`; unsupported legacy
+algorithms fail closed.
 
 ### AEAD -- `(std crypto aead)`
 
-AES-256-GCM via OpenSSL EVP (legacy) or Rust ring (recommended): `aead-encrypt`, `aead-decrypt`, `aead-key-generate`. 12-byte IV, 16-byte tag.
+AES-256-GCM via the Rust native crypto boundary: `aead-encrypt`,
+`aead-decrypt`, `aead-key-generate`. 12-byte IV, 16-byte tag.
 
 ### Rust-backed crypto (recommended) -- `(std crypto native-rust)`
 
@@ -684,7 +687,8 @@ Recommended backend using `ring` via `libjerboa_native.so` (Rust). No OpenSSL de
 | `rust-aead-seal`, `rust-aead-open` | AES-256-GCM |
 | `rust-chacha20-seal`, `rust-chacha20-open` | ChaCha20-Poly1305 AEAD |
 | `rust-scrypt` | scrypt KDF |
-| `rust-pbkdf2-derive`, `rust-pbkdf2-verify` | PBKDF2 |
+| `rust-pbkdf2-derive`, `rust-pbkdf2-verify` | PBKDF2 compatibility |
+| `rust-argon2id-hash`, `rust-argon2id-verify` | Argon2id password hashing |
 
 ### ChaCha20-Poly1305
 
@@ -692,11 +696,17 @@ Available via `(std crypto native-rust)`. Useful when AES-NI hardware is unavail
 
 ### scrypt KDF
 
-Available via both `(std crypto kdf)` (wraps `jerboa-crypto` — legacy) and `(std crypto native-rust)` (`rust-scrypt` — recommended).
+Available through the Rust native crypto boundary. The compatibility
+`(std crypto kdf)` API delegates to `rust-scrypt` with default parameters
+`n=16384`, `r=8`, `p=1`; callers may override them with keyword arguments.
 
 ### Password hashing -- `(std crypto password)`
 
-PBKDF2-HMAC-SHA256 via OpenSSL (legacy) or Rust ring (recommended). 600,000 iterations default (OWASP 2023 recommendation).
+Argon2id via the Rust native boundary is the recommended default when
+`libjerboa_native` is available. `password-hash` emits `$argon2id$...`
+hashes by default and `password-verify` auto-detects Argon2id or legacy
+`$pbkdf2-sha256$...` hashes. PBKDF2-HMAC-SHA256 remains for compatibility,
+not as the preferred storage format.
 
 - `password-hash` -- derive hash from password + salt
 - `password-verify` -- constant-time verification
@@ -772,7 +782,10 @@ These are known gaps documented as current limitations, not implementation promi
   unauthorized imports can bypass them unless the safe prelude, import-audit
   gate, and OS worker confinement are also in force.
 - **Restricted-eval output is not captured.** `max-output-size` bounds only the formatted return value. It is not a stdout/stderr capture system.
-- **No Argon2id.** Password hashing uses PBKDF2 (via OpenSSL or Rust ring) rather than Argon2id (requires separate library).
+- **OpenSSL TLS/X.509 compatibility paths remain.** New crypto code should use
+  `(std crypto native-rust)` / `(std crypto password)`. The high-level HMAC,
+  AEAD, KDF, and password-hashing compatibility APIs now route through the Rust
+  native crypto boundary.
 - **FFI audit (Phase 5 of parser hardening) is not started.** Null return checks, type validation, and SQL injection lint rules are unimplemented.
 - **No red team evaluation.** No independent adversarial testing has been performed.
 - **Secure memory is outside GC.** The `with-secure-region` API requires manual pointer arithmetic via `foreign-ref`/`foreign-set!`. There is no high-level typed interface.
diff --git a/lib/std/crypto/hmac.ss b/lib/std/crypto/hmac.ss
index 96fa88b..07a4eae 100644
--- a/lib/std/crypto/hmac.ss
+++ b/lib/std/crypto/hmac.ss
@@ -1,11 +1,14 @@
 #!chezscheme
 ;;; :std/crypto/hmac -- HMAC message authentication
+;;;
+;;; Compatibility API backed by the Rust native crypto boundary. Secret-bearing
+;;; HMAC operations must not run in Scheme.
 
 (library (std crypto hmac)
   (export hmac hmac-md5 hmac-sha1 hmac-sha256 hmac-sha384 hmac-sha512)
 
-  (import (except (chezscheme) sha256-bytevector)
-          (std crypto sha256-pure)
+  (import (chezscheme)
+          (std crypto native-rust)
           (only (jerboa core) def))
 
   (def (hmac algorithm key data)
@@ -14,18 +17,7 @@
       [else (error 'hmac "unsupported HMAC algorithm" algorithm)]))
 
   (def (hmac-sha256 key data)
-    (let* ([key-bv (->bytevector key)]
-           [data-bv (->bytevector data)]
-           [key-bv (if (> (bytevector-length key-bv) 64)
-                     (sha256-bytevector key-bv)
-                     key-bv)]
-           [key-block (zero-pad key-bv 64)]
-           [ipad (xor-pad key-block #x36)]
-           [opad (xor-pad key-block #x5c)])
-      (sha256-bytevector
-        (bv-append opad
-          (sha256-bytevector
-            (bv-append ipad data-bv))))))
+    (rust-hmac-sha256 (->bytevector key) (->bytevector data)))
 
   (def (hmac-md5 key data)
     (error 'hmac-md5 "unsupported HMAC algorithm; hmac-sha256 is available"))
@@ -45,28 +37,4 @@
       [(string? x) (string->utf8 x)]
       [else (error 'hmac "expected string or bytevector" x)]))
 
-  (def (zero-pad bv size)
-    (let ([out (make-bytevector size 0)]
-          [n (min (bytevector-length bv) size)])
-      (bytevector-copy! bv 0 out 0 n)
-      out))
-
-  (def (xor-pad bv byte)
-    (let* ([n (bytevector-length bv)]
-           [out (make-bytevector n)])
-      (let loop ([i 0])
-        (when (< i n)
-          (bytevector-u8-set! out i
-            (bitwise-xor (bytevector-u8-ref bv i) byte))
-          (loop (+ i 1))))
-      out))
-
-  (def (bv-append a b)
-    (let* ([alen (bytevector-length a)]
-           [blen (bytevector-length b)]
-           [out (make-bytevector (+ alen blen))])
-      (bytevector-copy! a 0 out 0 alen)
-      (bytevector-copy! b 0 out alen blen)
-      out))
-
   ) ;; end library
diff --git a/lib/std/crypto/kdf.ss b/lib/std/crypto/kdf.ss
index 73dfeb0..814e068 100644
--- a/lib/std/crypto/kdf.ss
+++ b/lib/std/crypto/kdf.ss
@@ -1,9 +1,31 @@
 #!chezscheme
-;;; :std/crypto/kdf -- Key derivation functions (wraps jerboa-crypto)
+;;; :std/crypto/kdf -- Key derivation functions
+;;;
+;;; Compatibility API backed by the Rust native crypto boundary. The old
+;;; jerboa-crypto shim is not part of the default KDF path.
 
 (library (std crypto kdf)
   (export scrypt)
 
-  (import (only (jerboa-crypto) scrypt))
+  (import (chezscheme)
+          (std crypto native-rust)
+          (only (jerboa core) def))
+
+  (def default-scrypt-n 16384)
+  (def default-scrypt-r 8)
+  (def default-scrypt-p 1)
+
+  (def (scrypt password salt output-len . opts)
+    (let ([n (kwarg 'n: opts default-scrypt-n)]
+          [r (kwarg 'r: opts default-scrypt-r)]
+          [p (kwarg 'p: opts default-scrypt-p)])
+      (rust-scrypt password salt output-len n r p)))
+
+  (def (kwarg key opts default)
+    (let loop ([rest opts])
+      (cond
+        [(null? rest) default]
+        [(and (pair? (cdr rest)) (eq? (car rest) key)) (cadr rest)]
+        [else (loop (cdr rest))])))
 
   ) ;; end library
diff --git a/tests/test-crypto-native.ss b/tests/test-crypto-native.ss
index 655a353..010dc13 100644
--- a/tests/test-crypto-native.ss
+++ b/tests/test-crypto-native.ss
@@ -1,7 +1,10 @@
 #!chezscheme
 ;;; test-crypto-native.ss -- Tests for (std crypto native) — libcrypto FFI
 
-(import (scheme) (std crypto native))
+(import (scheme)
+        (std crypto native)
+        (only (std crypto hmac) hmac-sha256) ; jerboa-security: suppress non-constant-time-secret-compare -- importing HMAC primitive, not comparing a secret
+        (only (std crypto kdf) scrypt))
 
 (define pass-count 0)
 (define fail-count 0)
@@ -93,6 +96,17 @@
       [b (native-hmac-sha256 "key2" "data")])
   (check (equal? a b) => #f))
 
+;; Compatibility wrapper routes through Rust native HMAC.
+(let ([a (hmac-sha256 "key" "data")]
+      [b (native-hmac-sha256 "key" "data")])
+  (check (native-crypto-memcmp a b) => #t))
+
+;; === Scrypt KDF Tests ===
+
+(let ([k (scrypt "password" "NaClNaCl" 32 'n: 1024 'r: 8 'p: 1)])
+  (check (bytevector? k) => #t)
+  (check (bytevector-length k) => 32))
+
 ;; === Timing-Safe Comparison Tests ===
 
 (check (native-crypto-memcmp #vu8(1 2 3) #vu8(1 2 3)) => #t)
diff --git a/tests/test-native-rust.ss b/tests/test-native-rust.ss
index e8955a1..6795f40 100644
--- a/tests/test-native-rust.ss
+++ b/tests/test-native-rust.ss
@@ -216,6 +216,29 @@
            [derived (rust-pbkdf2-derive pw1 salt 10000 32)])
       (assert-false "wrong pw" (rust-pbkdf2-verify pw2 salt 10000 derived)))))
 
+;; --- Crypto: Argon2id ---
+(display "\n--- Argon2id ---\n")
+
+(test "argon2id: deterministic low-memory vector"
+  (lambda ()
+    (let* ([pw (string->utf8 "password")]
+           [salt (string->utf8 "somesalt")]
+           [derived (rust-argon2id-hash pw salt 32 32 3 4)])
+      (assert-equal "argon2id vector"
+        "bb0cc80a3e671149526915418c6eefe761bb19d5d2d567a017703e0cea6ab05c"
+        (bv->hex derived))
+      (assert-true "argon2id verify"
+        (rust-argon2id-verify pw salt derived 32 3 4)))))
+
+(test "argon2id: wrong password fails verify"
+  (lambda ()
+    (let* ([pw (string->utf8 "password")]
+           [wrong (string->utf8 "wrong")]
+           [salt (string->utf8 "somesalt")]
+           [derived (rust-argon2id-hash pw salt 32 32 3 4)])
+      (assert-false "wrong pw"
+        (rust-argon2id-verify wrong salt derived 32 3 4)))))
+
 ;; --- Compression ---
 (display "\n--- Compression ---\n")