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.
40 lines
1.1 KiB
Go
40 lines
1.1 KiB
Go
package controller
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/heicode/manager/service"
|
|
)
|
|
|
|
// GetServerPubkey serves the Manager's long-term X25519 public key.
|
|
// Public endpoint — no auth — so freshly installed clients can fetch
|
|
// it before they have any credentials. The matching private key never
|
|
// leaves the Manager process; see service/server_keys.go.
|
|
//
|
|
// Client side (cc-haha/src/services/device/serverPubkey.ts) caches
|
|
// the response for 24h. Rotation is handled by adding a second row
|
|
// with role="ecdh_rotation_candidate" and switching the long_term
|
|
// pointer; that change is out of scope for P0.
|
|
func GetServerPubkey(c *gin.Context) {
|
|
pub, err := service.GetServerECDHPublicKeyB64()
|
|
if err != nil {
|
|
// EnsureServerECDHKey must run at startup; if we get here
|
|
// something is very wrong with the boot sequence.
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"success": false,
|
|
"message": "server ECDH key not initialized",
|
|
})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"data": gin.H{
|
|
"algorithm": "x25519",
|
|
"pubkey_b64": pub,
|
|
"version": 1,
|
|
},
|
|
})
|
|
}
|