# VerifyBundle — Normative Specification

**Status:** Draft pending context-fresh review (gate). **profile_version:** `1` · **canon_profile:** `aga-sep-vectors/2` · **algorithm:** `Ed25519-SHA256-JCS`.

**Authority & scope.** This document is the normative protocol VerifyBundle implements; the implementation follows this spec, not the reverse. It **refines the SPEC layer only** — it does not reopen the frozen AGA v2 SEP crypto contract (the upstream `CANONICAL_CONSTRUCTION_v2.md`, an AGA-internal document **not** vendored into this repo — §2 restates everything CORE verification needs, so this SPEC is self-contained per P0.1) or the proven Phase-0 code. On conflict: the Master Concept wins on product questions; the **AGA v2 freeze + its frozen vectors win on crypto**; the built code is ground truth for current state.

**Conventions.** MUST / MUST NOT / SHOULD / MAY per RFC 2119. "Lower-hex" = `[0-9a-f]`. `canon` and `sha256` are defined in §2. All multi-field hashing uses `canon`.

---

## §0 — Purpose and the sovereign-bundle principle

VerifyBundle produces a **portable cryptographic evidence container** and the **verifier** for it. The verifier is the product; the sealing interface is a producer of artifacts for that verifier.

- **P0.1 (sovereignty).** A bundle MUST be verifiable with **zero live dependencies** — no VerifyBundle server, no AGA, no Arweave, no company. The math + the bundle + this spec are the only requirements.
- **P0.2 (CORE vs DISCLOSURE).** **CORE verification** (integrity, authorship, time, inclusion — §4.1) MUST require only Ed25519 (RFC 8032) + SHA-256 (FIPS 180-4) + `canon`. **DISCLOSURE reveal** (§3) additionally requires Argon2id + HKDF-SHA256 + XChaCha20-Poly1305. A bundle with no private fields MUST be fully verifiable by CORE alone.
- **P0.3 (offline verifier is the reference).** The downloadable single-file offline verifier is the reference implementation; the hosted verifier is a thin wrapper over the **same** integrity engine. They MUST NOT diverge.
- **P0.4 (never break issued artifacts).** Any conformant verifier MUST verify every bundle whose `profile_version` ≤ its own, forever (§5).

---

## §1 — Bundle and seal format

### §1.1 Bundle envelope (normative field set)
```
Bundle = {
  algorithm:        "Ed25519-SHA256-JCS",      // construction tag (inherited from AGA SEP)
  profile:          "vb-seal/1",               // consumer-seal profile discriminator
  profile_version:  1,                         // monotonically increasing; see §5
  canon_profile:    "aga-sep-vectors/2",       // honest canonicalization descriptor (NOT "RFC 8785 JCS")
  public_key:       <64-hex Ed25519>,          // the seal's signing key (Phase 1: ephemeral)
  receipts:         [ Seal ],                  // Phase 1: exactly one Seal (checkpoint-of-one)
  merkle_proofs:    [ MerkleProof ],           // one per receipt; see §2
  checkpoint:       SignedCheckpoint           // MANDATORY; see §2
}
```
`profile`, `profile_version`, and `canon_profile` are informational to the CORE crypto verifier (which is profile-agnostic) and authoritative to the profile-aware layer (§4.3). At the envelope these are **unsigned string** descriptors that mirror the signed seal fields (§1.2); the seal-level copies inside `canon` carry the authority. The schematic `profile_version` shown above denotes the string `"1"`.

### §1.2 Seal object (the leaf; the consumer artifact)
```
Seal = {
  schema:            "vb-seal/1",
  schema_version:    "1",                              // seal field-schema version (R1.2.3)
  profile_version:   "1",                              // §5 forward-compat axis (R1.2.3)
  protocol_version:  "1",                              // overall protocol version (R1.2.3)
  algorithm:         "Ed25519-SHA256-JCS",
  subject:           { bytes_hash, metadata_hash }    // single file/note/fields
                   | { manifest_hash },                // multi-file (see §1.4)
  public_fields:     { <string>: <string>, ... },      // disclosed in clear
  disclosure:        [ DisclosureField ],              // private fields; see §3
  claimed_context:   [ { label, value, claimed:"true", verified:"false" }, ... ],
  timestamps:        { local: <iso8601>,               // device clock at seal time
                       universal: TimeAttestation? },   // server-signed; see §1.3
  kdf_params:        Argon2idParams?,                  // present iff `disclosure` is non-empty (§3.5)
  timestamp:         <iso8601>,                        // mirrors timestamps.local; drives chain ordering
  ephemeral_public_key: <64-hex>,                      // == bundle.public_key in Phase 1
  public_key:        <64-hex>,
  previous_receipt_hash: "",                           // genesis (Phase 1 single seal)
  signature:         <128-hex>                         // Ed25519 over canon(Seal without "signature")
}
```
- **R1.2.1 (string-only).** Every value reachable inside a `Seal` MUST be a string, EXCEPT the values inside `checkpoint` (`leaf_count` is a number there, matching AGA SEP exactly). This guarantees `canon` never reaches the number-serialization path where the AGA spec text and the frozen vectors disagree. An implementation MUST reject (fail to seal) any non-string primitive in a `Seal` (this is the existing `assertCanonSafe` guard).
- **R1.2.2 (NFC).** See §2.3.
- **R1.2.3 (version fields).** A Seal carries three descriptive version strings inside the signed `canon` form — `schema_version`, `profile_version`, `protocol_version` (all `"1"` in Phase 1) — plus the `schema` discriminator (`"vb-seal/1"`). They are inherited from the AGA SEP envelope shape (AGA receipts carry `schema_version`) and are frozen into the profile_version-1 golden vector (§5 / R5.1). Of these, **only `profile_version` is load-bearing**: it is the forward-compatibility axis of §5 — a verifier MUST verify any bundle whose `profile_version ≤` its own and MUST NOT gate on `schema_version` or `protocol_version` (reserved for future differentiation). All are strings per R1.2.1 and are covered by the seal signature; an implementation MUST emit all four (the frozen vector is the authority on their Phase-1 values).

### §1.3 Time attestation (dual-time)
```
TimeAttestation = { value: <iso8601>, kid: <string>, sig: <128-hex> }
```
- **R1.3.1.** `sig` MUST be an Ed25519 signature, over `canon({value, kid})`, by the server time key identified by `kid`, verifiable by the **same** CORE Ed25519 path. The verifier MUST surface the universal time as *attested by the time service*, distinct from the device `local` time.
- **R1.3.2 (degraded mode).** If the time service is unreachable at seal time, the seal MAY omit `timestamps.universal` and proceed with `local` only. The UI and the verifier output MUST then label the seal **"local device time only — not independently time-attested."** Availability of the anonymous tier MUST NOT depend on the time service.
- **R1.3.3 (key story).** The time service holds exactly **one** server key per `kid`, pinned and rotatable; `kid` identifies which key signed. This is the only server-held key in Phase 1, and it signs **time only** — never a user's seal. Honest-claim copy MUST be "we never hold your signing key or your data," never "we hold no keys."

### §1.4 Subjects and the multi-file manifest
- **R1.4.1.** Single subject: `bytes_hash = sha256(file-bytes)` (raw bytes; see §2.3), `metadata_hash = sha256(canon(metadata))`.
- **R1.4.2 (manifest, locked).** Multi-file subject: `manifest_hash = sha256(canon( sorted-array-of {name, bytes_hash} ))`, where the array is sorted ascending by NFC-normalized `name` in **UTF-8 byte order** (equivalently, Unicode code-point order — **not** a JS UTF-16 `<` comparison, which diverges for non-BMP names such as emoji and would break cross-implementation reproduction), with **no** additional fields. Names MUST be unique after NFC normalization (a duplicate name is rejected at seal time), so the ordering is total. This is stable across file-reorder and matches the single-file `bytes_hash` path.

---

## §2 — Crypto profile (byte-identical to the AGA v2 vectors)

> The cryptography shared with AGA (signature scheme and algorithm identifiers, composite encoding,
> canonicalization, leaf, chain, Merkle, checkpoint, profile versioning, and the conformance vectors)
> is defined authoritatively in `SHARED_CRYPTO_FOUNDATION.md`, which both projects adopt. This section
> states the VerifyBundle v1 (Ed25519) profile; the v2 post-quantum hybrid
> (`ML-DSA-65+Ed25519-SHA256-JCS`) is specified there and, as of Phase 2, implemented as
> `profile_version 2` (§4.3 primitive dispatch, §5.4 agility).

### §2.1 Primitives
```
canon(x)   = recursive, sorted-key serialization, UTF-8 bytes (defined below)
sha256(b)  = SHA-256 (FIPS 180-4), output lower-hex
leaf(o)    = sha256(canon(o))                              // full object incl. signature; NO prefix
node(L,R)  = sha256( hexToBytes(L) ‖ hexToBytes(R) )       // RAW 32-byte concat; odd level: promote last
sign(sk,b) = Ed25519 (RFC 8032) over canon-bytes; signature lower-hex (128 chars)
verify     = Ed25519 verify; reject all-zero / small-order public key; reject all-zero / non-128-hex signature
```
`canon` (normative, byte-identical to the AGA independent verifier):
```
canon(v) =
  v is null or not an object  -> JSON.stringify(v)                       // strings JSON-escaped
  v is an array               -> "[" + v.map(canon).join(",") + "]"
  v is an object              -> "{" + sort(keys(v)).map(k =>
                                     JSON.stringify(k) + ":" + canon(v[k])
                                 ).join(",") + "}"
```
- **R2.1.1.** `canon` MUST be byte-identical to `aga-receipt-spec/independent-verifier/verify.ts`. It MUST NOT be changed without minting a new `canon_profile` and a versioned migration (§5). It is **not** literal RFC 8785 JCS; the `canon_profile` descriptor states this honestly.
- **R2.1.2.** The signed `algorithm` tag remains `"Ed25519-SHA256-JCS"` for byte-compatibility with AGA; the honest characterization of the canonicalization lives in `canon_profile`. ("signed tag inherited" ≠ "we implement literal JCS".)

### §2.2 Bundle construction
Leaf, chain linkage (`previous_receipt_hash`), Merkle tree (odd-promote), and the **mandatory signed checkpoint** `{algorithm, gateway_id, generated_at, head_leaf_hash, leaf_count, merkle_root, signature}` are exactly as `CANONICAL_CONSTRUCTION_v2.md` §3–§5. Phase-1 anonymous bundles are single-seal (`leaf_count = 1`), signed by the ephemeral key (a checkpoint-of-one).
- **R2.2.1 (honesty).** A checkpoint-of-one is signed by the same ephemeral key as its single receipt; it therefore provides **no independent attestation** beyond the receipt signature in Phase 1. Marketing/UX MUST NOT imply otherwise. Its security value (truncation-safety) applies to multi-receipt bundles (Phase 2 chains).

### §2.3 Unicode and binary normalization (NFC)
- **R2.3.1 (strings).** Every **string field** placed into a `Seal` MUST be Unicode **NFC**-normalized **at the input boundary**, before it enters the seal object. The stored seal therefore contains only NFC strings.
- **R2.3.2 (binary).** File **byte buffers** MUST be hashed **raw** and MUST NOT be NFC-normalized (normalizing bytes corrupts the hash). NFC applies to human-entered text fields only.
- **R2.3.3 (canon unchanged).** `canon` itself MUST NOT normalize; normalization is an upstream input step. Because the stored strings are already NFC, `canon` over a VB seal yields the same bytes as the AGA oracle would over the same stored object — interop is preserved.

### §2.4 Corpus-hash gate
- **R2.4.1.** The vendored AGA conformance corpus MUST be pinned by its SHA-256 in a CI test. Any change to the corpus bytes MUST fail the build, forcing a conscious, reviewed re-vendor. (Current pin: `C7808278D8378665387F7860CB9BC3378684108F0D5593DD71D66469BF44C71A`.)

---

## §3 — Selective disclosure construction (P1-B0) — load-bearing

This section is normative and is the gate-critical part of the spec. It closes blind spot **B1** (low-entropy commitment brute-force) and binds commitments to ciphertext, field, and mode.

> **Implementation status (2026-06-04).** The disclosure **crypto library** (`packages/integrity`: per-seal/per-field derivation, all three modes, AAD binding, commitment, reveal) is **complete and conformance-tested** (§7 #1–#6 green). The disclosure requirements that touch the **sealing app** — the password-strength warning (§3.6), Argon2id in a Web Worker (R3.5.2), and the irrevocable-disclosure warning (§3.4) — ship with the **Phase-1 sealing app**, which today seals public/claimed fields only; wiring private-field entry into the app is the open **P1-B UX** task. The normative requirements below hold regardless of when that UX lands.

### §3.1 Threat the construction must defeat
A holder of the bundle (and any third party who receives it) is an adversary against the **concealment** of private fields. Private values are frequently **low-entropy** (yes/no, dates, names, IDs). The naive construction `commitment = sha256(value ‖ stored_salt)` **fails**: with the salt in the bundle, the adversary brute-forces the small value space directly against the commitment, bypassing any encryption. **Validated** (throwaway harness, 2026-06-03): with a bundle-stored salt, `PROOF_ONLY("married"="yes")` is recovered in **1** guess and `date_of_birth` in **18,663** guesses; with a secret, password-derived salt absent from the bundle, the full **33,936**-value date space yields **zero** matches, while the legitimate holder still opens the field. Therefore **stored/public commitment salts are forbidden.**

### §3.2 Per-seal key derivation
A seal with ≥1 private field MUST derive one master key, then per-field subkeys:
```
master            = Argon2id(password, kdf_salt, params)            // §3.5; one derivation per seal
commit_salt(label)= HKDF-SHA256(ikm=master, salt=kdf_salt, info="vb-seal/1 commit:"+label, L=16)   // SECRET, never stored
enc_key(label)    = HKDF-SHA256(ikm=master, salt=kdf_salt, info="vb-seal/1 enc:"+label,    L=32)   // SECRET, never stored
```
- **R3.2.1.** `commit_salt` and `enc_key` MUST NOT be stored in the bundle. Only `kdf_params` (which contains the **public** Argon2id `kdf_salt`) is stored. The password is never stored, transmitted, or recoverable.
- **R3.2.2 (per-field isolation).** Because each subkey is `HKDF(..., info=...label)`, HKDF's one-wayness means revealing one field's `commit_salt` (when proving that field, §3.4) MUST NOT enable derivation of `master` or any sibling field's subkeys.
- **R3.2.3 (where the secret lives).** The HKDF `salt` is the **public** `kdf_salt`; secrecy is carried entirely by the IKM (`master`, derived from the password via Argon2id). RFC 5869 permits a non-secret HKDF salt, and per-field/per-purpose separation comes from `info`. Implementers MUST NOT treat the HKDF salt as the secret — the password (→ `master`) is the secret.

### §3.3 Commitment (all modes)
```
commitment(label) = sha256( canon({ label, value, mode, salt: hex(commit_salt(label)) }) )
```
- **R3.3.1.** `mode` MUST be included in the committed object. For `PROOF_ONLY` there is no ciphertext, so the commitment is the **only** place `mode` is cryptographically bound; including it prevents reinterpreting a `PROOF_ONLY` commitment as a `REVEAL_*` one (or vice versa).
- **R3.3.2.** `value` is NFC-normalized (§2.3). The salt is the **secret** `commit_salt(label)` from §3.2.

### §3.4 The three modes — stored fields and reveal procedures
```
DisclosureField (PROOF_ONLY)         = { label, mode:"PROOF_ONLY", commitment }
DisclosureField (REVEAL_MIN/FULL)    = { label, mode, commitment, nonce:<48-hex>, ciphertext:<hex> }
```
- **PROOF_ONLY** — store only `{label, mode, commitment}` (no value, no ciphertext, no salt).
  - *Reveal/prove (out-of-band):* the prover re-derives `commit_salt(label)` from the password and supplies `(value, commit_salt)` to a verifier out-of-band. The verifier computes `sha256(canon({label, value, mode:"PROOF_ONLY", salt}))` and asserts equality with `commitment`. **The bundle alone cannot reveal the value.** Wrong `value` or wrong `salt` MUST yield no-match.
    - *Irrevocable disclosure (normative).* Proving a `PROOF_ONLY` field discloses `(value, commit_salt)` for that field; this is **permanent** — anyone who later holds the bundle and that pair can re-confirm the value. Per-field HKDF (R3.2.2) keeps sibling fields protected, but a proven field cannot be un-proven. The UI MUST warn the prover before disclosure.
- **REVEAL_MIN** — `value` is a deliberately minimized/bucketed representation chosen by the sealer (e.g. `"over-18"` instead of a date). The full value is never placed in the bundle. Construction otherwise as REVEAL_FULL.
- **REVEAL_FULL** — `value` is the full value, encrypted:
  ```
  nonce      = 24 random bytes (per field)
  ciphertext = XChaCha20-Poly1305.encrypt(key=enc_key(label), nonce,
                                           aad = commitment ‖ ":" ‖ label ‖ ":" ‖ mode,
                                           plaintext = utf8(value))
  ```
  - *Reveal (holder, with password):* re-derive `master`→`enc_key(label)`; decrypt with `nonce`+`aad`; on success recompute `commitment` from the revealed `value` and re-derived `commit_salt` and assert equality. Decrypt failure (wrong password or tamper) MUST fail closed; commitment mismatch MUST fail closed.
- **R3.4.1 (AAD binding / anti-substitution).** The AEAD `aad = commitment ‖ ":" ‖ label ‖ ":" ‖ mode` binds each ciphertext to its commitment, field, and mode. A ciphertext copied to a different field or mode MUST fail AEAD authentication.
- **R3.4.2 (forgotten password).** With no password there is no `master`; private fields are **permanently unrecoverable**. The public fields and CORE integrity proof remain valid. The UI MUST state this plainly.

### §3.5 KDF parameters and execution
```
Argon2idParams (stored as kdf_params) = { alg:"argon2id", v:"0x13", m:"65536", t:"3", p:"1", hashLength:"32", salt:<hex kdf_salt> }
```
Numeric params are stored as **strings** (parsed to integers only for the Argon2id call) so the seal stays string-only per R1.2.1.
- **R3.5.1.** Default initial params: memory `m = 65536` KiB (64 MiB), time `t = 3`, parallelism `p = 1`, `hashLength = 32`, Argon2 version `0x13`. These MUST be recorded per-seal in `kdf_params` so any holder can re-derive on reveal.
- **R3.5.2.** Argon2id MUST run in a **Web Worker** (off the main thread) so the sealing UI stays responsive. Library `hash-wasm`, exact version pinned.
- **R3.5.3 (benchmark, not immutable).** The default params MUST be benchmarked on a real low-end device in P1-A (target unlock ≈ 0.5–0.8 s) and MAY be adjusted **once** before freeze; after freeze they are fixed for `profile_version 1`. Changing them later requires a new `profile_version` and is recorded per-seal regardless, so old bundles always re-derive correctly.

### §3.6 Honest concealment language (normative)
A private field's concealment strength **equals the strength of the user's password** — `commit_salt` and `enc_key` are derived from it via Argon2id. The product MUST NOT claim "only high-entropy values are hidden" nor "always hidden regardless of password." The UI MUST display, near private-field entry: *"Private fields are protected by your password. Choose a strong password — especially for short or guessable values (yes/no, dates, names) — because a weak password can be attacked offline by anyone who holds your bundle. Argon2id makes each guess slow."*

---

## §4 — Verifier rules

### §4.1 CORE verification (v1: Ed25519 + SHA-256)
A bundle is **CORE-VERIFIED** iff all six steps pass (`CANONICAL_CONSTRUCTION_v2.md` §6): structural floor; every receipt signature; chain linkage + non-decreasing timestamp; Merkle leaf-recompute + single-root + contiguous `0..N-1` bijection; signed checkpoint (signature + `merkle_root` + `leaf_count` + `head_leaf_hash`); provenance iff a key is pinned. This is already implemented and proven (`verifyBundle`).
- **R4.1.1 (profile primitive).** The six steps are identical across profiles; only the signature primitive in step 2 (receipt signatures) and step 5 (checkpoint) is selected by the bundle `algorithm`. v1 verifies Ed25519 over SHA-256; v2 verifies the `ML-DSA-65+Ed25519` composite, where BOTH components MUST pass (`SHARED_CRYPTO_FOUNDATION.md` §2.2). v1 bundles continue to CORE-verify with Ed25519 + SHA-256 alone.

### §4.2 Output field-categorization (B2) — where the claim boundary is enforced
The verifier result MUST tag **every** field it reports into exactly one category:
- **`cryptographically-sealed`** — unaltered since sealing and bound by the signature/checkpoint: `subject` hashes, `public_fields`, `timestamps.local`, the **existence + integrity** of each `disclosure` commitment, the signature and checkpoint themselves. (For `timestamps.universal`, also report the time-service attestation result separately.)
- **`claimed-unverified`** — inside the signed envelope (so integrity-sealed: unchanged since sealing) but **asserted by the sealer, not verified by anyone**: every `claimed_context` value, and any revealed disclosure `value`. The verifier output MUST render these as *"claimed by the sealer; integrity-sealed but not independently verified."*
- **`not-checked`** — anything outside the bundle: truth of the claims, identity of the sealer (beyond "holder of this key"), non-omission, and any external anchor (none in Phase 1).
- **R4.2.1 ("signed ≠ endorsed").** The signature covers `claimed_context`; the verifier MUST NOT present a signed claimed value as endorsed or verified. It is `claimed-unverified`.
- **R4.2.2 ("No Magic").** Every verification result MUST be able to answer, explicitly: *what was checked, what was not checked, what assumptions remain.* The three categories above are that answer.

### §4.3 Profile dispatch
One `verifyBundle` entry point MUST validate both the `vb-seal/1` profile and genuine AGA SEP governance receipts, sharing the CORE 6-step crypto logic and branching only on `profile`/`schema` for **field-schema** validation. All existing conformance tests MUST remain green; a genuine AGA SEP bundle and a VB seal MUST both return a correct verdict through the same entry point. The same entry point ALSO dispatches the **signature primitive** on the `algorithm` field (v1 Ed25519 / v2 composite; `SHARED_CRYPTO_FOUNDATION.md` §7), independently of the `profile`/`schema` field-schema branch; an unknown `algorithm`, or a key whose shape does not match the algorithm, fails closed.

### §4.4 Corruption resilience
- **R4.4.1.** On malformed input (truncated/invalid JSON, missing required fields, wrong types, damaged QR/zip, missing file, altered ordering, non-NFC where NFC is required) the verifier MUST **fail closed** with a precise, honest error and MUST NOT crash and MUST NOT return VERIFIED.

### §4.5 Error language (normative catalog)
Failure messages MUST be precise and MUST NOT use "verified" without the full honest qualifier, and MUST NOT imply truth or physical presence. Canonical strings:
- `"Signature invalid — the bundle was altered, or was not created with this public key."`
- `"Commitment mismatch — the revealed value is incorrect, or the bundle was tampered with."`
- `"Private field cannot be recovered (wrong password or corrupted data). The public fields and integrity proof remain valid."`
- `"This bundle proves the integrity of what is present — it does not assert that the claims are true or that the signer recorded everything."`

---

## §5 — Versioning and migration policy

- **R5.1 (forward compatibility).** A conformant verifier MUST verify every bundle with `profile_version ≤` its own. Issued artifacts MUST NEVER stop verifying. A **frozen golden VB bundle for each profile_version** (v1 Ed25519: `packages/integrity/vectors/vb-golden-v1.json`; v2 hybrid: `vb-golden-v2.json`) MUST be checked into the conformance suite; every future build MUST still verify each one and, because the construction is deterministic, reproduce it byte-for-byte.
- **R5.2 (additive change).** Non-breaking changes add OPTIONAL fields and keep `canon`, leaf, node, checkpoint, and signature unchanged; `profile_version` increments.
- **R5.3 (breaking change).** A change to the seal schema's required fields requires a new `profile` string; a change to `canon` requires a new `canon_profile` plus a versioned migration with dual-emission. The CORE crypto profile is **frozen** for byte-compatibility with AGA.
- **R5.4 (algorithm agility).** New signature algorithms MAY be added at the `algorithm` envelope under a new `profile_version`; algorithms MUST NOT be removed for the verification of already-issued bundles. The post-quantum hybrid `ML-DSA-65+Ed25519-SHA256-JCS` (`profile_version 2`) is **implemented and exercised** as of Phase 2: `verifyBundle` dispatches the signature primitive on the `algorithm` field (v1 Ed25519, v2 composite; `SHARED_CRYPTO_FOUNDATION.md` §7), every other construction step (canon, leaf, chain, Merkle, checkpoint) is identical across profiles, and both profiles are covered by the conformance suite (`agility.test.ts`, `forward-compat.test.ts`). Adding v2 MUST NOT stop v1 bundles from verifying.

---

## §6 — What this does NOT prove (normative; appears at protocol, verifier, UX, and marketing levels)

- **"Verified" means "cryptographically unaltered and matching the seal." It never means "true."**
- A seal does **NOT** prove the **truth** of the sealed claims; sealing "I was in Paris" proves the statement was committed, not that it is true.
- A seal does **NOT** prove **physical presence**; any location/device/network/claimed-time value is a **claimed** value the tool cannot independently confirm.
- A PASS proves the **integrity of what is present** — it does **NOT** prove **non-omission** (it cannot establish that the signer recorded every action it took).
- Authorship is "the holder of this key" — in Phase 1 (anonymous, ephemeral key) it is **not** tied to a known person.
- These statements MUST appear in the verifier output (§4.2), in the bundle `README.txt` (first paragraph), in the sealing and result UI, and in marketing copy.

---

## §7 — Conformance & falsifiable acceptance (the tests P1-A/P1-B/P1-F MUST pass)

Each item is a machine-checkable requirement for the implementation phase; this spec defines them so the implementation is built against falsifiable criteria.

**Disclosure (P1-B):**
1. **B1 closed:** a brute-force harness over a low-entropy field (yes/no, then a date) RECOVERS the value when the commitment salt is bundle-stored, and FAILS to recover it when the salt is password-derived-and-absent. Both results demonstrated; the failure under the fix is the evidence. *(Validated in §3.1.)*
2. **REVEAL round-trip:** right password decrypts and the recomputed commitment matches; wrong password fails closed.
3. **AAD anti-substitution:** a ciphertext moved to a different `label` or `mode` fails AEAD authentication.
4. **PROOF_ONLY:** with `(value, commit_salt)` the verifier returns match; the bundle alone cannot reveal the value; wrong value or wrong salt returns no-match.
5. **HKDF isolation:** revealing one field's `commit_salt` does not open sibling fields.
6. **Forgotten password ⇒ unrecoverable** (asserted by test); public fields + CORE proof remain valid.
7. **KDF:** RFC 9106 Argon2id known-answer vectors pass; `kdf_params` present and honored on reveal; Argon2id runs in a Web Worker.

**Canon / format (P1-B / integrity):**
8. A VB seal carrying `canon_profile`/`profile_version` still VERIFIES in the AGA independent-verifier (interop direction VB→AGA preserved — confirm against the crypto-only oracle).
9. A Unicode field (emoji / CJK / combining mark) yields an identical leaf in VB and the oracle.
10. Two visually-identical strings differing only in NFC form produce the **same** seal after normalization.
11. The pinned corpus-hash gate FAILS on a one-byte corpus change.
12. A property/differential test asserts VB `canon` == oracle `canon` over arbitrary string-only JSON.

**Verifier (P1-F):**
13. Output categorization tags every field `cryptographically-sealed | claimed-unverified | not-checked`; `claimed_context` is `claimed-unverified`.
14. Corruption matrix (truncated zip, damaged QR/JSON, missing file/metadata, reorder, non-NFC) → fail closed, precise error, never crash, never VERIFIED.
15. Profile dispatch: a genuine AGA SEP bundle and a VB seal both verify correctly through one entry point; all prior conformance tests stay green.

**Longevity (P1-F):**
16. A competent engineer can re-derive **CORE** verification from `README.txt` + this SPEC alone (no VB/AGA code); DISCLOSURE reveal is reproducible from `kdf_params`.
17. SPEC + offline-verifier source + VB golden vectors are published; every bundle ships a plain-language CORE procedure in `README.txt` whose first paragraph is the §6 boundary.

---

*End of normative spec. Implementation MUST follow this document. Changes follow §5.*
