Skip to Content
GuideFingerprinting

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.

0x20a7b5ffe611944e5773e165d2d2a25d79cea4880a27dbac4624b3158a7aef7e

Every 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?”

KindDetected viaExtra hashAlgorithm
Imagemagic bytesphash64-bit pHash via @stabilityprotocol.com/phash
Audiomagic byteschromaprintAcoustID-compatible Chromaprint via rusty-chromaprint-wasm
Otherfingerprint 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.

FormLooks likeWhere it appears
0x-hex string0x20a7b5ff…ef7ethe REST API, the CLI, GET /api/v1/artifacts/{fingerprint}
base64IKe1/+YRlE5Xc+Fl0tKiXXnOpIgKJ9usRiSzFYp6734=the c2pa.soft-binding assertion value
bytes320x20a7b5ff…ef7eevery contract call, argument, and event topic
raw bytes32 bytes, unencodedwhat 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:

# 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.

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

SurfaceHow it appears
POST /api/v1/artifactsfingerprint in the JSON body or the multipart form
GET /api/v1/artifacts/{fingerprint}the path parameter — the lookup key for every Artifact
Artifact responsefingerprint, plus metadata.pHash for images
C2PA manifestc2pa.hash.data (hard binding) and c2pa.soft-binding value (base64)
On-chain registryregisterManifest(bytes32 fingerprint, …), resolveByBindingDetailed(bytes32)
Drunken-bishop cardthe 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.

ValueAlgorithmCovers
fingerprintSHA-256the asset’s exact bytes — the key for every lookup
c2pa.hash.dataSHA-256the same bytes, as the manifest’s hard binding
manifestHashkeccak-256the Artifact’s metadata JSON — not the C2PA manifest binary
userIdHashkeccak-256the registering userId, then hashed again by ABI encoding into the event topic
transactionHashthe 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.

Next

Last updated on