Problem
On current main at a247244fb0bb97aea4e60d655f36d445cca18cd5, content_marking.verify_assertion() checks the duplicated binding fields with ordinary .get() equality:
if record.get("subject") != data.get("subject"):
raise RecordMismatch(...)
if record.get("eat_profile") != data.get("eat_profile"):
raise RecordMismatch(...)
That establishes equality only when at least one side has a value. It does not establish presence.
If a peer-produced assertion omits data.subject, and the hash-matching fetched record also omits subject, both reads produce None, so the comparison passes. The same is true for eat_profile.
A malformed assertion and malformed referenced record can therefore agree by mutual absence and verify_assertion() returns the parsed record successfully.
Why this is a contract violation
spec/content-marking-v1.md section 2 marks both fields as required:
| Field |
Required |
subject |
yes |
eat_profile |
yes |
The verification algorithm then requires the consumer to check that both assertion fields match the fetched record, and section 6 says a conforming consumer performs those checks.
The module's own public contract says malformed assertions raise ContentMarkingError. Mutual absence is malformed input, not a successful binding.
This matters independently of full TRACE-record verification. verify_assertion() explicitly performs only the content-marking binding check and returns the parsed record on success. A caller is allowed to run this layer separately from the later Trust Record signature/schema verification, so this layer has to establish its own required assertion shape rather than relying on another verifier to reject the fetched record later.
Minimal reproduction
Conceptually:
- Start from a valid assertion/record pair.
- Remove
subject from both the assertion data object and the referenced record.
- Recompute
record.hash over the exact modified record bytes.
- Call
verify_assertion(assertion, modified_record_bytes).
The hash check succeeds because it is over the modified bytes. The subject comparison becomes:
and therefore does not reject.
Repeat with eat_profile; the same thing happens.
A stronger regression matrix should include:
- assertion missing
subject, record missing subject;
- assertion missing
eat_profile, record missing eat_profile;
- both fields missing on both sides;
- assertion-only omission still refused;
- record-only omission still refused;
- valid pair still succeeds.
The first three are the cases current equality cannot distinguish.
Bounded fix direction
Establish the assertion's required fields before comparing them to the fetched record. For example, require:
data.subject to be a valid non-empty SPIFFE URI or DID string;
data.eat_profile to be a non-empty string;
- corresponding fetched-record fields to be present with the expected shape before equality is evaluated.
Then use RecordMismatch only for two present, individually valid fields that disagree. Missing/malformed fields should remain ContentMarkingError, because that means malformed content rather than a well-formed assertion pointing at a different record.
No digest algorithm, C2PA manifest-signature, TRACE signature, URL, anchor, or wire-format semantics need to change.
Adjacent producer-side observation
build_assertion() already validates the record subject shape, but its eat_profile guard is only:
if not profile:
raise ContentMarkingError(...)
so a truthy non-string eat_profile can be copied into a produced assertion. That is adjacent evidence that these two required binding fields do not yet have one shared shape boundary, but it is not necessary to fix the mutual-absence verifier defect and should not broaden this issue unless maintainers prefer to consolidate the field-validation contract.
Duplicate / ownership review
I searched issues and PRs for content_marking, verify_assertion, missing/required subject, and eat_profile. I found no existing issue or PR covering this mutual-absence success case.
PR #227 is adjacent but different: it fixed non-object / invalid-JSON record_bytes crashes. PR #325 sweeps malformed public-function arguments and several parameter-shape failures, but it does not validate nested required members inside a content-marking assertion.
AI-assistance disclosure: ChatGPT assisted with current-main source review, spec comparison, duplicate/ownership search, reproduction design, and issue drafting. altrudev reviewed the bounded claim and remains responsible for the contribution.
Problem
On current
mainata247244fb0bb97aea4e60d655f36d445cca18cd5,content_marking.verify_assertion()checks the duplicated binding fields with ordinary.get()equality:That establishes equality only when at least one side has a value. It does not establish presence.
If a peer-produced assertion omits
data.subject, and the hash-matching fetched record also omitssubject, both reads produceNone, so the comparison passes. The same is true foreat_profile.A malformed assertion and malformed referenced record can therefore agree by mutual absence and
verify_assertion()returns the parsed record successfully.Why this is a contract violation
spec/content-marking-v1.mdsection 2 marks both fields as required:subjecteat_profileThe verification algorithm then requires the consumer to check that both assertion fields match the fetched record, and section 6 says a conforming consumer performs those checks.
The module's own public contract says malformed assertions raise
ContentMarkingError. Mutual absence is malformed input, not a successful binding.This matters independently of full TRACE-record verification.
verify_assertion()explicitly performs only the content-marking binding check and returns the parsed record on success. A caller is allowed to run this layer separately from the later Trust Record signature/schema verification, so this layer has to establish its own required assertion shape rather than relying on another verifier to reject the fetched record later.Minimal reproduction
Conceptually:
subjectfrom both the assertiondataobject and the referenced record.record.hashover the exact modified record bytes.verify_assertion(assertion, modified_record_bytes).The hash check succeeds because it is over the modified bytes. The subject comparison becomes:
and therefore does not reject.
Repeat with
eat_profile; the same thing happens.A stronger regression matrix should include:
subject, record missingsubject;eat_profile, record missingeat_profile;The first three are the cases current equality cannot distinguish.
Bounded fix direction
Establish the assertion's required fields before comparing them to the fetched record. For example, require:
data.subjectto be a valid non-empty SPIFFE URI or DID string;data.eat_profileto be a non-empty string;Then use
RecordMismatchonly for two present, individually valid fields that disagree. Missing/malformed fields should remainContentMarkingError, because that means malformed content rather than a well-formed assertion pointing at a different record.No digest algorithm, C2PA manifest-signature, TRACE signature, URL, anchor, or wire-format semantics need to change.
Adjacent producer-side observation
build_assertion()already validates the record subject shape, but itseat_profileguard is only:so a truthy non-string
eat_profilecan be copied into a produced assertion. That is adjacent evidence that these two required binding fields do not yet have one shared shape boundary, but it is not necessary to fix the mutual-absence verifier defect and should not broaden this issue unless maintainers prefer to consolidate the field-validation contract.Duplicate / ownership review
I searched issues and PRs for
content_marking,verify_assertion, missing/requiredsubject, andeat_profile. I found no existing issue or PR covering this mutual-absence success case.PR #227 is adjacent but different: it fixed non-object / invalid-JSON
record_bytescrashes. PR #325 sweeps malformed public-function arguments and several parameter-shape failures, but it does not validate nested required members inside a content-marking assertion.AI-assistance disclosure: ChatGPT assisted with current-main source review, spec comparison, duplicate/ownership search, reproduction design, and issue drafting.
altrudevreviewed the bounded claim and remains responsible for the contribution.