Spike: wasm32 crypto and WebAuthn on Workers - #23
Conversation
Workspace scaffold plus the spike crate skeleton. webauthn-rs master at be696b79800bd1953df78e87d0215571733cc26f still has openssl/openssl-sys as hard dependencies of webauthn-rs-core and webauthn-attestation-ca; the wasm32 build fails in openssl-sys' build script and default-features = false changes nothing. Evidence commands in spikes/wasm-auth/README.md.
Registration and assertion verification on webauthn-rs-proto wire types, ciborium/coset for CBOR+COSE, p256 + sha2 for the crypto: rpId hash, UP/UV flags, client-data checks, credential-id match, signature-counter advance, ES256 over authenticatorData || sha256(clientDataJSON). Attestation format none accepted, others stored unverified. fixtures/passkey.json is recorded by examples/mint_passkey.rs, a software authenticator with deterministic RFC 6979 signing (no hardware authenticator on the spike machine). Native tests cover tampered signature, wrong challenge, wrong origin and replay. The fixture verifies inside wrangler dev (wasm) via POST /q2/verify-passkey; the tampered variant returns verified:false.
…er dev src/port.rs is the harness HttpClient port copied verbatim; the adapter implements it over worker::Fetch with fixture interception on Google's token and JWKS endpoints. oauth2's AsyncHttpClient is implemented over the port (OidcHttpClient). In wrangler dev: discovery runs live against the real Google document (issuer accounts.google.com, real token endpoint); token exchange and RS256 ID-token verification (signature, issuer, audience, expiry, nonce) run from fixtures minted by examples/mint_id_token.rs because no Google client credential exists on this machine. Send-future finding: the port's async_trait requires Send futures while worker handles are !Send; the adapter bridges via spawn_local plus a Send oneshot channel. Recorded for the harness adapter.
POST /q4/argon2?m&t&p&n and GET /q4/argon2/matrix run Argon2id v19 in the Worker and report internal (Date.now) and external wall time. Measured 8 parameter sets in local workerd: 118-931 ms per 3x (hash+verify), external 0.12-0.93 s. Recommendation: m=19456 KiB, t=2, p=1 (OWASP minimal, ~40 ms per hash+verify locally); free tier 10 ms CPU cannot fit a single verify. Deployed staging not measured (no Cloudflare credentials). docs/adr/0100 records all four answers, the adopted passkey path (webauthn-rs-proto + ciborium/coset + p256/sha2, sizes and risks), the openidconnect adapter and its Send-future finding, and the dependency proof: openssl/openssl-sys/reqwest/mio absent from the wasm tree, tokio only via the worker SDK.
|
Reviewed and verified independently: The ADR is unusually honest about what was proven versus recorded, which is what a spike is for. Three things to carry forward, none blocking: 1. The 2. The 3. The paid-plan finding is the one with consequences outside this repo. Argon2id at any sane parameters cannot fit the free tier's 10 ms CPU limit, so password login requires the paid Workers plan. Recorded here so it is not rediscovered during #19. Merging: a spike's bar is a reproducible result and an honest write-up, and this clears both. The production follow-ups (challenge storage in D1 with delete-on-use, per-client origin allow-list, hardware-authenticator fixture, one live Google code exchange, JWKS caching) belong to #12, #13 and #14 and are listed in the ADR's risk sections. |
Closes #2. Time-boxed spike, four questions answered with evidence. Not to be merged (spike output only).
Q1 — does webauthn-rs main build for wasm32? No.
master@be696b7(2026-06-02, still versioned 0.5.5):cargo build --target wasm32-unknown-unknownfails inopenssl-sys v0.9.117;cargo tree -i openssl-sysshows openssl as a hard dependency ofwebauthn-rs-coreandwebauthn-attestation-ca. No feature disables it;default-features = falsedoes not help.Q2 — pure-Rust verification path. Adopted for passkeys.
~340 lines on
webauthn-rs-proto 0.5.5(wire types) +ciborium/coset(CBOR/COSE) +p256/sha2(ES256), scoped to registration + assertion with attestationnoneaccepted and other formats stored unverified. Proven inwrangler dev: the recorded passkey fixture verifies (verified:true, sign_count:2); tampered signature, wrong challenge, wrong origin and replay are rejected (native tests + wasm run). The fixture is a software authenticator (no hardware key on the spike machine) with real ES256 math and spec-shaped bytes — limitation recorded in the ADR.Q3 — openidconnect through the harness HttpClient port. Works.
openidconnect 4.0.1/oauth2 5.0.0build to wasm32;oauth2'sAsyncHttpClientis implemented over the port (copied verbatim from the harness), which runs overworker::Fetch. Insidewrangler dev: discovery runs live against the real Google document; token exchange and RS256 ID-token verification (signature, issuer, audience, expiry, nonce) run from recorded fixtures — no Google client credential exists on this machine, so recorded in the ADR. Integration finding for the harness: the port's Send-future bound requires aspawn_local+ oneshot bridge overworker::Fetch.Q4 — argon2 on Workers. Measured; m=19456/t=2/p=1 recommended.
8 Argon2id sets in
wrangler dev(local workerd): 118–931 ms per 3×(hash+verify), external wall 0.12–0.93 s (full table in the ADR). Recommendation m=19456 KiB, t=2, p=1 (~40 ms hash+verify locally), three orders of magnitude under the paid 30 s CPU limit; free-tier 10 ms CPU cannot fit a single verify. Deployed staging not measured (no Cloudflare credentials) — re-measure in auth-password.Hard constraint
cargo tree --target wasm32-unknown-unknown:openssl,openssl-sys,reqwest,mioabsent from the tree;tokiopresent only via theworkerSDK itself, zero from auth dependencies.DoD all green:
cargo fmt --all --check,cargo clippy --all-targets -- -D warnings,cargo test(10 tests),cargo build --target wasm32-unknown-unknown,worker-build --release. One commit per question,spike(auth):prefixed. Full answers and risks:docs/adr/0100-wasm-crypto-and-webauthn.md; how to reproduce:spikes/wasm-auth/README.md.Closes #2