Satsignal conformance — bundle-v1 / .mbnt envelope

This is the conformance surface for the .mbnt proof bundle spec (docs/notary_spec/bundle-v1.md). It is the canonical "is this verifier / producer conformant with bundle-v1?" reference for third-party verifier authors, CLI / SDK implementers, adapter authors, and standards-adjacency reviewers.

bundle-v1.md is normative. This document is a flat checklist of the normative requirements with cross-references back to the relevant sections, plus pointers to the test vectors and executable test scripts that exercise each requirement. Where a requirement has no in-tree executable check, this is called out explicitly in the "Followups" section so adopters can supply their own.

This document complements — and does not duplicate — the prose conformance summary at bundle-v1.md §10. The summary there is authoritative for the list of MUSTs; this document maps each MUST to test material.

Relationship to CONFORMANCE.md (provenance-v1)

The two specs are intentionally independent:

A .mbnt bundle MAY carry a satsignal.provenance.v1 manifest as its content; in that case the envelope conformance is bundle-v1's concern, and the inner-content conformance is provenance-v1's. The two checks are independent and both apply.

A provenance manifest MAY be sealed (privacy.onchain_mode: "sealed", provenance-v1 §6): the bundle then carries the salted byte_exact commitment and the salt_b64 bearer secret but no manifest plaintext — the holder presents (manifest + salt) out-of-band to verify (B7.9–B7.10). This is the provenance analogue of the sealed file bundle (B7.1–B7.8).

Scope

A claim of conformance to bundle-v1 is a claim about one of two implementation roles:

A verifier claim covers bundle-v1.md §2§8 (envelope, parse, crypto, doc-hash, chain confirmation, error model). A producer claim covers bundle-v1.md §2§6 (envelope structure, JSON-entry shape, on-chain anchor binding) and the §2.2 hardening MUSTs (a producer MUST NOT emit any of the §2.2 malformations).

This document does not define a third "oracle" conformance class. The bundle-v1.md §6.2 chain resolver (any BSV node) is explicitly non-normative — verifiers are free to pick any explorer that exposes raw-tx-by-hash.

Conformance classes

ClassBound byScope
Verifierbundle-v1.md §2, §4.4, §6, §7, §8, §9.1§9.3parse, crypto, doc-hash, chain confirm, error model, version tolerance
Producerbundle-v1.md §2, §3, §4, §5, §6.1emit a bundle that every conformant verifier will accept

A verifier that omits the chain check (§7.4) by default is not conformant (bundle-v1.md §10 closing note). A verifier that implements §7.5 "offline mode" only behind an explicit caller opt-in is conformant.

Normative requirements — verifier checklist

Each item below is a MUST. The "see" reference points to the authoritative clause in bundle-v1.md; the "vector" and "script" columns point to the executable check, if one exists in-tree.

B1. Envelope structure (bundle-v1.md §2)

#RequirementSeeVectorScript
B1.1Parse the .mbnt as a standard PKZIP archive (deflate or store); magic 50 4B 03 04 MUST identify it§2tests/vectors/legacy-bundles/*.mbntscripts/test_legacy_bundles.py
B1.2Verifier MUST read manifest.json and canonical.json from the root of the archive§2 table; §7.1 step 1tests/vectors/legacy-bundles/*.mbntscripts/test_legacy_bundles.py
B1.3Verifier MUST read proofs.json iff canonical.json.subject.proofs.chunk_merkle is present§2 table; §7.1 step 2(no in-tree chunk_merkle vector)(none)
B1.4Verifier MUST tolerate unknown ZIP entries (skip and continue) and MUST NOT treat them as proof material§2 (post-table prose)(none)
B1.5Verifier MAY use legacy attachments/<name> to source file bytes for legacy bundles, but MUST NOT rely on its presence§2.1tests/vectors/legacy-bundles/v1-generic-attest-attach.mbntscripts/test_legacy_bundles.py

B2. Envelope hardening — MUST reject (bundle-v1.md §2.2)

These are the five malformations a conformant verifier MUST reject before extracting any entry. Cross-references back to bundle-v1.md §10 items 8–12.

#RequirementSeeVectorScript
B2.1Reject ZIP buffers whose first 4 bytes are not the local-file-header magic 50 4B 03 04 (leading-data smuggling)§2.2 bullet 1; §10 item 8(none in-tree)(none)
B2.2Reject ZIP buffers whose EOCD record reports a non-zero commentLen§2.2 bullet 2; §10 item 9(none in-tree)(none)
B2.3Reject ZIP buffers containing more than one EOCD signature (50 4B 05 06) anywhere in the buffer§2.2 bullet 3; §10 item 10(none in-tree)(none)
B2.4Reject ZIP central directories with duplicate filenames (most importantly duplicate manifest.json, canonical.json, or proofs.json) — JSZip / CPython zipfile resolve in opposite directions§2.2 bullet 4; §10 item 11(none in-tree)(none)
B2.5Reject central-directory entries whose name contains .., starts with /, or contains backslash \ (zip-slip)§2.2 bullet 5; §10 item 12(none in-tree)(none)

Reference enforcement implementation: verifier.html lines 1944–2111 (shipped 2026-05-22 against M#36, H#17, H#20). A producer MUST NOT emit any §2.2 malformation; a producer that does is non-conformant even if some verifiers tolerate it.

B3. manifest.json — common fields (bundle-v1.md §3.1)

#RequirementSeeVectorScript
B3.1manifest.mbnt_version MUST be present and MUST be one of "1.1", "2.0", "2.1"§3.1; §7.1 step 3; §9.1tests/vectors/legacy-bundles/v1-*.mbnt ("1.1"), v2-multi-proof.mbnt ("2.0")scripts/test_legacy_bundles.py
B3.2manifest.txid MUST be 64 lowercase hex chars§3.1 tabletests/vectors/legacy-bundles/*.mbntscripts/test_legacy_bundles.py
B3.3manifest.network MUST be present; verifiers MUST refuse unknown networks unless explicitly supported§3.1 tabletests/vectors/legacy-bundles/*.mbntscripts/test_legacy_bundles.py
B3.4manifest.doc_hash_expected MUST be 40 lowercase hex chars = first 20 bytes of sha256(canonical_bytes)§3.1 table; §6.4; §7.3tests/vectors/legacy-bundles/*.mbnt (.expected.json.doc_hash_expected)scripts/test_legacy_bundles.py
B3.5Verifier MUST tolerate optional informational fields (category, acceptance, proof_mode)§3.1 post-table(covered by proof_mode in v1-generic-file.mbnt)scripts/test_legacy_bundles.py
B3.6Verifier MUST NOT switch standard/sealed dispatch on proof_mode; dispatch is on mode§3.1 (proof_mode row)(none)

B4. mbnt_version per mode (bundle-v1.md §3.2§3.4, §9.1)

#RequirementSeeVectorScript
B4.1Standard-mode manifests use mbnt_version: "2.0" and have NO mode field§3.2; §9.1 row 2tests/vectors/legacy-bundles/v2-multi-proof.mbntscripts/test_legacy_bundles.py
B4.2Legacy single-proof manifests use mbnt_version: "1.1" (canonical schema_version: 1)§3.1; §4.5; §9.1 row 1tests/vectors/legacy-bundles/v1-generic-file.mbnt, v1-generic-attest-attach.mbntscripts/test_legacy_bundles.py
B4.3Sealed-mode manifests use mbnt_version: "2.1" and carry an explicit mode: "sealed" field§3.3; §9.1 row 3(no in-tree sealed bundle — see Followups)(none directly; scripts/test_sealed_flow.py exercises the wire flow against a live server)
B4.4Manifest-mode (Phase 8b) bundles use mbnt_version: "2.0" at the manifest layer; mode signal lives in canonical.json (subject.kind == "manifest")§3.4(no in-tree manifest-mode vector)(none)
B4.5Verifier MUST refuse unsupported mbnt_version values rather than attempt a partial parse§7.1 step 3; §9.1 post-table(none)

B5. canonical.json — required fields (bundle-v1.md §4.1)

The eight top-level keys MUST all be present. Source of truth: canonical.schema.json required[] and schema.py _TOP_LEVEL_KEYS, kept byte-for-byte in lockstep by tests/test_canonical_schema_lockstep.py (a differential battery that asserts the published JSON Schema and schema.py agree on accept/reject for every v1 and v2 document). schema.py is the runtime validator and emits missing top-level keys: [...] on any omission. The published canonical.schema.json validates both versions — its top-level oneOf branches on schema_version + subtype, and selects the v2 public / sealed (subject.kind == "file_anchor") / manifest (subject.kind == "manifest") shape by structural marker — and carries "maximum": 9007199254740991 on every integer field (the JS-safe-integer bound, §B6.3).

#RequirementSeeVectorScript
B5.1schema_version MUST be present (integer 1 or 2)§4; §4.1; §4.5tests/vectors/provenance-v1/invalid/i01-missing-schema.jsonscripts/run_vectors.py
B5.2subtype MUST be present§4.1tests/vectors/provenance-v1/invalid/i04-unknown-source-type.json (related)scripts/run_vectors.py
B5.3issued_at MUST be present (RFC 3339 UTC, second precision, Z suffix)§4.1; canonical.schema.json(validation gated by scripts/test_provenance.py)scripts/test_provenance.py
B5.4issuer MUST be present (non-empty string)§4.1(validation gated by scripts/test_provenance.py)scripts/test_provenance.py
B5.5subject MUST be present§4.1; §4.2(validation gated by scripts/test_provenance.py)scripts/test_provenance.py
B5.6attestation MUST be present§4.1(validation gated by scripts/test_provenance.py)scripts/test_provenance.py
B5.7attachments MUST be present (always present, always empty in v2)§4.1; post-shape prose(validation gated by scripts/test_provenance.py)scripts/test_provenance.py
B5.8nonce MUST be present (16 random bytes, 32 hex chars)§4.1; canonical.schema.json(validation gated by scripts/test_provenance.py)scripts/test_provenance.py
B5.9subject.proofs.byte_exact MUST be present in v2; content_canonical and chunk_merkle are optional§4.1; §7.2 step 1tests/vectors/legacy-bundles/v2-multi-proof.mbntscripts/test_legacy_bundles.py

B6. canonical.json canonicalization (bundle-v1.md §4.4)

#RequirementSeeVectorScript
B6.1canonical.json on disk MUST be byte-identical to the SCJ-v1 encoding whose SHA-256 was committed on-chain§2 post-table; §4.4tests/vectors/legacy-bundles/*.mbnt (.expected.json.canonical_sha256)scripts/test_legacy_bundles.py
B6.2doc_hash = sha256(SCJ_v1(canonical.json))[:20] and MUST equal manifest.doc_hash_expected§4.4; §6.4; §7.3tests/vectors/legacy-bundles/*.mbntscripts/test_legacy_bundles.py
B6.3SCJ-v1 encoding rules (not RFC 8785): UTF-8 output, NFC normalization, keys sorted by Unicode code point, no whitespace, no NaN/±Inf, integers as bare decimals (floats forbidden) and within the JS-safe range ≤ 2^53−1 (canonical.schema.json "maximum" on every integer field)§4.4tests/vectors/provenance-v1/valid/v05-key-reorder-stable.*scripts/run_vectors.py, scripts/test_offline_verifier.py

B7. Sealed-mode specifics (bundle-v1.md §3.3, §4.2, §5, §7.2)

#RequirementSeeVectorScript
B7.1Sealed manifests MUST carry mode: "sealed" as the dispatch signal§3.3; §7.2 (sealed branch); §10 item 3(no in-tree sealed .mbnt vector)scripts/test_sealed_flow.py (end-to-end)
B7.2Sealed manifests MUST carry salt_version: "salt_v1" — the single currently-accepted value (per schema.py:_VALID_SALT_VERSIONS = {"salt_v1"} and sealed.py:SALT_VERSION)§3.3 tabletests/vectors/provenance-v1/valid/v03-typed-authority-full.json (sealed-shape gating)scripts/test_sealed_session_commitment.py, scripts/test_proof_set_sealed_schema.py
B7.3Sealed manifests MUST carry salt_b64 (base64url-encoded 32-byte master salt) and bearer_secret: true§3.3 table(none in-tree as .mbnt)scripts/test_sealed_session_commitment.py
B7.4Sealed manifests MUST NOT carry sha256, file_size, or any other field that would leak the underlying file's identity§3.3 closing prose— (negative — checked by scripts/check_no_payload_upload.py at producer boundary)scripts/check_no_payload_upload.py
B7.5Sealed byte_exact and content_canonical proofs use algo: "hmac-sha256", carry salt_version, replace hash with commitment§4.2 tabletests/vectors/provenance-v1/valid/v03-typed-authority-full.json (related sealed-shape)scripts/test_proof_set_sealed_schema.py, scripts/test_sealed_session_commitment.py
B7.6Sealed chunk_merkle uses algo: "merkle-hmac-sha256", carries salt_version, keeps the field name root (NOT commitment)§4.2 table; §7.2 sealed step 4 closing note(none in-tree)(none)
B7.7Sealed Merkle leaves use per-leaf salts derived via HKDF with literal salt b"satsignal-sealed-v1/per-leaf" and `info = b"chunk/"u32_be(i)`§5 post-metadata prose(none in-tree as vector)scripts/test_sealed_flow.py (end-to-end)
B7.8The blind-submission "manifest-stripped" variant omits server_retain_until_utc (no server-side copy exists)§3.3 table (server_retain_until_utc row)(none)
B7.9A sealed satsignal.provenance.v1 bundle MUST NOT carry a proofs.json.manifest plaintext key — the manifest is the holder's bearer secret, presented out-of-band, not shipped (contrast a hash_only provenance bundle, which carries "manifest": <clean manifest>)provenance-v1 §6.5(no in-tree sealed-provenance .mbnt vector)tests/test_provenance_sealed_e2e.py (no-leak assertions)
B7.10A sealed satsignal.provenance.v1 bundle's byte_exact proof carries {algo: "hmac-sha256", salt_version: "salt_v1", commitment} where commitment = HMAC-SHA256(master_salt, SCJ_v1(canonical_manifest)); the on-chain anchor is the unchanged 20-byte byte_exact sealed anchor (NO new merkle scheme literal)provenance-v1 §6.3, §6.5(no in-tree vector)tests/test_provenance_sealed_e2e.py, scripts/test_proof_set_sealed_schema.py

B8. On-chain anchor binding (bundle-v1.md §6)

#RequirementSeeVectorScript
B8.1OP_RETURN payload MUST begin with ASCII "MBNT" (0x4D 0x42 0x4E 0x54), version byte 0x01, subtype byte 0x01§6.1; §7.4 step 2(off-chain — payload reconstruction inside tests/vectors/legacy-bundles/*.mbnt)scripts/test_legacy_bundles.py (40-hex truncation match)
B8.2doc_hash_on_chain = payload[8:28] MUST equal the 20-byte form of manifest.doc_hash_expected (constant-time compare)§6.4; §7.4 step 3tests/vectors/legacy-bundles/*.mbntscripts/test_legacy_bundles.py
B8.3Verifier MUST tolerate unknown TLV tags (record opaquely) but MUST refuse unknown version bytes and unknown subtype bytes§6.1 post-prose; §9.3(none)
B8.4A .mbnt that passes 7.1–7.3 with confirmations >= 1 is verified; with confirmations == 0 is pending (verifier MUST NOT claim "verified" on a 0-confirmation proof)§7.4 step 4; closing prose of §7(none — requires live chain)

B9. Verification procedure (bundle-v1.md §7)

#RequirementSeeVectorScript
B9.1Verifier MUST perform checks §7.1§7.4 in order; failing any check is a verification failure with the appropriate error class§7 opening(covered indirectly by legacy + offline tests)scripts/test_legacy_bundles.py, scripts/test_offline_verifier.py
B9.2Standard-mode crypto MUST recompute sha256(file_bytes) and match byte_exact.hash; SHOULD recompute content_canonical / chunk_merkle when present§7.2 standard branchtests/vectors/legacy-bundles/v2-multi-proof.mbntscripts/test_legacy_bundles.py
B9.3Sealed-mode crypto MUST recompute HMAC-SHA256(master_salt, file_bytes) and match byte_exact.commitment; SHOULD recompute content_canonical / chunk_merkle when present§7.2 sealed branch(no in-tree sealed .mbnt vector — Followups)scripts/test_sealed_flow.py (end-to-end)
B9.4Verifier MUST confirm len(proofs.json.merkle_leaves) == chunk_merkle.leaf_count when chunk_merkle is present§7.2 standard step 3(no in-tree chunk_merkle vector)(none)
B9.5Offline mode MUST report results as "cryptographic checks pass; on-chain status NOT verified" and MUST NOT downgrade the warning under any flag§7.5scripts/test_offline_verifier.py (analogous offline invariant on the provenance-v1 surface)
B9.6Verifier MUST distinguish the error classes CRYPTO, CHAIN, VERSION, NETWORK, PENDING, OFFLINE per §8§8; §10 item 6(none)

B10. Tolerance / forward-compat (bundle-v1.md §9.2§9.3)

The B10 rules apply to JSON object keys inside an already-extracted entry. They are independent of the §2.2 envelope-entry duplicate / unknown rule (which is strict).

#RequirementSeeVectorScript
B10.1Verifier MUST tolerate unknown top-level keys in manifest.json, canonical.json, and proofs.json (skip and continue)§9.2; §2.2 bullet 4 closing note(no in-tree positive vector — provenance-v1 rejects unknowns per i03)(none — this is the deliberate asymmetry with provenance-v1 strict-mode)
B10.2Verifier MUST tolerate unknown TLV tags in the OP_RETURN payload (record as opaque (tag, value))§9.3(none)
B10.3Verifier MUST refuse unknown OP_RETURN version bytes and unknown subtype bytes§9.3(none)

Asymmetry note. provenance-v1 (the canonical-manifest layer) rejects unknown top-level keys (CONFORMANCE.md item 3, vector i03-unknown-top-level-key); bundle-v1 (the envelope-and-bundle layer) tolerates them (§9.2). These are different layers and the rules do not conflict. A producer that wants forward-compatible custom data inside the canonical manifest MUST use the extensions object (per provenance-v1).

Test vector pointers

Vector dirCoverage
tests/vectors/legacy-bundles/Frozen .mbnt bundles: re-validates schema, canonicalizer reproduces canonical bytes, 40-hex truncation matches doc_hash_expected. 3 fixtures: v1-generic-file.mbnt, v1-generic-attest-attach.mbnt, v2-multi-proof.mbnt.
tests/vectors/provenance-v1/valid/5 canonical-manifest vectors. Bundle-v1-relevant: B5.1–B5.8 (required-fields), B6.3 (SCJ-v1 stability).
tests/vectors/provenance-v1/invalid/12 rejection vectors. Bundle-v1-relevant: B5.1 (i01), B5.2-adjacent (i04).
tests/vectors/sealed/session-commitment-v1/merkle-session-v1 session_commitment vectors (single-leaf / two-leaf / odd-width). Recorded, not verified (B-63): the session leaves are not in the canonical doc, so an independent verifier cannot re-derive the root; these pin the server-side derivation incl. the root == sha256(leaf‖leaf) single-leaf edge. Driven by tests/test_wg5b_merkle_conformance.py.
tests/vectors/chain-anchor-v1/MBNT OP_RETURN reference-parser conformance (verifier/chain.mjs): valid/ positive parses (strip-optional-0x00 for both the raw-tx 00 6a and WhatsOnChain bare-6a script shapes; an out-of-registry subtype reports the commitment, subtype_known=false) + invalid/ rejection vectors (bad version, duplicate / overrunning TLV, TLV section > 192B, too-short). Driven by tests/verifier/test_chain_anchor_vectors.mjs.
tests/vectors/merkle-single-leaf-v1/Cross-scheme Merkle single-leaf / odd-node conformance (chunk_merkle vs satsignal.disclosure.v1 vs merkle-session-v1). Pins the SPEC_mbnt §5.1 normative table. Driven by tests/test_wg5b_merkle_conformance.py.
tests/vectors/adapters/Empty as of 2026-05-25 — Phase-5 deferred.

Test script pointers

ScriptAsserts
scripts/test_legacy_bundles.pyRe-validates frozen .mbnt bundles: schema, canonicalizer, doc_hash_expected truncation. Stdlib-only. (B1.1–B1.5, B3.1–B3.5, B4.1–B4.2, B5.9, B6.1–B6.2, B8.1–B8.2, B9.1–B9.2)
tests/test_canonical_schema_lockstep.pyDifferential battery: the published canonical.schema.json and schema.py MUST agree on accept/reject for every v1 + v2 (public/sealed/manifest) document, and a frozen v2 vector validates against the schema. Requires jsonschema (skips if absent). (B5.x; B-62/B-67/B-69)
tests/test_wg5b_merkle_conformance.pyMerkle single-leaf / odd-node rules per scheme (chunk_merkle vs disclosure.v1 vs merkle-session-v1) against the in-tree builders. Stdlib-only. (B-66; SPEC_mbnt §5.1)
tests/verifier/test_chain_anchor_vectors.mjsMBNT OP_RETURN reference parser: strip-optional-0x00, version/subtype handling, TLV duplicate/overrun/192B-cap refusals. Node node:test (rides js-tests.yml). (B-64/B-65/B-68)
scripts/run_vectors.pyWalks tests/vectors/provenance-v1/{valid,invalid}/; each input either canonicalizes to its frozen manifest_sha256 or raises an error containing the pinned needle. (B5.1, B6.3)
scripts/test_offline_verifier.pyRe-validates every valid provenance-v1 vector with sockets blocked; proves the validator path never touches the network. (B9.5 analog)
scripts/test_provenance.pyCanonical-manifest accept/reject rules. (B5.3–B5.8)
scripts/test_proof_set_sealed_schema.pySealed-mode proof_set schema. (B7.2, B7.5)
scripts/test_sealed_session_commitment.pySealed session_commitment derivation. (B7.2, B7.3, B7.5)
scripts/test_sealed_flow.pyEnd-to-end sealed submit / fetch / verify against a live server. (B4.3, B7.1, B7.7, B9.3)
scripts/test_sealed_no_plaintext_session_id.pySealed-mode plaintext session-label leak invariant. (B7.4 adjacent)
scripts/check_no_payload_upload.pyOutbound boundary stays payload-free (no customer bytes leave the process). (B7.4)

All of the above are stdlib-only unless noted; the legacy-bundle and provenance vector runners gate every PR via .github/workflows/conformance.yml.

CI gating

Bundle-v1's executable conformance checks ride the same workflow as provenance-v1: .github/workflows/conformance.yml. The legacy-bundle backward-compat suite is the load-bearing bundle-v1 gate — a failure there means a refactor silently broke the byte-exact promise to every existing customer bundle on disk.

The §2.2 envelope-hardening MUSTs (B2.1–B2.5) are currently enforced in verifier.html (reference implementation, lines 1944–2111) but do not yet have stdlib regression vectors in this tree. Adding crafted malformed-ZIP fixtures under tests/vectors/envelope-hardening/ is a tracked followup; until then, B2.1–B2.5 are conformance requirements without an in-tree executable check.

Cross-references

Followups (in-tree gaps)

  1. §2.2 envelope-hardening vectors. B2.1–B2.5 are reference- implemented in verifier.html but have no in-tree crafted-malformed ZIP fixtures. Suggested location: tests/vectors/envelope-hardening/ with sibling .expected.json recording the expected rejection reason. (M#36, H#17, H#20 references in bundle-v1.md §2.2.)
  2. Full sealed .mbnt bundle vector. tests/vectors/sealed/ now carries session-commitment-v1/ (the merkle-session-v1 derivation, B-63), but a complete captured sealed .mbnt bundle (with a published master_salt) is still absent: B4.3, B7.1, B7.6, B7.7, B9.3 rely on the live-server end-to-end script scripts/test_sealed_flow.py rather than a frozen fixture. Such a bundle would let stdlib-only conformance cover the full sealed dispatch path.
  3. chunk_merkle vectors. No in-tree vector exercises the chunk_merkle / proofs.json codepath (B1.3, B9.4, B7.6). The live verifier at verifier.html covers this; a frozen vector would close the regression-test gap.
  4. Manifest-mode (Phase 8b) vectors. No in-tree vector for subject.kind == "manifest" / scheme: "manifest-items-v1" (B4.4).
  5. Chain-confirmation step (§7.4). B8.4 and B9.6 are intrinsic to the chain check and cannot be exercised by a stdlib offline test. A mock-explorer harness would let the error-class distinctions (CHAIN, NETWORK, PENDING) gate on a vector rather than a live BSV node.

Questions about this specification? Email hello@satsignal.cloud.