添加对签名证书修改后的证书识别能力。

This commit is contained in:
2026-01-24 19:49:51 +08:00
parent df745e6f83
commit 26a64b44e9

View File

@@ -11,15 +11,17 @@ import java.io.InputStream;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.Base64; import java.util.Base64;
import java.util.Enumeration;
import java.util.jar.JarEntry; import java.util.jar.JarEntry;
import java.util.jar.JarFile; import java.util.jar.JarFile;
/** /**
* APK文件工具类单例- 生产级签名+哈希双校验版修复Too short异常 * APK文件工具类单例- 生产级签名+哈希双校验版修复Too short异常兼容MT重签名
* 1. 稳定解析CERT.RSA原始字节与客户端Signature.toByteArray()1:1对齐解决X509解析异常 * 1. 稳定解析META-INF下所有RSA原始字节与客户端Signature.toByteArray()1:1对齐解决X509解析异常
* 2. 支持SHA256文件哈希字节级唯一校验签名+哈希双重验证 * 2. 支持SHA256文件哈希字节级唯一校验签名+哈希双重验证
* 3. 入参包含:项目名/版本名/APK名/客户端签名/客户端哈希,适配生产级版本管理 * 3. 入参包含:项目名/版本名/APK名/客户端签名/客户端哈希,适配生产级版本管理
* 4. APK路径规范apks_root/项目名/debug/tag/APK文件支持调试/正式环境) * 4. APK路径规范apks_root/项目名/debug/tag/APK文件支持调试/正式环境)
* 5. 兼容MT重签名遍历META-INF所有.RSA/.rsa文件适配自定义签名文件命名
* @Author ZhanGSKen<zhangsken@qq.com> * @Author ZhanGSKen<zhangsken@qq.com>
*/ */
public class APKFileUtils { public class APKFileUtils {
@@ -31,9 +33,10 @@ public class APKFileUtils {
// 算法常量(与客户端严格对齐) // 算法常量(与客户端严格对齐)
private static final String SIGN_ALGORITHM = "SHA1"; // 签名摘要算法 private static final String SIGN_ALGORITHM = "SHA1"; // 签名摘要算法
private static final String HASH_ALGORITHM = "SHA-256"; // 文件哈希算法 private static final String HASH_ALGORITHM = "SHA-256"; // 文件哈希算法
// 签名文件(兼容大小写,适配所有打包工具 // 签名文件目录与后缀兼容MT重签名遍历所有RSA文件
private static final String CERT_RSA_UPPER = "META-INF/CERT.RSA"; private static final String META_INF_DIR = "META-INF/";
private static final String CERT_RSA_LOWER = "META-INF/cert.rsa"; private static final String RSA_SUFFIX_UPPER = ".RSA";
private static final String RSA_SUFFIX_LOWER = ".rsa";
// APK根目录 // APK根目录
private String apksRootPath; private String apksRootPath;
@@ -91,6 +94,7 @@ public class APKFileUtils {
* 对外暴露核心校验方法:签名 + SHA256文件哈希 双校验 * 对外暴露核心校验方法:签名 + SHA256文件哈希 双校验
* 入参包含:项目名/版本名/APK文件名/客户端签名Base64/客户端文件哈希 * 入参包含:项目名/版本名/APK文件名/客户端签名Base64/客户端文件哈希
* APK路径规范apksRootPath/项目名/版本名/APK文件 * APK路径规范apksRootPath/项目名/版本名/APK文件
* @param isDebug 调试环境标识
* @param projectName 项目名(非空) * @param projectName 项目名(非空)
* @param versionName 版本名非空如15.11.11 * @param versionName 版本名非空如15.11.11
* @param apkFileName APK文件名非空需以.apk结尾 * @param apkFileName APK文件名非空需以.apk结尾
@@ -164,7 +168,7 @@ public class APKFileUtils {
} }
LogUtils.i("APKFileUtils", "【哈希对比结果】✅ 匹配(字节级文件完全一致)"); LogUtils.i("APKFileUtils", "【哈希对比结果】✅ 匹配(字节级文件完全一致)");
// ===== 第二步:签名校验(直接读取CERT.RSA原始字节,与客户端严格对齐)===== // ===== 第二步:签名校验(遍历META-INF所有RSA文件,与客户端严格对齐)=====
String serverSignBase64 = getAPKSign(apkFile); String serverSignBase64 = getAPKSign(apkFile);
if (isParamEmpty(serverSignBase64)) { if (isParamEmpty(serverSignBase64)) {
LogUtils.w("APKFileUtils", "解析服务端APK签名失败" + apkFileName); LogUtils.w("APKFileUtils", "解析服务端APK签名失败" + apkFileName);
@@ -189,8 +193,8 @@ public class APKFileUtils {
} }
/** /**
* 稳定解析APK签名直接读取CERT.RSA原始字节SHA1+Base64与客户端1:1对齐 * 稳定解析APK签名遍历META-INF所有.RSA/.rsa文件读取第一个有效文件原始字节SHA1+Base64与客户端1:1对齐
* 解决X509证书解析的Too short异常兼容所有APK普通/加固/自定义打包 * 兼容MT重签名自定义签名文件命名解决X509证书解析的Too short异常适配所有APK普通/加固/重签名
* @param apkFile APK文件 * @param apkFile APK文件
* @return 签名Base64字符串失败返回null * @return 签名Base64字符串失败返回null
*/ */
@@ -199,23 +203,37 @@ public class APKFileUtils {
InputStream certIs = null; InputStream certIs = null;
try { try {
jarFile = new JarFile(apkFile); jarFile = new JarFile(apkFile);
// 先找大写CERT.RSA找不到再找小写兼容所有打包工具 Enumeration<JarEntry> entries = jarFile.entries();
JarEntry certEntry = jarFile.getJarEntry(CERT_RSA_UPPER); JarEntry targetRsaEntry = null;
if (certEntry == null) {
certEntry = jarFile.getJarEntry(CERT_RSA_LOWER); // 核心改造遍历META-INF下所有条目找到第一个有效.RSA/.rsa文件兼容MT重签名
if (certEntry == null) { while (entries.hasMoreElements()) {
LogUtils.w("APKFileUtils", "APK中未找到签名文件META-INF/CERT.RSA/cert.rsa"); JarEntry entry = entries.nextElement();
return null; String entryName = entry.getName();
// 过滤条件META-INF目录下 + 非目录 + 以.RSA/.rsa结尾
if (entryName.startsWith(META_INF_DIR) && !entry.isDirectory()
&& (entryName.endsWith(RSA_SUFFIX_UPPER) || entryName.endsWith(RSA_SUFFIX_LOWER))) {
targetRsaEntry = entry;
LogUtils.d("APKFileUtils", "找到有效签名文件,路径:" + entryName);
break; // 取第一个有效RSA文件即可重签名APK仅会有一个签名文件
} }
} }
// 核心直接读取CERT.RSA的原始字节流不做证书解析适配PKCS7签名块
certIs = jarFile.getInputStream(certEntry); // 未找到任何RSA签名文件
byte[] sigRawBytes = readStreamToBytes(certIs); if (targetRsaEntry == null) {
if (sigRawBytes == null || sigRawBytes.length == 0) { LogUtils.w("APKFileUtils", "APK中META-INF目录下未找到任何.RSA/.rsa签名文件");
LogUtils.w("APKFileUtils", "读取CERT.RSA原始字节为空");
return null; return null;
} }
// 与客户端完全一致的处理流程SHA1摘要 → Base64编码去换行
// 读取签名文件原始字节流不做证书解析适配PKCS7签名块与客户端保持一致
certIs = jarFile.getInputStream(targetRsaEntry);
byte[] sigRawBytes = readStreamToBytes(certIs);
if (sigRawBytes == null || sigRawBytes.length == 0) {
LogUtils.w("APKFileUtils", "读取签名文件原始字节为空:" + targetRsaEntry.getName());
return null;
}
// 与客户端完全一致的处理流程SHA1摘要 → Base64编码去换行保证格式一致
MessageDigest md = MessageDigest.getInstance(SIGN_ALGORITHM); MessageDigest md = MessageDigest.getInstance(SIGN_ALGORITHM);
byte[] signDigest = md.digest(sigRawBytes); byte[] signDigest = md.digest(sigRawBytes);
String signBase64 = Base64.getEncoder().encodeToString(signDigest) String signBase64 = Base64.getEncoder().encodeToString(signDigest)