扫描签名校验
本文面向已经读过 扫描并行化 和 ScanRequest 与 Result 的读者,继续追踪一个 APK 从解析结果到可注册包之间的签名路径。前文解释 parser 线程如何交付 ParsedPackage,本文解释证书何时收集、何时复用、与哪个历史对象比较,以及失败之后由谁决定清理或拒绝。
本文只讨论 Android 17 android-17.0.0_r1 的扫描/安装校验,不把 APK 签名方案本身写成独立密码学教程,也不把“证书收集成功”误写成“包已经和旧版本兼容”。在当前源码中,至少要区分四个层次:APK 内容签名验证、base/split 证书一致性、与 PackageSetting/disabled setting 的兼容性比较、与 shared user 或 upgrade keyset 的关系检查。
读完后,读者应能从 InstallPackageHelper.scanPackageForInitLI() 找到 collectCertificatesLI() 的调用,判断何时命中签名缓存、何时跳过完整性校验、何时强制重新收集;能够沿 ParsingPackageUtils 和 ApkSignatureVerifier 解释 V4/V3/V2/V1 的尝试顺序;还能根据错误码区分证书缺失、证书不一致、包升级签名不兼容和 shared user 不兼容。
1. 校验分层
1.1 四层状态
源码文件:
frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.javaframeworks/base/services/core/java/com/android/server/pm/ScanPackageUtils.javaframeworks/base/services/core/java/com/android/server/pm/ReconcilePackageUtils.java
签名相关对象和动作可以按下表定位:
| 层次 | 输入 | 核心实现 | 输出或失败 |
|---|---|---|---|
| APK 验证 | base/split APK 文件 | ApkSignatureVerifier | SigningDetails 或 parse error |
| 包内一致性 | base 的 SigningDetails、每个 split | ParsingPackageUtils.getSigningDetails() | 所有 split 使用同一签名,否则 INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES |
| 缓存复用 | PackageSetting 路径、时间、版本信息 | ScanPackageUtils.collectCertificatesLI() | 复用或重新设置 ParsedPackage.signingDetails |
| 已知包兼容 | 新签名、旧 PackageSetting、disabled setting | PackageManagerServiceUtils.verifySignatures() | 允许、兼容升级、回滚或 INSTALL_FAILED_UPDATE_INCOMPATIBLE |
| shared user/KeySet | 新签名、SharedUserSetting、upgrade keyset | ReconcilePackageUtils | 允许加入或 shared user/keyset 失败 |
前两层回答“这个 APK 的签名是否真实且内部一致”,后三层回答“它是否能替换当前系统中的对象”。同一签名验证错误可能在不同层得到不同日志:Failed to parse 表示收集阶段失败,Existing package ... signatures do not match 表示已知包兼容性失败。
1.2 调用时机
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
final boolean forceCollect = scanSystemPartition ? isUpgrade
: pkgAlreadyExists && needSignatureMatchToSystem(pkgSetting.getPackageName());
// APK verification can be skipped during certificate collection, only if the file is in
// a verified partition.
final boolean skipVerify = scanSystemPartition;
ScanPackageUtils.collectCertificatesLI(pkgSetting, parsedPackage,
mPm.getSettingsVersionForPackage(parsedPackage), forceCollect, skipVerify,
mPm.isPreNMR1Upgrade());
metrics.setIsFsiEnabled(forceCollect)
.setPackageName(parsedPackage.getPackageName())
.setNumApkSplits(parsedPackage.getSplitCodePaths() == null
? 0 : parsedPackage.getSplitCodePaths().length)
.setSignatureSchemeVersion(
parsedPackage.getSigningDetails().getSignatureSchemeVersion());签名收集发生在 scanPackageForInitLI() 的系统包恢复/初始化路径中,位于版本和路径判断之后、scanPackageNew() 生成 ScanResult 之前。普通安装路径也会在 installPackageLI() 中直接调用 ParsingPackageUtils.getSigningDetails(),但本文主线关注扫描阶段的 collectCertificatesLI()。
forceCollect 与 skipVerify 是两个独立开关:前者决定能否复用 PackageSetting 中的签名缓存,后者决定重新收集时是否做完整内容验证。系统分区因为受 verified partition 保护可以跳过完整性验证,但仍需要拿到证书用于后续比较;data 包不能因为 forceCollect=false 就跳过旧包兼容性检查。
2. 缓存收集
2.1 命中条件
源码文件:frameworks/base/services/core/java/com/android/server/pm/ScanPackageUtils.java
public static void collectCertificatesLI(PackageSetting ps, ParsedPackage parsedPackage,
Settings.VersionInfo settingsVersionForPackage, boolean forceCollect,
boolean skipVerify, boolean isPreNMR1Upgrade)
throws PackageManagerException {
// When upgrading from pre-N MR1, verify the package time stamp using the package
// directory and not the APK file.
final long lastModifiedTime = isPreNMR1Upgrade
? new File(parsedPackage.getPath()).lastModified()
: getLastModifiedTime(parsedPackage);
if (ps != null && !forceCollect
&& ps.getPathString().equals(parsedPackage.getPath())
&& ps.getLastModifiedTime() == lastModifiedTime
&& !ReconcilePackageUtils.isCompatSignatureUpdateNeeded(settingsVersionForPackage)
&& !ReconcilePackageUtils.isRecoverSignatureUpdateNeeded(
settingsVersionForPackage)) {
if (ps.getSigningDetails().getSignatures() != null
&& ps.getSigningDetails().getSignatures().length != 0
&& ps.getSigningDetails().getSignatureSchemeVersion()
!= SigningDetails.SignatureSchemeVersion.UNKNOWN) {
// Optimization: reuse the existing cached signing data
// if the package appears to be unchanged.
parsedPackage.setSigningDetails(
new SigningDetails(ps.getSigningDetails()));
return;
}
Slog.w(TAG, "PackageSetting for " + ps.getPackageName()
+ " is missing signatures. Collecting certs again to recover them.");
} else {
Slog.i(TAG, parsedPackage.getPath() + " changed; collecting certs"
+ (forceCollect ? " (forced)" : ""));
}缓存命中需要同时满足五个条件:存在旧设置、没有强制收集、路径相同、修改时间相同、Settings 版本不要求兼容性或恢复性签名升级。即使这五项满足,旧 SigningDetails 还必须有非空签名数组且 scheme version 不能是 UNKNOWN;否则只记录 warning 并重新收集。
isPreNMR1Upgrade 是历史兼容分支:升级自旧版本时,时间戳取包路径的 File.lastModified();其他情况使用 getLastModifiedTime(parsedPackage),后者可以处理当前包模型支持的 base/split/APEX 路径。不要把时间戳相同解释成内容一定相同,它只是 PMS 当前的缓存失效条件。
2.2 缓存副本
源码文件:frameworks/base/services/core/java/com/android/server/pm/ScanPackageUtils.java
try {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "collectCertificates");
final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing();
final ParseResult<SigningDetails> result = ParsingPackageUtils.getSigningDetails(
input, parsedPackage, skipVerify);
if (result.isError()) {
throw new PackageManagerException(
result.getErrorCode(), result.getErrorMessage(), result.getException());
}
parsedPackage.setSigningDetails(result.getResult());
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}缓存命中时使用 new SigningDetails(ps.getSigningDetails()),不会把 PackageSetting 内部对象直接暴露给当前 ParsedPackage。缓存失效时,getSigningDetails() 返回 ParseResult;错误被转换为 PackageManagerException,成功结果才写入 ParsedPackage。Trace 在异常路径也必须结束,否则启动 trace 会留下不完整区间。
2.3 强制收集场景
scanPackageForInitLI() 中的 forceCollect 在扫描 system 分区时取 isUpgrade,在扫描 data 分区时取“已有包且该包在严格 system signature 检查名单中”。这意味着 OTA/system upgrade 和特定预装包会放弃缓存,即便路径和时间没有变化。
缓存命中只跳过证书收集,不跳过后续的 verifySignatures()、KeySet 和 shared user 比较。旧缓存可能证明“上次这个文件拿到过某证书”,但不能替代本次安装请求与当前历史设置的兼容性判断。
3. APK 验证
3.1 解析工具入口
源码文件:frameworks/base/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java
/**
* Collect certificates from all the APKs described in the given package. Also asserts that
* all APK contents are signed correctly and consistently.
*/
@CheckResult
public static ParseResult<SigningDetails> getSigningDetails(ParseInput input,
ParsedPackage pkg, boolean skipVerify) {
return getSigningDetails(input, pkg.getBaseApkPath(), pkg.isStaticSharedLibrary(),
pkg.getTargetSdkVersion(), pkg.getSplitCodePaths(), skipVerify);
}
@CheckResult
public static ParseResult<SigningDetails> getSigningDetails(ParseInput input,
String baseApkPath, boolean isStaticSharedLibrary, int targetSdkVersion,
String[] splitCodePaths, boolean skipVerify) {
SigningDetails signingDetails = SigningDetails.UNKNOWN;
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "collectCertificates");
try {
ParseResult<SigningDetails> result = getSigningDetails(
input, baseApkPath, skipVerify, isStaticSharedLibrary,
signingDetails, targetSdkVersion);
if (result.isError()) {
return input.error(result);
}
signingDetails = result.getResult();
final File frameworkRes = new File(Environment.getRootDirectory(),
"framework/framework-res.apk");
boolean isFrameworkResSplit = frameworkRes.getAbsolutePath().equals(baseApkPath);
if (!ArrayUtils.isEmpty(splitCodePaths) && !isFrameworkResSplit) {
for (int i = 0; i < splitCodePaths.length; i++) {
result = getSigningDetails(input, splitCodePaths[i], skipVerify,
isStaticSharedLibrary, signingDetails, targetSdkVersion);
if (result.isError()) {
return input.error(result);
}
}
}
return result;
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
}解析工具先验证 base APK,再逐个验证 split,并把先前得到的 SigningDetails 作为 existingSigningDetails 传入。framework-res.apk 被特殊排除在 split 一致性循环之外,这是 Android 17 源码的真实条件,不能泛化成“所有 split 都必定逐个比较”。
3.2 skipVerify 语义
源码文件:frameworks/base/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java
@CheckResult
public static ParseResult<SigningDetails> getSigningDetails(ParseInput input,
String baseCodePath, boolean skipVerify, boolean isStaticSharedLibrary,
@NonNull SigningDetails existingSigningDetails, int targetSdk) {
int minSignatureScheme = ApkSignatureVerifier.getMinimumSignatureSchemeVersionForTargetSdk(
targetSdk);
if (isStaticSharedLibrary) {
// must use v2 signing scheme
minSignatureScheme = SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V2;
}
final ParseResult<SigningDetails> verified;
if (skipVerify) {
// systemDir APKs are already trusted, save time by not verifying
verified = ApkSignatureVerifier.unsafeGetCertsWithoutVerification(input, baseCodePath,
minSignatureScheme);
} else {
verified = ApkSignatureVerifier.verify(input, baseCodePath, minSignatureScheme);
}
if (verified.isError()) {
return input.error(verified);
}
if (existingSigningDetails == SigningDetails.UNKNOWN) {
return verified;
} else {
if (!Signature.areExactMatch(existingSigningDetails, verified.getResult())) {
return input.error(INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES,
baseCodePath + " has mismatched certificates");
}
return input.success(existingSigningDetails);
}
}skipVerify=true 并不是“不读取签名”,而是调用 unsafeGetCertsWithoutVerification() 获取证书,跳过 APK 内容完整性验证。源码只把它用于已经信任的 systemDir APK;data APK 走 ApkSignatureVerifier.verify()。
静态共享库无论 target SDK 是多少,都把最低 scheme 强制设置为 V2。普通 APK 的最低 scheme 由 target SDK 决定。base 之后的 split 使用 Signature.areExactMatch(),要求证书集合完全一致;签名 lineage 兼容性不是这里判断的主题,而在已知包比较阶段由 SigningDetails.checkCapability() 处理。
3.3 方案尝试顺序
源码文件:frameworks/base/core/java/android/util/apk/ApkSignatureVerifier.java
public static ParseResult<SigningDetailsWithDigests> verifySignaturesInternal(ParseInput input,
String apkPath, @SignatureSchemeVersion int minSignatureSchemeVersion,
boolean verifyFull) {
if (minSignatureSchemeVersion > SignatureSchemeVersion.SIGNING_BLOCK_V4) {
return input.error(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"No signature found in package of version " + minSignatureSchemeVersion
+ " or newer for package " + apkPath);
}
// first try v4
try {
return verifyV4Signature(input, apkPath, minSignatureSchemeVersion, verifyFull);
} catch (SignatureNotFoundException e) {
// not signed with v4, try older if allowed
if (minSignatureSchemeVersion >= SignatureSchemeVersion.SIGNING_BLOCK_V4) {
return input.error(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"No APK Signature Scheme v4 signature in package " + apkPath, e);
}
}
if (minSignatureSchemeVersion > SignatureSchemeVersion.SIGNING_BLOCK_V3) {
return input.error(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"No signature found in package of version " + minSignatureSchemeVersion
+ " or newer for package " + apkPath);
}
return verifyV3AndBelowSignatures(input, apkPath, minSignatureSchemeVersion, verifyFull);
}
private static ParseResult<SigningDetailsWithDigests> verifyV3AndBelowSignatures(
ParseInput input, String apkPath, @SignatureSchemeVersion int minSignatureSchemeVersion,
boolean verifyFull) {
// try v3
try {
return verifyV3Signature(input, apkPath, verifyFull);
} catch (SignatureNotFoundException e) {
if (minSignatureSchemeVersion >= SignatureSchemeVersion.SIGNING_BLOCK_V3) {
return input.error(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"No APK Signature Scheme v3 signature in package " + apkPath, e);
}
}
if (minSignatureSchemeVersion > SignatureSchemeVersion.SIGNING_BLOCK_V2) {
return input.error(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"No signature found in package of version " + minSignatureSchemeVersion
+ " or newer for package " + apkPath);
}
// try v2
try {
return verifyV2Signature(input, apkPath, verifyFull);
} catch (SignatureNotFoundException e) {
if (minSignatureSchemeVersion >= SignatureSchemeVersion.SIGNING_BLOCK_V2) {
return input.error(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"No APK Signature Scheme v2 signature in package " + apkPath, e);
}
}
if (minSignatureSchemeVersion > SignatureSchemeVersion.JAR) {
return input.error(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"No signature found in package of version " + minSignatureSchemeVersion
+ " or newer for package " + apkPath);
}
// v2 didn't work, try jarsigner
return verifyV1Signature(input, apkPath, verifyFull);
}“V1-V4 逐级降级”只有在最低要求允许降级时才成立:算法先尝试 V4,再到 V3、V2、V1;如果 minSignatureSchemeVersion 已经是 V2,V2 缺失后不会再接受 V1。V4 是 add-on,需要结合 V2/V3 证书和 digest;它不是简单地替换 V3。
3.4 最低 scheme
源码文件:frameworks/base/core/java/android/util/apk/ApkSignatureVerifier.java
/**
* Returns the minimum signature scheme version required for an app targeting the specified
* {@code targetSdk}.
*/
public static int getMinimumSignatureSchemeVersionForTargetSdk(int targetSdk) {
if (targetSdk >= Build.VERSION_CODES.R) {
return SignatureSchemeVersion.SIGNING_BLOCK_V2;
}
return SignatureSchemeVersion.JAR;
}Android 17 当前源码的最低 scheme 映射非常具体:target SDK >= R 要求至少 V2,低于 R 仍允许 JAR/V1。不能凭“Android 9 引入 V3”推断 target SDK >= 28 就强制 V3;当前方法没有这样的分支。静态共享库的 V2 要求来自 ParsingPackageUtils 的额外覆盖。
4. 已知包比较
4.1 比较入口
源码文件:frameworks/base/services/core/java/com/android/server/pm/ReconcilePackageUtils.java
final PackageSetting signatureCheckPs =
lastStaticSharedLibSetting != null
? lastStaticSharedLibSetting
: installRequest.getScannedPackageSetting();
boolean removeAppKeySetData = false;
boolean sharedUserSignaturesChanged = false;
SigningDetails signingDetails = null;
if (parsedPackage != null) {
signingDetails = parsedPackage.getSigningDetails();
}
final boolean isSystemPackage =
((parseFlags & ParsingPackageUtils.PARSE_IS_SYSTEM_DIR) != 0);
final boolean isApex = (scanFlags & SCAN_AS_APEX) != 0;
SharedUserSetting sharedUserSetting = settings.getSharedUserSettingLPr(signatureCheckPs);
if (ksms.shouldCheckUpgradeKeySetLocked(signatureCheckPs, sharedUserSetting, scanFlags)) {
if (ksms.checkUpgradeKeySetLocked(signatureCheckPs, parsedPackage)) {
// The package is signed by an allowed upgrade key.
} else {
if (!isSystemPackage) {
throw new ReconcileFailure(INSTALL_FAILED_UPDATE_INCOMPATIBLE,
"Package " + parsedPackage.getPackageName()
+ " upgrade keys do not match the previously installed version");
} else {
String msg = "System package " + parsedPackage.getPackageName()
+ " signature changed; retaining data.";
PackageManagerService.reportSettingsProblem(Log.WARN, msg);
}
}
} else {
// verifySignatures(...) is called here for ordinary certificate matching.
}reconcile 先决定比较对象:静态共享库使用该库的最新 PackageSetting,普通包使用扫描结果设置。随后优先判断 KeySet 是否要求升级密钥检查;只有不走 upgrade keyset 分支时,才进入 PackageManagerServiceUtils.verifySignatures() 的普通证书比较。
system package 与非 system package 的失败策略不同:非 system 包的 upgrade keyset 不匹配抛 INSTALL_FAILED_UPDATE_INCOMPATIBLE;system 包记录“signature changed; retaining data”的设置问题。这里已经不是 APK 内容验证,而是安装关系验证。
4.2 旧包与回滚
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
public static boolean verifySignatures(PackageSetting pkgSetting,
@Nullable SharedUserSetting sharedUserSetting,
PackageSetting disabledPkgSetting, SigningDetails parsedSignatures,
boolean compareCompat, boolean compareRecover, boolean isRollback)
throws PackageManagerException {
final String packageName = pkgSetting.getPackageName();
boolean compatMatch = false;
if (pkgSetting.getSigningDetails().getSignatures() != null) {
boolean match = parsedSignatures.checkCapability(
pkgSetting.getSigningDetails(),
SigningDetails.CertCapabilities.INSTALLED_DATA)
|| pkgSetting.getSigningDetails().checkCapability(
parsedSignatures,
SigningDetails.CertCapabilities.ROLLBACK);
if (match && disabledPkgSetting != null
&& disabledPkgSetting.getSigningDetails() != SigningDetails.UNKNOWN) {
match = matchSignatureInSystem(packageName, parsedSignatures, disabledPkgSetting);
}
if (!match && compareCompat) {
match = matchSignaturesCompat(packageName, pkgSetting.getSignatures(),
parsedSignatures);
compatMatch = match;
}
if (!match && compareRecover) {
match = matchSignaturesRecover(packageName, pkgSetting.getSigningDetails(),
parsedSignatures, SigningDetails.CertCapabilities.INSTALLED_DATA)
|| matchSignaturesRecover(packageName, parsedSignatures,
pkgSetting.getSigningDetails(), SigningDetails.CertCapabilities.ROLLBACK);
}
if (!match && isRollback) {
// A rollback may return to a previous signer in the known lineage.
match = pkgSetting.getSigningDetails().hasAncestorOrSelf(parsedSignatures);
}
if (!match) {
throw new PackageManagerException(INSTALL_FAILED_UPDATE_INCOMPATIBLE,
"Existing package " + packageName
+ " signatures do not match newer version; ignoring!");
}
}
return compatMatch;
}普通比较先尝试 INSTALLED_DATA 能力,再尝试旧包授予新签名的 ROLLBACK 能力;disabled system package 存在且签名不是 UNKNOWN 时还必须通过 system 版本比较。兼容升级和恢复升级是由 Settings 版本信息控制的额外分支,回滚则可以在已知 lineage 中回到祖先签名。
签名 lineage 的“同一证书”不能只用字节数组相等解释。SigningDetails.checkCapability() 会考虑当前 signer、past signing certificates 和 capability flags;这也是 key rotation 能够在特定能力下继续更新的原因。
4.3 shared user
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
if (sharedUserSetting != null
&& sharedUserSetting.getSigningDetails() != SigningDetails.UNKNOWN) {
boolean match = canJoinSharedUserId(packageName, parsedSignatures, sharedUserSetting,
pkgSetting.getSigningDetails().getSignatures() != null
? SHARED_USER_ID_JOIN_TYPE_UPDATE : SHARED_USER_ID_JOIN_TYPE_INSTALL);
if (!match && compareCompat) {
match = matchSignaturesCompat(packageName, sharedUserSetting.signatures,
parsedSignatures);
}
if (!match && compareRecover) {
match = matchSignaturesRecover(packageName,
sharedUserSetting.signatures.mSigningDetails, parsedSignatures,
SigningDetails.CertCapabilities.SHARED_USER_ID)
|| matchSignaturesRecover(packageName, parsedSignatures,
sharedUserSetting.signatures.mSigningDetails,
SigningDetails.CertCapabilities.SHARED_USER_ID);
compatMatch |= match;
}
if (!match) {
throw new PackageManagerException(INSTALL_FAILED_SHARED_USER_INCOMPATIBLE,
"Package " + packageName
+ " has no signatures that match those in shared user "
+ sharedUserSetting.name + "; ignoring!");
}
if (!parsedSignatures.hasCommonAncestor(
sharedUserSetting.signatures.mSigningDetails)) {
throw new PackageManagerException(INSTALL_FAILED_SHARED_USER_INCOMPATIBLE,
"Package " + packageName + " has a signing lineage "
+ "that diverges from the lineage of the sharedUserId");
}
}shared user 比较有两个独立条件:新包必须能以 install/update join type 加入共享 UID,且 signing lineage 不能与共享用户的 lineage 分叉。即使某个 signer 通过 capability 比较,也不能绕过 lineage divergence 检查。
5. lineage 数据
5.1 SigningDetails
源码文件:frameworks/base/core/java/android/content/pm/SigningDetails.java
@IntDef({SignatureSchemeVersion.UNKNOWN,
SignatureSchemeVersion.JAR,
SignatureSchemeVersion.SIGNING_BLOCK_V2,
SignatureSchemeVersion.SIGNING_BLOCK_V3,
SignatureSchemeVersion.SIGNING_BLOCK_V4})
public @interface SignatureSchemeVersion {
int UNKNOWN = 0;
int JAR = 1;
int SIGNING_BLOCK_V2 = 2;
int SIGNING_BLOCK_V3 = 3;
int SIGNING_BLOCK_V4 = 4;
}
private final @Nullable Signature[] mSignatures;
private final @SignatureSchemeVersion int mSignatureSchemeVersion;
private final @Nullable ArraySet<PublicKey> mPublicKeys;
private final @Nullable Signature[] mPastSigningCertificates;SigningDetails 不只保存当前 Signature[] 和 scheme version,还保存 public keys、past signing certificates 以及 capability。PackageSetting 缓存的是整个 SigningDetails,因此 key rotation 的历史不会在缓存复用时丢失。
5.2 lineage 与能力
源码文件:frameworks/base/core/java/android/content/pm/SigningDetails.java
public boolean hasAncestorOrSelf(@NonNull SigningDetails oldDetails) {
if (this == UNKNOWN || oldDetails == UNKNOWN) {
return false;
}
if (oldDetails.mSignatures.length > 1) {
// multiple-signer packages cannot rotate signing certs, so we just compare current
// signers for an exact match
return signaturesMatchExactly(oldDetails);
} else {
// check whether the old signer was one of our old signing certificates
return hasCertificate(oldDetails.mSignatures[0]);
}
}多 signer 包不能进行 signing certificate rotation,因此 hasAncestorOrSelf() 对多 signer 走 exact match;单 signer 包才沿 past signing certificates 检查祖先关系。这个分支解释了为什么“证书内容看起来相同”并不足以判断所有升级场景。
5.3 设置持久化
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageSetting.java
public android.content.pm.SigningDetails getSigningDetails() {
return signatures.mSigningDetails;
}
public PackageSetting setSigningDetails(SigningDetails signingDetails) {
// TODO: Immutability
signatures.mSigningDetails = signingDetails;
onChanged();
return this;
}PackageSetting 通过 PackageSignatures 持有签名详情,setSigningDetails() 会触发 onChanged(),使设置观察者知道状态发生变化。扫描阶段把证书写入结果设置后,真正的 packages XML 持久化仍由 Settings 写回阶段完成;仅修改 ParsedPackage 不会自动持久化。
6. 失败与清理
6.1 解析失败
ParsingPackageUtils.getSigningDetails() 或 ApkSignatureVerifier 失败时,错误通常是 INSTALL_PARSE_FAILED_NO_CERTIFICATES、INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING、INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION 或 split 的 INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES。这些错误发生在证书收集阶段,尚未进入旧包兼容性比较。
6.2 扫描失败
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
if (throwable == null) {
try {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "addForInitLI");
addForInitLI(result.parsedPackage, scanParams.parseFlags, scanParams.scanFlags,
new UserHandle(UserHandle.USER_SYSTEM), scanParams.apexInfo);
} catch (PackageManagerException e) {
errorCode = e.error;
errorMsg = "Failed to scan " + result.scanFile + ": " + e.getMessage();
Slog.w(TAG, errorMsg);
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
} else if (throwable instanceof PackageManagerException) {
PackageManagerException e = (PackageManagerException) throwable;
errorCode = e.error;
errorMsg = "Failed to parse " + result.scanFile + ": " + e.getMessage();
Slog.w(TAG, errorMsg);
}
if ((scanParams.scanFlags & SCAN_AS_SYSTEM) == 0
&& errorCode != PackageManager.INSTALL_SUCCEEDED) {
logCriticalInfo(Log.WARN,
"Deleting invalid package at " + result.scanFile + " (" + errorMsg + ")");
mRemovePackageHelper.removeCodePath(result.scanFile);
}并行 parser 返回的 PackageManagerException 会在 processParseResult() 被标记为 parse 失败;addForInitLI() 抛出的异常则标记为 scan 失败。二者都可能最终导致 data code path 清理,但 system 输入带 SCAN_AS_SYSTEM 时不会走删除分支。签名不兼容若发生在 reconcile,可能在 addForInitLI() 内以 scan failure 表现;因此日志前缀和错误码需要同时查看。
6.3 system 与 data
系统包签名不匹配时,初始化逻辑可能删除 data 上的冲突用户包、隐藏 system 版本、启用 system 版本或保留数据,具体由版本、路径和 SigningDetails capability 共同决定。证书收集本身不会决定最终选择哪个 code path;它只提供后续决策所需的可信签名详情。
APK-in-APEX 还有额外边界:system 包扫描失败会影响 APEX 安装报告,data 上较新版本的 APK-in-APEX 不应因为 system 侧较旧版本比较而直接让 APEX 安装失败,相关条件在 scanPackageForInitLI() 中单独处理。
7. 测试与诊断
7.1 签名持久化测试
源码文件:frameworks/base/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageSignaturesTest.java
@Test
public void testReadXmlWithSigningLineage() throws Exception {
// Verifies the good path of reading a single sigs tag including pastSigs with the
// signing lineage returns the expected signatures and lineage for two and three signers
// in the lineage.
verifyReadXmlReturnsExpectedSignaturesAndLineage("xml/two-signers-in-lineage.xml", 3,
FIRST_EXPECTED_SIGNATURE, SECOND_EXPECTED_SIGNATURE);
verifyReadXmlReturnsExpectedSignaturesAndLineage("xml/three-signers-in-lineage.xml", 3,
FIRST_EXPECTED_SIGNATURE, SECOND_EXPECTED_SIGNATURE, THIRD_EXPECTED_SIGNATURE);
}
private void verifyReadXmlReturnsExpectedSignaturesAndLineage(String xmlFile,
int schemeVersion, String... expectedSignatureValues) throws Exception {
TypedXmlPullParser parser = getXMLFromResources(xmlFile);
ArrayList<Signature> signatures = new ArrayList<>();
mPackageSetting.getSignatures().readXml(parser, signatures);
Set<String> expectedSignatures = createSetOfSignatures(expectedSignatureValues);
verifySignaturesContainExpectedValues(signatures, expectedSignatures);
assertEquals("The returned signature scheme is not the expected value", schemeVersion,
mPackageSetting.getSigningDetails().getSignatureSchemeVersion());
for (Signature signature : signatures) {
String signatureValue = HexDump.toHexString(signature.toByteArray(), false);
int expectedCapabilities = SIGNATURE_TO_CAPABILITY_MAP.get(signatureValue);
assertTrue(mPackageSetting.getSigningDetails().hasCertificate(
signature, expectedCapabilities));
}
}这个测试从 XML fixture 读取 sigs/pastSigs,断言签名数量、scheme version 和 lineage capability。它证明 Settings 持久化后的签名历史可以恢复,不证明 APK 文件本身在本次启动时重新验证成功,也不证明 shared user 比较通过。
7.2 损坏 fixture
源码文件:frameworks/base/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageSignaturesTest.java
测试还覆盖:
- 缺失
sigscount 或 scheme version 时,设置可以退化到UNKNOWN或保留可读签名; - cert index、cert key、cert tag 缺失或非法时,读取过程不应无界崩溃;
- past signer 的 flags 缺失/非法时,签名可读取但 capability 不完整;
- 多余 cert tag 被忽略,过少 cert tag 只保留能够读取的签名;
- 启用 APK PQC hybrid signing flag 时,scheme minor version 也被验证。
这些 fixture 测试的是 XML 恢复容错,不应反推 ApkSignatureVerifier 对真实 APK 的验证行为。真实 APK 证书错误仍要回到 ParsingPackageUtils 和 ApkSignatureVerifier 的 ParseResult 错误码。
7.3 扫描结果测试
源码文件:frameworks/base/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ScanTests.java
可与签名主线对照的扫描测试包括:
newInstallSimpleAllNominal:验证新包能生成扫描结果,但不包含真实证书 fixture;updateSystemApp_applicationInfoFlagSet:注入 system/disabled setting,断言扫描结果派生出FLAG_UPDATED_SYSTEM_APP;scanSystemApp_isOrphanedTrue:断言 system package 扫描后设置为 orphaned;executeScan():直接调用scanPackageOnly(),并手动hideAsFinal(),说明测试没有经过完整 reconcile/commit。
这些测试证明扫描状态承接了 SigningDetails 所在的 PackageSetting,但签名兼容性边界仍由 ReconcilePackageUtils/PackageManagerServiceUtils 负责。若要验证真实签名替换失败,必须使用带不同证书的 APK 或对应安装测试,而不能只看 ScanResult 非空。
7.4 只读诊断
设备上可以使用:
adb shell dumpsys package <package-name>
adb shell logcat -s PackageManager PackageManagerService ApkSignatureVerifier
adb shell dumpsys package packages诊断时按顺序记录:
- code path、版本、是否 system/updated system/APEX;
SigningDetails的 scheme version、当前 signer 和 lineage 信息;PackageSetting与 disabled system setting 是否同时存在;- 错误是
NO_CERTIFICATES、INCONSISTENT_CERTIFICATES、UPDATE_INCOMPATIBLE还是SHARED_USER_INCOMPATIBLE; - 失败后 data code path 是否被删除,system package 是否被隐藏/恢复,shared user 是否被裁剪。
日志中出现“changed; collecting certs”只说明缓存未命中;出现“signature mismatch”还需要判断它来自 APK 验证、split 一致性、旧包比较还是 shared user。不同 owner 的日志不能互相替代。
8. 源码路线
遇到签名相关问题时,可以沿下面的顺序追踪:
InstallPackageHelper.scanPackageForInitLI():确认forceCollect、skipVerify和collectCertificatesLI()调用时机。ScanPackageUtils.collectCertificatesLI():核对路径、时间、Settings 版本、缓存签名和重收集条件。ParsingPackageUtils.getSigningDetails():确认 base/split、static shared library 和skipVerify分支。ApkSignatureVerifier.verifySignaturesInternal():确认最低 scheme、V4→V3→V2→V1 尝试和错误码。ReconcilePackageUtils.reconcilePackages():确认 upgrade keyset 是否优先于普通证书比较。PackageManagerServiceUtils.verifySignatures():确认旧包、disabled system、compat/recover/rollback 和 shared user 比较。SigningDetails:确认 lineage、ancestor 和 capability,而不是只比较当前证书数组。processParseResult()与清理 helper:确认失败日志、APEX 报告和 data code path 删除边界。
一个实用练习是:给定“APK 使用 V1、target SDK 35、安装在 /data/app、旧包使用 V2、shared user 有一条不同 lineage”的现象,分别指出它会在哪一层失败、哪个错误码最先出现、是否会调用 removeCodePath(),以及需要查看哪些测试 fixture 或 dumpsys 字段。只有把证书真实性、包内一致性和历史兼容性分开,才能正确解释结果。
9. 设计收束
Android 17 的扫描签名校验不是一个单独的 verify() 调用,而是一条分层链路:
collectCertificatesLI()用路径、修改时间和 Settings 版本控制缓存;缓存只减少证书收集成本,不替代兼容性比较。ParsingPackageUtils先验证 base,再验证 split;skipVerify只跳过受信任 system APK 的内容完整性检查,仍然返回证书。ApkSignatureVerifier按 V4、V3、V2、V1 尝试,但最低 scheme 由 target SDK/静态共享库规则限制;Android 17 对 target SDK >= R 的普通 APK 最低要求是 V2。ReconcilePackageUtils先处理 upgrade keyset,再进入旧包、disabled system、compat/recover/rollback 和 shared user 签名比较。SigningDetails的 lineage 与 capability 决定 key rotation、回滚和 shared user 加入是否可行;多 signer 包不能沿 lineage 轮换。- 失败发生在 parser、scan、reconcile 的不同阶段,日志前缀、错误码和 code path 清理范围都不同;system 输入和 data 输入不能用同一清理规则解释。
后续专题会转向 native library 扫描,说明签名通过后 PackageSetting 如何继续承载 ABI、native library path 和卸载清理状态。
