Tesseras

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

What comes next

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.