feat(P3.1): WASM verifier + zero-network /verify drop-zone

sdk/go/cmd/attesto-verify-wasm compiles the offline verification functions
(receipt, inclusion, checkpoint root, completeness) — and nothing else —
to WebAssembly, exported on a global attestoVerify object.
scripts/build_wasm_verifier.sh prefers TinyGo and falls back to Go stdlib
(current build: stdlib, 5.9 MB; the <4 MB target applies when TinyGo is in
the toolchain). docs-site /verify is a drag-drop page that verifies
receipts entirely in the browser against a user-pinned witness key.

Verified, both wired into CI as a new wasm-verifier job:
- scripts/wasm_verifier_smoke.mjs loads the wasm in Node with no network
  and reproduces all 19 sdk-parity corpus cases (receipts + inclusion +
  checkpoint-root + completeness) — the same corpus gating the three SDKs;
- the smoke also asserts the /verify page is zero-network: its only fetch
  is the same-origin wasm asset and no script references an absolute URL.

wasm + page hashed into the release manifest; docs-hub contract green
(shared chrome + content rules).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-06-12 09:25:43 +02:00
parent 2276f4da09
commit 4a2d8645b0

View File

@@ -0,0 +1,97 @@
//go:build js && wasm
// attesto-verify-wasm [P3.1] — the verifier-only WebAssembly build.
//
// Exposes the offline verification functions (and nothing else: no client,
// no CLI, no network capability is ever invoked) on a global
// `attestoVerify` object for the docs-site /verify drop-zone. Every
// function takes JSON strings and returns a JSON string, so the JS side
// stays a thin shell.
package main
import (
"encoding/json"
"syscall/js"
attesto "go.attesto.eu/sdk"
)
func respond(value any) string {
raw, err := json.Marshal(value)
if err != nil {
return `{"ok":false,"problems":["internal: response marshal failed"]}`
}
return string(raw)
}
func fail(problem string) string {
return respond(map[string]any{"ok": false, "problems": []string{problem}})
}
// verifyReceipt(receiptJSON, publicKeyHex) -> VerifyReport JSON
func verifyReceipt(_ js.Value, args []js.Value) any {
if len(args) != 2 {
return fail("usage: verifyReceipt(receiptJSON, publicKeyHex)")
}
var receipt attesto.SignedReceipt
if err := json.Unmarshal([]byte(args[0].String()), &receipt); err != nil {
return fail("receipt is not valid JSON: " + err.Error())
}
return respond(attesto.VerifyReceiptOffline(receipt, args[1].String()))
}
// verifyInclusion(leafHash, proofJSON, rootHash) -> {ok, problems}
func verifyInclusion(_ js.Value, args []js.Value) any {
if len(args) != 3 {
return fail("usage: verifyInclusion(leafHash, proofJSON, rootHash)")
}
var proof []attesto.InclusionStep
if err := json.Unmarshal([]byte(args[1].String()), &proof); err != nil {
return fail("proof is not valid JSON: " + err.Error())
}
ok, err := attesto.VerifyInclusionProof(args[0].String(), proof, args[2].String())
if err != nil {
return fail(err.Error())
}
return respond(map[string]any{"ok": ok, "problems": []string{}})
}
// verifyCheckpointRoot(windowHashesJSON, expectedRoot) -> {ok, problems}
func verifyCheckpointRoot(_ js.Value, args []js.Value) any {
if len(args) != 2 {
return fail("usage: verifyCheckpointRoot(windowHashesJSON, expectedRoot)")
}
var hashes []string
if err := json.Unmarshal([]byte(args[0].String()), &hashes); err != nil {
return fail("windowHashes is not valid JSON: " + err.Error())
}
ok, err := attesto.VerifyCheckpointRoot(hashes, args[1].String())
if err != nil {
return fail(err.Error())
}
return respond(map[string]any{"ok": ok, "problems": []string{}})
}
// verifyCompleteness(eventsJSON, fromSeqNo, toSeqNo) -> VerifyReport JSON
func verifyCompleteness(_ js.Value, args []js.Value) any {
if len(args) != 3 {
return fail("usage: verifyCompleteness(eventsJSON, fromSeqNo, toSeqNo)")
}
var events []map[string]any
if err := json.Unmarshal([]byte(args[0].String()), &events); err != nil {
return fail("events is not valid JSON: " + err.Error())
}
return respond(attesto.VerifyCompleteness(events, args[1].Int(), args[2].Int()))
}
func main() {
exports := js.Global().Get("Object").New()
exports.Set("verifyReceipt", js.FuncOf(verifyReceipt))
exports.Set("verifyInclusion", js.FuncOf(verifyInclusion))
exports.Set("verifyCheckpointRoot", js.FuncOf(verifyCheckpointRoot))
exports.Set("verifyCompleteness", js.FuncOf(verifyCompleteness))
exports.Set("sdkVersion", attesto.SDKVersion)
js.Global().Set("attestoVerify", exports)
// Keep the runtime alive for calls from JS.
select {}
}