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

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

Powered by Google App Engine
This is Rietveld 408576698