licensing-cotton/internal/handlers/license.go
2025-11-01 15:19:24 +08:00

182 lines
4.7 KiB
Go

package handlers
import (
"crypto/ecdsa"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"math/big"
"net/http"
"time"
"licensing-cotton/internal/database"
"licensing-cotton/internal/logger"
"licensing-cotton/internal/security"
)
// License 结构
type License struct {
DeviceID string `json:"device_id"`
Expiration string `json:"expiration"`
SignDate string `json:"sign_date"`
Signature string `json:"signature"`
}
type LicenseWithoutSign struct {
DeviceID string `json:"device_id"`
Expiration string `json:"expiration"`
SignDate string `json:"sign_date"`
}
// 签发License (仅管理员)
func HandleSignLicense(w http.ResponseWriter, r *http.Request) {
if !isAdminRequest(w, r) {
return
}
var req struct {
DeviceID string `json:"device_id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "解析请求失败", http.StatusBadRequest)
return
}
// 查设备
var expiration time.Time
err := database.DB.QueryRow(`SELECT expiration FROM devices WHERE device_id=?`, req.DeviceID).
Scan(&expiration)
if err != nil {
http.Error(w, "设备不存在", http.StatusNotFound)
return
}
// 构造 license
lic := License{
DeviceID: req.DeviceID,
Expiration: expiration.Format(time.RFC3339),
}
// 序列化并哈希
dataBytes, _ := json.Marshal(lic)
hash := sha256.Sum256(dataBytes)
// 签名
priv, _, _ := security.GetKeys()
rSig, sSig, err := ecdsa.Sign(nil, priv, hash[:])
if err != nil {
http.Error(w, "ECDSA签名失败", http.StatusInternalServerError)
return
}
sigBytes := append(rSig.Bytes(), sSig.Bytes()...)
lic.Signature = base64.StdEncoding.EncodeToString(sigBytes)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(lic)
}
// 验证License (可供服务端或第三方调用)
func HandleVerifyLicense(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "只允许POST", http.StatusMethodNotAllowed)
return
}
var lic License
if err := json.NewDecoder(r.Body).Decode(&lic); err != nil {
http.Error(w, "解析License失败", http.StatusBadRequest)
return
}
sigStr := lic.Signature
lic.Signature = ""
dataBytes, _ := json.Marshal(lic)
hash := sha256.Sum256(dataBytes)
sigData, err := base64.StdEncoding.DecodeString(sigStr)
if err != nil {
http.Error(w, "Signature base64解码失败", http.StatusBadRequest)
return
}
half := len(sigData) / 2
rSig := new(big.Int).SetBytes(sigData[:half])
sSig := new(big.Int).SetBytes(sigData[half:])
_, pub, _ := security.GetKeys()
if !ecdsa.Verify(pub, hash[:], rSig, sSig) {
http.Error(w, "签名验证失败", http.StatusUnauthorized)
return
}
// 检查是否过期
exp, err := time.Parse(time.RFC3339, lic.Expiration)
if err != nil {
http.Error(w, "时间格式错误", http.StatusBadRequest)
return
}
if time.Now().After(exp) {
http.Error(w, "License已过期", http.StatusUnauthorized)
return
}
// 可进一步检查数据库中的device_id与expiration是否匹配
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"message": "License有效"}`))
}
func SignLicense(deviceID string, expirationTime time.Time) (License, error) {
// 1. 准备待签名数据
licWithoutSign := LicenseWithoutSign{
DeviceID: deviceID,
Expiration: expirationTime.Format(time.RFC3339),
SignDate: time.Now().Format(time.RFC3339),
}
// 2. 序列化为 JSON
licBytes, err := json.Marshal(licWithoutSign)
if err != nil {
return License{}, fmt.Errorf("序列化License失败: %w", err)
}
// 3. 调用你的 Ed25519Sign 函数签名
signature, signErr := security.Ed25519Sign(licBytes) // <-- 这里是你已有的签名函数
if signErr != nil {
return License{}, fmt.Errorf("签名失败: %w", signErr)
}
// 4. 封装最终 License
lic := License{
DeviceID: deviceID,
Expiration: licWithoutSign.Expiration,
SignDate: licWithoutSign.SignDate,
Signature: base64.StdEncoding.EncodeToString(signature),
}
logger.Info("签发了 License: device=%s, expiration=%s", deviceID, expirationTime.Format(time.RFC3339))
return lic, nil
}
// HandleGetPublicKey 获取 Ed25519 公钥
func HandleGetPublicKey(w http.ResponseWriter, r *http.Request) {
sshKey, err := security.GetEd25519PublicKeySSH()
if err != nil {
http.Error(w, "获取公钥失败: "+err.Error(), http.StatusInternalServerError)
return
}
base64Key, err := security.GetEd25519PublicKeyBase64()
if err != nil {
http.Error(w, "获取公钥失败: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"ssh_format": sshKey,
"base64": base64Key,
"algorithm": "Ed25519",
"usage": "用于验证 License 签名",
})
}