Feat: ConnAuth HMAC verification gated by YAMA_CONN_AUTH_KEY

Verify the HMAC-SHA256 signature and timestamp of device sub-connections
(screen/terminal/file) when the YAMA_CONN_AUTH_KEY environment variable is
set. With the key set, a sub-connection whose signature or clock skew fails
is answered with a non-OK ConnAuth status and dropped; without the key the
legacy always-OK behavior is preserved.

The key is never compiled in: it is read from the environment at startup, so
no secret enters the repository. Source-IP binding in hub.BindScreenConn
remains in place as defense-in-depth.

Co-Authored-By: deepseek-v4-pro
This commit is contained in:
yuanyuanxianganddeepseek-v4-pro committed 2026-10-15 19:43:58 +02:00
1 parent 4ff965a2b4
commit 7651ee8602
3 files changed
+222 -19

No files matched your search

+103
View File
@@ -0,0 +1,103 @@
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"testing"
"time"
"github.com/yuanyuanxiang/SimpleRemoter/server/go/protocol"
)
// connAuthTestKey is an arbitrary non-secret key used only to exercise the
// verifyConnAuth code path in tests. The production key is never compiled in:
// it is read from the YAMA_CONN_AUTH_KEY environment variable at startup.
const connAuthTestKey = "test-conn-auth-key"
// signConnAuth reproduces the client's ConnAuth signature: hex(HMAC-SHA256(key,
// clientID||timestamp||nonce)) as 64 lowercase hex chars. Mirrors
// client/IOCPClient.cpp PerformConnAuth + license.cpp signMessage.
func signConnAuth(key string, clientID, ts uint64, nonce [16]byte) string {
mac := hmac.New(sha256.New, []byte(key))
var msg [32]byte
binary.LittleEndian.PutUint64(msg[0:8], clientID)
binary.LittleEndian.PutUint64(msg[8:16], ts)
copy(msg[16:32], nonce[:])
mac.Write(msg[:])
return hex.EncodeToString(mac.Sum(nil))
}
func testNonce() [16]byte {
var n [16]byte
for i := range n {
n[i] = byte(i)
}
return n
}
// TestConnAuthHmacAlgorithmVector pins the signing algorithm and byte layout
// (HMAC-SHA256 over clientID||timestamp||nonce, lowercase hex) against a vector
// computed independently with Python's hmac/hashlib (not Go's crypto). The key
// value is irrelevant here — what matters is that the server reproduces the
// exact signing the client performs. The real key is supplied at runtime via
// YAMA_CONN_AUTH_KEY, never compiled in.
func TestConnAuthHmacAlgorithmVector(t *testing.T) {
const want = "e59bf1259c77e065afa604cb5b1794b0c8c03fbb7e3473f5c3c34c2b94a90406"
if got := signConnAuth(connAuthTestKey, 0x1122334455667788, 1700000000, testNonce()); got != want {
t.Fatalf("HMAC mismatch:\n got %s\n want %s", got, want)
}
}
func TestVerifyConnAuth(t *testing.T) {
h := &MyHandler{connAuthKey: []byte(connAuthTestKey)}
build := func(ts uint64, sig []byte) []byte {
pkt := make([]byte, protocol.ConnAuthPacketSize)
pkt[0] = protocol.TokenConnAuth
binary.LittleEndian.PutUint64(pkt[protocol.ConnAuthOffClientID:], 0x1122334455667788)
binary.LittleEndian.PutUint64(pkt[protocol.ConnAuthOffTimestamp:], ts)
for i := range 16 {
pkt[protocol.ConnAuthOffNonce+i] = byte(i)
}
copy(pkt[protocol.ConnAuthOffSignature:], sig)
return pkt
}
nonce := testNonce()
t.Run("ok", func(t *testing.T) {
ts := uint64(time.Now().Unix())
sig := signConnAuth(connAuthTestKey, 0x1122334455667788, ts, nonce)
if got := h.verifyConnAuth(build(ts, []byte(sig))); got != protocol.ConnAuthStatusOK {
t.Fatalf("expected OK, got status %d", got)
}
})
t.Run("bad_signature", func(t *testing.T) {
ts := uint64(time.Now().Unix())
sig := signConnAuth(connAuthTestKey, 0x1122334455667788, ts, nonce)
pkt := build(ts, []byte(sig))
pkt[protocol.ConnAuthOffSignature] ^= 0xff
if got := h.verifyConnAuth(pkt); got != protocol.ConnAuthStatusBadSignature {
t.Fatalf("expected BadSignature, got %d", got)
}
})
t.Run("clock_skew", func(t *testing.T) {
// Just outside the 300 s tolerance, with a signature valid for that
// timestamp, so the skew check fires before the signature check.
ts := uint64(time.Now().Unix()) - protocol.ConnAuthTimestampToleranceSec - 1
sig := signConnAuth(connAuthTestKey, 0x1122334455667788, ts, nonce)
if got := h.verifyConnAuth(build(ts, []byte(sig))); got != protocol.ConnAuthStatusClockSkew {
t.Fatalf("expected ClockSkew, got %d", got)
}
})
t.Run("bad_size", func(t *testing.T) {
if got := h.verifyConnAuth([]byte{protocol.TokenConnAuth}); got != protocol.ConnAuthStatusBadSize {
t.Fatalf("expected BadSize, got %d", got)
}
})
}