Inspiration

AI media tooling is excellent at generation and poor at accountability. From 2 August 2026, Article 50(2) of the EU AI Act requires generative outputs to be "marked in a machine-readable format and detectable as artificially generated or manipulated," with breaches of Article 50 carrying fines up to €15,000,000 or 3% of worldwide annual turnover. Yet there is no clean path from "a model produced this file" to "here is a record a regulator would accept."

Genblaze already produces the raw provenance. Its own docs/features/trust-modes.md names the gap precisely: Mode 1 (integrity) ships today, and it proves self-consistency, not authorship — "a tamperer can modify the asset, recompute the manifest, re-embed, and produce a manifest that verifies against itself." Mode 3, the C2PA bridge, is listed as roadmap. Backblaze B2 already sells the missing half: Object Lock, which makes a record physically unrewritable.

Attestable is the bridge between the two.

What it does

  1. Generates through a five-step chained Genblaze pipeline: key art → image-to-video → voiceover → music bed → FFmpeg composite, with per-stage fallback models.
  2. Seals the deliverable with a signed C2PA Content Credential built from the Genblaze manifest — one C2PA action per generation step, each carrying the provider, model, prompt and seed that produced it, plus the manifest's canonical hash.
  3. Anchors an audit record in Backblaze B2 under Object Lock retention, committing the manifest hash and both asset digests, so an attacker who edits the asset cannot also edit the record that would expose the edit.
  4. Verifies every asset by reading it back before publishing it, and gives anyone a public verification page — no login — showing what the credential proves and, just as importantly, what it does not.
  5. Proves it live. A "Run tamper attempt" button corrupts a copy of the asset and tries to delete its audit record. The credential reports assertion.bmffHash.mismatch; the delete is refused by retention. Nothing is pre-recorded.

How we use Genblaze

Genblaze is the generation and provenance engine, not a wrapper:

  • Chained multi-provider Pipeline with input_from fan-in — motion consumes the key art while voiceover and music run as independent branches that the compositor recombines — and fallback_models per stage.
  • Provider swapping as configuration. Stages resolve at runtime from whichever credentials are present, so Kling → Runway or ElevenLabs → LMNT is a provider-table edit, not an orchestration rewrite. That is the argument the Pipeline API exists to make, so the app is built to show it.
  • The manifest is the product input. Manifest.from_run(run) and canonical_hash are what the C2PA claim is built from and commits to.
  • ObjectStorageSink with manifest_lock — Genblaze's own Object Lock hook, so the manifest lands under retention in the same call that uploads it.
  • Content-addressable keys, streaming step events for the live UI, and StorageBackend implemented a second time so the keyless demo path runs production code.

We built the genblaze-c2pa Mode 3 bridge the maintainers documented but have not shipped. It lives in attestable/c2pa_bridge/, written against the SDK's public data model so it can be lifted out and used standalone. It maps GENERATEc2pa.created/c2pa.edited, MIX/TRANSCODEc2pa.converted, INGESTc2pa.placed, provider+model→softwareAgent, and adds two custom assertions: org.attestable.genblaze.provenance (the pointer and canonical hash back to the manifest of record) and org.attestable.compliance.eu-ai-act.

Digital source types are chosen per step, not applied uniformly. Trained-model output is marked trainedAlgorithmicMedia; a chained model step becomes compositeWithTrainedAlgorithmicMedia; the offline renderer's procedural placeholders are marked plain algorithmicMedia. Labelling a gradient card as trained-model output would be exactly the false provenance claim the project exists to prevent.

Two SDK findings worth reporting upstream, both hit while building this:

  • C2paSignerInfo.ta_url must be a NULL pointer when there is no timestamp authority. Passing b"" makes every sign fail with Signature: empty string, and the Python constructor rejects None.
  • c2pa.created requires a digitalSourceType; omitting it yields assertion.action.malformed at read time while signing appears to succeed.

How we use Backblaze B2

B2 is the system of record, not a file dump. Five prefixes, five different guarantees:

attestable/assets/{sha[:2]}/{sha[2:4]}/{sha}.{ext}     content-addressed, deduplicated
attestable/manifests/{run_id}.json                     Genblaze manifest — Object Lock
attestable/sealed/{asset_id}.{ext}                     deliverable with embedded credential
attestable/c2pa/{asset_id}.c2pa                        detached credential copy
attestable/audit/{tenant}/{yyyy-mm}/{run_id}/seal.json audit seal — Object Lock
attestable/scratch/{run_id}/…                          intermediates — lifecycle expiry
  • Object Lock on manifests/ and audit/; GOVERNANCE for a resettable demo, COMPLIANCE for production, where not even the account root can remove the object before expiry.
  • Lifecycle rules expire scratch/ and abort stale multipart uploads, applied by attestable doctor.
  • Event Notifications on sealed/ fire a webhook so downstream work starts without polling. The endpoint verifies B2's HMAC-SHA256 signature and returns 503 until a secret is configured, rather than trusting unsigned callers.
  • Presigned URLs for the private reviewer view; durable credential-free URLs for anything persisted into a manifest.
  • Bandwidth Alliance CDN support for zero-egress public verify pages. Honestly stated: free egress requires a partner CDN; direct API egress is free to 3× stored bytes, then $0.01/GB.

The local fallback store implements StorageBackend including retention semantics — an object under an ObjectLockConfig refuses later deletes and overwrites, exactly as B2 would. That is why the tamper demonstration is meaningful without credentials: the same call and the same assertion run against either backend.

Providers and models

Stage Provider Model Fallback
Key art GMI Cloud seedream-5.0-lite flux-1-schnell, then OpenAI gpt-image-1
Motion GMI Cloud kling-v3-image-to-video seedance-1-0-pro-fast-251015
Voiceover ElevenLabs eleven_v3 eleven_multilingual_v2
Music bed Stability AI stable-audio-2.5
Composite Genblaze FFmpegCompositor (ffmpeg)
Claim signing c2pa-rs 0.90.1 via c2pa-python 0.37.1 ES256
Offline fallback Attestable LocalSynthProvider procedural (marked algorithmicMedia)

The music bed is generated rather than licensed, so nothing unlicensed ships in an output or in the demo video.

How we built it

Python 3.12, FastAPI + Jinja for the app, SQLite as a rebuildable index over the durable B2 record, an in-process worker pool with SSE progress, c2pa-python for signing, and cryptography for the ES256 development chain. 62 tests run with no credentials.

Challenges

  • Signature: empty string. Every C2PA sign failed identically until we isolated it to an empty ta_url needing to be a NULL pointer.
  • Genblaze's file:// allowlist. Asset transfer accepts local URLs only from the system temp directory — an anti-arbitrary-file-read control. The right fix was to render into temp as the SDK intends, not to widen the allowlist.
  • The digests genuinely do not match. Embedding a credential rewrites the file, so the digest Genblaze committed cannot equal the delivered file's digest. Rather than paper over it, the audit seal records both and each is checked against the artifact it binds.
  • Saying "untrusted" out loud. A development certificate produces valid signatures from an issuer no trust list vouches for. It would have been easy to show a green check. The verify page reports anchored trust and public trust as two separate rows.

What's next

Enrol a C2PA Conformance Programme certificate; add SynthID or a comparable imperceptible watermark as the third layer the Commission's Code endorses; move from an in-process pool to a shared queue for multi-node deployment; and open the genblaze-c2pa bridge as a standalone package with a feature-request issue on the Genblaze repo.

Known limitations

Stated plainly because a provenance tool that overstates itself is self-defeating:

  • The signing certificate is a development certificate. Signatures verify cryptographically, but the issuer is on no trust list, so third-party C2PA viewers will correctly report the signer as untrusted. Production requires a Conformance Programme certificate. The verify page says so on every asset.
  • No imperceptible watermark. We implement the C2PA and registry layers of the Commission's recommended three; a screenshot or re-encode that strips metadata defeats the credential, which is why the detached copy and the B2 registry exist as fallbacks.
  • Attestable helps you comply; it does not certify compliance. That remains the deployer's obligation. The Article 50(2) marking obligation is also deferred to 2 December 2026 for systems already on the market before 2 August 2026, and the Commission's Code of Practice is voluntary guidance rather than a binding technical mandate.
  • Single-node. Jobs run in an in-process thread pool with SQLite as the index. The durable record lives in B2; the database is a rebuildable cache.

Try it in 60 seconds

No API keys and no B2 account required — the app runs the same code paths against an offline renderer and a local store that emulates Object Lock.

  1. Submit the pre-filled brief. Watch the live pipeline, seal and audit events.
  2. Open the verification page and read the per-step provenance table.
  3. Click Run tamper attempt — the credential fails, the delete is refused.
  4. Click Download audit bundle for the single JSON an auditor would receive

Built With

Share this project:

Updates