# Phase 7.3: Security — ES256 Algorithm Upgrade - Discussion Log

> **Audit trail only.** Do not use as input to planning, research, or execution agents.
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.

**Date:** 2026-06-05
**Phase:** 07.3-security-es256-algorithm-upgrade-inserted
**Areas discussed:** Key format in env vars, Startup refresh-token rotation, Key generation workflow, JWT TTL / Remember Me

---

## Key Format in Env Vars

### Q1: How should P-256 PEM keys be stored in env vars?

| Option | Description | Selected |
|--------|-------------|----------|
| Base64-encoded | Single line, shell-safe, decoded in config.py | ✓ |
| Escaped `\n` in .env | python-dotenv expands `\n`; loader-dependent | |
| Multiline .env block | Breaks in docker-compose environment: block | |

**User's choice:** Base64-encoded  
**Notes:** Standard 12-factor approach; no loader ambiguity.

---

### Q2: Both keys required, or derive public from private at startup?

| Option | Description | Selected |
|--------|-------------|----------|
| Both required env vars | Clean separation; workers can hold public key only | ✓ |
| Private key only — derive public | Fewer env vars but more magic in config.py | |

**User's choice:** Both required env vars

---

### Q3: What happens to SECRET_KEY after ES256?

| Option | Description | Selected |
|--------|-------------|----------|
| Remove from JWT code | SECRET_KEY no longer used for signing; stays in env for future HMAC | ✓ |
| Repurpose as HMAC key for Phase 7.4 | Forward-compatible but couples phases | |

**User's choice:** Remove SECRET_KEY from JWT code

---

## Startup Refresh-Token Rotation

### Q1: How to implement startup rotation?

| Option | Description | Selected |
|--------|-------------|----------|
| DELETE all refresh_tokens | Simple, one-shot; no schema change | |
| Bulk revoke (set revoked=True) | Cleaner audit trail; same user impact | ✓ |
| Let natural expiry handle it | Doesn't satisfy ROADMAP requirement | |

**User's choice:** Bulk revoke (set revoked=True)

---

### Q2: How to detect "first boot after ES256 migration"?

| Option | Description | Selected |
|--------|-------------|----------|
| Check system_settings table | Stores `jwt_algorithm`; idempotent on restart | ✓ |
| New env var JWT_TOKEN_GENERATION=N | Simple but requires operator bump on deploy | |
| One-time Alembic migration | Runs at migrate time, not startup | |

**User's choice:** Check system_settings table (`jwt_algorithm` key)

---

## Key Generation Workflow

### Q1: How should developers generate the key pair?

| Option | Description | Selected |
|--------|-------------|----------|
| Python one-liner in README | Uses existing `cryptography` dep; no extra tooling | ✓ |
| openssl CLI command | Familiar to ops but multi-step, requires openssl | |
| Auto-generate on first boot | Convenient for dev; dangerous in prod (ephemeral keys) | |

**User's choice:** Python one-liner documented in README

---

### Q2: docker-compose key placeholder vs. .env reference?

| Option | Description | Selected |
|--------|-------------|----------|
| .env file reference only | Follows existing ${SECRET_KEY} pattern | ✓ |
| Placeholder comment in docker-compose | Helpful but could mislead | |

**User's choice:** .env file reference only

---

## JWT TTL / Remember Me

### Q1 (freeform): What token should default to 12 hours?

User note: "JWT Tokens should only be valid for 12 hours and the user can opt-in to create a token which is valid for 30 days."

Asked for clarification on whether this applied to the access token or refresh token. User asked: "What do you think is the most secure way to handle tokens?"

**Claude's recommendation:** Keep access token at 15 min (CLAUDE.md requirement). Change refresh token default to 16–24 hours with explicit "remember me" opt-in for 30 days.

**User's decision:** 16 hours — covers a full workday; guarantees session is revoked overnight.

---

### Q2: Fold "remember me" into Phase 7.3 or separate phase?

| Option | Description | Selected |
|--------|-------------|----------|
| Fold into Phase 7.3 | Already touching token creation code; incremental work | ✓ |
| Separate phase 7.5 | Keeps 7.3 purely as ES256 upgrade | |

**User's choice:** Fold into Phase 7.3

---

## Claude's Discretion

- PyJWT API form: `jwt.encode(payload, pem_str, algorithm="ES256")` — PEM strings passed directly, no key object conversion needed
- Bulk revoke via raw SQL `UPDATE` rather than ORM row-by-row — efficiency decision
- "Stay signed in for 30 days" as checkbox label (clearer than "Remember me")
- system_settings key name: `jwt_algorithm`, value `"ES256"`

## Deferred Ideas

- **Phase 7.4:** Token fingerprinting / token binding (`fgp` claim = HMAC of User-Agent + Accept-Language)
- **Key rotation ceremony:** Dual-key overlap process for rotating P-256 keys in production — deferred until production hardening milestone
