Check the Content Credentials on a file without sending the file anywhere. Encypher C2PA reads the provenance record embedded in an image, video, audio file, document, font, or text, and tells you two things separately: whether the record and the content are intact, and whether you trust whoever signed it.
Verification runs on your machine. It needs no account, and verifying a file makes no network request unless you allow online checks. The command line also checks once a day for a newer release, which you can turn off (see Updates). Teams that mark AI-generated output for the EU AI Act (Article 50) or the California AI Transparency Act can use it to confirm that what they ship still verifies after it leaves their systems.
The SDK verifies manifests under caller-selected C2PA 2.2, 2.4, and 2.5 policies, plus CAWG Identity 1.3 assertions. C2PA 2.4 remains the default. One Rust core serves the command line, Rust, Python, browser JavaScript, Go, and C.
Quick start
Command line
The MIME type comes from the file extension; pass --mime for a file whose name does not say what it is. Exit codes: 0 integrity valid, 2 provenance absent or invalid, 3 unsupported MIME type, 1 operational or input error.
Rust
[]
= "1.10.0"
use ;
let report = verify_file?;
println!;
# Ok::
Python
=
The wheel supports Python 3.9 and later on Linux (glibc and musl, x86_64 and aarch64), macOS, and Windows x64.
verify and verify_stream accept external_data (URI to standard-base64
body), external_data_media_types (URI to observed MIME type), and
external_data_unreachable (URIs already attempted without a usable body).
These supply evidence, not permission to fetch. Omitted evidence remains
not-attempted; supplied bytes are still checked against the signed references.
Browser JavaScript
import init from "@encypherai/c2pa";
await ;
const file = document..;
const report = ;
console.log;
Verification runs in WebAssembly inside the page. See examples/browser.
To report a local check to your own server without uploading the file, call localEvidence instead. It returns the same report plus the embedded manifest store it verified (manifest_store, a Uint8Array, or null when the file has none), that store's SHA-256, the file's SHA-256, and the active manifest's hard binding (hard_binding.algorithm, such as c2pa.hash.boxes, and hard_binding.status: match, mismatch, or unknown). All of it comes from one verification, so the server can re-verify the exact store the page checked. Rust callers use local_evidence_with_options.
Go
The Go binding is a source distribution for Linux and macOS. It links the C ABI, so build the static library first:
&&
report, err := c2pa.Verify(asset, "video/mp4", nil)
if err != nil
fmt.Println(report.Integrity, report.Trust.Status)
The C ABI is declared in bindings/c/include/encypher_c2pa.h. Every returned string must be released with encypher_c2pa_free_string.
Path-based APIs in Rust, Python, and Go read regular files up to 128 MiB. Byte-buffer APIs are bounded only by caller memory.
Reading a report
Every report answers the integrity question and the trust question in separate fields, because they fail for different reasons. A photo can be untouched since signing and still be signed by someone you do not trust.
integrity: the claim signature verifies, every referenced assertion matches its hash, and the content matches its hard binding (the hash that ties the manifest to the file's bytes).trust: whether the signing certificate chains to an anchor you accept, evaluated atvalidation_time.validation_results: the C2PA and CAWG status codes behind both conclusions. Consumers should branch on these codes rather than on explanation text.
The report schema is 1.0 and stable. See Report schema.
What it verifies
- Embedded manifest readers for 73 canonical MIME types under the default C2PA 2.4 policy, 18 under 2.2, and 99 under 2.5, across images, video, audio, documents, archives, fonts, and text. These inventories include Brotli-compressed stores and update manifests; they do not enumerate every supported open MIME-family member.
- Claim signatures, including RFC 3161 timestamps and OCSP responses stapled into the manifest. Signing, timestamp, and CAWG certificates are checked against the C2PA certificate profile and an RFC 5280 path to a trusted anchor.
- Hashed references from the claim to each assertion, and the ingredient links that tie a manifest to the ones before it.
- Hard bindings: data hash, BMFF hash (including Merkle trees for fragmented MP4), general box hash, ZIP collection hash, and multi-asset hash.
- Live video streams signed under C2PA 2.4, segment by segment.
- Every manifest in a PDF's incremental-update history, each against the version of the file that introduced it.
- CAWG identity assertions signed with X.509 certificates or identity claims aggregation credentials, with
did:jwkresolution anddid:webdocuments you pin or, with online checks allowed, fetch. X.509 identity signatures report the registered CAWG Identity 1.3cawg.x509.*codes. CAWG results apply to the identity assertion only; they never change the C2PA integrity verdict.
encypher-c2pa formats prints the default 2.4 inventory; encypher-c2pa formats --spec 2.5 prints the 2.5 inventory. Format coverage provides the policy support matrix, carrier and binding rules, detached-only formats, and limitations. Text support includes the 2.4 plain-text wrapper, structured-text comment block, and HTML script element, plus 2.5 append-only text and JSON Lines checkpoints.
Video streams
For fMP4 and CMAF, pass the signed initialization segment as the asset and each available media segment as a fragment. The MIME type stays video/mp4. The verifier checks the initialization-segment hash and each supplied segment's Merkle leaf, so a partial recording can be verified: supply the fragments you have, in playback order. Fragments not yet available at the end are not a failure, but a gap or a reordering inside the supplied run is, because C2PA 2.4 requires a validator to flag a sequence that does not match the signed one. When a player seeks on purpose, pass the index of each fragment that starts after the jump (--expected-seek <N>, expected_seek_positions); every fragment is still authenticated. A segment that no binding in the manifest covers fails verification; it is never reported as matched.
Live streams signed under C2PA 2.4 bind each segment differently, so they take a declared encapsulation and protection method. With verifiable-segment-info, the initialization manifest carries session keys, and every segment's signed segment information must verify under them. With per-segment, each segment carries its own manifest, and the verifier recomputes the chain from each segment to the one before it. Which of the two a stream actually uses is read from the stream, not taken from the caller, and a stream whose bytes contradict its declared encapsulation is refused.
| Surface | Fragmented recording | Live stream |
|---|---|---|
| Rust | verify_fragmented |
verify_stream |
| Python | verify(init, "video/mp4", fragments=[...]) |
verify_stream(init, segments, encapsulation=, method=) |
| WebAssembly | verifyFragmented |
verifyStream |
| Go | VerifyFragmented |
VerifyStream |
| C | encypher_c2pa_verify_fragmented |
encypher_c2pa_verify_stream |
The stream report carries a top-level integrity, the initialization manifest's report, a report per segment, and, for per-segment streams, whether the chain holds.
Session keys expire. Each segment's key is checked at the validation time, which defaults to now, so a recording verified after its keys expired fails unless you say when each segment was presented. Pass one trusted RFC 3339 time per segment, in segment order (--stream-presentation-time, stream_presentation_times, Go StreamPresentationTimes). Each segment's key window is then checked at its own time. Certificates are still checked at the validation time, and sequence continuity is still enforced across the whole run, so a recording that moved to a new key part way through verifies as one stream.
The verifier takes these times on your word, so supply them only from a record you control, such as an ingest log. It never reads them from the segments; each segment's signed iat remains an extra check inside the key window. A time stamp on the initialization manifest dates that manifest, not the segments after it. A malformed time, or a count that differs from the number of segments, is refused, and Merkle-bound and per-segment streams refuse the option.
Trust
Each install carries a pinned trust snapshot dated 2026-09-24, compiled into every package. Trust material is never fetched: no trust list, no certificate chain, no CRL. With online checks off, which is the default, verification is entirely local and deterministic. Turning them on adds revocation status, a remote manifest store, a DID document, or external content as evidence; the trust snapshot is still the one on disk.
| Packaged source | Used for |
|---|---|
| C2PA Trust List | Claim-signing anchors |
| C2PA TSA Trust List | Timestamp-authority anchors |
| IPTC Verified News Publishers, end-entity list | Directly allowed claim-signing and CAWG identity certificates, interim S/MIME rules |
| IPTC Verified News Publishers, anchor list | CAWG identity anchors, interim S/MIME rules (empty in this snapshot) |
| Mozilla Root Store, email trust bit | CAWG identity anchors, interim S/MIME rules (section 8.2.4.1) |
| Encypher C2PA Root CA | Claim-signing anchor |
| Encypher C2PA TSA Issuing CA | Timestamp-authority anchor |
| Encypher Verified Organizations List | CAWG identity anchor, base trust model |
CAWG Identity 1.3 ties its interim S/MIME conditions to the Mozilla and IPTC
lists by name, so they apply to those two sources only. An identity that
chains to the Encypher Verified Organizations root, or to an anchor you
supply, is accepted under the base trust model: emailProtection with one of
the six approved CA/Browser Forum S/MIME certificate policies, no 31 March
2027 cutoff and no time-stamp condition. Every other check is unchanged.
Source URLs and SHA-256 digests are in default_trust/sources.json. A new release refreshes the snapshot.
Your own PEM bundles extend the snapshot. To evaluate only your own material, set no_default_trust.
| CLI flag | Option | Purpose |
|---|---|---|
--trust |
trust_pem |
Claim-signing anchors |
--tsa-trust |
tsa_trust_pem |
Timestamp-authority anchors |
--allowed |
allowed_list_pem |
Directly allowed claim-signing certificates |
--cawg-trust |
cawg_trust_pem |
CAWG X.509 identity anchors |
--cawg-allowed |
cawg_allowed_certs_pem |
Directly allowed CAWG identity certificates |
--cawg-trust-configurations |
cawg_trust_configurations |
Typed CAWG trust sources (base or smime_interim), each with its own validity window, as a JSON array |
--trust-anchor-not-before, --trust-anchor-not-after |
trust_anchor_not_before, trust_anchor_not_after |
Validity window for your own anchors (RFC 3339) |
--cawg-did-documents |
cawg_did_documents |
Pinned DID documents for did:web issuers |
--cawg-ica-trusted-issuer |
cawg_ica_trusted_issuers |
Identity-aggregation issuer DIDs you trust directly |
--cawg-ica-trust-anchor |
cawg_ica_trust_anchors |
DIDs an issuer may reach through controller links in pinned DID documents |
--cawg-ica-status-lists |
cawg_ica_status_lists |
Revocation status lists, as a JSON map of list URI to base64 bitstring |
--no-default-trust |
no_default_trust |
Ignore every packaged snapshot |
--time |
validation_time |
RFC 3339 validation instant |
--stream-presentation-time |
stream_presentation_times |
Trusted RFC 3339 presentation time per session-key stream segment, one per segment |
Trust flags repeat, and repeated bundles merge. Python keyword arguments and the WebAssembly options object use the same names; Go uses the CamelCase equivalents, such as NoDefaultTrust.
Each anchor serves one purpose. A claim-signing anchor cannot validate a timestamp authority, and a timestamp anchor cannot validate a claim signer. An identity-aggregation credential is trusted only when its issuer is listed or reaches a listed anchor, and a credential that names a revocation list is checked against the lists you supply; a missing list is reported, not assumed good.
A verifier that has not been allowed online cannot prove that a trust list is current, so freshness.status is unknown, and revocation is read only from responses stapled into the manifest. Allowing online checks lets the verifier ask the OCSP responder named by the certificate. See Trust model.
Manifests stored outside the file
By default the SDK reads; it does not fetch. When an asset names its manifest by URL, the report returns the URL as manifest.inaccessible and stops there. Fetch the manifest yourself if you choose, then verify it with verify_with_manifest_store. The same entry point verifies a .c2pa sidecar, with the same trust material, validation time, and posture as an embedded manifest.
To have the store fetched for you, allow online checks (below).
Online checks (opt-in)
Some questions cannot be answered from the file alone. Where the manifest lives, whether a signing certificate has been revoked, what an identity issuer's DID document says, what the content stored outside the asset is: each needs a server. Nothing is fetched unless you say so.
Every report carries a network block. Offline, it lists what a fetch would settle, so you can see what allowing one would do:
"network":
Allow it for one run, or save the answer:
With ask, or before you have answered, a run that needs a fetch prints each purpose and host, says that contacting them tells those servers the file is being checked, and offers [y] yes, this time [N] no [a] always [v] never. A run with nobody at the terminal never prompts and stays offline.
ENCYPHER_C2PA_ONLINE=on or off is the operator's switch. It applies to every surface and outranks the saved answer.
Libraries never read the saved answer and never prompt, because the machine calling them may be checking files sent in by strangers. Pass the option instead:
let options = VerifyOptions ;
allow := true
report, err := c2pa.Verify(asset, "image/jpeg", &c2pa.Options)
In the browser, verify stays synchronous and offline; await verifyOnline(bytes, mime, options) does the same verification and fetches through the page's own fetch, so the page's CORS and Content-Security-Policy rules apply.
With explicit spec_version: "2.5", browser verification discovers resources
without hashing or checking signatures, resolves terminal stores, predecessor
chains, external assertion content and ICA DID documents, then verifies once.
Caller evidence and pinned DID documents take precedence. Requests use HTTPS
GET, omit credentials, refuse redirects and share a 16-URI attempt budget.
Streamed evidence has a 64 MiB cumulative budget, including bytes consumed by
failed reads; DID documents also have a 256 KiB limit. An oversized stream is
canceled; bytes already read remain charged. OCSP remains browser-skipped.
The default 2.4 and explicit 2.2 paths retain their prior behavior.
What each host learns is one request for one URL: that somebody is checking a file that references it. The SDK never sends asset bytes. An OCSP request carries a certificate serial number and issuer hashes, nothing more. Full detail is in Privacy.
The native fetcher uses HTTPS, except for OCSP responders, whose answers are signed and verified. It refuses redirects, uses 5-second connect and 10-second total timeouts, and sends no cookies or credentials. Its user agent is encypher-c2pa/<version>. Per-response caps are 64 MiB for manifest stores or external content, 64 KiB for OCSP, and 256 KiB for DID documents. Every hostname is resolved through a filter that refuses loopback, private, link-local (including 169.254.169.254), carrier-NAT, unique-local, multicast, and documentation addresses; the connection uses the vetted address. Set --online-allow-private-networks (online_allow_private_networks) for an intranet deployment: that lifts the address filter and accepts plaintext HTTP. Do not set it where files arrive from strangers.
With explicit spec_version: "2.5", native verification uses parse-only discovery to resolve terminal stores, predecessor chains, external content and unpinned ICA DID documents before the authoritative graph pass. Caller evidence takes precedence and GET resources are fetched once per URI. All frontiers and validation-dependent OCSP requests share 16 HTTP requests and 64 MiB of received-body bytes, including failed and oversized reads. Reusing cached content as a DID document still enforces the DID size cap. Actual OCSP response or unreachable evidence can require a final evidence re-evaluation; discovery never performs an extra signature or asset-hash pass. Offline and caller-evidence-only paths allocate no HTTP client. Default 2.4 and explicit 2.2 retain their existing verification and budgeting behavior.
What stays caller-supplied, and why: ICA status-list credentials, because a credential has to be verified as a credential before it can be trusted; trust lists, because their currency is a snapshot policy rather than a fetch; and AIA caIssuers chain completion.
Build without the fetcher entirely with --no-default-features --features telemetry, if you need to be able to prove a binary cannot reach the network for verification.
Updates
Each release carries verification fixes and a refreshed trust snapshot, so an old copy judges files against old trust lists. The command line checks for a newer release once a day, when a person is at the terminal, and offers to install it:
encypher-c2pa 1.8.1 is available. You have 1.8.0, with trust lists dated 2026-09-24.
Releases carry verification fixes and refreshed trust lists.
Update now? [y] yes [N] not now [s] skip this version [o] stop checking
y runs cargo install encypher-c2pa-cli --version 1.8.1 --locked, then runs your command again on the new version. s skips that release; a later one is still offered. o turns the check off.
The check is one request for the crate's public entry in the crates.io index. It carries no file, path, or identifier, gives up after two seconds, and fails silently. Runs with nobody at the terminal (pipes, CI, cron) never check, and --offline skips the check for that run. The libraries never check; update them through your package manager.
The setting is saved in update.json in the configuration directory (~/.config/encypher/ on Linux and macOS, %APPDATA%\Encypher\ on Windows, or ENCYPHER_C2PA_CONFIG_DIR). {"check": false} turns the check off. ENCYPHER_C2PA_UPDATE_CHECK=on or off overrides the file.
Verification posture
By default the verifier applies the C2PA 2.4 validation rules. A few rules that content written under C2PA 1.x could not have followed, such as the links from an action to its ingredients, apply to that content only in strict mode.
Set strict_conformance (--strict-conformance) to apply strict validation. With the 2.2 or 2.4 policy, strict mode selects that revision's published C2PA Conformance Program posture. It turns additional program requirements into failures, such as a trusted timestamp and usable revocation information, and requires CAWG Identity 1.3 deterministic encoding of identity signatures. It also adds the report's Content Credentials JSON (content_credentials), the form conformance rubrics evaluate.
Set spec_version (--spec) to "2.2", "2.4", or "2.5" to select the validation policy. The default, and "2.4", keep the 2.4 rules and produce identical reports. The 2.2 option applies the older policy. The 2.5 option targets the published C2PA 2.5 standard (specs-core tag 2.5, commit d538a73795b64f2c9d0c1a332b7ae38728070413). Under 2.5, strict_conformance enables strict core-spec diagnostics; it does not claim a C2PA Conformance Program scope or certification. Any other value fails with unsupported_spec_version before the asset is read. Reports name the selected policy in profile (c2pa-2.2, c2pa-2.4, or c2pa-2.5). The policy is chosen by the caller, never by what a manifest declares. See docs/FORMATS.md for the format-level differences.
Where the two postures differ, the default report says so with an informational status. For example, an identity signature over the CAWG 1.1 field order, which c2pa-rs still writes, verifies by default with com.encypher.cawg.legacyProfile. To refuse that encoding without taking on the rest of strict mode, set cawg_strict_encoding (--cawg-strict-encoding).
Ingredient graphs and external resources
Rust callers can use verify_with_graph_inputs, verify_with_manifest_store_and_graph_inputs, verify_fragmented_with_graph_inputs, and verify_manifest_store_only_with_graph_inputs to validate ingredient graphs and external references in one bounded pass. The caller-verified-binding feature's whole-verification and evaluator-driven CAWG functions take the same GraphInputs as their final argument. GraphInputs accepts an ExternalResources provider for bytes the caller already holds. One memoized provider path covers generic hashed references, linked-resource integrity, cloud-data and external-reference assertions, and external predecessor stores in a c2pa.hash.segment history. The verifier borrows provider bodies, does not open sockets, and asks the provider at most once for each URI during the pass. OnlineEvidence::external_data remains authoritative when both inputs name a URI, and an explicit unreachable evidence outcome is never replaced by provider content. Default GraphLimits allow 32 ingredient edges, 1,024 distinct manifests, 16,384 references, and 64 MiB of cumulative manifest bytes across the whole pass, including append-history predecessor stores. GraphLimits::lowered can reduce those ceilings but cannot raise them. Reaching one records com.encypher.resource.graphLimitExceeded and stops that traversal. Store-only verification remains unbound because no content comparison occurs.
Services that resolve external URLs can call locate_external_resources or locate_external_resources_with_manifest_store before one authoritative verification. The returned ExternalResourcePlan<A> owns the caller's asset container and exposes an ordered frontier of typed Opaque or SegmentManifestStore requests. Root discovery uses the verifier's format-specific remote-manifest locator, including a C2PA 2.5 append-only checkpoint that names an external active store. Under 2.5, a syntactically valid MIME type with no embedded reader returns no store and no request, so the caller can apply an HTTP Link or repository root without inventing an embedded carrier; older policy admission is unchanged. After the caller resolves that frontier into an ExternalResources provider, continue_with replaces it with the next frontier. The retrieved initial store and append-history predecessor stores are each parsed once. A URI first seen as opaque is promoted and parsed from the already-held provider body without another fetch. manifest_store_present is set only when an initial store body is located, before parsing. A malformed embedded store therefore cannot trigger remote fallback; an extraction error remains a typed malformed stop with presence false and also forbids fallback. The supplied-store locator always uses that store even when the asset embeds another, matching the supplied-store verifier. truncated carries a typed graph-limit, unavailable-predecessor, or malformed-store reason. Discovery performs no signature, digest, trust, time, or network operation. Final verification remains authoritative.
ExternalResourcePlan::did_documents() exposes parse-only ICA DID requests
from the selected identity scope. VerifyOptions::validate_with_external_data
returns owned, decoded caller evidence for adapters that retain a provider,
avoiding a second base64 decode. Under exact 2.5, the core also consumes that
provider for a declared remote root: unavailable and not-attempted remain
distinct, and a predecessor can never replace the terminal store.
Failure telemetry (opt-in)
Telemetry is off until you turn it on. On the first interactive run, the SDK shows what it would send and asks once. Non-interactive processes never prompt and stay off.
An event is sent only when provenance is invalid or verification fails. It contains the SDK name and version, the engine profile, the MIME type, the outcome, and at most eight status codes. It never contains the file, the manifest, the report, a filename or path, a certificate or key, trust material, or any account or machine identifier. Sending never blocks verification. The full contract is in Privacy.
Python uses configure_telemetry(True), Go c2pa.ConfigureTelemetry(true), and JavaScript configureTelemetry(true). Native deployments can set ENCYPHER_C2PA_TELEMETRY=on or off instead. A per-call setting overrides the saved preference.
Optional cross-check with the Encypher API
--encypher-api asks the Encypher API whether Encypher holds a provenance record for the file. Set ENCYPHER_API_KEY first.
The request carries the file's SHA-256, size, MIME type, and a summary of the local result. When the format keeps the manifest in one contiguous block, it also carries that manifest store so the API can validate it independently. The file itself, its name, and its path stay local.
The answer is attached under encypher_api and never changes the local verdict or exit code. A match there is Encypher's record, not a trust decision about the signer. A network error produces a warning, not a failed verification.
Scope
This SDK verifies open-standard Content Credentials. It does not sign media, build manifests, or write into files, and the published API has no path that could. Verification boundary describes the three CI controls that enforce this.
The SDK does not detect Encypher's proprietary sentence-level or durable markers. It can consume a caller-extracted registered soft binding through the C2PA 2.5 Soft Binding Resolution API, then verify each recovered manifest locally. Every endpoint origin must be allowlisted, redirects are refused, and each manifest ID is encoded as one repository URL path segment. Asset upload and reference disclosure require explicit route-specific consent, and network access remains opt-in. Signing, proprietary marker detection, hosted trust policy, and durable receipt issuance are available through the Encypher API.
Standards status
The verifier shares no code with other C2PA implementations. Interoperability is tested offline against pinned vectors: core media from contentauth/c2pa-rs, the CAWG identity corpus, and generated conformance vectors. Expected results come from the status-code definitions for the selected C2PA policy, not from another implementation's output.
The C2PA 2.2, 2.4, and 2.5 labels and the CAWG Identity 1.3 label describe what the verifier targets. They are not a conformance certification. C2PA and Content Credentials are marks of their respective owners.
Build from source
Requires Rust 1.88 or later. Python packaging needs uv and maturin. Browser packaging needs wasm-pack and the wasm32-unknown-unknown target.
Contribution rules are in CONTRIBUTING.md. The architecture is described in Architecture.
Security
Report vulnerabilities through GitHub private vulnerability reporting. Parser limits and the threat model are in SECURITY.md.
License
Copyright 2026 Encypher Corporation. Licensed under the Apache License 2.0.
Redistributions and derivative works must keep the NOTICE file, which attributes the software to Encypher Corporation (Section 4(d)), and the copyright and license header in each source file (Section 4(c)). The license does not grant use of the Encypher name or logo (Section 6). Third-party test vectors under tests/vectors/ keep their upstream licenses, recorded next to each asset.