Repository navigation
content_marking.build_assertion accepts truthy non-string eat_profile values #399
Description
Problem
On current main, content_marking.build_assertion() reads the record's eat_profile and only checks its truthiness:
profile = record.get("eat_profile")
...
if not profile:
raise ContentMarkingError("the record declares no eat_profile")That rejects missing and falsy values, but accepts any truthy non-string value. The accepted value is then copied directly into the produced assertion:
"eat_profile": profile,So values such as 1, True, [1], or {"x": 1} can be emitted by build_assertion() even though this field is a profile identifier and the companion assertion format shows it as a string.
This is the adjacent producer-side observation originally recorded in #326. @lywinged independently resurfaced it during review of #390 and noted that it currently has no issue carrying it.
Why this is separate from #326
#326 was a consumer-side presence defect in verify_assertion(): mutual absence of required fields could pass equality checks. That issue was fixed in #335.
This is different:
- it is on the producer path;
- the member is present;
- the defect is a shape/type boundary, not a presence boundary;
- a malformed truthy value can be emitted by the library itself.
Keeping this separate avoids reopening the already-completed #326 scope.
Minimal reproduction
Start from record bytes accepted by the producer path except for the eat_profile value, then set:
for value in [1, True, [1], {"x": 1}]:
record["eat_profile"] = value
assertion = build_assertion(json.dumps(record).encode(), url="https://example.test/r.json")
assert assertion["data"]["eat_profile"] == valueThe current truthiness guard accepts each of those values and preserves its original type.
Useful controls:
- absent
eat_profile-> refused; None-> refused;""-> refused;- valid non-empty profile string -> accepted;
- truthy non-string values -> currently accepted.
Contract boundary
spec/content-marking-v1.md section 2 defines eat_profile as the record's profile so a consumer knows what it is fetching before it fetches it. The example is textual, and build_assertion() is the producer for that assertion shape.
More importantly, the repository already uses the opposite validation pattern elsewhere: required textual identifiers are checked as non-empty strings rather than by truthiness alone. A producer should not manufacture an assertion shape that its own downstream consumers cannot safely treat as the documented profile identifier.
Bounded fix direction
Replace the truthiness check with an explicit type/emptiness check, for example:
if not isinstance(profile, str) or not profile:
raise ContentMarkingError("the record declares no valid eat_profile")Regression matrix:
- valid non-empty string succeeds;
- absent /
None/ empty string refuse; 0,False,[],{}continue to refuse;1,True,[1],{"x": 1}newly refuse.
No verifier semantics, digest binding, C2PA signature behavior, TRACE signature behavior, or wire-format version needs to change.
Duplicate / ownership check
I searched current issues/PRs for content_marking, build_assertion, eat_profile, and truthiness/type handling. #326 contains the original adjacent observation but is closed and fixed for a different consumer-side defect. I found no open issue or PR carrying this producer-side type boundary.
AI-assisted source review, duplicate search, and regression design; @altrudev remains responsible for the contribution.
Activity
github-actions commentedon Sep 22, 2026
🔴 Contributor Check: HIGH
Automated check by AgenTrust Contributor Check.