How to build a manifest, sign an asset and read one back — from Laravel, or from plain PHP with any PSR-18 client. Start with the quickstart in the README if you have not signed anything yet.
The Quickstart covers the short version. This section is the full set of knobs.
composer require provemark/content-credentialsFor the latest unreleased changes, require the main branch:
composer require provemark/content-credentials:dev-mainIn Laravel the service provider and ContentCredentials facade are
registered automatically (package auto-discovery). Publish the config if you
want to tweak it:
php artisan vendor:publish --tag=content-credentials-configSet the service location and shared secret in your .env:
CONTENTAUTH_SERVICE_URL=http://localhost:3000
CONTENTAUTH_API_KEY=your-shared-secret
# Optional HTTP timeouts (seconds) for the signing-service calls:
CONTENTAUTH_TIMEOUT=10
CONTENTAUTH_CONNECT_TIMEOUT=5
# Optional cap (bytes) on a service response the client will buffer (default 96 MiB):
CONTENTAUTH_MAX_RESPONSE_BYTES=100663296These timeouts apply to the HTTP client this package builds for you. If you bind your own PSR-18 client into the container, that client owns its timeouts — PSR-18 has no timeout API, so the package cannot set one on a client it did not construct.
use Provemark\ContentCredentials\Core\Manifest\ManifestBuilder;
use Provemark\ContentCredentials\Core\Manifest\MediaType;
use Provemark\ContentCredentials\Core\Signing\Asset;
use Provemark\ContentCredentials\Laravel\ContentCredentials;
$bytes = file_get_contents('image.png');
// 1. Describe the asset as AI-generated (EU AI Act Art. 50 marking).
$manifest = ManifestBuilder::forAiGenerated(MediaType::Png)
->withSoftwareAgent('ACME GenAI Image Model', '3.1.0')
->withClaimGenerator(config('app.name'), '1.0.0')
->build();
// 2. Sign it via the service.
$signed = ContentCredentials::sign(new Asset($bytes, MediaType::Png), $manifest);
file_put_contents('signed.png', $signed->bytes); // never re-encode these bytes
// 3. Read the credential back.
$report = ContentCredentials::read(new Asset($signed->bytes, MediaType::Png));
// The marking, verified: true only if the signature also checked out.
$report->isVerifiedAiGenerated(); // true
// The first question, and the one that decides whether the rest mean anything:
// an asset with no Content Credentials reads as an empty report rather than an
// error, and every accessor below then answers false or empty.
$report->hasManifest(); // false for an unsigned or stripped asset
// The individual pieces. Note isAiGenerated() reports what the manifest
// CLAIMS — it does not imply the signature verified, so gate on
// isSignatureValid() before acting on it.
$report->isSignatureValid(); // true — integrity verdict
$report->isAiGenerated(); // true — the claim
$report->digitalSourceTypes(); // ['http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia']
$report->softwareAgents(); // [SoftwareAgent('ACME GenAI Image Model', '3.1.0')] — generated by what
$report->involvesGenerativeAi(); // true — wider than isAiGenerated(); see docs/marking.md
$report->signer()?->issuer; // e.g. "C2PA Test Signing Cert"
$report->hasTimestamp(); // true when signed with a trusted timestamp (see "Going to production")
$report->isTrusted(); // true only when the service verified against a trust list
$report->declaredSpecVersion(); // e.g. '2.4.0' — which C2PA rules the generator says it followed, or nullClaims versus verdicts.
isAiGenerated(),signer(),digitalSourceTypes(),softwareAgents()anddeclaredSpecVersion()describe what a manifest asserts; they answer for a tampered or unverifiable manifest too.isSignatureValid()andisTrusted()are the verdicts.isVerifiedAiGenerated()combines the marking with the signature verdict, so the safe check is also the short one to write.
softwareAgents() answers the question that follows "is this AI-generated":
generated by what. It returns
Core\Manifest\SoftwareAgent objects — the same type the builder takes — with a
name and an optional version.
foreach ($report->softwareAgents() as $agent) {
echo $agent->name; // 'ACME GenAI Image Model'
echo $agent->version; // '3.1.0', or null when the manifest names none
}Both halves of the Article 50 marking are readable this way: the
digitalSourceType says what kind of source, the software agent says what
produced it, and a compliant marking carries both.
Three details worth knowing before you build on it. The list is distinct and
in first-appearance order, and the same name at a different version counts as
a different agent — a version change is usually the thing worth noticing.
Manifests are untrusted input, so nothing throws: an entry whose name is
not a string is skipped entirely, while a non-string version drops only the
version, because a name read correctly is worth keeping. And an asset with no
manifest, or none naming an agent, gives an empty list rather than an error.
Core depends only on PSR interfaces — inject any PSR-18 client and PSR-17 factories:
use Provemark\ContentCredentials\Core\Manifest\ManifestBuilder;
use Provemark\ContentCredentials\Core\Manifest\MediaType;
use Provemark\ContentCredentials\Core\Signing\Asset;
use Provemark\ContentCredentials\Core\Signing\SigningServiceConfig;
use Provemark\ContentCredentials\Core\Signing\SigningServiceSigner;
use GuzzleHttp\Client; // any PSR-18 client
use Nyholm\Psr7\Factory\Psr17Factory; // any PSR-17 factory
$factory = new Psr17Factory();
$signer = new SigningServiceSigner(
new Client(),
$factory,
$factory,
new SigningServiceConfig('http://localhost:3000', getenv('CONTENTAUTH_API_KEY')),
);
$manifest = ManifestBuilder::forAiGenerated(MediaType::Jpeg)
->withSoftwareAgent('ACME GenAI Image Model', '3.1.0')
->build();
$signed = $signer->sign(new Asset(file_get_contents('in.jpg'), MediaType::Jpeg), $manifest);
file_put_contents('out.jpg', $signed->bytes);Reading works the same way with SigningServiceReader → ManifestReport.