Skip to content
AIAssuranceLabs
Menu

Verify content credentials: C2PA manifest validation step by step

This guide validates the provenance manifest on one synthetic AI-generated image with the published c2patool command-line tool, as the metadata layer check of PR-50-MARK v1.0 under Art. 50(2). The validation rules come from C2PA Content Credentials specificationPublished, a published specification. At the end the reader holds a manifest-validation-log entry per sample: manifest present or absent, signature and hard-binding state, trust-list result, and whether the c2pa.actions assertion declares the content as generated by a trained model.

What the obligation asks for

Art. 50(2) of Regulation (EU) 2024/1689 requires the provider of an AI system that generates synthetic audio, image, video or text to mark the outputs in a machine-readable format and to make them detectable as artificially generated or manipulated, with technical solutions that are effective, interoperable, robust and reliable as far as technically feasible. The duty applies from the general application date In force; a system placed on the market before that date has a grace period for this duty only Deferred. The Art. 50 obligation page lists every element with its split. This guide covers three testable elements of the record: every synthetic output carries a machine-readable mark on generation, the mark is readable by a tool other than the provider's own, and the mark survives a defined transformation set, each verified on the metadata layer only.

The Act names no technique. Code of Practice on Transparency of AI-Generated ContentPublished, a published code under Art. 50(7), names digitally signed and time-stamped metadata as one marking layer in Sub-measure 1.1.1 and states in Measure 3.4 that, at publication, interoperability standards exist for signed metadata and for no other technique. C2PA Content Credentials specificationPublished is the published specification for that layer; it is a technique reference on the standards page, and no harmonised standard is cited in the Official Journal for Art. 50, so no presumption of conformity attaches to any technique. Which technique set counts as adequate under Art. 50(2) is an unsettled point. The block below shows both readings; this guide states no reading of its own, and a validated manifest is evidence for the metadata layer under either reading.

Inputs

The inputs below are the subset of the PR-50-MARK v1.0 input table that this guide uses. The full table is on the protocol page.

  • One or more sampled outputs of the generating system per output path, with the hash and the generation record of each (protocol step 11).
  • The marking architecture description naming the manifest as a layer, the pipeline point where it is signed, and the signing certificate issuer (protocol step 4).
  • The provider's own manifest reader or its result per sample (protocol step 23).
  • The published c2patool binary at a recorded version, with network access once to fetch a trust list.
  • openssl, python3 and ffmpeg on the test machine, at recorded versions.
  • A test signing certificate and key, only where the guide is rehearsed on a self-signed synthetic asset before the sampled outputs are available.

Procedure

Each step names the protocol step or the specification clause it derives from and the record it produces. Code blocks follow the step they belong to. Every file name, label, serial number, issuer and result in the examples is synthetic.

  1. Install c2patool, fetch the C2PA trust list once, and record the versions and the trust list used.

    Record tool-register: c2patool version, trust list name and fetch date, openssl and python3 versions

    [C2PA Content Credentials specification §14.4.1; PR-50-MARK step 10]

  2. Store each sampled output with its hash and generation record, or sign one synthetic asset for a rehearsal.

    Record generation-log: sample id, output path, sha256 of the file, prompt or seed, model version

    [PR-50-MARK step 11]

  3. Run the information report on each sample and record whether an active manifest is present.

    Record manifest-presence-log: sample id, manifest present or absent, manifest store size

    [C2PA Content Credentials specification §15.5; PR-50-MARK step 12]

  4. Write the full JSON report of each sample to a file and keep it with the sample hash.

    Record report.json per sample: the validation report with its state, results and status arrays

    [C2PA Content Credentials specification §15.2; PR-50-MARK step 13]

  5. Re-run the report with the trust list applied and record whether the signing credential is on it.

    Record manifest-validation-log: trust field, Trusted or Valid or Invalid, with the trust list name

    [C2PA Content Credentials specification §14.3.6 and §14.4.1; PR-50-MARK step 10]

  6. Extract the certificate chain from the active manifest and record the issuer and the end-entity subject.

    Record manifest-validation-log: signer field, certificate issuer and subject, validity dates as read

    [C2PA Content Credentials specification §14.5; PR-50-MARK step 13]

  7. Read the c2pa.actions assertion and record the digitalSourceType on the c2pa.created action.

    Record metadata-content-log: sample id, AI-generated statement present or absent, digitalSourceType value

    [C2PA Content Credentials specification §18.12; Code of Practice on Transparency of AI-Generated Content, Section 1, Sub-measure 1.1.1; PR-50-MARK step 14]

  8. Run the Python check over each report and write the log entry for the sample.

    Record manifest-validation-log: one entry per sample from the Python check

    [C2PA Content Credentials specification §15.7, §15.8 and §15.12; PR-50-MARK step 13]

  9. Compare the state from step 8 with the provider's own reader result for the same sample.

    Record independent-reader-log: sample id, c2patool result, provider reader result, agreement yes or no

    [C2PA Content Credentials specification §15.2; PR-50-MARK step 23]

  10. Re-encode one copy of each sample, repeat step 3 on the copy, and record the metadata layer as survived or lost.

    Record survival-matrix: sample, transformation, layer metadata, survived or lost

    [Code of Practice on Transparency of AI-Generated Content, Section 1, Measure 3.3; PR-50-MARK steps 19 and 22]

  11. File the log entries in the third-party detectability verification report with the tool register.

    Record Third-party detectability verification report (PR-50-MARK output): manifest-validation-log and independent-reader-log attached

    [AIAssuranceLabs procedure; PR-50-MARK step 42]

Example for steps 1 to 6. The commands use the flags as documented for c2patool: -V prints the version, init trust fetches the C2PA trust list once and caches it next to the settings file, --info prints the presence report, the bare invocation prints the JSON report, the trust subcommand with --trust_anchors applies an explicit trust list for one run, and --certs prints the certificate chain in PEM. The --create trainedAlgorithmicMedia line signs a synthetic rehearsal asset with the tool's built-in test certificate, which the tool warns is for development only; the sampled outputs of the system under test are signed by the provider's pipeline, not by the tester. The file names are synthetic.

# Synthetic example: IMG-0001.jpg is one sampled output of the system under test.
c2patool -V                                  # step 1: version into tool-register
c2patool init trust                          # step 1: fetch and cache the C2PA trust list
sha256sum IMG-0001.jpg                       # step 2: hash into generation-log

# Optional rehearsal on a self-signed synthetic asset before real samples exist.
c2patool blank.jpg -c '{"assertions":[]}' --create trainedAlgorithmicMedia -o REH-0001.jpg

c2patool IMG-0001.jpg --info                 # step 3: manifest present or absent
c2patool IMG-0001.jpg > IMG-0001.report.json # step 4: full JSON report
c2patool IMG-0001.jpg --detailed > IMG-0001.detailed.json   # step 4: low-level view

# Step 5: one run against an explicit trust list, in place of the cached one.
c2patool IMG-0001.jpg trust \
  --trust_anchors='https://raw.githubusercontent.com/c2pa-org/conformance-public/refs/heads/main/trust-list/C2PA-TRUST-LIST.pem' \
  > IMG-0001.trust.json

# Step 6: certificate chain of the active manifest, issuer and subject of the end-entity.
c2patool IMG-0001.jpg --certs > IMG-0001.chain.pem
openssl x509 -in IMG-0001.chain.pem -noout -issuer -subject -dates

Example for step 7. The excerpt below is a synthetic JSON report in the shape the tool writes: a label for the active manifest, the manifests keyed by label, the assertion list of the active manifest, the signer summary, and the validation arrays. The c2pa.actions.v2 assertion carries a c2pa.created action whose digitalSourceType is the IPTC value for media produced by a trained model; that value is the machine-readable statement that the content is AI-generated for the metadata layer. The validation_results.activeManifest.failure array holds one entry, signingCredential.untrusted, so the state is Valid and not Trusted. Every label, serial number, issuer and time in the excerpt is synthetic.

{
  "active_manifest": "urn:c2pa:0f3a9c2e-synthetic-0001",
  "manifests": {
    "urn:c2pa:0f3a9c2e-synthetic-0001": {
      "claim_generator_info": [{ "name": "synthetic-image-service", "version": "0.0.0" }],
      "title": "IMG-0001.jpg",
      "format": "image/jpeg",
      "instance_id": "xmp:iid:synthetic-0001",
      "assertions": [
        {
          "label": "c2pa.actions.v2",
          "data": {
            "actions": [
              {
                "action": "c2pa.created",
                "softwareAgent": { "name": "synthetic-image-service", "version": "0.0.0" },
                "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia"
              }
            ],
            "allActionsIncluded": true
          }
        },
        { "label": "c2pa.hash.data", "data": { "alg": "sha256", "name": "jumbf manifest" } }
      ],
      "signature_info": {
        "alg": "Es256",
        "issuer": "Synthetic Test CA",
        "cert_serial_number": "000000000000000001",
        "time": "<time-stamp as read>"
      }
    }
  },
  "validation_status": [
    {
      "code": "signingCredential.untrusted",
      "url": "self#jumbf=/c2pa/urn:c2pa:0f3a9c2e-synthetic-0001/c2pa.signature",
      "explanation": "signing certificate untrusted"
    }
  ],
  "validation_results": {
    "activeManifest": {
      "success": [
        { "code": "claimSignature.insideValidity", "url": "self#jumbf=/c2pa/urn:c2pa:0f3a9c2e-synthetic-0001/c2pa.signature" },
        { "code": "claimSignature.validated", "url": "self#jumbf=/c2pa/urn:c2pa:0f3a9c2e-synthetic-0001/c2pa.signature" },
        { "code": "timeStamp.validated", "url": "self#jumbf=/c2pa/urn:c2pa:0f3a9c2e-synthetic-0001/c2pa.signature" },
        { "code": "assertion.hashedURI.match", "url": "self#jumbf=/c2pa/urn:c2pa:0f3a9c2e-synthetic-0001/c2pa.assertions/c2pa.actions.v2" },
        { "code": "assertion.dataHash.match", "url": "self#jumbf=/c2pa/urn:c2pa:0f3a9c2e-synthetic-0001/c2pa.assertions/c2pa.hash.data" }
      ],
      "informational": [
        { "code": "signingCredential.ocsp.skipped", "url": "self#jumbf=/c2pa/urn:c2pa:0f3a9c2e-synthetic-0001/c2pa.signature" }
      ],
      "failure": [
        { "code": "signingCredential.untrusted", "url": "self#jumbf=/c2pa/urn:c2pa:0f3a9c2e-synthetic-0001/c2pa.signature" }
      ]
    }
  },
  "validation_state": "Valid"
}

Example for step 8. The script reads one report, maps the status codes to the fields of the manifest-validation-log layout on the protocol page, and prints the entry. The state rule follows the specification: Valid needs claimSignature.validated and claimSignature.insideValidity with no untolerated failure; Trusted adds signingCredential.trusted. The code strings are those the C2PA specification defines and the c2pa-rs reader emits; where a tool version names a field differently, the constants are edited and the version is in the tool register. The check treats a missing validation_results block as a report from an older tool and falls back to the validation_status error list.

# Synthetic example: IMG-0001.report.json is the output of step 4 for one sample.
import json
import sys

AI_SOURCE_TYPES = {
    "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia",
    "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia",
}
HARD_BINDING_OK = ("assertion.dataHash.match", "assertion.bmffHash.match",
                   "assertion.boxesHash.match", "assertion.collectionHash.match")


def codes(report, bucket):
    results = report.get("validation_results") or {}
    active = results.get("activeManifest") or {}
    return {s.get("code") for s in active.get(bucket, [])}


def main(path, sample_id, validator):
    with open(path, encoding="utf-8") as f:
        report = json.load(f)

    active_label = report.get("active_manifest")
    manifest = (report.get("manifests") or {}).get(active_label, {})
    success, failure = codes(report, "success"), codes(report, "failure")
    if not success and not failure:                       # older reader: errors only
        failure = {s.get("code") for s in report.get("validation_status") or []}

    source_types = set()
    for a in manifest.get("assertions", []):
        if str(a.get("label", "")).startswith("c2pa.actions"):
            for act in a.get("data", {}).get("actions", []):
                if act.get("action") == "c2pa.created":
                    source_types.add(act.get("digitalSourceType"))

    entry = {
        "sample_id": sample_id,
        "validator": validator,
        "active_manifest": "present" if active_label else "absent",
        "claim_signature": "valid" if "claimSignature.validated" in success
                           and "claimSignature.insideValidity" in success else "invalid",
        "time_stamp": "valid" if {"timeStamp.validated", "timeStamp.trusted"} & success
                      else "absent-or-invalid",
        "credential_revocation": "not-revoked" if "signingCredential.ocsp.notRevoked" in success
                                 else "revoked" if "signingCredential.ocsp.revoked" in failure
                                 else "not-checked",
        "hard_binding": "valid" if any(c in success for c in HARD_BINDING_OK) else "invalid",
        "trust": "trusted" if "signingCredential.trusted" in success
                 else "untrusted" if "signingCredential.untrusted" in failure
                 else "not-checked",
        "ai_generated_statement": "present" if source_types & AI_SOURCE_TYPES else "absent",
        "digital_source_type": sorted(t for t in source_types if t),
        "signer_issuer": (manifest.get("signature_info") or {}).get("issuer"),
        "validation_state": report.get("validation_state"),
        "failure_codes": sorted(c for c in failure if c),
    }
    json.dump(entry, sys.stdout, indent=2)


if __name__ == "__main__":
    main(sys.argv[1], sys.argv[2], sys.argv[3])
    # python3 check_manifest.py IMG-0001.report.json IMG-0001 "c2patool <version>"

Example for step 10. The re-encode below is one row of the transformation set; ffmpeg decodes and re-encodes the JPEG, and the output carries no C2PA manifest unless the writing tool preserves it. The repeated --info records the metadata layer for that transformation. The quality value is a parameter of the synthetic transformation, not a requirement of the Act or of the Code.

# Synthetic example: one transformation row for the survival-matrix.
ffmpeg -i IMG-0001.jpg -q:v 5 IMG-0001.reencoded.jpg
c2patool IMG-0001.reencoded.jpg --info      # expected on most pipelines: no manifest found
sha256sum IMG-0001.reencoded.jpg            # transformed-sample-register: output hash
Synthetic survival-matrix rows for the metadata layer of one sample after steps 3 and 10
sample_idtransformationparameterslayerresult
IMG-0001noneoriginalmetadatapresent, Valid, untrusted
IMG-0001re-encodeffmpeg q:v 5metadatalost
IMG-0001screenshotdisplay-nativemetadatalost

A pitfall that vendor documentation omits

A manifest reader that prints "validated" has answered one of three questions. Under C2PA Content Credentials specificationPublished, a published specification, §14.3 defines three manifest states: Well-Formed, Valid and Trusted. A manifest is Valid when the claim signature receives claimSignature.validated and claimSignature.insideValidity and the content bindings match; it is Trusted only when, in addition, the signing credential receives signingCredential.trusted because its chain leads to an anchor on the validator's trust list under §14.4.1. A provider that signs with a certificate its own reader accepts, but that no published trust list carries, produces manifests that every independent validator reports as Valid with signingCredential.untrusted in the failure array, as in the excerpt for step 7; the usage notes of c2patool show the same code on the tool's own sample image, because its test certificate is on no list. Under Art. 50(2) the mark has to be interoperable, and step 23 of PR-50-MARK v1.0 records agreement between the independent validator and the provider's reader; a Valid result with an untrusted credential is agreement on integrity and disagreement on identity, and the manifest-validation-log records both fields rather than one word. Which trust list the validator consulted, the C2PA trust list or another anchor set, is a field of the tool register, since the same file gives a different trust result under a different list.

The second omission is removal. A manifest is metadata; a re-encode, a screenshot or a platform upload that rewrites the file leaves no manifest to validate, and --info on the copy reports none, as in the example for step 10. Code of Practice on Transparency of AI-Generated ContentPublished, a published code, addresses this in Measure 1.2 as a best-efforts duty on signatories to retain markings on inputs and to prohibit removal in their terms, and it records that signatories cannot control removal by a downstream party; Measure 3.3 asks that marking and detection together survive typical processing operations, and Sub-measure 1.1.2 places a watermark next to the metadata layer for that reason. A validated manifest on the file as generated is therefore evidence for the metadata layer on that file; the survival matrix, filled for each transformation the provider recorded in the transformation set, is the evidence that survives the copy. No published document sets a survival rate; the pass criterion per transformation is the target the provider justified in step 6 of the protocol, and where the Act has no threshold the source hierarchy on the obligation page applies.

What the result shows and what it does not

The log entry shows the state of the manifest on the sampled file on the test date, under the trust list recorded in the tool register. It does not show the presence of a manifest on outputs outside the sample or on output paths not listed in the scope record, and it does not show that the metadata layer survives transformations outside the recorded set. A Trusted result shows that the signing chain led to an anchor on the list consulted; it does not show that the Commission or a market surveillance authority accepts the technique set, which is the unsettled point shown above. The check reads the metadata layer only; the watermark layer and the detection rate on control samples are separate steps of the protocol. The result is evidence for the provider's own file under Art. 50(2); the decision whether the marking meets the four criteria remains with the provider, its notified body where one is required, and its counsel. The Article 50 transparency and marking/watermark testing service runs the full protocol, and its scope limits are on what we cannot do; the report formats are on deliverables.

Related pages

The obligation, the protocol and the hub for the role this guide serves are listed below; every protocol on the site is on the methodology index.

Obligations
Protocols
ICP hubs

Every inquiry receives a reply within 24 hours. Scope and availability are discussed per request.

Ask about scopehello@aiassurancelabs.com