Skip to content

Latest commit

 

History

History
170 lines (136 loc) · 8.36 KB

File metadata and controls

170 lines (136 loc) · 8.36 KB

Reading and verifying C2PA metadata

How to read the C2PA metadata out of a media file in PHP — extract what the credential claims, inspect the assertions, check whether the asset is marked as AI-generated, and verify the signature. Verification does not need the signing service. This page covers both readers, how to bind the in-process one in Laravel, and the trade-off between them. Back to the README.

Reading without the signing service

Verification needs no signing service. It needs no private key and no certificate either — checking a credential is a function of the asset bytes plus, optionally, a trust list. Until now it needed a service anyway, because reading and signing shared one transport. ExtC2paReader removes that.

pie install ericmann/ext-c2pa      # https://github.com/php/pie
use Provemark\ContentCredentials\Core\Reading\ExtC2paReader;

$reader = new ExtC2paReader($anchorsPem);   // PEM contents, not a path
$report = $reader->read(new Asset($bytes, MediaType::Png));

$report->isVerifiedAiGenerated();   // no HTTP, no service, no key

Both readers implement ReaderInterface and return the same ManifestReport, so choosing between them is an installation decision, not an API one:

$reader = ExtC2paReader::isAvailable()
    ? new ExtC2paReader($anchorsPem)
    : new SigningServiceReader($client, $factory, $factory, $config);

Construction throws ExtensionMissingException when the extension is absent. It deliberately does not fall back to the service reader: a caller who asked for in-process reading and silently got HTTP cannot tell, and the fallback would need a URL and token they never supplied.

In Laravel

Set the mode in config/content-credentials.php; everything resolved from the container — the facade, the jobs, the artisan commands — follows it.

CONTENTAUTH_READER=auto            # service (default) | extension | auto
CONTENTAUTH_TRUST_ANCHORS=/path/to/anchors.pem   # or the PEM contents

⚠️ The default is service, so installing the extension does nothing until you set this. That is deliberate. The two readers run different c2pa-rs versions, and an extension installed for an unrelated reason should not silently change which engine decides your trust verdicts. auto is the setting most people want — but as a choice you made, not one that happened to you.

One property of auto worth knowing: it picks the extension whenever the extension is loaded, without considering the media type. That is harmless today, because both engines sign and read all thirteen supported types. It would stop being harmless if a format were ever readable by one engine and not the other — which is the situation a capability method would exist for, and which does not exist yet. See Stability and support for why that is left unbuilt.

php artisan content-credentials:read <file> prints the engine it resolved and what the configuration asked for, so "which engine produced this report, and was that a choice or a detection?" is answerable without reading config:

reader             : extension (configured: auto)
hasManifest        : true

CONTENTAUTH_TRUST_ANCHORS accepts PEM contents or a path — a path is read for you, because every trust surface underneath this one takes contents and silently verifies nothing when handed a path.

It applies to the extension reader only. The service reader's trust verification is configured on the service, through CONTENTAUTH_TRUST_SETTINGS. Same concept, two places: if you set CONTENTAUTH_TRUST_ANCHORS and the service reader still reports isTrusted() false, that is why.

What you are taking on

  • ericmann/ext-c2pa is at v0.1.0. It is an Automattic VIP product built for a WordPress plugin, not neutral infrastructure, and its API may move. The adapter is the containment: a break is one class to fix, and callers see nothing.
  • The two readers run different engines. The extension carries c2pa-rs 0.89.0; the signing service carries 0.90.16. They agree today — an integration test compares both readers accessor by accessor on the same asset, and that test is what would tell us they had stopped. Run it with vendor/bin/pest --group=SPEC-019 before relying on a mixed setup.
  • Signing still goes through the service. The extension can sign too, and this library does not expose that: it would put the private key in your web process, which is the one thing this architecture exists to avoid. Reading in-process while signing through the service is a supported, and probably the best, combination.

What the report tells you

Both readers return the same ManifestReport. It answers four different questions, and keeping them apart is the point — a file can be marked as AI-generated by a signature nobody trusts.

Question Accessor
Is there a credential at all? hasManifest(), activeManifestLabel()
Does it claim AI? isAiGenerated(), involvesGenerativeAi(), digitalSourceTypes(), softwareAgents()
Does the cryptography hold? isSignatureValid(), hasTimestamp(), validationState(), validationStatusCodes()
Do you trust who signed it? isTrusted(), signer()
Which rules does it claim to follow? declaredSpecVersion()

One inherited exception to that declaration. Manifests signed by this package declare 2.4.0, and satisfy it — with one departure we do not control. The engine generates a thumbnail of the asset and places it in the manifest's gathered_assertions, the field C2PA defines for assertions "provided to the claim generator by other components in the workflow". That thumbnail was made by the generator, so the placement contradicts what the field means. It is c2pa-rs's default, it affects every tool built on it including c2patool, and upstream tracks it as c2pa-rs #2106 with a fix that moves the thumbnail rather than removes it. We keep the thumbnail and record the exception rather than delete a useful feature to make a declaration look tidier.

isVerifiedAiGenerated() is the two middle columns at once — marked and the signature checked out — which is usually the one you want.

Two distinctions that catch people out:

  • isAiGenerated() means exactly trainedAlgorithmicMedia, generative AI output. involvesGenerativeAi() is the wider question; it is false for algorithmicMedia, which is an algorithm but not a trained model. See What you can mark.
  • A valid signature is not a trusted certificate. isSignatureValid() can be true while isTrusted() is false, and for test certificates it always is. Trust is a separate check against anchors you supply.

A file with no credential is not an error. Reading an ordinary photo gives a report where hasManifest() is false and the rest answers accordingly — no exception, nothing to catch. Most files on the internet are this case.

Which reader, and what it costs

SigningServiceReader sends the asset to the service; ExtC2paReader parses it in-process through ext-c2pa. The usual comparison is operational — no second process, no network hop, faster — and that is real. There is a second difference worth deciding deliberately.

The extension parses untrusted input inside your application process. A manifest arrives as bytes from somewhere you do not control, and verifying it means parsing a container format in native code. With the service reader that parsing happens in a separate, disposable process; with the extension it happens in the PHP worker that also holds your session data and your database connections.

This is the mirror image of the argument in ADR-0003: the signing key is kept out of the web process by putting the signer behind a service, and the extension reader moves parsing in the opposite direction. Neither is wrong — a memory-safety bug in c2pa-rs is not a thing anyone has demonstrated — but the trade is worth making on purpose rather than inheriting it because an extension happened to be installed.

That is also why reader defaults to service and why auto has to be chosen explicitly (SPEC-020): installing the extension for an unrelated reason should not silently move where hostile input is parsed.