Skip to content

fix(verify): enforce manifest signer window and metadata - #51

Merged
shreyanshjain7174 merged 2 commits into
mainfrom
fix/verify-manifest-window
Oct 18, 2026
Merged

shreyanshjain7174 merged 2 commits into
mainfrom
fix/verify-manifest-window

Conversation

@shreyanshjain7174

Copy link
Copy Markdown
Contributor

Plan

Security hardening found while reviewing the RECEIPT-v1 spec (#49). Non-breaking: no wire or encoding change, and the version lock is unchanged.

Weakness. The offline verifier did not check the manifest signer's key validity window or the manifest metadata. Someone holding a retired (for example, compromised) key still in the trust history could sign a manifest for a truncated range with head_seq = resolved_to. The CLI then reported the range as complete and full. Key rotation exists to stop exactly this.

What changed

  • New verifier checks, each with its own reason:

    • manifest_signer_inactive_at_head: the signer's window must cover the manifest head (valid_from <= head + 1, valid_until unset or >= head).
    • manifest_resolved_to_beyond_head
    • manifest_count_mismatch
    • manifest_first_entry_hash_mismatch
    • manifest_head_hash_mismatch, when the export reaches the head

    Every manifest an honest gateway produces passes, including exports that race a rotation.

  • Export snapshot. Export takes the signer lease before reading SQLite, the same lock order as the ledger and Rotate. Head, anchor, rows and keys come from one snapshot, so the manifest signer is always in the embedded keyset. This closes a race where it could be missing. Manifest bytes are unchanged.

  • Empty exports (count = 0, endpoint equals anchor) now verify. Before, they were rejected.

  • CLI wording. Without --expected-head, the CLI says completeness is proven against the signed manifest endpoint and freshness is not proven. It no longer claims the database's true head. The help text and README explain why.

  • 14 new golden vectors: signer windows, retired-key full-range claim (rejected), metadata corruption, rotation boundaries, historical ranges, empty exports. The vector now rejected was renamed to manifest-inflated-count.

  • RECEIPT-v1 §8, §9, §11 and §12 are updated. §12 notes that tightening manifest checks is outside the wire freeze.

Residual

A key that was active at head S could legitimately sign a full export at head S. No verifier rule can tell an archived honest export from a replay without a freshness input, so --expected-head is required for freshness. This is documented in the spec, CLI help and README.

Validation

  • go build ./...: passed
  • go vet ./...: passed
  • go test -race ./...: passed, including 64 concurrent exports against 32 rotations
  • semgrep --config auto --error --quiet ./internal ./cmd ./pkg: passed
  • Vectors regenerated twice: empty diff. Version-lock entries for passing vectors unchanged.

Review

Two rounds by a second model (Claude Opus 5.5).

  • It traced all 14 vectors by hand.
  • It confirmed no false rejections given Rotate's atSeq == head + 1 rule.
  • It reproduced the attack and confirmed it is now rejected.
  • It flagged the CLI overclaim, fixed here.

shreyanshjain7174 and others added 2 commits October 18, 2026 06:37
Reject out-of-window manifest signers and inconsistent range metadata.
Pin export signing to its snapshot across key rotation.
Accept authenticated empty exports without changing v1 bytes.

Signed-off-by: Shreyansh Sancheti <43677304+shreyanshjain7174@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed manifest endpoints prove only their selected range.
Avoid claiming an independent head or freshness without --expected-head.
Rename the rejected inflated-count vector without changing v1 locks.

Signed-off-by: Shreyansh Sancheti <43677304+shreyanshjain7174@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@shreyanshjain7174
shreyanshjain7174 merged commit f71e282 into main Oct 18, 2026
1 check passed
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.

1 participant