Audience. Integrators ingesting a packet downstream (persona P-23) and anyone auditing the wire format. This is the human companion to the machine-readable
packet-bundle.schema.json(JSON Schema 2020-12). Realizes backlog E-26 / R-51 (a documented, versioned bundle with a stability contract).
habitable export (or build_packet) produces a self-contained directory:
4B-packet/
├── bundle.json # the canonical, signed manifest (this document)
├── bundle.sig.json # producer Ed25519 signature over bundle.json's bytes
├── media/ # location-stripped shared copies (referenced by items[].shared_name)
├── originals/ # OPTIONAL sealed originals (present only with --include-originals)
├── packet.html # accessible human-readable rendering (the conformant view)
└── packet.pdf # paginated print rendering (optional)
bundle.json is the source of truth a verifier reads. The other rendered files (packet.html,
packet.pdf) are presentation; they are not what verification trusts.
bundle.json is serialized canonically: UTF-8, keys sorted, tight separators (, and :), no
insignificant whitespace, NaN/Infinity disallowed. This makes the bytes reproducible across
machines and Python versions — a prerequisite for the signature and for independent verification.
The signature in bundle.sig.json is over the SHA-256 of these exact bytes, so do not
re-serialize bundle.json before checking the signature.
| Field | Type | Notes |
|---|---|---|
packet_version |
int | Format version. Verifier accepts 1..SUPPORTED_PACKET_VERSION; newer is rejected, not mis-verified. |
case_id |
string | Case identifier. |
unit |
string | Unit label; may be empty. |
scope |
object | {type: "issue"|"unit", issue_id, since} — what the packet covers. |
generated_at |
string | ISO 8601 UTC, e.g. 2026-01-02T00:00:00Z. |
producer_fingerprint |
string | Producing device fingerprint (xxxx-xxxx-xxxx-xxxx). |
hash_algorithm |
string | Always "sha256". |
language |
string | Language of the rendered packet (e.g. en, es). |
template |
object | {header, footer} — presentation only. |
issues |
array | Selected issues (see below). |
timeline |
array | Timeline entries for the selected issues. |
items |
array | The media items — the evidentiary core (see below). |
custody_proof |
object | Identity-stripped chain-of-custody proof (see below). |
disclosures |
array | Human-readable notes of what the packet reveals (location stripped, custody identities not exported, originals embedded). Also rendered, localized, in packet.html/packet.pdf. |
appendix |
object | {item_count, timestamped_count, includes_originals}. |
Every exported id — issues[].issue_id, items[].capture_id, timeline[].entry_id, and the
custody_proof item_ids — is an opaque, per-case-salted digest (prefix-<16 hex>). It is
stable (the same event yields the same id on every device that shares the case) but encodes
no device wall-clock time and no HLC node id. The hlc fields in timeline[] and
custody_proof.entries[] are likewise pseudonymized to opaque tokens in the export. Internally the
tool still keeps a full hybrid logical clock for CRDT ordering and merge; that raw stamp simply never
leaves the vault. (In packet_version 1 these fields carried the raw wall_ms.counter.node_id HLC;
treat all ids and hlc values as opaque strings regardless of version.)
| Field | Type | Notes |
|---|---|---|
capture_id |
string | Stable id; also the filename under originals/ when embedded. |
issue_id |
string | The issue this item documents. |
content_hash |
hex SHA-256 | Of the sealed original. The RFC 3161 token is taken over this. |
media_type |
string | MIME type, e.g. image/jpeg. |
captured_at |
string | Capture time. |
shared_name |
string | Filename under media/ of the location-stripped copy; empty if none. |
shared_hash |
hex SHA-256 | "" | Of the shared copy; empty when no shared media. |
stripped |
string | Which metadata was removed from the shared copy (gps, none, skipped, …). |
has_original |
bool | Whether the sealed original is embedded under originals/. |
timestamp |
object | null | RFC 3161/dev token over content_hash; null while awaiting timestamp. |
archive_timestamps |
array | Archive (re-)timestamps chaining back to the primary token. |
additional_timestamps |
array | Independent redundant tokens from other authorities over the same content_hash (not a chain). The verifier counts the item as timestamped if ≥1 authority verifies — no proof rests on a single TSA. Absent in single-authority packets. |
A timestamp token is {kind: "rfc3161"|"dev", tsa_name, token_b64} where token_b64 is base64
of the DER token (rfc3161) or a canonical-JSON token (dev, non-production/offline only).
The exported chain proves no insertion/deletion/reorder without disclosing who did what. It
contains algorithm (sha256), length, head_hash, a per-item items summary, and entries[].
Each exported entry is:
{ seq, action, item_id, hlc, actor_commitment, details{…}, prev_hash, entry_hash }
with entry_hash = SHA-256(canonical_json({seq, action, item_id, hlc, actor_commitment, details(sorted), prev_hash})) and prev_hash linking to the prior entry_hash (genesis = 64
zeros). action is one of captured, imported, fixity_checked, timestamped, viewed, copied_for_sharing, included_in_packet, note_added. The clear actor, the per-entry salt, the
Ed25519 signature, and any identity/PII private_details are vault-only and never appear here —
so a recipient confirms the chain is intact but cannot learn the actors. See
crypto-spec.md §6.2.
The verifiability bridge: because a shared copy is metadata-stripped, its bytes differ from the
original and cannot hash back to content_hash. A signed copied_for_sharing entry whose details
carry content_hash + shared_hash binds the two; the verifier requires that binding for any item
with shared media.
{ producer_fingerprint, sign_public(b64 Ed25519), bundle_sha256(hex), signature(b64) }
signature is an Ed25519 signature over the ASCII hex of bundle_sha256, which must equal the
SHA-256 of the bundle.json bytes.
- SemVer on the format, independent of the package. The packet format and the verification
protocol are versioned by
packet_version, separately from thehabitablepackage version. - Old packets keep verifying. A change that could break verification of an existing packet is a
major
packet_versionbump with a migration note — never a silent change. A committed golden-packet corpus enforces this in CI. - Additive within a major. New optional fields may appear within a
packet_version. Consumers must ignore unknown fields (the JSON Schema setsadditionalProperties: trueat the document and object level for exactly this reason) and must not assume field order — the bytes are sorted, but treat the document as a mapping. - Forward rejection. A verifier that meets a
packet_versionnewer than it supports rejects the packet cleanly rather than guessing.
- Verify a packet (or cross-check it with general tools):
verifier-decision-table.md. - Embed verification in your own software:
embedding-the-verifier.md. - How the evidence is produced and what it proves:
evidence-method.md.