_verify_assertion_token now takes expected_host and rejects any token
whose bundle["host"] does not match — closing the cross-server replay
path where a token issued for server-a could have passed on server-b.
ServerState gains protected_host (default 127.0.0.1); k_server exposes
--protected-host CLI flag so operators declare which host they protect.
New abuse tests (unit + round-trip):
test_cross_server_replay_rejected
test_cross_server_replay_case_insensitive
test_roundtrip_cross_server_replay_rejected
test_roundtrip_cross_server_replay_accepted_on_correct_server
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Challenge is now SHA256(host|nonce) instead of SHA256(url|method|nonce).
A single card interaction authorises access to any path and method on the
gated domain, which is the intended granularity. Tests updated accordingly:
path/method rejection cases replaced with domain-level and tampered-host cases.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
tests/test_k_server.py:
- TestVerifyAssertionToken (12 tests): unit tests using raw P-256 keys —
valid accept, wrong path/method, tampered nonce/signature/key, cross-
resource replay, malformed/empty token, wrong cdj type, missing field.
- TestVerifyAssertionTokenRoundTrip (5 tests): end-to-end via CardEmulator
— register, getAssertion with bound challenge, build bundle as k_phone
does, verify on server. Tests include wrong path/method and cross-user
key swap. Skipped automatically if fido2 is not installed.
All 17 pass.
proxy_service.dart: add comment to _handleSessionLogin explaining why
random challenge is correct there (user-presence proof for portal session,
not per-request resource binding).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>