Skip to content

Add tamper-evident integrity to spend-cap state#22

Merged
TeoSlayer merged 1 commit into
mainfrom
security/spendcap-integrity
Jun 22, 2026
Merged

Add tamper-evident integrity to spend-cap state#22
TeoSlayer merged 1 commit into
mainfrom
security/spendcap-integrity

Conversation

@TeoSlayer

Copy link
Copy Markdown
Contributor

Problem

The cap-state spend log (cap-state.jsonl) was plain JSONL with no integrity, and loadSpendRecords silently skipped malformed lines. The same OS user could therefore tamper with or erase spend history to bypass caps, and a garbage line overwriting a real spend went unnoticed (under-counting → cap loses force).

Fix

  • Per-record chained HMAC-SHA256 over each record, keyed off the wallet identity via HKDF (info=pilot-cap-state-v1), exposed as UseCapStateFileWithHMAC. cmd/wallet derives the key from the LocalSigner and uses the authenticated path by default.
  • Fail-closed load: a malformed line, an HMAC mismatch, or a signed chain spliced with an unauthenticated record now refuses to load (the wallet refuses to spend rather than spending against a forged history). The legacy UseCapStateFile path stays for unauthenticated files but also stops silently skipping malformed lines.
  • Backward-compat migration: a wholly-legacy (HMAC-less) file is loaded once and rewritten as a fresh authenticated chain (atomic temp+rename). A mixed file is rejected (only reachable via tampering).

The HKDF derivation matches pilotctl's reader (appstore caps) so the daemon reads what the wallet writes.

Tests

  • TestCapStateTamperedRecordDetected — altered amount with stale HMAC, and a plain record spliced into a signed chain, both caught.
  • TestCapStateMalformedLineNotSilentlyDropped — garbage line errors (with and without a key) instead of being dropped.
  • TestCapStateHMACRoundTrip — authenticated chain survives restart and the cap still holds.
  • TestCapStateLegacyMigration — legacy file migrated, then tamper-evident.

Validation

GOWORK=off go build/vet ./... clean; go test -race ./... green.

The cap-state spend log was plain JSONL with no integrity, and load
silently skipped malformed lines. The same OS user could therefore
erase or rewrite spend history to bypass caps, and a garbage line that
overwrote a real spend went unnoticed.

Add a per-record chained HMAC-SHA256 (UseCapStateFileWithHMAC), keyed
off the wallet identity via HKDF (info=pilot-cap-state-v1). Load now
fails closed: a malformed line, an HMAC mismatch, or a signed-chain
mixed with an unauthenticated record refuses to load rather than
under-counting. A wholly-legacy file is migrated once to an
authenticated chain; the legacy unauthenticated format stays readable
via UseCapStateFile and still fails closed on malformed lines.

Tests: tampered record detected, malformed line not silently dropped,
HMAC round-trip survives restart, legacy migration then tamper-evident.
@codecov

codecov Bot commented Jun 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 53.50877% with 53 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pkg/wallet/spendcap.go 63.54% 23 Missing and 12 partials ⚠️
pkg/wallet/signer.go 0.00% 12 Missing ⚠️
cmd/wallet/main.go 0.00% 6 Missing ⚠️

📢 Thoughts on this report? Let us know!

@TeoSlayer TeoSlayer merged commit 6a32813 into main Jun 22, 2026
2 of 3 checks passed
@matthew-pilot matthew-pilot deleted the security/spendcap-integrity branch June 22, 2026 15:48
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.

2 participants