OneCrypto's core package runs in the browser's .NET runtime (browser-wasm). That runtime offers hashes and HMAC (and, in the
.NET 10 version tested, PBKDF2 over HMAC) but no AES, no RSA, no ECDSA or ECDH and no SHA-3. Parsing, inspecting and verifying have to work
there anyway, through the verify-only managed fallbacks and our own curves; everything the runtime cannot do must fail with
AlgorithmNotSupportedException naming the algorithm and platform, never with the runtime's own PlatformNotSupportedException.
availability.md lists what each platform provides; its browser-wasm section is checked against the running browser.
What runs
tests/BrowserSuite is one browser-wasm app. Its cases print CASE <area> <id> <pass|fail|skip> <verdict> <detail> lines. The status says
whether the case met its expectation. The verdict is the observed outcome in words that don't depend on the host, so the same case can
be compared across hosts.
| Area | What it checks |
|---|---|
platform | the provider the dispatch picked (managed fallback in the browser), the runtime's own PBKDF2 and AES, hashes and HMAC; the availability section is compared with docs/availability.md |
parse-fixtures, parse-limbo-certs | certificates (generated fixtures, and every certificate of the sampled x509-limbo cases), a CRL, OCSP responses, a time-stamp request, PEM framing; the signature of each fixture certificate, CRL and OCSP response is verified |
private-keys, load, ssh | PKCS#8, Crypto.Load on every fixture (PEM, DER, PKCS#12, OpenSSH), OpenSSH public keys to SubjectPublicKeyInfo |
kdf | PBKDF2-HMAC-SHA-256, scrypt and Argon2id against the RFC vectors |
protected-inputs | which password-protected inputs open (see below) |
verify-wycheproof | RSA PKCS#1 v1.5 and PSS, ECDSA (P-256, P-384, P-521, brainpool, DER and P1363), Ed25519 and Ed448 on a sample of Wycheproof vectors |
cms | CMS SignedData (RSA PKCS#1 v1.5, RSA-PSS, P-256, P-384, P-521, Ed25519), signer chains validated against a trust anchor, and a tampered copy of each |
timestamp | an RFC 3161 response against its request, with the authority's certificate validated, and against a different request |
validate-limbo | path validation on a sample of x509-limbo testcases (the first four of each category) |
unsupported | AES key wrap, OCB, EAX, PBES2 decryption and PKCS#12 opening with AES or legacy ciphers: typed refusal in the browser, success natively |
The fixtures are generated by tools/gen-browser-fixtures.sh (OpenSSL and ssh-keygen; keys are throwaway) and committed under
testdata/browser. tools/gen-browser-data.py bundles them with samples of the fetched Wycheproof and x509-limbo suites into one JSON
file, embedded in the app at build time; the vector suites themselves are never committed. A run without the suites tests the fixtures
only.
How it runs
tests/BrowserSuite/run.sh <output dir> publishes the app, then runs the same published files three ways and compares them:
- Headless Chromium.
serve-and-drive.mjsserves the published folder on a loopback port, starts the pinned Chrome for Testing headless shell (tools/fetch-chrome-headless.sh, version and SHA-256 pinned in the script) with a throwaway profile and waits for the page to post its output lines back. A crash, an early exit or no answer within 15 minutes is a failure. - Node, the host the WebAssembly smoke app uses.
- Natively on the .NET runtime, and again natively with the managed verifiers forced (
force-fallbacks), which runs the browser's verification code on the same vectors where a fast platform could hide a defect.
compare.py requires: no failed case anywhere; Chromium and Node identical on every case; Chromium and native identical on every area
that tests the same code on the same inputs. The areas that probe the platform itself (platform, unsupported, protected-inputs,
load, private-keys) are listed, not judged, and so are the three ML-DSA limbo testcases, whose certificates a platform without ML-DSA cannot
verify (the validator refuses the path, with Signature as the deciding rule). The report is report.md in the output folder.
Results
Latest run: Chrome for Testing headless shell 156.0.8078.80 (linux64), .NET 10.0.14 browser-wasm, Node 26.13.0, on a Linux x64 worker, with the Wycheproof and x509-limbo suites fetched (40 Wycheproof vectors per key group and the first four x509-limbo testcases of each category). Cases per area, pass / fail / skip:
| area | chromium (pass/fail/skip) | node (pass/fail/skip) | native (pass/fail/skip) | native, fallbacks forced (pass/fail/skip) |
|---|---|---|---|---|
| platform | 7/0/0 | 7/0/0 | 7/0/0 | 7/0/0 |
| parse-fixtures | 12/0/0 | 12/0/0 | 12/0/0 | 12/0/0 |
| private-keys | 2/0/0 | 2/0/0 | 2/0/0 | 2/0/0 |
| parse-limbo-certs | 778/0/0 | 778/0/0 | 778/0/0 | 778/0/0 |
| load | 30/0/0 | 30/0/0 | 30/0/0 | 30/0/0 |
| kdf | 3/0/0 | 3/0/0 | 3/0/0 | 3/0/0 |
| protected-inputs | 12/0/0 | 12/0/0 | 12/0/0 | 12/0/0 |
| verify-wycheproof | 920/0/0 | 920/0/0 | 920/0/0 | 920/0/0 |
| ssh | 3/0/0 | 3/0/0 | 3/0/0 | 3/0/0 |
| cms | 12/0/0 | 12/0/0 | 12/0/0 | 12/0/0 |
| timestamp | 2/0/0 | 2/0/0 | 2/0/0 | 2/0/0 |
| validate-limbo | 153/0/0 | 153/0/0 | 153/0/0 | 153/0/0 |
| unsupported | 6/0/0 | 6/0/0 | 6/0/0 | 6/0/0 |
- Chromium and Node agree on all 1,940 shared cases (status and verdict); Chromium and the native run agree on every judged area. The only
differences are the platform probes and the three ML-DSA limbo testcases (
Invalidwith the signature rule in the browser,Validnatively). - Nothing was skipped. The availability section the browser prints equals the
browser-wasmsection ofavailability.md. - The managed verifiers (forced natively) and the browser give the same verdicts on all 920 Wycheproof vectors; no vector fails anywhere.
- The suite takes about a minute in Chromium once the app is published.
A run without the vector suites (the hosted CI job) covers the committed fixtures only: certificates, CRL, OCSP, CMS, time-stamp, keys, OpenSSH, KDFs, and the typed refusals.
What the browser cannot do
| Needs | In the browser | Because |
|---|---|---|
| AES (PBES2 keys, PKCS#12 with AES, OCB, EAX, AES key wrap, AES-GCM/CCM in CMS and OpenSSH) | AlgorithmNotSupportedException | the runtime has no AES; the library never implements it |
| triple DES and RC2 (legacy PKCS#12 and PEM, read only) | AlgorithmNotSupportedException | the runtime has no symmetric ciphers |
| RSA, ECDSA or ECDH key objects, private-key operations, deriving an EC public key from a private key | AlgorithmNotSupportedException | no managed private-key operations, on any platform |
| ML-KEM, ML-DSA, SLH-DSA | AlgorithmNotSupportedException | no fallback; ML-DSA certificates fail path validation with the signature rule |
Password-protected inputs. PBKDF2 is available in the browser runtime tested (.NET 10.0.14), and so are scrypt and Argon2 (ours), so key
derivation is not the limit; the cipher is. A PKCS#12 file's MAC can be checked with VerifyMac (HMAC and the PKCS#12 key derivation only),
and the container can be parsed. Crypto.Load and Open of any file with AES-, 3DES- or RC2-encrypted content (a PBES2 ENCRYPTED PRIVATE KEY,
a PKCS#12 written by current OpenSSL or by the legacy provider) report Unsupported; a PKCS#12 with no encryption and no MAC opens through
Pkcs12File.TryOpen(requireMac: false) but Crypto.Load refuses it for want of a MAC.
Reproducing
tools/gen-browser-fixtures.sh # only to regenerate the committed fixtures ONECRYPTO_VECTORS=<folder with wycheproof/ and x509-limbo/> tests/BrowserSuite/run.sh <output dir>
Needs the wasm-tools workload, Node 22 or newer and python3. ONECRYPTO_BROWSER points at an installed headless shell instead of fetching one.