LicenseManager 是一个功能完整的许可证管理系统,用于生成、验证和管理软件许可证。v1.2.0 版本新增 Ed25519 非对称签名(v2 许可证格式),客户端只需内嵌公钥即可验证许可证,从根本上解决了对称加密下"验证密钥即可伪造许可证"的问题。
- 🔏 Ed25519 签名(v2,推荐):私钥签发、公钥验证,客户端无法伪造许可证
- 🔐 AES加密(v1,兼容):支持16/24/32字节密钥,使用随机IV
- 📱 设备绑定:基于设备UUID的许可证绑定机制
- ⏰ 到期管理:支持许可证过期时间设置和剩余天数计算
- 🛠️ 统一工具:一个命令行工具包含所有功能(create/verify/show/checkuuid/genkeys)
- 📊 多种输出:支持文本和JSON格式输出
- 🔍 详细信息:完整的许可证信息查看功能
# 克隆项目
git clone https://github.com/xinjiayu/LicenseManager.git
cd LicenseManager
# 编译统一工具
go build -o licensemanager ./cmd/licensemanager
# 或者直接运行
go run ./cmd/licensemanager help# 1. 生成 Ed25519 密钥对(一次性操作,私钥妥善保管)
./licensemanager genkeys -o ./keys
# 2. 获取设备UUID(发给签发方)
./licensemanager checkuuid
# 3. 签发许可证(签发方持有私钥)
./licensemanager create -f config.json -privkey keys/license_private_key.pem -o app.lic
# 4. 验证许可证(客户端只需公钥)
./licensemanager verify -f app.lic -pubkey keys/license_public_key.pem
# 5. 查看许可证信息
./licensemanager show -f app.lic -pubkey keys/license_public_key.pem -format json./licensemanager create -f config.json -k "your-16-byte-key"
./licensemanager verify -f app.lic -k "your-16-byte-key"
./licensemanager show -f app.lic -k "your-16-byte-key"v1 使用 AES 对称加密:客户端要验证许可证就必须内嵌解密密钥,而持有解密密钥就意味着可以自己加密生成任意许可证——逆向客户端二进制拿到密钥后即可无限伪造。v2 使用 Ed25519 签名:私钥只存在于签发方,客户端仅内嵌公钥;公钥只能验证、不能签发,任何对许可证内容的篡改(改到期时间、换设备UUID、换应用名)都会导致验签失败。
{
"version": 2,
"alg": "Ed25519",
"issued_at": "2026-09-04T00:23:02+08:00",
"payload": "base64编码的授权信息JSON",
"signature": "base64编码的Ed25519签名(覆盖payload解码后的原始字节)"
}说明:v2 格式的授权内容是"签名保护的明文"而非加密密文。许可证需要的是防篡改而非保密——被授权设备上本就能读到这些信息。如确有保密需求,可在签名前自行对 payload 做一层加密。
- 私钥(
license_private_key.pem):只在签发许可证的机器上保存,权限 0600,可离线保管 - 公钥(
license_public_key.pem):内嵌到客户端程序(硬编码常量或随二进制分发) - 密钥遗失/泄露时的处理:换新密钥对重新签发,客户端升级时更新内嵌公钥
- 通用许可证(不绑定设备,适合内嵌试用授权、批量延期):签发用
EncryptLicSignedUniversal或 CLIcreate -universal(须与-privkey组合)。库层安全底线:禁止签发永久通用许可证,LimitedTime 必填;有效期上限等产品规则由接入方自行叠加。 - 仅验签解析:
ParseSignedLicenseFile(文件)/ParseSignedLicenseContent(字节,适配-ldflags内嵌授权)只做 Ed25519 验签与内容解析,不做设备/到期/回拨判定,供接入方实现自定义分级(如通用授权跳过设备绑定)。 - 回拨状态路径:
SetClockStatePath(path)在进程初始化期设置防回拨状态文件路径(容器部署建议指向持久卷;传空串显式禁用该机制)。
package main
import (
"fmt"
"log"
"github.com/xinjiayu/LicenseManager"
)
func main() {
// 签发端:生成密钥对(一次性)
privatePEM, publicPEM, err := LicenseManager.GenerateSigningKeyPair()
if err != nil {
log.Fatal(err)
}
// privatePEM 妥善保存,publicPEM 内嵌到客户端 ...
// 签发端:生成签名许可证
if err := LicenseManager.EncryptLicSigned("config.json", "app.lic", privatePEM); err != nil {
log.Fatal(err)
}
// 客户端:验证签名许可证(只需公钥)
ok, err := LicenseManager.ValidAppLicSigned("app.lic", publicPEM)
if err != nil || !ok {
log.Fatalf("许可证无效: %v", err)
}
// 客户端:获取详细信息
info, err := LicenseManager.GetLicenseInfoSigned("app.lic", publicPEM)
if err != nil {
log.Fatal(err)
}
fmt.Printf("应用: %s, 状态: %s, 剩余: %d天\n", info.AppName, info.Status, info.DaysRemaining)
}从JSON配置文件生成许可证文件,推荐使用 v2 签名格式。
# v2 Ed25519签名格式(推荐)
licensemanager create -f config.json -privkey license_private_key.pem
# v1 AES加密格式(已不推荐,-k 必须显式传入16/24/32字节密钥)
licensemanager create -f config.json -k "your-32-byte-secret-key" -o production.lic配置文件格式:
{
"AppName": "YourApp",
"AppCompany": "Your Company Ltd",
"AppUUID": "app-unique-identifier",
"ObjUUID": "device-uuid-from-checkuuid-command",
"AuthorizedName": "客户公司名称",
"LimitedTime": "20261231",
"LicenseID": "LIC202501001",
"LicenseQuantity": 100
}验证许可证文件的有效性,包括:
- 签名验证(v2)或解密校验(v1)
- 设备UUID匹配验证
- 过期时间检查(按UTC)
- 系统时间回拨检测
# v2 签名格式(推荐)
licensemanager verify -f app.lic -pubkey license_public_key.pem
# v1 AES格式(已不推荐)
licensemanager verify -f app.lic -k "your-16-byte-key"获取许可证的详细信息,支持文本和JSON格式。
# v2 签名格式(推荐)
licensemanager show -f app.lic -pubkey license_public_key.pem
# JSON格式(适合API集成)
licensemanager show -f app.lic -pubkey license_public_key.pem -format jsonJSON输出示例:
{
"app_name": "YourApp",
"app_company": "Your Company Ltd",
"authorized_name": "客户公司名称",
"limited_time": "20261231",
"license_id": "LIC202501001",
"license_quantity": 100,
"status": "valid",
"days_remaining": 365,
"is_valid": true,
"error_message": ""
}获取当前设备的唯一标识符,用于生成设备绑定的许可证。
licensemanager checkuuid
# 输出: F6235A40-C9E2-5681-B236-ED9C4C15E58D生成 Ed25519 密钥对,用于 v2 签名格式许可证的签发与验证。
licensemanager genkeys -o ./keys
# 输出:
# keys/license_private_key.pem 私钥(签发用,权限0600,妥善保管)
# keys/license_public_key.pem 公钥(内嵌到客户端验证)以下为 v1 AES 格式的 API。新接入请使用上文 v2 签名 API(
ValidAppLicSigned等), v1 API 仅为兼容存量许可证保留。
package main
import (
"fmt"
"github.com/xinjiayu/LicenseManager"
)
func main() {
// 获取许可证信息
info, err := LicenseManager.GetLicenseInfo("app.lic", "your-16-byte-key")
if err != nil {
panic(err)
}
if info.IsValid {
fmt.Printf("应用: %s, 剩余: %d天\n", info.AppName, info.DaysRemaining)
} else {
fmt.Printf("许可证无效: %s\n", info.ErrorMessage)
}
// 验证许可证
isValid, err := LicenseManager.ValidAppLic("app.lic", "your-16-byte-key")
if err != nil {
fmt.Printf("验证失败: %v\n", err)
return
}
fmt.Printf("许可证状态: %v\n", isValid)
}package main
import (
"encoding/json"
"net/http"
"github.com/xinjiayu/LicenseManager"
)
func licenseHandler(w http.ResponseWriter, r *http.Request) {
info, err := LicenseManager.GetLicenseInfo("app.lic", "secret-key")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(info)
}
func main() {
http.HandleFunc("/license", licenseHandler)
http.ListenAndServe(":8080", nil)
}- Ed25519签名(v2):私钥签发、公钥验证,客户端无法伪造或篡改许可证
- 时间回拨防护:本地记录每个许可证最后通过验证的时间(只增不减),系统时间被明显回拨(超过48小时)时拒绝验证;状态文件损坏不阻塞验证
- 设备绑定:许可证与特定设备UUID绑定
- 到期管理:到期比较与剩余天数统一按UTC计算,避免跨时区偏差
- 完整性检查:签名验证(v2)与PKCS7填充校验(v1)确保数据完整性
- AES加密(v1,已弃用):随机IV;因客户端内嵌的解密密钥可被用于伪造许可证,v1仅保留用于兼容存量许可证,新签发请使用v2
- 防回拨是尽力而为:状态文件(
~/.licensemanager/clock_state.json)可被删除;从未在本机验证过的过期许可证回拨后仍可通过。高价值场景建议配合在线激活。 - machineid可被伪造:Windows注册表/部分平台文件型machine-id可被篡改或克隆。如需更强设备指纹,可自行组合硬件序列号、MAC等,或在
AppLicenseInfo.ObjUUID中使用自采指纹。 - 公钥分发:内嵌在客户端二进制中的公钥可能被连锅端替换(攻击者同时替换许可证与公钥常量并重打包);混淆/加壳可提高门槛,根因解决需在线验证。
LicenseManager/
├── cmd/
│ └── licensemanager/ # 统一命令行工具
│ ├── main.go # 主程序入口
│ └── README.md # 工具使用说明
├── utils/
│ ├── aes.go # AES加密解密工具
│ └── aes_test.go # AES单元测试
├── examples/
│ └── license_info/ # 使用示例
├── lic.go # 核心许可证功能(v1 AES格式)
├── sign.go # Ed25519签名功能(v2签名格式)
├── clock.go # 系统时间回拨防护
├── lic_test.go # v1单元测试
├── lic_sign_test.go # v2签名单元测试
├── clock_test.go # 回拨防护单元测试
├── go.mod # Go模块定义
├── LICENSE # MIT许可证
└── README.md # 本文档
# 运行全部单元测试(自包含,自动适配本机UUID,无需预先准备文件)
go test ./... -v
# 测试完整工作流程
go build -o licensemanager ./cmd/licensemanager
# 生成签名密钥对并测试v2签名流程
./licensemanager genkeys -o ./keys
./licensemanager create -f config.json -privkey keys/license_private_key.pem -o app.lic
./licensemanager verify -f app.lic -pubkey keys/license_public_key.pem
./licensemanager show -f app.lic -pubkey keys/license_public_key.pem -format json
# 测试v1 AES流程(兼容旧版,需显式传密钥)
./licensemanager create -f config.json -k "your-16-byte-key" -o v1.lic
./licensemanager verify -f v1.lic -k "your-16-byte-key"- API兼容:原有函数保持不变;
EncryptLic已标记 Deprecated,请改用EncryptLicToFile - 文件兼容:v1许可证文件(含旧格式固定IV)仍可验证;
LicenseDisplayInfo移除了内部字段EncryptSalt,LicenseID/LicenseQuantity已正式收进AppLicenseInfo - CLI变化:
-k不再有默认弱密钥,必须显式传入;错误信息统一输出到 stderr;genkeys默认拒绝覆盖已存在的密钥文件(需-force确认) - 错误判定:验证失败的错误文案有调整(如"授权文件不适用于此设备"改为由
ErrDeviceMismatch包装的新文案);请勿对错误做字符串匹配,改用errors.Is判定哨兵错误(ErrLicenseExpired/ErrDeviceMismatch/ErrInvalidSignature/ErrClockRollback/ErrUniversalRequiresExpiry等) - 行为变更提示:v1/v2 签发入口(
EncryptLicToFile/EncryptLicSigned/EncryptLicSignedUniversal)现要求LimitedTime非空时必须为合法YYYYMMDD日期(此前允许任意非空字符串)。验证端早已按此标准拒绝,该变更只把必然无效的签发拦在源头;存量调用方如传入过非法日期格式,会在签发时收到明确报错 - 推荐迁移:新签发许可证一律使用 v2 Ed25519 签名格式(
genkeys+-privkey/-pubkey),客户端只需内嵌公钥
# v1.0.x → v1.1.0+
cmd/CreateLic/main.go → licensemanager create
cmd/VerifyLic/main.go → licensemanager verify
cmd/ShowLicenseInfo/main.go → licensemanager show
cmd/checkuuid/main.go → licensemanager checkuuid欢迎提交Issue和Pull Request!
本项目使用MIT许可证。详见LICENSE文件。