Fingerprinting
Monolith never needs your file. It needs the one value that could only have come from your file.
The fingerprint
A fingerprint is the SHA-256 hash of a file’s exact bytes, written as a
0x-prefixed 64-character lowercase hex string.
0x20a7b5ffe611944e5773e165d2d2a25d79cea4880a27dbac4624b3158a7aef7eEvery surface enforces the same shape — ^0x[0-9a-f]{64}$. Uppercase hex, a
missing 0x, or a bare digest is rejected with a 400.
Two files with identical bytes share a fingerprint. Flip one bit and the result is completely different — not nearly different, completely. That is the property the whole system rests on: a value short enough to put in a URL and specific enough to mean exactly one file.
Exact bytes, nothing else
There is no canonicalization step. Monolith does not normalize, re-encode, strip metadata, or parse the format before hashing. It hashes the bytes it is given, in order, and stops.
So each of these yields a different fingerprint from the original:
- re-saving a JPEG, even at the same quality setting
- adding or stripping EXIF
- a lossless PNG re-compression
- converting between formats
- appending a trailing newline to a text file
None of that is a flaw to work around — it is what makes the fingerprint an exact-match key. Recovering provenance after a re-encode is the job of the soft binding and, informally, of perceptual hashes.
When you verify a c2pa.hash.data binding yourself, read the asset as
application/octet-stream — the format the manifest declares — so the raw
bytes are hashed rather than canonicalized by a format-specific handler.
Nothing more elaborate runs underneath, either. There is no IPFS, no content addressing, and no merkle root anywhere in Monolith — just this hash. See what is stored on-chain.
Perceptual hashes
The fingerprint is brittle by design, so Monolith also computes a perceptual hash when it recognizes the file kind. Where a fingerprint answers “is this the same file?”, a perceptual hash answers “is this the same work, re-encoded?”
| Kind | Detected via | Extra hash | Algorithm |
|---|---|---|---|
| Image | magic bytes | phash | 64-bit pHash via @stabilityprotocol.com/phash |
| Audio | magic bytes | chromaprint | AcoustID-compatible Chromaprint via rusty-chromaprint-wasm |
| Other | — | — | fingerprint only |
Perceptual hashes are advisory. They surface near-duplicates; they never establish identity. Nothing is bound, signed, or registered against one — an Artifact is keyed on its fingerprint alone.
They are deterministic across every surface: the same bytes produce the same
phash and chromaprint in the CLI, the library, and the API.
The two surfaces spell phash differently. The CLI prints it bare
(phash=918b8b916e4aee91); the API returns it 0x-prefixed on
metadata.pHash. Same 64 bits either way.
Representations
A fingerprint is 32 bytes. Which of its spellings you need depends on what you are talking to.
| Form | Looks like | Where it appears |
|---|---|---|
0x-hex string | 0x20a7b5ff…ef7e | the REST API, the CLI, GET /api/v1/artifacts/{fingerprint} |
| base64 | IKe1/+YRlE5Xc+Fl0tKiXXnOpIgKJ9usRiSzFYp6734= | the c2pa.soft-binding assertion value |
bytes32 | 0x20a7b5ff…ef7e | every contract call, argument, and event topic |
| raw bytes | 32 bytes, unencoded | what all three of the above encode |
bytes32 and the API’s hex string are the same characters — a contract takes
the string you already have. base64 is the one that catches people out.
Converting between the two is decoding hex to 32 bytes and re-encoding those bytes as base64, or the reverse. No library is required:
bash
# 0x-hex → base64 (what goes in the assertion)
printf '%s' "${FP#0x}" | xxd -r -p | base64
# base64 → 0x-hex (what the contract and the API expect)
printf '0x%s\n' "$(printf '%s' "$VALUE" | base64 -d | xxd -p -c 64)"Base64-encoding the hex string instead of the raw bytes is the single most common mistake when integrating against soft bindings. The correct value decodes to exactly 32 bytes; the mistaken one decodes to 66 characters of text. See Soft binding.
Computing one yourself
Hash the file’s bytes with SHA-256, lowercase the hex, prefix 0x.
bash
printf '0x%s\n' "$(shasum -a 256 my-art.png | cut -d' ' -f1)"All of these produce the same string for the same file, on any machine, at any time. There is no salt, no key, and no server involved.
Where the fingerprint shows up
| Surface | How it appears |
|---|---|
POST /api/v1/artifacts | fingerprint in the JSON body or the multipart form |
GET /api/v1/artifacts/{fingerprint} | the path parameter — the lookup key for every Artifact |
| Artifact response | fingerprint, plus metadata.pHash for images |
| C2PA manifest | c2pa.hash.data (hard binding) and c2pa.soft-binding value (base64) |
| On-chain registry | registerManifest(bytes32 fingerprint, …), resolveByBindingDetailed(bytes32) |
| Drunken-bishop card | the PNG at signedAssetUrl is a rendering of the fingerprint |
Attaching the asset to POST /api/v1/artifacts is optional, and the Artifact
is signed against the fingerprint you send either way. When you do attach a
file, the uploaded bytes must hash to that fingerprint — a mismatch is a
400, and the bytes are used only for a verifying read-back and never
stored. See Attaching the asset.
Hashes that are not the fingerprint
Several values in the API and the contract look like fingerprints and are not. They differ in algorithm, in what they cover, or both.
| Value | Algorithm | Covers |
|---|---|---|
fingerprint | SHA-256 | the asset’s exact bytes — the key for every lookup |
c2pa.hash.data | SHA-256 | the same bytes, as the manifest’s hard binding |
manifestHash | keccak-256 | the Artifact’s metadata JSON — not the C2PA manifest binary |
userIdHash | keccak-256 | the registering userId, then hashed again by ABI encoding into the event topic |
transactionHash | — | the chain transaction that wrote the record; identifies the write, not the content |
manifestHash and manifestId take their names from the C2PA soft binding
spec, not from Monolith’s own manifests. On-chain, manifestId is the
Artifact’s identifier — neither field hashes or points at the JUMBF
manifest you get from manifestBinaryUrl. See
What is stored on-chain.
What a fingerprint does and doesn’t prove
It proves that a specific sequence of bytes was bound to a specific Mark at a specific time, and that the binding was written to a public record you don’t control. Anyone holding the file can recompute the fingerprint and check that record.
It does not:
- Hide anything. A fingerprint is an identifier, not encryption. Anyone with the same file computes the same value, and a fingerprint you publish can be tested against any file someone already has.
- Reveal anything, either. It is one-way — the file cannot be reconstructed from it. Monolith stores the fingerprint, never your bytes.
- Survive re-encoding. A different encoding is a different fingerprint. Recovery after re-encoding is what the soft binding is for.
- Adjudicate authorship. It records who claimed a file first, not who made it. Once a fingerprint has a non-failed Artifact, no second Artifact can be created for it — the first Mark wins.
Collisions are not a practical concern: no SHA-256 collision has ever been
produced, and the contract advertises sha256 as its only supported
algorithm.