https://github.com/tegmentum/openssl-wasm
https://github.com/tegmentum/openssl-wasm
Last synced: about 11 hours ago
JSON representation
- Host: GitHub
- URL: https://github.com/tegmentum/openssl-wasm
- Owner: tegmentum
- License: apache-2.0
- Created: 2026-04-24T18:00:45.000Z (2 months ago)
- Default Branch: main
- Last Pushed: 2026-06-11T14:03:56.000Z (14 days ago)
- Last Synced: 2026-06-11T16:06:55.853Z (14 days ago)
- Language: C
- Size: 437 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
- Security: SECURITY.md
Awesome Lists containing this project
README
# openssl-wasm
[](https://github.com/tegmentum/openssl-wasm/actions/workflows/ci.yml)
[](https://github.com/tegmentum/openssl-wasm/actions/workflows/bench.yml)
[](https://github.com/tegmentum/openssl-wasm/actions/workflows/fuzz.yml)
[](LICENSE)
OpenSSL 3.6.2 compiled as a WebAssembly Component (target `wasm32-wasip2`),
callable from a `wasmtime` host via a curated WIT surface.
The component is **real OpenSSL** — `third_party/openssl` is the upstream
source, built with `wasi-sdk` 33's clang into `libcrypto.a` + `libssl.a`
and statically linked into a wasm component. The C glue in `src/` maps
the WIT exports to OpenSSL's `EVP_*`, `X509_*`, `SSL_*`, etc. The only
Rust in the repo is the test driver in `examples/host`.
## Status
- All 256 WIT exports are implemented against real OpenSSL.
- 106 automated tests in `examples/host/tests/` pass (plus 1
opt-in `#[ignore]` slow-path test), including NIST/RFC/FIPS 197
known-answer vectors.
- TLS client verified live against `example.com:443` with real
certificate-chain validation against the system CA bundle.
## Interfaces (see `wit/`)
| Interface | Covers |
|-----------|--------|
| `error` | ERR_get_error_all, describe, drain |
| `random` | RAND_bytes / RAND_priv_bytes / RAND_add |
| `digest` | MD5/SHA-1/SHA-2/SHA-3/SHAKE/BLAKE2/RIPEMD/SM3 one-shot + streaming + XOF |
| `mac` | HMAC / CMAC / KMAC / Poly1305 / SipHash / GMAC |
| `cipher` | AES (ECB/CBC/CTR/CFB/OFB/XTS/GCM/CCM/OCB), ChaCha20-Poly1305, Camellia, ARIA, 3DES, RC4 |
| `kdf` | PBKDF2, HKDF, scrypt, Argon2, KBKDF, SSKDF, X9.63, TLS1.3-expand-label |
| `pkey` | RSA / RSA-PSS / EC / Ed25519,Ed448 / X25519,X448 / DH (+ named RFC 7919 / RFC 3526 groups) — keygen, sign/verify, encrypt/decrypt, derive, PEM/DER/PKCS#8 |
| `x509` | Certificates / CSRs / CRLs / Store / chain validation / PKCS#12 / CMS |
| `tls` | TLS 1.2 / 1.3 client and server, ALPN, SNI, session tickets, keylog |
| `bignum` | BIGNUM arithmetic |
## Layout
```
.
├── config/50-wasm.conf # OpenSSL Configure target for wasm32-wasip2
├── scripts/
│ ├── install-wasi-sdk.sh # downloads wasi-sdk 33 into .wasi-sdk/
│ └── gen-stubs.sh # regenerates src/stubs.c (should be empty now)
├── third_party/openssl/ # submodule, pinned to openssl-3.6.2
├── wit/ # Component Model interface definitions
│ ├── world.wit # re-exports everything below
│ ├── error.wit random.wit bignum.wit digest.wit mac.wit
│ ├── cipher.wit kdf.wit pkey.wit x509.wit tls.wit
├── src/ # C glue: WIT exports → OpenSSL calls
│ ├── component.c # error, random, digest (+ streaming)
│ ├── mac.c kdf.c bignum.c cipher.c pkey.c
│ ├── x509.c # parse / info / verify / store / PKCS#12 / CMS sign+verify
│ ├── x509_build.c # cert & CSR build-and-sign, PKCS#12 build, CMS encrypt/decrypt
│ ├── tls.c # client + server
│ └── include/ # shared helpers (support.h, algs.h)
├── examples/host/ # Rust wasmtime harness
│ ├── src/lib.rs # Fixture + bindgen
│ ├── src/main.rs # demo runner
│ └── tests/*.rs # 22 suites, 106 tests total (+1 ignored)
├── .github/workflows/ci.yml # build + test on every push
└── Makefile
```
## Prerequisites
- **wasi-sdk 33.** `scripts/install-wasi-sdk.sh` drops it into `.wasi-sdk/`
(gitignored). The Makefile picks that up automatically.
- **wasm-tools** and **wit-bindgen** (both from cargo). Tested with
wasm-tools 1.247 and wit-bindgen 0.48.
- **wasmtime** runtime for the Rust host (45.x tested).
- **cargo / rustc** stable.
## Build
```sh
git submodule update --init --recursive
scripts/install-wasi-sdk.sh # one-time, 200 MB download
make # → build/openssl-component.wasm (~3.8 MB)
make test # runs the full cargo test suite
make run # runs the demo binary against the component
```
Override wasi-sdk location: `WASI_SDK=/path/to/wasi-sdk make`.
## Using the component
From Rust with wasmtime 45:
```rust
wasmtime::component::bindgen!({
world: "openssl",
path: "wit",
imports: { default: async },
exports: { default: async },
});
// … instantiate, then call bindings.openssl_component_digest().call_one_shot(…)
```
See `examples/host/src/main.rs` and `examples/host/tests/*.rs` for
worked examples.
For TLS with certificate verification, grant the component `inherit_network()`,
`allow_ip_name_lookup(true)`, and preopen a directory containing a CA
bundle (`/etc/ssl/cert.pem` on macOS,
`/etc/ssl/certs/ca-certificates.crt` on Debian/Ubuntu).
## Design notes
- **Target:** `wasm32-wasip2`. wasi-sdk 33's clang emits a Component
directly (magic bytes `0d 00 01 00`), so there's no `wasm-tools
component new` step. We link with `-mexec-model=reactor` so the
component exports only our WIT interfaces, no `wasi:cli/run`.
- **Disables at Configure time:** `no-threads`, `no-shared`, `no-dso`,
`no-asm`, `no-engine`, `no-async`, `no-afalgeng`, `no-ktls`,
`no-ui-console`, `no-autoload-config`, `no-module`, `no-apps`,
`no-docs`, `no-quic`, `no-tests`, `OPENSSL_NO_UNIX_SOCK` (wasi-libc's
`sockaddr_un` has no `sun_path`).
- **Sockets:** TLS is routed through wasi-libc's BSD socket shims,
which forward to `wasi:sockets`. The host must grant socket permissions.
- **Filesystem:** CA bundle loading goes through `wasi:filesystem`.
Preopen a directory, then call `store.load-from-file`.
- **RNG:** OpenSSL's DRBG is seeded via `getentropy`, backed by
`wasi:random`.
- **Time:** certificate validity uses `wasi:clocks`.
- **setjmp/longjmp:** wasi-sdk 33 lowers these to Wasm exception
handling. Wasmtime ≥ 40 has this enabled by default.
## Performance
Measured on an Apple M-series, wasmtime 44, opt-level=2 (see
`examples/host/benches/component_vs_native.rs`):
| Operation | Component | Native (libssl) | Ratio |
|-------------------------------|----------------:|----------------:|------:|
| SHA-256 (1 MiB) | 256 MiB/s | 2,384 MiB/s | ~9× |
| AES-256-GCM seal (64 KiB) | 52 MiB/s | 6,208 MiB/s | ~120× |
| Ed25519 sign (1 KiB msg) | 360 µs | 31.5 µs| ~11× |
Interpretation:
- SHA-256 and signatures are within an order of magnitude of native.
- AES-256-GCM is much slower because native uses AES-NI hardware
instructions; the component runs OpenSSL's portable C implementation
(`no-asm` at Configure).
- If you need high AEAD throughput, consider batching or picking
ChaCha20-Poly1305 where both sides are pure software.
Run `cargo bench --manifest-path examples/host/Cargo.toml` to reproduce.
### Wasm SIMD
`make simd=on` turns on `-msimd128` during the OpenSSL build. Measured
effect on this workload:
| Operation | Baseline | `simd=on` | Δ |
|----------------------------|----------:|----------:|-------:|
| SHA-256 (1 MiB) | 226 MiB/s | 242 MiB/s | +7% |
| AES-256-GCM seal (64 KiB) | 41 MiB/s | 41 MiB/s | 0% |
| Ed25519 sign | 391 µs | 387 µs | ~1% |
Conclusion: modest SHA-256 improvement from clang's auto-vectorizer,
no material change on AES or elliptic curves.
### Wasm SIMD AES (experimental)
`make simd_aes=on` replaces OpenSSL's T-table `AES_encrypt` /
`AES_decrypt` with hand-written wasm SIMD implementations via linker
`--wrap`. Coverage: AES-128 / 192 / 256 encrypt and decrypt. Validated
against NIST FIPS-197 and CAVP ECBGFSbox / ECBKeySbox vectors plus a
1000-round differential against native libssl.
**Current state: correctness-complete but slower than T-tables.**
| Operation | T-table | `simd_aes=on`| Δ |
|----------------------------|----------:|-------------:|---------:|
| AES-256-GCM seal (64 KiB) | 56 MiB/s | 34 MiB/s | -40% |
| AES-256-GCM seal (1 KiB) | 1.9 MiB/s| 1.8 MiB/s | -2% |
Why slower: the current S-box is a scalar 256-entry lookup (16 byte
loads per block), surrounded by v128↔memory round-trips. T-tables
avoid this by fusing SubBytes + ShiftRows + MixColumns into a single
4-way-indexed 32-bit table load per byte. Closing the gap requires
replacing the scalar S-box with a vpAES nibble-swizzle
(Hamburg 2009) — ~12 SIMD ops per block instead of 16 scalar
lookups. That work isn't in-tree yet.
Default: **off**. Enable only if you're iterating on the algorithm.
## Security notes
### Constant-time guarantees
Whatever constant-time properties OpenSSL's native implementation
provides are preserved — the glue passes buffers through unmodified
and doesn't perform data-dependent branching. Specifically:
**Believed constant-time in OpenSSL (and preserved here):**
- `digest.one-shot`, `digest.context.update/finish` — block-based,
not data-dependent.
- `mac.*` — HMAC/CMAC/Poly1305/SipHash/GMAC all constant-time in
OpenSSL.
- `cipher.*` for AES via constant-time software implementations when
AES-NI isn't available (OpenSSL's default on wasm).
- `pkey.sign_*` / `pkey.verify_*` / `pkey.derive` for RSA/EC/Ed/X/DH.
- `cipher.open` tag verification (uses `CRYPTO_memcmp`).
**NOT constant-time — avoid with secret inputs:**
- `bignum.to-dec` / `bignum.to-hex` — base conversion is data-dependent.
- `bignum.from-dec` / `bignum.from-hex` — parsing branches on digits.
- `error.describe` — string formatting.
- `pkey.save-private` / `pkey.load-private` — ASN.1 encoding size
leaks key size bits.
- Any RSA/EC operation on a public key that happens to be used for
signing (the library doesn't know your intent).
### Threat model
- The component runs in a wasmtime sandbox. Memory corruption in
OpenSSL cannot escape the sandbox.
- The component assumes the host-provided `wasi:random` is a secure
CSPRNG. If the host is compromised or misconfigured (e.g., feeds
`/dev/zero` as entropy), all key material is compromised.
- The component loads CA bundles via `wasi:filesystem`. A malicious
host can substitute the bundle, so TLS trust is only as strong as
the preopened directory.
- Wasm exception handling is used for `setjmp`/`longjmp` in libssl
error paths. Wasmtime ≥ 40 handles this; older runtimes will trap.
### What this component does NOT protect against
- Timing side channels on the host CPU: the component runs on the host
CPU, so any data-dependent timing in OpenSSL flows through wasm
translation but is still observable.
- The host can read the component's linear memory at any time. Do not
use this for key custody in adversarial-host scenarios.
- The component does not implement side-channel hardening (e.g.
blinding) beyond what OpenSSL provides natively.
## Deliberate WIT-surface omissions
The WIT surface exposes OpenSSL's curated high-level APIs. Low-level
APIs that don't translate cleanly to the Component Model (BIO, ASN.1,
OSSL_PARAM, providers, engines, raw NID lookups) are not exported.
Callers that need those should add more WIT interfaces and glue
functions, rather than try to expose raw pointers.