Chromium Code Reviews| OLD | NEW |
|---|---|
| (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 } | |
| OLD | NEW |