pilotctl: anchor the cap-state HMAC chain to its length and tail - #429
Merged
Conversation
The per-record chained HMAC over cap-state.jsonl commits only to the records preceding each record, so every prefix of a valid chain is itself a valid chain. A log rewound to an earlier prefix verified line by line and simply reported less usage. Add a sidecar anchor file (cap-state.jsonl.anchor) holding the number of authenticated records and the final chain value, bound together by a MAC under the same key the chain uses. loadCapStateRecordsAnchored checks it on load: fewer records than anchored, or a different chain value at the anchored position, is an error; a strictly longer chain that still matches advances the anchor. Backward compatible: a log with no anchor beside it is accepted and an anchor is adopted for it, and unauthenticated (legacy) logs skip the anchor entirely. Setting PILOT_CAP_STATE_STRICT_ANCHOR=1 (default unset) turns the anchorless case into an error instead. Anchor writes are best-effort so a read-only install tree keeps working. Co-Authored-By: Claude Opus 5 <noreply@anthropic.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.
L5 — cap-state HMAC chain has no tail anchor
loadCapStateRecords(cmd/pilotctl/appstore.go) verifies a chained HMAC per line, where each record's MAC covers the record plus the previous record's MAC. That commits to everything before a record but to nothing after it, so every prefix of a valid chain is itself a valid chain. Acap-state.jsonlrewound to an earlier prefix verified line by line and reported less usage — despite the existing error string already claiming to detect "truncated" logs.Fix
A sidecar anchor file
cap-state.jsonl.anchorholds the number of authenticated records and the final chain value, bound together by a MAC under the same HKDF-derived key the chain uses (so it cannot be rewritten without the daemon identity).loadCapStateRecordsAnchoredwraps the existing loader and checks the anchor:pilotctl appstore capsnow goes through the anchored loader.Backward compatibility
hmacfield) logs skip the anchor path entirely.PILOT_CAP_STATE_STRICT_ANCHOR=1, default off.No wire or protocol change — this is a local on-disk sidecar read/written only by
pilotctl.Tests
TestLoadCapStateRecordsAnchoredDetectsTruncationbuilds a 4-record chain, adopts an anchor at 3, appends to 4, then rewinds to 2 — asserting the prefix still passes per-record verification (loadCapStateRecords) while the anchored loader rejects it. Also covers an emptied log and a forged anchor.TestLoadCapStateRecordsAnchoredLegacyStillLoadspins the legacy/missing-file/nil-key paths, andTestLoadCapStateRecordsStrictAnchorFlagcovers the default-off flag.go build ./cmd/pilotctl/...,go vet, andgo test ./cmd/pilotctl/...all pass.🤖 Generated with Claude Code