Skip to content

Bundle format

docs/bundle_format.md is the canonical specification — complete enough to be produced and read by something that is not this gem and not Ruby. It ships inside the gem, in docs/, so an offline implementer always has it. This page is the field reference: the container shape, the header fields, and the four failure modes, for a reader who already knows the format and wants the fields without re-reading the prose.

ofac_sdn.asb
├── ACTIVESANCTION-BUNDLE/1 magic, and the format version
├── {"format_version":1,...} header: one canonical JSON line
├── ecdsa-sha256 MEUCIQ... signature, or "-"
└── <deflated NDJSON> records, one JSON object per line, to EOF

Each of the first three lines is text, at most 65,536 bytes including its terminator. head -c 512 file.asb names the list, its record count, when it was fetched, and who signed it, with no tooling and nothing decompressed.

File.open("ofac_sdn.asb", "wb") { |io| ActiveSanction::Snapshot::Bundle.write(snapshot, io: io) }
File.open("ofac_sdn.asb", "rb") { |io| ActiveSanction::Snapshot::Bundle.read(io) }

One JSON object, keys in this exact order. A reader refuses one it does not recognize, and refuses a header carrying a key it does not know.

#KeyTypeNotes
1format_versionintegerEquals the version in the magic line
2gem_versionstringThe active_sanction that serialized the records
3generatorstringWho published this bundle — "active_sanction/1.0.0", or a mirror’s own name
4sourcestringThe list’s key, e.g. "ofac_sdn"
5schema_versionintegerSnapshot::SCHEMA_VERSION the records are written under
6snapshot_checksumdigestOver the records’ content
7record_countintegerLines in the uncompressed payload
8fetched_atstringRFC 3339, UTC, whole seconds — "2026-08-28T09:30:00Z"
9source_versionstring | nullThe publisher’s own version string, where it gives one
10payload_encodingstring"ndjson" in v1
11payload_compressionstring"deflate" in v1
12payload_digestdigestSHA-256 over the uncompressed payload
13payload_bytesintegerLength of the uncompressed payload

No written-at timestamp, and nothing describing the compressed bytes — either would make two writes of one snapshot two different files.

- for an unsigned bundle, or an algorithm name, a space, and RFC 4648 §4 base64 with padding. The signed bytes are the header line’s alone, without its terminator — because the header carries payload_digest, a few hundred signed bytes stand for every record in the file.

AlgorithmKeySignature
rsa-sha256RSARSASSA-PKCS1-v1_5 over SHA-256
ecdsa-sha256ECECDSA over SHA-256, DER-encoded
ed25519—Reserved. Not produced or verified by v1 readers

Verification is opt-in: a reader given no key does not look at this line, and an unsigned bundle is fully valid — its records are still proven against payload_digest and snapshot_checksum. What it is not is attested. Snapshot#trusted? says which; MatchResult#verified? carries it to a screening result.

ConditionRaises
Not a bundle, truncated, digest or record-count mismatch, trailing bytes, oversized payload, magic disagrees with headerSnapshot::Bundle::Corrupt
Signature does not verify under the key givenSnapshot::Bundle::UntrustedSignature
No signature, and verification was asked forSnapshot::Bundle::Unsigned (a subclass of the above)
Format version, snapshot schema, payload encoding or signature algorithm this reader does not implementSnapshot::Bundle::UnsupportedFormat

Full detail, including retryable status and every other error this library raises, is on the error hierarchy page.

  • A reader must refuse a format_version above what it implements, before reading anything past the magic line, with an error that says to upgrade.
  • A reader must accept every version at or below its own.
  • Within a format version, no key changes meaning, is removed, or is reordered.
  • schema_version moves independently of format_version — a bundle can carry records too new for a reader while its container is a version that reader understands.

The same snapshot produces the same bundle byte for byte, except the signature line — ECDSA is randomized, so two signings of the same header differ even though both verify. Record order is derived from content, header keys have a fixed order, and there is no field anywhere that records when the file was written.