Files
opsx af0298e994 feat(license): dual-path opsx-licence client alongside HMAC
Wire LIC-/LICOFF- activate/verify/heartbeat to licence.rlwj.cn/api,
keep legacy HMAC accepted with deprecation banner, add instance_id
fingerprint and licensing docs (product lokistack still needs admin create).
2026-11-01 02:55:37 +08:00

75 lines
2.3 KiB
Go

package services
import (
"crypto/ed25519"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"strings"
"time"
)
type offlineClaims struct {
V int `json:"v"`
ProductCode string `json:"pid"`
DeviceID string `json:"did"`
Email string `json:"email,omitempty"`
PlanType string `json:"pt"`
Iat int64 `json:"iat"`
Exp int64 `json:"exp"`
MaxDevices int `json:"md"`
}
func verifyOfflineSerial(serial, deviceID string) (*offlineClaims, error) {
trimmed := strings.TrimSpace(serial)
body, ok := strings.CutPrefix(trimmed, "LICOFF-1.")
if !ok {
return nil, fmt.Errorf("不是合法的离线序列号(需 LICOFF-1. 前缀)")
}
idx := strings.LastIndex(body, ".")
if idx <= 0 {
return nil, fmt.Errorf("离线序列号格式错误")
}
payloadB64, sigB64 := body[:idx], body[idx+1:]
sigBytes, err := base64.RawURLEncoding.DecodeString(sigB64)
if err != nil {
return nil, fmt.Errorf("离线序列号签名无法解码")
}
pub, err := hex.DecodeString(offlineLicensePublicKeyHex)
if err != nil || len(pub) != ed25519.PublicKeySize {
return nil, fmt.Errorf("离线验签公钥配置错误")
}
if !ed25519.Verify(ed25519.PublicKey(pub), []byte(payloadB64), sigBytes) {
return nil, fmt.Errorf("离线序列号签名校验失败")
}
payload, err := base64.RawURLEncoding.DecodeString(payloadB64)
if err != nil {
return nil, fmt.Errorf("离线序列号载荷无法解码")
}
var claims offlineClaims
if err := json.Unmarshal(payload, &claims); err != nil {
return nil, fmt.Errorf("离线序列号载荷无法解析")
}
if claims.V != 0 && claims.V != 1 {
return nil, fmt.Errorf("不支持的离线证版本")
}
if claims.ProductCode != licenceProductCode {
return nil, fmt.Errorf("离线证产品不匹配(需要 %s)", licenceProductCode)
}
if claims.DeviceID != deviceID {
return nil, fmt.Errorf("离线证与当前实例不匹配")
}
if claims.PlanType != "trial" && claims.PlanType != "yearly" {
return nil, fmt.Errorf("离线证计划无效(仅体验/包年)")
}
now := time.Now().Unix()
if claims.Exp <= now {
return nil, fmt.Errorf("离线许可证已过期")
}
if claims.Exp-claims.Iat > 365*86400+3600 {
return nil, fmt.Errorf("离线许可证有效期非法")
}
return &claims, nil
}