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

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: 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
(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 {
pkotwicz 2017/03/26 01:37:04 Can you split this class into two. One class which
ScottK 2017/03/27 20:27:56 I don't think it's that useful and am concerned so
pkotwicz 2017/03/28 04:31:03 I will defer to dominickn@ on this point
29 private static final String TAG = "WebApkVerifySignature";
30
31 /** End Of Central Directory Signature */
32 private static final long EOCD_SIG = 0x06054b50;
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 /** The signature algorithm used (must also match with HASH) */
47 private static final String SIGNING_ALGORITHM = "SHA256withECDSA";
48
49 /** The pattern we look for in the APK/zip comment for singing key */
50 private static final Pattern WEBAPK_COMMENT_PATTERN =
51 Pattern.compile("webapk:\\s*(\\d[^:]+:\\s*)?([a-fA-F0-9]*)");
pkotwicz 2017/03/26 01:37:05 Nit: webapk -> chrome-webapk Is there a place whi
ScottK 2017/03/27 20:27:56 I'm not sure if the "chrome-" part really makes se
pkotwicz 2017/03/28 04:31:03 I'm not sure if the "chrome-" part really makes se
ScottK 2017/04/03 17:44:17 I've simplified the regex and am not forcing the k
52
53 /** Maximum comment length permitted */
54 private static final int MAX_COMMENT_LENGTH = 0;
55
56 /** Maximum extra field length permitted */
57 private static final int MAX_EXTRA_LENGTH = 8;
58
59 /** The memory buffer we are going to read the zip from */
60 private final ByteBuffer mBuffer;
61
62 /** Number of total central directory (zip entry) records */
63 private int mRecordCount;
64
65 /** Byte offset from the start where the central directory is found */
66 private int mCentralDirOffset;
67
68 /** The zip archive comment as a UTF-8 strings */
69 private String mComment;
70
71 /** Errors codes */
72 public enum Error {
73 OK,
74 BAD_APK,
75 EXTRA_FIELD_TOO_LARGE,
76 COMMENT_TOO_LARGE,
77 INCORRECT_SIGNATURE,
78 SIGNATURE_NOT_FOUND,
79 }
80
81 /**
82 * Sorted list of 'blocks' of memory we will cryptographically hash. We sort the blocks by
83 * filename to ensure a repeatable order.
84 */
85 private ArrayList<Block> mBlocks;
86
87 /** Block is the offset and size of a compressed zip entry. */
88 private static class Block implements Comparable<Block> {
89 Block(String filename, int position, int compressedSize) {
90 mFilename = filename;
91 mPosition = position;
92 mHeaderSize = 0;
93 mCompressedSize = compressedSize;
94 }
95
96 /** added for Comparable, sort lexicographically. */
97 @Override
98 public int compareTo(Block o) {
99 return mFilename.compareTo(o.mFilename);
100 }
101
102 @Override
103 public boolean equals(Object o) {
104 if (!(o instanceof Block)) return false;
105 return mFilename.equals(((Block) o).mFilename);
106 }
107 @Override
108 public int hashCode() {
109 return mFilename.hashCode();
110 }
111
112 String mFilename;
113 int mPosition;
114 int mHeaderSize;
115 int mCompressedSize;
116 }
117
118 /** CTOR simply 'connects' to buffer passed. */
119 public WebApkVerifySignature(ByteBuffer buffer) {
120 mBuffer = buffer;
121 mBuffer.order(LITTLE_ENDIAN);
122 }
123
124 /**
125 * Read in the comment and directory. If there is no parseable comment we wo n't read the
126 * directory as there is no point (for speed). On success, all of our privat e variables will be
127 * set.
128 *
129 * @return OK on success.
130 */
131 public Error read() throws IllegalArgumentException {
132 Error err = readEOCD();
133 if (err != Error.OK) {
134 Log.d(TAG, "Missing EOCD Signature");
135 return err;
136 }
137 // Short circuit if no comment found.
138 if (parseCommentSignature(mComment) == null) {
139 return Error.SIGNATURE_NOT_FOUND;
140 }
141 err = readDirectory();
142 if (err != Error.OK) {
143 Log.d(TAG, "Error reading directory");
144 return err;
145 }
146 return Error.OK;
147 }
148
149 /**
150 * verifySignature hashes all the files and then verifies the signature.
151 *
152 * @param pub The public key that it should be verified against.
153 * @return OK if the public key signature verifies.
154 * @throws SignatureException for various reasons.
155 */
156 public Error verifySignature(PublicKey pub) throws SignatureException {
157 byte[] sig = parseCommentSignature(mComment);
158 if (sig == null || sig.length == 0) {
159 return Error.SIGNATURE_NOT_FOUND;
160 }
161 try {
162 Signature signature = Signature.getInstance(SIGNING_ALGORITHM);
163 signature.initVerify(pub);
164 calculateHash(signature);
165 return signature.verify(sig) ? Error.OK : Error.INCORRECT_SIGNATURE;
166 } catch (InvalidKeyException | NoSuchAlgorithmException | SignatureExcep tion e) {
167 throw new SignatureException(
168 "Failed to verify generated signature using public key from certificate", e);
169 }
170 }
171
172 /** @return Full APK/zip comment string. */
173 public String comment() {
174 return mComment;
175 }
176
177 /**
178 * calculateHash goes through each file listed in blocks and calculates the SHA-256
179 * cryptographic hash.
180 *
181 * @param sig Signature object you can call update on.
182 */
183 public void calculateHash(Signature sig) throws SignatureException, IllegalA rgumentException {
184 for (Block block : mBlocks) {
185 seek(block.mPosition + block.mHeaderSize);
186 byte[] bytes = new byte[block.mCompressedSize];
pkotwicz 2017/03/26 01:37:05 You might be allocating an array which is many kil
ScottK 2017/03/27 20:27:56 A good optimization, thanks.
187 mBuffer.get(bytes);
188 try {
189 byte[] filename = block.mFilename.getBytes("UTF-8");
190 sig.update(toUInt32BigEndian(filename.length));
pkotwicz 2017/03/26 01:37:04 Can you document why you are saving the length?
ScottK 2017/03/27 20:27:56 internally b/34716053 discussed the reasons why. A
191 sig.update(filename);
192 } catch (UnsupportedEncodingException e) {
193 Log.wtf(TAG, "Don't know UTF-8", e);
194 continue;
195 }
196 sig.update(toUInt32BigEndian(bytes.length));
197 sig.update(bytes);
198 }
199 }
200
201 /**
202 * toUInt32BigEndian converts an integer to a big endian array of bytes.
203 *
204 * @param value Integer value to convert.
205 * @return Array of bytes.
206 */
207 private byte[] toUInt32BigEndian(int value) {
208 ByteBuffer buffer = ByteBuffer.allocate(4);
209 buffer.order(BIG_ENDIAN);
pkotwicz 2017/03/26 01:37:04 I think that BIG_ENDIAN is the default for ByteBuf
ScottK 2017/03/27 20:27:56 Added a comment.
210 buffer.putInt(value);
211 return buffer.array();
212 }
213
214 /**
215 * Extract the bytes of the signature from the comment. We expect
216 * "chrome-webapk:0000:<hexvalues>" comment followed by hex values. Currentl y we ignore the key
217 * id which is always "0000".
218 *
219 * @return the bytes of the signature.
220 */
221 static byte[] parseCommentSignature(String comment) {
222 Matcher m = WEBAPK_COMMENT_PATTERN.matcher(comment);
223 if (!m.find()) {
224 return null;
225 }
226 String s = m.group(2);
227 if (s.length() == 0) {
228 return null;
229 }
230 return hexToBytes(s);
231 }
232
233 /**
234 * Reads the End of Central Directory Records, returns false if it can't fin d it.
235 *
236 * @return OK on success.
237 */
238 private Error readEOCD() throws IllegalArgumentException {
239 int start = findEOCDStart();
240 if (start < 0) {
241 return Error.BAD_APK;
242 }
243 // Signature(4), Disk Number(2), Start disk number(2), Records on this disk (2)
244 seek(start + 10);
245 mRecordCount = read2(); // Number of Central Directory records
246 read4(); // Size of central directory
247 mCentralDirOffset = read4(); // as bytes from start of file.
248 int commentLength = read2();
249 mComment = readString(commentLength);
250 return Error.OK;
251 }
252
253 /**
254 * readDirectory goes through the central directory and the local file heade r blocks. This is
255 * used to calculate the offset and size of each block. We also get the file names for sorting
256 * purposes.
257 *
258 * @return OK on success.
259 */
260 Error readDirectory() {
261 mBlocks = new ArrayList<>(mRecordCount);
262 int curr = mCentralDirOffset;
263 for (int i = 0; i < mRecordCount; i++) {
264 seek(curr);
265 int signature = read4();
266 if (signature != CD_SIG) {
267 Log.d(TAG, "Missing Central Directory Signature");
268 return Error.BAD_APK;
269 }
270 // Signature(4), CreatorVersion(2), ReaderVersion(2), Flags(2), Meth od(2)
pkotwicz 2017/03/26 01:37:05 Nit: Method(2) -> CompressionMethod(2)
ScottK 2017/03/27 20:27:56 done.
271 // ModifiedTime(2), ModifiedDate(2), CRC32(4) = 20 bytes
272 curr += 20;
273 seek(curr);
pkotwicz 2017/03/26 01:37:04 It would be useful to have a seekDelta() method wh
ScottK 2017/03/27 20:27:57 Thanks! Done.
274 int compressedSize = read4();
275 read4(); // uncompressed size
276 int fileNameLength = read2();
277 int extraLen = read2();
278 int fileCommentLength = read2();
279 read2(); // DiskNumberStart
280 read2(); // Internal Attrs
281 read4(); // External Attrs
282 int offset = read4();
283 String filename = readString(fileNameLength);
284 curr = mBuffer.position() + extraLen + fileCommentLength;
pkotwicz 2017/03/26 01:37:05 I would store the extra size and the comment size
ScottK 2017/03/27 20:27:56 I believe the reason I did this is that the the fi
pkotwicz 2017/03/28 04:31:03 Thank you for the explanation. I think that we sho
pkotwicz 2017/03/31 03:59:23 Ping on this comment
ScottK 2017/04/03 17:44:17 I fixed this earlier, guess I didn't publish in ti
ScottK 2017/04/03 17:44:17 Your are correct, I should check also in the local
285 if (extraLen > MAX_EXTRA_LENGTH) {
286 Log.w(TAG,
287 String.format(
288 "Extra field too large for file %s: %d bytes", f ilename, extraLen));
289 return Error.EXTRA_FIELD_TOO_LARGE;
290 }
291 if (fileCommentLength > MAX_COMMENT_LENGTH) {
292 Log.w(TAG,
293 String.format("Unexpected comment field file %s: %d byte s", filename,
294 fileCommentLength));
295 return Error.COMMENT_TOO_LARGE;
296 }
297 if (filename.startsWith("META-INF/")) {
298 // Files that begin with META-INF/ are skipped.
299 continue;
pkotwicz 2017/03/26 01:37:04 This logic belongs in calculateHash()
ScottK 2017/03/27 20:27:56 agreed, done.
300 }
301 mBlocks.add(new Block(filename, offset, compressedSize));
302 }
303
304 // Read the 'local file header' block to the size of the header in bytes .
305 for (Block block : mBlocks) {
306 curr = block.mPosition;
307 seek(curr);
308 int signature = read4();
309 if (signature != LFH_SIG) {
310 Log.d(TAG, "LFH Signature missing");
311 return Error.BAD_APK;
312 }
313 // Skip Signature(4), ReaderVersion(2), Flags(2), Method(2),
314 // ModifiedTime (2), ModifiedDate(2), CRC32(4), CompressedSize(4),
315 // UncompressedSize(4) = 26 bytes
316 curr += 26;
317 seek(curr);
pkotwicz 2017/03/26 01:37:04 seekDelta() would be useful here too
ScottK 2017/03/27 20:27:56 done.
318 int fileNameLength = read2();
319 int extraFieldLength = read2();
pkotwicz 2017/03/26 01:37:05 If we care about checking the size of the extra da
ScottK 2017/03/27 20:27:56 It often isn't. I think some zip implementations a
320 // 26 + 4 = 30 bytes
321 block.mHeaderSize = 30 + fileNameLength + extraFieldLength;
322 }
323 Collections.sort(mBlocks);
pkotwicz 2017/03/26 01:37:04 Can you move the sorting of the blocks to computeH
ScottK 2017/03/27 20:27:56 Agreed, done.
324 return Error.OK;
325 }
326
327 /**
328 * We search buffer for EOCD_SIG and return the location where we found it. If the file has no
329 * comment it should seek only once.
330 *
331 * @return Offset from start of buffer or -1 if not found.
332 */
333 private int findEOCDStart() throws IllegalArgumentException {
334 int offset = mBuffer.limit() - MIN_EOCD_SIZE;
335 seek(offset);
336 for (int count = 0; count < MAX_HEADER_SIZE; count++) {
337 if (read4() == EOCD_SIG) {
338 // found!
339 return offset;
340 }
341 offset--;
342 if (offset < 0) {
343 return -1;
344 }
345 seek(offset);
346 }
347 return -1;
348 }
349
350 /**
351 * Seek to this position.
352 *
353 * @param offset offset from start of file.
354 */
355 private void seek(int offset) throws IllegalArgumentException {
356 mBuffer.position(offset);
357 }
358
359 /**
360 * Reads two bytes in little endian format.
361 *
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 *
pkotwicz 2017/03/26 01:37:05 Nit: No new line
ScottK 2017/03/27 20:27:56 done.
371 * @return value read.
372 */
373 private int read4() {
374 return mBuffer.getInt();
375 }
376
377 /** Read `length` many bytes into a string */
pkotwicz 2017/03/26 01:37:05 'length' -> {@link length}
ScottK 2017/03/27 20:27:56 Done.
378 private String readString(int length) {
379 if (length <= 0) {
380 return "";
381 }
382 byte[] bytes = new byte[length];
383 mBuffer.get(bytes);
384 return new String(bytes);
385 }
386
387 /** Convert a hex string into bytes. */
pkotwicz 2017/03/26 01:37:05 Can you make your comment more specific. In partic
ScottK 2017/03/27 20:27:56 Added a comment.
388 static byte[] hexToBytes(String s) {
389 int len = s.length();
390 if (len % 2 != 0) {
391 Log.d(TAG, "Got an odd number of hex nibbles.");
392 return null;
393 }
394 if (len == 0) {
395 Log.d(TAG, "Got no hex values.");
396 return null;
397 }
398 byte[] data = new byte[len / 2];
399 for (int i = 0; i < len; i += 2) {
400 data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
401 + Character.digit(s.charAt(i + 1), 16));
402 }
403 return data;
404 }
405 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698