security: add checked secure bytevectors
Jaime Fournier <jaimef@linbsd.org>
06260b1a005cb38b2bc0e07120c1a599610dc48b
--- a/docs/kimi3-security-recommmendations.md +++ b/docs/kimi3-security-recommmendations.md @@ -220,7 +220,7 @@ when" must be answerable from `dist/release-evidence/` in minutes. | 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; 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 | +| Secure memory (mlock, guard pages, DONTDUMP/DONTFORK, explicit_bzero, checked secure bytevector view) | `(std crypto secure-mem)` | exists; high-level `secure-bytevector` API plus raw region escape hatch | 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` | | Sanitization (HTML/attr/URL/SQL/path/header) | `(std security sanitize)` | exists | security-reference §12 | @@ -678,13 +678,17 @@ excluded, depending on expansion environment semantics. `secure-mem` requires manual `foreign-ref`/`foreign-set!` pointer arithmetic — itself an FFI footgun (`security-reference.md` §13). -- **Do:** Add a high-level API: `secure-bytevector` view type with bounds- - checked get/set, `with-secure-bytevector` scoped form, and integration - with `(std security secret)` so `with-secret` can back secrets by - mlock'd memory. No raw pointer arithmetic exposed. -- **Accept:** tests exercise bounds violations (expect exceptions, not - corruption); guard-page trip test (SIGSEGV on out-of-bounds at the page - level) verified on Linux; §13 entry removed. +- **Status:** complete for the Scheme-facing API. `(std crypto secure-mem)` + now exposes `secure-bytevector` storage with bounds-checked get/set, + copy-out, wipe/random-fill/free, and `with-secure-bytevector`. + `(std security secret)` accepts secure bytevectors and wipes/frees them on + `secret-use` or scope exit. The raw `secure-region` API remains as an + explicit FFI escape hatch. +- **Accept:** satisfied for checked Scheme access: tests cover get/set, + copy-out, negative/end indexes, invalid byte values, use-after-free, + `with-secure-bytevector`, and `with-secret` cleanup of secure storage. + Guard-page process-crash tests remain platform smoke coverage rather than + normal unit tests. ### K3-P1-07 — Memory-limit story for untrusted evaluation **Serves:** G2. **Effort:** 1 week. @@ -1037,7 +1041,8 @@ fake confidence happens. (by design — the worker, P0-02, is the boundary). - 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. +- Secure memory has a checked `secure-bytevector` API; raw secure-region + pointers remain for explicit FFI integration. - FFI audit (phase 5) unstarted until P1-01; `vendor/jsqlite` is C in the TCB pending its decision. - No independent red-team evaluation yet (P2-04 starts the practice). --- a/docs/native-rust.md +++ b/docs/native-rust.md @@ -710,7 +710,7 @@ Native Rust remains the audited boundary for OS, crypto, TLS, sandboxing, databa | 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` (35 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` (38 tests). ### TLS --- a/docs/security-reference.md +++ b/docs/security-reference.md @@ -632,6 +632,12 @@ Allocates memory outside the GC heap with protections against leakage. Backed by ```scheme (import (std crypto secure-mem)) +;; Bounds-checked secure bytevector view +(with-secure-bytevector ([key 32]) + (secure-bytevector-random-fill! key) + (secure-bytevector-set! key 0 42) + (secure-bytevector-ref key 0)) + ;; Manual lifecycle (let ([region (secure-alloc 32)]) ;; ... use region ... @@ -646,6 +652,19 @@ Allocates memory outside the GC heap with protections against leakage. Backed by ) ;; auto-freed here ``` +The high-level `secure-bytevector` API is the default for Scheme code: +`make-secure-bytevector`, `bytevector->secure-bytevector`, +`secure-bytevector-ref`, `secure-bytevector-set!`, +`secure-bytevector->bytevector`, `secure-bytevector-wipe!`, +`secure-bytevector-random-fill!`, `secure-bytevector-free`, and +`with-secure-bytevector`. Indexes and byte values are checked before any +native memory access. `(std security secret)` accepts secure bytevectors and +wipes/frees them on `secret-use` or scope exit. + +The lower-level `secure-region` API remains available for FFI integration +that needs a raw pointer, but normal application code should not use +`foreign-ref`/`foreign-set!` against secure memory directly. + --- ## 10. Cryptography @@ -788,7 +807,10 @@ These are known gaps documented as current limitations, not implementation promi 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. +- **Secure memory still exposes a raw region escape hatch.** The high-level + `secure-bytevector` API is bounds-checked and integrated with + `(std security secret)`, but `secure-region-pointer` remains available for + low-level FFI integrations that need an explicit pointer. --- --- a/lib/std/crypto/secure-mem.ss +++ b/lib/std/crypto/secure-mem.ss @@ -9,7 +9,13 @@ secure-alloc secure-free secure-wipe secure-random-fill with-secure-region - secure-region? secure-region-pointer secure-region-size) + secure-region? secure-region-pointer secure-region-size + make-secure-bytevector bytevector->secure-bytevector + secure-bytevector? secure-bytevector-length + secure-bytevector-ref secure-bytevector-set! + secure-bytevector->bytevector secure-bytevector-wipe! + secure-bytevector-random-fill! secure-bytevector-free + with-secure-bytevector) (import (chezscheme) (std native) @@ -19,6 +25,10 @@ (jerboa-native-load!)) (defstruct secure-region (pointer size)) + (defstruct %secure-bytevector (region)) + (def secure-bytevector? %secure-bytevector?) + (def %make-secure-bytevector make-%secure-bytevector) + (def secure-bytevector-region %secure-bytevector-region) ;; Use uptr (unsigned pointer-sized integer) for void* returns (def c-jerboa-secure-alloc @@ -34,6 +44,8 @@ (foreign-procedure "jerboa_secure_random_fill" (uptr size_t) int)) (def (secure-alloc size) + (unless (and (integer? size) (>= size 0)) + (error 'secure-alloc "size must be a non-negative integer" size)) (let ([ptr (c-jerboa-secure-alloc size)]) (when (= ptr 0) (error 'secure-alloc "secure allocation failed" size)) @@ -67,6 +79,79 @@ (error 'secure-random-fill "CSPRNG failed")) (void))) + (def (live-region who sbv) + (unless (secure-bytevector? sbv) + (error who "expected secure-bytevector" sbv)) + (let ([region (secure-bytevector-region sbv)]) + (when (= (secure-region-pointer region) 0) + (error who "secure bytevector has been freed")) + region)) + + (def (checked-index who sbv index) + (let ([len (secure-bytevector-length sbv)]) + (unless (and (integer? index) (>= index 0) (< index len)) + (error who "index out of bounds" index len)) + index)) + + (def (checked-byte who byte) + (unless (and (integer? byte) (>= byte 0) (<= byte 255)) + (error who "byte must be an integer in [0,255]" byte)) + byte) + + (def (make-secure-bytevector size . fill-opt) + (let ([sbv (%make-secure-bytevector (secure-alloc size))]) + (when (pair? fill-opt) + (let ([byte (checked-byte 'make-secure-bytevector (car fill-opt))]) + (let loop ([i 0]) + (when (< i size) + (secure-bytevector-set! sbv i byte) + (loop (+ i 1)))))) + sbv)) + + (def (bytevector->secure-bytevector bv) + (unless (bytevector? bv) + (error 'bytevector->secure-bytevector "expected bytevector" bv)) + (let* ([len (bytevector-length bv)] + [sbv (make-secure-bytevector len)]) + (let loop ([i 0]) + (when (< i len) + (secure-bytevector-set! sbv i (bytevector-u8-ref bv i)) + (loop (+ i 1)))) + sbv)) + + (def (secure-bytevector-length sbv) + (secure-region-size (live-region 'secure-bytevector-length sbv))) + + (def (secure-bytevector-ref sbv index) + (let* ([region (live-region 'secure-bytevector-ref sbv)] + [i (checked-index 'secure-bytevector-ref sbv index)]) + (foreign-ref 'unsigned-8 (secure-region-pointer region) i))) + + (def (secure-bytevector-set! sbv index byte) + (let* ([region (live-region 'secure-bytevector-set! sbv)] + [i (checked-index 'secure-bytevector-set! sbv index)] + [b (checked-byte 'secure-bytevector-set! byte)]) + (foreign-set! 'unsigned-8 (secure-region-pointer region) i b)) + (void)) + + (def (secure-bytevector->bytevector sbv) + (let* ([len (secure-bytevector-length sbv)] + [out (make-bytevector len)]) + (let loop ([i 0]) + (when (< i len) + (bytevector-u8-set! out i (secure-bytevector-ref sbv i)) + (loop (+ i 1)))) + out)) + + (def (secure-bytevector-wipe! sbv) + (secure-wipe (live-region 'secure-bytevector-wipe! sbv))) + + (def (secure-bytevector-random-fill! sbv) + (secure-random-fill (live-region 'secure-bytevector-random-fill! sbv))) + + (def (secure-bytevector-free sbv) + (secure-free (live-region 'secure-bytevector-free sbv))) + (define-syntax with-secure-region (syntax-rules () [(_ ([name size] ...) body ...) @@ -77,4 +162,14 @@ (lambda () (secure-free name) ...)))])) + (define-syntax with-secure-bytevector + (syntax-rules () + [(_ ([name size] ...) body ...) + (let ([name (make-secure-bytevector size)] ...) + (dynamic-wind + void + (lambda () body ...) + (lambda () + (secure-bytevector-free name) ...)))])) + ) ;; end library --- a/lib/std/security/secret.ss +++ b/lib/std/security/secret.ss @@ -19,6 +19,7 @@ wipe-bytevector!) (import (chezscheme) + (std crypto secure-mem) (only (jerboa core) def defstruct)) ;; ========== Memory Wiping ========== @@ -41,56 +42,71 @@ (def secret-value %secret-rec-value) (def secret-consumed? %secret-rec-consumed?) - (def (make-secret bv) - ;; Wrap a bytevector as a secret. The bytevector will be zeroed on consume. - (unless (bytevector? bv) - (error 'make-secret "secret must be a bytevector" bv)) - (%make-secret bv #f)) + (def (secret-storage? value) + (or (bytevector? value) (secure-bytevector? value))) + + (def (secret-storage->bytevector value) + (cond + [(secure-bytevector? value) (secure-bytevector->bytevector value)] + [(bytevector? value) + (let ([copy (make-bytevector (bytevector-length value))]) + (bytevector-copy! value 0 copy 0 (bytevector-length value)) + copy)] + [else (error 'secret-storage->bytevector "invalid secret storage")])) + + (def (secret-storage-wipe! value) + (cond + [(secure-bytevector? value) + (secure-bytevector-wipe! value) + (secure-bytevector-free value)] + [(bytevector? value) (wipe-bytevector! value)] + [else (void)])) + + (def (make-secret value) + ;; Wrap bytevector or secure-bytevector storage as a secret. + (unless (secret-storage? value) + (error 'make-secret "secret must be a bytevector or secure-bytevector" value)) + (%make-secret value #f)) (def (secret-use s) - ;; Consume the secret — returns the value and marks as consumed. - ;; After consumption, the original bytevector is wiped. + ;; Consume the secret, returning a bytevector copy and wiping storage. (unless (secret? s) (error 'secret-use "not a secret")) (when (secret-consumed? s) (error 'secret-use "secret already consumed — use-after-wipe")) (%secret-set-consumed! s #t) (let* ([val (%secret-value s)] - ;; Make a copy for the caller — original will be wiped - [copy (let ([bv (make-bytevector (bytevector-length val))]) - (bytevector-copy! val 0 bv 0 (bytevector-length val)) - bv)]) - (wipe-bytevector! val) + [copy (secret-storage->bytevector val)]) + (secret-storage-wipe! val) copy)) (def (secret-peek s) - ;; Read the secret without consuming it. Use with care — the value - ;; is still valid after peek but will be wiped when the secret scope exits. + ;; Read a bytevector copy without consuming the secret. (unless (secret? s) (error 'secret-peek "not a secret")) (when (secret-consumed? s) (error 'secret-peek "secret already consumed")) - (%secret-value s)) + (secret-storage->bytevector (%secret-value s))) ;; ========== Scoped Secret ========== (define-syntax with-secret ;; (with-secret ([name expr] ...) body ...) - ;; Each expr must produce a bytevector which is wrapped as a secret. + ;; Each expr must produce a bytevector or secure-bytevector. ;; On scope exit (normal or exception), all secrets are wiped. (syntax-rules () [(_ ([name expr]) body ...) - (let ([bv expr]) - (unless (bytevector? bv) - (error 'with-secret "expression must produce a bytevector")) - (let ([name (make-secret bv)]) + (let ([storage expr]) + (unless (secret-storage? storage) + (error 'with-secret "expression must produce a bytevector or secure-bytevector")) + (let ([name (make-secret storage)]) (dynamic-wind (lambda () (void)) (lambda () body ...) (lambda () (unless (secret-consumed? name) (%secret-set-consumed! name #t) - (wipe-bytevector! (%secret-value name)))))))] + (secret-storage-wipe! (%secret-value name)))))))] [(_ ([name1 expr1] [name2 expr2] rest ...) body ...) (with-secret ([name1 expr1]) (with-secret ([name2 expr2] rest ...) --- a/tests/test-native-rust.ss +++ b/tests/test-native-rust.ss @@ -360,6 +360,33 @@ (assert-true "r1 is region" (secure-region? r1)) (assert-true "r2 is region" (secure-region? r2))))) +(test "secure-bytevector: checked get/set and copy" + (lambda () + (let ([sbv (make-secure-bytevector 4 0)]) + (secure-bytevector-set! sbv 0 17) + (secure-bytevector-set! sbv 3 255) + (assert-equal "length" 4 (secure-bytevector-length sbv)) + (assert-equal "first" 17 (secure-bytevector-ref sbv 0)) + (assert-equal "last" 255 (secure-bytevector-ref sbv 3)) + (assert-equal "copy" #vu8(17 0 0 255) (secure-bytevector->bytevector sbv)) + (secure-bytevector-free sbv)))) + +(test "secure-bytevector: bounds and byte validation" + (lambda () + (let ([sbv (make-secure-bytevector 2)]) + (assert-error "negative index" (lambda () (secure-bytevector-ref sbv -1))) + (assert-error "end index" (lambda () (secure-bytevector-ref sbv 2))) + (assert-error "bad byte" (lambda () (secure-bytevector-set! sbv 0 256))) + (secure-bytevector-free sbv) + (assert-error "use after free" (lambda () (secure-bytevector-ref sbv 0)))))) + +(test "with-secure-bytevector macro" + (lambda () + (with-secure-bytevector ([sbv 8]) + (assert-true "is secure-bytevector" (secure-bytevector? sbv)) + (secure-bytevector-random-fill! sbv) + (assert-equal "length" 8 (secure-bytevector-length sbv))))) + ;;; ============ Summary ============ (display "\n=== Results ===\n") (display (string-append "Total: " (number->string test-count) "\n")) --- a/tests/test-phase4-safety.ss +++ b/tests/test-phase4-safety.ss @@ -7,6 +7,7 @@ (std security capability) (std security capability-typed) (std security secret) + (std crypto secure-mem) (std security io-intercept) (std actor core) (std actor bounded)) @@ -215,6 +216,15 @@ (error 'test "boom"))) (check (bytevector-u8-ref key-bv 0) => 0)) +;; with-secret accepts secure-bytevector storage and frees it on exit +(let ([sbv (make-secure-bytevector 4)]) + (secure-bytevector-set! sbv 0 7) + (secure-bytevector-set! sbv 3 9) + (let ([result (with-secret ([key sbv]) + (secret-peek key))]) + (check result => #vu8(7 0 0 9)) + (check-error (secure-bytevector-ref sbv 0)))) + ;; wipe-bytevector! utility (let ([bv (bytevector-copy #vu8(1 2 3 4 5))]) (wipe-bytevector! bv)