Token hardening: immediate revocation, boot-bound monotonic lifetime - #159
Open
bentumbler wants to merge 1 commit into
Open
Token hardening: immediate revocation, boot-bound monotonic lifetime#159bentumbler wants to merge 1 commit into
bentumbler wants to merge 1 commit into
Conversation
Two fixes to the token layer (P2 of the MCP auth plan): Revocation now takes effect immediately. verify_token's cache fast-path returned success without checking the revoked flag, and revoke_token never evicted the cache entry, so a revoked token kept validating until an unrelated cache clear or service restart. revoke_token now evicts the token from the in-process cache (sufficient while gunicorn runs a single worker). Token lifetime is now enforced without trusting the wall clock. The device has no RTC, so exp validation was disabled (ba79239) and expiry only bit via the hourly DB purge. Tokens now carry bid (kernel boot id), upt (uptime at issuance), and ttl claims; within the issuing boot, age is measured against CLOCK_BOOTTIME, so setting the clock can never extend or resurrect a token. exp/iat remain as advisory display values. Across reboots, TOKEN_LIFETIME_MODE (config) selects the policy: - boot_bound (default): tokens die with their boot; a startup sweep removes previous-boot rows as cleanup. - wall_clock_grace: previous-boot tokens stay valid until wall-clock expiry; while the clock still lags the token's issuance time the token is rejected retryably ("Clock not yet synchronized"), never purged. Trusting the clock across reboots is sound because setting time requires the same SSH/sudo privilege that can mint tokens directly. Note: in boot_bound mode, upgrading invalidates already-issued tokens once (they predate the bid claim). wall_clock_grace accepts them until wall-clock expiry. Co-Authored-By: Claude Fable 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.
Implements P2 of the MCP auth & transport plan (design/discussion doc, sections 1.2 defects 2–3 and 4.1 P2; the boot-bound/monotonic design refines the doc's original “enable exp validation” wording after the clock-skew history in ba79239 surfaced): make revocation and expiry actually work, without trusting a wall clock the device (no RTC) cannot guarantee.
What changed
Revocation takes effect immediately.
verify_token's cache fast-path returned success without checking the revoked flag, andrevoke_tokennever evicted the cache entry — a revoked token kept validating until an unrelated cache clear or service restart.revoke_tokennow evicts the token from the in-process cache. (Sufficient while gunicorn runs--workers 1; noted in-code that multiple workers would need a shared invalidation channel.)Boot-bound monotonic token lifetime.
expvalidation was disabled deliberately (ba79239, clock-skew on RTC-less devices) leaving expiry to the hourly DB purge. Tokens now carrybid(kernel boot id),upt(uptime at issuance), andttlclaims. Within the issuing boot, age is measured againstCLOCK_BOOTTIME, so setting the wall clock can never extend or resurrect a token — in either mode.exp/iatremain in the token as advisory/display values.TOKEN_LIFETIME_MODE(config constant) selects cross-reboot policy:boot_bound(default): tokens die with the boot they were issued in; a startup sweep deletes previous-boot rows (cleanup only — they can no longer verify regardless). Zero wall-clock reliance.wall_clock_grace: previous-boot tokens stay valid until wall-clock expiry. While the clock still lags the token's issuance time (early boot, before NTP/fake-hwclock catch-up), verification returns a retryableClock not yet synchronizedrejection and nothing is purged; once the clock catches up the token verifies again.Trusting the clock across reboots in grace mode is sound because setting time requires SSH/sudo — the same privilege that can run
getjwtor read the HMAC secret, so clock manipulation gains an attacker nothing they don't already have. External API clients cannot set time.Reviewer notes
boot_boundmeans the WLAN Pi app's 7-day tokens die on every reboot (re-pair via SSHgetjwt).wall_clock_gracepreserves today's app UX exactly and accepts already-issued tokens (they predatebidand take the wall-clock path). Flipping the default is one line inconfig.py.boot_boundmode, upgrading invalidates all previously issued tokens once (nobidclaim → previous-boot path).Tests
tests/test_token_revocation.py— 12 tests against the realTokenManager+ temp SQLite: immediate revocation (cache-hit and cold path; the cache-hit test fails on the unfixed code), previous-boot rejection, monotonic expiry on both verify paths, expire→reboot→clock-reset stays dead, boot sweep spares same-boot tokens and service restarts, and grace mode (accept within wall expiry, reject after, retryable clock-lag rejection, same-boot monotonic immunity to a 30-day clock jump).Ran locally under the box's py3.9 venv (34 passed across the auth suites); the tree is py313-only so CI is the authoritative gate.
🤖 Generated with Claude Code