签名机制与密钥管理
在 Android 中,“签过名”不是构建结束时贴上的一个标签。证书和私钥先由构建系统选择,随后被 SignApk 消费成 APK 签名工件;设备解析 APK 时,ApkSignatureVerifier 再把工件转换成 SigningDetails。Package Manager 保存并消费的不是私钥,而是当前证书、历史证书和每个历史证书 允许继续承担的 capability。
本文面向已经读过 lunch目标与编译变体、APK签名工具链 和 ADB安装与权限管理 的读者。你需要知道 android_app、预置 APK、安装更新和 APK 的基本结构,但不需要先学习 X.509、PKCS#8 或 HSM 实现。本文只讨论 AOSP Android 17 中“构建输入如何变成 Package Manager 的身份判断”;不展开 密码学算法、厂商密钥托管、OTA payload 算法和 Keystore 用户证书。
读完后,你应能从一个模块的 certificate 属性找到 .x509.pem 与 .pk8,解释 release 阶段 为什么还会重新签名,并从 SigningDetails 追到更新、shared UID、签名权限和 rollback 的实际 消费者。文章中的源码均来自 android-17.0.0_r1,路径相对对应 AOSP project。
1. 身份边界
APK 签名身份有三个容易混淆的层次:构建输入是私钥和证书文件,APK 工件是签名方案写入的 ZIP 结构,framework 身份是 SigningDetails。前一层拥有秘密,后一层只保留可以被系统比较和授权的 结果。
SigningDetails 不等于“证书字符串”。Android 17 的真实字段包括:
| 字段 | 所有者 | 用途 | 生效时机 |
|---|---|---|---|
mSignatures | SigningDetails | 当前 signer 的证书集合 | APK 解析成功后 |
mPublicKeys | SigningDetails | 当前公钥集合 | 构造对象时 |
mPastSigningCertificates | SigningDetails | v3 proof-of-rotation 的历史证书 | capability 比较时 |
mSignatureSchemeVersion | SigningDetails | v1/v2/v3/v4 方案版本 | verifier 返回时 |
历史证书是否仍能承担更新、权限或 shared UID,不由“它曾经出现过”决定,而由 lineage 中记录的 capability 决定。这样可以轮换私钥,同时选择性撤销旧证书的某种能力。
2. 模块证书
2.1 Soong 属性
build/soong/java/app.go 把 android_app 的 certificate、lineage 和 additional_certificates 变成签名动作的输入。certificate 可以是证书名,也可以是 android_app_certificate 模块引用;没有显式值时使用产品默认证书。
源码文件:build/soong/java/app.go
// build/soong/java/app.go :: processMainCert
func processMainCert(m android.ModuleBase, certPropValue string,
certificates []Certificate, ctx android.ModuleContext) (
mainCertificate Certificate, allCertificates []Certificate) {
if android.SrcIsModule(certPropValue) == "" {
var mainCert Certificate
if certPropValue != "" {
defaultDir := ctx.Config().DefaultAppCertificateDir(ctx)
mainCert = Certificate{
Pem: defaultDir.Join(ctx, certPropValue+".x509.pem"),
Key: defaultDir.Join(ctx, certPropValue+".pk8"),
}
} else {
pem, key := ctx.Config().DefaultAppCertificate(ctx)
mainCert = Certificate{Pem: pem, Key: key}
}
certificates = append([]Certificate{mainCert}, certificates...)
}
if len(certificates) > 0 {
mainCertificate = certificates[0]
} else {
mainCertificate = Certificate{
Key: android.PathForModuleOut(ctx, "missing.pk8"),
Pem: android.PathForModuleOut(ctx, "missing.x509.pem"),
}
}
// 随后检查非 platform 模块使用 system certificate 的限制。
return mainCertificate, certificates
}真正的调用点在 AndroidApp.generateAndroidBuildActions:
源码文件:build/soong/java/app.go
// build/soong/java/app.go :: generateAndroidBuildActions
a.certificate, certificates = processMainCert(
a.ModuleBase, a.getCertString(ctx), certificates, ctx)
CreateAndSignAppPackage(ctx, packageFile, packageResources, jniJarFile,
dexJarFile, certificates, apkDeps, v4SignatureFile, lineageFile,
rotationMinSdkVersion, a.MinSdkVersion(ctx))这里的 owner 是 Soong 的 module variant。它只决定“这个 variant 要交给签名工具的文件”,不负责 验证安装时的兼容性。processMainCert 还会把缺失依赖变成 missing.pk8 和 missing.x509.pem 占位并报告错误;这不是一种可发布的签名成功状态。另一个边界是 system certificate 限制:当 非 platform 模块从默认 system certificate 目录取证书时,EnforceSystemCertificate 和 allowlist 可能在分析阶段拒绝它。
2.2 Make 预置包
预置 APK 不经过 Soong 的 android_app 属性时,build/make/core/app_prebuilt_internal.mk 用 LOCAL_CERTIFICATE 定义相同的契约,但 EXTERNAL 和 PRESIGNED 的语义不同。
源码文件:build/make/core/app_prebuilt_internal.mk
ifeq ($(LOCAL_CERTIFICATE),EXTERNAL)
LOCAL_CERTIFICATE := $(DEFAULT_SYSTEM_DEV_CERTIFICATE)
PACKAGES.$(LOCAL_MODULE).EXTERNAL_KEY := 1
endif
ifeq ($(LOCAL_CERTIFICATE),PRESIGNED)
PACKAGES.$(LOCAL_MODULE).CERTIFICATE := PRESIGNED
PACKAGES := $(PACKAGES) $(LOCAL_MODULE)
else
PACKAGES.$(LOCAL_MODULE).PRIVATE_KEY := $(LOCAL_CERTIFICATE).pk8
PACKAGES.$(LOCAL_MODULE).CERTIFICATE := $(LOCAL_CERTIFICATE).x509.pem
$(built_module): PRIVATE_PRIVATE_KEY := $(LOCAL_CERTIFICATE).pk8
$(built_module): PRIVATE_CERTIFICATE := $(LOCAL_CERTIFICATE).x509.pem
$(built_module): PRIVATE_CERTIFICATE_LINEAGE := $(LOCAL_CERTIFICATE_LINEAGE)
endifPRESIGNED 表示构建系统不重新签名;对 SDK 30 及以上的预置 APK,Make 还会避免会破坏 v2+ 签名的修改。EXTERNAL 则相反:构建期间用 dev key 让 dexpreopt 等流程可以继续,但记录 EXTERNAL_KEY,期待 release 阶段换成真正的 key。把两者都理解成“跳过签名”会导致 release 产物身份错误。
3. 签名动作
Soong 收集完证书后,build/soong/java/app_builder.go 先合并未签名 APK,再调用 SignAppPackage。证书按 pem, pk8 成对传给 SignApk;lineage 和 v4 是独立输入。
源码文件:build/soong/java/app_builder.go
func SignAppPackage(ctx android.ModuleContext, signedApk android.WritablePath,
unsignedApk android.Path, certificates []Certificate,
v4SignatureFile android.WritablePath, lineageFile android.Path,
rotationMinSdkVersion string, minSdkVersion android.ApiLevel) {
var certificateArgs []string
var deps android.Paths
for _, c := range certificates {
certificateArgs = append(certificateArgs, c.Pem.String(), c.Key.String())
deps = append(deps, c.Pem, c.Key)
}
outputFiles := android.WritablePaths{signedApk}
var flags []string
if v4SignatureFile != nil {
outputFiles = append(outputFiles, v4SignatureFile)
flags = append(flags, "--enable-v4")
}
if lineageFile != nil {
flags = append(flags, "--lineage", lineageFile.String())
deps = append(deps, lineageFile)
}
if rotationMinSdkVersion != "" {
flags = append(flags, "--rotation-min-sdk-version", rotationMinSdkVersion)
}
if minSdkVersion.GreaterThanOrEqualTo(android.UncheckedFinalApiLevel(24)) {
flags = append(flags, "--disable-v1")
}
// ctx.Build(... Rule: Signapk, Input: unsignedApk, Outputs: outputFiles ...)
}minSdk >= 24 时显式加入 --disable-v1,所以不能仅根据 APK 是否存在 META-INF 判断构建 配置。v4 的 .idsig 是额外输出,和 APK 不是同一个文件;v4 写入失败时,调用方必须把两者 视为一组工件重新生成,不能拿只生成了 APK 的目录继续做增量安装验证。
4. Release 映射
开发构建使用 dev key 并不意味着设备最终应该信任 dev key。META/apkcerts.txt 汇总每个 APK 的证书记录,sign_target_files_apks.py 在 target-files 到 release 包的阶段读取它并应用 OPTIONS.key_map。
源码文件:build/make/tools/releasetools/sign_target_files_apks.py
def GetApkCerts(certmap):
if OPTIONS.override_apk_keys is not None:
for apk in certmap.keys():
certmap[apk] = OPTIONS.override_apk_keys
for apk, cert in certmap.items():
certmap[apk] = OPTIONS.key_map.get(cert, cert)
for apk, cert in OPTIONS.extra_apks.items():
if not cert:
cert = "PRESIGNED"
certmap[apk] = OPTIONS.key_map.get(cert, cert)
return certmap
def BuildKeyMap(misc_info, key_mapping_options):
for s, d in key_mapping_options:
if s is None: # -d
devkey = misc_info.get("default_system_dev_certificate", ".../testkey")
devkeydir = os.path.dirname(devkey)
OPTIONS.key_map.update({
devkeydir + "/testkey": d + "/releasekey",
devkeydir + "/devkey": d + "/releasekey",
devkeydir + "/media": d + "/media",
devkeydir + "/shared": d + "/shared",
devkeydir + "/platform": d + "/platform",
devkeydir + "/networkstack": d + "/networkstack",
devkeydir + "/sdk_sandbox": d + "/sdk_sandbox",
})
else:
OPTIONS.key_map[s] = d映射只改变 release 阶段的签名输入,不改变 APK 的 package name 或 version code。它还会作用于 OTA 验证证书:ReplaceOtaKeys 按同一个 key_map 改写 META/otakeys.txt,并检查目标 .x509.pem 是否存在。APK 已重签而 OTA 验证 key 未替换,会得到“单个 APK 身份正确、整包升级信任错误”的 不一致状态。
PRESIGNED 不会被映射成 release key;-e 可以为指定 APK 增加或覆盖记录。release 失败时, 不要用“重签过的半成品”继续打包:应回到原始 target-files,修正 key map 或缺失 key,再重新 生成整个输出包。
5. Lineage 轮转
APK Signature Scheme v3 的 proof-of-rotation 把旧证书和能力写进签名数据。构建端只负责把 lineage 文件传给 SignApk;设备端是否接受旧证书,取决于 SigningDetails 的 capability 检查。
源码文件:frameworks/base/core/java/android/content/pm/SigningDetails.java
@IntDef(flag = true, value = {
CertCapabilities.INSTALLED_DATA,
CertCapabilities.SHARED_USER_ID,
CertCapabilities.PERMISSION,
CertCapabilities.ROLLBACK})
public @interface CertCapabilities {
int INSTALLED_DATA = 1;
int SHARED_USER_ID = 2;
int PERMISSION = 4;
int ROLLBACK = 8;
int AUTH = 16;
}这里要区分两个事实:AUTH 在 Android 17 文件中仍有常量定义,且 verifier 会把它作为 rotation flags 的一部分解析;但当前 @IntDef 的公开约束和本文追踪到的 Package Manager 消费者只把 INSTALLED_DATA、SHARED_USER_ID、PERMISSION、ROLLBACK 当作正式判断维度。不能把常量存在 误写成某个独立的安装授权流程。
SigningDetails.checkCapability 的语义是:当前 signer 直接匹配时通过;如果匹配的是历史 signer, 还必须检查该历史 signer 的 capability。hasAncestorOrSelf 则只回答 lineage 关系,不自动授予 某种 capability。
轮转的 owner 是 APK 的签名数据和 framework 保存的 SigningDetails,不是某个运行时服务。轮转 真正生效发生在设备比较两个 SigningDetails 的那一刻;仅仅在 keystore 中生成新私钥,不会让 旧包自动获得更新能力。
6. 解析身份
ParsingPackageUtils 明确把“解析 manifest”和“验证签名”分成两个阶段。parsePackage 的注释 直接说明它不执行签名验证;调用方需要在 collectCertificates 或 getSigningDetails 阶段完成。
源码文件:frameworks/base/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java
if ((flags & PARSE_COLLECT_CERTIFICATES) != 0) {
ParseResult<SigningDetails> ret =
getSigningDetails(input, pkg, false /*skipVerify*/);
if (ret.isError()) {
return input.error(ret);
}
pkg.setSigningDetails(ret.getResult());
} else {
pkg.setSigningDetails(SigningDetails.UNKNOWN);
}getSigningDetails 对 base APK 和 split APK 逐一收集并断言签名一致;skipVerify 为 true 时只 能用于调用方已经确认可信的场景。它最终调用 ApkSignatureVerifier.verify,因此 SigningDetails 是 verifier 的结果,而不是 manifest 中一个可任意填写的字段。
源码文件:frameworks/base/core/java/android/util/apk/ApkSignatureVerifier.java
try {
return verifyV4Signature(input, apkPath, minSignatureSchemeVersion, verifyFull);
} catch (SignatureNotFoundException e) {
if (minSignatureSchemeVersion >= SignatureSchemeVersion.SIGNING_BLOCK_V4) {
return input.error(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"No APK Signature Scheme v4 signature in package " + apkPath, e);
}
}
return verifyV3AndBelowSignatures(input, apkPath, minSignatureSchemeVersion, verifyFull);后续函数按 v3、v2、v1 继续尝试。只有 SignatureNotFoundException 表示“该方案不存在,可以 降级”;摘要不匹配、签名块损坏和证书编码错误都是失败。失败结果沿 ParseResult 返回,PMS 不会得到一个可继续安装的 SigningDetails。
7. 更新校验
安装更新时,InstallPackageHelper 在进入完整 reconcile 前调用 verifySignatures 做快速检查; ReconcilePackageUtils 随后再次检查并处理 system package、shared UID 和 lineage 合并。核心 比较不是简单的字符串相等:
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
boolean match = parsedSignatures.checkCapability(
pkgSetting.getSigningDetails(),
SigningDetails.CertCapabilities.INSTALLED_DATA)
|| pkgSetting.getSigningDetails().checkCapability(
parsedSignatures,
SigningDetails.CertCapabilities.ROLLBACK);
if (!match && compareRecover) {
match = matchSignaturesRecover(packageName, pkgSetting.getSigningDetails(),
parsedSignatures, SigningDetails.CertCapabilities.INSTALLED_DATA)
|| matchSignaturesRecover(packageName, parsedSignatures,
pkgSetting.getSigningDetails(), SigningDetails.CertCapabilities.ROLLBACK);
}
if (!match && isRollback) {
match = pkgSetting.getSigningDetails().hasAncestorOrSelf(parsedSignatures);
}
if (!match) {
throw new PackageManagerException(INSTALL_FAILED_UPDATE_INCOMPATIBLE,
"Existing package " + packageName
+ " signatures do not match newer version; ignoring!");
}正常更新首先要求已安装数据 capability;恢复比较是兼容旧 settings 数据的显式分支;rollback 还有一条特殊规则,允许回到设备上曾安装过的祖先 signer,即使 rollback capability 没有授予。 这三条路径不能合并成“支持密钥轮转”。它们的输入、允许范围和错误码都不同。
8. Shared UID 权限
shared UID 的 owner 是 SharedUserSetting 中保存的共同 SigningDetails。新包要加入 shared UID, verifySignatures 会调用 canJoinSharedUserId,并在 lineage 分叉时拒绝安装。即使某一次 capability 检查通过,hasCommonAncestor 仍可能成为第二道边界。
signature 权限则由 permission service 消费。Android 17 的 AppIdPermissionPolicy 使用 hasCommonSignerWithCapability(..., PERMISSION),并在 platform signature permission 上叠加 allowlist flag;“证书相同”不是唯一条件。
| 消费者 | 关键方法 | 历史证书所需 capability | 失败结果 |
|---|---|---|---|
| 包更新 | PackageManagerServiceUtils.verifySignatures | INSTALLED_DATA 或 ROLLBACK | INSTALL_FAILED_UPDATE_INCOMPATIBLE |
| shared UID | canJoinSharedUserId | SHARED_USER_ID | INSTALL_FAILED_SHARED_USER_INCOMPATIBLE |
| 签名权限 | AppIdPermissionPolicy.shouldGrantPermissionBySignature | PERMISSION | 不授予权限;platform 权限还受 allowlist 影响 |
| system package 对照 | matchSignatureInSystem | INSTALLED_DATA / ROLLBACK | 更新 system app 被拒绝或保留数据 |
9. 失败与恢复
签名链的失败点按 owner 分布,而不是都归到 apksigner:
| 阶段 | 典型失败 | 谁发现 | 可恢复动作 |
|---|---|---|---|
| Soong/Make | key 文件不存在、system certificate 不在 allowlist | 构建分析 | 修正 module/product 配置和依赖 |
| SignApk | lineage、ZIP 区段或私钥读取失败 | 签名规则 | 删除输出,基于 unsigned APK 重签 |
| release | key map 缺目标 key、PRESIGNED 状态不一致 | sign_target_files_apks.py | 回到原 target-files 重新映射 |
| 解析 | 方案缺失、摘要不匹配、证书编码错误 | ApkSignatureVerifier | 用原输入重新签名;不能把篡改当降级 |
| 更新 | lineage capability 不满足 | verifySignatures | 使用兼容旧身份的签名或明确 rollback 流程 |
| shared UID | lineage 分叉或 capability 被撤销 | PMS | 不能通过重试安装绕过,需统一签名身份 |
取消或中断时,构建系统不会把半成品变成可用 APK;尤其 .idsig 和 APK 必须成对清理。设备 安装失败时,PMS 抛出异常并保留既有 PackageSetting,不会因为一次失败更新自动替换旧的 SigningDetails。这就是为什么恢复动作应从原始输入重新生成,而不是修改已签名 ZIP。
10. 反向验证
10.1 签名验证
frameworks/base/core/tests/coretests/src/android/content/pm/SigningDetailsTest.java 的 mergeLineageWith_* 系列测试构造根证书、后继证书和分叉 lineage,断言祖先、后继、无共同根和 capability 修改时的返回对象。它证明的是 SigningDetails 的关系算法,不证明真实 APK 已由 SignApk 写入对应 proof-of-rotation。
10.2 输入验证
build/make/tools/releasetools/test_sign_target_files_apks.py 的 test_ReplaceCerts_duplicateEntries 先构造两个证书,再让 key map 把其中一个替换为另一个,断言 ReplaceCerts 抛出 AssertionError;test_CheckApkAndApexKeysAvailable_invalidApexKeys 则断言 APEX 容器和 payload 不能只有一边是 PRESIGNED。这些测试证明 release 映射的输入约束,不证明目标设备接受该包。
可以在本地做不写设备的源码验证:
git -C frameworks/base show android-17.0.0_r1:core/java/android/content/pm/SigningDetails.java \
| rg -n 'INSTALLED_DATA|SHARED_USER_ID|PERMISSION|ROLLBACK|AUTH'
git -C frameworks/base show android-17.0.0_r1:services/core/java/com/android/server/pm/PackageManagerServiceUtils.java \
| rg -n -C 3 'verifySignatures|checkCapabilityRecover|INSTALL_FAILED_'这两个命令只能验证固定 tag 中的符号和失败分支;它们不能证明你的产品配置、私钥权限、实际 target-files 或设备 OTA 签名。因此,真实 release 仍需对 META/apkcerts.txt、META/otakeys.txt 和最终 APK 使用 apksigner verify 做独立检查。
11. 源码导航
按下面顺序阅读,可以从构建输入一路走到 framework 消费者:
build/soong/java/app.go:processMainCert、Lineage、Additional_certificates。build/soong/java/app_builder.go:CreateAndSignAppPackage、SignAppPackage。build/make/core/app_prebuilt_internal.mk:EXTERNAL、PRESIGNED和预置 APK 证书依赖。build/make/tools/releasetools/sign_target_files_apks.py:BuildKeyMap、GetApkCerts、OTA key 替换。frameworks/base/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java:getSigningDetails。frameworks/base/core/java/android/util/apk/ApkSignatureVerifier.java:v4 到 v1 的降级入口。frameworks/base/core/java/android/content/pm/SigningDetails.java:字段、lineage 和 capability。frameworks/base/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java:更新与 shared UID 结果。frameworks/base/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt:签名权限消费者。
读者可以用一个具体模块复述这条链:它从哪个 certificate 属性取到哪两个文件,release 阶段是否 发生映射,设备解析后哪个 SigningDetails 字段承载历史证书,最后哪个消费者决定安装成功、权限 授予或回滚接受。只要其中一跳无法在上述源码中定位,说明仍把“签名工具”“证书身份”和“策略 消费”混在了一起。
