Is redactions[].salted_sha256 mandatory?
Status
Accepted — revised Option A, version-gated — 23 August 2026 (owner review). Raised the same day; decided the same day.
This was the only open decision on the VER standards track. It is now closed, and the artifacts reflect it: standards/ver/1.1-draft/VER-1.1-draft.md §5 carries the normative text, the draft schema encodes the requirement and its exception, and VER1002's severity question has an answer (it stays a warning for 1.0.x records, forever, and has no jurisdiction over 1.1 ones — 1.1 makes the requirement a schema error).
Context
§7.5 defines redaction-with-proof, and it is one of the standard's better ideas:
A redacted value is replaced by
{ redacted: true, reason, salted_sha256 }with the salt held out-of-record. The Record can thereby prove a value existed — and match it on challenge — without disclosing it.
The whole construction rests on salted_sha256. Without it, "redacted" carries no proof; it is a claim that something was removed, indistinguishable in evidential weight from a claim that nothing was there.
The schema does not agree with the prose, in two independent ways (Appendix D #8, findings SCH-14, SPEC-02):
- The §7.5 literal is rejected outright by
redactions[]. That array's items requirefield, forbid additional properties, and therefore reject the memberredactedentirely. Empirically,redactions: [{"redacted": true, "reason": "privacy", "salted_sha256": "…"}]fails validation against the published schema. salted_sha256is optional inredactions[], whose items require onlyfieldandreason. A Record may therefore declare a redaction with no proof at all and remain fully schema-valid.
Annex E15 resolved the first half: the §7.5 literal describes the replacement value inside the normalized view, while redactions[] is the ledger. Two objects, two places, no conflict. E15 also ruled that a Record MUST NOT claim redaction of a value while preserving a raw segment that still contains it (VER1005), and described the two-envelope pattern that satisfies both §7.1 and §7.5.
E15 did not resolve the second half, because it cannot be resolved by interpretation. The prose says the proof is the point; the schema says the proof is optional. One of them has to change, and which one changes is a policy question with legal and operational consequences, not an editorial one.
Options
Option A — salted_sha256 becomes REQUIRED
Every redactions[] entry must carry a salted hash of the removed value.
- Redaction-with-proof means what §7.5 says. A
redactedstate is evidence. - Challenge-response works uniformly: given a candidate value and the salt, any holder of the Record can confirm or refute.
- Scope decides the release, not the direction. Required for all Records is a major change: every existing hash-less redaction becomes invalid, which `VERSIONING.md` §8 classifies as VER 2.0 material. Required only for Records declaring
ver_version: "1.1"is a legal minor change, on exactly the same footing as ADR-0008'sdim/dtyperequirement: §2.2 permits a minor release to promote a member to REQUIRED for records declaring the newver_version, and the 1.1 draft already ships one such promotion. - Some redactions genuinely cannot produce a hash. A value destroyed upstream, or removed by a platform before acquisition, was never held by the producer; requiring a hash of it forces either a fabricated value or a state other than
redacted. - Salt management becomes a conformance dependency. A producer who loses the salt holds a Record whose proofs can never be exercised, and there is nothing in the Record to signal that.
Option B — salted_sha256 stays OPTIONAL, prose relaxed
The schema is correct; §7.5's prose is aspirational and is softened to SHOULD.
- No Record becomes invalid; ships as a patch-compatible clarification.
- Accommodates the destroyed-upstream case honestly.
redactedloses its evidential meaning. It degrades to "we removed something", which is barely stronger thanabsentand cannot be relied on by a downstream party.- Producers will take the cheap path. An optional proof field in a privacy-sensitive context is a field that is usually absent.
- The standard's stated advantage over "just delete it" evaporates, and §0's claim to have fixed the privacy defect weakens with it.
Option C — required-when-possible, with an explicit reason code
salted_sha256 is required unless the entry carries a reason indicating the value was never held (upstream_destroyed, platform_stripped, never_acquired).
- Preserves the proof where proof is possible; permits honesty where it is not.
- The distinction is stated rather than inferred from an absent field, which is the actual defect in option B.
- Expressible in JSON Schema as a conditional
requiredon a closed reason enum. - Costs a new closed enum — which
SCH-22warns is a registry waiting to happen — and adds a way to opt out that a lazy producer will use uniformly. - Whether it is a narrowing depends on the enum: existing hash-less redactions become valid only if their
reasonvalues happen to match. Realistically 1.1 material with aver_versiongate, not 1.0.x.
Decision
Option A, revised, gated on ver_version: "1.1". The revision takes Option C's honest half — a stated escape for the never-held case — without Option C's cost, by putting the distinction in a two-value state rather than in a reason enum that would grow into an ungoverned registry.
Four parts, all of them in the 1.1 draft and its schema:
1. The requirement, and its one exception
metadata.redactions[] entries gain a REQUIRED state:
state | Meaning | Commitment |
|---|---|---|
producer_performed | The Producer held the value and removed it | REQUIRED |
upstream_withheld | The Producer never received the value | MUST NOT be present |
Both halves are schema-encoded. A producer_performed redaction without a commitment is a claim with nothing behind it; an upstream_withheld redaction with one is a fabrication, since there was nothing to commit to.
metadata.availability gains a matching state, upstream_withheld — an additive 1.1 enum extension alongside the five 1.0 states, distinct from stripped_suspected (a belief about what an upstream did) and from absent (which says nothing about why). The ledger entry and the family's availability state MUST agree, which VER1001 enforces.
This is the case Option A could not express and Option B answered by weakening everything. Naming it costs one enum member and one required field, and it converts "we don't know whether this producer could have committed" into a statement the producer made on the record.
2. The rename: salted_sha256 → commitment_sha256 + commitment_alg
For 1.1 Records the member is commitment_sha256, with a sibling commitment_alg, const "ver-redaction-commitment/1". The two are present together or not at all.
The rename is not cosmetic and is not free: the preimage changes (part 3), so a 1.0.x salted_sha256 value is not a valid commitment_sha256. A producer migrating must recompute from the salt and the value, and a producer that has lost the salt cannot migrate that entry at all — it must be restated as upstream_withheld, or the Record must stay at 1.0. That is a real migration cost and it is listed as such in 1.1 §12.1 row 10.
Naming the algorithm in the Record is what makes a future construction possible without a second reading of the same field — the same discipline bundle_digest_alg applies in ADR-0007.
3. The construction, normative
- The domain tag is the 26 ASCII octets of
"VER-REDACTION-COMMITMENT-1", with no terminator, so a commitment digest cannot collide with a digest computed for any other purpose in this standard. len8(x)is the octet length ofxas an 8-octet big-endian unsigned integer. Both lengths are present because a preimage concatenating two variable-length fields without framing is ambiguous —SC-03is the same defect in the model-weights preimage, and §4'spixel_hashrationale eliminated it there first.saltis at least 16 octets, unique per(producer, field, asset)or better, never derived from the value, and never present in any Record — including insideextensions.JCS(value)is the RFC 8785 canonical serialization of the value **as it appeared in the normalized view**, so a string is serialized with its JSON quoting and a structured value as the object or array it was. The 1.0 prose'ssalt || "|" || valueleft the value's serialization unstated, which is a preimage two implementations cannot agree on.
4. The language: this is a commitment, not proof of history
1.0 §7.5 says the Record can "prove a value existed". It cannot, and 1.1 does not repeat the claim. What the construction gives is:
- A commitment: the Producer is bound to the value no later than the moment the Record is signed. Given the salt and a candidate, any holder can confirm or refute that the Producer committed to that candidate.
- Not evidence that the value was ever in the Asset. A Producer can commit to anything. Evidence about the Asset's own metadata requires a signature over that metadata by a party the Consumer trusts, which is what C2PA is for.
Third-party verification requires controlled salt disclosure to an authorized challenger. The salt is what makes the digest non-invertible; without it a challenger can compute nothing. Who is authorized, over what channel, with what audit trail, and with what defence against a challenger who simply guesses low-entropy values, is a protocol — and it is explicitly out of scope for VER 1.1, stated as such in the draft rather than left for a deployer to discover. A producer deploying redaction in a regulated context needs that protocol and MUST NOT assume this standard supplies it.
5. 1.0.x is unchanged
salted_sha256 keeps its 1.0.x spelling and stays optional there. VER1002 remains a warning for 1.0.x Records — not because the question is open, but because the answer is version-gated and a 1.0 Record is judged by 1.0. Nothing published becomes non-conformant.
Decision criteria, and how they were answered
The criteria this ADR set for itself, with the answers the decision gives:
- Does a
redactedstate without proof mislead a downstream consumer? Yes — which disqualifies Option B, and is why the commitment is required wherever it is possible. - Can a producer that never held the value comply? Yes, by saying so:
state: "upstream_withheld", with a matching availability state. The escape is explicit and machine-readable rather than inferred from an absent field. - What does the deployed corpus actually contain? Still unknown; no survey exists. The
ver_versiongate makes the answer non-blocking — no deployed Record is invalidated either way — which is precisely why the gated form was chosen over the ungated one. - Is
redactedload-bearing in any regulated deployment? Nobody has stated a concrete requirement of this shape. Part 4 is the honest response: the standard says what its construction does and does not establish, and names the missing protocol rather than implying it exists.
Consequences
Good.
redactedmeans something again for 1.1 Records, and what it means is stated precisely enough that a lawyer and an implementer read the same sentence.- The never-held case is expressible for the first time, in both the ledger and the availability vocabulary.
- The preimage is unambiguous, domain-separated and length-framed, so two implementations compute the same digest.
- The overclaim is retired. "Prove a value existed" was the sentence that made the mechanism sound like evidence; a reader now learns what it is worth before relying on it.
- The standards track has no open decisions.
Bad.
- The rename costs a real migration, and one that cannot always be performed: a producer that has lost a salt cannot recompute, and must restate the entry as
upstream_withheldor leave the Record at 1.0. Recording that plainly is better than a rename that silently kept the old digest under a new name, which would have been a false commitment. stateis a third thing to keep consistent — ledger entry, availability family, and normalized-view object — and only the first is fully schema-checkable.VER1001covers the pairing; the normalized-view agreement is a two-member comparison the schema cannot make.- A producer will use
upstream_withheldas a lazy opt-out, exactly as Option C predicted for its reason enum. The mitigation is that the claim is now stated, so it is auditable and falsifiable against the availability state — not that it is prevented. - Salt management is now a conformance dependency for 1.1. That was true of Option A from the start and does not go away.
Neutral.
- The wire profile carries no redaction ledger at all, so nothing on the wire changes.
VER1002keeps its code and its warning severity for 1.0.x. Under ADR-0003 a code's severity may tighten only when the underlying requirement becomes normative; here the requirement became normative for a different version, so the 1.0.x rule is untouched and the 1.1 rules are new codes.
