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.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 } | |
| OLD | NEW |