Repository navigation
fix(verify): enforce manifest signer window and metadata - #51
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_untilunset or>= head).manifest_resolved_to_beyond_headmanifest_count_mismatchmanifest_first_entry_hash_mismatchmanifest_head_hash_mismatch, when the export reaches the headEvery 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-headis required for freshness. This is documented in the spec, CLI help and README.Validation
go build ./...: passedgo vet ./...: passedgo test -race ./...: passed, including 64 concurrent exports against 32 rotationssemgrep --config auto --error --quiet ./internal ./cmd ./pkg: passedReview
Two rounds by a second model (Claude Opus 5.5).
Rotate'satSeq == head + 1rule.