Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(64)

Side by Side Diff: chrome/android/webapk/libs/client/src/org/chromium/webapk/lib/client/WebApkValidator.java

Issue 2772483002: Commment signed webapks working and verified. (Closed)
Patch Set: Wrong package order. Created 3 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2016 The Chromium Authors. All rights reserved. 1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 package org.chromium.webapk.lib.client; 5 package org.chromium.webapk.lib.client;
6 6
7 import static org.chromium.webapk.lib.common.WebApkConstants.WEBAPK_PACKAGE_PREF IX; 7 import static org.chromium.webapk.lib.common.WebApkConstants.WEBAPK_PACKAGE_PREF IX;
8 import static org.chromium.webapk.lib.common.WebApkMetaDataKeys.START_URL;
8 9
9 import android.content.Context; 10 import android.content.Context;
10 import android.content.Intent; 11 import android.content.Intent;
11 import android.content.pm.PackageInfo; 12 import android.content.pm.PackageInfo;
12 import android.content.pm.PackageManager; 13 import android.content.pm.PackageManager;
13 import android.content.pm.PackageManager.NameNotFoundException; 14 import android.content.pm.PackageManager.NameNotFoundException;
14 import android.content.pm.ResolveInfo; 15 import android.content.pm.ResolveInfo;
15 import android.content.pm.Signature; 16 import android.content.pm.Signature;
16 import android.util.Log; 17 import android.util.Log;
17 18
19 import java.io.IOException;
20 import java.io.RandomAccessFile;
21 import java.nio.MappedByteBuffer;
22 import java.nio.channels.FileChannel;
23 import java.security.KeyFactory;
24 import java.security.NoSuchAlgorithmException;
25 import java.security.PublicKey;
26 import java.security.SignatureException;
27 import java.security.spec.InvalidKeySpecException;
28 import java.security.spec.X509EncodedKeySpec;
18 import java.util.Arrays; 29 import java.util.Arrays;
19 import java.util.LinkedList; 30 import java.util.LinkedList;
20 import java.util.List; 31 import java.util.List;
21 32
22 /** 33 /**
23 * Checks whether a URL belongs to a WebAPK, and whether a WebAPK is signed by t he WebAPK Minting 34 * Checks whether a URL belongs to a WebAPK, and whether a WebAPK is signed by t he WebAPK Minting
24 * Server. 35 * Server.
25 */ 36 */
26 public class WebApkValidator { 37 public class WebApkValidator {
38 private static final String TAG = "WebApkValidator";
39 private static final String KEY_FACTORY = "EC"; // aka "ECDSA"
27 40
28 private static final String TAG = "WebApkValidator";
29 private static byte[] sExpectedSignature; 41 private static byte[] sExpectedSignature;
42 private static byte[] sCommentSignedPublicKeyBytes;
43 private static PublicKey sCommentSignedPublicKey;
30 44
31 /** 45 /**
32 * Queries the PackageManager to determine whether a WebAPK can handle the U RL. Ignores 46 * Queries the PackageManager to determine whether a WebAPK can handle the U RL. Ignores whether
33 * whether the user has selected a default handler for the URL and whether t he default 47 * the user has selected a default handler for the URL and whether the defau lt handler is the
34 * handler is the WebAPK. 48 * WebAPK.
35 * 49 *
36 * NOTE(yfriedman): This can fail if multiple WebAPKs can match the supplied url. 50 * <p>NOTE(yfriedman): This can fail if multiple WebAPKs can match the suppl ied url.
37 * 51 *
38 * @param context The application context. 52 * @param context The application context.
39 * @param url The url to check. 53 * @param url The url to check.
40 * @return Package name of WebAPK which can handle the URL. Null if the url should not be 54 * @return Package name of WebAPK which can handle the URL. Null if the url should not be
41 * handled by a WebAPK. 55 * handled by a WebAPK.
42 */ 56 */
43 public static String queryWebApkPackage(Context context, String url) { 57 public static String queryWebApkPackage(Context context, String url) {
44 return findWebApkPackage(context, resolveInfosForUrl(context, url)); 58 return findWebApkPackage(context, resolveInfosForUrl(context, url));
45 } 59 }
46 60
47 /** 61 /**
48 * Queries the PackageManager to determine whether a WebAPK can handle the U RL. Ignores 62 * Queries the PackageManager to determine whether a WebAPK can handle the U RL. Ignores whether
49 * whether the user has selected a default handler for the URL and whether t he default 63 * the user has selected a default handler for the URL and whether the defau lt handler is the
50 * handler is the WebAPK. 64 * WebAPK.
51 * 65 *
52 * NOTE: This can fail if multiple WebAPKs can match the supplied url. 66 * <p>NOTE: This can fail if multiple WebAPKs can match the supplied url.
53 * 67 *
54 * @param context The application context. 68 * @param context The application context.
55 * @param url The url to check. 69 * @param url The url to check.
56 * @return Resolve Info of a WebAPK which can handle the URL. Null if the ur l should not be 70 * @return Resolve Info of a WebAPK which can handle the URL. Null if the ur l should not be
57 * handled by a WebAPK. 71 * handled by a WebAPK.
58 */ 72 */
59 public static ResolveInfo queryResolveInfo(Context context, String url) { 73 public static ResolveInfo queryResolveInfo(Context context, String url) {
60 return findResolveInfo(context, resolveInfosForUrl(context, url)); 74 return findResolveInfo(context, resolveInfosForUrl(context, url));
61 } 75 }
62 76
63 /** 77 /**
64 * Fetches the list of resolve infos from the PackageManager that can handle the URL. 78 * Fetches the list of resolve infos from the PackageManager that can handle the URL.
65 * 79 *
66 * @param context The application context. 80 * @param context The application context.
67 * @param url The url to check. 81 * @param url The url to check.
(...skipping 15 matching lines...) Expand all
83 selector.setComponent(null); 97 selector.setComponent(null);
84 } 98 }
85 return context.getPackageManager().queryIntentActivities( 99 return context.getPackageManager().queryIntentActivities(
86 intent, PackageManager.GET_RESOLVED_FILTER); 100 intent, PackageManager.GET_RESOLVED_FILTER);
87 } 101 }
88 102
89 /** 103 /**
90 * @param context The context to use to check whether WebAPK is valid. 104 * @param context The context to use to check whether WebAPK is valid.
91 * @param infos The ResolveInfos to search. 105 * @param infos The ResolveInfos to search.
92 * @return Package name of the ResolveInfo which corresponds to a WebAPK. Nu ll if none of the 106 * @return Package name of the ResolveInfo which corresponds to a WebAPK. Nu ll if none of the
93 * ResolveInfos corresponds to a WebAPK. 107 * ResolveInfos corresponds to a WebAPK.
94 */ 108 */
95 public static String findWebApkPackage(Context context, List<ResolveInfo> in fos) { 109 public static String findWebApkPackage(Context context, List<ResolveInfo> in fos) {
96 ResolveInfo resolveInfo = findResolveInfo(context, infos); 110 ResolveInfo resolveInfo = findResolveInfo(context, infos);
97 if (resolveInfo != null) { 111 if (resolveInfo != null) {
98 return resolveInfo.activityInfo.packageName; 112 return resolveInfo.activityInfo.packageName;
99 } 113 }
100 return null; 114 return null;
101 } 115 }
102 116
103 /** 117 /**
104 * @param context The context to use to check whether WebAPK is valid. 118 * @param context The context to use to check whether WebAPK is valid.
105 * @param infos The ResolveInfos to search. 119 * @param infos The ResolveInfos to search.
106 * @return ResolveInfo which corresponds to a WebAPK. Null if none of the Re solveInfos 120 * @return ResolveInfo which corresponds to a WebAPK. Null if none of the Re solveInfos
107 * corresponds to a WebAPK. 121 * corresponds to a WebAPK.
108 */ 122 */
109 private static ResolveInfo findResolveInfo(Context context, List<ResolveInfo > infos) { 123 private static ResolveInfo findResolveInfo(Context context, List<ResolveInfo > infos) {
110 for (ResolveInfo info : infos) { 124 for (ResolveInfo info : infos) {
111 if (info.activityInfo != null 125 if (info.activityInfo != null
112 && isValidWebApk(context, info.activityInfo.packageName)) { 126 && isValidWebApk(context, info.activityInfo.packageName)) {
113 return info; 127 return info;
114 } 128 }
115 } 129 }
116 return null; 130 return null;
117 } 131 }
118 132
119 /** 133 /**
120 * Returns whether the provided WebAPK is installed and passes signature che cks. 134 * Returns whether the provided WebAPK is installed and passes signature che cks.
135 *
121 * @param context A context 136 * @param context A context
122 * @param webappPackageName The package name to check 137 * @param webappPackageName The package name to check
123 * @return true iff the WebAPK is installed and passes security checks 138 * @return true iff the WebAPK is installed and passes security checks
124 */ 139 */
125 public static boolean isValidWebApk(Context context, String webappPackageNam e) { 140 public static boolean isValidWebApk(Context context, String webappPackageNam e) {
126 if (sExpectedSignature == null) { 141 long now = System.nanoTime();
127 Log.wtf(TAG, "WebApk validation failure - expected signature not set ." 142 if (sExpectedSignature == null || sCommentSignedPublicKeyBytes == null) {
128 + "missing call to WebApkValidator.initWithBrowserHostSignat ure"); 143 Log.wtf(TAG,
144 "WebApk validation failure - expected signature not set."
145 + "missing call to WebApkValidator.initWithBrowserHo stSignature");
129 } 146 }
130 if (!webappPackageName.startsWith(WEBAPK_PACKAGE_PREFIX)) {
131 return false;
132 }
133 // check signature
134 PackageInfo packageInfo = null; 147 PackageInfo packageInfo = null;
135 try { 148 try {
136 packageInfo = context.getPackageManager().getPackageInfo(webappPacka geName, 149 packageInfo = context.getPackageManager().getPackageInfo(webappPacka geName,
137 PackageManager.GET_SIGNATURES); 150 PackageManager.GET_SIGNATURES | PackageManager.GET_META_DATA );
pkotwicz 2017/03/26 01:37:04 Is this call slower when called with PackageManage
ScottK 2017/03/27 20:27:56 Doesn't seem like much, although I only tested twi
pkotwicz 2017/03/28 04:41:34 That seems negligible
138 } catch (NameNotFoundException e) { 151 } catch (NameNotFoundException e) {
139 e.printStackTrace(); 152 e.printStackTrace();
140 Log.d(TAG, "WebApk not found"); 153 Log.d(TAG, "WebApk not found");
141 return false; 154 return false;
142 } 155 }
156 if (isNotWebApkQuick(packageInfo)) {
157 logTime(now, "get packageInfo Quick");
158 return false;
159 }
143 160
144 final Signature[] arrSignatures = packageInfo.signatures; 161 if (isV1WebApk(packageInfo, webappPackageName)) {
145 if (arrSignatures != null && arrSignatures.length == 2) { 162 logTime(now, "isV1WebApk");
146 for (Signature signature : arrSignatures) { 163 return foundV1Signature(packageInfo);
147 if (Arrays.equals(sExpectedSignature, signature.toByteArray())) {
148 Log.d(TAG, "WebApk valid - signature match!");
149 return true;
150 }
151 }
152 } 164 }
153 Log.d(TAG, "WebApk invalid"); 165
166 logTime(now, "isCommentSignedWebApk");
pkotwicz 2017/03/26 01:37:04 I think that it is useful to have a histogram whic
ScottK 2017/03/27 20:27:56 I've removed the timing logs.
167 try {
168 return verifyCommentSignedWebApk(packageInfo);
169 } catch (IOException e) {
170 Log.e(TAG, "WebApk IOException", e);
171 } catch (SignatureException e) {
172 Log.e(TAG, "WebApk SignatureException", e);
173 } catch (IllegalArgumentException e) {
174 Log.e(TAG, "WebApk IllegalArgument", e);
175 }
154 return false; 176 return false;
155 } 177 }
156 178
179 /** Determine quickly whether this is definitely not a WebAPK */
180 private static boolean isNotWebApkQuick(PackageInfo packageInfo) {
181 if (packageInfo.signatures == null || packageInfo.signatures.length == 0
182 || packageInfo.signatures.length > 2) {
pkotwicz 2017/03/26 01:37:04 I don't think that this check is useful anymore. I
ScottK 2017/03/27 20:27:56 If they have no signatures, that's bad. If they ha
pkotwicz 2017/03/28 04:41:34 We should let the user sign their APK however many
ScottK 2017/03/28 20:56:21 You can't upload an APK with no signatures and I'm
pkotwicz 2017/03/31 03:59:23 I think that the checks that you have for the amou
ScottK 2017/04/03 17:44:17 So the META-INF checks are only checked if it's a
pkotwicz 2017/04/04 18:19:36 I think that is reasonable. You have a separate ch
183 // Wrong number of signatures want 1 or 2.
184 return true;
185 }
186 if (packageInfo.applicationInfo == null || packageInfo.applicationInfo.m etaData == null) {
187 Log.e(TAG, "no application info, or metaData retrieved.");
188 return true;
189 }
190 // Having the startURL in AndroidManifest.xml is a strong signal.
191 String startUrl = packageInfo.applicationInfo.metaData.getString(START_U RL);
192 return (startUrl == null || startUrl.isEmpty());
193 }
194
195 private static boolean isV1WebApk(PackageInfo packageInfo, String webappPack ageName) {
196 return packageInfo.signatures.length == 2
197 && webappPackageName.startsWith(WEBAPK_PACKAGE_PREFIX);
198 }
199
200 private static boolean foundV1Signature(PackageInfo packageInfo) {
pkotwicz 2017/03/26 01:37:04 You can probably merge isV1WebApk() and foundV1Sig
ScottK 2017/03/27 20:27:55 Done.
201 for (Signature signature : packageInfo.signatures) {
202 if (Arrays.equals(sExpectedSignature, signature.toByteArray())) {
203 Log.d(TAG, "WebApk valid - signature match!");
204 return true;
205 }
206 }
207 return false;
208 }
209
210 private static long logTime(long start, String message) {
211 long now = System.nanoTime();
212 Log.d(TAG, String.format("%f ms: %s", (now - start) / 1E6, message));
213 return now;
214 }
215
216 /** Verify that the comment signed webapk matches the public key. */
217 private static boolean verifyCommentSignedWebApk(PackageInfo packageInfo)
218 throws IOException, SignatureException, IllegalArgumentException {
219 long now = System.nanoTime();
220
221 PublicKey CommentSignedPublicKey = getCommentSignedPublicKey();
222 if (CommentSignedPublicKey == null) {
223 Log.e(TAG, "WebApk validation failure - unable to decode public key" );
224 return false;
225 }
226 if (packageInfo.applicationInfo == null || packageInfo.applicationInfo.s ourceDir == null) {
227 Log.e(TAG, "WebApk validation failure - missing applicationInfo sour cedir");
228 return false;
229 }
230
231 String packageFilename = packageInfo.applicationInfo.sourceDir;
232 RandomAccessFile file = null;
233 FileChannel inChannel = null;
234 MappedByteBuffer buf = null;
235 WebApkVerifySignature.Error verified;
236 try {
237 file = new RandomAccessFile(packageFilename, "r");
238 inChannel = file.getChannel();
239 buf = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size ());
240 buf.load();
241
242 now = logTime(now, "opening file " + packageFilename);
243
244 WebApkVerifySignature v = new WebApkVerifySignature(buf);
245 verified = v.read();
246
247 now = logTime(now, "read");
248
249 if (verified != WebApkVerifySignature.Error.OK) {
250 Log.e(TAG, String.format("Failure reading %s: %s", packageFilena me, verified));
251 return false;
252 }
253 verified = v.verifySignature(sCommentSignedPublicKey);
254 Log.d(TAG, "File " + packageFilename + ": " + verified);
255 } finally {
256 if (buf != null) {
257 buf.clear();
258 }
259 if (inChannel != null) {
260 inChannel.close();
261 }
262 if (file != null) {
263 file.close();
264 }
265 logTime(now, "verify");
266 }
267 return verified == WebApkVerifySignature.Error.OK;
268 }
269
157 /** 270 /**
158 * Initializes the WebApkValidator with the expected signature that WebAPKs must be signed 271 * Initializes the WebApkValidator with the expected signature that WebAPKs must be signed with
159 * with for the current host. 272 * for the current host.
273 *
160 * @param expectedSignature 274 * @param expectedSignature
275 * @param v2PublicKeyBytes
161 */ 276 */
162 public static void initWithBrowserHostSignature(byte[] expectedSignature) { 277 public static void initWithBrowserHostSignature(
163 if (sExpectedSignature != null) { 278 byte[] expectedSignature, byte[] v2PublicKeyBytes) {
164 return; 279 if (sExpectedSignature == null) {
280 sExpectedSignature = Arrays.copyOf(expectedSignature, expectedSignat ure.length);
165 } 281 }
166 sExpectedSignature = Arrays.copyOf(expectedSignature, expectedSignature. length); 282 if (sCommentSignedPublicKeyBytes == null) {
283 sCommentSignedPublicKeyBytes = Arrays.copyOf(v2PublicKeyBytes, v2Pub licKeyBytes.length);
284 }
pkotwicz 2017/03/26 01:37:04 Is there a reason that we are copying the array?
ScottK 2017/03/27 20:27:56 The original code has Arrays.copyOf() for sExpecte
ScottK 2017/03/28 20:56:21 The try bots didn't like this change. I think the
285 }
286
287 /**
288 * Lazy evaluate the creation of the Public Key as the KeyFactories may not yet be initialized.
289 *
290 * @return The decoded PublicKey or null
291 */
292 private static PublicKey getCommentSignedPublicKey() {
293 if (sCommentSignedPublicKey == null) {
294 try {
295 sCommentSignedPublicKey = KeyFactory.getInstance(KEY_FACTORY)
296 .generatePublic(new X509Encode dKeySpec(
297 sCommentSignedPublicKe yBytes));
298 } catch (InvalidKeySpecException e) {
pkotwicz 2017/03/26 01:37:04 Nit: You can probably catch the generic Exception
ScottK 2017/03/27 20:27:56 done.
299 Log.e(TAG, "Failed to understand public key: " + e.getMessage()) ;
300 } catch (NoSuchAlgorithmException e) {
301 Log.e(TAG, "Failed to find KeyFactory: " + e.getMessage());
302 }
303 }
304 return sCommentSignedPublicKey;
167 } 305 }
168 } 306 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698