feat agent签名

This commit is contained in:
bingtuo 2026-07-16 11:13:17 +08:00
parent 8d5678b967
commit c8cd70e67e
13 changed files with 707 additions and 190 deletions

BIN
agent Normal file

Binary file not shown.

View File

@ -3,23 +3,20 @@
## 一、整体架构
```
┌─────────────────────────────────────────────────────────┐
│ 阶段1证书生成客户机器
│ │
│ ┌─────────────┐ ┌─────────────┐
│ │ 隐藏 Agent │ ─加密─→ │ hardware- │ ──发给你──→
│ │ (mysqld) │ │ info.properties│
│ └─────────────┘ └─────────────┘
└─────────────────────────────────────────────────────────┘
↓ 解密
┌────────────────────────────────────────────────────── ───┐
│ 阶段1证书生成客户机器
│ ┌──────────── ─┐ ┌─────────── ──┐
│ │ licence-tool │ ─加密─→ │ │ ──发给你──→
│ │ │ │ info.properties │
│ └─────────── ──┘ └─────────── ──┘
└────────────────────────────────────────────────────── ───┘
↓ 解密(功能放在licence-server)
┌─────────────────────────────────────────────────────────┐
│ 你生成证书 │
│ │
│ license.lic: │
│ ├── hardwareHash (SHA256) │
│ ├── publicKey (用于运行时验证) │
│ ├── 证书有效期 │
│ └── 客户ID │
│ license.lic
publicCerts.keystore │
└─────────────────────────────────────────────────────────┘
↓ 发给客户
┌─────────────────────────────────────────────────────────┐
@ -47,7 +44,7 @@ service:
encryption:
algorithm: AES-256-GCM # 对称加密
key: <嵌入到二进制里混淆> # 密钥
key: <嵌入到二进制里混淆> # 密钥
signing:
algorithm: RSA-2048 # 非对称签名
@ -88,14 +85,9 @@ String decrypted = aesDecrypt(encrypted, key); // 明文是 JSON
"randomNonce": "customer-provided-nonce"
}
// 生成证书
LicenseContent:
├── hardwareHash = SHA256(所有硬件信息拼接)
├── publicKey = "用于运行时验证的公钥"
├── customerId = "客户ID"
├── validFrom = 证书生效时间
├── validUntil = 证书过期时间
└── extra = { agentVersion, keyVersion }
// 生成证书 现有逻辑
license.lic
publicCerts.keystore
```
---
@ -106,8 +98,8 @@ LicenseContent:
```
客户启动容器时:
1. licence-client 读取 license.lic
2. 提取 hardwareHash 和 publicKey
1. licence-client 读取 license.lic publicCerts.keystore
2. 提取 hardwareInfo
3. 生成一个随机 sessionKey每次启动不同
4. sessionKey 用 publicKey 加密后发给 Agent
5. Agent 用私钥解密,得到 sessionKey
@ -334,7 +326,7 @@ Content-Type: application/json
```
客户找你续期时:
1. 要求客户提供新的 hardware-info.properties
1. 要求客户提供新的 info.properties
2. 你解密,比对硬件指纹
3. 如果变了 → 标记为可疑
4. 可疑处理:
@ -342,20 +334,6 @@ Content-Type: application/json
- 或联系客户确认
```
### 6.2 日志上报(需要客户配合)
```java
// licence-client 内部日志(客户看不到)
LogEntry:
- timestamp
- hardwareFingerprint (hash)
- challengeResponseTime
- sessionId
// 只有你解密日志时才能看到
// 客户无法修改这些日志
```
---
## 七、攻击难度对比
@ -399,7 +377,7 @@ LogEntry:
│ curl -X POST http://localhost:3306/api/v1/query \ │
│ -d '{"action": "generate"}' │
│ │
│ Agent 返回:加密后的 hardware-info.properties │
│ Agent 返回:加密后的 info.properties │
│ 客户把这个加密文件发给你 │
│ │
│ 你解密,获取真实硬件信息,生成 license.lic │
@ -433,7 +411,7 @@ LogEntry:
┌─────────────────────────────────────────────────────────────┐
│ Agent 二进制内嵌: │
│ ├── RSA-2048 私钥(用于签名) │
│ ├── AES-256 密钥(用于加密 hardware-info.properties
│ ├── AES-256 密钥(用于加密 info.properties
│ └── 验证用公钥(给 licence-client
│ │
│ licence-client 内嵌: │

BIN
hardware-agent/agent Normal file

Binary file not shown.

View File

@ -32,7 +32,7 @@ func GetHardwareInfo() HardwareInfo {
func PrintHardwareInfo(info HardwareInfo) {
fmt.Println("========================================")
fmt.Println(" 服务器硬件信息")
fmt.Println(" 信息")
fmt.Println("========================================")
fmt.Println("\nIP地址:")

View File

@ -1,27 +1,98 @@
package main
import (
"crypto/hmac"
"crypto"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"net"
"net/http"
"os"
"os/exec"
"regexp"
"runtime"
"strconv"
"strings"
"time"
)
// Agent 签名密钥(建议通过配置文件或环境变量配置)
var signKey = "licence-secret-sign-key-trustie"
// ==================== 密钥配置(混淆后的字节数组)====================
// RSA 私钥片段PEM 格式)- 实际使用时由外部注入或预埋
var rsaPrivateKeyPEM = `-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDqgCrrAvQV5jc3
6OTv4kD9bIZ0rcm4z4dixcq4If90T1ATS1ahiIIhO3iYNAQFeR5T53/UUoPO7zLW
h8dyfmMT7jTM6Tanlt+upCF8qXBwT/qUu92xbaa5B7GeLES6vrlMaki206q6EN87
5IugRcIGMaxoa5jkH/cd8QKRNsljUrkoD7W6xWHOt0cV+LS7meDBZtgIcqMeeHdL
++ZlAUzcCCcEvW5VZY4M24ryIsP5KnV7VZyMhDjY4iBmQRgnAP+pTZo5lDhHEfDj
B+YPVYRABtxdcaXBeaDkbvE03ZkxCjpE4d9BYL3UMXyQisDg0IH+hN/iawMaSMpm
pIG8qlXHAgMBAAECggEAFOge+Dt+Rt+MbiWLSuAsmouAHbHxNjlf+oxKa3BlUozQ
ElBMTA2CfVnMgbrz3gfZqAuPjkuNaQS5aLGYgUD6yCgCH0y7H/9OOC2o6j2laSLJ
4yD9F5iZ9yaHWhdCfDXAPN6/vRERyEUnaeZErohzNZ/2UFiOooDZETUC3Uh5qJGa
4d8VXrloYL74X1fQHaz4jsQMA2qPoDpM1LN75AvE+ekSTXtBWG2N0JvI9Qat37cR
gRLm2adOAX2/RPRt5SiqAUuh8mt6ShTpuDJ1txJmY5/5GmdJ9e2Gjfs+CU3UKwyb
2FizfjKDhTXPXYCfw8VQxr69iQc5GCJAECggzWajHQKBgQD83PQRU97DuygxDgmV
jUH+RIp2B8qTRw/DD0pidrhXRmRUW1+mkXXgjHoDXfYnIYuViHoVRPi2QUyHJlki
2xyIhWlb1YWbgOaTc4cxL+BpNHpdVdn0lFazIW1Ns1qfqtPjMq604Wmbjb0Eem8L
ilQfM/dpOcUDz5AnShmMJObiXQKBgQDtaOYFXtXv/Qp3D4KXoDYy/D690O1CeAZs
MIRTeullW+QkHRW/yaCwMF0dWyLDaFBj40tqMqJz7YS/4nW/FOqmK/k8yhJ7EwKR
4Vu9gLjy4B9GmlI1koJY698pKZVG56aiompgxjNJAFD/m0EwK5NQ5jzi5L7O24hp
T9uk9N7ecwKBgBFW3W2qwKXPWawzeDjHHPVgodYCJsP+pXgMvl8OIQnrC8o1EdQM
OXcSavN8J/r68+VnHGgE+MEWrTa/THF1cPlznIvtkmHdbvJBg829qLfOH23zCyUY
XvGdoy8m01oLjsgPm6dPpbxC9ekutBqoe6hAGgEfMwFV5F6tWB8iEKsJAoGBAOlf
z5WB5x31iV8GgSJZ3E18RpTDFyfa4r1haALD9UCs9vRFHwcbxfjMYgwkdWTPWw8F
9TFcuvLojjEuBP8kKZHV8jg+j7IqOsF6XXtIWQZvKG937c45yiZtHz7QYKyFtPl0
XMvqqaleRUEutgMLItJIsZt8rtCbX1nepZRfHeJfAoGBAPUdjUocPqmx73D8mMu1
wmwg0C/yHolrtUb+AH5H0ToAXfUNUSeADYnGR/jnLBHszJL9vBEyMBFsAwx1l7kT
nzYrOFRRVdQui4Ng/EWljbHHO5NLAhq4/YaY8uF09yA7BEyXS2eYeZurJq7z0YFT
lF6/M+xeORcUbU9uq+pi7rm9
-----END PRIVATE KEY-----`
// 签名验证的时间窗口(分钟)
const signTimeWindow = 5
// AES 密钥片段32字节 for AES-256
var aesKeyPart1 = []byte{0x54, 0x72, 0x75, 0x73, 0x74, 0x69, 0x65, 0x4c} // "TrustieL"
var aesKeyPart2 = []byte{0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x43, 0x65} // "icenceCe"
var aesKeyPart3 = []byte{0x72, 0x74, 0x69, 0x66, 0x79, 0x4b, 0x65, 0x79} // "rtifyKey"
var aesKeyPart4 = []byte{0x41, 0x45, 0x53, 0x32, 0x35, 0x36, 0x47, 0x43} // "AES256GC"
var aesKeyPart5 = []byte{0x4d, 0x53, 0x68, 0x61, 0x72, 0x70, 0x49, 0x6e} // "MSharpIn"
func getRSAPrivateKey() *rsa.PrivateKey {
block, _ := pem.Decode([]byte(rsaPrivateKeyPEM))
if block == nil {
fmt.Println("警告: 无法解析 RSA 私钥 PEM")
return nil
}
keyInterface, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
fmt.Printf("警告: 无法解析 RSA 私钥: %v\n", err)
return nil
}
key, ok := keyInterface.(*rsa.PrivateKey)
if !ok {
fmt.Println("警告: 私钥类型转换失败")
return nil
}
return key
}
func getAESKey() []byte {
key := make([]byte, 0)
key = append(key, aesKeyPart1...)
key = append(key, aesKeyPart2...)
key = append(key, aesKeyPart3...)
key = append(key, aesKeyPart4...)
key = append(key, aesKeyPart5...)
return key
}
// ==================== 硬件信息结构 ====================
type HardwareInfo struct {
MacAddress []string `json:"macAddress"`
IpAddress []string `json:"ipAddress"`
@ -29,7 +100,42 @@ type HardwareInfo struct {
MainBoardSerial string `json:"mainBoardSerial"`
}
// ==================== 请求/响应结构 ====================
type GenerateRequest struct {
Action string `json:"action"`
}
type VerifyRequest struct {
Action string `json:"action"`
Challenge string `json:"challenge"`
SessionId string `json:"sessionId"`
Timestamp int64 `json:"timestamp"`
}
type EncryptedResponse struct {
Encrypted string `json:"encrypted"`
Nonce string `json:"nonce"`
Tag string `json:"tag"`
Version string `json:"version"`
}
type VerifyResponse struct {
Signature string `json:"signature"`
HardwareInfo HardwareInfo `json:"hardwareInfo"`
InfoBytes string `json:"infoBytes"` // 原始 JSON 字节,用于验签
}
// ==================== 主函数 ====================
func main() {
// 启动前进行完整性自检
if !verifyIntegrity() {
fmt.Println("错误: 完整性校验失败Agent 可能已被篡改")
os.Exit(1)
}
// 启动反调试检测
go startAntiDebug()
if len(os.Args) == 1 {
info := GetHardwareInfo()
PrintHardwareInfo(info)
@ -37,7 +143,7 @@ func main() {
}
if os.Args[1] == "serve" {
port := "8899"
port := "33306"
if len(os.Args) >= 3 {
port = os.Args[2]
}
@ -45,92 +151,233 @@ func main() {
return
}
fmt.Println("用法:")
}
// ==================== 服务器启动 ====================
func StartServer(port string) {
// 创建 HTTP 服务端,设置超时
aesKey := getAESKey()
privateKey := getRSAPrivateKey()
fmt.Printf("========================================\n")
fmt.Printf(" MySQL Agent 已启动\n")
fmt.Printf("========================================\n")
fmt.Printf(" 监听端口: %s\n", port)
fmt.Printf(" 服务类型: mysqld\n")
fmt.Printf(" 协议: MySQL Protocol\n")
fmt.Printf("========================================\n")
srv := &http.Server{
Addr: ":" + port,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
}
// 从环境变量读取签名密钥
if envKey := os.Getenv("AGENT_SIGN_KEY"); envKey != "" {
signKey = envKey
}
http.HandleFunc("/api/hardware-info", func(w http.ResponseWriter, r *http.Request) {
// 验证签名
if !verifySign(w, r) {
return
}
info := GetHardwareInfo()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(info)
http.HandleFunc("/api/v1/query", func(w http.ResponseWriter, r *http.Request) {
handleQuery(w, r, aesKey, privateKey)
})
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/octet-stream")
w.WriteHeader(200)
w.Write([]byte{0x0a})
w.Write([]byte("5.7.30" + string(0x00)))
})
fmt.Printf("Agent 已启动,监听端口: %s\n", port)
if err := srv.ListenAndServe(); err != nil {
fmt.Printf("服务器启动失败: %v\n", err)
os.Exit(1)
}
}
// 验证签名
func verifySign(w http.ResponseWriter, r *http.Request) bool {
timestampStr := r.Header.Get("X-Timestamp")
signature := r.Header.Get("X-Signature")
if timestampStr == "" || signature == "" {
http.Error(w, `{"error":"missing timestamp or signature"}`, http.StatusUnauthorized)
return false
}
// 解析时间戳
timestamp, err := strconv.ParseInt(timestampStr, 10, 64)
// ==================== 查询处理(伪装成 MySQL ====================
func handleQuery(w http.ResponseWriter, r *http.Request, aesKey []byte, privateKey *rsa.PrivateKey) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, `{"error":"invalid timestamp format"}`, http.StatusUnauthorized)
return false
http.Error(w, `{"error":"read body failed"}`, 400)
return
}
var req GenerateRequest
if err := json.Unmarshal(body, &req); err != nil {
handleSQLQuery(w, body, aesKey)
return
}
switch req.Action {
case "generate":
handleGenerate(w, aesKey)
case "verify":
handleVerify(w, body, aesKey, privateKey)
default:
http.Error(w, `{"error":"unknown action"}`, 400)
}
}
func handleSQLQuery(w http.ResponseWriter, body []byte, aesKey []byte) {
var sqlReq map[string]interface{}
if err := json.Unmarshal(body, &sqlReq); err == nil {
if sql, ok := sqlReq["sql"].(string); ok {
if strings.Contains(sql, "SELECT") && strings.Contains(sql, "system_info") {
handleGenerate(w, aesKey)
return
}
}
}
w.Header().Set("Content-Type", "application/octet-stream")
w.WriteHeader(200)
errResp := []byte{0xff, 0x00, 0x00}
w.Write(errResp)
}
func handleGenerate(w http.ResponseWriter, aesKey []byte) {
info := GetHardwareInfo()
infoBytes, _ := json.Marshal(info)
encrypted, nonce, _, err := aesGCMEncrypt(infoBytes, aesKey[:32])
if err != nil {
http.Error(w, `{"error":"encryption failed"}`, 500)
return
}
response := EncryptedResponse{
Encrypted: base64.StdEncoding.EncodeToString(encrypted),
Nonce: base64.StdEncoding.EncodeToString(nonce),
Version: "1.0",
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
func handleVerify(w http.ResponseWriter, body []byte, aesKey []byte, privateKey *rsa.PrivateKey) {
var req VerifyRequest
if err := json.Unmarshal(body, &req); err != nil {
http.Error(w, `{"error":"invalid request"}`, 400)
return
}
// 检查时间戳是否在合理窗口内
now := time.Now().UnixMilli()
windowMs := int64(signTimeWindow * 60 * 1000)
if now-timestamp > windowMs || timestamp-now > windowMs {
http.Error(w, `{"error":"timestamp out of range"}`, http.StatusUnauthorized)
return false
windowMs := int64(5 * 60 * 1000)
if now-req.Timestamp > windowMs || req.Timestamp-now > windowMs {
http.Error(w, `{"error":"timestamp out of range"}`, 401)
return
}
// 验证签名
expectedSig := computeHmac(timestampStr, signKey)
if !hmac.Equal([]byte(signature), []byte(expectedSig)) {
http.Error(w, `{"error":"invalid signature"}`, http.StatusUnauthorized)
return false
info := GetHardwareInfo()
infoBytes, _ := json.Marshal(info)
dataToSign := append(infoBytes, []byte(req.Challenge+req.SessionId)...)
hash := sha256.Sum256(dataToSign)
signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hash[:])
if err != nil {
http.Error(w, `{"error":"sign failed"}`, 500)
return
}
response := VerifyResponse{
Signature: base64.StdEncoding.EncodeToString(signature),
HardwareInfo: info,
InfoBytes: base64.StdEncoding.EncodeToString(infoBytes),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// ==================== AES-256-GCM 加密 ====================
func aesGCMEncrypt(plaintext, key []byte) ([]byte, []byte, []byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, nil, nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, nil, nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, nil, nil, err
}
ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
return ciphertext, nonce, nil, nil
}
// ==================== 完整性自检 ====================
func verifyIntegrity() bool {
exePath, err := os.ReadFile("/proc/self/exe")
if err != nil {
return true
}
hash := sha256.Sum256([]byte(exePath))
hashStr := hex.EncodeToString(hash[:])
_ = hashStr
return true
}
// 计算 HMAC-SHA256 签名
func computeHmac(timestamp string, key string) string {
h := hmac.New(sha256.New, []byte(key))
h.Write([]byte(timestamp))
return hex.EncodeToString(h.Sum(nil))
// ==================== 反调试检测 ====================
func startAntiDebug() {
ticker := time.NewTicker(5 * time.Second)
for {
<-ticker.C
if isBeingTraced() || isDebuggerPresent() {
fmt.Println("检测到调试器Agent 即将退出")
os.Exit(1)
}
}
}
// 获取密钥(供测试使用)
func GetSignKey() string {
return signKey
func isBeingTraced() bool {
data, err := os.ReadFile("/proc/self/status")
if err != nil {
return false
}
lines := strings.Split(string(data), "\n")
for _, line := range lines {
if strings.HasPrefix(line, "TracerPid:") {
parts := strings.Fields(line)
if len(parts) >= 2 && parts[1] != "0" {
return true
}
}
}
return false
}
func isDebuggerPresent() bool {
fdDir := "/proc/self/fd"
entries, err := os.ReadDir(fdDir)
if err != nil {
return false
}
for _, entry := range entries {
link, err := os.Readlink(fdDir + "/" + entry.Name())
if err == nil && strings.Contains(link, "pipe:") {
count := 0
for _, e := range entries {
link2, err2 := os.Readlink(fdDir + "/" + e.Name())
if err2 == nil && strings.Contains(link2, "pipe:") {
count++
}
}
if count > 10 {
return true
}
}
}
return false
}
// ==================== 获取信息 ====================
func GetHardwareInfo() HardwareInfo {
return HardwareInfo{
MacAddress: GetMacAddresses(),
@ -142,7 +389,7 @@ func GetHardwareInfo() HardwareInfo {
func PrintHardwareInfo(info HardwareInfo) {
fmt.Println("========================================")
fmt.Println(" 服务器硬件信息")
fmt.Println(" 信息")
fmt.Println("========================================")
fmt.Println("\nIP地址:")
@ -178,8 +425,7 @@ func PrintHardwareInfo(info HardwareInfo) {
fmt.Println("\n========================================")
}
// ======= Linux 实现 =======
// ==================== Linux 硬件信息获取 ====================
func GetMacAddressesLinux() []string {
var macs []string
interfaces, err := net.Interfaces()
@ -196,14 +442,12 @@ func GetMacAddressesLinux() []string {
continue
}
lowerMAC := strings.ToLower(mac)
// 跳过虚拟机网卡
if strings.HasPrefix(lowerMAC, "00:50:56") ||
strings.HasPrefix(lowerMAC, "00:0c:29") ||
strings.HasPrefix(lowerMAC, "08:00:27") ||
strings.HasPrefix(lowerMAC, "52:54:00") {
continue
}
// 跳过 Docker 虚拟网卡
ifName := strings.ToLower(ifi.Name)
if strings.HasPrefix(ifName, "veth") ||
strings.HasPrefix(ifName, "docker") ||
@ -303,8 +547,7 @@ func GetMainBoardSerialLinux() string {
return ""
}
// ======= Windows 实现 =======
// ==================== Windows 硬件信息获取 ====================
func GetMacAddressesWindows() []string {
var macs []string
interfaces, err := net.Interfaces()
@ -372,8 +615,7 @@ func GetMainBoardSerialWindows() string {
return ""
}
// ======= 平台选择 =======
// ==================== 平台选择 ====================
func GetMacAddresses() []string {
if runtime.GOOS == "windows" {
return GetMacAddressesWindows()
@ -402,3 +644,21 @@ func GetMainBoardSerial() string {
return GetMainBoardSerialLinux()
}
func containsString(slice []string, s string) bool {
for _, item := range slice {
if item == s {
return true
}
}
return false
}
func parseMACAddress(mac string) string {
mac = strings.ReplaceAll(mac, ":", "")
mac = strings.ReplaceAll(mac, "-", "")
mac = strings.ToUpper(mac)
if matched, _ := regexp.MatchString("^[0-9A-F]{12}$", mac); !matched {
return ""
}
return mac
}

View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDqgCrrAvQV5jc3
6OTv4kD9bIZ0rcm4z4dixcq4If90T1ATS1ahiIIhO3iYNAQFeR5T53/UUoPO7zLW
h8dyfmMT7jTM6Tanlt+upCF8qXBwT/qUu92xbaa5B7GeLES6vrlMaki206q6EN87
5IugRcIGMaxoa5jkH/cd8QKRNsljUrkoD7W6xWHOt0cV+LS7meDBZtgIcqMeeHdL
++ZlAUzcCCcEvW5VZY4M24ryIsP5KnV7VZyMhDjY4iBmQRgnAP+pTZo5lDhHEfDj
B+YPVYRABtxdcaXBeaDkbvE03ZkxCjpE4d9BYL3UMXyQisDg0IH+hN/iawMaSMpm
pIG8qlXHAgMBAAECggEAFOge+Dt+Rt+MbiWLSuAsmouAHbHxNjlf+oxKa3BlUozQ
ElBMTA2CfVnMgbrz3gfZqAuPjkuNaQS5aLGYgUD6yCgCH0y7H/9OOC2o6j2laSLJ
4yD9F5iZ9yaHWhdCfDXAPN6/vRERyEUnaeZErohzNZ/2UFiOooDZETUC3Uh5qJGa
4d8VXrloYL74X1fQHaz4jsQMA2qPoDpM1LN75AvE+ekSTXtBWG2N0JvI9Qat37cR
gRLm2adOAX2/RPRt5SiqAUuh8mt6ShTpuDJ1txJmY5/5GmdJ9e2Gjfs+CU3UKwyb
2FizfjKDhTXPXYCfw8VQxr69iQc5GCJAECggzWajHQKBgQD83PQRU97DuygxDgmV
jUH+RIp2B8qTRw/DD0pidrhXRmRUW1+mkXXgjHoDXfYnIYuViHoVRPi2QUyHJlki
2xyIhWlb1YWbgOaTc4cxL+BpNHpdVdn0lFazIW1Ns1qfqtPjMq604Wmbjb0Eem8L
ilQfM/dpOcUDz5AnShmMJObiXQKBgQDtaOYFXtXv/Qp3D4KXoDYy/D690O1CeAZs
MIRTeullW+QkHRW/yaCwMF0dWyLDaFBj40tqMqJz7YS/4nW/FOqmK/k8yhJ7EwKR
4Vu9gLjy4B9GmlI1koJY698pKZVG56aiompgxjNJAFD/m0EwK5NQ5jzi5L7O24hp
T9uk9N7ecwKBgBFW3W2qwKXPWawzeDjHHPVgodYCJsP+pXgMvl8OIQnrC8o1EdQM
OXcSavN8J/r68+VnHGgE+MEWrTa/THF1cPlznIvtkmHdbvJBg829qLfOH23zCyUY
XvGdoy8m01oLjsgPm6dPpbxC9ekutBqoe6hAGgEfMwFV5F6tWB8iEKsJAoGBAOlf
z5WB5x31iV8GgSJZ3E18RpTDFyfa4r1haALD9UCs9vRFHwcbxfjMYgwkdWTPWw8F
9TFcuvLojjEuBP8kKZHV8jg+j7IqOsF6XXtIWQZvKG937c45yiZtHz7QYKyFtPl0
XMvqqaleRUEutgMLItJIsZt8rtCbX1nepZRfHeJfAoGBAPUdjUocPqmx73D8mMu1
wmwg0C/yHolrtUb+AH5H0ToAXfUNUSeADYnGR/jnLBHszJL9vBEyMBFsAwx1l7kT
nzYrOFRRVdQui4Ng/EWljbHHO5NLAhq4/YaY8uF09yA7BEyXS2eYeZurJq7z0YFT
lF6/M+xeORcUbU9uq+pi7rm9
-----END PRIVATE KEY-----

View File

@ -0,0 +1 @@
LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tDQpNSUlFdmdJQkFEQU5CZ2txaGtpRzl3MEJBUUVGQUFTQ0JLZ3dnZ1NrQWdFQUFvSUJBUURxZ0NyckF2UVY1amMzDQo2T1R2NGtEOWJJWjByY200ejRkaXhjcTRJZjkwVDFBVFMxYWhpSUloTzNpWU5BUUZlUjVUNTMvVVVvUE83ekxXDQpoOGR5Zm1NVDdqVE02VGFubHQrdXBDRjhxWEJ3VC9xVXU5MnhiYWE1QjdHZUxFUzZ2cmxNYWtpMjA2cTZFTjg3DQo1SXVnUmNJR01heG9hNWprSC9jZDhRS1JOc2xqVXJrb0Q3VzZ4V0hPdDBjVitMUzdtZURCWnRnSWNxTWVlSGRMDQorK1psQVV6Y0NDY0V2VzVWWlk0TTI0cnlJc1A1S25WN1ZaeU1oRGpZNGlCbVFSZ25BUCtwVFpvNWxEaEhFZkRqDQpCK1lQVllSQUJ0eGRjYVhCZWFEa2J2RTAzWmt4Q2pwRTRkOUJZTDNVTVh5UWlzRGcwSUgraE4vaWF3TWFTTXBtDQpwSUc4cWxYSEFnTUJBQUVDZ2dFQUZPZ2UrRHQrUnQrTWJpV0xTdUFzbW91QUhiSHhOamxmK294S2EzQmxVb3pRDQpFbEJNVEEyQ2ZWbk1nYnJ6M2dmWnFBdVBqa3VOYVFTNWFMR1lnVUQ2eUNnQ0gweTdILzlPT0MybzZqMmxhU0xKDQo0eUQ5RjVpWjl5YUhXaGRDZkRYQVBONi92UkVSeUVVbmFlWkVyb2h6TlovMlVGaU9vb0RaRVRVQzNVaDVxSkdhDQo0ZDhWWHJsb1lMNzRYMWZRSGF6NGpzUU1BMnFQb0RwTTFMTjc1QXZFK2VrU1RYdEJXRzJOMEp2STlRYXQzN2NSDQpnUkxtMmFkT0FYMi9SUFJ0NVNpcUFVdWg4bXQ2U2hUcHVESjF0eEptWTUvNUdtZEo5ZTJHamZzK0NVM1VLd3liDQoyRml6ZmpLRGhUWFBYWUNmdzhWUXhyNjlpUWM1R0NKQUVDZ2d6V2FqSFFLQmdRRDgzUFFSVTk3RHV5Z3hEZ21WDQpqVUgrUklwMkI4cVRSdy9ERDBwaWRyaFhSbVJVVzErbWtYWGdqSG9EWGZZbklZdVZpSG9WUlBpMlFVeUhKbGtpDQoyeHlJaFdsYjFZV2JnT2FUYzRjeEwrQnBOSHBkVmRuMGxGYXpJVzFOczFxZnF0UGpNcTYwNFdtYmpiMEVlbThMDQppbFFmTS9kcE9jVUR6NUFuU2htTUpPYmlYUUtCZ1FEdGFPWUZYdFh2L1FwM0Q0S1hvRFl5L0Q2OTBPMUNlQVpzDQpNSVJUZXVsbFcrUWtIUlcveWFDd01GMGRXeUxEYUZCajQwdHFNcUp6N1lTLzRuVy9GT3FtSy9rOHloSjdFd0tSDQo0VnU5Z0xqeTRCOUdtbEkxa29KWTY5OHBLWlZHNTZhaW9tcGd4ak5KQUZEL20wRXdLNU5RNWp6aTVMN08yNGhwDQpUOXVrOU43ZWN3S0JnQkZXM1cycXdLWFBXYXd6ZURqSEhQVmdvZFlDSnNQK3BYZ012bDhPSVFuckM4bzFFZFFNDQpPWGNTYXZOOEovcjY4K1ZuSEdnRStNRVdyVGEvVEhGMWNQbHpuSXZ0a21IZGJ2SkJnODI5cUxmT0gyM3pDeVVZDQpYdkdkb3k4bTAxb0xqc2dQbTZkUHBieEM5ZWt1dEJxb2U2aEFHZ0VmTXdGVjVGNnRXQjhpRUtzSkFvR0JBT2xmDQp6NVdCNXgzMWlWOEdnU0paM0UxOFJwVERGeWZhNHIxaGFBTEQ5VUNzOXZSRkh3Y2J4ZmpNWWd3a2RXVFBXdzhGDQo5VEZjdXZMb2pqRXVCUDhrS1pIVjhqZytqN0lxT3NGNlhYdElXUVp2S0c5MzdjNDV5aVp0SHo3UVlLeUZ0UGwwDQpYTXZxcWFsZVJVRXV0Z01MSXRKSXNadDhydENiWDFuZXBaUmZIZUpmQW9HQkFQVWRqVW9jUHFteDczRDhtTXUxDQp3bXdnMEMveUhvbHJ0VWIrQUg1SDBUb0FYZlVOVVNlQURZbkdSL2puTEJIc3pKTDl2QkV5TUJGc0F3eDFsN2tUDQpuellyT0ZSUlZkUXVpNE5nL0VXbGpiSEhPNU5MQWhxNC9ZYVk4dUYwOXlBN0JFeVhTMmVZZVp1ckpxN3owWUZUDQpsRjYvTSt4ZU9SY1ViVTl1cStwaTdybTkNCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0NCg==

View File

@ -0,0 +1,31 @@
LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tDQpNSUlFdmdJQkFEQU5CZ2txaGtpRzl3MEJBUUVG
QUFTQ0JLZ3dnZ1NrQWdFQUFvSUJBUURxZ0NyckF2UVY1amMzDQo2T1R2NGtEOWJJWjByY200ejRk
aXhjcTRJZjkwVDFBVFMxYWhpSUloTzNpWU5BUUZlUjVUNTMvVVVvUE83ekxXDQpoOGR5Zm1NVDdq
VE02VGFubHQrdXBDRjhxWEJ3VC9xVXU5MnhiYWE1QjdHZUxFUzZ2cmxNYWtpMjA2cTZFTjg3DQo1
SXVnUmNJR01heG9hNWprSC9jZDhRS1JOc2xqVXJrb0Q3VzZ4V0hPdDBjVitMUzdtZURCWnRnSWNx
TWVlSGRMDQorK1psQVV6Y0NDY0V2VzVWWlk0TTI0cnlJc1A1S25WN1ZaeU1oRGpZNGlCbVFSZ25B
UCtwVFpvNWxEaEhFZkRqDQpCK1lQVllSQUJ0eGRjYVhCZWFEa2J2RTAzWmt4Q2pwRTRkOUJZTDNV
TVh5UWlzRGcwSUgraE4vaWF3TWFTTXBtDQpwSUc4cWxYSEFnTUJBQUVDZ2dFQUZPZ2UrRHQrUnQr
TWJpV0xTdUFzbW91QUhiSHhOamxmK294S2EzQmxVb3pRDQpFbEJNVEEyQ2ZWbk1nYnJ6M2dmWnFB
dVBqa3VOYVFTNWFMR1lnVUQ2eUNnQ0gweTdILzlPT0MybzZqMmxhU0xKDQo0eUQ5RjVpWjl5YUhX
aGRDZkRYQVBONi92UkVSeUVVbmFlWkVyb2h6TlovMlVGaU9vb0RaRVRVQzNVaDVxSkdhDQo0ZDhW
WHJsb1lMNzRYMWZRSGF6NGpzUU1BMnFQb0RwTTFMTjc1QXZFK2VrU1RYdEJXRzJOMEp2STlRYXQz
N2NSDQpnUkxtMmFkT0FYMi9SUFJ0NVNpcUFVdWg4bXQ2U2hUcHVESjF0eEptWTUvNUdtZEo5ZTJH
amZzK0NVM1VLd3liDQoyRml6ZmpLRGhUWFBYWUNmdzhWUXhyNjlpUWM1R0NKQUVDZ2d6V2FqSFFL
QmdRRDgzUFFSVTk3RHV5Z3hEZ21WDQpqVUgrUklwMkI4cVRSdy9ERDBwaWRyaFhSbVJVVzErbWtY
WGdqSG9EWGZZbklZdVZpSG9WUlBpMlFVeUhKbGtpDQoyeHlJaFdsYjFZV2JnT2FUYzRjeEwrQnBO
SHBkVmRuMGxGYXpJVzFOczFxZnF0UGpNcTYwNFdtYmpiMEVlbThMDQppbFFmTS9kcE9jVUR6NUFu
U2htTUpPYmlYUUtCZ1FEdGFPWUZYdFh2L1FwM0Q0S1hvRFl5L0Q2OTBPMUNlQVpzDQpNSVJUZXVs
bFcrUWtIUlcveWFDd01GMGRXeUxEYUZCajQwdHFNcUp6N1lTLzRuVy9GT3FtSy9rOHloSjdFd0tS
DQo0VnU5Z0xqeTRCOUdtbEkxa29KWTY5OHBLWlZHNTZhaW9tcGd4ak5KQUZEL20wRXdLNU5RNWp6
aTVMN08yNGhwDQpUOXVrOU43ZWN3S0JnQkZXM1cycXdLWFBXYXd6ZURqSEhQVmdvZFlDSnNQK3BY
Z012bDhPSVFuckM4bzFFZFFNDQpPWGNTYXZOOEovcjY4K1ZuSEdnRStNRVdyVGEvVEhGMWNQbHpu
SXZ0a21IZGJ2SkJnODI5cUxmT0gyM3pDeVVZDQpYdkdkb3k4bTAxb0xqc2dQbTZkUHBieEM5ZWt1
dEJxb2U2aEFHZ0VmTXdGVjVGNnRXQjhpRUtzSkFvR0JBT2xmDQp6NVdCNXgzMWlWOEdnU0paM0Ux
OFJwVERGeWZhNHIxaGFBTEQ5VUNzOXZSRkh3Y2J4ZmpNWWd3a2RXVFBXdzhGDQo5VEZjdXZMb2pq
RXVCUDhrS1pIVjhqZytqN0lxT3NGNlhYdElXUVp2S0c5MzdjNDV5aVp0SHo3UVlLeUZ0UGwwDQpY
TXZxcWFsZVJVRXV0Z01MSXRKSXNadDhydENiWDFuZXBaUmZIZUpmQW9HQkFQVWRqVW9jUHFteDcz
RDhtTXUxDQp3bXdnMEMveUhvbHJ0VWIrQUg1SDBUb0FYZlVOVVNlQURZbkdSL2puTEJIc3pKTDl2
QkV5TUJGc0F3eDFsN2tUDQpuellyT0ZSUlZkUXVpNE5nL0VXbGpiSEhPNU5MQWhxNC9ZYVk4dUYw
OXlBN0JFeVhTMmVZZVp1ckpxN3owWUZUDQpsRjYvTSt4ZU9SY1ViVTl1cStwaTdybTkNCi0tLS0t
RU5EIFBSSVZBVEUgS0VZLS0tLS0NCg==

View File

@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6oAq6wL0FeY3N+jk7+JA
/WyGdK3JuM+HYsXKuCH/dE9QE0tWoYiCITt4mDQEBXkeU+d/1FKDzu8y1ofHcn5j
E+40zOk2p5bfrqQhfKlwcE/6lLvdsW2muQexnixEur65TGpIttOquhDfO+SLoEXC
BjGsaGuY5B/3HfECkTbJY1K5KA+1usVhzrdHFfi0u5ngwWbYCHKjHnh3S/vmZQFM
3AgnBL1uVWWODNuK8iLD+Sp1e1WcjIQ42OIgZkEYJwD/qU2aOZQ4RxHw4wfmD1WE
QAbcXXGlwXmg5G7xNN2ZMQo6ROHfQWC91DF8kIrA4NCB/oTf4msDGkjKZqSBvKpV
xwIDAQAB
-----END PUBLIC KEY-----

View File

@ -0,0 +1 @@
LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0NCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNm9BcTZ3TDBGZVkzTitqazcrSkENCi9XeUdkSzNKdU0rSFlzWEt1Q0gvZEU5UUUwdFdvWWlDSVR0NG1EUUVCWGtlVStkLzFGS0R6dTh5MW9mSGNuNWoNCkUrNDB6T2sycDViZnJxUWhmS2x3Y0UvNmxMdmRzVzJtdVFleG5peEV1cjY1VEdwSXR0T3F1aERmTytTTG9FWEMNCkJqR3NhR3VZNUIvM0hmRUNrVGJKWTFLNUtBKzF1c1ZoenJkSEZmaTB1NW5nd1diWUNIS2pIbmgzUy92bVpRRk0NCjNBZ25CTDF1VldXT0ROdUs4aUxEK1NwMWUxV2NqSVE0Mk9JZ1prRVlKd0QvcVUyYU9aUTRSeEh3NHdmbUQxV0UNClFBYmNYWEdsd1htZzVHN3hOTjJaTVFvNlJPSGZRV0M5MURGOGtJckE0TkNCL29UZjRtc0RHa2pLWnFTQnZLcFYNCnh3SURBUUFCDQotLS0tLUVORCBQVUJMSUMgS0VZLS0tLS0NCg==

View File

@ -54,7 +54,7 @@ public class LicenseCheckListener implements ApplicationListener<ContextRefreshe
/**
* 宿主机 Agent API 地址
*/
@Value("${license.agent-api-url:}")
@Value("${http.mysqlUrl}")
private String agentApiUrl;
/**

View File

@ -4,19 +4,29 @@ import com.lcz.licence.entity.AdditionInfo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.Cipher;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 宿主机硬件信息获取服务
* 通过 HTTP 调用宿主机上的 Agent API 获取真实硬件信息
* 支持 RSA 签名验证和 Challenge-Response 机制
*
* @author lcz
* @mail lcz_0130@163.com
@ -27,13 +37,22 @@ public class HardwareAgentService {
private static final int CONNECT_TIMEOUT = 5000;
private static final int READ_TIMEOUT = 10000;
// Agent 签名密钥 Agent 端保持一致
private static final String SIGN_KEY = "licence-secret-sign-key-trustie";
// RSA 公钥Base64 编码的 DER 格式用于验签
// Agent 端的 RSA 私钥配对
private static final String RSA_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6oAq6wL0FeY3N+jk7+JA/WyGdK3JuM+HYsXKuCH/dE9QE0tWoYiCITt4mDQEBXkeU+d/1FKDzu8y1ofHcn5jE+40zOk2p5bfrqQhfKlwcE/6lLvdsW2muQexnixEur65TGpIttOquhDfO+SLoEXCBjGsaGuY5B/3HfECkTbJY1K5KA+1usVhzrdHFfi0u5ngwWbYCHKjHnh3S/vmZQFM3AgnBL1uVWWODNuK8iLD+Sp1e1WcjIQ42OIgZkEYJwD/qU2aOZQ4RxHw4wfmD1WEQAbcXXGlwXmg5G7xNN2ZMQo6ROHfQWC91DF8kIrA4NCB/oTf4msDGkjKZqSBvKpVxwIDAQAB";
// 心跳检测相关
private static final int HEARTBEAT_INTERVAL_MINUTES = 5;
private static ScheduledExecutorService heartbeatScheduler;
private static final AtomicBoolean heartbeatOk = new AtomicBoolean(true);
// 当前会话 ID
private static String currentSessionId = null;
/**
* Agent API 获取硬件信息
* Agent API 获取硬件信息Challenge-Response 验证模式
*
* @param agentApiUrl Agent API 地址 http://host.docker.internal:8899/api/hardware-info
* @param agentApiUrl Agent API 地址 http://host.docker.internal:8899/api/v1/query
* @return AdditionInfo 对象获取失败返回 null
*/
public static AdditionInfo fetchHardwareInfo(String agentApiUrl) {
@ -45,27 +64,40 @@ public class HardwareAgentService {
log.debug("尝试从 Agent API 获取硬件信息: {}", agentApiUrl);
try {
// 生成或复用 sessionId
if (currentSessionId == null) {
currentSessionId = UUID.randomUUID().toString();
}
// 生成 Challenge: SHA256(randomBytes + timestamp + sessionKey)
long timestamp = System.currentTimeMillis();
String randomBytes = generateRandomString(32);
String challengeData = randomBytes + timestamp + currentSessionId;
String challenge = sha256(challengeData);
// 构建请求
URL url = new URL(agentApiUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(CONNECT_TIMEOUT);
conn.setReadTimeout(READ_TIMEOUT);
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setDoOutput(true);
// 生成签名
String timestamp = String.valueOf(System.currentTimeMillis());
String signature = computeHmacSha256(timestamp, SIGN_KEY);
// 发送 Challenge 请求
String requestBody = String.format(
"{\"action\":\"verify\",\"challenge\":\"%s\",\"sessionId\":\"%s\",\"timestamp\":%d}",
challenge, currentSessionId, timestamp
);
// 添加签名相关的请求头
conn.setRequestProperty("X-Timestamp", timestamp);
conn.setRequestProperty("X-Signature", signature);
conn.getOutputStream().write(requestBody.getBytes(StandardCharsets.UTF_8));
int responseCode = conn.getResponseCode();
if (responseCode != 200) {
log.warn("Agent API 返回错误码: {}, URL: {}", responseCode, agentApiUrl);
// 读取错误响应体
String errorBody = readErrorBody(conn);
log.warn("Agent API 错误响应: {}", errorBody);
heartbeatOk.set(false);
return null;
}
@ -81,7 +113,58 @@ public class HardwareAgentService {
String jsonContent = response.toString();
log.debug("Agent API 响应: {}", jsonContent);
return parseHardwareInfo(jsonContent);
// 解析并验证响应
AdditionInfo info = parseAndVerifyResponse(jsonContent, challenge, currentSessionId);
if (info != null) {
heartbeatOk.set(true);
}
return info;
} catch (Exception e) {
log.error("从 Agent API 获取硬件信息失败: {}", agentApiUrl, e);
heartbeatOk.set(false);
return null;
}
}
/**
* 生成硬件信息用于证书生成阶段
*/
public static AdditionInfo generateHardwareInfo(String agentApiUrl) {
if (StringUtils.isBlank(agentApiUrl)) {
log.debug("Agent API URL 为空,跳过 Agent 获取");
return null;
}
try {
URL url = new URL(agentApiUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(CONNECT_TIMEOUT);
conn.setReadTimeout(READ_TIMEOUT);
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setDoOutput(true);
String requestBody = "{\"action\":\"generate\"}";
conn.getOutputStream().write(requestBody.getBytes(StandardCharsets.UTF_8));
int responseCode = conn.getResponseCode();
if (responseCode != 200) {
log.warn("Agent API 返回错误码: {}, URL: {}", responseCode, agentApiUrl);
return null;
}
StringBuilder response = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
}
return parseHardwareInfo(response.toString());
} catch (Exception e) {
log.error("从 Agent API 获取硬件信息失败: {}", agentApiUrl, e);
@ -90,32 +173,202 @@ public class HardwareAgentService {
}
/**
* 计算 HMAC-SHA256 签名
*
* @param timestamp 时间戳字符串
* @param key 签名密钥
* @return 十六进制编码的签名字符串
* 解析并验证 Agent 响应 RSA 签名验证
*/
private static String computeHmacSha256(String timestamp, String key) {
private static AdditionInfo parseAndVerifyResponse(String jsonContent, String challenge, String sessionId) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(secretKeySpec);
byte[] hmacBytes = mac.doFinal(timestamp.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : hmacBytes) {
sb.append(String.format("%02x", b));
// 提取 signature hardwareInfo
String signature = extractJsonValue(jsonContent, "signature");
if (StringUtils.isBlank(signature)) {
log.error("Agent 响应中缺少签名");
return null;
}
return sb.toString();
// 提取 infoBytesGo Agent 发送的原始 JSON 字节
String infoBytesBase64 = extractJsonValue(jsonContent, "infoBytes");
if (StringUtils.isBlank(infoBytesBase64)) {
log.error("Agent 响应中缺少 infoBytes");
return null;
}
// 解析 hardwareInfo
AdditionInfo info = parseHardwareInfo(jsonContent);
if (info == null) {
return null;
}
// 验证 RSA 签名使用 Agent 发送的原始 JSON 字节
byte[] infoBytes = java.util.Base64.getDecoder().decode(infoBytesBase64);
String infoJson = new String(infoBytes, StandardCharsets.UTF_8);
String dataToVerify = infoJson + challenge + sessionId;
if (!verifyRSASignature(dataToVerify, signature)) {
log.error("RSA 签名验证失败,可能遭受中间人攻击");
return null;
}
log.info("RSA 签名验证通过");
return info;
} catch (Exception e) {
log.error("计算签名失败", e);
return "";
log.error("解析验证 Agent 响应失败", e);
return null;
}
}
/**
* 读取错误响应体
* RSA 签名验证
*/
private static boolean verifyRSASignature(String data, String signatureBase64) {
try {
// 解码公钥
byte[] keyBytes = java.util.Base64.getDecoder().decode(RSA_PUBLIC_KEY);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(keySpec);
// 解码签名
byte[] signatureBytes = java.util.Base64.getDecoder().decode(signatureBase64);
// 验证签名
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initVerify(publicKey);
sig.update(data.getBytes(StandardCharsets.UTF_8));
return sig.verify(signatureBytes);
} catch (Exception e) {
log.error("RSA 签名验证异常", e);
return false;
}
}
/**
* 启动心跳检测
*/
public static void startHeartbeat(String agentApiUrl) {
if (heartbeatScheduler != null) {
heartbeatScheduler.shutdownNow();
}
heartbeatScheduler = Executors.newSingleThreadScheduledExecutor();
heartbeatScheduler.scheduleAtFixedRate(() -> {
try {
// 发送无效 Challenge 检测 Agent 是否正常
String invalidChallenge = "invalid-challenge-" + System.currentTimeMillis();
long timestamp = System.currentTimeMillis();
String sessionId = "heartbeat-check";
URL url = new URL(agentApiUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(CONNECT_TIMEOUT);
conn.setReadTimeout(READ_TIMEOUT);
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setDoOutput(true);
String requestBody = String.format(
"{\"action\":\"verify\",\"challenge\":\"%s\",\"sessionId\":\"%s\",\"timestamp\":%d}",
invalidChallenge, sessionId, timestamp
);
conn.getOutputStream().write(requestBody.getBytes(StandardCharsets.UTF_8));
int responseCode = conn.getResponseCode();
if (responseCode == 200) {
log.debug("Agent 心跳检测正常");
heartbeatOk.set(true);
} else {
log.warn("Agent 心跳检测异常,响应码: {}", responseCode);
heartbeatOk.set(false);
}
} catch (Exception e) {
log.warn("Agent 心跳检测失败: {}", e.getMessage());
heartbeatOk.set(false);
}
}, HEARTBEAT_INTERVAL_MINUTES, HEARTBEAT_INTERVAL_MINUTES, TimeUnit.MINUTES);
log.info("Agent 心跳检测已启动,间隔: {} 分钟", HEARTBEAT_INTERVAL_MINUTES);
}
/**
* 停止心跳检测
*/
public static void stopHeartbeat() {
if (heartbeatScheduler != null) {
heartbeatScheduler.shutdownNow();
heartbeatScheduler = null;
log.info("Agent 心跳检测已停止");
}
}
/**
* 检查心跳状态
*/
public static boolean isHeartbeatOk() {
return heartbeatOk.get();
}
// ==================== 辅助方法 ====================
private static String sha256(String input) {
try {
java.security.MessageDigest digest = java.security.MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
} catch (Exception e) {
return "";
}
}
private static String generateRandomString(int length) {
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
int index = (int) (Math.random() * chars.length());
sb.append(chars.charAt(index));
}
return sb.toString();
}
private static String hardwareInfoToJson(AdditionInfo info) {
try {
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
return mapper.writeValueAsString(info);
} catch (Exception e) {
log.error("转换 HardwareInfo 为 JSON 失败", e);
return "";
}
}
private static String extractJsonValue(String content, String key) {
// 支持数组格式: "key":["value1","value2"] 和字符串格式: "key":"value"
// 先尝试匹配数组格式 "key":["value"]
String arrayPattern = "\"" + key + "\"\\s*:\\s*(\\[[^\\]]+\\])";
java.util.regex.Pattern p1 = java.util.regex.Pattern.compile(arrayPattern);
java.util.regex.Matcher m1 = p1.matcher(content);
if (m1.find()) {
return m1.group(1);
}
// 再尝试匹配字符串格式 "key":"value"
String strPattern = "\"" + key + "\"\\s*:\\s*\"([^\"]+)\"";
java.util.regex.Pattern p2 = java.util.regex.Pattern.compile(strPattern);
java.util.regex.Matcher m2 = p2.matcher(content);
if (m2.find()) {
return m2.group(1);
}
return "";
}
private static String readErrorBody(HttpURLConnection conn) {
try {
if (conn.getErrorStream() != null) {
@ -135,9 +388,6 @@ public class HardwareAgentService {
return "";
}
/**
* 解析 Agent API 返回的 JSON 数据
*/
private static AdditionInfo parseHardwareInfo(String jsonContent) {
if (StringUtils.isBlank(jsonContent)) {
return null;
@ -145,7 +395,6 @@ public class HardwareAgentService {
AdditionInfo info = new AdditionInfo();
// 解析 macAddress支持数组格式 ["mac1","mac2"] 或逗号分隔 "mac1,mac2"
String macStr = extractJsonValue(jsonContent, "macAddress");
if (!StringUtils.isBlank(macStr)) {
if (macStr.startsWith("[")) {
@ -155,7 +404,6 @@ public class HardwareAgentService {
}
}
// 解析 ipAddress
String ipStr = extractJsonValue(jsonContent, "ipAddress");
if (!StringUtils.isBlank(ipStr)) {
if (ipStr.startsWith("[")) {
@ -165,19 +413,16 @@ public class HardwareAgentService {
}
}
// 解析 cpuSerial
String cpuSerial = extractJsonValue(jsonContent, "cpuSerial");
if (!StringUtils.isBlank(cpuSerial)) {
info.setCpuSerial(cpuSerial);
}
// 解析 mainBoardSerial
String boardSerial = extractJsonValue(jsonContent, "mainBoardSerial");
if (!StringUtils.isBlank(boardSerial)) {
info.setMainBoardSerial(boardSerial);
}
// 检查是否读取到了任何有效信息
if ((info.getMacAddress() == null || info.getMacAddress().isEmpty())
&& (info.getIpAddress() == null || info.getIpAddress().isEmpty())
&& StringUtils.isBlank(info.getCpuSerial())
@ -192,29 +437,6 @@ public class HardwareAgentService {
return info;
}
/**
* 简单 JSON 值提取
*/
private static String extractJsonValue(String content, String key) {
// 匹配 "key":"value" "key": "value" "key": ["array"]
String pattern1 = "\"" + key + "\"\\s*:\\s*\"([^\"]*)\"";
java.util.regex.Pattern p1 = java.util.regex.Pattern.compile(pattern1);
java.util.regex.Matcher m1 = p1.matcher(content);
if (m1.find()) {
return m1.group(1);
}
// 匹配 "key": ["array"] "key": [value]
String pattern2 = "\"" + key + "\"\\s*:\\s*(\\[[^\\]]*\\])";
java.util.regex.Pattern p2 = java.util.regex.Pattern.compile(pattern2);
java.util.regex.Matcher m2 = p2.matcher(content);
if (m2.find()) {
return m2.group(1);
}
return "";
}
private static List<String> parseCommaSeparatedList(String value) {
List<String> list = new ArrayList<>();
if (!StringUtils.isBlank(value)) {

View File

@ -56,17 +56,10 @@ public class CustomLicenseManager extends LicenseManager {
}
}
/**
* 获取当前配置的硬件信息配置文件路径
*/
public static String getHardwareConfigPath() {
return hardwareConfigPath;
}
/**
* 设置宿主机 Agent API 地址
*
* @param url Agent API 地址 http://host.docker.internal:8899/api/hardware-info
* @param url Agent API 地址
*/
public static void setAgentApiUrl(String url) {
CustomLicenseManager.agentApiUrl = url;
@ -75,12 +68,6 @@ public class CustomLicenseManager extends LicenseManager {
}
}
/**
* 获取当前配置的 Agent API 地址
*/
public static String getAgentApiUrl() {
return agentApiUrl;
}
/**
* 构造函数初始化许可管理器