| <!doctype html> |
| <!-- Copyright 2026 The Go Authors. All rights reserved. |
| Use of this source code is governed by a BSD-style |
| license that can be found in the LICENSE file. --> |
| <html lang="en"> |
| <meta charset="utf-8"> |
| <title>Go FIPS 140-3 ACVP bridge</title> |
| <style> |
| body { font: 14px/1.5 system-ui, sans-serif; margin: 2rem; max-width: 60rem; } |
| #status { font-weight: 600; margin: 1rem 0; } |
| pre { background: #f4f4f4; padding: 1rem; overflow: auto; max-height: 70vh; white-space: pre-wrap; } |
| </style> |
| <script> |
| // The standard input and output of the module are routed through this fs |
| // shim, installed before wasm_exec.js runs, as it only provides its own |
| // stub when globalThis.fs is not already defined. Reads and writes are |
| // delegated to the active session, see Session below. |
| (() => { |
| const enosys = () => { |
| const err = new Error("not implemented"); |
| err.code = "ENOSYS"; |
| return err; |
| }; |
| globalThis.bridge = { session: null }; |
| globalThis.fs = { |
| constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_DIRECTORY: -1 }, // unused |
| writeSync(fd, buf) { |
| const s = bridge.session; |
| if (!s || (fd !== 1 && fd !== 2)) { |
| // The runtime prints panics and post-exit diagnostics |
| // through writeSync directly, with no error path; surface |
| // them in the console rather than dropping them. fs.write |
| // rejects these cases with ENOSYS before delegating here. |
| console.log(new TextDecoder().decode(buf)); |
| return buf.length; |
| } |
| // buf is a view into the module's memory; copy it before queueing. |
| s.write(fd, buf.slice()); |
| return buf.length; |
| }, |
| write(fd, buf, offset, length, position, callback) { |
| if (offset !== 0 || length !== buf.length || position !== null) { |
| callback(enosys()); |
| return; |
| } |
| if (!bridge.session || (fd !== 1 && fd !== 2)) { |
| callback(enosys()); |
| return; |
| } |
| const n = this.writeSync(fd, buf); |
| callback(null, n); |
| }, |
| read(fd, buffer, offset, length, position, callback) { |
| const s = bridge.session; |
| if (fd !== 0 || position !== null || !s) { |
| callback(enosys()); |
| return; |
| } |
| s.read({ buffer, offset, length, callback }); |
| }, |
| chmod(path, mode, callback) { callback(enosys()); }, |
| chown(path, uid, gid, callback) { callback(enosys()); }, |
| close(fd, callback) { callback(enosys()); }, |
| fchmod(fd, mode, callback) { callback(enosys()); }, |
| fchown(fd, uid, gid, callback) { callback(enosys()); }, |
| fstat(fd, callback) { callback(enosys()); }, |
| fsync(fd, callback) { callback(null); }, |
| ftruncate(fd, length, callback) { callback(enosys()); }, |
| lchown(path, uid, gid, callback) { callback(enosys()); }, |
| link(path, link, callback) { callback(enosys()); }, |
| lstat(path, callback) { callback(enosys()); }, |
| mkdir(path, perm, callback) { callback(enosys()); }, |
| open(path, flags, mode, callback) { callback(enosys()); }, |
| readdir(path, callback) { callback(enosys()); }, |
| readlink(path, callback) { callback(enosys()); }, |
| rename(from, to, callback) { callback(enosys()); }, |
| rmdir(path, callback) { callback(enosys()); }, |
| stat(path, callback) { callback(enosys()); }, |
| symlink(path, link, callback) { callback(enosys()); }, |
| truncate(path, length, callback) { callback(enosys()); }, |
| unlink(path, callback) { callback(enosys()); }, |
| utimes(path, atime, mtime, callback) { callback(enosys()); }, |
| }; |
| })(); |
| </script> |
| <script src="wasm_exec.js"></script> |
| <body> |
| <h1>Go FIPS 140-3 ACVP bridge</h1> |
| <div id="status">loading…</div> |
| <pre id="out"></pre> |
| <script type="module"> |
| const statusEl = document.getElementById("status"); |
| const outEl = document.getElementById("out"); |
| const status = (text) => { statusEl.textContent = text; }; |
| const append = (text) => { |
| outEl.textContent += text; |
| if (outEl.textContent.length > 4 << 20) { |
| outEl.textContent = outEl.textContent.slice(-(2 << 20)); |
| } |
| outEl.scrollTop = outEl.scrollHeight; |
| }; |
| |
| status("fetching module…"); |
| // Keep the raw bytes: go.run needs them to load the fipsrelocinfo section |
| // for the FIPS 140-3 integrity check. |
| const wasmBytes = await (await fetch("module.wasm")).arrayBuffer(); |
| const module = await WebAssembly.compile(wasmBytes); |
| status("module compiled — waiting for first session…"); |
| |
| // Report the browser environment to the server once, as evidence for |
| // operational environment qualification. The host hardware model and CPU |
| // aren't available from JS; capture those on the host running the browser. |
| (async () => { |
| const ua = navigator.userAgent; |
| const info = { userAgent: ua, hardwareConcurrency: navigator.hardwareConcurrency }; |
| if (navigator.deviceMemory) info.deviceMemory = navigator.deviceMemory; |
| let list = null; |
| // userAgentData exists only on Chromium, and only in a secure context |
| // (open the page via http://localhost to get it). |
| if (navigator.userAgentData) { |
| info.brands = navigator.userAgentData.brands; |
| info.uaPlatform = navigator.userAgentData.platform; |
| try { |
| info.highEntropy = await navigator.userAgentData.getHighEntropyValues( |
| ["platform", "platformVersion", "architecture", "bitness", |
| "model", "uaFullVersion", "fullVersionList"]); |
| list = info.highEntropy.fullVersionList; |
| } catch (e) {} |
| list ||= navigator.userAgentData.brands; |
| } |
| try { |
| const gl = document.createElement("canvas").getContext("webgl"); |
| const dbg = gl && gl.getExtension("WEBGL_debug_renderer_info"); |
| if (dbg) { |
| info.webglVendor = gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL); |
| info.webglRenderer = gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL); |
| } |
| } catch (e) {} |
| // Inferred for convenience; the validator should confirm against the raw |
| // fields above, especially the engine, which has no API. |
| let name = "unknown", version = "", m; |
| const brand = list && list.find((x) => !/Not.?A.?Brand|^Chromium$/i.test(x.brand)); |
| if (brand) { name = brand.brand; version = brand.version; } |
| else if ((m = ua.match(/Firefox\/([\d.]+)/))) { name = "Mozilla Firefox"; version = m[1]; } |
| else if (/Safari/.test(ua) && !/Chrome/.test(ua) && (m = ua.match(/Version\/([\d.]+)/))) { name = "Apple Safari"; version = m[1]; } |
| else if ((m = ua.match(/Edg\/([\d.]+)/))) { name = "Microsoft Edge"; version = m[1]; } |
| else if ((m = ua.match(/Chrome\/([\d.]+)/))) { name = "Google Chrome"; version = m[1]; } |
| info.inferredBrowser = `${name} ${version}`.trim(); |
| info.inferredWasmEngine = /Edge|Chrome|Chromium/i.test(name) ? "V8" |
| : /Safari/i.test(name) ? "JavaScriptCore" |
| : /Firefox/i.test(name) ? "SpiderMonkey" : "unknown"; |
| await fetch("clientinfo", { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify(info), |
| }).catch(() => {}); |
| })(); |
| |
| // A Session adapts the fs shim above to the bridge server's HTTP API for |
| // one module execution: standard input is polled from /stdin a chunk at a |
| // time, and output is POSTed to /io in order. |
| class Session { |
| constructor(meta) { |
| this.meta = meta; |
| this.queue = []; // pending stdin chunks (Uint8Array) |
| this.eof = false; |
| this.pending = null; |
| this.ioChain = Promise.resolve(); |
| this.bytesIn = 0; |
| this.bytesOut = 0; |
| // The wrapper protocol is binary; display progress instead of output. |
| this.binary = meta.env["ACVP_WRAPPER"] === "1"; |
| this.decoders = { 1: new TextDecoder(), 2: new TextDecoder() }; |
| } |
| |
| write(fd, bytes) { |
| this.bytesOut += bytes.length; |
| if (this.binary) { |
| this.progress(); |
| } else { |
| append(this.decoders[fd].decode(bytes, { stream: true })); |
| } |
| this.ioChain = this.ioChain |
| .then(() => fetch(`io?s=${this.meta.id}&fd=${fd}`, { method: "POST", body: bytes })) |
| .catch(() => {}); |
| } |
| |
| read(req) { |
| this.pending = req; |
| this.service(); |
| } |
| |
| // service completes the pending read if input is available. The Go |
| // runtime's fs.read callback may be held indefinitely; it is invoked |
| // when the /stdin stream delivers bytes or ends. |
| service() { |
| const req = this.pending; |
| if (!req) return; |
| if (this.queue.length === 0) { |
| if (this.eof) { |
| this.pending = null; |
| req.callback(null, 0); |
| } |
| return; |
| } |
| const src = this.queue[0]; |
| const n = Math.min(src.length, req.length); |
| req.buffer.set(src.subarray(0, n), req.offset); |
| if (n === src.length) { |
| this.queue.shift(); |
| } else { |
| this.queue[0] = src.subarray(n); |
| } |
| this.pending = null; |
| this.bytesIn += n; |
| if (this.binary) this.progress(); |
| req.callback(null, n); |
| } |
| |
| progress() { |
| const now = performance.now(); |
| if (this.lastProgress && now - this.lastProgress < 100) return; |
| this.lastProgress = now; |
| status(`session ${this.meta.id}: ${this.meta.argv.join(" ")} — ` + |
| `${this.bytesIn} bytes in, ${this.bytesOut} bytes out`); |
| } |
| } |
| |
| async function runSession(meta) { |
| const s = new Session(meta); |
| bridge.session = s; |
| const argv = meta.argv.join(" "); |
| status(`session ${meta.id}: ${argv}`); |
| if (!s.binary) append(`=== session ${meta.id}: ${argv} ===\n`); |
| |
| (async () => { |
| // Poll for standard input a chunk at a time. Each response completes, |
| // so Safari delivers it in full; the body of a single long-lived |
| // streaming response is buffered by Safari and never surfaced, which |
| // starves the module of input that has already arrived. |
| try { |
| for (;;) { |
| const resp = await fetch(`stdin?s=${meta.id}`); |
| if (!resp.ok || resp.headers.get("Stdin-Eof")) break; |
| const buf = new Uint8Array(await resp.arrayBuffer()); |
| if (buf.length) { |
| s.queue.push(buf); |
| s.service(); |
| } |
| } |
| } catch (err) { |
| console.error("stdin poll:", err); |
| } |
| s.eof = true; |
| s.service(); |
| })(); |
| |
| const go = new Go(); |
| go.argv = meta.argv.slice(); |
| go.env = meta.env; |
| let exitCode = 0; |
| go.exit = (code) => { exitCode = code; }; |
| try { |
| // A corrupt session is served, at a per-session URL, a module whose |
| // integrity checksum has been overwritten; compile it fresh instead of |
| // reusing the cached good module, so its load-time check fails. |
| let sessionModule = module, sessionBytes = wasmBytes; |
| if (meta.corrupt) { |
| sessionBytes = await (await fetch(`module.wasm?s=${meta.id}`)).arrayBuffer(); |
| sessionModule = await WebAssembly.compile(sessionBytes); |
| } |
| const instance = await WebAssembly.instantiate(sessionModule, go.importObject); |
| await go.run(instance, sessionModule, sessionBytes); |
| } catch (err) { |
| console.error(err); |
| exitCode = 2; |
| const msg = new TextEncoder().encode(`browserbridge: module crashed: ${err}\n`); |
| if (!s.binary) append(`module crashed: ${err}\n`); |
| s.ioChain = s.ioChain |
| .then(() => fetch(`io?s=${meta.id}&fd=2`, { method: "POST", body: msg })) |
| .catch(() => {}); |
| } |
| // The Go runtime can leave scheduler wake-up timers pending at exit; a |
| // stale one firing later would call _resume on the exited instance and |
| // throw. Pending timers can't run before this microtask continuation. |
| for (const t of go._scheduledTimeouts.values()) clearTimeout(t); |
| go._scheduledTimeouts.clear(); |
| await s.ioChain; |
| await fetch(`exit?s=${meta.id}`, { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ code: exitCode }), |
| }).catch(() => {}); |
| bridge.session = null; |
| if (!s.binary) append(`=== session ${meta.id}: exit ${exitCode} ===\n`); |
| status(`session ${meta.id} done (exit ${exitCode}, ` + |
| `${s.bytesIn} bytes in, ${s.bytesOut} bytes out) — waiting for next session…`); |
| } |
| |
| for (;;) { |
| try { |
| const resp = await fetch("session"); |
| if (resp.status === 204) continue; // long-poll timeout |
| if (!resp.ok) throw new Error(`HTTP ${resp.status}`); |
| await runSession(await resp.json()); |
| } catch (err) { |
| status(`bridge server unreachable (${err}) — retrying…`); |
| await new Promise((resolve) => setTimeout(resolve, 2000)); |
| } |
| } |
| </script> |