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

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

Powered by Google App Engine
This is Rietveld 408576698