Publish and verify a signed bundle
Move one synced list, as one file, to a machine that cannot or should not reach the publisher directly — and let the machine receiving it verify who produced it before trusting it.
For the byte-level format — enough detail to implement outside Ruby — see
docs/bundle_format.md.
This page is the two commands and the decisions around them.
Export, transfer, import
Section titled “Export, transfer, import”ActiveSanction.export(:ofac_sdn, to: "ofac_sdn.asb", sign_with: private_key)# ...on another host, in another datacentre, next weeksnapshot = ActiveSanction.import("ofac_sdn.asb", verify_with: public_key)snapshot.trusted? # => trueRSA and EC keys, through openssl and nothing else — generate one however
your organization already manages key material; nothing here mints or
distributes keys. Signing is optional: ActiveSanction.import("ofac_sdn.asb")
with no verify_with: loads the bundle unchecked, which is fine for a mirror
you already trust the transport of and simply do not want to attest.
When to reach for this instead of a second sync
Section titled “When to reach for this instead of a second sync”A publisher is down or has changed format without notice. A bundle produced once and distributed is the difference between a bad afternoon at Treasury and a failed deploy for everyone downstream that depends on you.
The installation is air-gapped or privacy-sensitive. It will not send subject names to a third-party API but will happily consume a file somebody else produced — a bundle serves that; a request/response sync cannot.
An audit is asking a question a checksum alone cannot answer. A checksum proves a list is internally intact; a signature proves it is the one that was actually published, by whoever holds the signing key.
Check which failure you got — they mean different things
Section titled “Check which failure you got — they mean different things”ActiveSanction.import("ofac_sdn.asb", verify_with: public_key)
# => Snapshot::Bundle::Corrupt a byte was flipped, or a record edited — fetch it again# => Snapshot::Bundle::UntrustedSignature intact, and signed by somebody else — do not screen against it# => Snapshot::Bundle::Unsigned nobody signed it, and you asked for verification — a subclass of the above# => Snapshot::Bundle::UnsupportedFormat written by a newer active_sanction — upgrade the gemRescue these separately rather than as one generic import failure — a
Corrupt bundle is a transport problem worth retrying the download for; an
UntrustedSignature is a security event worth escalating, not retrying.
Keep verified? if your process needs it
Section titled “Keep verified? if your process needs it”The verified flag survives only as long as the imported snapshot itself
does — write it into a store and read it back, and trusted? is false,
because nothing signed covers what is now sitting in that store’s own
gzipped JSON or table:
store.write_snapshot(snapshot)store.read_snapshot(:ofac_sdn).trusted? # => false — the honest answerIf a downstream process needs to see verified? on its results, hold the
imported snapshot in memory instead of round-tripping it through a directory
store:
client = ActiveSanction::Client.new(storage: ActiveSanction::Storage::Memory.new)client.storage.write_snapshot(snapshot)client.screen(name: "Bosco Ntaganda").first.verified? # => trueTwo properties worth relying on
Section titled “Two properties worth relying on”The same snapshot always produces the same bundle. Records are ordered by the fingerprint the snapshot checksum is built from, the header’s keys have a fixed order, and there is no written-at timestamp in the file. Two mirrors that bundled the same list can be diffed byte-for-byte against each other, which is a useful check that they really are the same list version.
Verification is cheap regardless of list size. The signature covers the header, and the header covers a SHA-256 over the payload — so a verifier settles who published a bundle before inflating any of it, and checking an OFAC-sized bundle costs the same as checking the EU’s.