Skip to content
How it works

For developers

How it works, under the hood

Three small services, one HSM and a registry. This page follows a picture through them: what happens to it, what gets written into it, and how it is found again. Everything here is in the open-source repository under Apache-2.0, with a decision record for each choice.

1

The services

Each piece is written in the language with the best library for its job: the C2PA reference implementation is Rust, TrustMark and PDQ are Python, and the API is .NET. They talk gRPC over mutual TLS 1.3, and only the API is reachable from outside.

Your app

Any language

  • REST, the .NET or Python SDK, or proxy mode for fal and Replicate

Anyone checking

Public verify page

  • No key; 30 a minute per address
HTTPS

API

.NET 10, ASP.NET Core

  • Keys, quotas, the sign pipeline
  • Verify and records
  • Rules engine and packs

Registry

Postgres (Supabase, EU)

  • A record per output: hashes, marks, report
  • Never the picture
gRPC, mutual TLS 1.3

Signer

Rust, c2pa-rs (pinned)

  • Builds, signs and validates manifests
  • CAWG identity assertions

Cloud KMS

Google, HSM, Frankfurt

  • Signing keys; never exported

Watermark worker

Python, TrustMark and PDQ

  • Embeds and reads the watermark
  • Computes fingerprints
The API, signer and worker run as containers on Fly.io in Frankfurt. Signed copies, when kept, go to S3-compatible storage for 30 days.

2

Signing, step by step

One call to POST /v1/sign with a JPEG, PNG or WebP. The same pipeline serves proxy mode.

  1. 1API

    Authenticate and count

    The API key (su_live_…, 256 random bits, stored only as a SHA-256 hash) names the company and must carry the sign scope. The month’s allowance is checked; requests are limited to 300 a minute per key.

  2. 2Worker

    Watermark

    A fresh random 61-bit payload is embedded with Adobe’s TrustMark (variant Q). This changes pixels, so it has to happen before the manifest binds them.

  3. 3Worker

    Fingerprint

    A 256-bit PDQ hash of the watermarked image, with PDQ’s quality score, for the case where both the manifest and the watermark are gone.

  4. 4API

    Build the manifest

    A c2pa.created action with the IPTC source type for AI output, a c2pa.watermarked.bound action, and a soft-binding assertion carrying the watermark payload, so each can be found from the other.

  5. 5Signer

    Name the company

    If the company has proved its domain and its name has been approved, the signer adds a CAWG identity assertion with the role cawg.publisher, signed with the company’s identity certificate.

  6. 6Signer

    Sign

    c2pa-rs signs the claim with ES256 through Cloud KMS. The private key is in an HSM and never leaves it; KMS’s DER signature is converted to the fixed-width form COSE expects.

  7. 7Signer

    Validate

    c2pa-rs validates the signed file before it leaves the signer, and an invalid one is refused. The verdict and the full crJSON report are kept with the output’s record.

  8. 8API

    Record

    A registry record with a ULID: the hashes, the watermark payload, the fingerprint, the validation report and the model, but never the picture. A signed copy is kept for 30 days only when asked for.

curl https://signunder-staging-api.fly.dev/v1/sign \
  -H "Authorization: Bearer $SIGNUNDER_API_KEY" \
  -F "file=@picture.png;type=image/png" \
  -o picture.signed.png

Serve the returned bytes unchanged: re-encoding breaks the signature, though the watermark and fingerprint still identify the picture. SDKs: pip install signunder and dotnet add package Signunder. Full reference in the docs.

3

What goes in the manifest

Simplified from what ImageSigner builds and the signer completes. The hard binding (c2pa.hash.data) and the signature are added by c2pa-rs. The identity assertion appears only once the company’s name is approved.

{
  "claim_generator_info": [
    { "name": "SignUnder", "version": "0.1.0", "specVersion": "…" }
  ],
  "assertions": [
    {
      "label": "c2pa.actions",
      "data": {
        "actions": [
          {
            "action": "c2pa.created",
            "digitalSourceType":
              "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia"
          },
          { "action": "c2pa.watermarked.bound" }
        ],
        "allActionsIncluded": true
      }
    },
    {
      "label": "c2pa.soft-binding",
      "data": {
        "alg": "com.adobe.trustmark.Q",
        "blocks": [{ "scope": {}, "value": "<the 61-bit payload, 8 bytes>" }]
      }
    },
    {
      "label": "cawg.identity",
      "data": {
        "signer_payload": { "role": ["cawg.publisher"], "sig_type": "cawg.x509.cose", "…": "…" },
        "signature": "…"
      }
    }
  ]
}

4

How a picture is found again

POST /public/v1/{company}/verify needs no key and is limited to 30 requests a minute per address. It looks only among that company’s own records, and says nothing about anyone else’s pictures.

  1. First

    The manifest

    If Content Credentials are present, c2pa-rs validates them and the manifest leads to the record. The answer says whether the picture has been altered since signing.

  2. Then

    The watermark

    If the manifest is gone, the worker decodes TrustMark from the pixels. The 61-bit payload matches one record exactly.

  3. Last

    The fingerprint

    If both are gone, the PDQ hash is compared with the company’s records. A match needs 31 or fewer of the 256 bits to differ, and the answer says it is weaker evidence.

The fingerprint search scans the company’s records today; it will get an index before any company has many thousands of them.

5

Keys and certificates

Two separate chains of trust end up in one file: one says the claim was signed by a conforming product, the other says which company published it.

The claim signature

  1. 1

    C2PA trust list

    Certificate authorities recognised by the C2PA

  2. 2

    Certificate authority

    Issues a claim-signing certificate once the product passes the conformance programme

  3. 3

    Claim-signing key

    ES256 in Google Cloud KMS, HSM protection, Frankfurt. The CSR is built by the sidecar’s kms-csr tool, signed by KMS itself

Until the listing, signatures validate but viewers show them as unrecognised. The local stack uses a committed development key for the same reason.

The company’s name

  1. 1

    SignUnder Identity CA

    Its key in Cloud KMS HSM

  2. 2

    Company identity certificate

    Issued after a DNS TXT record proves the domain and the name is approved by hand

  3. 3

    CAWG identity assertion

    “Published by Acme Studio (acme.com, verified by SignUnder)”

Validators report the assertion as well-formed but untrusted, because no shared identity trust list includes this CA yet.

The services on Fly.io reach KMS through workload identity federation, so no long-lived cloud credential is stored anywhere.

6

Proxy mode

For fal and Replicate, point the client’s base URL at SignUnder (/proxy/fal/queue/…, /proxy/replicate/v1/…). Your provider key passes through and is never stored; the SignUnder key travels in X-SignUnder-Key.

Follow-up URLs in each response are rewritten to come back through the proxy. When a finished result is fetched, each image is downloaded from the provider’s own media hosts (HTTPS, no redirects, 32 MB at most), put through the same pipeline, and the result points at the signed copy instead. If an image cannot be signed, the call fails with a 502 rather than passing an unmarked picture through.

7

The rules engine

Each jurisdiction and platform is a versioned JSON rule pack. Every rule cites its primary source and the date it was read, and a malformed pack refuses to load. POST /public/v1/rules/evaluate takes where and how the content is published and returns the findings and the pack versions used, which is what the checker runs on.

A weekly job compares every source with its last accepted snapshot and opens an issue when one changes. Sources that refuse automated reads are checked by hand each month.

8

Security

  • Signing keys never leave the HSM; nothing signs with a key held in software outside the development stack.
  • Mutual TLS 1.3 between the API, the signer and the worker, from a private CA; each server accepts only the API’s certificate.
  • API keys are stored as SHA-256 hashes, scoped, revocable and rate limited. Another company’s record reads as not found.
  • Internal error details are logged, not returned; callers get a reference to quote.
  • Images and lockfiles are scanned for known vulnerabilities, and an OWASP Top 10 review has been done on the API.

9

Measured, and not yet

On 13 October 2026, on four photographs, the TrustMark watermark survived all five transforms we tried: resizing to 50%, JPEG at quality 50, a 10% crop, a framed screenshot and re-encoding PNG to WebP. The PDQ fingerprint survived resizing and recompression but not the crop or the screenshot, so it is a fallback for some cases only.

Not yet measured: what each platform does to the manifest on upload and download, and load beyond staging. Images only for now: no video, audio or text.

10

Run it yourself

You need .NET 10, Rust, Python with uv, protobuf and Docker. The local stack signs with a development key and certificate committed on purpose, so its output validates but shows as unrecognised. To sign for real you need your own key in a KMS and a certificate from a CA on the C2PA trust list.

git clone https://github.com/AivaStack/signunder
cd signunder
make up       # API, signer, worker and Postgres in Docker, signing with the development key
make sample   # sign the fixtures and save each with its validation report