Eliminate sk- bearer from the client wire entirely. V2 requests
authenticate via Ed25519 device signature (over a canonical that
binds method/path/timestamp/nonce/fingerprint/eph-pubkey/plaintext-
body-hash) and encrypt the request body with X25519 ECDH +
ChaCha20-Poly1305-AEAD. Server-issued sk- tokens still exist for
legacy callers during a 30-day deadline window; after the deadline
bare-bearer sk- on /v1/* is rejected.
What's new server-side:
- model/server_key.go + service/server_keys.go: long-lived X25519
keypair persisted in DB. Private half is AES-256-GCM-sealed with a
key derived from CRYPTO_SECRET so a SQL dump alone doesn't leak it.
Generated on first launch by main.go::EnsureServerECDHKey.
- common/crypto.go: SealWithCryptoSecret / UnsealWithCryptoSecret
helpers (AES-GCM); SafeWipe defense-in-depth zero-out.
- controller/server_pubkey.go + GET /api/server-pubkey: public
endpoint clients fetch at startup to obtain the ECDH pubkey.
- middleware/body_decrypt.go: ChaCha20-Poly1305 decrypt of V2 bodies.
AD binds device_id/timestamp/nonce/method/path so tampering any
fails AEAD verify. Replaces c.Request.Body with plaintext for
downstream relay handlers to consume unchanged.
- middleware/device_signature.go: new VerifyV2DeviceSignedRequest()
looks up token by device_id (not bearer) and verifies an extended
canonical that includes the ephemeral pubkey + plaintext body hash.
- middleware/auth.go::TokenAuth: dispatch on Content-Encoding header.
V2 path skips ValidateUserToken entirely. Legacy path adds a 30-day
/v1/* deadline knob.
- model/token.go::FindTokenByDeviceId: V2 lookup helper.
- controller/device.go::PairDevice: stops returning the sk in
responses. Client identifies itself by device_id + signature from
now on, no bearer needed.
- setting/operation_setting/device_binding_setting.go: new
LegacySkV1DeadlineMs knob (0 = disabled until operator sets it).
Backward compatibility: V1 device-signed tokens (those issued by
the earlier PairDevice that DID return a sk-) keep working through
the legacy bearer path; the existing V1 signature middleware still
runs for them. The 30-day deadline is opt-in until ops sets it.
Tests: V1 regression suite passes (middleware + common).
V2-specific tests come in a follow-up commit alongside the client
encryptedFetch wiring; deferring lets us land the server-side
plumbing first without coupling.