EVIDE Payload Canonicalization
JSON does not define an order for object keys. The same object can be serialized with its keys in different orders, and each order produces a different byte sequence — and therefore a different SHA-256 hash.
Without canonicalization, hash verification would be fragile: a developer rebuilding the payload from their system's internal representation in a different key order would obtain a different hash, making integrity verification impossible without the original byte stream.
Canonicalization solves this by defining a single deterministic normalization that all implementations apply before computing the hash. The hash then no longer depends on key order, on whitespace or on the way characters are escaped. The items of extensions and evidence_references are also sorted, so their submitted order usually does not matter, with one exception: items that compare as equal under the sort rules keep their submitted order (Section 2, step 4). The hash still depends on everything else: values and the order of every other array. A client that computes the hash itself also has to reproduce what the server changes when it decodes the payload (Section 2, step 1).
intake_hash returned in the API response is the SHA-256 of that form. Source systems implementing the same algorithm can compute the expected hash before submission and compare it with the returned value.
The server applies the following steps in sequence. A source system that wants to compute the same hash applies the same steps to the request body it sends.
json_decode (associative arrays) and works on the decoded value, not on the bytes that were sent. Whitespace between tokens and the escape form of characters (\u00e8 or è, \/ or /) therefore do not affect the hash. Decoding also has effects that a client has to reproduce: see the note after the steps.content_hashcontent_hash member, remove it before any further processing. This member is a source-side hash declaration — including it in the canonical form would create a circular dependency. Only the top-level member is removed: a member named content_hash inside another object stays in the canonical form.ksort with SORT_STRING): uppercase letters sort before lowercase letters, _ sorts between them, and non-ASCII characters sort after all ASCII characters.extensions is sorted by value with the PHP <=> operator: two strings are compared byte-wise, unless both are numeric strings, which are compared as numbers; an item that is not a string counts as an empty string. For the registered extension names (evidence_references, declarations, declared_uncertainties), the only values the intake accepts in the top-level extensions array, this is plain byte-wise ordering. Every array whose member name is evidence_references is sorted by the composite key artifact_type, pointer, declared_origin, hash.value, compared byte-wise in that order; a missing component counts as an empty string, and no other field is considered. In both arrays, items that compare as equal keep their submitted order: two references with the same artifact_type, pointer, declared_origin and hash.value but a different declared_description, for example, produce a different hash if they are swapped. The rule follows the member name at any nesting level.handoff.boundary_readiness.unresolved_signals and declared_uncertainties: the declared order is part of what is hashed. Each element is canonicalized recursively.\uXXXX escape sequences, and forward slashes are not escaped (/ not \/). Quotation marks, backslashes and control characters are escaped as in any JSON (\", \\, \n, \u0001…), and so are the characters U+2028 and U+2029 (\u2028, \u2029), as PHP json_encode does.— an empty object
{} is re-encoded as an empty array [];— numbers written with a decimal point or an exponent are re-serialized by PHP. With PHP's default
serialize_precision setting (-1), which governs how json_encode writes floating-point numbers, 1.0 becomes 1, 1.50 becomes 1.5 and 1e2 becomes 100; these examples assume that setting;— when a member name appears twice in the same object, only the last value is kept;
— an object whose member names are the integers
"0", "1", "2"… can be encoded as an array.
json_encode($canonical, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)The first two flags determine the output. Without
JSON_UNESCAPED_UNICODE, multi-byte characters are escaped as \uXXXX sequences, producing a different byte string. Without JSON_UNESCAPED_SLASHES, forward slashes in URLs are escaped as \/, again producing a different byte string. JSON_THROW_ON_ERROR only turns encoding errors into exceptions.
The following tables show the key order for the most relevant objects after canonicalization. These orderings follow from the byte-wise sort in Section 2, step 3, and are provided here as a reference to avoid implementation errors in common nested objects.
| # | Key |
|---|---|
| 1 | authority |
| 2 | chain |
| 3 | created_at_utc |
| 4 | decision |
| 5 | evide_schema |
| 6 | evidence_references |
| 7 | extensions |
| 8 | fedis_requested |
| 9 | handoff |
| 10 | human_oversight |
| 11 | intervention |
| 12 | object_class |
| 13 | source_reference |
| 14 | source_system |
| 15 | source_timestamp_utc |
| # | Key |
|---|---|
| 1 | taxonomy_reference |
| 2 | threshold_authority |
| 3 | threshold_reference |
| 4 | threshold_status |
| # | Key |
|---|---|
| 1 | readiness_gate |
| 2 | status |
| 3 | unresolved_signals |
| 4 | visibility_surface |
| # | Key |
|---|---|
| 1 | artifact_type |
| 2 | declared_description |
| 3 | declared_origin |
| 4 | declared_relationship |
| 5 | declared_retention_status |
| 6 | hash |
| 7 | hash_scope |
| 8 | hashed_by |
| 9 | pointer |
| # | Key |
|---|---|
| 1 | algorithm |
| 2 | value |
| Array | Order in the canonical form |
|---|---|
| extensions | sorted by value with PHP <=>: byte-wise for the registered extension names; numeric strings compared as numbers, non-strings as empty strings; equal items keep their submitted order |
| evidence_references | sorted by artifact_type, pointer, declared_origin, hash.value, byte-wise; items with equal keys keep their submitted order |
| any other array | submitted order |
intake_hash returned by the API for the same payload, your implementation reproduces the server's canonical form for that payload. Run this check after integration on representative payloads, including ones that submit extensions and evidence_references in a non-sorted order.The following simplified object demonstrates the canonicalization process. It is intentionally limited to the fields needed for this example and is not a complete API-conformant intake payload. The canonical form is what EVIDE hashes.
assessment.pdf now precedes interview-notes.pdf: the two references have the same artifact_type, so pointer decides. A complete payload has a different hash: run the algorithm on your full payload and compare it with the intake_hash returned by the API.Reference implementations of the rules in Section 2. Each one takes the request body as sent (the raw JSON text), decodes it as the server does and returns the intake hash. The evide_canonical_json / evideCanonicalJson functions return the canonical string itself, which is useful for finding the difference when two hashes do not match. The PHP version uses the same calls as the server; for numbers written with a decimal point or an exponent, its output also depends on the serialize_precision setting of the PHP process that runs it.
{} into [], as the server does. They are not guaranteed to reproduce the server in these cases:— numbers written with a decimal point or an exponent, and integers beyond ±(253−1): PHP re-serializes them in its own format (for example
1e25 becomes 1.0e+25);— objects whose member names are the integers
"0", "1", "2"…: the server can encode them as arrays;— arrays named
extensions that contain two or more numeric strings: PHP compares them as numbers, while the Python and JavaScript versions compare them byte-wise.The fields defined by EVIDE Schema 2.1 do not need any of these; free-form blocks such as
execution_identity might. In those cases, compute the hash with the PHP version.
EVIDE uses two distinct hash values for a JSON payload:
- content_hash (optional, source-declared) — an object
{"algorithm": "SHA-256", "value": "…"}in which the source system declares the hash it computed on the canonical payload before submission. It is removed before hashing, stored with the payload as a declaration, and never compared by EVIDE with its own computation. - intake_hash (always present, server-computed) — the SHA-256 of the canonical form that the server builds from the decoded payload. It is the reference value computed by EVIDE, returned in the API response and stored with the record.
content_hash with the returned intake_hash. If they are equal, the server canonicalized the same content the client hashed. If they differ, the payload changed in transit, the client used a different canonicalization, or the client hashed a non-canonical form — for example without reproducing the decoding effects described in Section 2.
If a FEDIS document is issued for the record, the SHA-256 fingerprint it states for the canonical JSON is the intake_hash, alongside the fingerprint of the frozen Evidentiary Artifact Record (EAR). FEDIS is optional and is generated only after the certifier explicitly initiates generation. The source-declared content_hash is not required for FEDIS.
intake_hash cover the submitted payload only. The evidentiary_profile — including the Forensic Cross-Check continuity dimension (Dim 9), Decision Wave Compression (Dim 10), and Formal Accountability Collapse (Dim 11) — is computed server-side after intake and is not part of the canonical form. These dimensions do not affect the intake_hash and do not appear in the key ordering reference.
✓ Decode the request body before canonicalizing, or reproduce the decoding effects
✓ Remove the top-level
content_hash only✓ Sort object keys byte-wise, recursively, at every nesting level
✓ Sort
extensions by value and evidence_references by composite key, keeping the submitted order of items that compare as equal✓ Keep the order of every other array
✓ Encode empty objects as
[]✓ Minify with no whitespace
✓ Write non-ASCII characters as-is, except U+2028 and U+2029 (
\u2028, \u2029)✓ Do not escape forward slashes
✓ Encode the string as UTF-8 before hashing
✓ Compare your output against the returned
intake_hash
EVIDE Minimum Intake JSON — Schema Reference: app.certifywebcontent.com/docs/evide-intake-schema/
Live JSON Schema: app.certifywebcontent.com/json
FEDIS Reference: certifywebcontent.com — FEDIS
EVIDE Signals & Roadmap: app.certifywebcontent.com/signals
The same canonical form always produces the same fingerprint.