Files
kite/.planning/phases/07.3-security-es256-algorithm-upgrade-inserted/07.3-01-PLAN.md
T
curo1305andClaude Sonnet 4.6 b7994efd06 docs(07.3): create phase plan — ES256 algorithm upgrade
3 plans (3 waves): Wave 0 xfail stubs, Wave 1 ES256 core + startup
rotation, Wave 2 remember-me TTL split + LoginView checkbox.
Verification passed (0 blockers, 1 minor doc warning fixed).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 19:07:25 +02:00

15 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
07.3-security-es256-algorithm-upgrade-inserted 01 execute 0
backend/tests/test_auth_es256.py
backend/tests/test_task1_models_config.py
true
ES256-01
ES256-02
ES256-03
ES256-04
ES256-05
RM-01
RM-02
RM-03
CFG-01
truths artifacts key_links
Every Phase 7.3 verifiable behavior has a failing pytest stub before any production code change
Existing test_settings_has_jwt_config covers the new refresh_token_expire_hours setting
Tests are red (xfail) when run, not silently skipped
path provides contains
backend/tests/test_auth_es256.py 9 xfail stubs covering ES256-01..05 + RM-01..03 test_access_token_uses_es256, test_hs256_token_rejected, test_reset_token_uses_es256, test_startup_rotation_revokes_tokens, test_startup_rotation_idempotent, test_default_ttl_16_hours, test_remember_me_ttl_30_days, test_remember_me_cookie_max_age, es256_keys fixture
path provides contains
backend/tests/test_task1_models_config.py Asserts refresh_token_expire_hours == 16 and jwt key fields present (will xfail until Plan 02) refresh_token_expire_hours
from to via pattern
backend/tests/test_auth_es256.py backend/services/auth.py + backend/main.py + backend/api/auth.py Imports create_access_token / create_password_reset_token / FastAPI test client from services.auth import create_access_token
Create the Wave 0 Nyquist test scaffold for Phase 7.3 (ES256 + startup rotation + remember_me). Every requirement ID listed in this phase MUST have a failing-on-purpose test stub before any production code changes. This is the test infrastructure that subsequent plans promote from xfail to passing.

Purpose: Enforce TDD discipline; guarantee every locked decision (D-01..D-12) has an automated check. Output: One new test file (test_auth_es256.py) with 9 xfail stubs + a reusable ES256 key fixture, plus an extension to the existing config test asserting refresh_token_expire_hours == 16.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/STATE.md @.planning/ROADMAP.md @.planning/phases/07.3-security-es256-algorithm-upgrade-inserted/07.3-CONTEXT.md @.planning/phases/07.3-security-es256-algorithm-upgrade-inserted/07.3-RESEARCH.md @.planning/phases/07.3-security-es256-algorithm-upgrade-inserted/07.3-PATTERNS.md @.planning/phases/07.3-security-es256-algorithm-upgrade-inserted/07.3-VALIDATION.md @backend/tests/test_task1_models_config.py @backend/tests/test_task2_auth_service.py @backend/tests/test_auth_api.py @backend/tests/conftest.py

From backend/tests/conftest.py:

  • pytest_asyncio.fixture async_client — httpx.AsyncClient bound to the FastAPI app (used for integration tests against /api/auth/login)
  • pytest_asyncio.fixture db_session — AsyncSession for direct row inspection (used to assert RefreshToken.expires_at after login)
  • pytest_asyncio.fixture auth_user — returns dict with email, password, id for a real registered user

From backend/tests/test_auth_api.py (lines 47-97 area):

  • class FakeRedis — minimal in-memory stub exposing get(key), set(key, value, ex=...), delete(key) used to bypass real Redis during integration tests
  • helper _register(client, email, password) — registers a user via POST /api/auth/register
  • helper _login(client, email, password, **extra) — POSTs /api/auth/login and returns the response

From backend/services/auth.py:

def create_access_token(user_id: str, role: str) -> str  # currently HS256 — Plan 02 swaps to ES256
def create_password_reset_token(user_id: str) -> str      # currently HS256 — Plan 02 swaps to ES256
def decode_access_token(token: str) -> dict               # currently HS256 — Plan 02 swaps to ES256
async def create_refresh_token(session, user_id) -> str   # Plan 03 adds remember_me param

From backend/config.py (current JWT block, lines 33-35):

access_token_expire_minutes: int = 15
refresh_token_expire_days: int = 30
# Plan 02 adds: refresh_token_expire_hours: int = 16, jwt_private_key: str = "", jwt_public_key: str = ""

From cryptography library (already in requirements.txt):

  • ec.generate_private_key(ec.SECP256R1()) — generate P-256 private key
  • serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.PublicFormat.SubjectPublicKeyInfo — PEM serialization formats
Task 1: Create test_auth_es256.py with 9 xfail stubs + ES256 key fixture backend/tests/test_auth_es256.py backend/tests/test_task2_auth_service.py backend/tests/test_auth_api.py backend/tests/conftest.py .planning/phases/07.3-security-es256-algorithm-upgrade-inserted/07.3-RESEARCH.md .planning/phases/07.3-security-es256-algorithm-upgrade-inserted/07.3-PATTERNS.md .planning/phases/07.3-security-es256-algorithm-upgrade-inserted/07.3-VALIDATION.md Create a new pytest module at backend/tests/test_auth_es256.py. Module docstring: "TDD scaffold for Phase 7.3: ES256 algorithm upgrade, startup token rotation, and remember_me session TTL — all stubs xfail strict=True until promoted." Module imports: pytest, pytest_asyncio, base64, json, uuid, hashlib, time, datetime (timezone, timedelta), and (lazily inside test bodies where applicable) services.auth and config.settings.
Define an `autouse=True` module-level fixture `es256_keys(monkeypatch)` that (a) generates a P-256 private key with `ec.generate_private_key(ec.SECP256R1())` from cryptography.hazmat.primitives.asymmetric, (b) serializes private key as PEM PKCS8 NoEncryption then base64.b64encode().decode(), (c) serializes public key as PEM SubjectPublicKeyInfo then base64.b64encode().decode(), (d) monkeypatches `config.settings.jwt_private_key` and `config.settings.jwt_public_key` to these base64 strings. This fixture exists in Wave 0 even though those settings fields do not exist yet — `monkeypatch.setattr` with `raising=False` so it does not error before Plan 02 lands.

Declare exactly these 9 stubs, each guarded with `@pytest.mark.xfail(strict=True, reason="<REQ-ID>: not yet implemented")`. Function bodies contain ONLY `pytest.xfail("not yet implemented")` (single-line body) — no test logic. The reason strings and function names MUST match the VALIDATION.md per-task table exactly:

1. `test_access_token_uses_es256()` — ES256-01 — sync, no fixtures beyond es256_keys
2. `test_hs256_token_rejected()` — ES256-02 — sync; will craft HS256 token with PyJWT and pass to decode_access_token
3. `test_reset_token_uses_es256()` — ES256-03 — sync
4. `test_startup_rotation_revokes_tokens(db_session)` — ES256-04 — `@pytest.mark.asyncio async def`; uses db_session
5. `test_startup_rotation_idempotent(db_session)` — ES256-05 — `@pytest.mark.asyncio async def`
6. `test_default_ttl_16_hours(async_client, db_session, auth_user)` — RM-01 — `@pytest.mark.asyncio async def`
7. `test_remember_me_ttl_30_days(async_client, db_session, auth_user)` — RM-02 — `@pytest.mark.asyncio async def`
8. `test_remember_me_cookie_max_age(async_client, auth_user)` — RM-03 — `@pytest.mark.asyncio async def`
9. `test_settings_has_jwt_keys()` — CFG-01 satellite for jwt_private_key/jwt_public_key presence in settings — sync

Each stub MUST be reachable by `pytest --collect-only`. Use `strict=True` on xfail so that any premature pass breaks CI and forces explicit promotion. Do NOT import services.auth or main at module top level (it would crash before Plan 02 adds settings fields) — perform imports lazily inside test bodies where needed (already redundant because bodies only call `pytest.xfail`).

Do not write any assertion code. Do not implement the test bodies — Plans 02 and 03 promote each stub by replacing the `xfail` body with real assertions in the same task that lands the corresponding production change.
cd backend && pytest tests/test_auth_es256.py --collect-only -q 2>&1 | grep -E "test_(access_token_uses_es256|hs256_token_rejected|reset_token_uses_es256|startup_rotation_revokes_tokens|startup_rotation_idempotent|default_ttl_16_hours|remember_me_ttl_30_days|remember_me_cookie_max_age|settings_has_jwt_keys)" | wc -l | tr -d ' ' - File backend/tests/test_auth_es256.py exists - `grep -c "@pytest.mark.xfail(strict=True" backend/tests/test_auth_es256.py` returns 9 - `grep -c "def es256_keys" backend/tests/test_auth_es256.py` returns 1 - `grep -c "pytest.xfail" backend/tests/test_auth_es256.py` returns at least 9 (one per stub body) - `pytest tests/test_auth_es256.py --collect-only -q` lists all 9 tests by name - `pytest tests/test_auth_es256.py -v` exits 0 with all 9 tests reported as XFAIL (none XPASS, none ERROR) - File contains zero `assert` statements outside of the xfail body convention (greps `grep -c "^ assert " backend/tests/test_auth_es256.py` returns 0) - Module top-level imports do NOT include `from services.auth import` (lazy imports only inside test bodies to avoid load-time failure before Plan 02) 9 xfail stubs collected, all reported XFAIL on pytest run, fixture defined, no assertions present. Task 2: Extend test_settings_has_jwt_config for refresh_token_expire_hours + JWT key fields backend/tests/test_task1_models_config.py backend/tests/test_task1_models_config.py backend/config.py .planning/phases/07.3-security-es256-algorithm-upgrade-inserted/07.3-CONTEXT.md Append assertions to the existing `test_settings_has_jwt_config` function in backend/tests/test_task1_models_config.py. Do NOT replace existing assertions — keep `assert hasattr(settings, "refresh_token_expire_days")` and `assert settings.refresh_token_expire_days == 30` intact (Pitfall 5 in RESEARCH.md).
Add the following new assertions inside the same test function (after the existing ones), each on its own line:
- `assert hasattr(settings, "refresh_token_expire_hours")` — CFG-01
- `assert settings.refresh_token_expire_hours == 16` — CFG-01 / D-09
- `assert hasattr(settings, "jwt_private_key")` — CFG-01 / D-01
- `assert hasattr(settings, "jwt_public_key")` — CFG-01 / D-01

These assertions will FAIL until Plan 02 adds the three new fields to backend/config.py — that is intentional (Nyquist). Do NOT wrap this test in xfail. The test must hard-fail today and hard-pass after Plan 02. Plan 02 promotes by simply adding the fields; the test then passes automatically without code changes here.

Do not change any other tests in this file. Do not add new imports beyond what is already present (`from config import settings`).
cd backend && grep -c "refresh_token_expire_hours == 16" tests/test_task1_models_config.py - `grep "assert settings.refresh_token_expire_hours == 16" backend/tests/test_task1_models_config.py` returns exactly one match - `grep "assert hasattr(settings, \"jwt_private_key\")" backend/tests/test_task1_models_config.py` returns exactly one match - `grep "assert hasattr(settings, \"jwt_public_key\")" backend/tests/test_task1_models_config.py` returns exactly one match - `grep "assert settings.refresh_token_expire_days == 30" backend/tests/test_task1_models_config.py` still returns one match (existing assertion preserved) - Running `cd backend && pytest tests/test_task1_models_config.py::test_settings_has_jwt_config -x -v` FAILS with AttributeError on `refresh_token_expire_hours` (or on `jwt_private_key`) — this is the expected red-on-purpose state before Plan 02 - No new top-level imports added to the file beyond what already exists Test extended to assert all three new config fields; test fails red until Plan 02 lands the fields.

<threat_model>

Trust Boundaries

Boundary Description
pytest runner → backend modules Test code imports production modules; lazy imports prevent load-time crashes before Plan 02

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-07.3-01-01 Tampering Wave 0 stubs flipping to xpass silently mitigate strict=True on every @pytest.mark.xfail forces CI failure on unexpected pass; promotion is explicit (Plan 02 / Plan 03 must edit the file)
T-07.3-01-02 Spoofing Test fixture leaking real JWT keys into other tests mitigate es256_keys fixture uses freshly-generated P-256 keypair per module via monkeypatch — auto-reverted after each test; never reads real env vars
T-07.3-01-03 Denial of Service Module-level import of services.auth crashing before Plan 02 mitigate All imports of services.auth and config.settings.jwt_* are lazy (inside test bodies); module collects cleanly even before Plan 02
T-07.3-01-SC Tampering npm/pip/cargo installs accept No new packages introduced; PyJWT and cryptography already pinned in requirements.txt (per RESEARCH.md §Package Legitimacy Audit — slopcheck not required)
</threat_model>
- `pytest tests/test_auth_es256.py --collect-only -q` lists exactly 9 tests - `pytest tests/test_auth_es256.py -v` reports 9 XFAILED, 0 XPASSED, 0 ERROR, 0 PASSED - `pytest tests/test_task1_models_config.py::test_settings_has_jwt_config -x` FAILS (intentional red state) with `AttributeError` on a missing settings field - Full suite `pytest -v` shows same total + 9 new XFAILED + 1 new FAILED (existing 373 passed becomes 372 passed + 1 failed + 9 xfailed) — confirmed in Plan 02 verify step

<success_criteria>

  • backend/tests/test_auth_es256.py exists with 9 xfail stubs, all strict=True
  • es256_keys autouse fixture generates a fresh P-256 keypair and monkeypatches settings.jwt_private_key / jwt_public_key (uses raising=False)
  • backend/tests/test_task1_models_config.py::test_settings_has_jwt_config asserts refresh_token_expire_hours == 16 and presence of jwt_private_key / jwt_public_key
  • No production source files modified in this plan
  • pytest collects all stubs cleanly; collection does not require Plan 02 fields to exist </success_criteria>
Create `.planning/phases/07.3-security-es256-algorithm-upgrade-inserted/07.3-01-SUMMARY.md` documenting: - Files added/modified - Test counts before/after (e.g., 373 passed → 372 passed + 1 failed + 9 xfailed) - Confirmation that no production code was touched - Promotion plan: which task in Plan 02 / Plan 03 will flip each xfail to a real assertion