Skip to content

APK签名工具链

追踪 SignApk、ApkSignerEngine 与 ApkSignatureVerifier 如何完成 APK 签名生成、文件布局和设备验证。

基于android-17.0.0_r1
AndroidAPKapksigner签名PackageManager

APK签名工具链 ​

签名工具的输出不是“给 APK 加一个证书”。它同时改变 ZIP 条目、META-INF、APK Signing Block、 EOCD 偏移和可选的 .idsig 工件;设备侧又会按最低签名方案、文件格式和完整性摘要重新验证。只看 apksigner sign 命令,很难解释为什么签名后再 zipalign 会失效,为什么 v4 验证仍要依赖 APK 内的 v2/v3,或者为什么同一个证书在更新安装时仍可能被拒绝。

本文以 Android 17 checkout 的源码为准。生成入口是 build/make/tools/signapk/src/com/android/signapk/SignApk.java,它创建 DefaultApkSignerEngine 并消费 ApkSignerEngine 请求;设备侧入口是 frameworks/base/core/java/android/util/apk/ApkSignatureVerifier.java。本文不把某个 SDK Build Tools 发行包的 CLI 参数表当作源码教学,也不展开完整证书密码学算法;重点是从 APK 输入到设备验证结果的 字节、对象和失败路径。

本文面向已经理解 APK 安装事务、但还不能把签名文件布局与设备验证连接起来的读者;安装 session 的 前置边界见 ADB安装与权限管理。读完后,应能从 SignApk.main 追到 v1/v2/v3/v4 工件,再从 ApkSignatureVerifier 追到 SigningDetails 消费者, 并用一次篡改实验区分“证书可读取”“摘要完整”和“更新身份兼容”。

1. 生成端与消费端 ​

SignApk 负责读取私钥/证书、解析输入 APK、生成 v1 JAR 条目和 v2/v3 Signing Block,并在启用时生成 v4 工件。ApkSignatureVerifier 不读取 SignApk 的 Java 对象,而是从最终 APK 的 ZIP 区段和签名块 重建证书、摘要和 SigningDetails。两边通过文件格式连接,而不是共享内存状态。

PackageParser/ParsingPackageUtils 在解析 APK 时调用 verifier,并将结果交给包解析和安装路径。因而 签名工具的“成功”只表示生成端写出了格式正确的工件;最终是否能安装、更新或获得签名权限,要等 framework 消费 SigningDetails 后才能判断。

2. 输入解析 ​

旧版 SignApk 的命令行解析仍保留在 Android 17 源码中。默认同时打开 v1 和 v2,v4 需要显式启用; 最低 SDK 可以从 AndroidManifest.xml 推断,也可由参数覆盖。这个最低版本会影响签名引擎选择兼容 方案,而不是改变证书本身。

源码文件:build/make/tools/signapk/src/com/android/signapk/SignApk.java

java
// build/make/tools/signapk/src/com/android/signapk/SignApk.java :: main
boolean signUsingApkSignatureSchemeV1 = true;
boolean signUsingApkSignatureSchemeV2 = true;
boolean signUsingApkSignatureSchemeV4 = false;

// ... 解析命令行选项

if (signUsingApkSignatureSchemeV4) {
    // v4 需要独立的 idsig 输出;没有可用条件时由后续流程报告错误。
    outputV4Signature = new File(outputFilename + ".idsig");
}

这段状态属于 host 工具。它不会告诉设备“必须使用 v2”或“v3 一定存在”;设备验证器仍会根据 APK 中 实际存在的 block、调用方要求的最低方案和当前平台能力决定接受哪一条路径。也就是说,--v2-signing-enabled 是生成策略,不是安装策略。

3. ZIP签名 ​

签名时必须先完成可签名的 ZIP 内容,再写入签名材料。findMainZipSections 将输出文件拆成 beforeCentralDir、Central Directory 和 EOCD;v2/v3 block 插入在前两者之间,所以 EOCD 中的 Central Directory offset 必须随之调整。

源码文件:build/make/tools/signapk/src/com/android/signapk/SignApk.java

java
// build/make/tools/signapk/src/com/android/signapk/SignApk.java :: findMainZipSections
ApkUtils.ZipSections sections = ApkUtils.findZipSections(apk);
long centralDirStartOffset = sections.getZipCentralDirectoryOffset();
long eocdStartOffset = sections.getZipEndOfCentralDirectoryOffset();
if (centralDirStartOffset + sections.getZipCentralDirectorySizeBytes()
        != eocdStartOffset) {
    throw new ZipFormatException("ZIP Central Directory is not immediately followed by "
            + "End of Central Directory");
}

result.beforeCentralDir = apk.slice(0, centralDirStartOffset);
result.centralDir = apk.getByteBuffer(centralDirStartOffset, centralDirSize);
result.eocd = apk.getByteBuffer(eocdStartOffset, eocdSize);

校验 Central Directory 与 EOCD 相邻,是为了保证后续插入 Signing Block 时偏移关系可计算。输入 APK 结构不满足这个不变量时,工具应停止,而不是生成一个设备侧再失败的 APK。

4. v1条目请求 ​

DefaultApkSignerEngine 先处理 ZIP 条目。outputJarEntries() 返回 v1 请求后,SignApk 调用 addV1Signature 将 MANIFEST.MF、.SF 和证书条目写入输出 JAR。v1 保护的是条目摘要,不直接覆盖 Central Directory 与 EOCD 元数据。

源码文件:build/make/tools/signapk/src/com/android/signapk/SignApk.java

java
// build/make/tools/signapk/src/com/android/signapk/SignApk.java :: sign APK main path
try (ApkSignerEngine apkSigner = builder.build()) {
    // 输入 APK 的旧 Signing Block 不复用,避免把旧签名带入新输出。
    apkSigner.inputApkSigningBlock(null);

    JarOutputStream outputJar = new JarOutputStream(outputFile);
    copyFiles(inputJar, null, apkSigner, outputJar, outputJarCounter, timestamp, alignment);

    ApkSignerEngine.OutputJarSignatureRequest request = apkSigner.outputJarEntries();
    if (request != null) {
        addV1Signature(apkSigner, request, outputJar, timestamp);
        request.done();
    }
    outputJar.close();
}

request.done() 是生命周期边界:它告诉签名引擎 v1 请求已经被消费。若在完成请求前关闭输出,证书 条目可能不完整;若保留输入 Signing Block,则会把已失效的旧摘要与新 ZIP 内容混在一起。因此“先复制 条目、再请求签名、最后关闭 JAR”不是风格选择,而是引擎协议的消费顺序。

5. v2/v3 Block ​

v2/v3 不是普通 ZIP entry,而是插入 Central Directory 前的特殊块。outputZipSections2 接收已经 写完的 beforeCentralDir、Central Directory 和 EOCD,返回 block 字节及所需 padding;调用方再把 block 写回输出,并修改 EOCD 的 Central Directory offset。

源码文件:build/make/tools/signapk/src/com/android/signapk/SignApk.java

java
// build/make/tools/signapk/src/com/android/signapk/SignApk.java :: v2/v3 output loop
ApkSignerEngine.OutputApkSigningBlockRequest2 addV2SignatureRequest =
        apkSigner.outputZipSections2(
                zipSections.beforeCentralDir,
                DataSources.asDataSource(zipSections.centralDir),
                DataSources.asDataSource(eocd));
if (addV2SignatureRequest == null) break;

int padding = addV2SignatureRequest.getPaddingSizeBeforeApkSigningBlock();
byte[] apkSigningBlock = addV2SignatureRequest.getApkSigningBlock();
ByteBuffer modifiedEocd = ByteBuffer.allocate(eocd.remaining());
modifiedEocd.put(eocd);
modifiedEocd.flip();
modifiedEocd.order(ByteOrder.LITTLE_ENDIAN);
ApkUtils.setZipEocdCentralDirectoryOffset(
        modifiedEocd,
        zipSections.beforeCentralDir.size() + padding + apkSigningBlock.length);
addV2SignatureRequest.done();

v2 和 v3 的差异主要在 Signing Block 中的 signer 数据:v3 还携带 proof-of-rotation,允许设备把新 证书与历史证书联系起来。文件布局层面两者都依赖同一个“block 位于 ZIP Central Directory 前”的不变量。 因此对已签名 APK 再运行会改变被摘要的区段,必须重新生成签名,不能只替换一个证书文件。

6. v4独立工件 ​

v4 不把 Merkle 根和签名写回 APK,而是写入同目录的 .idsig。它服务于增量文件系统的块级验证, 但设备仍需要 APK 内的签名方案提供证书身份和整体关系。SignApk 在输出 APK 完成后才进入 v4 路径, 所以 .idsig 的生成不能替代 APK 本体签名。

源码文件:build/make/tools/signapk/src/com/android/signapk/SignApk.java

java
// build/make/tools/signapk/src/com/android/signapk/SignApk.java :: v4 output path
if (signUsingApkSignatureSchemeV4) {
    final DataSource outputApkIn = DataSources.asDataSource(
            new RandomAccessFile(new File(outputFilename), "r"));
    final File outputV4File = new File(outputV4Filename);
    apkSigner.signV4(outputApkIn, outputV4File, false /* ignore failures */);
}

v4 请求为空或写入失败时,APK 文件可能已经生成;调用方必须把 APK 与 .idsig 作为一组工件处理, 不能只上传其中一个。该路径的生效时机发生在增量安装消费者读取 idsig 时,而不是普通 ZIP 解析时。

7. 设备验证降级 ​

设备侧入口 verifySignaturesInternal 按 v4、v3.1/v3、v2、v1 的顺序尝试。SignatureNotFoundException 表示该方案不存在,可以降级;解析错误、摘要不匹配或签名密码学验证失败则属于失败,不应静默当成“没有 这个方案”。

源码文件:frameworks/base/core/java/android/util/apk/ApkSignatureVerifier.java

java
// frameworks/base/core/java/android/util/apk/ApkSignatureVerifier.java :: verifySignaturesInternal
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);
    }
}
return verifyV3AndBelowSignatures(
        input, apkPath, minSignatureSchemeVersion, verifyFull);

verifyV3AndBelowSignatures 内部以同样的 try/catch SignatureNotFoundException 结构依次尝试 v3 和 v2, 最后才调用 v1。这里的“降级”只针对缺失方案或版本条件,不是对篡改内容放宽要求。v2/v3 verifier 在解析 signer 后还会 调用 ApkSigningBlockUtils.verifyIntegrity;任何摘要不匹配都会使该路径失败。成功返回的消费者是 SigningDetailsWithDigests,随后被 PackageParser、PackageSessionVerifier 和权限/更新检查使用。

图中的“缺失”可以触发下一方案,“解析失败”和“完整性异常”不能被当成缺失。这个区分是安装错误 诊断的关键:没有 v3 但有合法 v2 是兼容降级;v3 block 存在却摘要不匹配,则是被篡改或生成链损坏。

8. v2完整性校验 ​

v2 verifier 先读取长度前缀的 signer 列表,限制 signer 数量,要求至少一个 signer 和至少一个 content digest,然后再做完整性校验。这个顺序把“签名结构可解析”和“APK 字节未被修改”分成两个状态。

源码文件:frameworks/base/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java

java
// frameworks/base/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java :: verify
while (signers.hasRemaining()) {
    signerCount++;
    if (signerCount > MAX_V2_SIGNERS) {
        throw new SecurityException("APK Signature Scheme v2 only supports a maximum of "
                + MAX_V2_SIGNERS + " signers");
    }
    ByteBuffer signer = getLengthPrefixedSlice(signers);
    signerCerts.add(verifySigner(signer, contentDigests, certFactory));
}
if (signerCount < 1) {
    throw new SecurityException("No signers found");
}
if (contentDigests.isEmpty()) {
    throw new SecurityException("No content digests found");
}
if (doVerifyIntegrity) {
    ApkSigningBlockUtils.verifyIntegrity(contentDigests, apk, signatureInfo);
}

verifySigner 还会比较签名算法、证书公钥和 signed data 中的摘要。读者排查“签名块存在但安装失败” 时,应区分 signer 结构错误、证书/公钥不一致和 APK 内容摘要错误,三者都不是简单的“缺少 v2”。

9. 证书消费者 ​

verifier 返回的不是布尔值,而是签名证书、签名方案版本、历史证书和 content digest。SigningDetails 会被已安装包记录,并在更新时比较新旧签名;v3 的 proof-of-rotation 让旧证书历史可以携带能力,而不是 把“证书相同”简化为单个字节数组比较。

源码文件:frameworks/base/core/java/android/content/pm/SigningDetails.java

java
// frameworks/base/core/java/android/content/pm/SigningDetails.java :: hasCertificate
public boolean hasCertificate(@NonNull Signature signature,
        @CertCapabilities int flags) {
    return hasCertificateInternal(signature, flags);
}

private boolean hasCertificateInternal(@NonNull Signature signature, int flags) {
    if (this == UNKNOWN) return false;

    if (hasPastSigningCertificates()) {
        for (int i = 0; i < mPastSigningCertificates.length - 1; i++) {
            if (mPastSigningCertificates[i].equals(signature)) {
                if (flags == PAST_CERT_EXISTS
                        || (flags & mPastSigningCertificates[i].getFlags()) == flags) {
                    return true;
                }
            }
        }
    }
    return mSignatures.length == 1 && mSignatures[0].equals(signature);
}

循环刻意排除历史数组的最后一个元素,因为源码把当前证书也放在该数组末尾;当前 signer 在最后一行单独 判断,并自动拥有全部 capability。历史证书则必须满足“只检查是否存在”或 flags 全包含条件。当前签名与 历史签名的消费语义不同:当前证书决定新 APK 身份,历史证书只有在 proof-of-rotation 标记授予相应 capability 时才能参与权限、共享 UID 或已安装数据兼容判断。签名工具生成 lineage 并不自动 保证更新成功,framework 仍会检查 capability 和安装上下文。

10. 失败与清理 ​

阶段失败输入结果恢复边界
读取密钥密码错误、证书与私钥不匹配host 终止,不应发布输出修正密钥输入后重新签名
ZIP解析Central Directory/EOCD 不连续不创建可信签名块修复 APK 结构,不手工改 offset
v1请求输出 JAR 未完成或请求未 done()META-INF 不完整删除输出并重跑,不复用半成品
v2/v3请求block 写入或 EOCD 调整失败APK 可能已截断校验文件长度和 ZIP,再重新生成
v4请求.idsig 缺失/写入失败APK 与 idsig 不成组清理孤立 idsig,重新生成整组工件
设备验证digest、签名或证书错误安装解析失败回到生成端比较方案、证书和对齐顺序
更新检查新旧 SigningDetails 不兼容更新被拒绝检查 lineage/capability,不降低设备校验

签名工具不拥有设备上的已安装包状态;它只能保证输出工件符合生成协议。安装失败后的第一份证据应是 apksigner verify --verbose(或等价 verifier 输出)、证书摘要、APK/idsig 是否成组,以及设备侧解析 错误,而不是只重签一次。

11. 测试与验证 ​

源码测试和 fixture 应分层理解。SignApk 的主要证据是 ApkSignerEngine 请求消费顺序和生成器分支; framework 侧 verifier 的对应测试来自签名解析、摘要校验和 Package Manager 测试资源。它们证明正常/失败 分支的控制流和格式约束,不证明任意厂商 keystore、所有 SDK Build Tools 发行版本或真实设备的增量安装。

读者可以用一个最小实验建立闭环:

bash
# 仅在测试 APK 和测试密钥上执行
apksigner sign --ks test.jks --ks-key-alias test --out signed.apk unsigned.apk
apksigner verify --verbose --print-certs signed.apk
unzip -l signed.apk | sed -n '/META-INF/p'

然后复制 APK 后修改一个非签名工具会重写的字节,再次验证;预期是 v2/v3 完整性失败,而不是“仍能读取 证书”。再把 --v1-signing-enabled true --v2-signing-enabled false 的结果交给不同 min SDK 的验证 环境,观察方案存在性与完整性检查的区别。实验只能证明所用工具和输入,不能外推到所有平台策略。

源码阅读练习是从 SignApk.main 追到 outputZipSections2,再从 ApkSignatureVerifier.verifySignaturesInternal 追到 ApkSigningBlockUtils.verifyIntegrity,最后说明 SigningDetails 哪些字段被更新检查消费。完成这条链,才能判断问题发生在 ZIP 结构、签名请求、摘要验证 还是证书兼容性,而不是把所有安装错误归为“签名不对”。