What it checks
| Stage | Concern | Codes |
|---|---|---|
| 0 | The validator itself, not the record | VER001 validator-internal-error |
| 1 | Parse (RFC 8259, UTF-8) and size | VER101 unparseable-json · VER102 not-a-json-object · VER103 record-exceeds-hard-cap · VER104 record-size-policy (W) · VER105 record-size-exceeded ¹ · VER106 size-should-cap-exceeded (W) ¹ |
| 2 | Structure, then format/contentEncoding/pattern-anchoring assertion | VER201 schema-invalid · VER202 format-invalid |
| 3 | Conformance level gates | VER301 level-absent (W) · VER302 raw-preservation-missing (one per family) · VER303 availability-incomplete · VER304 normalized-view-missing · VER305 trust-entry-missing · VER306 provenance-missing · VER307 c2pa-status-missing · VER308 signature-absent (W) |
| 4 | Space descriptors | VER401 duplicate-space-id · VER402 space-registry-conflict · VER403 embedding-space-id-malformed · VER404 space-reference-missing (W) · VER405 visual-preprocessing-underspecified (W, checked per kind) · VER406 hamming-tolerance-exceeds-dim ¹ · VER407 bundle-manifest-digest-mismatch ¹ · VER408 bundle-paths-unsorted-or-duplicate ¹ · VER409 bundle-path-illegal ¹ · VER410 artifact-digest-mismatch ¹ · VER411 descriptor-digest-mismatch ¹ |
| 5 | Referential integrity | VER501 embedding-references-undeclared-space |
| 6 | Dimensions, dtypes and component values | VER601 dim-mismatch-vs-descriptor · VER602 dtype-mismatch-vs-descriptor · VER603 inline-vector-length-mismatch · VER604 b64-vector-byte-length-mismatch · VER605 ref-byte-length-mismatch · VER606 dim-dtype-unstated (W) · VER607 non-finite-vector-component · VER608 vector-carriage-not-exactly-one ¹ · VER609 binary-vector-inline ¹ |
| 7 | Role / kind agreement | VER701 canonical-visual-in-nonvisual-space ² · VER702 contextual-in-nontext-space ² · VER703 fused-in-nonfused-space · VER704 fused-recipe-invalid · VER705 no-canonical-visual-embedding |
| 8 | Perceptual digests | VER801 pdq-length · VER802 phash-length |
| 9 | Availability completeness | folded into VER303; stage retained for traceability |
| 10 | Availability basis and raw-segment carriage | VER901 stripped-suspected-without-basis ² · VER902 raw-index-not-unique-or-contiguous ¹ · VER903 raw-index-missing-multisegment ¹ · VER904 raw-ref-digest-mismatch ¹ |
| 11 | Trust, conflict, redaction | VER1001 redaction-availability-inconsistent ² · VER1002 redaction-missing-salted-hash (W, 1.0.x only) · VER1003 conflict-sources-not-distinct · VER1004 conflict-field-untracked (W) · VER1005 redacted-family-raw-present · VER1006 producer-performed-without-commitment ¹ · VER1007 upstream-withheld-with-commitment ¹ · VER1008 commitment-mismatch-ledger-vs-view ¹ · VER1009 redaction-pointer-unresolvable ¹ · VER1010 upstream-withheld-family-raw-present ¹ · VER1011 redacted-source-embedding-carried ¹ |
| 12 | C2PA state | VER1101 c2pa-raw-present-status-missing · VER1102 c2pa-valid-without-manifest-hash (W) · VER1103 c2pa-state-inconsistent |
| 13 | Provenance chronology and action tokens | VER1201 chain-not-chronological · VER1202 chain-precedes-acquisition · VER1203 record-precedes-chain (W) · VER1204 action-order-implausible (W) · VER1205 bare-action-token-unregistered ¹ · VER1206 registry-membership-unverified (W) ¹ · VER1207 sign-event-without-signature ¹ · VER1208 registry-snapshot-mismatch (W) ¹ |
| 14 | Checksums | VER1301 b64-vector-undecodable · VER1302 raw-segment-sha256-mismatch · VER1305 source-ref-content-hash-mismatch |
| 15 | Signature | VER1401 signature-object-invalid · VER1402 signature-verification-failed · VER1403 signature-unverified (W) |
| 16 | Lineage | VER1601 lineage-duplicate-parent ¹ · VER1602 lineage-exported-pixel-hash-mismatch ¹ · VER1603 lineage-self-parent ¹ · VER1604 lineage-cycle ¹ · VER1605 lineage-recipe-uri-without-digest ¹ · VER1606 lineage-parent-content-hash-absent (W, error under --profile high-trust) ¹ |
(W) marks warning severity; everything else is an error. ¹ fires only under --schema 1.1-draft. ² reads a shape the 1.1 draft adds and is therefore worded differently there — see Four rules that read 1.1 shapes; no ² rule changes meaning, severity or 1.0.x behaviour. The registry holds 83 codes (67 error, 16 warning) plus one reserved number; 29 of them are the 1.1 additions marked ¹, and they are the only difference between the two profiles' code sets. Codes are stable and are never renumbered, and neither are their titles: a retired code keeps its number and moves to RESERVED_CODES rather than being reused. VER405's title still reads visual-preprocessing-underspecified from when the rule covered only visual spaces; the rule now covers every kind annex E13 enumerates members for, and the title keeps its original spelling because downstream documents index the registry by it.
Stage 16 is lineage, and it is a stage rather than an extension of stage 13 because every rule in it is a statement about a graph spanning records rather than about the document under test — the one class of question the rest of this validator never asks (VER-1.1-draft.md §15.4).
Stage 0 is the one stage that is not about the record. VER001 is emitted when a rule raises, which is a defect in this package. It is numbered 0 because it precedes and can interrupt any stage, and it is the reason status exists: see Fail-closed below.
Three size numbers that are not the same number (VER104), plus a fourth that is different again (VER103). VER103's hard cap and VER104's document advisory measure serialized bytes — the compact UTF-8 JSON encoding of the whole record, base64 text included. The other two VER104 advisories measure decoded payload octets:
| Quantity | Threshold | Measured on |
|---|---|---|
| serialized record document | 10 MiB | the JSON text as written |
summed inline metadata.raw[].bytes | 6 MiB | decoded octets, before base64 |
| any single inline vector | 6 MiB | decoded octets (see the proxy below) |
The two payload numbers sit below the document number on purpose: base64 inflates 4:3, so a decoded budget at or above 10 MiB could never be exceeded before the document advisory fired first. The previous 16 MiB raw and 8 MiB vector figures had exactly that defect — unreachable inside a 10 MiB document, and therefore decoration rather than policy. Bytes carried by reference are governed separately and are counted against none of the three.
An inline JSON array is measured by a documented proxy of 8 bytes per component, independent of the declared dtype — a JSON array is not dtype-tagged, so charging it the declared dtype's width would size it as the reference form the record did not use. Eight is IEEE 754 binary64, the width a JSON number is read at, and the widest dtype VER 1.0 defines. The consequence a reader reconciling the corpus against "6 MiB" needs: the advisory fires at 786,433 components whatever the dtype, and the JSON text carrying them weighs a different number of bytes — smaller for a zero-filled array, several times larger for full-precision floats. The corpus fixture invalid/ver104-record-size-policy.json carries 1,048,577 components in 2.1 MB of text for exactly that reason: it trips the vector advisory (8,388,616 proxy bytes) and not the document one.
The same three quantities and the same thresholds are what VER105 reports under --schema 1.1-draft, where §7 makes them normative limits rather than advice; VER104 stands down there. See The 1.1 draft profile.
Profile strictness beyond the schema
Where this validator is deliberately stricter than a bare 2020-12 validator, each case grounded in the 1.0.1 errata annex or in the RFC the format names:
- JSON means RFC 8259 JSON.
NaN,Infinityand-Infinityare JavaScript literals that Python'sjsonaccepts by default; a document containing one is rejected as VER101 (stage 1). A document that is not valid UTF-8 is also VER101, naming the offending byte offset — never decoded with replacement characters and then validated, which would audit and signature-check a document the producer never wrote. A leading UTF-8 BOM is accepted and discarded. - Vector components must be finite (VER607, stage 6). Unreachable from a file, by the rule above; reachable through
validate_record, which takes an already-parsed object. OneNaNcomponent makes every similarity computed against that vectorNaN, so the vector indexes cleanly and then silently removes itself from every comparison. formatandcontentEncodingare asserted, not annotated (annex E23). JSON Schema 2020-12 treats both as annotations by default. A record whosecreated_atis"yesterday"is schema-valid and useless, so the profile asserts them (VER202).jsonschemaships nodate-timeorurichecker unless optional dependencies are installed; this package supplies its own so an assertion it promised is never silently skipped.uuidmeans the canonical spelling, not "somethinguuid.UUIDwill parse" (VER202).uuid.UUIDis a parser and a permissive one: it stripsurn:uuid:, drops braces, ignores where the hyphens fall and accepts uppercase, so seven different strings all "validate" as one UUID. Arecord_idis compared, indexed and used as a filename by systems that compare strings, so accepting several spellings of one identifier lets one record hold several identities. RFC 4122 section 3 defines exactly one string form — lowercase hex, hyphens in the 8-4-4-4-12 positions — and that is the one asserted, re-parsed and compared back.urimeans the RFC 3986 character set (VER202). The check used to be "a scheme, then anything that is not whitespace", which admits every C0 control character and" < > \ ^{ | }` — while VER202's own remediation promised RFC 3986. A URI is also the one field a consumer hands straight to a fetcher, so a NUL or a newline in one is how a request header or a log line gets split. Percent-escapes must be% HEXDIG HEXDIG.- JWS parts are strict, unpadded base64url (VER1401, stage 15).
base64.urlsafe_b64decodeis not a validator: it accepts+and/from the standard alphabet and ignores characters it does not recognise, so several distinct strings decode to the same octets. That is the wrong property for a signature — the signing input is the header part as text, so a verifier that normalizes what the signer never wrote produces a result no other implementation reproduces. RFC 7515 §2 admits one encoding,=padding included in what it forbids. - A
critorb64protected header is refused (VER1401).crit(RFC 7515 §4.1.11) names parameters a verifier MUST understand or reject the signature; this validator implements no extensions, so anycrit— even an empty one — is a signature it is required to refuse.b64(RFC 7797) redefines the signing input to the unencoded payload, a construction VER 1.0 does not define (annex E17 signs the JCS form out of band with an empty payload part), so honouring it would verify against a preimage the signer did not use. - An ES256 signature is checked to be exactly 64 octets before it is split (VER1401). RFC 7518 §3.4 fixes it at R‖S, two 32-octet integers. Splitting any other length in half invents an
(r, s)pair from the wrong octets, and the verification that then fails would be reported as VER1402 — "modified after signing" — over what is usually a DER/DSS signature pasted in where the raw form belongs. patternis evaluated with ECMA-262 anchoring (VER202, stage 2c). JSON Schema definespatternover ECMA-262 regexes, where$matches at end of input only. Python's$also matches immediately before a trailing newline, which makes every Python-backed validator looser than a conformant JavaScript one:"<64 hex>\n"satisfies^[a-f0-9]{64}$here and fails it everywhere else. The profile validates against the published schema twice — once as written, once with every$rewritten to(?![\s\S])— and reports the difference.org.x.y/1.0andorg.x.y/1.0\nare two different, visually identical space identities (annex E18), and a 65-charactercontent_hashis not a hex digest (annex E22).- Perceptual digest widths and alphabet are asserted under both schemas (annex E22). The 1.0.0 schema allows any hex length; spec section 5 does not. Stage 8 applies the prose, so a record validated at
--schema 1.0.0still fails a truncated PDQ digest — including one padded back to the right length with a character that is not a hex digit, which is why width and alphabet are tested independently and reported distinctly. - Base64 is strict RFC 4648 everywhere, in every code path: standard alphabet, padding present, no line breaks. Asserted as VER202 for
metadata.raw[].bytesand VER1301 forvector_b64, and the size-accounting helper rejects whitespace rather than stripping it, so no part of the validator sizes a wrapped payload the rules would reject. - Digests over inline bytes are recomputed (stage 14). A raw segment that ships bytes and a
sha256that does not match them is self-refuting, whatever the schema says.
What it does not do
- No network I/O.
vector_refandsource_bytes_reftargets are never fetched, and neither arebytes_ref,recipe_uri,descriptor_uri, a lineagerecord_urior a registry snapshot named byprovenance.registry. An unfetchable reference is not evidence of non-conformance; code VER1303 is allocated for out-of-band verification and is deliberately never emitted (seeRESERVED_CODES). Everything the 1.1 rules resolve is resolved out of band, from--registryand--lineage-peer, or not at all. - No tenancy, privacy or publication policy. Those are deployment decisions — properties of who may see a record, not of whether the record is well-formed and truthful — and belong to the Studio layer. That document is not in this repository, so the scope-out is stated on its own terms here rather than by cross-reference. (Its own stage list happens to number that concern "stage 16". The two numberings are unrelated: **stage 16 in this package is lineage**, and stage numbers here are this validator's, not another document's.)
- COSE_Sign1 signatures are not verified; they are reported as unverified (VER1403). Detached JWS with
EdDSA,ES256,RS256orPS256is verified when a key and thesignaturesextra are both present. A JWS that is malformed — wrong part count, a non-empty payload part, a header or signature that is not strict base64url, analgthis validator does not implement, acritorb64header, an ES256 signature that is not 64 octets — is VER1401, never VER1402: nothing was verified, and that is not evidence the record was modified.
Signature payload
Per annex E17 the signing payload is the record with provenance.signature removed and the result canonicalized with RFC 8785 JCS. Removal deletes exactly that member — it does not prune a provenance object left empty by the removal. Producers must sign that same preimage or verification will fail.
