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.
- 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:443with real certificate-chain validation against the system CA bundle.
| 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 |
.
├── 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
- wasi-sdk 33.
scripts/install-wasi-sdk.shdrops 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.
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 componentOverride wasi-sdk location: WASI_SDK=/path/to/wasi-sdk make.
From Rust with wasmtime 45:
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).
- Target:
wasm32-wasip2. wasi-sdk 33's clang emits a Component directly (magic bytes0d 00 01 00), so there's nowasm-tools component newstep. We link with-mexec-model=reactorso the component exports only our WIT interfaces, nowasi: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'ssockaddr_unhas nosun_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 callstore.load-from-file. - RNG: OpenSSL's DRBG is seeded via
getentropy, backed bywasi: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.
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-asmat 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.
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.
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.
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.derivefor RSA/EC/Ed/X/DH.cipher.opentag verification (usesCRYPTO_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).
- The component runs in a wasmtime sandbox. Memory corruption in OpenSSL cannot escape the sandbox.
- The component assumes the host-provided
wasi:randomis a secure CSPRNG. If the host is compromised or misconfigured (e.g., feeds/dev/zeroas 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/longjmpin libssl error paths. Wasmtime ≥ 40 handles this; older runtimes will trap.
- 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.
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.