Phase 4: Verify Without Installing Anything
2026-02-15
Trust shouldn't require installing software. If someone sends you a tessera — a
bundle of preserved memories — you should be able to verify it's genuine and
unmodified without downloading an app, creating an account, or trusting a
server. That's what tesseras-wasm delivers: drag a tessera archive into a web
page, and cryptographic verification happens entirely in your browser.
What was built
tesseras-wasm — A Rust crate that compiles to WebAssembly via wasm-pack,
exposing four stateless functions to JavaScript. The crate depends on
tesseras-core for manifest parsing and calls cryptographic primitives directly
(blake3, ed25519-dalek) rather than depending on tesseras-crypto, which pulls
in C-based post-quantum libraries that don't compile to
wasm32-unknown-unknown.
parse_manifest takes raw MANIFEST bytes (UTF-8 plain text, not MessagePack),
delegates to tesseras_core::manifest::Manifest::parse(), and returns a JSON
string with the creator's Ed25519 public key, signature file paths, and a list
of files with their expected BLAKE3 hashes, sizes, and MIME types. Internal
structs (ManifestJson, CreatorPubkey, SignatureFiles, FileEntry) are
serialized with serde_json. The ML-DSA public key and signature file fields are
present in the JSON contract but set to null — ready for when post-quantum
signing is implemented on the native side.
hash_blake3 computes a BLAKE3 hash of arbitrary bytes and returns a
64-character hex string. It's called once per file in the tessera to verify
integrity against the MANIFEST.
verify_ed25519 takes a message, a 64-byte signature, and a 32-byte public key,
constructs an ed25519_dalek::VerifyingKey, and returns whether the signature
is valid. Length validation returns descriptive errors ("Ed25519 public key must
be 32 bytes") rather than panicking.
verify_ml_dsa is a stub that returns an error explaining ML-DSA verification
is not yet available. This is deliberate: the ml-dsa crate on crates.io is
v0.1.0-rc.7 (pre-release), and tesseras-crypto uses pqcrypto-dilithium
(C-based CRYSTALS-Dilithium) which is byte-incompatible with FIPS 204 ML-DSA.
Both sides need to use the same pure Rust implementation before
cross-verification works. Ed25519 verification is sufficient — every tessera is
Ed25519-signed.
All four functions use a two-layer pattern for testability: inner functions
return Result<T, String> and are tested natively, while thin #[wasm_bindgen]
wrappers convert errors to JsError. This avoids JsError::new() panicking on
non-WASM targets during testing.
The compiled WASM binary is 109 KB raw and 44 KB gzipped — well under the 200 KB
budget. wasm-opt applies -Oz optimization after wasm-pack builds with
opt-level = "z", LTO, and single codegen unit.
@tesseras/verify — A TypeScript npm package (crates/tesseras-wasm/js/)
that orchestrates browser-side verification. The public API is a single
function:
async function verifyTessera(
archive: Uint8Array,
onProgress?: (current: number, total: number, file: string) => void
): Promise<VerificationResult>
The VerificationResult type provides everything a UI needs: overall validity,
tessera hash, creator public keys, signature status (valid/invalid/missing for
both Ed25519 and ML-DSA), per-file integrity results with expected and actual
hashes, a list of unexpected files not in the MANIFEST, and an errors array.
Archive unpacking (unpack.ts) handles three formats: gzip-compressed tar
(detected by \x1f\x8b magic bytes, decompressed with fflate then parsed as
tar), ZIP (PK\x03\x04 magic, unpacked with fflate's unzipSync), and raw tar
(ustar at offset 257). A normalizePath function strips the leading
tessera-<hash>/ prefix so internal paths match MANIFEST entries.
Verification runs in a Web Worker (worker.ts) to keep the UI thread
responsive. The worker initializes the WASM module, unpacks the archive, parses
the MANIFEST, verifies the Ed25519 signature against the creator's public key,
then hashes each file with BLAKE3 and compares against expected values. Progress
messages stream back to the main thread after each file. If any signature is
invalid, verification stops early without hashing files — failing fast on the
most critical check.
The archive is transferred to the worker with zero-copy
(worker.postMessage({ type: "verify", archive }, [archive.buffer])) to avoid
duplicating potentially large tessera files in memory.
Build pipeline — Three new justfile targets: wasm-build runs wasm-pack
with --target web --release and optimizes with wasm-opt; wasm-size reports
raw and gzipped binary size; test-wasm runs the native test suite.
Tests — 9 native unit tests cover BLAKE3 hashing (empty input, known value),
Ed25519 verification (valid signature, invalid signature, wrong key, bad key
length), and MANIFEST parsing (valid manifest, invalid UTF-8, garbage input). 3
WASM integration tests run in headless Chrome via
wasm-pack test --headless --chrome, verifying that hash_blake3,
verify_ed25519, and parse_manifest work correctly when compiled to
wasm32-unknown-unknown.
Architecture decisions
- No tesseras-crypto dependency: the WASM crate calls blake3 and
ed25519-dalek directly.
tesseras-cryptodepends onpqcrypto-kyber(C-based ML-KEM via pqcrypto-traits) which requires a C compiler toolchain and doesn't target wasm32. By depending only on pure Rust crates, the WASM build has zero C dependencies and compiles cleanly to WebAssembly. - ML-DSA deferred, not faked: rather than silently skipping post-quantum
verification, the stub returns an explicit error. This ensures that if a
tessera contains an ML-DSA signature, the verification result will report
ml_dsa: "missing"rather than pretending it was checked. The JS orchestrator handles this gracefully — a tessera is valid if Ed25519 passes and ML-DSA is missing (not yet implemented on either side). - Inner function pattern:
JsErrorcannot be constructed on non-WASM targets (it panics). Splitting each function intofoo_inner() -> Result<T, String>andfoo() -> Result<T, JsError>lets the native test suite exercise all logic without touching JavaScript types. The WASM integration tests in headless Chrome test the full#[wasm_bindgen]surface. - Web Worker isolation: cryptographic operations (especially BLAKE3 over
large media files) can take hundreds of milliseconds. Running in a Worker
prevents UI jank. The streaming progress protocol
(
{ type: "progress", current, total, file }) lets the UI show a progress bar during verification of tesseras with many files. - Zero-copy transfer:
archive.bufferis transferred to the Worker, not copied. For a 50 MB tessera archive, this avoids doubling memory usage during verification. - Plain text MANIFEST, not MessagePack: the WASM crate parses the same
plain-text MANIFEST format as the CLI. This is by design — the MANIFEST is the
tessera's Rosetta Stone, readable by anyone with a text editor. The
rmp-serdedependency in the Cargo.toml is not used and will be removed.
What comes next
- Phase 4: Resilience and Scale — OS packaging (Alpine, Arch, Debian, FreeBSD, OpenBSD), CI on SourceHut and GitHub Actions, security audits, browser-based tessera explorer at tesseras.net using @tesseras/verify
- Phase 5: Exploration and Culture — Public tessera browser by era/location/theme/language, institutional curation, genealogy integration, physical media export (M-DISC, microfilm, acid-free paper with QR)
Verification no longer requires trust in software. A tessera archive dropped into a browser is verified with the same cryptographic rigor as the CLI — same BLAKE3 hashes, same Ed25519 signatures, same MANIFEST parser. The difference is that now anyone can do it.