Files
kite/.planning/phases/07.3-security-es256-algorithm-upgrade-inserted/07.3-02-SUMMARY.md
T
curo1305 e7e3f527a5 docs(07.3-02): complete plan 02 SUMMARY — ES256 core upgrade + startup rotation
6 tests promoted from xfail to passing (ES256-01..05 + CFG-01); 3 remain xfail for Plan 03
2026-06-06 17:05:31 +02:00

9.0 KiB

phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics
phase plan subsystem tags dependency_graph tech_stack key_files decisions metrics
07.3-security-es256-algorithm-upgrade-inserted 02 backend/auth
security
jwt
es256
algorithm-upgrade
startup-hook
requires provides affects
07.3-01
ES256 JWT signing at all 4 token sites
Startup token rotation hook (idempotent)
Operator key generation documentation
backend/config.py
backend/services/auth.py
backend/main.py
docker-compose.yml
README.md
.env.example
frontend/package.json
added patterns
base64.b64decode(settings.jwt_*_key).decode() inline at each JWT call site
Idempotent startup SystemSettings upsert pattern (matching ai_config.py seed)
Raw SQL bulk UPDATE inside helper with session.commit() — no Python iteration
created modified
path lines note
backend/config.py 35-38 Added refresh_token_expire_hours=16, jwt_private_key='', jwt_public_key=''
path lines note
backend/services/auth.py 20,100-101,109-110,133-134,142-143 Added import base64; 4 JWT sites swapped from HS256+secret_key to ES256+inline PEM decode
path lines note
backend/main.py 1-3,133-175,225-232,295 Added import logging + select; _rotate_tokens_on_algorithm_change helper; lifespan call with try/except; version 0.1.1 -> 0.1.2
path lines note
backend/tests/test_auth_es256.py 39-95,109-239,244-249 Promoted 6 tests from xfail to passing (ES256-01..05 + CFG-01)
path lines note
docker-compose.yml 66-67,105-106 Added JWT_PRIVATE_KEY + JWT_PUBLIC_KEY env injection to backend and celery-worker
path lines note
README.md 143-144,162-181 Added JWT key vars to env table; added JWT Key Generation section with Python one-liner
path lines note
.env.example 33-37 Added JWT_PRIVATE_KEY= and JWT_PUBLIC_KEY= with section header
path lines note
frontend/package.json 3 Version bump 0.1.1 -> 0.1.2
Inline PEM decode at each call site (base64.b64decode inline) — no module-level PEM cache per RESEARCH.md Anti-Pattern; prevents leaked PEM via reload
expire_all() in tests after _rotate_tokens_on_algorithm_change — conftest uses expire_on_commit=False; raw SQL UPDATE bypasses ORM identity map so session must be expired manually before re-query
is_active=False on jwt_algorithm SystemSettings row — prevents AI provider loader from returning the metadata marker row
duration completed tasks_completed files_modified
~25 minutes 2026-06-06 3 8

Phase 07.3 Plan 02: ES256 Core Upgrade — JWT Sites + Startup Rotation + Operator Wiring Summary

ES256 algorithm upgrade: all 4 JWT signing/decoding sites use ECDSA P-256 with inline base64-decoded PEM keys, with idempotent startup bulk-revocation hook and full operator documentation.


Tasks Completed

Task Name Commit Files
1 Add JWT key + TTL settings; swap all 4 JWT sites to ES256; remove secret_key from JWT code fd3f611 backend/config.py, backend/services/auth.py, backend/tests/test_auth_es256.py
2 Add lifespan _rotate_tokens_on_algorithm_change hook + promote startup rotation tests 8d261b0 backend/main.py, backend/tests/test_auth_es256.py
3 Wire JWT key env vars through docker-compose, README key-gen snippet, .env.example, version bump 0d1ab05 docker-compose.yml, README.md, .env.example, backend/main.py, frontend/package.json

Grep Gate Results (Acceptance Criteria Verified)

Gate Expected Actual Status
settings.secret_key in services/auth.py 0 0 PASS
algorithm="ES256" in services/auth.py 2 2 PASS
algorithms=["ES256"] in services/auth.py 2 2 PASS
"HS256" in services/auth.py 0 0 PASS
base64.b64decode(settings.jwt_ in services/auth.py 4 4 PASS
import base64 in services/auth.py 1 1 PASS
refresh_token_expire_hours: int = 16 in config.py 1 1 PASS
jwt_private_key: str = "" in config.py 1 1 PASS
jwt_public_key: str = "" in config.py 1 1 PASS
async def _rotate_tokens_on_algorithm_change in main.py 1 1 PASS
await _rotate_tokens_on_algorithm_change(session) in main.py 1 1 PASS
UPDATE refresh_tokens SET revoked = true WHERE revoked = false in main.py 1 1 PASS
provider_id == "jwt_algorithm" in main.py 1 1 PASS
is_active=False in main.py >= 1 2 PASS
ES256 rotation check skipped in main.py 1 1 PASS
JWT_PRIVATE_KEY=${JWT_PRIVATE_KEY} in docker-compose.yml 2 2 PASS
JWT_PUBLIC_KEY=${JWT_PUBLIC_KEY} in docker-compose.yml 2 2 PASS
Literal base64 PEM in docker-compose.yml 0 0 PASS
ec.generate_private_key(ec.SECP256R1()) in README.md >= 1 1 PASS
Rotating these keys invalidates every active session in README.md 1 1 PASS
JWT_PRIVATE_KEY= in .env.example (no value) present present PASS
JWT_PUBLIC_KEY= in .env.example (no value) present present PASS
version="0.1.2" in backend/main.py 1 1 PASS
"version": "0.1.2" in frontend/package.json 1 1 PASS
docker compose config -q exits 0 0 0 PASS

Promoted Tests (xfail → PASSED)

Test Requirement Before After
test_access_token_uses_es256 ES256-01 XFAIL (strict) PASSED
test_hs256_token_rejected ES256-02 XFAIL (strict) PASSED
test_reset_token_uses_es256 ES256-03 XFAIL (strict) PASSED
test_startup_rotation_revokes_tokens ES256-04 XFAIL (strict) PASSED
test_startup_rotation_idempotent ES256-05 XFAIL (strict) PASSED
test_settings_has_jwt_keys CFG-01 XFAIL (strict) PASSED

Net suite delta: +6 PASSED. Final count: 6 PASSED + 3 XFAILED (RM-01..03 remain for Plan 03). test_task1_models_config.py::test_settings_has_jwt_config also passes (previously failing due to missing fields).


Deviations from Plan

Auto-fixed Issues

1. [Rule 1 - Bug] SQLAlchemy identity map returns stale values after raw SQL UPDATE with expire_on_commit=False

  • Found during: Task 2 test execution
  • Issue: conftest.py creates the test AsyncTestSession with expire_on_commit=False. After _rotate_tokens_on_algorithm_change runs a raw SQL UPDATE refresh_tokens SET revoked = true WHERE revoked = false followed by session.commit(), SQLAlchemy's identity map still holds the pre-commit stale state of RefreshToken objects (revoked=False). Re-querying via select() returned the stale cached object instead of issuing a fresh DB query.
  • Fix: Added db_session.expire_all() in both startup rotation tests immediately after await _rotate_tokens_on_algorithm_change(db_session). This invalidates the identity map cache, forcing SQLAlchemy to re-fetch from DB on the subsequent select(). Production code is unchanged — the issue is test isolation specific to expire_on_commit=False.
  • Files modified: backend/tests/test_auth_es256.py
  • Commit: 8d261b0

Remaining XFAIL Tests (Plan 03)

Test Requirement Reason
test_default_ttl_16_hours RM-01 remember_me param not yet added to create_refresh_token
test_remember_me_ttl_30_days RM-02 same
test_remember_me_cookie_max_age RM-03 _set_refresh_cookie + LoginView.vue changes deferred to Plan 03

Operator Handoff

Operators must generate a P-256 key pair before the backend can sign or verify JWTs:

python3 -c "
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import serialization
import base64
k = ec.generate_private_key(ec.SECP256R1())
priv = base64.b64encode(k.private_bytes(serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption())).decode()
pub = base64.b64encode(k.public_key().public_bytes(serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo)).decode()
print(f'JWT_PRIVATE_KEY={priv}')
print(f'JWT_PUBLIC_KEY={pub}')
"

Paste the two output lines into .env. On next docker compose up, the startup rotation hook will bulk-revoke all existing refresh tokens (forcing all users to re-login) and record the ES256 migration marker in system_settings. Subsequent boots are idempotent.


Self-Check: PASSED

  • backend/config.py exists and contains the three new fields: FOUND
  • backend/services/auth.py uses ES256 at all 4 sites, zero HS256/secret_key references: FOUND
  • backend/main.py contains _rotate_tokens_on_algorithm_change at module scope: FOUND
  • docker-compose.yml has JWT_PRIVATE_KEY and JWT_PUBLIC_KEY in both services: FOUND
  • README.md contains key generation snippet: FOUND
  • .env.example contains JWT_PRIVATE_KEY= and JWT_PUBLIC_KEY=: FOUND
  • Commits fd3f611, 8d261b0, 0d1ab05 all exist: FOUND
  • Test suite: 6 PASSED + 3 XFAILED, 0 FAILED: CONFIRMED