Lays down the server side of a per-request Ed25519 signature scheme that
binds a token to a specific desktop install, so the bearer key can't be
extracted from ~/.claude/cc-haha/providers.json and resold. Plan lives
at ~/.claude/plans/peaceful-sprouting-crane.md.
Compatibility: legacy bare-bearer sk- callers (CLI/SDK) pass through
unchanged until P3 (30-day deadline) flips RequireGlobal=true. No
existing token rows are modified — pubkey is nullable and defaults to
null.
Pieces:
- model.Token gains DeviceId, DevicePubkey, DeviceFingerprint, DeviceName,
DevicePlatform, DeviceAppVersion, DeviceBoundAt, DeviceLastSeenIp,
DeviceLastUsedAt, RequireDeviceBinding, RevokedAt, RevokedReason.
Pure additive columns, GORM AutoMigrate handles SQLite/MySQL/PG.
- common.VerifyEd25519Signature: thin wrapper around crypto/ed25519
stdlib, used by the new middleware. No new external deps.
- service.MarkNonceUsed: Redis SETNX-based nonce store with an
in-memory sync.Map fallback for single-instance dev. TTL = setting.
- middleware.VerifyDeviceSignatureIfRequired: wired into TokenAuth as
a fail-fast dispatch right after model.ValidateUserToken. Verifies
canonical = METHOD\nPATH\nTS_MS\nNONCE\nFINGERPRINT\nSHA256(BODY),
signed as Ed25519(sha256(canonical)). 120s timestamp window, 300s
nonce window, fingerprint stored at pair time must match the header.
- controller.PairDevice / ListUserDevices / RenameUserDevice /
RevokeUserDevice, mounted at /api/devices/* behind UserAuth().
PairDevice enforces 5-per-user cap and returns the raw sk- once,
to be stored in the client's OS keychain (not providers.json).
- operation_setting.DeviceBindingSetting: MaxDevicesPerUser=5,
TimestampWindowMs=120000, NonceTTLSec=300, RequireGlobal=false.
Tests:
- common/crypto_test.go covers round-trip + tamper + malformed inputs.
- middleware/device_signature_test.go covers all error-path branches
(expired ts, wrong sig, tampered body, fingerprint mismatch, replay,
revoked, missing headers, legacy fallthrough).
- testdata/device_signature_vectors.json is the cross-language contract
Rust+TS sides will load to assert byte-identical canonical strings.
Untouched but reserved for follow-up phases:
- Anomaly detection / IP-diversity flagging (P1)
- 30-day deprecation banner + email notifications (P2)
- Hard cutover RequireGlobal=true (P3, day 31)
- Add hide_from_user_ui on tokens; EnsureUserRelayToken on login and Agnet session
- List/search tokens: end-users see only visible keys; admins see all
- Add /available-models and sidebar entry; i18n en/zh + locales
- desktop download / router hooks if present under heicode/