Remove last OpenSSL dependency: vault crypto now uses ring (Rust)
ober
df45e21dd1c579d317e2ce82d7fa5524d52e008b
--- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ scheme --libdirs lib --script your-file.ss | `(std os temporaries)` | Temporary file creation | | `(std os signal)` | POSIX signal constants + handlers | | `(std os fdio)` | File descriptor I/O | -| `(std crypto digest)` | MD5/SHA hashing via openssl | +| `(std crypto digest)` | MD5/SHA hashing via ring (Rust) | | `(std srfi srfi-13)` | SRFI-13 string operations | | `(std srfi srfi-19)` | Date/time handling | --- a/docs/native-rust.md +++ b/docs/native-rust.md @@ -46,8 +46,8 @@ Replacing them with a single Rust shared library (`libjerboa_native.so`) provide | Library | Module(s) | Purpose | |----------------------------|---------------------------------------------------------------|----------------------------------------------------------------------------------------------| -| **libcrypto.so** (OpenSSL) | `std/crypto/native`, `std/crypto/aead`, `std/crypto/password` | Digests (MD5, SHA-*), HMAC, AES-GCM, PBKDF2, CSPRNG (`RAND_bytes`), constant-time comparison | -| **libssl.so** (OpenSSL) | `std/net/tls` | TLS 1.2/1.3 protocol | +| ~~**libcrypto.so** (OpenSSL)~~ | ~~`std/crypto/native`~~ | **Eliminated** — Digests, HMAC, AES-GCM, PBKDF2, CSPRNG now via ring in `libjerboa_native` | +| ~~**libssl.so** (OpenSSL)~~ | ~~`std/net/tls`~~ | **Eliminated** — TLS 1.2/1.3 now via rustls in `libjerboa_native` | ### Optional Wrappers (chez-* repos) new file mode 100644 --- /dev/null +++ b/docs/remove-crypto.md @@ -0,0 +1,274 @@ +# Remove Last OpenSSL (libcrypto) Dependency + +## Status: Implemented + +## Goal + +Eliminate the last C OpenSSL dependency (`libcrypto.a`) from all jerboa-shell +build targets (musl, macOS, FreeBSD, Android). After this, the only native +code linked is `libjerboa_native.a` (Rust) and platform libc. + +## Background + +All other C dependencies have already been replaced: + +| C Library | Rust Replacement | Status | +|-----------|-----------------|--------| +| libssl (OpenSSL TLS) | rustls | Done | +| libpcre2 | regex crate | Done | +| libz | flate2 | Done | +| libsqlite3 | rusqlite (bundled) | Done | +| **libcrypto (OpenSSL)** | **ring** | **This task** | + +The sole remaining consumer is `vault-stage/chez/vault/crypto.sls` in +jerboa-shell, which calls 13 OpenSSL C functions for: + +1. **CSPRNG** — `RAND_bytes` +2. **PBKDF2** — `PKCS5_PBKDF2_HMAC` + `EVP_sha256` +3. **AES-256-GCM** — `EVP_CIPHER_CTX_new/free`, `EVP_aes_256_gcm`, + `EVP_EncryptInit_ex/Update/Final_ex`, `EVP_DecryptInit_ex/Update/Final_ex`, + `EVP_CIPHER_CTX_ctrl` + +## Key Finding: Rust FFI Already Exists + +All three operations are already implemented in `jerboa-native-rs/src/crypto.rs`: + +| Vault operation | Rust FFI function | ring API | +|-----------------|-------------------|----------| +| `RAND_bytes` | `jerboa_random_bytes(output, len)` → 0 on success | `ring::rand::SystemRandom` | +| `PKCS5_PBKDF2_HMAC` | `jerboa_pbkdf2_derive(pw, pw_len, salt, salt_len, iters, out, out_len)` → 0 on success | `ring::pbkdf2::PBKDF2_HMAC_SHA256` | +| AES-256-GCM seal | `jerboa_aead_seal(key, 32, nonce, 12, pt, pt_len, aad, aad_len, out, out_max, &out_len)` → 0 on success | `ring::aead::AES_256_GCM` | +| AES-256-GCM open | `jerboa_aead_open(key, 32, nonce, 12, ct, ct_len, aad, aad_len, out, out_max, &out_len)` → 0 on success | `ring::aead::AES_256_GCM` | + +No new Rust code is needed. This is purely a Scheme rewrite + build cleanup. + +## Plan + +### Phase 1: Rewrite vault/crypto.sls (jerboa-shell) + +Rewrite `vault-stage/chez/vault/crypto.sls` to call the existing Rust FFI +instead of OpenSSL. The public API is unchanged: + +``` +vault-rand-bytes, vault-pbkdf2, vault-block-key, +vault-encrypt-block, vault-decrypt-block, +vault-encrypt-small, vault-decrypt-small +``` + +#### Mapping (current → new) + +**vault-rand-bytes:** +```scheme +;; OLD: (c-rand-bytes bv n) where c-rand-bytes = foreign-procedure "RAND_bytes" +;; NEW: +(define c-rand-bytes + (foreign-procedure "jerboa_random_bytes" (u8* size_t) int)) + +(define (vault-rand-bytes n) + (let ([bv (make-bytevector n 0)]) + (let ([r (c-rand-bytes bv n)]) + (unless (= r 0) (error 'vault-rand-bytes "jerboa_random_bytes failed")) + bv))) +;; NOTE: return code changes — RAND_bytes returns 1 on success, jerboa_random_bytes returns 0 +``` + +**vault-pbkdf2:** +```scheme +;; OLD: 3 FFI calls (EVP_sha256 + PKCS5_PBKDF2_HMAC) +;; NEW: 1 FFI call +(define c-pbkdf2 + (foreign-procedure "jerboa_pbkdf2_derive" + (u8* size_t u8* size_t unsigned-32 u8* size_t) int)) + +(define (vault-pbkdf2 password-bv salt-bv iterations key-len) + (let ([out (make-bytevector key-len 0)]) + (let ([r (c-pbkdf2 + password-bv (bytevector-length password-bv) + salt-bv (bytevector-length salt-bv) + iterations + out key-len)]) + (unless (= r 0) (error 'vault-pbkdf2 "jerboa_pbkdf2_derive failed")) + out))) +``` + +**gcm-encrypt / gcm-decrypt:** +```scheme +;; OLD: 7+ FFI calls per operation (CTX_new, init, update, final, ctrl, CTX_free) +;; NEW: 1 FFI call each +(define c-aead-seal + (foreign-procedure "jerboa_aead_seal" + (u8* size_t u8* size_t u8* size_t u8* size_t u8* size_t u8*) int)) + +(define c-aead-open + (foreign-procedure "jerboa_aead_open" + (u8* size_t u8* size_t u8* size_t u8* size_t u8* size_t u8*) int)) + +(define (gcm-encrypt key plaintext-bv) + (let* ([pt-len (bytevector-length plaintext-bv)] + [nonce (vault-rand-bytes BLOCK-NONCE-LEN)] ;; 12 bytes + [out-max (+ pt-len 16)] ;; ciphertext + tag + [out (make-bytevector out-max 0)] + [out-len-bv (make-bytevector 8 0)]) ;; size_t output + (let ([r (c-aead-seal key 32 nonce 12 + plaintext-bv pt-len + (make-bytevector 0) 0 ;; no AAD + out out-max out-len-bv)]) + (unless (= r 0) (error 'vault-encrypt "jerboa_aead_seal failed")) + ;; Reassemble: nonce[12] || ciphertext[N] || tag[16] (same wire format) + (let ([result (make-bytevector (+ BLOCK-NONCE-LEN out-max))]) + (bytevector-copy! nonce 0 result 0 BLOCK-NONCE-LEN) + (bytevector-copy! out 0 result BLOCK-NONCE-LEN out-max) + result)))) + +(define (gcm-decrypt key ciphertext-bv) + (let* ([total (bytevector-length ciphertext-bv)] + [ct-len (- total BLOCK-NONCE-LEN)]) ;; nonce is prepended + (if (< ct-len 16) #f ;; too short for tag + (let* ([nonce (bv-sub ciphertext-bv 0 BLOCK-NONCE-LEN)] + [ct+tag (bv-sub ciphertext-bv BLOCK-NONCE-LEN ct-len)] + [out (make-bytevector ct-len 0)] + [out-len-bv (make-bytevector 8 0)]) + (let ([r (c-aead-open key 32 nonce 12 + ct+tag ct-len + (make-bytevector 0) 0 ;; no AAD + out ct-len out-len-bv)]) + (if (= r 0) + ;; ring returns plaintext in out[0..pt-len] where pt-len = ct-len - 16 + (bv-sub out 0 (- ct-len 16)) + #f)))))) +``` + +**Wire format compatibility:** The nonce||ciphertext||tag layout stays +identical. Existing vault data decrypts without migration. + +**Critical detail:** `jerboa_aead_seal` outputs ciphertext||tag (no nonce). +The Scheme layer prepends the nonce, matching the current format exactly. +`jerboa_aead_open` expects ciphertext||tag as input (nonce separate), +matching how we strip the nonce before calling it. + +### Phase 2: Update build scripts — remove libcrypto + +Remove all OpenSSL/libcrypto references from the 4 build scripts. + +#### 2a. Remove OpenSSL symbol whitelists + +Each build script registers 13 OpenSSL symbols for the Chez FFI. Remove them: + +| File | Lines | What | +|------|-------|------| +| `build-jsh-musl.ss` | ~1285-1292 | `vault-crypto-symbols` inline list | +| `build-jsh-macos.ss` | ~1114-1120 | `vault-crypto-symbols` define | +| `build-jsh-freebsd.ss` | ~1077-1083 | `vault-crypto-symbols` define | + +The Rust functions (`jerboa_random_bytes`, `jerboa_pbkdf2_derive`, +`jerboa_aead_seal`, `jerboa_aead_open`) are already exported by +`libjerboa_native.a` and already in the jerboa-native symbol whitelists. +Verify this for each platform. + +#### 2b. Remove libcrypto.a from linker flags + +| File | Lines | Current | Change | +|------|-------|---------|--------| +| `build-jsh-musl.ss` | ~1501-1518 | Finds and links `/usr/lib/.../libcrypto.a` | Remove libcrypto from link list | +| `build-jsh-macos.ss` | ~1585-1591 | Links `brew-ssl-prefix/lib/libcrypto.a` | Remove libcrypto from link list | +| `build-jsh-freebsd.ss` | ~1454-1456 | Links `/usr/lib/libcrypto.a` | Remove libcrypto from link list | +| `build-jsh-android.sh` | ~195, 265 | References `libcrypto.so` for vault | Remove libcrypto from staging | + +#### 2c. Remove glibc-compat shim (musl only) + +`build-jsh-musl.ss` lines ~1423-1465 generate a `glibc-compat.c` shim that +stubs out glibc symbols referenced by `libcrypto.a` (`__stack_chk_fail_local`, +`__fprintf_chk`, OSSL_ASYNC stubs, ARM64 `getauxval`/`__environ`). Delete the +entire shim generation — it exists solely for libcrypto. + +#### 2d. Remove `openssl-include-dir` (macOS only) + +`build-jsh-macos.ss` lines ~222-224 locate Homebrew OpenSSL include paths, +used when compiling the (now-deleted) chez-ssl shim. If no other code +references this, remove it. + +#### 2e. Remove load-shared-object exclusion (Android) + +`build-jsh-android.sh` line ~270 excludes `vault/crypto.sls` from the +load-shared-object patching pass because it loads `libcrypto.so`. After the +rewrite, vault/crypto.sls no longer calls `load-shared-object`, so this +exclusion can be removed. + +### Phase 3: Clean up Dockerfiles + +| File | Line | Current | Change | +|------|------|---------|--------| +| `Dockerfile` | 28 | `libssl-dev \` | Remove (check no other consumer first) | +| `Dockerfile.android` | 28 | `libssl-dev \` | Remove (check no other consumer first) | + +**Caution:** `libssl-dev` pulls in both `libssl` and `libcrypto` headers + +static libs. Verify that nothing else in the Docker build needs them. The +`openssl` CLI tool (used in `Makefile:162` for cert generation) comes from +the `openssl` package, not `libssl-dev`, so check if `openssl` needs to be +added separately if `libssl-dev` is removed. + +### Phase 4: Clean up remaining references + +- `jerboa/README.md:108` — update `(std crypto digest)` description to say + "via ring" instead of "via openssl" +- `jerboa/docs/native-rust.md:49-50` — update status table to show libcrypto + as fully replaced +- `jerboa/docs/security-reference.md:352` — update to say "Via Rust ring + (recommended)" and remove "(legacy)" qualifier from OpenSSL mention +- `jerboa-shell/android.md:38` — remove `libcrypto.so` from linked + libraries list + +### Phase 5: Test + +1. **Roundtrip test:** Create a vault, write data, read it back — verify + encrypt/decrypt works with the Rust backend +2. **Cross-compatibility test:** Encrypt data with old (OpenSSL) binary, + decrypt with new (ring) binary — verify wire format compatibility +3. **Build all platforms:** `make jsh-musl`, `make macos`, `make jsh-freebsd`, + `make android` — verify no unresolved symbols +4. **Binary size check:** Confirm binary shrinks (libcrypto.a is ~3-5 MB) +5. **Run existing vault tests** if any exist + +## Risks + +1. **Wire format mismatch** — Low risk. Both OpenSSL and ring use standard + AES-256-GCM with 12-byte nonce and 16-byte tag. The Scheme layer controls + the nonce||ciphertext||tag layout, which is preserved. + +2. **PBKDF2 output difference** — No risk. PBKDF2-HMAC-SHA256 is a + deterministic standard (RFC 2898). Same inputs produce same outputs + regardless of implementation. + +3. **size_t ABI** — The Rust FFI uses `size_t` (Chez `size_t` or `unsigned-64` + on 64-bit). Verify the `foreign-procedure` declarations match the C ABI + on all platforms (LP64 Linux/macOS/FreeBSD and Android aarch64). + +4. **out-len parameter** — `jerboa_aead_seal/open` write the output length + to a `*mut usize` pointer. In Scheme, this needs a bytevector large enough + for a `size_t` (8 bytes on 64-bit). Use `bytevector-u64-native-ref` to + read it back if needed, or ignore it when the length is deterministic + (pt_len + 16 for seal, ct_len - 16 for open). + +## Files Modified (Summary) + +### jerboa-shell (the main work) +- `vault-stage/chez/vault/crypto.sls` — rewrite to use Rust FFI +- `build-jsh-musl.ss` — remove OpenSSL symbols, libcrypto link, glibc-compat shim +- `build-jsh-macos.ss` — remove OpenSSL symbols, libcrypto link, openssl-include-dir +- `build-jsh-freebsd.ss` — remove OpenSSL symbols, libcrypto link +- `build-jsh-android.sh` — remove libcrypto references, vault exclusion +- `build-jsh-android.ss` — remove libcrypto from link +- `Dockerfile` — remove libssl-dev +- `Dockerfile.android` — remove libssl-dev +- `android.md` — update linked libraries list + +### jerboa (docs only) +- `README.md` — update crypto digest description +- `docs/native-rust.md` — mark libcrypto as fully replaced +- `docs/security-reference.md` — update crypto backend description + +### No changes needed +- `jerboa-native-rs/` — all required Rust FFI functions already exist +- `Cargo.toml` — ring is already a dependency +- `test-mux-tcp.sh` — uses `openssl` CLI for test cert generation (unrelated to libcrypto linkage) +- `Makefile:162` — uses `openssl` CLI for embedded cert generation (unrelated) --- a/docs/security-reference.md +++ b/docs/security-reference.md @@ -349,7 +349,7 @@ Reads directly from `/dev/urandom`. Never uses Chez's `(random N)` for security ### Digests -- `(std crypto digest)` and `(std crypto native)` -Via OpenSSL libcrypto FFI (legacy) or Rust ring (recommended): MD5, SHA-1, SHA-256, SHA-384, SHA-512. +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)`