探究Applink的校验过程
实际上就是IntentFilter的校验, 对应的Intent的Action值为 ACTION_INTENT_FILTER_NEEDS_VERIFICATION=”android.intent.action.INTENT_FILTER_NEEDS_VERIFICATION”
有两处使用的位置(一个接收处, 一个发送处):
- PackageManagerService.java: 1. 启动时根据Action获取mIntentFilterVerifierComponent值; 2. sendVerificationRequest用于发送验证请求
- StatementService服务中, IntentFilterrVerificationReceiver中用于过滤Intent
两者传参值:
- URI_SCHEME(“android.content.pm.extra.INTENT_FILTER_VERIFICATION_URI_SCHEME”): url scheme的类型, 类型为String, 一般是https
- HOSTS(“android.content.pm.extra.INTENT_FILTER_VERIFICATION_HOSTS”): 需要验证的Hosts,类型为String, 多个String之间用” “分割, 每个App限制Host数量为10个(一次请求)
- PACKAGE_NAME(“android.content.pm.extra.INTENT_FILTER_VERIFICATION_PACKAGE_NAME”): 需要验证的包名, 类型String
调用adb shell dumpsys package d打印的信息是由PackageManagerService的dump方法处理的, d表示 domain-preferrred-apps. 打印代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
final String prefix = " ";
Collection<PackageSetting> allPackageSettings = mSettings.mPackages.values();
if (allPackageSettings.size() == 0) {
pw.println("No domain preferred apps!");
pw.println();
} else {
pw.println("App verification status:");
pw.println();
count = 0;
for (PackageSetting ps : allPackageSettings) {
IntentFilterVerificationInfo ivi = ps.getIntentFilterVerificationInfo();
if (ivi == null || ivi.getPackageName() == null) continue;
pw.println(prefix + "Package: " + ivi.getPackageName());
pw.println(prefix + "Domains: " + ivi.getDomainsString());
pw.println(prefix + "Status: " + ivi.getStatusString());
pw.println();
count++;
}
if (count == 0) {
pw.println(prefix + "No app verification established.");
pw.println();
}
for (int userId : sUserManager.getUserIds()) {
pw.println("App linkages for user " + userId + ":");
pw.println();
count = 0;
for (PackageSetting ps : allPackageSettings) {
final long status = ps.getDomainVerificationStatusForUser(userId);
if (status >> 32 == INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED
&& !DEBUG_DOMAIN_VERIFICATION) {
continue;
}
pw.println(prefix + "Package: " + ps.name);
pw.println(prefix + "Domains: " + dumpDomainString(ps.name));
String statusStr = IntentFilterVerificationInfo.
getStatusStringFromValue(status);
pw.println(prefix + "Status: " + statusStr);
pw.println();
count++;
}
if (count == 0) {
pw.println(prefix + "No configured app linkages.");
pw.println();
}
}
}
可以发现每个安装包Package都有一个IntentFilterVerificationInfo的对象, status状态有一下几种:
- always: 表示该App是该链接的默认App, 即使有两个App拥有相同的授权host, 也不会弹出选择App的对话框, 代表校验成功
- ask: 表示如果有两个应用host, path验证通过, 系统会弹出对话框供用户选择, 代表校验失败
- never: Applink永远不会打开该App
- always-ask: always与ask的混合体, 即时某个App的状态是always, 其也会作为选择的一部分(没见过这种状态)
- undefined: 初始值, 仅表示未校验完成.
从源代码验证文档细节, PackageManagerService.filterCandidatesWithDomainPreferredActivitiesLPr:
- 第一步: 收集always, undefined, alwaysAsk, never, matchAll(表示浏览器)各种状态的列表
- 第二步: 如果always候选人存在, 则结果中放置全部的always候选人, 不包含浏览器
- 第二步: 如果always候选人不存在, 则将undefined的列表(ask与undefined行为一致)放置在候选人中, 并且包含浏览器
- 第三步: 如果存在always-ask状态的候选人, 将其也放在候选人列表中, 后续包含浏览器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155private List<ResolveInfo> filterCandidatesWithDomainPreferredActivitiesLPr(Intent intent,
int matchFlags, List<ResolveInfo> candidates, CrossProfileDomainInfo xpDomainInfo,
int userId) {
final boolean debug = (intent.getFlags() & Intent.FLAG_DEBUG_LOG_RESOLUTION) != 0;
if (DEBUG_PREFERRED || DEBUG_DOMAIN_VERIFICATION) {
Slog.v(TAG, "Filtering results with preferred activities. Candidates count: " +
candidates.size());
}
ArrayList<ResolveInfo> result = new ArrayList<ResolveInfo>();
ArrayList<ResolveInfo> alwaysList = new ArrayList<ResolveInfo>();
ArrayList<ResolveInfo> undefinedList = new ArrayList<ResolveInfo>();
ArrayList<ResolveInfo> alwaysAskList = new ArrayList<ResolveInfo>();
ArrayList<ResolveInfo> neverList = new ArrayList<ResolveInfo>();
ArrayList<ResolveInfo> matchAllList = new ArrayList<ResolveInfo>();
synchronized (mPackages) {
final int count = candidates.size();
// First, try to use linked apps. Partition the candidates into four lists:
// one for the final results, one for the "do not use ever", one for "undefined status"
// and finally one for "browser app type".
for (int n=0; n<count; n++) {
ResolveInfo info = candidates.get(n);
String packageName = info.activityInfo.packageName;
PackageSetting ps = mSettings.mPackages.get(packageName);
if (ps != null) {
// Add to the special match all list (Browser use case)
if (info.handleAllWebDataURI) {
matchAllList.add(info);
continue;
}
// Try to get the status from User settings first
long packedStatus = getDomainVerificationStatusLPr(ps, userId);
int status = (int)(packedStatus >> 32);
int linkGeneration = (int)(packedStatus & 0xFFFFFFFF);
if (status == INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS) {
if (DEBUG_DOMAIN_VERIFICATION || debug) {
Slog.i(TAG, " + always: " + info.activityInfo.packageName
+ " : linkgen=" + linkGeneration);
}
// Use link-enabled generation as preferredOrder, i.e.
// prefer newly-enabled over earlier-enabled.
info.preferredOrder = linkGeneration;
alwaysList.add(info);
} else if (status == INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER) {
if (DEBUG_DOMAIN_VERIFICATION || debug) {
Slog.i(TAG, " + never: " + info.activityInfo.packageName);
}
neverList.add(info);
} else if (status == INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS_ASK) {
if (DEBUG_DOMAIN_VERIFICATION || debug) {
Slog.i(TAG, " + always-ask: " + info.activityInfo.packageName);
}
alwaysAskList.add(info);
} else if (status == INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED ||
status == INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ASK) {
if (DEBUG_DOMAIN_VERIFICATION || debug) {
Slog.i(TAG, " + ask: " + info.activityInfo.packageName);
}
undefinedList.add(info);
}
}
}
// We'll want to include browser possibilities in a few cases
boolean includeBrowser = false;
// First try to add the "always" resolution(s) for the current user, if any
if (alwaysList.size() > 0) {
result.addAll(alwaysList);
} else {
// Add all undefined apps as we want them to appear in the disambiguation dialog.
result.addAll(undefinedList);
// Maybe add one for the other profile.
if (xpDomainInfo != null && (
xpDomainInfo.bestDomainVerificationStatus
!= INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER)) {
result.add(xpDomainInfo.resolveInfo);
}
includeBrowser = true;
}
// The presence of any 'always ask' alternatives means we'll also offer browsers.
// If there were 'always' entries their preferred order has been set, so we also
// back that off to make the alternatives equivalent
if (alwaysAskList.size() > 0) {
for (ResolveInfo i : result) {
i.preferredOrder = 0;
}
result.addAll(alwaysAskList);
includeBrowser = true;
}
if (includeBrowser) {
// Also add browsers (all of them or only the default one)
if (DEBUG_DOMAIN_VERIFICATION) {
Slog.v(TAG, " ...including browsers in candidate set");
}
if ((matchFlags & MATCH_ALL) != 0) {
result.addAll(matchAllList);
} else {
// Browser/generic handling case. If there's a default browser, go straight
// to that (but only if there is no other higher-priority match).
final String defaultBrowserPackageName = getDefaultBrowserPackageName(userId);
int maxMatchPrio = 0;
ResolveInfo defaultBrowserMatch = null;
final int numCandidates = matchAllList.size();
for (int n = 0; n < numCandidates; n++) {
ResolveInfo info = matchAllList.get(n);
// track the highest overall match priority...
if (info.priority > maxMatchPrio) {
maxMatchPrio = info.priority;
}
// ...and the highest-priority default browser match
if (info.activityInfo.packageName.equals(defaultBrowserPackageName)) {
if (defaultBrowserMatch == null
|| (defaultBrowserMatch.priority < info.priority)) {
if (debug) {
Slog.v(TAG, "Considering default browser match " + info);
}
defaultBrowserMatch = info;
}
}
}
if (defaultBrowserMatch != null
&& defaultBrowserMatch.priority >= maxMatchPrio
&& !TextUtils.isEmpty(defaultBrowserPackageName))
{
if (debug) {
Slog.v(TAG, "Default browser match " + defaultBrowserMatch);
}
result.add(defaultBrowserMatch);
} else {
result.addAll(matchAllList);
}
}
// If there is nothing selected, add all candidates and remove the ones that the user
// has explicitly put into the INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER state
if (result.size() == 0) {
result.addAll(candidates);
result.removeAll(neverList);
}
}
}
if (DEBUG_PREFERRED || DEBUG_DOMAIN_VERIFICATION) {
Slog.v(TAG, "Filtered results with preferred activities. New candidates count: " +
result.size());
for (ResolveInfo info : result) {
Slog.v(TAG, " + " + info.activityInfo);
}
}
return result;
}
注意校验过程中的签名获取:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Returns the normalized sha-256 fingerprints of a given package according to the Android
* package manager.
*/
public static List<String> getCertFingerprintsFromPackageManager(String packageName,
Context context) throws NameNotFoundException {
Signature[] signatures = context.getPackageManager().getPackageInfo(packageName,
PackageManager.GET_SIGNATURES).signatures;
ArrayList<String> result = new ArrayList<String>(signatures.length);
for (Signature sig : signatures) {
result.add(computeNormalizedSha256Fingerprint(sig.toByteArray()));
}
return result;
}
另外需要注意的是:
AOSP中Intent Filter Verifier:
1
2
3
$ adb shell dumpsys package i
Intent Filter Verifier:
Using: com.android.statementservice (uid=10048)
使用Google Play Service的Intent Filter Verifier:
1
2
3
$ adb shell dumpsys package i
Intent Filter Verifier:
Using: com.google.android.gms (uid=10028)
我们用来分析的是开源的statementservice, 在statementservice并不校验Google Console。 但是gms是需要校验Google Console的, 请参见官方文档 “to verify ownership through Google Search Console”