Skip to content

Docs · Pro

Signed audit reports

A record of every file Markproof marked for your project in a period, signed by Markproof, so an auditor can check it without having to trust you or us. Export it from the compliance page on the Pro plan.

What's in a report

One row per marked file: asset id, time marked, media type, watermark code, public verify URL, C2PA manifest label, the generator and model names you supplied, and the prefix of the API key that marked it. Never the files themselves. The period, project and row count are part of the signed data.

JSON downloads one .signed.json envelope. CSV downloads a ZIP with the CSV, a .csv.sig.json envelope whose report pins the CSV's SHA-256, and a README. One report covers up to 366 days and 200,000 files; export larger projects a shorter period at a time. Periods are in UTC; the end is exclusive and never later than the time of export. CSV cells starting with =, +, - or @ get a leading ' so spreadsheets don't run them as formulas.

report.signed.json
{  "format": "markproof.signed-report/v1",  "algorithm": "ES256",  "canonicalization": "RFC8785",  "report": {    "assets": [      {        "api_key_prefix": "lt_live_7f2a9c1e",        "asset_id": "5b0c…",        "c2pa_manifest_label": "urn:c2pa:…",        "created_at": "2026-10-02T09:15:00.123456+00:00",        "generator_name": "your-app",        "kind": "image",        "model_name": "your-model",        "verify_url": "https://markproof.co/v/ARN26QVH",        "watermark_code": "ARN26QVH"      }    ],    "generated_at": "2026-10-31T09:00:00.000Z",    "issuer": "Markproof (markproof.co)",    "period": { "from": "2026-10-01T00:00:00.000Z", "to": "2026-10-31T09:00:00.000Z" },    "project": { "id": "…", "name": "My project" },    "row_count": 1,    "schema": "markproof.audit-report/v1"  },  "sha256": "…",  "signature": "MEUCIQ…",  "cert_chain": ["-----BEGIN CERTIFICATE-----…", "-----BEGIN CERTIFICATE-----…"]}

How it's signed

The report object is canonicalised with RFC 8785 (JSON Canonicalization Scheme: sorted keys, no whitespace), hashed with SHA-256 (sha256), and that 32-byte digest is signed with ES256 (ECDSA P-256, SHA-256), DER-encoded and base64'd (signature). cert_chain holds the signing certificate and the root, PEM.

The signing certificate is Markproof's report-signing certificate, separate from the one that signs C2PA Content Credentials. It's issued by the Markproof Root CA and carries the document-signing extended key usage (1.3.6.1.5.5.7.3.36). Get the root from markproof.co/api/verify-report/root.

The certificate in the envelope is only trusted because it chains to that root. Use the root you downloaded, not the copy inside the envelope.

Verify online

Drop the report on markproof.co/verify-report. It shows valid or invalid, the signer and the period. The report is hashed in your browser; large reports aren't uploaded.

Verify offline

Python, with the cryptography package:

verify_report.py
# verify_report.py — pip install cryptography# python verify_report.py report.signed.json markproof-root-ca.pemimport base64, hashlib, json, sysfrom datetime import datetimefrom cryptography import x509from cryptography.hazmat.primitives import hashesfrom cryptography.hazmat.primitives.asymmetric import ec env = json.load(open(sys.argv[1], encoding="utf-8"))root = x509.load_pem_x509_certificate(open(sys.argv[2], "rb").read()) # 1. Hash the canonical report (RFC 8785; for these reports, sorted compact JSON).canonical = json.dumps(env["report"], sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")digest = hashlib.sha256(canonical).digest()if digest.hex() != env["sha256"]:    sys.exit("INVALID: the report was changed after signing") # 2. The certificate: issued by the Markproof root, for document signing, valid when the report was made.leaf = x509.load_pem_x509_certificate(env["cert_chain"][0].encode())leaf.verify_directly_issued_by(root)eku = leaf.extensions.get_extension_for_class(x509.ExtendedKeyUsage).valueassert x509.ObjectIdentifier("1.3.6.1.5.5.7.3.36") in eku, "not a report-signing certificate"made = datetime.fromisoformat(env["report"]["generated_at"].replace("Z", "+00:00"))assert leaf.not_valid_before_utc <= made <= leaf.not_valid_after_utc, "certificate not valid at generation time" # 3. The signature: ES256 over the 32-byte digest. Raises InvalidSignature on mismatch.leaf.public_key().verify(base64.b64decode(env["signature"]), digest, ec.ECDSA(hashes.SHA256())) r = env["report"]print(f"VALID: {leaf.subject.rfc4514_string()} | {r['period']['from']} -> {r['period']['to']} | {r['row_count']} rows")

Or with openssl (Python only produces the canonical bytes):

openssl
# 1. Canonical report bytes (RFC 8785), then their SHA-256python3 -c 'import json,sys; r=json.load(open(sys.argv[1],encoding="utf-8"))["report"]; sys.stdout.buffer.write(json.dumps(r,sort_keys=True,separators=(",",":"),ensure_ascii=False).encode())' report.signed.json > report.canonical.jsonopenssl dgst -sha256 -binary report.canonical.json > digest.binopenssl dgst -sha256 -r report.canonical.json# must equal the envelope's "sha256" # 2. Pull out the signature and the signing certificatepython3 -c 'import json,sys,base64; e=json.load(open(sys.argv[1],encoding="utf-8")); print(e["sha256"]); open("sig.der","wb").write(base64.b64decode(e["signature"])); open("leaf.pem","w").write(e["cert_chain"][0])' report.signed.json # 3. The certificate chains to the Markproof root and is for document signingopenssl verify -CAfile markproof-root-ca.pem leaf.pemopenssl x509 -in leaf.pem -noout -ext extendedKeyUsage# expect 1.3.6.1.5.5.7.3.36 (documentSigning) # 4. The signature over the digestopenssl x509 -in leaf.pem -pubkey -noout > leaf.pubopenssl dgst -sha256 -verify leaf.pub -signature sig.der digest.bin# Verified OK

For a CSV export, unzip it first:

CSV
# Verify the .csv.sig.json as above (it's the same envelope), then:sha256sum markproof-audit-2026-10-01_2026-10-31.csv# must equal report.csv.sha256 in the .csv.sig.json

What a signature does and doesn't prove

A valid report proves that Markproof produced exactly this record for this project and period, and that nothing in it has changed since. It doesn't prove that every file you generated was sent for marking. The generation time is Markproof's own clock, not an independent timestamp. A report isn't legal advice and doesn't by itself establish compliance with the EU AI Act.