Skip to content

feat(#6263): offload RSA crypto to host Web Crypto API for WASM - #6265

Closed
fullsend-ai-coder[bot] wants to merge 1 commit into
mainfrom
agent/6263-offload-rsa-crypto-wasm
Closed

feat(#6263): offload RSA crypto to host Web Crypto API for WASM#6265
fullsend-ai-coder[bot] wants to merge 1 commit into
mainfrom
agent/6263-offload-rsa-crypto-wasm

Conversation

@fullsend-ai-coder

Copy link
Copy Markdown
Contributor

Summary

Offloads RSA crypto operations (OIDC JWT verification and GitHub App JWT signing) from the Go WASM binary to the Cloudflare Worker host via Web Crypto API (crypto.subtle). This removes crypto/rsa, crypto/x509, math/big, and encoding/pem from the WASM dependency tree — the largest source of mint-specific binary bloat (~24 packages).

Changes

  • crypto_native.go (//go:build !js): Platform-specific RSA functions using Go's standard crypto — verifyRS256Signature, signRS256WithPEM, parseRSAPublicKey (moved from jwks_verifier.go)
  • crypto_js.go (//go:build js): Same function signatures, delegating to JavaScript callbacks registered via RegisterHostCrypto during WASM init
  • jwks_verifier.go: JWKS cache stores raw jwkKey entries instead of *rsa.PublicKey; verification calls verifyRS256Signature (platform-dispatched)
  • github.go: GenerateAppJWT delegates signing to signRS256WithPEM (platform-dispatched)
  • cmd/mint-wasm/main.go: initMint now accepts 5 arguments (adds verifyRS256Callback, signRS256Callback)
  • Worker index.ts: New createVerifyRS256Callback and createSignRS256Callback using crypto.subtle.importKey + verify/sign
  • Embed sync: crypto_native.go added to embeddedMintFiles, crypto_js.go added to gcfSkip; all .embed copies synced

Design

Follows the existing host-bridge pattern (HostFetchDoer, HostPEMAccessor): WASM-only code in crypto_js.go calls registered JS callbacks via awaitPromise; native code in crypto_native.go uses Go's standard library. No behavior change for GCF or standalone mint — they link crypto_native.go via the !js build tag.

Testing

  • go test -race ./... passes in internal/mintcore/ (all existing tests + new crypto_native_test.go)
  • TestEmbeddedMintSource_MatchesOriginal passes (embed sync verified)
  • lint-mint-embed-sync hook passes
  • New test file covers verifyRS256Signature (valid + invalid), signRS256WithPEM (PKCS1 + PKCS8 + invalid PEM)
  • WASM binary size measurement deferred to CI (make wasm-build reports before/after)

Closes #6263

Post-script verification

  • Branch is not main/master (agent/6263-offload-rsa-crypto-wasm)
  • Secret scan passed (gitleaks — dc87d7d6484555ba91661a3e94f126287a27246f..HEAD)
  • PR body secret scan passed (gitleaks — no-git)

Extract RSA signature verification and signing into platform-specific
functions (verifyRS256Signature, signRS256WithPEM) behind build tags.

On native builds (//go:build !js), these use Go's crypto/rsa and
math/big as before. On WASM builds (//go:build js), they delegate
to JavaScript callbacks registered by the Cloudflare Worker host,
using the Web Crypto API (crypto.subtle.verify/sign).

This removes crypto/rsa, crypto/x509, math/big, and encoding/pem
from the WASM dependency tree — the largest source of mint-specific
bloat (~24 packages). The JWKS cache now stores raw jwkKey entries
instead of *rsa.PublicKey, deferring key parsing to verification
time (native) or passing JWK JSON to the host (WASM).

Changes:
- New crypto_native.go: verifyRS256Signature, signRS256WithPEM,
  parseRSAPublicKey (moved from jwks_verifier.go)
- New crypto_js.go: same signatures, delegating to host callbacks
  via RegisterHostCrypto + awaitPromise
- jwks_verifier.go: cache stores jwkKey, calls verifyRS256Signature
- github.go: GenerateAppJWT calls signRS256WithPEM
- cmd/mint-wasm: initMint accepts 5 args (adds verifyRS256, signRS256)
- Worker index.ts: createVerifyRS256Callback, createSignRS256Callback
  using crypto.subtle.importKey + verify/sign
- Embed sync: crypto_native.go in embeddedMintFiles, crypto_js.go in
  gcfSkip; all .embed copies synced

GCF and standalone mint paths unchanged — they use crypto_native.go.

Note: pre-commit could not run (sandbox network policy blocks
git fetch for hook setup). The post-script runs an authoritative
pre-commit on the runner. lint-mint-embed-sync passes locally.

Closes #6263
@fullsend-ai-coder
fullsend-ai-coder Bot requested a review from a team as a code owner August 16, 2026 19:29
@fullsend-ai-coder fullsend-ai-coder Bot added the ready-for-review Agent PR ready for human review label Aug 16, 2026
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 16, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 7:30 PM UTC · Completed 7:46 PM UTC

Commit: fc5cc4c · View workflow run →

@fullsend-ai-review

Copy link
Copy Markdown

Review

Findings

High

  • [api-contract] internal/dispatch/cf/workersrc/src/index.ts:302createSignRS256Callback uses crypto.subtle.importKey("pkcs8", ...) exclusively, but GitHub App private keys are historically issued in PKCS1 format (-----BEGIN RSA PRIVATE KEY-----). The Web Crypto API's pkcs8 format only accepts PKCS8-encoded DER data (-----BEGIN PRIVATE KEY-----). When given PKCS1 DER, importKey will throw a DataError. The native Go path (signRS256WithPEM in crypto_native.go) correctly handles both PKCS1 and PKCS8 with a try/fallback; the JS WASM host path does not, creating a behavioral asymmetry that would break token signing for GitHub Apps with PKCS1 keys.
    Remediation: Either convert PKCS1 DER to PKCS8 DER before calling importKey (prepend the PKCS8 header bytes to wrap the PKCS1 RSAPrivateKey structure), or attempt importKey with pkcs8 first, catch the error, and retry after wrapping the DER in a PKCS8 envelope.

Medium

  • [stale-doc] docs/contributing/go-code.md:11 — States "The adapter handles I/O only (Worker secrets, host fetch, Fetch Request/Response mapping); all mint logic stays in Go." After this PR the adapter also handles RSA crypto operations via createVerifyRS256Callback() and createSignRS256Callback(). The phrase "handles I/O only" is now factually incorrect.
    Remediation: Update to mention crypto callbacks alongside I/O callbacks.

  • [stale-doc] docs/contributing/go-code.md:20 — States "The three current entries are fetch_js.go and pem_js.go (Worker-only, //go:build js) and file_pem.go (standalone-mint-only, //go:build !js)." This PR adds crypto_js.go to gcfSkip, making it four entries.
    Remediation: Update count and enumeration to include crypto_js.go.

Low

  • [key-size-validation-bypass] internal/mintcore/crypto_js.go:51 — WASM build path delegates verification to the host without enforcing the 2048-bit minimum RSA key size that the native path enforces in parseRSAPublicKey. Previously refreshKeys() enforced this at cache-population time for all builds. Real-world exploitability is negligible since the JWKS source is GitHub's OIDC provider.

  • [stale-reference] cmd/mint-wasm/main.go:4 — Package-level doc comment still shows old 3-argument signature mintcoreInitMint(configJSON, fetchCallback, pemCallback). The function-level doc on line 40 was correctly updated to the 5-argument signature.

  • [trust-boundary] internal/mintcore/crypto_js.go:81signRS256WithPEM passes raw PEM private key data across the WASM-to-JS boundary. No net-new exposure since the PEM data was already accessible to JS via createPemCallback.

  • [base64url-padding] internal/dispatch/cf/workersrc/src/index.ts:255 — Base64url-to-binary conversion uses atob() without adding padding. Correct for Cloudflare Workers V8 but could be hardened with explicit padding.

  • [code-organization] internal/mintcore/crypto_native_test.go:33 — Tautological comment // crypto.SHA256 = crypto.SHA256 is a development leftover.


Labels: PR modifies mint crypto offload between WASM and Cloudflare Worker host


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the review comment for full details.


Note: The following inline comments could not be posted on the diff (GitHub returned 422) and are included here instead:

  • internal/dispatch/cf/workersrc/src/index.ts:302: [high] api-contract

createSignRS256Callback uses crypto.subtle.importKey("pkcs8", ...) exclusively, but GitHub App private keys are historically issued in PKCS1 format (-----BEGIN RSA PRIVATE KEY-----). The Web Crypto API's pkcs8 format only accepts PKCS8-encoded DER data. When given PKCS1 DER, importKey will throw a DataError. The native Go path (signRS256WithPEM in crypto_native.go) correctly handles both PKCS1 and PKCS8 with a try/fallback; the JS WASM host path does not, creating a behavioral asymmetry that would break token signing for GitHub Apps with PKCS1 keys.

Suggested fix: Either convert PKCS1 DER to PKCS8 DER before calling importKey (prepend the PKCS8 header bytes to wrap the PKCS1 RSAPrivateKey structure), or attempt importKey with pkcs8 first, catch the error, and retry after wrapping the DER in a PKCS8 envelope.

  • internal/mintcore/crypto_js.go:51: [low] key-size-validation-bypass

WASM build path delegates verification to the host without enforcing the 2048-bit minimum RSA key size that the native path enforces in parseRSAPublicKey. Previously refreshKeys() enforced this at cache-population time for all builds. Real-world exploitability is negligible since the JWKS source is GitHub's OIDC provider.

  • cmd/mint-wasm/main.go (file-level): Line 4 · [low] stale-reference

Package-level doc comment still shows old 3-argument signature mintcoreInitMint(configJSON, fetchCallback, pemCallback). The function-level doc on line 40 was correctly updated to the 5-argument signature.

Suggested fix: Update line 4 to show the new 5-argument signature.

  • internal/mintcore/crypto_js.go:81: [low] trust-boundary

signRS256WithPEM passes raw PEM private key data across the WASM-to-JS boundary. No net-new exposure since the PEM data was already accessible to JS via createPemCallback.

  • internal/dispatch/cf/workersrc/src/index.ts:255: [low] base64url-padding

Base64url-to-binary conversion uses atob() without adding padding. Correct for Cloudflare Workers V8 but could be hardened with explicit padding.

  • internal/mintcore/crypto_native_test.go:33: [low] code-organization

Tautological comment '// crypto.SHA256 = crypto.SHA256' is a development leftover.

Suggested fix: Remove the comment.

@ifireball

Copy link
Copy Markdown
Member

Superseded: the #6262 size regression was fixed by the VerifierFactory change on PR #6255 (~1.92 MB gzip). Crypto offload to Web Crypto is not needed at this time.

@fullsend-ai-retro

fullsend-ai-retro Bot commented Aug 16, 2026

Copy link
Copy Markdown

🤖 Finished Retro · ✅ Success · Started 9:41 PM UTC · Completed 9:55 PM UTC

Commit: fc5cc4c · View workflow run →

@fullsend-ai-retro

Copy link
Copy Markdown

Retro: PR #6265 — offload RSA crypto to host Web Crypto API for WASM

Outcome: Closed without merging. The underlying WASM size regression (issue #6262) was resolved by a VerifierFactory pattern fix in PR #6255 (~1.92 MB gzip), making the crypto offload spike unnecessary.

Timeline

  1. Issue mintcore: spike — offload RSA crypto to Worker Web Crypto and measure WASM size savings #6263 created by ifireball (18:25 UTC) — research spike to offload RSA crypto to Web Crypto API
  2. Triage agent (18:25–18:30) — auto-triaged successfully in ~5 min
  3. Code agent attempt 1 (18:34–18:59) — Failed. Post-code pre-commit caught two issues: detect-private-key false positive on TypeScript crypto code in index.ts, and lint-mint-embed-sync failure from a leftover coverage.out file in internal/mintcore/
  4. Human retry (19:10) — /fs-code redo without forgetting to sync the mint embed files!
  5. Code agent attempt 2 (19:11–19:29) — Succeeded, PR feat(#6263): offload RSA crypto to host Web Crypto API for WASM #6265 created
  6. Review agent (19:29–19:46) — Found a genuine high-severity PKCS1 vs PKCS8 behavioral asymmetry, plus stale docs and minor issues. Requested changes.
  7. Fix agent (19:46) — Failed immediately at eligibility check — misidentified bot-authored PR as human-authored
  8. Human closed PR (21:39) — Superseded by VerifierFactory fix

What went well

  • Review quality was strong. The review agent correctly identified a real high-severity bug: the JS host path only supports PKCS8 private keys via crypto.subtle.importKey("pkcs8", ...), while the native Go path handles both PKCS1 and PKCS8 with a try/fallback. This would have broken token signing for GitHub Apps with PKCS1 keys. The stale-doc findings were also valid.
  • Triage was fast and accurate (~5 min).
  • Second code attempt succeeded after human guidance.

What could go better

  • First code failure cost ~25 min of compute + human intervention. The coverage.out artifact left by go test -coverprofile tripped the embed-sync lint. The lint-mint-embed-sync script iterates all files in internal/mintcore/ except *_test.go but doesn't skip non-source artifacts. One proposal below addresses this.
  • Fix agent bot-author misidentification is a known bug. Evidence from this PR: the fix agent's eligibility check ran gh pr view --json author --jq '.author.login' which returns app/fullsend-ai-coder (GraphQL format), but the [bot]$ regex only matches the REST format fullsend-ai-coder[bot]. Already tracked in Fix agent eligibility check misidentifies bot-authored PRs as human-authored #5536 and Fix agent eligibility gate misclassifies bot-authored PRs as human-authored #1569. This PR provides another concrete data point — the fix agent was correctly dispatched (routing uses webhook event context with REST format) but then immediately failed its own eligibility re-check (which re-queries via gh pr view and gets GraphQL format).
  • Pre-commit can't run in sandbox due to network restrictions. The agent explicitly noted this in logs. Already extensively tracked across agents#281, fullsend#3746, fullsend#5263, fullsend#1997, fullsend#1866.
  • detect-private-key false positive on crypto-handling TypeScript code. Already tracked in fullsend#1996.

Proposals filed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component/mint Token mint and cross-boundary credentials ready-for-review Agent PR ready for human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mintcore: spike — offload RSA crypto to Worker Web Crypto and measure WASM size savings

1 participant