【实战拆解】自制证书+power插件,彻底告别“改一次数据写一条规则”
1. 为什么还要写这一篇?
在上一篇《无需私钥也能通过 RSA 验签?power 插件原理与规则构造全记录》里,我们做到了“不用私钥也能通过 RSA 验签”:只要在 power.conf 里配一条 EQUAL 规则,就能让修改后的激活码绕过阶段 2 的数据签名校验。
但上一篇的方案有个明显的代价:每次修改数据,都要重新算一次规则。
简单回顾一下上一篇的流程:
- 修改
licensePartBase64里的日期 - 重新计算 SHA-1 哈希
- 构造
DigestInfo - 做 PKCS#1 填充
- 把结果写成一条
EQUAL, x, y, z -> fakeResult规则
如果你只是想改一次日期,写一条规则,完全能忍。
但如果你的需求是反复签发不同数据的激活码呢?每次都要重复“提取签名值 → 计算 DigestInfo → 组装规则”这条链,弄个三五次你估计就烦了。
于是问题就变成了:能不能一劳永逸?
能,而且解法比你想的要直接:自己当 CA。
这次我们直接回到验签流程的起点:阶段 1:证书链校验。
如果我们能做一张自己的证书,然后用配套的私钥去签名数据,那就不需要 power 规则来“伪造验签结果”了:我们本身就是合法的签名者。
但这里还有一个坎:我们的自制证书不在官方根 CA 的信任链里。验签程序拿到我们的证书,往上追溯,发现根证书不是它信任的那个,会直接拒绝。
怎么过这个坎?相信你已经猜到了:用一条 power 规则,让官方根证书的验签“短路”。
所以本篇的逻辑就是:
自制一张证书 → 用自制证书的私钥签名数据 → 用一条 power 规则让官方根证书“相信”我们的证书 → 从此不再为每一次数据修改写规则。
废话不多说,直接开干。
2. 先看官方证书长啥样
动手之前,得先知道我们模仿的对象长什么样。
在 CheckLicense 类里,证书加载的逻辑大概是这样:
CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509Certificate cert = (X509Certificate) cf.generateCertificate(
new ByteArrayInputStream(certBase64.getBytes())
);
我加了一行打印,把证书内容直接打到控制台:
System.out.println(cert);
跑起来之后,控制台会输出两张证书:一张是根证书,一张是终端实体证书。
根证书(Root CA):
[
Version: V3
Subject: CN=JetProfile CA
Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11
Key: Sun RSA public key, 4096 bits
modulus: 860106576952879101192782278876319243486...............
public exponent: 65537
Validity: [From: Fri Oct 02 19:00:56 MYT 2015,
To: Tue Oct 24 19:00:56 MYT 2045]
Issuer: CN=JetProfile CA
SerialNumber: [ d26cb183 b28379e1]
]
终端实体证书(End-Entity):
[
Version: V3
Subject: CN=prod2y-from-20240920
Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11
Key: Sun RSA public key, 2048 bits
modulus: 23642313805943905243041450226779686298................
public exponent: 65537
Validity: [From: Fri Sep 20 20:11:27 MYT 2024,
To: Tue Sep 22 20:11:27 MYT 2026]
Issuer: CN=JetProfile CA
SerialNumber: [ 11]
]
两个关键信息:
- 根证书是自签的,
Subject和Issuer都是CN=JetProfile CA,有效期直接拉到 2045 年。 - 终端证书由根证书签发,有效期两年,2048 位 RSA。
我们要做的就是仿造这个结构,生成一套自己的证书。
3. 用 keytool 做一套证书
JDK 自带的 keytool 就够用了,不用装别的工具。
整个流程分三步:生成根证书 → 生成终端证书 → 用根证书签终端证书。
第 1 步:生成根证书(自签名)
keytool -genkeypair -alias root -keyalg RSA -keysize 4096 \
-sigalg SHA256withRSA \
-dname "CN=JetProfile CA" \
-ext BC=CA:true \
-ext KU=keyCertSign,cRLSign \
-validity 730 \
-keypass 123456 \
-keystore root.jks \
-storepass 123456 \
-storetype jks
参数拆开看:
| 参数 | 含义 |
|---|---|
-alias root | 别名,后面引用方便 |
-keysize 4096 | 根证书用 4096 位,跟官方一致 |
-ext BC=CA:true | 标记这是一张 CA 证书 |
-ext KU=keyCertSign,cRLSign | 证书用途:签发证书和吊销列表 |
然后导出根证书的 PEM 文件,后面要用:
keytool -exportcert -alias root -keystore root.jks \
-storepass 123456 -storetype jks -rfc > root.pem
第 2 步:生成终端证书
keytool -genkeypair -alias ca -keyalg RSA -keysize 2048 \
-sigalg SHA256withRSA \
-dname "CN=prod2y-from-20240920" \
-validity 730 \
-keypass 123456 \
-keystore ca.jks \
-storepass 123456 \
-storetype jks
注意 -dname 跟官方证书的 Subject 保持一致:CN=prod2y-from-20240920。
这一步生成的终端证书还是自签的,还不是由根证书签发的。下一步用根证书的私钥去签它。
第 3 步:用根证书签署终端证书
keytool -certreq -sigalg SHA256withRSA -alias ca \
-keystore ca.jks -storepass 123456 \
| keytool -gencert -sigalg SHA256withRSA -alias root \
-ext BC=CA:false \
-ext EKU=serverAuth \
-ext KU=digitalSignature,keyEncipherment \
-keystore root.jks -storepass 123456 -rfc > ca.pem
然后导入信任链:
# 先把根证书导进去,让终端证书信任它
keytool -importcert -noprompt -alias root \
-keystore ca.jks -storepass 123456 -file root.pem
# 再把由根签过的终端证书导进去,覆盖原来的自签版本
keytool -importcert -alias ca \
-keystore ca.jks -storepass 123456 -file ca.pem
验证一下:
keytool -printcert -v -file root.pem
keytool -printcert -v -file ca.pem
keytool -list -v -storepass 123456 -keystore root.jks -alias root
keytool -list -v -storepass 123456 -keystore ca.jks -alias ca
到这里,我们就有两套证书了:
| 证书 | 密钥库 | 是否含私钥 | 用途 |
|---|---|---|---|
| 根证书 | root.jks | ✅ | 签其他证书 |
| 终端证书 | ca.jks | ✅ | 签激活码 |
证书准备好了,但验签程序还不认它:因为它的根不是官方根 CA。
接下来解决这个问题。
4. 让自制证书“骗过”官方根证书的验证
验签程序在做证书链校验的时候,逻辑是这样的:
- 拿到终端证书,看到
Issuer是CN=JetProfile CA - 去找
CN=JetProfile CA的根证书 - 用根证书的公钥去验终端证书的签名
- 验签通过 → 证书合法
我们的自制证书,Issuer 也是 CN=JetProfile CA,但验签程序用的是官方根证书的公钥,不是我们自己的公钥。
官方根证书的公钥和我们自制证书的私钥不是一对,验签必然失败。
这时候 power 插件就该登场了。
我们在上一篇已经验证过一个结论:验签程序只检查 sig.modPow(e, n) 的结果,不关心这个结果是“算出来的”还是“被替换的”。
把这个结论套到证书链校验里:
sig= 自制证书的签名值(ca.getSignature())(e, n)= 官方根证书的公钥(root2.getPublicKey())sig.modPow(e, n)的期望结果 = 自制证书内容的DigestInfo
我们不需要用官方根证书的私钥去签我们的证书:我们只需要让 sig.modPow(e, n) 返回一个正确的 DigestInfo 就行了。
而 DigestInfo 是可以算出来的:用我们自制证书的 TBSCertificate 算一个 SHA-256 哈希,再按标准格式编码成 DigestInfo。
这跟上一篇算激活码的 DigestInfo 思路完全一样,只是数据来源换成了证书。
生成 power 规则的代码:
static void genPowerConf(X509Certificate ca, X509Certificate root, X509Certificate root2)
throws NoSuchAlgorithmException {
// 自制证书的签名值(验签程序的输入)
BigInteger signature = new BigInteger(1, ca.getSignature());
// 官方根证书的公钥(验签程序实际用的钥匙)
RSAPublicKey root2PublicKey = (RSAPublicKey) root2.getPublicKey();
// 自制证书的 TBSCertificate 的 SHA-256 哈希(验签程序期望的“正确答案”)
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] tbsCert = ca.getTBSCertificate();
byte[] expectedDigest = md.digest(tbsCert);
// 构造 SHA-256 的 DigestInfo(OID + 哈希值)
byte[] sha256Oid = {0x30, 0x31, 0x30, 0x0D, 0x06, 0x09, 0x60,
(byte)0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02,
0x01, 0x05, 0x00, 0x04, 0x20};
byte[] digestInfo = new byte[sha256Oid.length + expectedDigest.length];
System.arraycopy(sha256Oid, 0, digestInfo, 0, sha256Oid.length);
System.arraycopy(expectedDigest, 0, digestInfo, sha256Oid.length, expectedDigest.length);
// PKCS#1 填充(4096 位根证书 → 512 字节)
byte[] padded = padV15(digestInfo, 512);
BigInteger fakeResult = new BigInteger(1, padded);
// 组装 power 规则
StringBuffer sb = new StringBuffer();
sb.append("EQUAL");
sb.append(",");
sb.append(signature); // x:自制证书的签名值
sb.append(",");
sb.append(root2PublicKey.getPublicExponent()); // y:官方根证书的公钥指数
sb.append(",");
sb.append(root2PublicKey.getModulus()); // z:官方根证书的模数
sb.append("->");
sb.append(fakeResult); // fakeResult:预制的 DigestInfo
System.out.println(sb.toString());
}
这条规则的含义很直白:
当验签程序用官方根证书的公钥 (e, n) 去验自制证书的签名值 sig 时,不要真的算 sig.modPow(e, n),直接返回我预制好的 DigestInfo。
因为预制 DigestInfo 正好对应自制证书的内容哈希,后续的比对必然通过。
到这一步,我们的自制证书在验签程序眼里,就已经是一张“由官方根证书签发”的合法证书了。
5. 一劳永逸:用私钥签自己的激活码
证书被“认证”通过后,我们就有了两把可用的私钥:
| 私钥 | 所在文件 | 能做什么 |
|---|---|---|
| 根证书私钥 | root.jks | 签发更多终端证书 |
| 终端证书私钥 | ca.jks | 签署激活码数据 |
而我们最需要的是第二把:ca.jks 里的私钥。
回到上一篇的场景:修改了激活码里的日期,但没有私钥重新签名,只能靠 power 规则来“偷换”验签结果。
现在不同了。有了终端证书的私钥,我们可以真正地签名:
- 修改
licensePartBase64里的日期 - 用
ca.jks里的私钥对修改后的数据做SHA1withRSA签名 - 把新的签名值拼回 key 文件
- 验签程序用我们的终端证书(已通过 power 规则被认定为合法)去验签 → 通过
跟上一篇的方案对比如下:
| 上一篇 | 这一篇 | |
|---|---|---|
| 有私钥吗? | ❌ | ✅(自制证书的私钥) |
| 修改数据后 | 重新算 DigestInfo 写 power 规则 | 直接用私钥签名 |
| 每次修改数据 | 要重新算规则 | 不需要,签一下就完 |
| power 规则用于 | 阶段2(数据验签) | 阶段1(证书链校验) |
| power 规则数量 | 每次改数据都要改 | 固定一条,终生有效 |
这就是“一劳永逸”的真正含义:
一条 power 规则保护证书链校验,一把私钥解放所有后续的数据签名。
6. 两篇串起来看
两篇文章走完,CheckLicense 的两阶段校验都被我们摸透了:
阶段1:证书链校验
- 问题:自制证书不在官方根 CA 信任链里
- 解法:一条 power 规则,让官方根证书的公钥验签“短路”
- 效果:自制证书被认定为合法证书
阶段2:数据签名校验
- 问题:修改数据后没有私钥重新签名
- 解法:用自制终端证书的私钥直接签名
- 效果:任意修改后的数据都能生成合法签名
两篇加起来,完整的路径是这样的:
- 从合法 key 文件里提取官方根证书的公钥
(e, n) - 自制一套证书,用 power 规则让它通过阶段 1 的校验
- 用自制证书的私钥签署任意修改后的数据,通过阶段 2 的校验
再回头看一下上一篇开头的问题:“修改日期后验签失败,能不能让它在没有私钥的情况下通过?”
现在有两个答案了:
- 只改一次 → 上一篇的规则法够用
- 长期维护 → 这一篇的证书法才是王道
免责声明
本文内容仅供技术研究与学术探讨,旨在帮助开发者理解 RSA 验签流程与 Java 安全机制。作者不支持、不鼓励、不教唆任何形式的软件破解或侵权行为。请勿将文中所述技术用于任何违反当地法律法规、侵犯他人知识产权或破坏软件许可协议的行为。读者因不当使用而产生的任何法律后果,均与作者无关。