Content Provenance and Watermarking
Content provenance answers the question "where did this image or clip come from, and what happened to it?" Two different mechanisms address it. A signed provenance record is metadata that states the origin and edit history of a file and is protected by a cryptographic signature. A watermark is a signal hidden in the content itself that a detector with the right key can find later. They fail in opposite ways, which is why they are used together, and neither one proves that a piece of content is real or fake.
Signed provenance metadata
The C2PA (Coalition for Content Provenance and Authenticity) specification defines a manifest: a set of assertions about an asset, such as "this was created by a generator" or "this crop was applied", gathered into a claim that carries a digital signature. Each assertion is a labeled statement, and the claim signature is made with the private key of whoever produced or edited the file. A validator checks three things: that the signature is valid, that the assertions have not been changed since signing, and that the manifest belongs to this asset.
The last check is the binding between manifest and content. A hard binding uses cryptographic hashes in signed assertions to detect changes to the covered parts of the asset. Embedded manifest storage and any other specified exclusion ranges are outside that hash coverage. A soft binding stores a fingerprint of the content or relies on an invisible watermark, which is not statistically unique but lets a validator find the right manifest for a copy whose bytes differ, for example after re-encoding.
What a valid manifest establishes is bounded. The signature shows that the holder of a key made these claims and that they are unchanged, and the hash shows that the covered asset data has not changed since. Whether the signer is trustworthy is decided separately, from a list of trusted certificates, and whether the claims are true (that a photo was taken by a camera, say) depends on that signer. A file with no manifest says nothing: metadata is easily removed when a platform re-compresses or strips it.
Embedded watermarks
A watermark is added to the pixels, or to the generation process itself, and detected with a secret key. A minimal scheme adds a pseudorandom pattern of small positive and negative changes seeded by the key, and detects it by correlation: remove the image's own smooth content, multiply what is left by the key's pattern, sum, and normalize so that for an unmarked image the result \(z\) behaves like a standard normal variable. A threshold on \(z\) then fixes the false-positive rate; for a normal null, \(z>4\) happens by chance about 3 times in 100,000. Robust schemes spread the signal so it survives some processing, and a tradeoff follows: the more robust a mark is, the more visible or costly it tends to be.
A runnable comparison
The program below uses a synthetic 64×64 image (a texture, not a photograph). Part 1 signs a claim with an HMAC and a shared key; this stands in for the certificate-based signature of the real standard, so it shows only how a hash binding behaves, and it is not the C2PA format. The "re-encoded" copy is the image with pixel values rounded to multiples of 8, a stand-in for lossy compression. Part 2 computes an average-hash fingerprint: one bit per 8×8 block, set when the block is brighter than the image mean. Part 3 adds a key-seeded ±1 pattern of amplitude 2 and measures \(z\).
import hashlib, hmac, json
import numpy as np
rng = np.random.default_rng(0)
yy, xx = np.mgrid[0:64, 0:64]
img = (128 + 60*np.sin(xx/6) + 50*np.cos(yy/9) + rng.normal(size=(64, 64))*4).clip(0, 255).astype(np.uint8)
# 1. signed manifest with a hard binding (HMAC stands in for a certificate-based signature)
KEY = b"demo-signing-key"
def sha(a): return hashlib.sha256(a.tobytes()).hexdigest()
def sign(claim): return hmac.new(KEY, json.dumps(claim, sort_keys=True).encode(), "sha256").hexdigest()
def validate(a, claim, sig): return hmac.compare_digest(sig, sign(claim)), claim["asset_sha256"] == sha(a)
claim = {"generator": "example-model-v1", "action": "created", "asset_sha256": sha(img)}
sig = sign(claim)
reenc = (np.round(img / 8) * 8).clip(0, 255).astype(np.uint8) # lossy re-save stand-in
print("original (signature ok, hash matches):", validate(img, claim, sig))
print("re-encoded copy (signature ok, hash matches):", validate(reenc, claim, sig))
print("edited claim (signature ok, hash matches):", validate(img, dict(claim, action="camera capture"), sig))
# 2. soft binding by fingerprint: average hash = which 8x8 blocks are brighter than the mean
def ahash(a):
b = a.reshape(8, 8, 8, 8).mean((1, 3)); return (b > b.mean()).flatten()
other = (128 + 60*np.sin(yy/5) + 50*np.cos(xx/11) + rng.normal(size=(64, 64))*4).clip(0, 255).astype(np.uint8)
print("fingerprint distance, original vs re-encoded: %d of 64 bits" % (ahash(img) != ahash(reenc)).sum())
print("fingerprint distance, original vs unrelated : %d of 64 bits" % (ahash(img) != ahash(other)).sum())
# 3. invisible watermark: key-seeded +-1 pattern added at amplitude 2, detected by correlation
def residual(a):
p = np.pad(a.astype(float), 2, mode="reflect")
return a - sum(p[i:i+64, j:j+64] for i in range(5) for j in range(5)) / 25 # remove local mean
def z(a, key):
pat = np.random.default_rng(key).choice([-1., 1.], size=a.shape)
r = residual(a); return (r * pat).sum() / np.sqrt((r ** 2).sum())
wm = img.astype(float) + 2 * np.random.default_rng(42).choice([-1., 1.], size=img.shape)
blur = lambda a: sum(np.roll(np.roll(a, i, 0), j, 1) for i in (-1, 0, 1) for j in (-1, 0, 1)) / 9
print("z, marked, right key: %.1f" % z(wm, 42))
print("z, marked, wrong key: %.1f" % z(wm, 7))
print("z, unmarked, right key: %.1f" % z(img, 42))
print("z, marked then 3x3 blur: %.1f" % z(blur(wm), 42))
print("z, marked then 1-pixel shift: %.1f" % z(np.roll(wm, 1, 1), 42))
zs = [z((128 + 60*np.sin(xx/rng.uniform(4, 9)) + 50*np.cos(yy/rng.uniform(6, 12)) + rng.normal(size=(64, 64))*4).clip(0, 255), 42) for _ in range(300)]
print("unmarked images, z over 300 samples: mean %.2f sd %.2f max %.2f" % (np.mean(zs), np.std(zs), np.max(zs)))
original (signature ok, hash matches): (True, True)
re-encoded copy (signature ok, hash matches): (True, False)
edited claim (signature ok, hash matches): (False, True)
fingerprint distance, original vs re-encoded: 0 of 64 bits
fingerprint distance, original vs unrelated : 30 of 64 bits
z, marked, right key: 23.5
z, marked, wrong key: 1.3
z, unmarked, right key: -1.6
z, marked then 3x3 blur: 1.8
z, marked then 1-pixel shift: -1.9
unmarked images, z over 300 samples: mean -0.62 sd 1.10 max 2.18
Reading the output:
- The hash binding is exact. The original validates on both checks. The re-encoded copy keeps a valid signature but fails the hash check, because its bytes changed, even though it looks the same to a person. An edited claim fails the signature check although the pixels are untouched. A hard binding is strict by design, so a re-save or screenshot breaks it without any attack.
- The fingerprint survives the re-encode (0 of 64 bits differ), while an unrelated image differs in 30 bits, near the 32 expected for random bits. This is the soft binding's job: find the manifest again for a copy that is not byte-identical. It identifies a likely match and carries no proof.
- The watermark is detected with the right key (z = 23.5) and not with the wrong key (1.3) or on the unmarked image (−1.6). After a 3×3 blur the statistic falls to 1.8 and after a 1-pixel shift to −1.9, both below any sensible threshold, because the blur removes the fine pattern and the shift misaligns it with the key's pattern. Over 300 unmarked synthetic images z has mean −0.62 and standard deviation 1.10, which is not exactly the assumed standard normal; a deployed detector has to measure its false-positive rate on a large sample of real images.
What each can and cannot prove
| Signed manifest | Watermark | |
|---|---|---|
| Carries | Rich claims: generator, edits, signer | A few bits, often just "marked with key K" |
| Survives re-encoding or screenshot | Hard binding does not; metadata is often stripped | Designed to, for some transformations |
| Removal | Trivial: delete the metadata | Possible with attacks such as blurring, resizing, regeneration |
| Forgery | Requires the signing key | Possible for anyone who learns the scheme or key |
| If absent | The file's history is unknown | The detector found nothing, which does not make the content real |
| If present | A named signer made these unmodified claims | A mark of this key was found at a stated false-positive rate |
Coverage depends on who adds the record or mark. A model provider's watermark covers outputs it marks; an open-weight model can be run without it. A provenance manifest can record camera capture, editing, or model generation when the device or software signs those assertions. A later editor's signature establishes that editor's claims, not an unsigned capture history. The outcome of a check, such as "valid signature by this signer" or "mark detected", says nothing alone about whether the depicted scene happened.
Neighbors
Fingerprinting and reverse image search find copies of known content and do not embed anything. Steganography hides arbitrary messages and does not need to resist removal. Detection classifiers guess whether content was generated from its statistics; they carry no cryptographic evidence and degrade as generators improve.
References
Discover more from Insightful Data Lab
Subscribe to get the latest posts sent to your email.
