Skip to content

provenance.build_record coerces malformed issued_at values before validation #320

Description

@altrudev

Problem

On current main at a247244fb0bb97aea4e60d655f36d445cca18cd5, provenance.build_record() converts an explicitly supplied issued_at with int(...) before passing it to the shared structural validator:

stamped_at = int(issued_at if issued_at is not None else time.time())
_check_structure(
    ...,
    issued_at=stamped_at,
)

But _check_structure() explicitly intends to enforce the original JSON-integer boundary:

# bool is an int subclass, and True would otherwise pass as a timestamp.
if not isinstance(issued_at, int) or isinstance(issued_at, bool) or issued_at < 0:
    raise ProvenanceError(...)

Because validation happens after coercion, that guard cannot distinguish several malformed caller values from legitimate integers.

Measured directly from the conversion used by the function:

supplied issued_at value handed to _check_structure
True 1
False 0
1.9 1
"123" 123
-0.5 0

All five therefore satisfy the validator's integer/non-negative test after conversion. The last case is particularly diagnostic: a negative non-integer is turned into an accepted non-negative timestamp.

Why this is distinct from #142 / #146

#142 found that consumer-side verification failed to enforce structural rules that build_record() was assumed to enforce, including the issued_at rule. #146 moved those rules into the shared _check_structure() helper.

This is a different producer-side ordering defect: build_record() transforms the value before the shared rule sees it. The shared validator itself is strict; the caller path defeats it by normalizing first.

Expected invariant

For an explicitly supplied issued_at:

  • a non-negative JSON/Python integer is accepted unchanged;
  • bool, float, string, null-equivalent explicit values, containers, and negative values are refused with ProvenanceError;
  • only the omitted/default path may derive an integer timestamp from time.time().

The emitted record should never contain a timestamp different in primitive/value semantics from the one the caller explicitly supplied.

Bounded fix direction

Validate the explicit issued_at before conversion, or avoid converting explicit values entirely. Keep conversion only for the internally generated default, e.g. conceptually:

stamped_at = int(time.time()) if issued_at is None else issued_at
_check_structure(..., issued_at=stamped_at)

Regression coverage should pin the coercible malformed values above plus an unchanged valid-integer control and the default-timestamp path.

No provenance wire-format, signature, freshness, revocation, catalog, or verifier semantics need to change.

Duplicate / ownership review

I searched the TRACE issue and PR history for provenance.build_record, issued_at, boolean/float/string coercion, and found no existing issue or PR covering this exact pre-validation coercion. #142/#146 are adjacent but address consumer/producer structural-rule symmetry, not this ordering bug.

AI-assistance disclosure: ChatGPT assisted with current-main source review, duplicate search, conversion reproduction, 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