---
phase: "07.4"
slug: security-token-fingerprinting-token-binding-inserted
status: verified
threats_open: 0
asvs_level: 1
created: 2026-06-06
---

# Phase 07.4 — Security

> Per-phase security contract: threat register, accepted risks, and audit trail.

---

## Trust Boundaries

| Boundary | Description | Data Crossing |
|----------|-------------|---------------|
| client → API (login/refresh) | Client `User-Agent` and `Accept-Language` headers are untrusted; HMAC treats them as opaque bytes — no injection vector | Header strings → HMAC-SHA256 digest (hex) |
| JWT payload → get_current_user | `fgp` claim extracted from decoded (ES256 signature-verified) JWT; attacker cannot forge without ES256 private key | fgp hex string (16 chars) |
| fgp comparison | Fixed-length (16-char hex) strings compared with `hmac.compare_digest` | Constant-time boolean |
| test harness → test DB | xfail stubs (Plan 01) create no DB state; no trust boundary crossed | None |

---

## Threat Register

| Threat ID | Category | Component | Disposition | Mitigation | Status |
|-----------|----------|-----------|-------------|------------|--------|
| T-07.4-01 | Spoofing | Access token replay from different device | mitigate | `fgp` mismatch → HTTP 401 in `deps/auth.py:98-101`; attacker must match UA + Accept-Language from original client context | closed |
| T-07.4-02 | Information Disclosure | Timing attack on fingerprint comparison | mitigate | `hmac.compare_digest` (constant-time) used at `deps/auth.py:98`; satisfies SEC-06 requirement | closed |
| T-07.4-03 | Spoofing | Empty-string collision (CLI clients share one fingerprint) | accept | D-02 deliberate design decision — CLI tools with no UA headers bind to `fgp("","")` and are internally consistent; equivalent to pre-7.4 security level for those clients | closed |
| T-07.4-04 | Tampering | fgp HTTPException swallowed by broad `except` | mitigate | fgp validation block placed **outside** try/except in `deps/auth.py:88-104` (pure computation, no I/O); HTTP 401 always propagates | closed |
| T-07.4-SC | Tampering | Supply-chain via npm/pip/cargo installs | accept | No new third-party packages installed in either plan; only stdlib `hmac` and `hashlib` used | closed |

*Status: open · closed*
*Disposition: mitigate (implementation required) · accept (documented risk) · transfer (third-party)*

---

## Accepted Risks Log

| Risk ID | Threat Ref | Rationale | Accepted By | Date |
|---------|------------|-----------|-------------|------|
| AR-07.4-01 | T-07.4-03 | CLI clients (no User-Agent or Accept-Language) all share `fgp("")` — they are internally consistent but one CLI session token cannot distinguish another CLI session. Design decision D-02 documents this as acceptable: CLI use is already authenticated via the same token family; the fingerprint adds value for browser clients where header variance is meaningful. | project owner (gsd workflow) | 2026-06-06 |
| AR-07.4-02 | T-07.4-SC | Plans 01 and 02 introduce zero new dependencies; stdlib `hmac`/`hashlib` only. Supply-chain risk is identical to baseline. | project owner (gsd workflow) | 2026-06-06 |

---

## Implementation Evidence

| File | Evidence |
|------|----------|
| `backend/services/auth.py:87-88` | `_compute_fgp(user_agent, accept_lang)` — HMAC-SHA256 producing 16-char hex digest |
| `backend/services/auth.py:115` | `"fgp": _compute_fgp(user_agent, accept_lang)` embedded in access token payload |
| `backend/api/auth.py:293, 372` | Login and refresh both pass `user_agent=request.headers.get("User-Agent", "")` |
| `backend/deps/auth.py:88-104` | fgp validation block — **outside** try/except, `hmac.compare_digest` comparison, `detail="Token fingerprint mismatch"` on mismatch |
| `backend/tests/test_auth_fgp.py` | 4 FGP tests (FGP-01..04) — all passing (promoted from xfail stubs in Plan 01) |

---

## Security Audit Trail

| Audit Date | Threats Total | Closed | Open | Run By |
|------------|---------------|--------|------|--------|
| 2026-06-06 | 5 | 5 | 0 | gsd-secure-phase (automated — short-circuit: register_authored_at_plan_time=true, threats_open=0) |

---

## Sign-Off

- [x] All threats have a disposition (mitigate / accept / transfer)
- [x] Accepted risks documented in Accepted Risks Log
- [x] `threats_open: 0` confirmed
- [x] `status: verified` set in frontmatter

**Approval:** verified 2026-06-06
