如何防止公钥被替换和许可证伪造?License3j防欺诈安全实践清单
【免费下载链接】License3jFree Licence Management Library项目地址: https://gitcode.com/gh_mirrors/li/License3j
License3j 是一款免费的 Java 许可证管理库,它能创建、读取并验证带电子签名的许可证文件。但很多新手只做了签名,却没注意到:如果公钥本身能被替换,整个签名验证就会形同虚设。本文以 License3j 的源码实践为线索,给你一份防止公钥替换和许可证伪造的完整安全清单。
先弄清:为什么你的公钥会被人替换?
License3j 的防伪造逻辑建立在非对称加密上:许可证用只有你才持有的私钥签名,客户端用公钥验证签名。没有私钥,攻击者数学上无法伪造合法签名——这正是签名提供的"不可抵赖性"。
但这里有个常被忽视的漏洞:
- 公钥如果以资源文件形式放在 jar/war 包里,格式又足够简单,任何新手都能用 ZIP 工具把 key 文件换成自己的,然后给自己发许可证
- 公钥被替换后,攻击者用自己生成的"新公钥"去验证伪造的许可证,
isOK()照样返回true
项目里专门有一篇 fraud.md 讨论这个问题,核心观点是:不存在 100% 安全的许可证方案,目标应该是让破解的成本高到"没有动机去破解"。
防欺诈实践清单
1. 把公钥直接嵌入 Java 源码,而不是放成文件
这是最重要的一条。不要通过 KeyPairReader.java 从文件加载公钥,而是把公钥的字节数组以(byte)0x52, 0x53, 0x41...的形式硬编码进你的源代码。
这样公钥会被编译进.class文件——攻击者想换公钥,就必须同时反编译并修改你的业务代码类,难度陡然上升。README 中给出的推荐用法就是:
// 公钥编码后直接写进应用 private final byte[] public_key = { (byte)0x52, (byte)0x53, (byte)0x41, ... }; if (!license.isOK(public_key)) { return; // 签名校验失败,拒绝运行 }注意:自 3.0.0 起,官方建议嵌入完整公钥而非仅嵌入公钥摘要值(digest 校验 API 已不再支持),这也是 fraud.md 中明确推荐的升级点。
2. 若必须从文件加载公钥,用摘要做二次校验
某些场景下公钥确实需要动态加载,此时可以:
- 预先计算公钥的SHA-512 摘要并写入源码
- 加载公钥后比对摘要,不一致立即拒绝
即使文件被替换,摘要对不上也会立刻暴露。可再对摘要字符串做轻度混淆,提高替换成本。
3. 用isOK()做签名完整性验证,别只检查文件存在
读取许可证文件本身不会检查签名——无论签名是否存在、是否被破坏,LicenseReader 都会把它读进来。真正的验证在 License.java 的isOK()里:
- 取出被签名时的消息摘要算法(
signatureDigest特征,通常是 SHA-512) - 把许可证去掉签名特征后重新序列化为二进制
- 计算摘要,再用公钥解密签名里的密文摘要
- 两者一致才返回
true
任何一处特征被改过(比如把过期日期expiryDate往后挪一年),校验必然失败。所以检查流程应该是:读文件 →isOK(public_key)→ 校验通过后再使用特征值。
4. 私钥永远不要出现在客户端环境
KeyPairReader.java 的类注释说得很直白:验证许可证的环境不应该有私钥。签名和发许可证是你(软件提供方)在自己环境里做的事,客户端只做验证。一旦私钥泄露,整个签名体系作废——请把它和源码分开保管。
5. 用硬件绑定增加"拷机"成本(但别做成硬拦截)
用 HardwareBinder.java 可以基于网卡 MAC、主机名、系统架构计算出一台机器的 UUID,再把许可证绑定到该机器上。这样许可证拷到别的机器就失效了。
官方推荐的姿势是警告而非强拒(源码注释里有详细说明):网卡坏了没时间换新许可证时,强拒会把正常用户也挡在门外。建议检测到 UUID 不匹配时记录日志、提示用户,而不是直接停机。
6. 远程吊销:把"撤销权"握在自己手里
签名验证只保证许可证没被篡改,但无法阻止你"卖出去之后想收回"。RevocableLicense.java 提供了解法:
- 在许可证里写入
revocationUrl,URL 中可以带${licenseId}占位符,会自动替换成许可证 ID 或指纹 - 运行时
isRevoked()会去访问该 URL:返回 200 表示未吊销,返回 404 表示已吊销 - 还支持"服务器不可达时默认视为已吊销"的严格模式
简单理解:在服务器上按 UUID 传一个空文件,程序发现文件被删(404)就知道许可证被吊销了。
7. 轻量场景:SimpleLicense 的密钥保护
如果你的产品只需要一个短许可证码(如DCWI3U-6RDTB8-EBMPTJ-TVURQ7),SimpleLicense.java 用"秘密 + 用户值"的 SHA-256 哈希生成码。它的防伪造关键在于:secret 必须藏在代码里,绝不能公开给用户。码本身不含任何用户信息,是纯哈希截断,无法逆向——但 secret 一旦泄露,任何人都能批量生成合法码。
一份可执行的自查清单
| # | 检查项 | 参考 |
|---|---|---|
| 1 | 公钥是否嵌入.class,而非 jar 内资源文件? | fraud.md |
| 2 | 动态加载公钥时是否做了 SHA-512 摘要比对? | KeyPairReader.java |
| 3 | 是否每次启动都调用isOK()验证签名? | License.java |
| 4 | 私钥是否只存在于你的签发环境? | KeyPairReader.java |
| 5 | 硬件绑定是否按"警告优先"实现? | HardwareBinder.java |
| 6 | 是否配置了远程吊销 URL? | RevocableLicense.java |
最后说句实话
fraud.md 开篇就提醒:License3j 也可能被攻破,没有绝对安全的许可证方案。真正的安全目标不是"不可破解",而是把破解成本推到合理收益之上,同时通过电子签名拿到不可抵赖的证据链——谁篡改了许可证,就无法狡辩"以为是官方发的"。按上面 7 条清单逐项落实,你的 Java 产品就能获得相当扎实的防欺诈保护。
【免费下载链接】License3jFree Licence Management Library项目地址: https://gitcode.com/gh_mirrors/li/License3j
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考