---
title: "The browser run"
url: https://recursiveai.org/onecrypto/docs/browser/
description: "What the core package does in the browser’s .NET runtime (WebAssembly), how its suite runs in headless Chromium and compares with Node and a native run, and what the browser can’t do."
version: "0.2.0-preview.1"
source: https://github.com/recursive-ai-oss/onecrypto-csharp/blob/fbb5d23f450008e9804a31b8de8e778f1dd26ba9/docs/browser.md
---

# The browser run

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](https://recursiveai.org/onecrypto/docs/availability/) 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:

1. **Headless Chromium.** `serve-and-drive.mjs` serves 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.
2. **Node**, the host the WebAssembly smoke app uses.
3. **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 (`Invalid` with the signature rule in the browser, `Valid` natively).
- Nothing was skipped. The availability section the browser prints equals the `browser-wasm` section of `availability.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.
