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 into +foo_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.
+ +