cowork: fail-closed expiry verification + rotation type guards - #24
Open
Coding-Dev-Tools wants to merge 4 commits into
Open
cowork: fail-closed expiry verification + rotation type guards#24Coding-Dev-Tools wants to merge 4 commits into
Coding-Dev-Tools wants to merge 4 commits into
Conversation
…jwt_token A present-but-unparseable expires_at (naive timestamp, malformed string, or non-string value) previously hit an 'except (ValueError, TypeError): pass' and fell through to status='valid' -- a silent fail-open that let a corrupted, naive, or tampered expiry bypass expiry entirely in a security library. - Add _parse_expiry(): returns an aware UTC datetime, normalizing naive values to UTC so comparisons never raise; returns None only when truly unparseable. - verify_api_key/verify_jwt_token now fail CLOSED: a set-but-unparseable expires_at yields status='invalid' instead of 'valid'. - Harden the key-hash match with hmac.compare_digest (constant-time) and skip entries whose stored hash is missing/non-string. - +7 regression tests (tests/test_verify_expiry_failclosed.py) covering naive past/future, malformed, and non-string expiries for keys and JWTs. check_expiry (display helper) behavior is unchanged (tests lock in None-on- unparseable). 76 tests pass; changed files ruff-clean.
…oss-type keystore corruption Calling the API-key rotation path on a JWT entry (or vice versa) silently corrupted the entry: key_hash/prefix overwritten, signing_secret_hash left stale, version bumped — a half-migrated entry neither verifier trusts. Both rotators now raise ValueError on a mismatched entry type, leaving the entry untouched; correct-type rotation behavior is unchanged. Adds tests/test_rotate_type_guard.py (3 tests). Full suite: 79 passed, ruff clean.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
🤖 Automated Code Review
|
- _save() now writes to a temp file in the key dir, fsyncs, and os.replace()s into keys.json. Previously an in-place torn write would brick the whole keystore (decrypt-failure -> RuntimeError on every later load). - _get_or_create_master_key() validates a 32-byte master.key; a truncated or corrupt key now fails loudly instead of raising an opaque crypto error or being silently regenerated (which would make all entries undecryptable). - tests/test_keystore_atomic.py: 3 tests covering reloadability, no temp-file leftovers, torn-store refusal to overwrite, corrupt-master-key guard.
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.
Two security/correctness fixes for the verification and rotation paths:
Fail-closed expiry verification — verify_api_key()/verify_jwt_token() compared parsed expires_at inside an except-and-pass block, so naive/malformed/non-string timestamps silently reported an expired or tampered key as VALID. A shared _parse_expiry() normalizes naive timestamps to UTC; verifiers now return status='invalid' for a present-but-unparseable expires_at. Key-hash matching hardened with hmac.compare_digest.
Rotation type guards — rotate_key() on a JWT entry (or rotate_jwt() on an API-key entry) silently corrupted the keystore entry (stale signing_secret_hash, bogus key_hash, bumped version). Both now raise ValueError on mismatched type and leave the entry untouched.
Adds tests/test_verify_expiry_failclosed.py (7) and tests/test_rotate_type_guard.py (3). Full suite: 79 passed, ruff clean.