Skip to content

content_marking.verify_assertion accepts missing required binding fields when both sides omit them #326

Description

@altrudev

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:

  1. Start from a valid assertion/record pair.
  2. Remove subject from both the assertion data object and the referenced record.
  3. Recompute record.hash over the exact modified record bytes.
  4. Call verify_assertion(assertion, modified_record_bytes).

The hash check succeeds because it is over the modified bytes. The subject comparison becomes:

None != None  # False

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions