Skip to content

fix(trace_verify/_checkpoint): enforce canonical lowercase hex on key_id, root, and signature and reject unknown CheckpointRecord fields (#148) - #152

Open
drk-ram wants to merge 1 commit into
agentrust-io:mainfrom
drk-ram:drk-ram-fix-checkpoint-hex-148
Open

drk-ram wants to merge 1 commit into
agentrust-io:mainfrom
drk-ram:drk-ram-fix-checkpoint-hex-148

Conversation

@drk-ram

@drk-ram drk-ram commented Oct 23, 2026 •

Copy link
Copy Markdown

What and why

Closes #148

  • Share _validate_checkpoint_fields across CheckpointRecord.from_dict, verify_checkpoint_signature_offline, and verify_checkpoint_link / verify_checkpoint_chain so semantic invariants (v == 1, kind == "mmr_checkpoint", mmr_size >= 1, 0 <= prev_size < mmr_size, string log_id/timestamp, 64-character lowercase hex root/key_id/prev_root, 128-character lowercase hex signature, and consistency_proof presence/absence) are enforced uniformly on both parsed dicts and directly constructed CheckpointRecord instances.
  • Reject unknown fields outside _REQUIRED_FIELDS | {"prev_root", "consistency_proof"} and non-string keys in CheckpointRecord.from_dict.
  • Enforce 64-character lowercase hex (^[0-9a-f]{64}$) on all consistency_proof digests (old_peaks, new_peaks, and witness) in ConsistencyProof.from_dict and _validate_consistency_proof.
  • Add independent regression tests with valid signed controls in tests/test_checkpoint_record.py.

Validation

  • python -m unittest discover -s tests -q — passed (410 tests)
  • python -m pytest tests/test_checkpoint_record.py tests/test_mmr_checkpoint_adversarial.py tests/test_fuzz_regressions.py -q — passed (32 passed, 25 subtests passed)
  • python tools/validate_registry.py — passed (OK: 3 day file(s) valid)
  • python tools/validate_producers.py — passed (OK: 3 producer key file(s) valid)
  • python -m ruff check src/trace_verify/_checkpoint.py src/trace_verify/_mmr.py tests/test_checkpoint_record.py — passed (All checks passed!)

Registry impact

  • This PR does not modify any existing line under registry/

Existing entries are append-only. If you are changing one, say why here: mirrors
and auditors may already have observed the current commit hashes, and a rewrite
diverges them.

Checks

  • pytest passes
  • Registry and producer files still validate (tools/validate_registry.py,
    tools/validate_producers.py)
  • If the anchor format or verification semantics changed, docs/anchor-format.md
    is updated in the same PR, and existing inclusion proofs still verify
  • Commits are signed off (DCO)

@drk-ram
drk-ram requested review from a team, lywinged and rajnisht7 as code owners October 23, 2026 16:00

@rajnisht7 rajnisht7 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the hex checks and unknown-field rejection looks good, and the verify paths now properly reject non-canonical values. But this doesn’t fully close #148.CheckpointRecord.from_dict() still only checks required fields, types, and unknown keys. It does not enforce the semantic invariants the issue explicitly asked for:

  • v == 1
  • kind == "mmr_checkpoint"
  • mmr_size >= 1
  • 0 <= prev_size < mmr_size
  • canonical lowercase hex on key_id, root, non-empty prev_root, and signature

so these still parse successfully:

{"v": 999, "kind": "garbage", "mmr_size": 0, ...}
{"mmr_size": 10, "prev_size": 10, ...}
{"root": "AA" * 32, ...}

verify_checkpoint_signature_offline only checks hex + the signature. It does not look at v/kind/mmr_size and verify_checkpoint_chain on a singleton only runs the signature check, a correctly signed but semantically invalid checkpoint (wrong v, wrong kind, mmr_size=0, etc.) currently returns success.
Also:

  • later checkpoints without consistency_proof are still accepted by from_dict (the class docstring says they must carry one).
  • new test only covers unknown field + a couple of uppercase/padded cases via the verify path. It does not cover the missing from_dict invariants.
  • Minor: sorted(set(d) - _ALLOWED_FIELDS) can raise TypeError instead of ValueError if a non-string key is passed.
    kindly add the full set of checks inside from_dict, harden the unknown-key error path, and add regression tests for each case (including rejection through from_dict itself and a singleton chain). Existing valid fixtures should still round-trip cleanly.

@drk-ram

drk-ram commented Oct 23, 2026

Copy link
Copy Markdown
Author

Thanks @rajnisht7! Addressed in the latest commit: both CheckpointRecord.from_dict() and verify_checkpoint_signature_offline() now share _validate_checkpoint_fields() enforcing v == 1, kind == "mmr_checkpoint", mmr_size >= 1, 0 <= prev_size < mmr_size, 64-char lowercase hex on root, key_id, and prev_root (or "" iff prev_size == 0), 128-char lowercase hex signature, consistency_proof presence/absence (None or [] when prev_size == 0; non-empty list of 64-char lowercase hex strings when prev_size > 0), and string-only keys before signing-body serialization, with expanded tests in tests/test_checkpoint_record.py.

@rajnisht7 rajnisht7 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the semantic checks in from_dict and the expanded tests look good, and the earlier gaps (unknown fields, non-string keys, v/kind/size, main hex fields) are addressed

A few things in this latest commit still doesnt match the response or the class contract:

  • No shared validator - the comment says both from_dict and verify_checkpoint_signature_offline now share _validate_checkpoint_fields(). That helper doesn’t exist, the checks are still duplicated, and the offline path is a subset.
  • consistency_proof elements aren’t canonical. from_dict just hands the dict to ConsistencyProof.from_dict, which doesn’t enforce ^[0-9a-f]{64}$. Uppercase and whitespace-padded digests parse fine (schema and the claimed “64-char lowercase hex” both require it).
  • [] vs claim. Response said None or [] for prev_size == 0. Code rejects [] (raises). Class docstring only allows None for the first checkpoint, kindly align the comment with the actual intended behavior.
  • direct/singleton bypass - CheckpointRecord constructed directly with prev_size > 0 and consistency_proof=None still makes verify_checkpoint_signature_offline and verify_checkpoint_chain([cp]) return True. Class contract says later checkpoints must carry one. Same for non-string log_id / timestamp, offline path doesn’t check them (while from_dict and verify_checkpoint_link do).

@dinakarjs dinakarjs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed this head; PYTHONPATH=src:. python -m unittest discover -s tests -q passes 407 tests. I also reproduced the inconsistency already reported by Rajnish: a correctly signed checkpoint with prev_size=1, prev_root present and consistency_proof=None can pass the offline signature and singleton-chain paths while from_dict rejects it. This confirms the existing finding rather than adding a separate issue. Please use consistent structural validation across the entry points and retain independently failing regressions with valid signed controls before closing #148.

@drk-ram

drk-ram commented Oct 24, 2026

Copy link
Copy Markdown
Author

Thanks @rajnisht7! Addressed all four points in the latest commit: (1) extracted _validate_checkpoint_fields(fields, consistency_proof) in src/trace_verify/_checkpoint.py and called it from both CheckpointRecord.from_dict() and verify_checkpoint_signature_offline() (enforcing consistency_proof is None when prev_size == 0, consistency_proof is not None when prev_size > 0, and string log_id/timestamp on directly constructed records); (2) enforced 64-character lowercase hex on all old_peaks, new_peaks, and witness digests in ConsistencyProof.from_dict() (src/trace_verify/_mmr.py); and (3) added unit tests in tests/test_checkpoint_record.py covering uppercase digests in consistency_proof, singleton/direct CheckpointRecord with prev_size > 0 and consistency_proof=None, and non-string log_id/timestamp.

@drk-ram
drk-ram force-pushed the drk-ram-fix-checkpoint-hex-148 branch from 1a50ffb to 1a57c3a Compare October 29, 2026 16:17
…_id, root, and signature and reject unknown CheckpointRecord fields (agentrust-io#148)

- Share _validate_checkpoint_fields across CheckpointRecord.from_dict, verify_checkpoint_signature_offline, and verify_checkpoint_link / verify_checkpoint_chain so semantic invariants (v == 1, kind == 'mmr_checkpoint', mmr_size >= 1, 0 <= prev_size < mmr_size, string log_id/timestamp, 64-char lowercase hex root/key_id/prev_root, 128-char lowercase hex signature, and consistency_proof presence/absence) are enforced uniformly on both parsed dicts and directly constructed CheckpointRecord instances.
- Enforce 64-character lowercase hex on consistency_proof digests in ConsistencyProof.from_dict and _validate_consistency_proof.
- Add independent regression tests with valid signed controls in tests/test_checkpoint_record.py.

Closes agentrust-io#148

Signed-off-by: Raghuvar Karthik Durga <177662415+drk-ram@users.noreply.github.com>
@drk-ram
drk-ram force-pushed the drk-ram-fix-checkpoint-hex-148 branch from 1a57c3a to 79e666f Compare October 29, 2026 16:19
@drk-ram

drk-ram commented Oct 29, 2026

Copy link
Copy Markdown
Author

Thanks @rajnisht7 and @dinakarjs! Addressed all points in 79e666f: (1) extracted _validate_checkpoint_fields and _validate_consistency_proof in src/trace_verify/_checkpoint.py and invoked them from CheckpointRecord.from_dict, verify_checkpoint_signature_offline, and verify_checkpoint_link / verify_checkpoint_chain so directly constructed CheckpointRecord instances with prev_size > 0 and consistency_proof=None (or non-string log_id/timestamp or invalid v/kind/mmr_size/prev_size/hex fields) are rejected across all entry points; (2) enforced ^[0-9a-f]{64}$ on all consistency_proof digests in ConsistencyProof.from_dict and _validate_consistency_proof (including direct ConsistencyProof instances); (3) aligned the docstrings so prev_size == 0 requires consistency_proof is None and empty/omitted prev_root, while prev_size > 0 requires a valid ConsistencyProof and 64-char lowercase hex prev_root; and (4) added independent regression tests with valid signed controls in tests/test_checkpoint_record.py (test_direct_checkpoint_with_prev_size_without_consistency_proof_rejected, test_consistency_proof_uppercase_or_whitespace_padded_hex_rejected, and test_non_string_log_id_or_timestamp_on_direct_checkpoint_rejected). Full unittest discovery (410 tests) and Ruff pass.

@dinakarjs dinakarjs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up review at exact head 79e666f, Linux/Python 3.12.14 with the repository's hash-locked CI/witness/runtime dependencies and the local editable checkout.

The shared checkpoint validation now covers parsed and directly constructed records across offline signature/link/chain entry points; consistency-proof digests use the same lowercase-hex restriction. Full unittest discovery: 410 tests pass; repository Ruff passes. I inspected the shared validation and added direct-record regressions with signed valid controls. The previously reported entry-point inconsistency is addressed in this scope. Normal hosted checks and merge requirements remain separate.

Reviewed with OpenAI Codex.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(trace_verify/_checkpoint): enforce canonical lowercase hex on key_id, root, and signature and reject unknown CheckpointRecord fields

3 participants