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

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

Powered by Google App Engine
This is Rietveld 408576698