# Verify it yourself

Every proof ContextFile issues uses a public format, so you can check it with tools we
did not write. You need curl, jq and the tool named in each step. You never need
ContextFile code, an account, or to trust our servers beyond fetching the files.

## 1. Find the keys

Each purpose has its own Ed25519 key, published in a key set. A rotated key stays
listed with `status: "retired"` and its `valid_to` date, so what it signed before
rotation still checks.

| Key set | Signs |
| --- | --- |
| `https://contextfile.ai/.well-known/report-keys` | reports and their attestation bundles |
| `https://contextfile.ai/.well-known/receipt-keys.json` | receipts |
| `https://contextfile.ai/.well-known/receipt-anchor-keys.json` | daily receipt tree heads (Rekor statement and witness checkpoint) |
| `https://contextfile.ai/.well-known/did.json` | claim credentials and status lists (`did:web:contextfile.ai`, Multikey) |

Each entry has `key_id`, `public_key` (32 raw bytes, base64url) and, for report keys,
`pem`, which is what cosign reads.

## 2. Check a report with cosign

Each report is an in-toto Statement v1 whose subject is the SHA-256 of the artifact
file and whose predicate type is `https://contextfile.ai/verification-report/v1`. The
statement sits in a DSSE envelope signed with the report key, delivered as a Sigstore
bundle at `/reports/<digest>/attestation.sigstore.json`. The report page shows these
commands with the digest and key id filled in.

```sh
curl -sSo attestation.sigstore.json 'https://contextfile.ai/reports/<digest>/attestation.sigstore.json'
KEY_ID=$(jq -r .verificationMaterial.publicKey.hint attestation.sigstore.json)
curl -sS https://contextfile.ai/.well-known/report-keys \
  | jq -r --arg k "$KEY_ID" '.keys[] | select(.key_id == $k) | .pem' > report-key.pem
cosign verify-blob-attestation \
  --new-bundle-format --bundle attestation.sigstore.json \
  --key report-key.pem \
  --type https://contextfile.ai/verification-report/v1 \
  ./artifact-file
```

cosign checks the signature against the key and that the file's SHA-256 is the
statement subject. A report whose owner put it on the registry is also logged in the
public Rekor instance, and its bundle carries the log entry. A bundle without one
(the report is not public on the registry, or not logged yet) needs
`--insecure-ignore-tlog=true`. Change one byte of the payload and the check fails.

To read what the report checked and what it does not cover:

```sh
jq -r .dsseEnvelope.payload attestation.sigstore.json | base64 -d | jq .predicate.body.scope
```

The predicate holds `reportDigest`, `reportUri`, the report key's own signature
(`reportSignature`) and the full signed `body`. Recompute the digest yourself: SHA-256
of the body as JSON with object keys sorted at every level. The JSON Schema is at
[/schemas/verification-report-v1.json](/schemas/verification-report-v1.json).

## 3. Check an OCI image

Images built by the platform are signed keyless by the GitHub Actions workflow that
built them, and carry SLSA provenance, an SPDX SBOM and the report attestation, all
attached with cosign.

```sh
slsa-verifier verify-image '<image>@sha256:<digest>' --source-uri github.com/<org>/<repo>
cosign verify-attestation --key report-key.pem \
  --type https://contextfile.ai/verification-report/v1 '<image>@sha256:<digest>'
cosign verify-attestation --type spdxjson \
  --certificate-identity-regexp '^https://github.com/<org>/<repo>/' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com '<image>@sha256:<digest>'
```

slsa-verifier prints the source commit the image was built from.

## 4. Check a receipt against an outside log

Once a day ContextFile builds a Merkle tree (RFC 6962) over the receipts it issued the
day before. The tree head is posted to AI Source of Truth, which belongs to the same
company, and anchored outside it:

- **Rekor first.** The tree head, as an in-toto statement with predicate type
  `https://contextfile.ai/receipt-tree-head/v1` signed with the receipt anchor key, is
  logged in the public Rekor instance.
- **A witness when Rekor will not take it.** The day's checkpoint becomes one leaf of
  the `contextfile.ai/receipt-days` log, whose C2SP checkpoint the anchor key signs and
  an independent witness co-signs, after checking it only grew since the size it last
  co-signed.

`GET /receipts/<id>/proof?hash=<receipt_hash>` returns `outside_anchor` once the day
is logged. To check it:

1. Walk `outside_anchor.receipt_proof.audit_path` from the receipt's leaf hash
   (SHA-256 of `0x00` and the 32 bytes of `receipt_hash`) to a root. It must equal
   `outside_anchor.tree_head.root_hash`.
2. Rekor path: check the DSSE `envelope` against the anchor key set, and that its
   statement's subject digest is that root. Then look up the entry and check it is the
   same envelope:

   ```sh
   rekor-cli get --log-index <outside_anchor.rekor.logIndex> --format json
   ```

3. Witness path: check that `day_record_hash` is in the receipt-days log
   (`receipt_days.audit_path` to `receipt_days.root_hash`), that `receipt_days.note`
   names that size and root and carries a signature by the anchor key's `note_vkey`,
   and a cosignature by the witness key you trust.

The receipt page shows where each receipt's day is anchored.

## 5. Check a claim as a Verifiable Credential

Any verified claim of a public report exports as a W3C Verifiable Credential 2.0:

```sh
curl -sSo claim.vc.json 'https://contextfile.ai/reports/<digest>/claims/<claim-id>/credential'
```

The issuer is `did:web:contextfile.ai`, which resolves to
`https://contextfile.ai/.well-known/did.json`. The proof is a `DataIntegrityProof` with
cryptosuite `eddsa-rdfc-2022`, so any VC library that supports W3C Data Integrity
checks it, for example Digital Bazaar's `@digitalbazaar/vc` with
`@digitalbazaar/eddsa-rdfc-2022-cryptosuite`. The subject holds `claim_id`,
`text_hash` (SHA-256 of the claim text, not the text itself), `source_locator`,
`entailment` and `report_digest`; its terms are defined in the credential's own
context, so a verifier fetches nothing but the W3C context and the DID document.

## 6. Check whether a claim was recalled

Each credential has a `credentialStatus` entry of type `BitstringStatusListEntry`
pointing at a W3C Bitstring Status List: `statusListCredential` is the list URL
(`/status-lists/<YYYY-MM>`, one list per month of export) and `statusListIndex` is the
claim's bit. When errata correct a claim, its bit is set. The list is itself a
credential signed by the same DID, so check its proof first, then read the bit:

```sh
curl -sSo list.vc.json 'https://contextfile.ai/status-lists/<YYYY-MM>'
node -e 'const l = require("./list.vc.json").credentialSubject.encodedList;
  const bits = require("zlib").gunzipSync(Buffer.from(l.slice(1), "base64url"));
  const i = Number(process.argv[1]);
  console.log((bits[i >> 3] >> (7 - (i & 7))) & 1 ? "revoked" : "not revoked");' <statusListIndex>
```

`@digitalbazaar/vc-bitstring-status-list` does both steps. Lists can be cached for the
`ttl` they carry. The [errata ledger](/transparency/digest) links every list.

## 7. Check Content Credentials on an image or PDF

Images and PDFs ContextFile delivers carry a C2PA manifest with an
`ai.contextfile.report` assertion: `report_digest`, `report_url`, `key_id` (the report
key that signed the report) and, for a listing, `license_url`. Open the file in any
Content Credentials viewer (contentcredentials.org/verify) or run
`c2patool <file>`, then check the report itself with section 2. PDFs also carry
`ContextFileReportDigest` and `ContextFileReportURL` in their document information, so
the digest is there even where a viewer does not read C2PA in PDFs yet.

## 8. Read the license terms

Each listing page embeds its terms in the RSL format (`application/rsl+xml`) and links
the same file at `/listings/<id>/license.xml`. The terms combine the licenses of every
source the report cites with the listing's own license; the most restrictive wins, so
one non-commercial source makes the whole listing non-commercial.

## For agents: sign your requests

Report, status list, listing and download paths accept Web Bot Auth: HTTP Message
Signatures (RFC 9421) with `tag="web-bot-auth"`, an Ed25519 key from your signature
directory (`Signature-Agent`), and `@authority` covered. A signed request from an
allowed directory is served without a bot challenge and carries
`x-contextfile-signed-agent: verified` in the response. Unsigned requests are served as
before, and agents signed in with a token are never challenged.

## Older reports

Reports signed before attestations shipped carry only the original signature: an
Ed25519 signature by the report key over the report digest string. Check it with the
key's `public_key` and any Ed25519 library, after recomputing the digest from the body
as above. These reports have no scope block. A worker job builds bundles for them, so
most will also have an attestation bundle.



Version 1, updated 2026-11-01.
