Skip to content
C2PA

A Stream Copy Is Enough to Destroy Content Credentials

Sighting.aiAugust 27, 202610 min read

Run these three commands and you can watch a cryptographic provenance record disappear.

```bash curl -sL -o truepic.mp4 \ https://raw.githubusercontent.com/c2pa-org/public-testfiles/main/legacy/1.4/video/mp4/truepic-20230212-zoetrope.mp4 ffmpeg -i truepic.mp4 -c copy remuxed.mp4 grep -ac c2pa truepic.mp4 remuxed.mp4 ```

The grep reports a count of 4 for the original and 0 for the remux. `truepic.mp4` is the C2PA-signed clip from the standard's own public test files: 15,456,823 bytes, with a `uuid` box 30,192 bytes long sitting at byte 24, tagged with C2PA's registered identifier `d8fec3d61b0e483c92975828877ec481` — the same constant our parser pins as `_C2PA_BMFF_UUID`. That box is the Content Credential. After the remux the file is 15,427,264 bytes and there is no `uuid` box in it at all.

Nothing was re-encoded. `-c copy` is a stream copy — ffmpeg reads the encoded video and audio out of one container and writes them into another without asking a codec to touch them. You can confirm that the pictures are untouched:

```bash ffmpeg -v error -i truepic.mp4 -map 0:v -c copy -f md5 - # a6862f2e9fdc58ff6f22d941e8de4f87 ffmpeg -v error -i remuxed.mp4 -map 0:v -c copy -f md5 - # a6862f2e9fdc58ff6f22d941e8de4f87 ```

Same digest. The audio streams match too. Every encoded bit of picture and sound survived; the wrapper around them did not, and the provenance lived in the wrapper. (Run on ffmpeg 8.0.1. The result is recorded in `evidence/DETECTION-STATUS.md`, and this is a fresh reproduction of it.)

What exactly did the remux change?

It replaced the container and everything the container was carrying, including the manifest and the capture timestamp. The 29,559 bytes the file lost account for themselves exactly: the C2PA `uuid` box, −30,192; the padding `free` box shrinking from 3,192 bytes to 8; the movie index `moov` growing from 5,357 to 9,174 as ffmpeg rewrote it in its own style; `ftyp` gaining 8 bytes because ffmpeg declares twice as many compatible brands; and `mdat` shrinking by 8. Those five figures sum to −29,559.

The metadata fields tell the same story more plainly.

| Field | `truepic.mp4` | `remuxed.mp4` | | --- | --- | --- | | `major_brand` | `mp42` | `isom` | | `compatible_brands` | `isommp42` | `isomiso2avc1mp41` | | `creation_time` | `2023-02-12T18:40:19.000000Z` | absent | | `com.android.version` | `13` | absent | | `encoder` | absent | `Lavf62.3.100` | | C2PA `uuid` box | present, 30,192 bytes | absent |

Before the remux, the container says an Android 13 device wrote it in February 2023. After, it says libavformat 62.3.100 wrote it — which is true, and is a fact about my laptop, not about the footage. One command turned a capture record into a record of the last program that handled the file.

Does uploading to a social platform strip Content Credentials?

We have not measured any specific platform's pipeline, and we are not going to claim we have. What we can say is the direction, and the direction follows from the experiment above rather than from anybody's policy page: an upload that re-encodes is strictly more destructive than a stream copy, and a stream copy already removed everything.

Two independent mechanisms are at work, and they fail differently.

The first is that the manifest is *in* the file, as a box in the container. Any program that writes a new container decides for itself which boxes to carry over, and a program that has never heard of C2PA carries none. That is what you just watched happen.

The second survives even a pipeline that deliberately copies the manifest across. C2PA binds a manifest to content with what the specification (version 2.1, which is the text quoted throughout here) calls a hard binding, defined in §2.3.12 as "one or more cryptographic hashes that uniquely identifies either the entire asset or a portion thereof." For video that hash covers the box structure as delivered. Change the box structure — which the table above shows a remux doing thoroughly — and the hash no longer matches, so a reader reports a broken content binding rather than a verified one. A manifest that arrives with the wrong bytes is not provenance; at best it is a reason to look harder.

This is why our video engine caps what it will conclude from a container. Its container-structure observations — brand, box order, index placement, handler names — each carry this precondition, verbatim from `inference/app/engines/video_ladder.py`:

> Attributes the last program to write this file, not the origin of the footage. Any transcode - including every upload to a social platform - replaces it.

What about a screenshot, or a screen recording?

A screenshot has no manifest to strip, because it was never the same file. Your operating system asked the display for pixels and wrote a new image, in a new container, built by a different program. There is nothing to remove and nothing to preserve; the output is a fresh asset whose only honest provenance is "this machine captured a screen at this time," which nobody signs. A screen recording is the same thing repeated frame after frame, with a fresh encode on top of it.

That distinction matters for what could work instead. The manifest lives in the container and dies with it.

Why only three of our six generated test videos were first-party bytes

Our video corpus is nine clips — three camera captures and six generated — and `evidence/video-corpus.md` records, for each generated clip, whether the bytes came from the generator or from something downstream. Only three qualify. Not carelessness about sourcing: the phenomenon this post describes had already happened to the rest before we ever saw them.

Two of them are CogVideoX gallery videos whose frame count, resolution and muxer tag match the model's own export path. The third is the Google-signed clip whose `bmffHash` matches its exact bytes, which is the only cryptographic answer among the generated clips. The other three reached us through a HuggingFace dataset archive or a repository's assets directory, and whatever tooling assembled those rewrote the container on the way. A re-hosted clip is close to worthless for provenance work. It is still useful for other things — a burned-in visible watermark, like the static "PIKA LABS" mark in one of the clips, is part of the picture and travels with it.

What a container can still tell you, and what it cannot

It can tell you what last wrote the file, which is useful for some questions and useless for the one people want answered. We tested the useful-sounding version: build a table mapping known container writer strings to known generators, and read the generator off the file.

The result was that **0 of 6 generators self-identified**. The strings we found named the pipeline, not the model. One Kling clip carries `encoder: Lavf60.16.100` — libavformat again, the fingerprint of whoever assembled the dataset. The single string in the whole corpus that names a vendor outright, `(c)too = "Google"`, sits on the one file whose signed manifest had already settled the question, so it added nothing. That table was not built. In our engine, the rung that reads container structure is capped so it can never decide a verdict, however suggestive its observations look.

Why a detector must not treat missing Content Credentials as suspicious

Because provenance is fragile in one direction only. It survives almost nothing, so its absence tells you almost nothing — the population of files with no manifest contains every honest photograph that was ever emailed, resized, or posted, alongside every generated clip. A detector that reads absence as a signal is really reading "this file has been handled," which is true of nearly all media.

The numbers on our own bench say the same. Five of the six generated clips in the corpus carry no manifest, so nothing in the video lane can speak about them and they return `uncertain`, 50, with an interval of [20, 80]. That is the common case and it is the honest answer. The rule runs the other way too, and harder: nothing in this product can call content human-made without positive provenance of the exact bytes in front of it. A missing manifest is not evidence of a human, and it is not evidence of a machine. When our engine finds no manifest, the report says so in these words:

> Absence of a manifest is not evidence of human authorship - social platforms strip manifests from everything they transcode.

The intended fix, which we do not implement

The standard already anticipates this problem. Alongside the hard binding it defines a soft binding, §2.3.13: "a content identifier that is either (a) not statistically unique, such as a fingerprint, or (b) embedded as an invisible watermark in the identified digital content." Because a soft binding is computed from or embedded in the content rather than hashed over the file, it can match "even if the underlying bits differ," and §9.3 states the consequence directly: "if a C2PA manifest is removed from an asset, but a copy of that manifest remains in a provenance store elsewhere, the manifest and asset may be matched using available soft bindings."

That is what the Content Authenticity Initiative's documentation calls Durable Content Credentials — the manifest kept in an online database, with a watermark or a fingerprint used to find it again after a platform has stripped the file. Watermarks are pushed into the content; fingerprints are computed from it; combining both is more robust than either.

We ship neither, and the report says so rather than staying quiet about it. Our watermark rung is an interface with a null decoder that always reports `skipped`, and the reason is not laziness. The obvious first decoder reads one family of watermarks — 3 of the 53 soft-binding algorithms on C2PA's approved list — and its presence test is decode success alone, which models out around a 2.5% false accept per attempt. On a rung ranked above forensics, a few percent chance of inventing a watermark on a clean camera photo is disqualifying, so shipping any real decoder here requires a measured false-accept rate against a clean corpus first, and we have not run that measurement. Our fingerprint rung does compute a perceptual hash for every keyframe, but there is no index anywhere to look those hashes up in, so it reports them and decides nothing. A hash nobody looked up is not evidence.

Consequently our report can never say "no watermark found." It can only say that no *named* algorithm recovered a payload, which is a weaker and true statement.

If you need provenance to survive, keep the original

Practical, in order of how much they help:

  • Archive the original file the moment you receive it, before anything else touches it. This is the only step that reliably works, and every step below is a partial substitute for it.
  • Transfer originals as file attachments or downloads, not through pipelines that re-encode. Whether a manifest survives a transfer is exactly whether the bytes were copied rather than rewritten.
  • Ask for the original when you are given a screenshot. A screenshot of a signed photograph carries none of the signature, and asking costs nothing.
  • Verify against the bytes you were handed, not a version something has re-saved. A plain file copy is fine; anything that re-encodes or rewrites on export is not. Check that the hard binding matched — a manifest present but unverified is a different situation from a manifest that binds.
  • Record where a file came from yourself, in whatever system you already use. If your own chain of custody is the only record that survives, make sure it exists.

And when a file arrives with no credentials at all, which will be most of them, the correct reading is that you have learned nothing about how it was made.

C2PAProvenanceVideo Detection