先看结论与判断条件
- App Link 关联同时依赖最终 Manifest URL、应用包名、最终签名证书和域名上的 Digital Asset Links 声明。
- 验收对象必须是准备发布的最终 APK 或商店真实签名结果,上传密钥证书不能代替设备最终看到的应用签名证书。
- assetlinks.json 可以登记受控证书集合,但每个指纹都要有明确发布用途、轮换边界和删除条件,不能无限累积。
- 静态三方比对先检查包名、主机、relation 和证书集合,设备验证再检查系统关联状态、冷启动、路径和异常参数。
- 系统显示域名已验证只证明关联层成立,不能替代应用内路由、登录状态、参数校验和业务授权。
- 每次证书、包名、主机、路径规则或渠道签名责任变化都要重新验收,并保留最终候选与线上声明的时间点证据。
证书漂移问题要从三方身份链检查
Android App Links 不是 APK 内部的单一开关。Manifest 声明应用愿意处理哪些 HTTPS 主机与路径,网站上的 Digital Asset Links 声明哪些应用身份可以处理该域名,Android 系统再结合应用包名和签名证书建立关联。最终 APK 的证书指纹一旦变化,即使包名、页面和 Manifest 都没有改,域名文件若仍只登记旧指纹,系统也可能无法把链接直接交给应用。
证书漂移可能来自计划内轮换,也可能来自构建变体串用、渠道重签、Play App Signing 责任误解、测试包进入发布或验收对象被替换。验收不能先假定原因,而应从最终候选重新提取证书,再与发布台账和域名声明比较。只有三方值一致,才能进入设备验证;发现未知指纹时应停止发布并追查候选来源,不能临时把未知指纹加入 assetlinks.json 让测试变绿。
本文只回答证书指纹变化怎样验收,不重复讨论 AAB 和 split APK 的完整发布身份,也不把 App Link 验证扩大成应用内授权指南。结论要严格区分静态事实、系统关联状态和业务回归:证书与声明一致是必要条件,设备显示 verified 是系统证据,用户是否进入正确页面并完成授权仍需应用测试。没有真实候选和线上回执时,不能宣称某个域名已经通过。
| 证据层 | 关键字段 | 取得位置 | 错配结果 |
|---|---|---|---|
| 最终 APK | packageName 与签名证书 | 最终候选和 apksigner 回执 | 域名声明无法匹配应用身份 |
| Manifest | scheme、host、path 与 autoVerify | 最终 APK 二进制清单 | 系统不尝试或错误匹配链接 |
| assetlinks.json | relation、包名和证书指纹 | 目标域名固定路径 | 目标应用身份未被域名授权 |
| 系统验证 | 每个域名的关联状态 | 目标设备域名验证状态 | 链接仍进入选择器或浏览器 |
| 应用路由 | 目标页面与参数处理 | 冷启动和前台端到端测试 | 关联成功但页面或状态错误 |
| 业务授权 | 登录、权限和敏感动作 | 应用与服务端规则 | 深链被错误当作授权依据 |
先从最终候选提取真实签名者
Android apksigner 可以验证 APK 的签名方案和证书,并强调签名后不应再修改 APK。验收脚本应直接读取准备交付的最终文件,保存候选摘要、包名、versionCode、证书 SHA-256 指纹和签名验证回执。不能从 Gradle 配置、keystore 文件名或流水线变量推断实际签名者,因为这些只表示预期配置,不是最终二进制事实。回执还要绑定文件摘要,避免证书检查的是一个包,设备测试用的是另一个包。
Sign your Android app 区分应用签名密钥、上传密钥、证书与 Play App Signing 的责任。采用 Play App Signing 时,开发者上传候选所用证书可能不是商店交付到设备的最终应用签名证书。assetlinks.json 应登记设备最终验证所对应的应用签名身份,而不是把上传证书误当成最终证书。若同时存在侧载、企业渠道或多个商店,还要逐渠道记录最终签名责任,不能用一个渠道回执覆盖全部交付。
证书指纹应采用规范化比较。assetlinks.json 常见表示包含冒号和大写十六进制,工具输出也可能使用不同分隔方式;比较前可以去除分隔符并统一大小写,但不能截短。输出报告同时保留规范化值和原始来源,便于人工复核。任何无法解析、长度异常或重复来源冲突都应失败,不能使用模糊匹配或只比较开头字符。
- 证书从最终 APK 读取而非从配置推断
- apksigner 验证与候选文件摘要绑定
- 包名和 versionCode 来自同一最终候选
- 上传证书与最终应用签名证书责任分开
- 每个分发渠道拥有独立最终签名回执
- 指纹做完整规范化比较且不允许截短
Manifest 主机必须从最终 APK 重新盘点
Android app manifest 定义组件、权限、intent filter、SDK 约束和应用元数据。App Link 验收需要从最终 APK 的 Manifest 提取所有 HTTPS intent filter、host、path 规则和 autoVerify 状态,而不是只查看源码文件。构建变体、库 Manifest 合并和占位符可能让最终主机集合与仓库中的单一配置不同。缺少主机、意外新增测试域名或把 HTTP 与 HTTPS 混淆,都应在联网验证前被发现。
主机比较要明确规范化规则。域名大小写、前导点、通配配置和国际化域名需要按平台规则处理,不能用简单字符串包含判断。路径部分也要逐项保存 path、pathPrefix 或其他匹配形式,因为同一域名验证通过并不表示所有 URL 都能进入相同页面。本文的核心是证书关联,但若最终 Manifest 没有声明目标主机,单独补 assetlinks.json 不会建立完整链接。
每个主机还要标记业务所有者与声明文件来源,防止发布团队修改一个域名却遗漏另一个。静态门禁输出“最终候选声明主机集合”“计划允许主机集合”和“线上 assetlinks 可达主机集合”三列差异。意外主机必须删除或获得审批,缺失主机必须修正候选;不能自动把最终 APK 里的所有主机加入允许清单,因为这会掩盖 Manifest 合并错误。
| 字段 | 检查目标 | 常见漂移 | 阻断条件 |
|---|---|---|---|
| scheme | 目标为受控 HTTPS 链接 | 混入自定义或测试 scheme | 目标协议缺失或未批准 |
| host | 与域名台账逐项一致 | 变体占位符残留或测试域名 | 新增未知主机或缺失生产主机 |
| autoVerify | 需要验证的 filter 明确启用 | 合并后属性被覆盖 | 发布策略要求验证但未启用 |
| path 规则 | 覆盖计划 URL 且不过宽 | 路径前缀变化或规则遗漏 | 核心路径不匹配或暴露超范围 |
| 组件 | 接收 Activity 身份稳定 | 别名或导出状态变化 | 目标组件与发布基线不一致 |
| 变体来源 | 字段可追溯到配置与依赖 | 渠道配置串用 | 无法解释最终字段来源 |
assetlinks.json 要按包名、关系和指纹集合验收
Android App Links overview 说明 Manifest URL、Digital Asset Links 与应用签名证书共同参与关联。assetlinks.json 中的目标应明确 Android App 身份,至少核对 namespace、package_name、sha256_cert_fingerprints 和 relation。只搜索指纹字符串是不够的,因为同一个文件可能包含多个 statement,指纹若落在错误包名或错误 relation 下仍不能支持目标关联。验收脚本应找到与最终包名和所需 relation 同时匹配的 statement。
证书轮换期可以存在受控的指纹集合,但集合必须有来源。旧指纹用于已安装版本,新指纹用于新交付时,应记录各自适用渠道、开始时间、观察窗口和删除条件。assetlinks.json 中长期保留不再使用的未知指纹会扩大可关联身份,不能把“多写几个更保险”当成治理策略。静态差异报告应区分缺失的最终指纹、额外但已批准的历史指纹和完全未知的指纹。
线上文件还要检查获取条件。验证时使用目标生产域名和真实 HTTPS 响应,保存获取时间、最终 URL、状态和正文摘要。搜索缓存、源码副本或运维截图不能替代线上文件。若存在 CDN、地区路由或重定向,应确认目标设备实际取得的内容符合平台要求。本文不提供域名已生效的声明,只有特定时间点的线上回执和设备状态能够支持该结论。
- 目标 statement 同时匹配 relation 与包名
- 最终应用签名指纹存在于同一 target
- 历史指纹具有渠道、时间和删除条件
- 未知额外指纹触发人工复核
- 线上 HTTPS 响应而非源码副本作为证据
- 响应时间、正文摘要和目标域名一并保存
证书轮换和渠道差异要使用集合而不是单值
只保存一个 expectedFingerprint 会在轮换和多渠道场景中产生两个问题:轮换窗口内新旧版本无法同时表达,其他合法渠道又会被错误标记。更合理的数据结构是每个域名和包名对应一个受控证书集合,每项包含证书用途、渠道、状态和证据来源。最终候选的证书必须属于该集合,assetlinks.json 的声明也要满足策略要求;集合中的每个额外成员都必须可解释。
集合并不意味着静态门禁可以放松。若候选证书在允许集合中,但线上 assetlinks 没有同包名 relation 下的该指纹,仍然失败;若线上多出允许集合之外的指纹,也应阻断并调查。轮换开始前先部署域名声明还是先交付新证书,需要按发布流程确定并留下回滚窗口,不能让设备在中间状态依赖运气。完成迁移后再依据仍在使用的安装基础决定旧指纹删除时间。
渠道差异还会改变证据取得方式。自有分发可以直接检查最终签名 APK,Play App Signing 则需要确认商店应用签名证书及真实交付身份。任何渠道重签都产生新的应用身份关系,不能复用另一个渠道的 assetlinks 回执。报告应逐渠道列出包名、最终指纹、域名声明、设备验证和业务路径,缺少真实交付文件时标为未确认,而不是用上传包推断。
| 候选指纹 | 线上声明 | 允许集合 | 发布决策 |
|---|---|---|---|
| 匹配 | 同包名 relation 下存在 | 已批准当前证书 | 进入设备验证 |
| 匹配 | 缺失 | 已批准当前证书 | 阻断并先修复域名声明 |
| 未知 | 存在 | 未登记 | 阻断并调查候选与声明来源 |
| 历史证书 | 存在 | 批准过渡期 | 不用于新候选但保留观察 |
| 历史证书 | 存在 | 已到删除条件 | 先完成影响评估再移除 |
| 渠道证书 | 在其他渠道 statement 中 | 渠道已批准 | 不得作为当前渠道通过证据 |
设备验证要重置状态并覆盖真实 URL
Verify Android App Links 描述了重置、重新验证并读取每个域名状态的方法。设备可能保留旧关联结果,所以证书变更后的验收不能只点击一次链接观察。测试计划应在目标系统版本上清理或重置相关状态,触发重新验证,等待平台处理后读取域名级结果。回执绑定设备、系统、应用版本、最终候选摘要和验证时间,避免把旧安装或旧缓存状态当作新证书证据。
Test Android App Links 强调逐一核对 Manifest 主机、assetlinks.json、签名指纹和设备链接策略。设备用例要覆盖冷启动、应用已在前台、多个 path 规则、查询参数、片段、无效参数和未登录状态。系统验证通过后仍可能出现路由栈、参数解析或目标页面错误,因此设备测试的断言不能只写“打开了 App”,而要写目标组件、页面、状态和失败处理。
用户偏好和系统策略也可能影响实际打开方式。报告要把“域名关联状态”“链接路由行为”和“业务授权结果”分开,避免一项通过覆盖另外两项。单一设备或单一路径通过不能代表所有系统版本和 URL 模式;矩阵应依据用户分布和最低支持范围选择。没有完整矩阵时可以写已验证范围,不能宣称全设备稳定。
用差异脚本同时核对候选、Manifest 和域名声明
下面的 Python 示例读取三份脱敏 JSON:从最终 APK 提取的包名和证书摘要、从最终 Manifest 提取的 HTTPS 主机、以及按主机归档的 assetlinks statement。脚本不处理私钥、不访问真实域名,也不执行安装或绕过操作。它将指纹规范化,要求主机集合非空,逐域名寻找同包名且包含 handle_all_urls relation 的 target,再比较最终证书集合。
提取步骤应由受控工具在脚本之前完成。证书数据来自 apksigner 的最终候选回执,Manifest 数据来自同一 APK 的二进制清单,assetlinks 数据来自保存了获取时间和正文摘要的线上响应。示例只验证三份输入之间的一致性,不能证明提取工具可信、线上内容持续不变或设备验证成功。若任何输入缺失、结构错误、指纹非法或域名 statement 不匹配,脚本会产生可达失败。
生产门禁可在此基础上加入渠道与轮换状态,但不能把差异自动写回 assetlinks.json。修复必须由域名和发布责任人审核,因为多出的指纹可能暴露未授权应用身份,缺失指纹也可能只是拿错候选。脚本输出应作为人工决策证据,最终放行还需要设备重新验证和真实 URL 回归。
from pathlib import Path
import json
import re
import sys
if len(sys.argv) != 4:
raise SystemExit("usage: check_links.py apk.json manifest.json assetlinks.json")
def load(path_text):
path = Path(path_text).resolve()
if not path.is_file():
raise SystemExit(f"input file missing: {path.name}")
return json.loads(path.read_text(encoding="utf-8"))
def fingerprint(value):
normalized = re.sub(r"[^0-9A-Fa-f]", "", str(value)).upper()
if len(normalized) != 64:
raise SystemExit("certificate fingerprint must contain 64 hex digits")
return normalized
apk = load(sys.argv[1])
manifest = load(sys.argv[2])
links_by_host = load(sys.argv[3])
package_name = apk.get("package_name")
certificates = {fingerprint(value) for value in apk.get("sha256_certificates", [])}
hosts = {str(value).lower().strip() for value in manifest.get("https_hosts", [])}
if not package_name or not certificates:
raise SystemExit("APK identity is incomplete")
if not hosts or any(not host or "/" in host for host in hosts):
raise SystemExit("Manifest host set is invalid")
relation = "delegate_permission/common.handle_all_urls"
failures = []
for host in sorted(hosts):
statements = links_by_host.get(host)
if not isinstance(statements, list):
failures.append({"host": host, "problem": "missing statements"})
continue
declared = set()
for statement in statements:
target = statement.get("target", {})
relations = statement.get("relation", [])
if target.get("namespace") != "android_app":
continue
if target.get("package_name") != package_name or relation not in relations:
continue
for value in target.get("sha256_cert_fingerprints", []):
declared.add(fingerprint(value))
missing = sorted(certificates - declared)
if missing:
failures.append({"host": host, "problem": "candidate certificate missing",
"fingerprints": missing})
if failures:
raise SystemExit(json.dumps(failures, ensure_ascii=False))
print(json.dumps({"package_name": package_name,
"verified_hosts": sorted(hosts)}, ensure_ascii=False))发布门禁要保存时间点证据和责任边界
一份可复核的发布记录应绑定最终 APK 摘要、包名、versionCode、签名验证回执、完整证书指纹、Manifest 主机与路径集合、线上 assetlinks 响应摘要、系统域名状态和端到端用例。证书轮换还要记录旧新指纹用途与有效窗口。任何记录缺少候选摘要,后续都可能与另一个包混淆;任何线上证据缺少时间点,也无法说明设备测试时实际读取的声明。
门禁通过只能写到证据覆盖的层次。静态比对通过说明所列候选、主机和线上声明在检查时一致;设备 verified 说明系统关联状态符合预期;URL 用例通过说明列出的路由行为正确。它们都不能证明应用内登录、权限或敏感操作天然安全。业务必须继续校验参数、用户状态和服务端授权,不能因为链接来自受控域名就跳过裁决。
准备证书变更时,应提前收集旧候选、新候选、最终签名责任、全部生产主机、线上声明变更和设备矩阵,先演练可回退顺序再进入发布。可参阅本站关于签名证书轮换与 lineage 的技术说明,但不要把一般升级连续性复制成第二套 App Link 答案。需要评估实际 APK 时,可通过御盾中央平台提交脱敏候选摘要、证书与主机清单,先固定验收范围,再安排域名与设备验证。
- 最终 APK 与所有静态回执绑定同一摘要
- 线上 assetlinks 回执包含时间和正文摘要
- 证书集合每个成员都有用途和责任人
- 设备域名状态与候选安装身份对应
- 真实 URL 覆盖冷启动、前台和异常参数
- 业务授权不会被 App Link 关联状态替代
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| App Links 将 Manifest URL、Digital Asset Links 和应用签名证书关联起来。 | Android App Links overview 描述 Android App Links 的域名与应用身份关联。 | 域名关联成功不代表应用内参数校验、登录状态和业务授权正确。 |
| Android 可以重置、重新验证并读取每个域名的 App Link 验证状态。 | Verify Android App Links 描述设备端域名验证与状态检查流程。 | 系统状态不能替代冷启动、返回栈、路径和异常参数的端到端回归。 |
| App Link 测试需要核对 Manifest 主机、assetlinks 声明、签名指纹和设备策略。 | Test Android App Links 描述 App Link 配置与设备测试重点。 | 单一设备或单一路径通过不能代表全部系统版本和 URL 模式。 |
| Android 发布流程需要区分应用签名密钥、上传密钥和证书责任。 | Sign your Android app 描述应用签名、上传密钥与 Play App Signing 的关系。 | 文档不能确认某个实际候选使用了正确的生产证书,仍需从最终身份取证。 |
| apksigner 可以验证 APK 的签名方案和证书,签名后修改会影响签名。 | Android apksigner 描述 APK 签名、验证和签名后字节约束。 | 工具验证成功不证明 keystore 管理、渠道交付和候选归档流程正确。 |
| Manifest 定义组件、intent filter、权限、SDK 约束和应用元数据。 | Android app manifest 描述 Manifest 的核心声明范围。 | 静态 Manifest 不能证明设备关联状态、应用内路由和运行时授权没有被业务代码放宽。 |
| 证书漂移验收应比较最终 APK、最终 Manifest 和线上 assetlinks 的同一身份链。 | 工程判断:三方比对可以在设备测试前发现包名、主机、relation 和指纹集合错配。 | 静态一致性不能替代目标设备重新验证,也不能证明全部业务 URL 行为。 |
| 证书轮换期应使用有来源和删除条件的受控指纹集合。 | 工程判断:集合可表达新旧安装基础,但未知或无限保留的指纹会扩大无法解释的关联身份。 | 具体轮换顺序、观察时间和删除条件需要按渠道与真实安装证据确定。 |
工程常见问题
APK 签名验证通过后,App Link 是否一定正常?
不一定。还要核对最终 Manifest 主机、assetlinks 中同包名 relation 下的证书指纹,并在目标设备重新验证和测试真实 URL。
assetlinks.json 应填写上传密钥还是应用签名证书?
应以设备最终验证的应用签名身份为准。采用 Play App Signing 时,上传密钥与应用签名密钥责任不同,不能直接混用。
证书轮换时能否同时保留新旧两个指纹?
可以按受控迁移策略保留,但每个指纹要有渠道、用途、证据和删除条件。未知或不再使用的指纹不能无限留在声明中。
系统显示 verified 后还需要测试 App 页面吗?
需要。verified 只覆盖域名关联层,冷启动、前台路由、返回栈、异常参数、登录状态和业务授权仍需端到端回归。
源码 Manifest 已确认主机,为什么还要读取最终 APK?
构建变体、依赖合并和占位符可能改变最终二进制清单。发布证据必须来自与签名和设备测试相同的最终候选。
准备 App Link 证书变更验收需要哪些材料?
准备最终 APK、候选摘要、包名、完整证书指纹、Manifest 主机与路径、线上 assetlinks 回执、渠道签名责任和目标设备矩阵。