先看结论与判断条件

  • v1 审计的核心是把最终 APK 有效负载条目与 MANIFEST.MF 中的 Name 段落建立可复核对应。
  • META-INF 签名材料、目录项和实际负载承担不同角色,门禁不能把所有 ZIP 项用同一覆盖规则处理。
  • 重复名称、规范化后冲突、危险路径和清单指向不存在条目都应在验签之外单独报告。
  • apksigner 是签名方案与证书验证权威工具,独立条目清单用于补充审计,不应自造平台验签结论。
  • v2 通过 APK Signing Block 覆盖文件主体,现代发布应保存各方案结果和最低系统范围,不能只依赖 v1。
  • zipalign 必须位于签名之前,签名后的任何文件修改都产生新候选并使旧回执失效。

先给结论:验签结果与条目覆盖清单要同时保存

APK v1 签名采用 JAR 风格的逐条目摘要材料。发布门禁不能只记录 apksigner verify 返回成功,还要从最终候选枚举 ZIP 条目,并把有效负载与 META-INF/MANIFEST.MF 中的 Name 段落对应。这样才能发现清单未登记、清单指向不存在对象、重复名称和路径规范化冲突等需要复核的结构问题。

独立清单工具不能替代 Android 平台验签。它只回答候选包含什么、v1 清单声明覆盖什么以及结构是否存在歧义;签名方案有效性、证书身份和目标系统兼容仍由 apksigner 与真实安装路径验证。门禁应把两类回执并列保存,不把自写脚本的 pass 写成平台已接受。

现代 APK 还可能包含 v2 及更高签名方案。v1 的条目模型与 v2 的 APK Signing Block 覆盖方式不同,不能用一份 MANIFEST.MF 清单推导整包签名结论。发布报告应记录目标最低系统、各方案验证结果、最终证书和候选摘要,明确哪些结论属于 v1 覆盖审计,哪些属于平台签名验证。

APK v1 覆盖门禁的四类输入
输入回答的问题不能单独证明保存方式
最终 APK ZIP 清单候选实际有哪些条目条目已被有效签名名称、大小与压缩信息
MANIFEST.MFv1 声明哪些条目摘要证书和方案有效原文与解析结果
apksigner 回执方案与证书验证结果渠道和归档正确原始输出与工具版本
候选摘要报告绑定哪个文件业务代码安全SHA-256 与版本
系统范围需要哪些签名方案所有设备已兼容minSdk 与测试矩阵
构建来源候选如何产生运行语义通过流水线与输入摘要

先理解 MANIFEST.MF、SF 与签名块的不同职责

v1 签名材料通常位于 META-INF。MANIFEST.MF 以分段形式记录条目名称和摘要属性,SF 文件与相应签名块承担签名材料角色。覆盖审计重点是解析 MANIFEST.MF 的 Name 段落并与 ZIP 有效负载比较,不应把 SF、RSA、DSA、EC 或其他签名元数据本身当作普通业务负载再次要求同样记录。

解析不能只用逐行正则搜索。Manifest 属性可能存在续行,段落由空行分隔,名称需要在解折叠后读取。工具应保存原始字节并严格处理编码错误,遇到重复 Name、空名称或无法解析的段落时失败关闭。静默忽略异常段落会把未知覆盖状态伪装成完整。

条目存在清单摘要不等于摘要算法和签名链已经被平台接受。自写工具若进一步计算摘要,也只能检查文件内容与清单值的关系,不能复现全部平台验证规则。更可靠的边界是结构审计负责条目对应,apksigner 负责签名方案和证书验证,设备测试负责目标安装与升级路径。

META-INF 材料的审计角色
材料主要职责覆盖审计动作常见误判
MANIFEST.MF记录条目摘要段解析 Name 集合有文件就等于完整
SF 文件签名相关清单材料作为元数据登记按业务负载要求覆盖
RSA、DSA、EC保存签名块材料识别但不展开密钥文件名代表证书正确
SIG 前缀文件实现相关签名材料独立分类当作普通资源
目录项ZIP 结构信息排除有效负载比较未列入就是未签名
业务条目代码、资源与资产要求可解释覆盖只看数量不看名称

最终 APK 条目清单要先消除名称歧义

ZIP 可以包含重复文件名,解析库和下游工具对重复项的选择可能不同。覆盖审计应使用 infolist 保留所有物理条目,先检查完全重复名称,再检查移除多余分隔、点段或反斜线后的规范化冲突。发现冲突时不应猜测哪个条目生效,也不应靠清单中出现一次名称就放行全部重复对象。

路径安全也属于结构门禁。绝对路径、父目录段、空名称和反斜线混用都需要报告,工具绝不能把 APK 条目直接解压到工作目录。审计只读取中央目录与必要的 MANIFEST.MF 字节,限制条目数量和清单大小,避免把异常包当成普通输入处理。公开代码应以只读方式工作。

条目分类要基于规则并保存原因。目录项、Manifest 和签名元数据进入 metadata;其余代码、资源、原生库和 assets 进入 payload。某些工具链可能产生额外 META-INF 业务元数据,不能简单把整个目录排除。规则应只排除明确的签名材料,其余条目仍进入覆盖比较或人工复核。

未登记、悬空与重复 Name 都需要失败关闭

payload 条目存在于 APK,却没有对应 MANIFEST.MF Name 时,结构审计应输出 uncovered。它不立即声称平台一定接受未签名内容,因为实际结果还受签名方案和平台验证影响,但发布不能忽略这个差异。团队需要确认该条目为何缺失、目标最低系统如何处理,以及 v2 及更高方案是否按预期存在。

MANIFEST.MF 中出现 Name,但 ZIP 找不到对应条目,应输出 dangling。它可能来自构建残留、名称编码差异或后处理删除。即使 apksigner 对目标系统范围给出通过,悬空段也说明构建链与清单不一致,至少需要来源解释和可重复构建证据,不能把异常当作无害噪声长期保留。

重复 Name 段落会让结构语义不清,重复 ZIP 名称则让物理条目选择不清。门禁应分别报告 manifestDuplicates 和 zipDuplicates,并拒绝用集合去重掩盖。只有修复构建输入并重新生成、对齐、签名和验签,才能产生新的 publish-ready 候选;手工删除一项会改变文件身份和签名状态。

v1 条目对应关系的异常分类
异常观察事实不能直接声称处置
uncoveredpayload 无清单 Name平台一定可绕过检查方案与构建来源
dangling清单 Name 无 ZIP 条目候选一定无法安装清理构建残留
zipDuplicates多个物理条目同名某一个必然生效修复并重建
manifestDuplicates多个段落同 Name摘要一定相同拒绝歧义
normalizedCollision规范化后名称冲突所有工具解释一致人工审查并修复
unsafePath绝对或父目录路径已经发生利用阻止处理和发布

apksigner 结果必须覆盖方案、证书和系统范围

apksigner 可以验证 APK 的签名方案和证书,并根据目标 SDK 范围评估验证结果。发布流水线应固定工具版本,保存完整原始输出、退出状态、证书摘要和各方案结果。只截取最后一行 success 会丢失警告、方案覆盖和 signer 信息,也无法在后续调查中还原当时输入。

条目扫描与 apksigner 的关系是交叉核对。扫描器报告 v1 清单结构,apksigner 判断平台签名验证;两者结果不一致时,门禁保持 blocked 并调查工具输入、最低系统和候选摘要。不能修改扫描规则去迁就一次验签,也不能用自写摘要计算替代 apksigner。

签名有效只证明完整性与签名者身份范围,不证明业务代码安全。攻击面还包括应用内授权、服务端校验、运行时输入和分发渠道。文章不会把 v1 覆盖完整写成无法重打包,也不会把验签失败写成所有风险已被阻断。结论只描述实际工具与候选观察。

v2 及更高方案要和 v1 分层记录

APK Signature Scheme v2 使用 APK Signing Block 覆盖文件主体,并包含防止降级到较弱方案的机制。它与 v1 的逐条目清单模型不同。现代发布不能因为 MANIFEST.MF 对应完整就忽略 v2 及更高方案,也不能因为 v2 验证通过就删除 v1 兼容审计,具体要求取决于应用支持的系统范围。

方案矩阵应以目标最低系统为输入,记录 v1、v2 和候选包含的更高方案分别是否存在、验证是否通过以及 signer 是否符合允许策略。报告需要区分 absent、verified、failed 和 not-required,不能把所有状态压成一个 signed 布尔值。系统范围改变后要重新计算门禁。

降级保护是平台签名方案能力的一部分,不是项目已阻断所有重新打包的证明。分发渠道、用户安装来源、包名模仿和服务端接入仍有独立风险。签名门禁只负责官方更新身份和候选完整性证据,业务系统仍要核对账号、设备、版本和授权。

签名方案分层回执
方案层主要覆盖模型回执字段不能替代
v1JAR 条目摘要与签名材料条目对应、验证、signerv2 整包验证
v2APK Signing Block 与文件主体存在、验证、证书外部资产完整性
更高方案平台新增身份能力按候选实际记录旧系统路径
目标系统决定验证需求minSdk 与测试范围所有设备结论
允许证书批准发布身份完整证书摘要密钥治理证明
候选摘要绑定全部回执文件 SHA-256业务安全

对齐、签名和渠道处理的顺序不能颠倒

Android zipalign 文档要求在 apksigner 之前完成对齐,并可单独验证对齐结果。对齐会改变 APK 字节布局,签名之后再次执行会使原签名回执失效。发布流水线应固定顺序为构建、必要后处理、对齐、签名、验签和归档,不能为修复条目问题直接编辑已签名包。

渠道元数据和资源处理也必须发生在身份冻结之前。任何注入、删除、重压缩或重排都可能改变文件摘要和签名状态。若渠道必须产生多个最终包,每个包都需要独立条目清单、对齐回执、apksigner 回执和候选摘要,不能共用母包证据。

构建顺序正确仍不证明密钥治理正确。Android 发布文档区分应用签名密钥、上传密钥和证书责任,Play App Signing 还可能让上传对象与设备交付对象具有不同角色。条目覆盖报告必须标明 artifactRole,避免用上传包或中间包替代最终设备 APK。

用只读代码生成 v1 条目覆盖差异

自动扫描器应只读取最终 APK,不解压文件到磁盘。它先检查 ZIP 条目数量、重复名称和不安全路径,再读取受限大小的 MANIFEST.MF,处理续行并提取 Name。签名元数据采用明确规则分类,其余 payload 与 Name 集合比较,输出 uncovered、dangling、duplicates 和 collisions。

下面的 Python 示例使用 zipfile.infolist 保留重复物理条目,通过 PurePosixPath 检查路径,并解析 MANIFEST.MF 段落。它不读取私钥、不修改 APK、不执行条目内容,也不尝试复现 Android 签名验证。发现结构异常时输出 JSON 后失败,便于流水线保留证据并阻止自动发布。

脚本通过只表示清单对应关系在该规则下没有发现异常,不表示 v1 摘要值、证书或其他签名方案有效。正式门禁仍要运行 apksigner,并把工具输出、候选摘要、系统范围和允许证书绑定。若自定义工具链产生特殊 META-INF 条目,需要在规则中逐项登记理由,不能直接排除整个目录。

APK v1 清单与 ZIP 条目只读覆盖检查器
from pathlib import Path, PurePosixPath
from collections import Counter
import json
import sys
import zipfile

if len(sys.argv) != 2:
    raise SystemExit("usage: audit_v1_entries.py application.apk")

apk_path = Path(sys.argv[1]).resolve()
if not apk_path.is_file():
    raise SystemExit(f"APK missing: {apk_path.name}")

def is_signing_metadata(name):
    upper = name.upper()
    if upper == "META-INF/MANIFEST.MF":
        return True
    if not upper.startswith("META-INF/"):
        return False
    leaf = upper.rsplit("/", 1)[-1]
    return leaf.endswith((".SF", ".RSA", ".DSA", ".EC")) or leaf.startswith("SIG-")

def normalized_name(name):
    return str(PurePosixPath(name.replace("\\", "/")))

with zipfile.ZipFile(apk_path) as archive:
    entries = archive.infolist()
    if not entries or len(entries) > 200000:
        raise SystemExit("APK entry count is empty or outside the review limit")
    names = [entry.filename for entry in entries if not entry.is_dir()]
    exact_duplicates = sorted(name for name, count in Counter(names).items() if count > 1)
    unsafe_paths = sorted(name for name in names if name.startswith("/") or ".." in PurePosixPath(name.replace("\\", "/")).parts)
    normalized = [(name, normalized_name(name)) for name in names]
    groups = {}
    for original, canonical in normalized:
        groups.setdefault(canonical, []).append(original)
    normalized_collisions = {key: value for key, value in groups.items() if len(set(value)) > 1}
    manifest_candidates = [entry for entry in entries if entry.filename.upper() == "META-INF/MANIFEST.MF"]
    if len(manifest_candidates) != 1:
        raise SystemExit("APK must contain exactly one META-INF/MANIFEST.MF for this v1 audit")
    manifest_entry = manifest_candidates[0]
    if manifest_entry.file_size > 8 * 1024 * 1024:
        raise SystemExit("MANIFEST.MF exceeds the review size limit")
    manifest_text = archive.read(manifest_entry).decode("utf-8", errors="strict").replace("\r\n", "\n")

lines = manifest_text.split("\n")
unfolded = []
for line in lines:
    if line.startswith(" ") and unfolded:
        unfolded[-1] += line[1:]
    else:
        unfolded.append(line)
manifest_names = []
for line in unfolded:
    if line.startswith("Name: "):
        value = line[6:]
        if not value:
            raise SystemExit("MANIFEST.MF contains an empty Name attribute")
        manifest_names.append(value)
manifest_duplicates = sorted(name for name, count in Counter(manifest_names).items() if count > 1)
payload = sorted(name for name in names if not is_signing_metadata(name))
manifest_set = set(manifest_names)
uncovered = sorted(set(payload) - manifest_set)
dangling = sorted(manifest_set - set(names))
report = {
    "apk": apk_path.name,
    "payload_count": len(payload),
    "manifest_name_count": len(manifest_names),
    "uncovered": uncovered,
    "dangling": dangling,
    "zip_duplicates": exact_duplicates,
    "manifest_duplicates": manifest_duplicates,
    "normalized_collisions": normalized_collisions,
    "unsafe_paths": unsafe_paths,
    "next_gate": "run apksigner verify and bind its full output to this APK digest",
}
print(json.dumps(report, ensure_ascii=False, indent=2))
if any((uncovered, dangling, exact_duplicates, manifest_duplicates, normalized_collisions, unsafe_paths)):
    raise SystemExit("v1 entry coverage audit found review-blocking differences")

发布证据要让下一次构建能够复算

NIST SSDF 强调保留来源、构建、验证和变更证据。对应 v1 覆盖审计,证据包包含最终 APK 摘要、ZIP 条目清单、MANIFEST.MF 原文、解析器版本、结构差异、zipalign 回执、apksigner 原始输出、证书摘要、系统范围和构建来源。所有材料必须指向同一候选。

报告要区分事实与限制。可以登记候选有多少物理条目、是否存在重复名称、哪些 payload 缺少 Name、apksigner 对哪些方案给出什么结果;不能写成所有文件绝对可信、无法重新打包、密钥没有泄露或业务授权正确。没有项目数据时也不提供客户、排名、流量和性能数字。

准备实际审计时,可先查看[APK、DEX 与签名完整性指南](/zh-cn/apk-dex-signing-integrity/),整理最终 APK、目标系统、签名方案、证书允许策略和渠道步骤。需要提交候选,可从[御盾中央平台申请加固服务](https://www.leonadev.com/console/)。材料应脱敏,登录、价格、购买和控制台操作统一由中央平台承接。

  • 审计对象是最终 APK,并绑定文件 SHA-256
  • ZIP 物理条目与 MANIFEST.MF Name 已双向比较
  • 重复名称、规范化冲突和不安全路径均失败关闭
  • v1、v2 及候选更高方案分别记录验证状态
  • zipalign 位于签名之前,签名后没有任何修改
  • 上传、中间、渠道和设备交付对象角色明确
  • 结构通过、验签通过和业务安全保持不同边界

事实依据与适用边界

以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。

本文判断事实或工程依据适用限制
Android 使用应用签名建立更新身份,不同签名方案覆盖相应文件区域和平台版本。AOSP app signing 说明 Android 应用签名与签名方案总体职责。签名有效只证明完整性与签名者身份范围,不证明业务代码安全。
v2 使用 APK Signing Block 覆盖文件主体,并包含防止降级到较弱方案的机制。APK Signature Scheme v2 说明 v2 的文件覆盖与降级保护设计。v2 不覆盖 APK 外部分发资产,也不证明业务授权与运行时输入安全。
apksigner 可以验证签名方案与证书,并要求签名后不再修改 APK。Android apksigner 说明 sign、verify、证书与签名后修改边界。工具成功不证明密钥治理、渠道流程、候选归档或条目结构没有异常。
APK 对齐应在 apksigner 之前完成,并可单独校验对齐。Android zipalign 说明 zipalign 与 apksigner 的正确处理顺序。对齐通过不证明签名、资源内容或运行兼容性通过。
发布需要区分应用签名密钥、上传密钥、证书与 Play App Signing 责任。Sign your Android app 说明 Android 发布签名角色和密钥责任。文档不能确认某个实际 APK 使用了批准的生产证书。
安全发布应保存来源、构建、验证与变更证据,并纳入供应链风险。NIST SP 800-218 SSDF 给出组织级安全软件开发与发布实践。SSDF 不定义 v1 清单解析规则,也不证明某个候选已通过。
v1 覆盖审计应双向比较 payload 条目和 MANIFEST.MF Name。工程判断:单向检查会漏掉未登记 payload 或指向不存在条目的悬空记录。结构对应不替代摘要、证书和平台签名方案验证。
重复 ZIP 名称和规范化冲突应阻止自动发布。工程判断:不同解析器可能选择不同物理条目,集合去重会掩盖歧义。发现歧义不等于已经发生利用,仍需修复构建并重新验签。
发布回执需要绑定最终 APK、条目清单、Manifest、验签和系统范围。项目证据尚未接入:本文列出必须保存的字段,不声称当前候选已完成审计。脚本通过、命令成功或文件存在不能单独登记签名与发布通过。
本文不提供客户、排名、性能或攻击阻断结论。项目证据尚未接入:没有真实候选与平台回执时不扩张事实。公开代码只用于只读结构诊断,不能替代平台验签和具体产品验证。

工程常见问题

apksigner verify 成功为什么还要检查 v1 条目清单?

apksigner 负责平台签名方案验证,独立清单用于发现重复名称、覆盖对应和构建结构异常,两者证据职责不同。

MANIFEST.MF 有 Name 是否证明该条目摘要正确?

不证明。Name 只表示存在声明,还要由签名验证工具核对摘要、签名材料和证书。

META-INF 下所有文件都要出现在 MANIFEST.MF 吗?

不能这样要求。Manifest、SF 和签名块属于签名元数据,应按角色分类;其他 META-INF 业务条目则需单独判断。

重复 ZIP 名称可以取最后一个再继续吗?

不应。不同工具可能选择不同物理条目,门禁应拒绝歧义并从构建源修复,而不是自行决定生效项。

v2 验证通过后可以忽略 v1 吗?

取决于应用支持的系统范围。门禁应按 minSdk 记录各方案要求,不能从单一方案结果外推全部平台。

zipalign 应该放在签名之前还是之后?

应在 apksigner 之前完成。签名后修改字节布局会使已有签名回执失效,必须重新产生候选。

覆盖脚本发现 uncovered 是否等于存在可利用漏洞?

不等于。它表示结构差异需要调查,实际平台行为还取决于签名方案、系统版本和验签结果。

扫描脚本通过是否能证明 APK 无法被重打包?

不能。它只检查 v1 条目对应结构,不能证明密钥安全、分发身份、业务授权或所有攻击路径。

想用自己的 App 验证?

提交候选包、目标系统和关键业务路径,申请御盾 PoC 与兼容性评估。

继续阅读: APK、DEX 与签名完整性如何一起验收