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

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

Issue 2772483002: Commment signed webapks working and verified. (Closed)
Patch Set: Resond to Peter's comments. Created 3 years, 8 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
(Empty)
1 // Copyright 2017 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 package org.chromium.webapk.lib.client;
6
7 import static java.nio.ByteOrder.BIG_ENDIAN;
8 import static java.nio.ByteOrder.LITTLE_ENDIAN;
9
10 import android.support.annotation.IntDef;
11 import android.util.Log;
12
13 import java.io.UnsupportedEncodingException;
14 import java.nio.ByteBuffer;
15 import java.security.InvalidKeyException;
16 import java.security.NoSuchAlgorithmException;
17 import java.security.PublicKey;
18 import java.security.Signature;
19 import java.security.SignatureException;
20 import java.util.ArrayList;
21 import java.util.Collections;
22 import java.util.regex.Matcher;
23 import java.util.regex.Pattern;
24
25 /**
26 * WebApkVerifySignature reads in the APK file and verifies the WebApk signature . It reads the
27 * signature from the zip comment and verifies that it was signed by the public key passed.
28 */
29 public class WebApkVerifySignature {
30 private static final String TAG = "WebApkVerifySignature";
31
32 /** End Of Central Directory Signature */
33 private static final long EOCD_SIG = 0x06054b50;
34
35 /** Central Directory Signature */
36 private static final long CD_SIG = 0x02014b50;
37
38 /** Local File Header Signature */
39 private static final long LFH_SIG = 0x04034b50;
40
41 /** Max end-of-central-directory size, including variable length file commen t.. */
pkotwicz 2017/03/31 03:59:25 How about: "Minimum end-of-central-directory size.
ScottK 2017/04/03 17:44:19 Done.
42 private static final int MIN_EOCD_SIZE = 22;
43
44 /** Max local file header size, including long filename. */
45 private static final int MAX_HEADER_SIZE = 64 * 8192;
46
47 /** Maximum number of META-INF/ files (allowing for dual signing). */
48 private static final int MAX_META_INF_FILES = 5;
49
50 /** The signature algorithm used (must also match with HASH) */
51 private static final String SIGNING_ALGORITHM = "SHA256withECDSA";
52
53 /**
54 * The pattern we look for in the APK/zip comment for singing key.
55 * An example is "webapk:0000:<hexvalues>". This pattern can appear anywher in the comment but
56 * must be separated from any other parts with a separator that doesn't look like a hex
57 * character.
pkotwicz 2017/03/31 03:59:26 Cool! The signature checking code has a "singing f
ScottK 2017/04/03 17:44:18 Done.
58 */
59 private static final Pattern WEBAPK_COMMENT_PATTERN =
60 Pattern.compile("webapk:(\\d+):([a-fA-F0-9]+)");
pkotwicz 2017/03/31 03:59:24 Can you remove the brackets around \\d+ (You don't
61
62 /** Maximum comment length permitted */
63 private static final int MAX_COMMENT_LENGTH = 0;
64
65 /** Maximum extra field length permitted */
66 private static final int MAX_EXTRA_LENGTH = 8;
67
68 /** The memory buffer we are going to read the zip from */
pkotwicz 2017/03/31 03:59:25 Nit: "." at the end of the sentence (Here and in o
ScottK 2017/04/03 17:44:18 Done.
ScottK 2017/04/03 17:44:19 Done.
69 private final ByteBuffer mBuffer;
70
71 /** Number of total central directory (zip entry) records */
72 private int mRecordCount;
73
74 /** Byte offset from the start where the central directory is found */
75 private int mCentralDirOffset;
76
77 /** The zip archive comment as a UTF-8 strings */
pkotwicz 2017/03/31 03:59:25 "strings" -> "string."
ScottK 2017/04/03 17:44:19 Done.
78 private String mComment;
79
80 /** Errors codes */
81 @IntDef({
82 ERROR_OK, ERROR_BAD_APK, ERROR_EXTRA_FIELD_TOO_LARGE, ERROR_COMMENT_ TOO_LARGE,
83 ERROR_INCORRECT_SIGNATURE, ERROR_SIGNATURE_NOT_FOUND, ERROR_TOO_MANY _META_INF_FILES,
84 })
85 public @interface Error {}
86 public static final int ERROR_OK = 0;
87 public static final int ERROR_BAD_APK = 1;
88 public static final int ERROR_EXTRA_FIELD_TOO_LARGE = 2;
89 public static final int ERROR_COMMENT_TOO_LARGE = 3;
90 public static final int ERROR_INCORRECT_SIGNATURE = 4;
91 public static final int ERROR_SIGNATURE_NOT_FOUND = 5;
92 public static final int ERROR_TOO_MANY_META_INF_FILES = 6;
pkotwicz 2017/03/31 03:59:24 Nit: public static constants should be declared fi
ScottK 2017/04/03 17:44:19 Moved to the top.
93
94 /**
95 * Sorted list of 'blocks' of memory we will cryptographically hash. We sort the blocks by
96 * filename to ensure a repeatable order.
97 */
98 private ArrayList<Block> mBlocks;
99
100 /** Block is the offset and size of a compressed zip entry. */
pkotwicz 2017/03/31 03:59:26 How about: Block contains meta data about a zip en
ScottK 2017/04/03 17:44:20 Done.
101 private static class Block implements Comparable<Block> {
102 Block(String filename, int position, int compressedSize) {
103 mFilename = filename;
104 mPosition = position;
105 mHeaderSize = 0;
106 mCompressedSize = compressedSize;
107 }
108
109 /** added for Comparable, sort lexicographically. */
pkotwicz 2017/03/31 03:59:25 Nit: added -> Added
ScottK 2017/04/03 17:44:20 Done.
110 @Override
111 public int compareTo(Block o) {
112 return mFilename.compareTo(o.mFilename);
113 }
114
115 @Override
116 public boolean equals(Object o) {
117 if (!(o instanceof Block)) return false;
118 return mFilename.equals(((Block) o).mFilename);
119 }
pkotwicz 2017/03/31 03:59:24 Nit: New line
ScottK 2017/04/03 17:44:20 Done.
120 @Override
121 public int hashCode() {
122 return mFilename.hashCode();
123 }
124
125 String mFilename;
126 int mPosition;
127 int mHeaderSize;
128 int mCompressedSize;
pkotwicz 2017/03/31 03:59:26 Member variables should come before functions.
ScottK 2017/04/03 17:44:19 Getting confused with C++ :-)
129 }
130
131 /** CTOR simply 'connects' to buffer passed. */
pkotwicz 2017/03/31 03:59:26 CTOR -> Constructor
ScottK 2017/04/03 17:44:18 Done.
132 public WebApkVerifySignature(ByteBuffer buffer) {
133 mBuffer = buffer;
134 mBuffer.order(LITTLE_ENDIAN);
135 }
136
137 /**
138 * Read in the comment and directory. If there is no parseable comment we wo n't read the
139 * directory as there is no point (for speed). On success, all of our privat e variables will be
140 * set.
141 *
pkotwicz 2017/03/31 03:59:26 Nit: remove new line (here and in other function c
ScottK 2017/04/03 17:44:18 Done.
142 * @return OK on success.
143 */
144 public int read() {
145 try {
146 @Error
pkotwicz 2017/03/31 03:59:25 I haven't seen other places which annotate ints wh
ScottK 2017/04/03 17:44:19 Removed.
147 int err = readEOCD();
148 if (err != ERROR_OK) {
149 Log.d(TAG, "Missing EOCD Signature");
pkotwicz 2017/03/31 03:59:25 Log is unnecessary because calling function will l
ScottK 2017/04/03 17:44:19 Removed.
150 return err;
151 }
152 // Short circuit if no comment found.
153 if (parseCommentSignature(mComment) == null) {
154 return ERROR_SIGNATURE_NOT_FOUND;
155 }
156 err = readDirectory();
157 if (err != ERROR_OK) {
158 Log.d(TAG, "Error reading directory");
pkotwicz 2017/03/31 03:59:26 Log is unnecessary because calling function will l
ScottK 2017/04/03 17:44:19 Removed.
159 return err;
160 }
161 } catch (Exception e) {
162 Log.e(TAG, "Error reading directory", e);
163 return ERROR_BAD_APK;
164 }
165 return ERROR_OK;
166 }
167
168 /**
169 * verifySignature hashes all the files and then verifies the signature.
170 *
171 * @param pub The public key that it should be verified against.
172 * @return ERROR_OK if the public key signature verifies.
173 * @throws SignatureException for various reasons.
pkotwicz 2017/03/31 03:59:25 This function should not throw an exception. If an
ScottK 2017/04/03 17:44:19 Done.
174 */
175 public int verifySignature(PublicKey pub) throws SignatureException {
176 byte[] sig = parseCommentSignature(mComment);
177 if (sig == null || sig.length == 0) {
178 return ERROR_SIGNATURE_NOT_FOUND;
179 }
180 try {
181 Signature signature = Signature.getInstance(SIGNING_ALGORITHM);
182 signature.initVerify(pub);
183 @Error
184 int err = calculateHash(signature);
185 if (err != ERROR_OK) {
186 return err;
187 }
188 return signature.verify(sig) ? ERROR_OK : ERROR_INCORRECT_SIGNATURE;
189 } catch (InvalidKeyException | NoSuchAlgorithmException | SignatureExcep tion
190 | UnsupportedEncodingException e) {
pkotwicz 2017/03/31 03:59:25 catch (Exception e) {} instead for the sake of san
ScottK 2017/04/03 17:44:20 Done.
191 throw new SignatureException(
192 "Failed to verify generated signature using public key from certificate", e);
193 }
194 }
195
196 /** @return Full APK/zip comment string. */
197 public String comment() {
198 return mComment;
199 }
200
201 /**
202 * calculateHash goes through each file listed in blocks and calculates the SHA-256
pkotwicz 2017/03/31 03:59:24 calculateHash -> calculateHash()
ScottK 2017/04/03 17:44:19 Done.
203 * cryptographic hash.
204 *
205 * @param sig Signature object you can call update on.
206 */
207 public int calculateHash(Signature sig)
208 throws SignatureException, IllegalArgumentException, UnsupportedEnco dingException {
pkotwicz 2017/03/31 03:59:25 This function should throw Exception only for the
ScottK 2017/04/03 17:44:19 Done.
209 byte[] filename;
210 ByteBuffer slice;
211 Collections.sort(mBlocks);
212 int metaInfCount = 0;
213 for (Block block : mBlocks) {
214 if (block.mFilename.startsWith("META-INF/")) {
215 metaInfCount++;
216 if (metaInfCount > MAX_META_INF_FILES) {
217 return ERROR_TOO_MANY_META_INF_FILES;
218 }
219
220 // Files that begin with META-INF/ are not part of the hash.
221 // This is because these signatures are added after we comment s igned the rest of
222 // the APK.
223 continue;
224 }
225
226 // Hash the filename length and filename as well to prevent Horton p rinciple
227 // violation.
pkotwicz 2017/03/31 03:59:25 Remove 'as well'
ScottK 2017/04/03 17:44:19 Done.
228 filename = block.mFilename.getBytes("UTF-8");
pkotwicz 2017/03/31 03:59:25 - Is it worth storing mFilename as a String? Can i
ScottK 2017/04/03 17:44:20 It sounds like a good idea but ended up having som
229 sig.update(toUInt32BigEndian(filename.length));
230 sig.update(filename);
231
232 // Also hash the block length for the same reason.
233 sig.update(toUInt32BigEndian(block.mCompressedSize));
234
235 seek(block.mPosition + block.mHeaderSize);
236 slice = mBuffer.slice();
pkotwicz 2017/03/31 03:59:25 Might as well move the declaration of the |slice|
ScottK 2017/04/03 17:44:19 Moved.
237 slice.limit(block.mCompressedSize);
238 sig.update(slice);
239 }
240 return ERROR_OK;
241 }
242
243 /**
244 * toUInt32BigEndian converts an integer to a big endian array of bytes.
245 *
246 * @param value Integer value to convert.
247 * @return Array of bytes.
248 */
249 private byte[] toUInt32BigEndian(int value) {
250 ByteBuffer buffer = ByteBuffer.allocate(4);
251 buffer.order(BIG_ENDIAN); // Since the ZIP is all little endian, adding this for clarity.
252 buffer.putInt(value);
253 return buffer.array();
254 }
255
256 /**
257 * Extract the bytes of the signature from the comment. We expect
258 * "chrome-webapk:0000:<hexvalues>" comment followed by hex values. Currentl y we ignore the key
pkotwicz 2017/03/31 03:59:25 How about: "The signature should have the format w
ScottK 2017/04/03 17:44:19 Done.
259 * id which is always "0000".
pkotwicz 2017/03/31 03:59:25 Nit: Add quotes around "key id"
ScottK 2017/04/03 17:44:19 Done.
260 *
261 * @return the bytes of the signature.
262 */
263 static byte[] parseCommentSignature(String comment) {
264 Matcher m = WEBAPK_COMMENT_PATTERN.matcher(comment);
265 if (!m.find()) {
266 return null;
267 }
268 String s = m.group(2);
269 if (s.length() == 0) {
pkotwicz 2017/03/31 03:59:24 Can you remove this conditional? - It should be i
ScottK 2017/04/03 17:44:19 Good point.
270 return null;
271 }
272 return hexToBytes(s);
273 }
274
275 /**
276 * Reads the End of Central Directory Records, returns false if it can't fin d it.
pkotwicz 2017/03/31 03:59:25 Records -> Record Remove the part of the comment
ScottK 2017/04/03 17:44:19 Done.
277 *
278 * @return ERROR_OK on success.
279 */
280 private int readEOCD() {
281 int start = findEOCDStart();
282 if (start < 0) {
283 return ERROR_BAD_APK;
284 }
285 // Signature(4), Disk Number(2), Start disk number(2), Records on this disk (2)
286 seek(start + 10);
287 mRecordCount = read2(); // Number of Central Directory records
288 read4(); // Size of central directory
pkotwicz 2017/03/31 03:59:25 seekDelta(4)
ScottK 2017/04/03 17:44:20 Oops missed one,thanks.
289 mCentralDirOffset = read4(); // as bytes from start of file.
290 int commentLength = read2();
291 mComment = readString(commentLength);
292 return ERROR_OK;
293 }
294
295 /**
296 * readDirectory goes through the central directory and the local file heade r blocks. This is
297 * used to calculate the offset and size of each block. We also get the file names for sorting
298 * purposes.
pkotwicz 2017/03/31 03:59:25 How about: Reads the central directory and populat
ScottK 2017/04/03 17:44:19 Done.
299 *
300 * @return ERROR_OK on success.
301 */
302 @Error
303 int readDirectory() {
304 mBlocks = new ArrayList<>(mRecordCount);
305 int curr = mCentralDirOffset;
306 for (int i = 0; i < mRecordCount; i++) {
307 seek(curr);
308 int signature = read4();
309 if (signature != CD_SIG) {
310 Log.d(TAG, "Missing Central Directory Signature");
311 return ERROR_BAD_APK;
312 }
313 // CreatorVersion(2), ReaderVersion(2), Flags(2), CompressionMethod( 2)
314 // ModifiedTime(2), ModifiedDate(2), CRC32(4) = 16 bytes
315 seekDelta(16);
316 int compressedSize = read4();
317 seekDelta(4); // uncompressed size
318 int fileNameLength = read2();
319 int extraLen = read2();
320 int fileCommentLength = read2();
321 seekDelta(8); // DiskNumberStart(2), Internal Attrs(2), External Att rs(4)
322 int offset = read4();
323 String filename = readString(fileNameLength);
324 curr = mBuffer.position() + extraLen + fileCommentLength;
325 if (extraLen > MAX_EXTRA_LENGTH) {
326 Log.w(TAG,
327 String.format(
328 "Extra field too large for file %s: %d bytes", f ilename, extraLen));
329 return ERROR_EXTRA_FIELD_TOO_LARGE;
330 }
331 if (fileCommentLength > MAX_COMMENT_LENGTH) {
332 Log.w(TAG,
333 String.format("Unexpected comment field file %s: %d byte s", filename,
334 fileCommentLength));
335 return ERROR_COMMENT_TOO_LARGE;
336 }
337 mBlocks.add(new Block(filename, offset, compressedSize));
338 }
339
340 // Read the 'local file header' block to the size of the header in bytes .
341 for (Block block : mBlocks) {
342 curr = block.mPosition;
343 seek(curr);
344 int signature = read4();
345 if (signature != LFH_SIG) {
346 Log.d(TAG, "LFH Signature missing");
347 return ERROR_BAD_APK;
348 }
349 // ReaderVersion(2), Flags(2), Method(2),
350 // ModifiedTime (2), ModifiedDate(2), CRC32(4), CompressedSize(4),
351 // UncompressedSize(4) = 22 bytes
352 seekDelta(22);
353 int fileNameLength = read2();
354 int extraFieldLength = read2();
355 if (extraFieldLength > MAX_EXTRA_LENGTH) {
356 Log.w(TAG, String.format("Extra field too large: %d bytes", extr aFieldLength));
357 return ERROR_EXTRA_FIELD_TOO_LARGE;
358 }
359
360 // 26 + 4 = 30 bytes
361 block.mHeaderSize = 30 + fileNameLength + extraFieldLength;
362 }
363 return ERROR_OK;
364 }
365
366 /**
367 * We search buffer for EOCD_SIG and return the location where we found it. If the file has no
368 * comment it should seek only once.
369 *
370 * @return Offset from start of buffer or -1 if not found.
371 */
372 private int findEOCDStart() {
373 int offset = mBuffer.limit() - MIN_EOCD_SIZE;
pkotwicz 2017/03/31 03:59:25 Nit: You need to handle |offset| being smaller tha
ScottK 2017/04/03 17:44:20 My unit tests do cover this, however being explici
374 seek(offset);
375 for (int count = 0; count < MAX_HEADER_SIZE; count++) {
pkotwicz 2017/03/31 03:59:26 Can you do this: int minSearchOffset = Math.max(0
ScottK 2017/04/03 17:44:19 Done, much cleaner.
376 if (read4() == EOCD_SIG) {
377 // found!
378 return offset;
379 }
380 offset--;
381 if (offset < 0) {
382 return -1;
383 }
384 seek(offset);
385 }
386 return -1;
387 }
388
389 /**
390 * Seek to this position.
391 *
392 * @param offset offset from start of file.
393 */
394 private void seek(int offset) {
395 mBuffer.position(offset);
396 }
397
398 /**
399 * Skip forward this number of bytes.
400 *
401 * @param delta number of bytes to seek forward.
402 */
403 private void seekDelta(int delta) {
404 mBuffer.position(mBuffer.position() + delta);
405 }
406
407 /**
408 * Reads two bytes in little endian format.
409 * @return short value read (as an int).
410 */
411 private int read2() {
412 return mBuffer.getShort();
413 }
414
415 /**
416 * Reads four bytes in little endian format.
417 * @return value read.
418 */
419 private int read4() {
420 return mBuffer.getInt();
421 }
422
423 /** Read {@link length} many bytes into a string */
424 private String readString(int length) {
425 if (length <= 0) {
426 return "";
427 }
428 byte[] bytes = new byte[length];
429 mBuffer.get(bytes);
430 return new String(bytes);
431 }
432
433 /** Convert a hex string into bytes. We store hex in the signature as zip to ols often don't *
434 * like binary strings. */
435 static byte[] hexToBytes(String s) {
436 int len = s.length();
437 if (len % 2 != 0) {
438 Log.d(TAG, "Got an odd number of hex nibbles.");
pkotwicz 2017/03/31 03:59:25 Can we remove this log message? We are already lo
ScottK 2017/04/03 17:44:20 Done.
439 return null;
440 }
441 if (len == 0) {
pkotwicz 2017/03/31 03:59:25 Can we remove this conditional? (For the reasons m
ScottK 2017/04/03 17:44:19 Done.
442 Log.d(TAG, "Got no hex values.");
443 return null;
444 }
445 byte[] data = new byte[len / 2];
446 for (int i = 0; i < len; i += 2) {
447 data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
448 + Character.digit(s.charAt(i + 1), 16));
449 }
450 return data;
451 }
452 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698