1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.apksig.internal.apk; 18 19 import static com.android.apksig.internal.apk.ContentDigestAlgorithm.CHUNKED_SHA256; 20 import static com.android.apksig.internal.apk.ContentDigestAlgorithm.CHUNKED_SHA512; 21 import static com.android.apksig.internal.apk.ContentDigestAlgorithm.VERITY_CHUNKED_SHA256; 22 23 import com.android.apksig.ApkVerifier; 24 import com.android.apksig.SigningCertificateLineage; 25 import com.android.apksig.apk.ApkFormatException; 26 import com.android.apksig.apk.ApkSigningBlockNotFoundException; 27 import com.android.apksig.apk.ApkUtils; 28 import com.android.apksig.internal.asn1.Asn1BerParser; 29 import com.android.apksig.internal.asn1.Asn1DecodingException; 30 import com.android.apksig.internal.asn1.Asn1DerEncoder; 31 import com.android.apksig.internal.asn1.Asn1EncodingException; 32 import com.android.apksig.internal.asn1.Asn1OpaqueObject; 33 import com.android.apksig.internal.pkcs7.AlgorithmIdentifier; 34 import com.android.apksig.internal.pkcs7.ContentInfo; 35 import com.android.apksig.internal.pkcs7.EncapsulatedContentInfo; 36 import com.android.apksig.internal.pkcs7.IssuerAndSerialNumber; 37 import com.android.apksig.internal.pkcs7.Pkcs7Constants; 38 import com.android.apksig.internal.pkcs7.SignedData; 39 import com.android.apksig.internal.pkcs7.SignerIdentifier; 40 import com.android.apksig.internal.pkcs7.SignerInfo; 41 import com.android.apksig.internal.util.ByteBufferDataSource; 42 import com.android.apksig.internal.util.ChainedDataSource; 43 import com.android.apksig.internal.util.Pair; 44 import com.android.apksig.internal.util.VerityTreeBuilder; 45 import com.android.apksig.internal.x509.RSAPublicKey; 46 import com.android.apksig.internal.x509.SubjectPublicKeyInfo; 47 import com.android.apksig.internal.zip.ZipUtils; 48 import com.android.apksig.util.DataSink; 49 import com.android.apksig.util.DataSinks; 50 import com.android.apksig.util.DataSource; 51 import com.android.apksig.util.DataSources; 52 import com.android.apksig.util.RunnablesExecutor; 53 54 import java.io.IOException; 55 import java.math.BigInteger; 56 import java.nio.BufferUnderflowException; 57 import java.nio.ByteBuffer; 58 import java.nio.ByteOrder; 59 import java.security.DigestException; 60 import java.security.InvalidAlgorithmParameterException; 61 import java.security.InvalidKeyException; 62 import java.security.KeyFactory; 63 import java.security.MessageDigest; 64 import java.security.NoSuchAlgorithmException; 65 import java.security.PrivateKey; 66 import java.security.PublicKey; 67 import java.security.Signature; 68 import java.security.SignatureException; 69 import java.security.cert.CertificateEncodingException; 70 import java.security.cert.X509Certificate; 71 import java.security.spec.AlgorithmParameterSpec; 72 import java.security.spec.InvalidKeySpecException; 73 import java.security.spec.X509EncodedKeySpec; 74 import java.util.ArrayList; 75 import java.util.Arrays; 76 import java.util.Collections; 77 import java.util.HashMap; 78 import java.util.HashSet; 79 import java.util.List; 80 import java.util.Map; 81 import java.util.Set; 82 import java.util.concurrent.atomic.AtomicInteger; 83 import java.util.function.Supplier; 84 85 import javax.security.auth.x500.X500Principal; 86 87 public class ApkSigningBlockUtils { 88 89 private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray(); 90 private static final long CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024; 91 public static final int ANDROID_COMMON_PAGE_ALIGNMENT_BYTES = 4096; 92 private static final byte[] APK_SIGNING_BLOCK_MAGIC = 93 new byte[] { 94 0x41, 0x50, 0x4b, 0x20, 0x53, 0x69, 0x67, 0x20, 95 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x34, 0x32, 96 }; 97 private static final int VERITY_PADDING_BLOCK_ID = 0x42726577; 98 99 private static final ContentDigestAlgorithm[] V4_CONTENT_DIGEST_ALGORITHMS = 100 {CHUNKED_SHA512, VERITY_CHUNKED_SHA256, CHUNKED_SHA256}; 101 102 public static final int VERSION_SOURCE_STAMP = 0; 103 public static final int VERSION_JAR_SIGNATURE_SCHEME = 1; 104 public static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2; 105 public static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3; 106 public static final int VERSION_APK_SIGNATURE_SCHEME_V4 = 4; 107 108 /** 109 * Returns positive number if {@code alg1} is preferred over {@code alg2}, {@code -1} if 110 * {@code alg2} is preferred over {@code alg1}, and {@code 0} if there is no preference. 111 */ compareSignatureAlgorithm(SignatureAlgorithm alg1, SignatureAlgorithm alg2)112 public static int compareSignatureAlgorithm(SignatureAlgorithm alg1, SignatureAlgorithm alg2) { 113 ContentDigestAlgorithm digestAlg1 = alg1.getContentDigestAlgorithm(); 114 ContentDigestAlgorithm digestAlg2 = alg2.getContentDigestAlgorithm(); 115 return compareContentDigestAlgorithm(digestAlg1, digestAlg2); 116 } 117 118 /** 119 * Returns a positive number if {@code alg1} is preferred over {@code alg2}, a negative number 120 * if {@code alg2} is preferred over {@code alg1}, or {@code 0} if there is no preference. 121 */ compareContentDigestAlgorithm( ContentDigestAlgorithm alg1, ContentDigestAlgorithm alg2)122 private static int compareContentDigestAlgorithm( 123 ContentDigestAlgorithm alg1, 124 ContentDigestAlgorithm alg2) { 125 switch (alg1) { 126 case CHUNKED_SHA256: 127 switch (alg2) { 128 case CHUNKED_SHA256: 129 return 0; 130 case CHUNKED_SHA512: 131 case VERITY_CHUNKED_SHA256: 132 return -1; 133 default: 134 throw new IllegalArgumentException("Unknown alg2: " + alg2); 135 } 136 case CHUNKED_SHA512: 137 switch (alg2) { 138 case CHUNKED_SHA256: 139 case VERITY_CHUNKED_SHA256: 140 return 1; 141 case CHUNKED_SHA512: 142 return 0; 143 default: 144 throw new IllegalArgumentException("Unknown alg2: " + alg2); 145 } 146 case VERITY_CHUNKED_SHA256: 147 switch (alg2) { 148 case CHUNKED_SHA256: 149 return 1; 150 case VERITY_CHUNKED_SHA256: 151 return 0; 152 case CHUNKED_SHA512: 153 return -1; 154 default: 155 throw new IllegalArgumentException("Unknown alg2: " + alg2); 156 } 157 default: 158 throw new IllegalArgumentException("Unknown alg1: " + alg1); 159 } 160 } 161 162 163 164 /** 165 * Verifies integrity of the APK outside of the APK Signing Block by computing digests of the 166 * APK and comparing them against the digests listed in APK Signing Block. The expected digests 167 * are taken from {@code SignerInfos} of the provided {@code result}. 168 * 169 * <p>This method adds one or more errors to the {@code result} if a verification error is 170 * expected to be encountered on Android. No errors are added to the {@code result} if the APK's 171 * integrity is expected to verify on Android for each algorithm in 172 * {@code contentDigestAlgorithms}. 173 * 174 * <p>The reason this method is currently not parameterized by a 175 * {@code [minSdkVersion, maxSdkVersion]} range is that up until now content digest algorithms 176 * exhibit the same behavior on all Android platform versions. 177 */ verifyIntegrity( RunnablesExecutor executor, DataSource beforeApkSigningBlock, DataSource centralDir, ByteBuffer eocd, Set<ContentDigestAlgorithm> contentDigestAlgorithms, Result result)178 public static void verifyIntegrity( 179 RunnablesExecutor executor, 180 DataSource beforeApkSigningBlock, 181 DataSource centralDir, 182 ByteBuffer eocd, 183 Set<ContentDigestAlgorithm> contentDigestAlgorithms, 184 Result result) throws IOException, NoSuchAlgorithmException { 185 if (contentDigestAlgorithms.isEmpty()) { 186 // This should never occur because this method is invoked once at least one signature 187 // is verified, meaning at least one content digest is known. 188 throw new RuntimeException("No content digests found"); 189 } 190 191 // For the purposes of verifying integrity, ZIP End of Central Directory (EoCD) must be 192 // treated as though its Central Directory offset points to the start of APK Signing Block. 193 // We thus modify the EoCD accordingly. 194 ByteBuffer modifiedEocd = ByteBuffer.allocate(eocd.remaining()); 195 int eocdSavedPos = eocd.position(); 196 modifiedEocd.order(ByteOrder.LITTLE_ENDIAN); 197 modifiedEocd.put(eocd); 198 modifiedEocd.flip(); 199 200 // restore eocd to position prior to modification in case it is to be used elsewhere 201 eocd.position(eocdSavedPos); 202 ZipUtils.setZipEocdCentralDirectoryOffset(modifiedEocd, beforeApkSigningBlock.size()); 203 Map<ContentDigestAlgorithm, byte[]> actualContentDigests; 204 try { 205 actualContentDigests = 206 computeContentDigests( 207 executor, 208 contentDigestAlgorithms, 209 beforeApkSigningBlock, 210 centralDir, 211 new ByteBufferDataSource(modifiedEocd)); 212 // Special checks for the verity algorithm requirements. 213 if (actualContentDigests.containsKey(VERITY_CHUNKED_SHA256)) { 214 if ((beforeApkSigningBlock.size() % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0)) { 215 throw new RuntimeException( 216 "APK Signing Block is not aligned on 4k boundary: " + 217 beforeApkSigningBlock.size()); 218 } 219 220 long centralDirOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocd); 221 long signingBlockSize = centralDirOffset - beforeApkSigningBlock.size(); 222 if (signingBlockSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0) { 223 throw new RuntimeException( 224 "APK Signing Block size is not multiple of page size: " + 225 signingBlockSize); 226 } 227 } 228 } catch (DigestException e) { 229 throw new RuntimeException("Failed to compute content digests", e); 230 } 231 if (!contentDigestAlgorithms.equals(actualContentDigests.keySet())) { 232 throw new RuntimeException( 233 "Mismatch between sets of requested and computed content digests" 234 + " . Requested: " + contentDigestAlgorithms 235 + ", computed: " + actualContentDigests.keySet()); 236 } 237 238 // Compare digests computed over the rest of APK against the corresponding expected digests 239 // in signer blocks. 240 for (Result.SignerInfo signerInfo : result.signers) { 241 for (Result.SignerInfo.ContentDigest expected : signerInfo.contentDigests) { 242 SignatureAlgorithm signatureAlgorithm = 243 SignatureAlgorithm.findById(expected.getSignatureAlgorithmId()); 244 if (signatureAlgorithm == null) { 245 continue; 246 } 247 ContentDigestAlgorithm contentDigestAlgorithm = 248 signatureAlgorithm.getContentDigestAlgorithm(); 249 // if the current digest algorithm is not in the list provided by the caller then 250 // ignore it; the signer may contain digests not recognized by the specified SDK 251 // range. 252 if (!contentDigestAlgorithms.contains(contentDigestAlgorithm)) { 253 continue; 254 } 255 byte[] expectedDigest = expected.getValue(); 256 byte[] actualDigest = actualContentDigests.get(contentDigestAlgorithm); 257 if (!Arrays.equals(expectedDigest, actualDigest)) { 258 if (result.signatureSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2) { 259 signerInfo.addError( 260 ApkVerifier.Issue.V2_SIG_APK_DIGEST_DID_NOT_VERIFY, 261 contentDigestAlgorithm, 262 toHex(expectedDigest), 263 toHex(actualDigest)); 264 } else if (result.signatureSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V3) { 265 signerInfo.addError( 266 ApkVerifier.Issue.V3_SIG_APK_DIGEST_DID_NOT_VERIFY, 267 contentDigestAlgorithm, 268 toHex(expectedDigest), 269 toHex(actualDigest)); 270 } 271 continue; 272 } 273 signerInfo.verifiedContentDigests.put(contentDigestAlgorithm, actualDigest); 274 } 275 } 276 } 277 findApkSignatureSchemeBlock( ByteBuffer apkSigningBlock, int blockId, Result result)278 public static ByteBuffer findApkSignatureSchemeBlock( 279 ByteBuffer apkSigningBlock, 280 int blockId, 281 Result result) throws SignatureNotFoundException { 282 checkByteOrderLittleEndian(apkSigningBlock); 283 // FORMAT: 284 // OFFSET DATA TYPE DESCRIPTION 285 // * @+0 bytes uint64: size in bytes (excluding this field) 286 // * @+8 bytes pairs 287 // * @-24 bytes uint64: size in bytes (same as the one above) 288 // * @-16 bytes uint128: magic 289 ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24); 290 291 int entryCount = 0; 292 while (pairs.hasRemaining()) { 293 entryCount++; 294 if (pairs.remaining() < 8) { 295 throw new SignatureNotFoundException( 296 "Insufficient data to read size of APK Signing Block entry #" + entryCount); 297 } 298 long lenLong = pairs.getLong(); 299 if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) { 300 throw new SignatureNotFoundException( 301 "APK Signing Block entry #" + entryCount 302 + " size out of range: " + lenLong); 303 } 304 int len = (int) lenLong; 305 int nextEntryPos = pairs.position() + len; 306 if (len > pairs.remaining()) { 307 throw new SignatureNotFoundException( 308 "APK Signing Block entry #" + entryCount + " size out of range: " + len 309 + ", available: " + pairs.remaining()); 310 } 311 int id = pairs.getInt(); 312 if (id == blockId) { 313 return getByteBuffer(pairs, len - 4); 314 } 315 pairs.position(nextEntryPos); 316 } 317 318 throw new SignatureNotFoundException( 319 "No APK Signature Scheme block in APK Signing Block with ID: " + blockId); 320 } 321 checkByteOrderLittleEndian(ByteBuffer buffer)322 public static void checkByteOrderLittleEndian(ByteBuffer buffer) { 323 if (buffer.order() != ByteOrder.LITTLE_ENDIAN) { 324 throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); 325 } 326 } 327 328 /** 329 * Returns new byte buffer whose content is a shared subsequence of this buffer's content 330 * between the specified start (inclusive) and end (exclusive) positions. As opposed to 331 * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source 332 * buffer's byte order. 333 */ sliceFromTo(ByteBuffer source, int start, int end)334 private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) { 335 if (start < 0) { 336 throw new IllegalArgumentException("start: " + start); 337 } 338 if (end < start) { 339 throw new IllegalArgumentException("end < start: " + end + " < " + start); 340 } 341 int capacity = source.capacity(); 342 if (end > source.capacity()) { 343 throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity); 344 } 345 int originalLimit = source.limit(); 346 int originalPosition = source.position(); 347 try { 348 source.position(0); 349 source.limit(end); 350 source.position(start); 351 ByteBuffer result = source.slice(); 352 result.order(source.order()); 353 return result; 354 } finally { 355 source.position(0); 356 source.limit(originalLimit); 357 source.position(originalPosition); 358 } 359 } 360 361 /** 362 * Relative <em>get</em> method for reading {@code size} number of bytes from the current 363 * position of this buffer. 364 * 365 * <p>This method reads the next {@code size} bytes at this buffer's current position, 366 * returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to 367 * {@code size}, byte order set to this buffer's byte order; and then increments the position by 368 * {@code size}. 369 */ getByteBuffer(ByteBuffer source, int size)370 private static ByteBuffer getByteBuffer(ByteBuffer source, int size) { 371 if (size < 0) { 372 throw new IllegalArgumentException("size: " + size); 373 } 374 int originalLimit = source.limit(); 375 int position = source.position(); 376 int limit = position + size; 377 if ((limit < position) || (limit > originalLimit)) { 378 throw new BufferUnderflowException(); 379 } 380 source.limit(limit); 381 try { 382 ByteBuffer result = source.slice(); 383 result.order(source.order()); 384 source.position(limit); 385 return result; 386 } finally { 387 source.limit(originalLimit); 388 } 389 } 390 getLengthPrefixedSlice(ByteBuffer source)391 public static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws ApkFormatException { 392 if (source.remaining() < 4) { 393 throw new ApkFormatException( 394 "Remaining buffer too short to contain length of length-prefixed field" 395 + ". Remaining: " + source.remaining()); 396 } 397 int len = source.getInt(); 398 if (len < 0) { 399 throw new IllegalArgumentException("Negative length"); 400 } else if (len > source.remaining()) { 401 throw new ApkFormatException( 402 "Length-prefixed field longer than remaining buffer" 403 + ". Field length: " + len + ", remaining: " + source.remaining()); 404 } 405 return getByteBuffer(source, len); 406 } 407 readLengthPrefixedByteArray(ByteBuffer buf)408 public static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws ApkFormatException { 409 int len = buf.getInt(); 410 if (len < 0) { 411 throw new ApkFormatException("Negative length"); 412 } else if (len > buf.remaining()) { 413 throw new ApkFormatException( 414 "Underflow while reading length-prefixed value. Length: " + len 415 + ", available: " + buf.remaining()); 416 } 417 byte[] result = new byte[len]; 418 buf.get(result); 419 return result; 420 } 421 toHex(byte[] value)422 public static String toHex(byte[] value) { 423 StringBuilder sb = new StringBuilder(value.length * 2); 424 int len = value.length; 425 for (int i = 0; i < len; i++) { 426 int hi = (value[i] & 0xff) >>> 4; 427 int lo = value[i] & 0x0f; 428 sb.append(HEX_DIGITS[hi]).append(HEX_DIGITS[lo]); 429 } 430 return sb.toString(); 431 } 432 computeContentDigests( RunnablesExecutor executor, Set<ContentDigestAlgorithm> digestAlgorithms, DataSource beforeCentralDir, DataSource centralDir, DataSource eocd)433 public static Map<ContentDigestAlgorithm, byte[]> computeContentDigests( 434 RunnablesExecutor executor, 435 Set<ContentDigestAlgorithm> digestAlgorithms, 436 DataSource beforeCentralDir, 437 DataSource centralDir, 438 DataSource eocd) throws IOException, NoSuchAlgorithmException, DigestException { 439 Map<ContentDigestAlgorithm, byte[]> contentDigests = new HashMap<>(); 440 Set<ContentDigestAlgorithm> oneMbChunkBasedAlgorithm = new HashSet<>(); 441 for (ContentDigestAlgorithm digestAlgorithm : digestAlgorithms) { 442 if (digestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA256 443 || digestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA512) { 444 oneMbChunkBasedAlgorithm.add(digestAlgorithm); 445 } 446 } 447 computeOneMbChunkContentDigests( 448 executor, 449 oneMbChunkBasedAlgorithm, 450 new DataSource[] { beforeCentralDir, centralDir, eocd }, 451 contentDigests); 452 453 if (digestAlgorithms.contains(VERITY_CHUNKED_SHA256)) { 454 computeApkVerityDigest(beforeCentralDir, centralDir, eocd, contentDigests); 455 } 456 return contentDigests; 457 } 458 computeOneMbChunkContentDigests( Set<ContentDigestAlgorithm> digestAlgorithms, DataSource[] contents, Map<ContentDigestAlgorithm, byte[]> outputContentDigests)459 static void computeOneMbChunkContentDigests( 460 Set<ContentDigestAlgorithm> digestAlgorithms, 461 DataSource[] contents, 462 Map<ContentDigestAlgorithm, byte[]> outputContentDigests) 463 throws IOException, NoSuchAlgorithmException, DigestException { 464 // For each digest algorithm the result is computed as follows: 465 // 1. Each segment of contents is split into consecutive chunks of 1 MB in size. 466 // The final chunk will be shorter iff the length of segment is not a multiple of 1 MB. 467 // No chunks are produced for empty (zero length) segments. 468 // 2. The digest of each chunk is computed over the concatenation of byte 0xa5, the chunk's 469 // length in bytes (uint32 little-endian) and the chunk's contents. 470 // 3. The output digest is computed over the concatenation of the byte 0x5a, the number of 471 // chunks (uint32 little-endian) and the concatenation of digests of chunks of all 472 // segments in-order. 473 474 long chunkCountLong = 0; 475 for (DataSource input : contents) { 476 chunkCountLong += 477 getChunkCount(input.size(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES); 478 } 479 if (chunkCountLong > Integer.MAX_VALUE) { 480 throw new DigestException("Input too long: " + chunkCountLong + " chunks"); 481 } 482 int chunkCount = (int) chunkCountLong; 483 484 ContentDigestAlgorithm[] digestAlgorithmsArray = 485 digestAlgorithms.toArray(new ContentDigestAlgorithm[digestAlgorithms.size()]); 486 MessageDigest[] mds = new MessageDigest[digestAlgorithmsArray.length]; 487 byte[][] digestsOfChunks = new byte[digestAlgorithmsArray.length][]; 488 int[] digestOutputSizes = new int[digestAlgorithmsArray.length]; 489 for (int i = 0; i < digestAlgorithmsArray.length; i++) { 490 ContentDigestAlgorithm digestAlgorithm = digestAlgorithmsArray[i]; 491 int digestOutputSizeBytes = digestAlgorithm.getChunkDigestOutputSizeBytes(); 492 digestOutputSizes[i] = digestOutputSizeBytes; 493 byte[] concatenationOfChunkCountAndChunkDigests = 494 new byte[5 + chunkCount * digestOutputSizeBytes]; 495 concatenationOfChunkCountAndChunkDigests[0] = 0x5a; 496 setUnsignedInt32LittleEndian( 497 chunkCount, concatenationOfChunkCountAndChunkDigests, 1); 498 digestsOfChunks[i] = concatenationOfChunkCountAndChunkDigests; 499 String jcaAlgorithm = digestAlgorithm.getJcaMessageDigestAlgorithm(); 500 mds[i] = MessageDigest.getInstance(jcaAlgorithm); 501 } 502 503 DataSink mdSink = DataSinks.asDataSink(mds); 504 byte[] chunkContentPrefix = new byte[5]; 505 chunkContentPrefix[0] = (byte) 0xa5; 506 int chunkIndex = 0; 507 // Optimization opportunity: digests of chunks can be computed in parallel. However, 508 // determining the number of computations to be performed in parallel is non-trivial. This 509 // depends on a wide range of factors, such as data source type (e.g., in-memory or fetched 510 // from file), CPU/memory/disk cache bandwidth and latency, interconnect architecture of CPU 511 // cores, load on the system from other threads of execution and other processes, size of 512 // input. 513 // For now, we compute these digests sequentially and thus have the luxury of improving 514 // performance by writing the digest of each chunk into a pre-allocated buffer at exactly 515 // the right position. This avoids unnecessary allocations, copying, and enables the final 516 // digest to be more efficient because it's presented with all of its input in one go. 517 for (DataSource input : contents) { 518 long inputOffset = 0; 519 long inputRemaining = input.size(); 520 while (inputRemaining > 0) { 521 int chunkSize = 522 (int) Math.min(inputRemaining, CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES); 523 setUnsignedInt32LittleEndian(chunkSize, chunkContentPrefix, 1); 524 for (int i = 0; i < mds.length; i++) { 525 mds[i].update(chunkContentPrefix); 526 } 527 try { 528 input.feed(inputOffset, chunkSize, mdSink); 529 } catch (IOException e) { 530 throw new IOException("Failed to read chunk #" + chunkIndex, e); 531 } 532 for (int i = 0; i < digestAlgorithmsArray.length; i++) { 533 MessageDigest md = mds[i]; 534 byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i]; 535 int expectedDigestSizeBytes = digestOutputSizes[i]; 536 int actualDigestSizeBytes = 537 md.digest( 538 concatenationOfChunkCountAndChunkDigests, 539 5 + chunkIndex * expectedDigestSizeBytes, 540 expectedDigestSizeBytes); 541 if (actualDigestSizeBytes != expectedDigestSizeBytes) { 542 throw new RuntimeException( 543 "Unexpected output size of " + md.getAlgorithm() 544 + " digest: " + actualDigestSizeBytes); 545 } 546 } 547 inputOffset += chunkSize; 548 inputRemaining -= chunkSize; 549 chunkIndex++; 550 } 551 } 552 553 for (int i = 0; i < digestAlgorithmsArray.length; i++) { 554 ContentDigestAlgorithm digestAlgorithm = digestAlgorithmsArray[i]; 555 byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i]; 556 MessageDigest md = mds[i]; 557 byte[] digest = md.digest(concatenationOfChunkCountAndChunkDigests); 558 outputContentDigests.put(digestAlgorithm, digest); 559 } 560 } 561 computeOneMbChunkContentDigests( RunnablesExecutor executor, Set<ContentDigestAlgorithm> digestAlgorithms, DataSource[] contents, Map<ContentDigestAlgorithm, byte[]> outputContentDigests)562 static void computeOneMbChunkContentDigests( 563 RunnablesExecutor executor, 564 Set<ContentDigestAlgorithm> digestAlgorithms, 565 DataSource[] contents, 566 Map<ContentDigestAlgorithm, byte[]> outputContentDigests) 567 throws NoSuchAlgorithmException, DigestException { 568 long chunkCountLong = 0; 569 for (DataSource input : contents) { 570 chunkCountLong += 571 getChunkCount(input.size(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES); 572 } 573 if (chunkCountLong > Integer.MAX_VALUE) { 574 throw new DigestException("Input too long: " + chunkCountLong + " chunks"); 575 } 576 int chunkCount = (int) chunkCountLong; 577 578 List<ChunkDigests> chunkDigestsList = new ArrayList<>(digestAlgorithms.size()); 579 for (ContentDigestAlgorithm algorithms : digestAlgorithms) { 580 chunkDigestsList.add(new ChunkDigests(algorithms, chunkCount)); 581 } 582 583 ChunkSupplier chunkSupplier = new ChunkSupplier(contents); 584 executor.execute(() -> new ChunkDigester(chunkSupplier, chunkDigestsList)); 585 586 // Compute and write out final digest for each algorithm. 587 for (ChunkDigests chunkDigests : chunkDigestsList) { 588 MessageDigest messageDigest = chunkDigests.createMessageDigest(); 589 outputContentDigests.put( 590 chunkDigests.algorithm, 591 messageDigest.digest(chunkDigests.concatOfDigestsOfChunks)); 592 } 593 } 594 595 private static class ChunkDigests { 596 private final ContentDigestAlgorithm algorithm; 597 private final int digestOutputSize; 598 private final byte[] concatOfDigestsOfChunks; 599 ChunkDigests(ContentDigestAlgorithm algorithm, int chunkCount)600 private ChunkDigests(ContentDigestAlgorithm algorithm, int chunkCount) { 601 this.algorithm = algorithm; 602 digestOutputSize = this.algorithm.getChunkDigestOutputSizeBytes(); 603 concatOfDigestsOfChunks = new byte[1 + 4 + chunkCount * digestOutputSize]; 604 605 // Fill the initial values of the concatenated digests of chunks, which is 606 // {0x5a, 4-bytes-of-little-endian-chunk-count, digests*...}. 607 concatOfDigestsOfChunks[0] = 0x5a; 608 setUnsignedInt32LittleEndian(chunkCount, concatOfDigestsOfChunks, 1); 609 } 610 createMessageDigest()611 private MessageDigest createMessageDigest() throws NoSuchAlgorithmException { 612 return MessageDigest.getInstance(algorithm.getJcaMessageDigestAlgorithm()); 613 } 614 getOffset(int chunkIndex)615 private int getOffset(int chunkIndex) { 616 return 1 + 4 + chunkIndex * digestOutputSize; 617 } 618 } 619 620 /** 621 * A per-thread digest worker. 622 */ 623 private static class ChunkDigester implements Runnable { 624 private final ChunkSupplier dataSupplier; 625 private final List<ChunkDigests> chunkDigests; 626 private final List<MessageDigest> messageDigests; 627 private final DataSink mdSink; 628 ChunkDigester(ChunkSupplier dataSupplier, List<ChunkDigests> chunkDigests)629 private ChunkDigester(ChunkSupplier dataSupplier, List<ChunkDigests> chunkDigests) { 630 this.dataSupplier = dataSupplier; 631 this.chunkDigests = chunkDigests; 632 messageDigests = new ArrayList<>(chunkDigests.size()); 633 for (ChunkDigests chunkDigest : chunkDigests) { 634 try { 635 messageDigests.add(chunkDigest.createMessageDigest()); 636 } catch (NoSuchAlgorithmException ex) { 637 throw new RuntimeException(ex); 638 } 639 } 640 mdSink = DataSinks.asDataSink(messageDigests.toArray(new MessageDigest[0])); 641 } 642 643 @Override run()644 public void run() { 645 byte[] chunkContentPrefix = new byte[5]; 646 chunkContentPrefix[0] = (byte) 0xa5; 647 648 try { 649 for (ChunkSupplier.Chunk chunk = dataSupplier.get(); 650 chunk != null; 651 chunk = dataSupplier.get()) { 652 int size = chunk.size; 653 if (size > CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES) { 654 throw new RuntimeException("Chunk size greater than expected: " + size); 655 } 656 657 // First update with the chunk prefix. 658 setUnsignedInt32LittleEndian(size, chunkContentPrefix, 1); 659 mdSink.consume(chunkContentPrefix, 0, chunkContentPrefix.length); 660 661 // Then update with the chunk data. 662 mdSink.consume(chunk.data); 663 664 // Now finalize chunk for all algorithms. 665 for (int i = 0; i < chunkDigests.size(); i++) { 666 ChunkDigests chunkDigest = chunkDigests.get(i); 667 int actualDigestSize = messageDigests.get(i).digest( 668 chunkDigest.concatOfDigestsOfChunks, 669 chunkDigest.getOffset(chunk.chunkIndex), 670 chunkDigest.digestOutputSize); 671 if (actualDigestSize != chunkDigest.digestOutputSize) { 672 throw new RuntimeException( 673 "Unexpected output size of " + chunkDigest.algorithm 674 + " digest: " + actualDigestSize); 675 } 676 } 677 } 678 } catch (IOException | DigestException e) { 679 throw new RuntimeException(e); 680 } 681 } 682 } 683 684 /** 685 * Thread-safe 1MB DataSource chunk supplier. When bounds are met in a 686 * supplied {@link DataSource}, the data from the next {@link DataSource} 687 * are NOT concatenated. Only the next call to get() will fetch from the 688 * next {@link DataSource} in the input {@link DataSource} array. 689 */ 690 private static class ChunkSupplier implements Supplier<ChunkSupplier.Chunk> { 691 private final DataSource[] dataSources; 692 private final int[] chunkCounts; 693 private final int totalChunkCount; 694 private final AtomicInteger nextIndex; 695 ChunkSupplier(DataSource[] dataSources)696 private ChunkSupplier(DataSource[] dataSources) { 697 this.dataSources = dataSources; 698 chunkCounts = new int[dataSources.length]; 699 int totalChunkCount = 0; 700 for (int i = 0; i < dataSources.length; i++) { 701 long chunkCount = getChunkCount(dataSources[i].size(), 702 CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES); 703 if (chunkCount > Integer.MAX_VALUE) { 704 throw new RuntimeException( 705 String.format( 706 "Number of chunks in dataSource[%d] is greater than max int.", 707 i)); 708 } 709 chunkCounts[i] = (int)chunkCount; 710 totalChunkCount = (int) (totalChunkCount + chunkCount); 711 } 712 this.totalChunkCount = totalChunkCount; 713 nextIndex = new AtomicInteger(0); 714 } 715 716 /** 717 * We map an integer index to the termination-adjusted dataSources 1MB chunks. 718 * Note that {@link Chunk}s could be less than 1MB, namely the last 1MB-aligned 719 * blocks in each input {@link DataSource} (unless the DataSource itself is 720 * 1MB-aligned). 721 */ 722 @Override get()723 public ChunkSupplier.Chunk get() { 724 int index = nextIndex.getAndIncrement(); 725 if (index < 0 || index >= totalChunkCount) { 726 return null; 727 } 728 729 int dataSourceIndex = 0; 730 long dataSourceChunkOffset = index; 731 for (; dataSourceIndex < dataSources.length; dataSourceIndex++) { 732 if (dataSourceChunkOffset < chunkCounts[dataSourceIndex]) { 733 break; 734 } 735 dataSourceChunkOffset -= chunkCounts[dataSourceIndex]; 736 } 737 738 long remainingSize = Math.min( 739 dataSources[dataSourceIndex].size() - 740 dataSourceChunkOffset * CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES, 741 CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES); 742 743 final int size = (int)remainingSize; 744 final ByteBuffer buffer = ByteBuffer.allocate(size); 745 try { 746 dataSources[dataSourceIndex].copyTo( 747 dataSourceChunkOffset * CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES, size, 748 buffer); 749 } catch (IOException e) { 750 throw new IllegalStateException("Failed to read chunk", e); 751 } 752 buffer.rewind(); 753 754 return new Chunk(index, buffer, size); 755 } 756 757 static class Chunk { 758 private final int chunkIndex; 759 private final ByteBuffer data; 760 private final int size; 761 Chunk(int chunkIndex, ByteBuffer data, int size)762 private Chunk(int chunkIndex, ByteBuffer data, int size) { 763 this.chunkIndex = chunkIndex; 764 this.data = data; 765 this.size = size; 766 } 767 } 768 } 769 770 @SuppressWarnings("ByteBufferBackingArray") computeApkVerityDigest(DataSource beforeCentralDir, DataSource centralDir, DataSource eocd, Map<ContentDigestAlgorithm, byte[]> outputContentDigests)771 private static void computeApkVerityDigest(DataSource beforeCentralDir, DataSource centralDir, 772 DataSource eocd, Map<ContentDigestAlgorithm, byte[]> outputContentDigests) 773 throws IOException, NoSuchAlgorithmException { 774 ByteBuffer encoded = createVerityDigestBuffer(true); 775 // Use 0s as salt for now. This also needs to be consistent in the fsverify header for 776 // kernel to use. 777 try (VerityTreeBuilder builder = new VerityTreeBuilder(new byte[8])) { 778 byte[] rootHash = builder.generateVerityTreeRootHash(beforeCentralDir, centralDir, 779 eocd); 780 encoded.put(rootHash); 781 encoded.putLong(beforeCentralDir.size() + centralDir.size() + eocd.size()); 782 outputContentDigests.put(VERITY_CHUNKED_SHA256, encoded.array()); 783 } 784 } 785 createVerityDigestBuffer(boolean includeSourceDataSize)786 private static ByteBuffer createVerityDigestBuffer(boolean includeSourceDataSize) { 787 // FORMAT: 788 // OFFSET DATA TYPE DESCRIPTION 789 // * @+0 bytes uint8[32] Merkle tree root hash of SHA-256 790 // * @+32 bytes int64 (optional) Length of source data 791 int backBufferSize = 792 VERITY_CHUNKED_SHA256.getChunkDigestOutputSizeBytes(); 793 if (includeSourceDataSize) { 794 backBufferSize += Long.SIZE / Byte.SIZE; 795 } 796 ByteBuffer encoded = ByteBuffer.allocate(backBufferSize); 797 encoded.order(ByteOrder.LITTLE_ENDIAN); 798 return encoded; 799 } 800 801 public static class VerityTreeAndDigest { 802 public final ContentDigestAlgorithm contentDigestAlgorithm; 803 public final byte[] rootHash; 804 public final byte[] tree; 805 VerityTreeAndDigest(ContentDigestAlgorithm contentDigestAlgorithm, byte[] rootHash, byte[] tree)806 VerityTreeAndDigest(ContentDigestAlgorithm contentDigestAlgorithm, byte[] rootHash, 807 byte[] tree) { 808 this.contentDigestAlgorithm = contentDigestAlgorithm; 809 this.rootHash = rootHash; 810 this.tree = tree; 811 } 812 } 813 814 @SuppressWarnings("ByteBufferBackingArray") computeChunkVerityTreeAndDigest(DataSource dataSource)815 public static VerityTreeAndDigest computeChunkVerityTreeAndDigest(DataSource dataSource) 816 throws IOException, NoSuchAlgorithmException { 817 ByteBuffer encoded = createVerityDigestBuffer(false); 818 // Use 0s as salt for now. This also needs to be consistent in the fsverify header for 819 // kernel to use. 820 try (VerityTreeBuilder builder = new VerityTreeBuilder(null)) { 821 ByteBuffer tree = builder.generateVerityTree(dataSource); 822 byte[] rootHash = builder.getRootHashFromTree(tree); 823 encoded.put(rootHash); 824 return new VerityTreeAndDigest(VERITY_CHUNKED_SHA256, encoded.array(), tree.array()); 825 } 826 } 827 getChunkCount(long inputSize, long chunkSize)828 private static long getChunkCount(long inputSize, long chunkSize) { 829 return (inputSize + chunkSize - 1) / chunkSize; 830 } 831 setUnsignedInt32LittleEndian(int value, byte[] result, int offset)832 private static void setUnsignedInt32LittleEndian(int value, byte[] result, int offset) { 833 result[offset] = (byte) (value & 0xff); 834 result[offset + 1] = (byte) ((value >> 8) & 0xff); 835 result[offset + 2] = (byte) ((value >> 16) & 0xff); 836 result[offset + 3] = (byte) ((value >> 24) & 0xff); 837 } 838 encodePublicKey(PublicKey publicKey)839 public static byte[] encodePublicKey(PublicKey publicKey) 840 throws InvalidKeyException, NoSuchAlgorithmException { 841 byte[] encodedPublicKey = null; 842 if ("X.509".equals(publicKey.getFormat())) { 843 encodedPublicKey = publicKey.getEncoded(); 844 // if the key is an RSA key check for a negative modulus 845 if ("RSA".equals(publicKey.getAlgorithm())) { 846 try { 847 // Parse the encoded public key into the separate elements of the 848 // SubjectPublicKeyInfo to obtain the SubjectPublicKey. 849 ByteBuffer encodedPublicKeyBuffer = ByteBuffer.wrap(encodedPublicKey); 850 SubjectPublicKeyInfo subjectPublicKeyInfo = Asn1BerParser.parse( 851 encodedPublicKeyBuffer, SubjectPublicKeyInfo.class); 852 // The SubjectPublicKey is encoded as a bit string within the 853 // SubjectPublicKeyInfo. The first byte of the encoding is the number of padding 854 // bits; store this and decode the rest of the bit string into the RSA modulus 855 // and exponent. 856 ByteBuffer subjectPublicKeyBuffer = subjectPublicKeyInfo.subjectPublicKey; 857 byte padding = subjectPublicKeyBuffer.get(); 858 RSAPublicKey rsaPublicKey = Asn1BerParser.parse(subjectPublicKeyBuffer, 859 RSAPublicKey.class); 860 // if the modulus is negative then attempt to reencode it with a leading 0 sign 861 // byte. 862 if (rsaPublicKey.modulus.compareTo(BigInteger.ZERO) < 0) { 863 // A negative modulus indicates the leading bit in the integer is 1. Per 864 // ASN.1 encoding rules to encode a positive integer with the leading bit 865 // set to 1 a byte containing all zeros should precede the integer encoding. 866 byte[] encodedModulus = rsaPublicKey.modulus.toByteArray(); 867 byte[] reencodedModulus = new byte[encodedModulus.length + 1]; 868 reencodedModulus[0] = 0; 869 System.arraycopy(encodedModulus, 0, reencodedModulus, 1, 870 encodedModulus.length); 871 rsaPublicKey.modulus = new BigInteger(reencodedModulus); 872 // Once the modulus has been corrected reencode the RSAPublicKey, then 873 // restore the padding value in the bit string and reencode the entire 874 // SubjectPublicKeyInfo to be returned to the caller. 875 byte[] reencodedRSAPublicKey = Asn1DerEncoder.encode(rsaPublicKey); 876 byte[] reencodedSubjectPublicKey = 877 new byte[reencodedRSAPublicKey.length + 1]; 878 reencodedSubjectPublicKey[0] = padding; 879 System.arraycopy(reencodedRSAPublicKey, 0, reencodedSubjectPublicKey, 1, 880 reencodedRSAPublicKey.length); 881 subjectPublicKeyInfo.subjectPublicKey = ByteBuffer.wrap( 882 reencodedSubjectPublicKey); 883 encodedPublicKey = Asn1DerEncoder.encode(subjectPublicKeyInfo); 884 } 885 } catch (Asn1DecodingException | Asn1EncodingException e) { 886 System.out.println("Caught a exception encoding the public key: " + e); 887 e.printStackTrace(); 888 encodedPublicKey = null; 889 } 890 } 891 } 892 if (encodedPublicKey == null) { 893 try { 894 encodedPublicKey = 895 KeyFactory.getInstance(publicKey.getAlgorithm()) 896 .getKeySpec(publicKey, X509EncodedKeySpec.class) 897 .getEncoded(); 898 } catch (InvalidKeySpecException e) { 899 throw new InvalidKeyException( 900 "Failed to obtain X.509 encoded form of public key " + publicKey 901 + " of class " + publicKey.getClass().getName(), 902 e); 903 } 904 } 905 if ((encodedPublicKey == null) || (encodedPublicKey.length == 0)) { 906 throw new InvalidKeyException( 907 "Failed to obtain X.509 encoded form of public key " + publicKey 908 + " of class " + publicKey.getClass().getName()); 909 } 910 return encodedPublicKey; 911 } 912 encodeCertificates(List<X509Certificate> certificates)913 public static List<byte[]> encodeCertificates(List<X509Certificate> certificates) 914 throws CertificateEncodingException { 915 List<byte[]> result = new ArrayList<>(certificates.size()); 916 for (X509Certificate certificate : certificates) { 917 result.add(certificate.getEncoded()); 918 } 919 return result; 920 } 921 encodeAsLengthPrefixedElement(byte[] bytes)922 public static byte[] encodeAsLengthPrefixedElement(byte[] bytes) { 923 byte[][] adapterBytes = new byte[1][]; 924 adapterBytes[0] = bytes; 925 return encodeAsSequenceOfLengthPrefixedElements(adapterBytes); 926 } 927 encodeAsSequenceOfLengthPrefixedElements(List<byte[]> sequence)928 public static byte[] encodeAsSequenceOfLengthPrefixedElements(List<byte[]> sequence) { 929 return encodeAsSequenceOfLengthPrefixedElements( 930 sequence.toArray(new byte[sequence.size()][])); 931 } 932 encodeAsSequenceOfLengthPrefixedElements(byte[][] sequence)933 public static byte[] encodeAsSequenceOfLengthPrefixedElements(byte[][] sequence) { 934 int payloadSize = 0; 935 for (byte[] element : sequence) { 936 payloadSize += 4 + element.length; 937 } 938 ByteBuffer result = ByteBuffer.allocate(payloadSize); 939 result.order(ByteOrder.LITTLE_ENDIAN); 940 for (byte[] element : sequence) { 941 result.putInt(element.length); 942 result.put(element); 943 } 944 return result.array(); 945 } 946 encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( List<Pair<Integer, byte[]>> sequence)947 public static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( 948 List<Pair<Integer, byte[]>> sequence) { 949 int resultSize = 0; 950 for (Pair<Integer, byte[]> element : sequence) { 951 resultSize += 12 + element.getSecond().length; 952 } 953 ByteBuffer result = ByteBuffer.allocate(resultSize); 954 result.order(ByteOrder.LITTLE_ENDIAN); 955 for (Pair<Integer, byte[]> element : sequence) { 956 byte[] second = element.getSecond(); 957 result.putInt(8 + second.length); 958 result.putInt(element.getFirst()); 959 result.putInt(second.length); 960 result.put(second); 961 } 962 return result.array(); 963 } 964 965 /** 966 * Returns the APK Signature Scheme block contained in the provided APK file for the given ID 967 * and the additional information relevant for verifying the block against the file. 968 * 969 * @param blockId the ID value in the APK Signing Block's sequence of ID-value pairs 970 * identifying the appropriate block to find, e.g. the APK Signature Scheme v2 971 * block ID. 972 * 973 * @throws SignatureNotFoundException if the APK is not signed using given APK Signature Scheme 974 * @throws IOException if an I/O error occurs while reading the APK 975 */ findSignature( DataSource apk, ApkUtils.ZipSections zipSections, int blockId, Result result)976 public static SignatureInfo findSignature( 977 DataSource apk, ApkUtils.ZipSections zipSections, int blockId, Result result) 978 throws IOException, SignatureNotFoundException { 979 // Find the APK Signing Block. 980 DataSource apkSigningBlock; 981 long apkSigningBlockOffset; 982 try { 983 ApkUtils.ApkSigningBlock apkSigningBlockInfo = 984 ApkUtils.findApkSigningBlock(apk, zipSections); 985 apkSigningBlockOffset = apkSigningBlockInfo.getStartOffset(); 986 apkSigningBlock = apkSigningBlockInfo.getContents(); 987 } catch (ApkSigningBlockNotFoundException e) { 988 throw new SignatureNotFoundException(e.getMessage(), e); 989 } 990 ByteBuffer apkSigningBlockBuf = 991 apkSigningBlock.getByteBuffer(0, (int) apkSigningBlock.size()); 992 apkSigningBlockBuf.order(ByteOrder.LITTLE_ENDIAN); 993 994 // Find the APK Signature Scheme Block inside the APK Signing Block. 995 ByteBuffer apkSignatureSchemeBlock = 996 findApkSignatureSchemeBlock(apkSigningBlockBuf, blockId, result); 997 return new SignatureInfo( 998 apkSignatureSchemeBlock, 999 apkSigningBlockOffset, 1000 zipSections.getZipCentralDirectoryOffset(), 1001 zipSections.getZipEndOfCentralDirectoryOffset(), 1002 zipSections.getZipEndOfCentralDirectory()); 1003 } 1004 1005 /** 1006 * Generates a new DataSource representing the APK contents before the Central Directory with 1007 * padding, if padding is requested. If the existing data entries before the Central Directory 1008 * are already aligned, or no padding is requested, the original DataSource is used. This 1009 * padding is used to allow for verity-based APK verification. 1010 * 1011 * @return {@code Pair} containing the potentially new {@code DataSource} and the amount of 1012 * padding used. 1013 */ generateApkSigningBlockPadding( DataSource beforeCentralDir, boolean apkSigningBlockPaddingSupported)1014 public static Pair<DataSource, Integer> generateApkSigningBlockPadding( 1015 DataSource beforeCentralDir, 1016 boolean apkSigningBlockPaddingSupported) { 1017 1018 // Ensure APK Signing Block starts from page boundary. 1019 int padSizeBeforeSigningBlock = 0; 1020 if (apkSigningBlockPaddingSupported && 1021 (beforeCentralDir.size() % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0)) { 1022 padSizeBeforeSigningBlock = (int) ( 1023 ANDROID_COMMON_PAGE_ALIGNMENT_BYTES - 1024 beforeCentralDir.size() % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES); 1025 beforeCentralDir = new ChainedDataSource( 1026 beforeCentralDir, 1027 DataSources.asDataSource( 1028 ByteBuffer.allocate(padSizeBeforeSigningBlock))); 1029 } 1030 return Pair.of(beforeCentralDir, padSizeBeforeSigningBlock); 1031 } 1032 copyWithModifiedCDOffset( DataSource beforeCentralDir, DataSource eocd)1033 public static DataSource copyWithModifiedCDOffset( 1034 DataSource beforeCentralDir, DataSource eocd) throws IOException { 1035 1036 // Ensure that, when digesting, ZIP End of Central Directory record's Central Directory 1037 // offset field is treated as pointing to the offset at which the APK Signing Block will 1038 // start. 1039 long centralDirOffsetForDigesting = beforeCentralDir.size(); 1040 ByteBuffer eocdBuf = ByteBuffer.allocate((int) eocd.size()); 1041 eocdBuf.order(ByteOrder.LITTLE_ENDIAN); 1042 eocd.copyTo(0, (int) eocd.size(), eocdBuf); 1043 eocdBuf.flip(); 1044 ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, centralDirOffsetForDigesting); 1045 return DataSources.asDataSource(eocdBuf); 1046 } 1047 generateApkSigningBlock( List<Pair<byte[], Integer>> apkSignatureSchemeBlockPairs)1048 public static byte[] generateApkSigningBlock( 1049 List<Pair<byte[], Integer>> apkSignatureSchemeBlockPairs) { 1050 // FORMAT: 1051 // uint64: size (excluding this field) 1052 // repeated ID-value pairs: 1053 // uint64: size (excluding this field) 1054 // uint32: ID 1055 // (size - 4) bytes: value 1056 // (extra dummy ID-value for padding to make block size a multiple of 4096 bytes) 1057 // uint64: size (same as the one above) 1058 // uint128: magic 1059 1060 int blocksSize = 0; 1061 for (Pair<byte[], Integer> schemeBlockPair : apkSignatureSchemeBlockPairs) { 1062 blocksSize += 8 + 4 + schemeBlockPair.getFirst().length; // size + id + value 1063 } 1064 1065 int resultSize = 1066 8 // size 1067 + blocksSize 1068 + 8 // size 1069 + 16 // magic 1070 ; 1071 ByteBuffer paddingPair = null; 1072 if (resultSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES != 0) { 1073 int padding = ANDROID_COMMON_PAGE_ALIGNMENT_BYTES - 1074 (resultSize % ANDROID_COMMON_PAGE_ALIGNMENT_BYTES); 1075 if (padding < 12) { // minimum size of an ID-value pair 1076 padding += ANDROID_COMMON_PAGE_ALIGNMENT_BYTES; 1077 } 1078 paddingPair = ByteBuffer.allocate(padding).order(ByteOrder.LITTLE_ENDIAN); 1079 paddingPair.putLong(padding - 8); 1080 paddingPair.putInt(VERITY_PADDING_BLOCK_ID); 1081 paddingPair.rewind(); 1082 resultSize += padding; 1083 } 1084 1085 ByteBuffer result = ByteBuffer.allocate(resultSize); 1086 result.order(ByteOrder.LITTLE_ENDIAN); 1087 long blockSizeFieldValue = resultSize - 8L; 1088 result.putLong(blockSizeFieldValue); 1089 1090 1091 for (Pair<byte[], Integer> schemeBlockPair : apkSignatureSchemeBlockPairs) { 1092 byte[] apkSignatureSchemeBlock = schemeBlockPair.getFirst(); 1093 int apkSignatureSchemeId = schemeBlockPair.getSecond(); 1094 long pairSizeFieldValue = 4L + apkSignatureSchemeBlock.length; 1095 result.putLong(pairSizeFieldValue); 1096 result.putInt(apkSignatureSchemeId); 1097 result.put(apkSignatureSchemeBlock); 1098 } 1099 1100 if (paddingPair != null) { 1101 result.put(paddingPair); 1102 } 1103 1104 result.putLong(blockSizeFieldValue); 1105 result.put(APK_SIGNING_BLOCK_MAGIC); 1106 1107 return result.array(); 1108 } 1109 1110 /** 1111 * Computes the digests of the given APK components according to the algorithms specified in the 1112 * given SignerConfigs. 1113 * 1114 * @param signerConfigs signer configurations, one for each signer At least one signer config 1115 * must be provided. 1116 * 1117 * @throws IOException if an I/O error occurs 1118 * @throws NoSuchAlgorithmException if a required cryptographic algorithm implementation is 1119 * missing 1120 * @throws SignatureException if an error occurs when computing digests of generating 1121 * signatures 1122 */ 1123 public static Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>> computeContentDigests( RunnablesExecutor executor, DataSource beforeCentralDir, DataSource centralDir, DataSource eocd, List<SignerConfig> signerConfigs)1124 computeContentDigests( 1125 RunnablesExecutor executor, 1126 DataSource beforeCentralDir, 1127 DataSource centralDir, 1128 DataSource eocd, 1129 List<SignerConfig> signerConfigs) 1130 throws IOException, NoSuchAlgorithmException, SignatureException { 1131 if (signerConfigs.isEmpty()) { 1132 throw new IllegalArgumentException( 1133 "No signer configs provided. At least one is required"); 1134 } 1135 1136 // Figure out which digest(s) to use for APK contents. 1137 Set<ContentDigestAlgorithm> contentDigestAlgorithms = new HashSet<>(1); 1138 for (SignerConfig signerConfig : signerConfigs) { 1139 for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) { 1140 contentDigestAlgorithms.add(signatureAlgorithm.getContentDigestAlgorithm()); 1141 } 1142 } 1143 1144 // Compute digests of APK contents. 1145 Map<ContentDigestAlgorithm, byte[]> contentDigests; // digest algorithm ID -> digest 1146 try { 1147 contentDigests = 1148 computeContentDigests( 1149 executor, 1150 contentDigestAlgorithms, 1151 beforeCentralDir, 1152 centralDir, 1153 eocd); 1154 } catch (IOException e) { 1155 throw new IOException("Failed to read APK being signed", e); 1156 } catch (DigestException e) { 1157 throw new SignatureException("Failed to compute digests of APK", e); 1158 } 1159 1160 // Sign the digests and wrap the signatures and signer info into an APK Signing Block. 1161 return Pair.of(signerConfigs, contentDigests); 1162 } 1163 1164 /** 1165 * Returns the subset of signatures which are expected to be verified by at least one Android 1166 * platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is 1167 * guaranteed to contain at least one signature. 1168 * 1169 * <p>Each Android platform version typically verifies exactly one signature from the provided 1170 * {@code signatures} set. This method returns the set of these signatures collected over all 1171 * requested platform versions. As a result, the result may contain more than one signature. 1172 * 1173 * @throws NoSupportedSignaturesException if no supported signatures were 1174 * found for an Android platform version in the range. 1175 */ getSignaturesToVerify( List<SupportedSignature> signatures, int minSdkVersion, int maxSdkVersion)1176 public static List<SupportedSignature> getSignaturesToVerify( 1177 List<SupportedSignature> signatures, int minSdkVersion, int maxSdkVersion) 1178 throws NoSupportedSignaturesException { 1179 // Pick the signature with the strongest algorithm at all required SDK versions, to mimic 1180 // Android's behavior on those versions. 1181 // 1182 // Here we assume that, once introduced, a signature algorithm continues to be supported in 1183 // all future Android versions. We also assume that the better-than relationship between 1184 // algorithms is exactly the same on all Android platform versions (except that older 1185 // platforms might support fewer algorithms). If these assumption are no longer true, the 1186 // logic here will need to change accordingly. 1187 Map<Integer, SupportedSignature> bestSigAlgorithmOnSdkVersion = new HashMap<>(); 1188 int minProvidedSignaturesVersion = Integer.MAX_VALUE; 1189 for (SupportedSignature sig : signatures) { 1190 SignatureAlgorithm sigAlgorithm = sig.algorithm; 1191 int sigMinSdkVersion = sigAlgorithm.getMinSdkVersion(); 1192 if (sigMinSdkVersion > maxSdkVersion) { 1193 continue; 1194 } 1195 if (sigMinSdkVersion < minProvidedSignaturesVersion) { 1196 minProvidedSignaturesVersion = sigMinSdkVersion; 1197 } 1198 1199 SupportedSignature candidate = bestSigAlgorithmOnSdkVersion.get(sigMinSdkVersion); 1200 if ((candidate == null) 1201 || (compareSignatureAlgorithm( 1202 sigAlgorithm, candidate.algorithm) > 0)) { 1203 bestSigAlgorithmOnSdkVersion.put(sigMinSdkVersion, sig); 1204 } 1205 } 1206 1207 // Must have some supported signature algorithms for minSdkVersion. 1208 if (minSdkVersion < minProvidedSignaturesVersion) { 1209 throw new NoSupportedSignaturesException( 1210 "Minimum provided signature version " + minProvidedSignaturesVersion + 1211 " > minSdkVersion " + minSdkVersion); 1212 } 1213 if (bestSigAlgorithmOnSdkVersion.isEmpty()) { 1214 throw new NoSupportedSignaturesException("No supported signature"); 1215 } 1216 List<SupportedSignature> signaturesToVerify = 1217 new ArrayList<>(bestSigAlgorithmOnSdkVersion.values()); 1218 Collections.sort( 1219 signaturesToVerify, 1220 (sig1, sig2) -> Integer.compare(sig1.algorithm.getId(), sig2.algorithm.getId())); 1221 return signaturesToVerify; 1222 } 1223 1224 public static class NoSupportedSignaturesException extends Exception { 1225 private static final long serialVersionUID = 1L; 1226 NoSupportedSignaturesException(String message)1227 public NoSupportedSignaturesException(String message) { 1228 super(message); 1229 } 1230 } 1231 1232 public static class SignatureNotFoundException extends Exception { 1233 private static final long serialVersionUID = 1L; 1234 SignatureNotFoundException(String message)1235 public SignatureNotFoundException(String message) { 1236 super(message); 1237 } 1238 SignatureNotFoundException(String message, Throwable cause)1239 public SignatureNotFoundException(String message, Throwable cause) { 1240 super(message, cause); 1241 } 1242 } 1243 1244 /** 1245 * uses the SignatureAlgorithms in the provided signerConfig to sign the provided data 1246 * 1247 * @return list of signature algorithm IDs and their corresponding signatures over the data. 1248 */ generateSignaturesOverData( SignerConfig signerConfig, byte[] data)1249 public static List<Pair<Integer, byte[]>> generateSignaturesOverData( 1250 SignerConfig signerConfig, byte[] data) 1251 throws InvalidKeyException, NoSuchAlgorithmException, SignatureException { 1252 List<Pair<Integer, byte[]>> signatures = 1253 new ArrayList<>(signerConfig.signatureAlgorithms.size()); 1254 PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey(); 1255 for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) { 1256 Pair<String, ? extends AlgorithmParameterSpec> sigAlgAndParams = 1257 signatureAlgorithm.getJcaSignatureAlgorithmAndParams(); 1258 String jcaSignatureAlgorithm = sigAlgAndParams.getFirst(); 1259 AlgorithmParameterSpec jcaSignatureAlgorithmParams = sigAlgAndParams.getSecond(); 1260 byte[] signatureBytes; 1261 try { 1262 Signature signature = Signature.getInstance(jcaSignatureAlgorithm); 1263 signature.initSign(signerConfig.privateKey); 1264 if (jcaSignatureAlgorithmParams != null) { 1265 signature.setParameter(jcaSignatureAlgorithmParams); 1266 } 1267 signature.update(data); 1268 signatureBytes = signature.sign(); 1269 } catch (InvalidKeyException e) { 1270 throw new InvalidKeyException("Failed to sign using " + jcaSignatureAlgorithm, e); 1271 } catch (InvalidAlgorithmParameterException | SignatureException e) { 1272 throw new SignatureException("Failed to sign using " + jcaSignatureAlgorithm, e); 1273 } 1274 1275 try { 1276 Signature signature = Signature.getInstance(jcaSignatureAlgorithm); 1277 signature.initVerify(publicKey); 1278 if (jcaSignatureAlgorithmParams != null) { 1279 signature.setParameter(jcaSignatureAlgorithmParams); 1280 } 1281 signature.update(data); 1282 if (!signature.verify(signatureBytes)) { 1283 throw new SignatureException("Failed to verify generated " 1284 + jcaSignatureAlgorithm 1285 + " signature using public key from certificate"); 1286 } 1287 } catch (InvalidKeyException e) { 1288 throw new InvalidKeyException( 1289 "Failed to verify generated " + jcaSignatureAlgorithm + " signature using" 1290 + " public key from certificate", e); 1291 } catch (InvalidAlgorithmParameterException | SignatureException e) { 1292 throw new SignatureException( 1293 "Failed to verify generated " + jcaSignatureAlgorithm + " signature using" 1294 + " public key from certificate", e); 1295 } 1296 1297 signatures.add(Pair.of(signatureAlgorithm.getId(), signatureBytes)); 1298 } 1299 return signatures; 1300 } 1301 1302 /** 1303 * Wrap the signature according to CMS PKCS #7 RFC 5652. 1304 * The high-level simplified structure is as follows: 1305 * // ContentInfo 1306 * // digestAlgorithm 1307 * // SignedData 1308 * // bag of certificates 1309 * // SignerInfo 1310 * // signing cert issuer and serial number (for locating the cert in the above bag) 1311 * // digestAlgorithm 1312 * // signatureAlgorithm 1313 * // signature 1314 * 1315 * @throws Asn1EncodingException if the ASN.1 structure could not be encoded 1316 */ generatePkcs7DerEncodedMessage( byte[] signatureBytes, ByteBuffer data, List<X509Certificate> signerCerts, AlgorithmIdentifier digestAlgorithmId, AlgorithmIdentifier signatureAlgorithmId)1317 public static byte[] generatePkcs7DerEncodedMessage( 1318 byte[] signatureBytes, ByteBuffer data, List<X509Certificate> signerCerts, 1319 AlgorithmIdentifier digestAlgorithmId, AlgorithmIdentifier signatureAlgorithmId) 1320 throws Asn1EncodingException, CertificateEncodingException { 1321 SignerInfo signerInfo = new SignerInfo(); 1322 signerInfo.version = 1; 1323 X509Certificate signingCert = signerCerts.get(0); 1324 X500Principal signerCertIssuer = signingCert.getIssuerX500Principal(); 1325 signerInfo.sid = 1326 new SignerIdentifier( 1327 new IssuerAndSerialNumber( 1328 new Asn1OpaqueObject(signerCertIssuer.getEncoded()), 1329 signingCert.getSerialNumber())); 1330 1331 signerInfo.digestAlgorithm = digestAlgorithmId; 1332 signerInfo.signatureAlgorithm = signatureAlgorithmId; 1333 signerInfo.signature = ByteBuffer.wrap(signatureBytes); 1334 1335 SignedData signedData = new SignedData(); 1336 signedData.certificates = new ArrayList<>(signerCerts.size()); 1337 for (X509Certificate cert : signerCerts) { 1338 signedData.certificates.add(new Asn1OpaqueObject(cert.getEncoded())); 1339 } 1340 signedData.version = 1; 1341 signedData.digestAlgorithms = Collections.singletonList(digestAlgorithmId); 1342 signedData.encapContentInfo = new EncapsulatedContentInfo(Pkcs7Constants.OID_DATA); 1343 // If data is not null, data will be embedded as is in the result -- an attached pcsk7 1344 signedData.encapContentInfo.content = data; 1345 signedData.signerInfos = Collections.singletonList(signerInfo); 1346 ContentInfo contentInfo = new ContentInfo(); 1347 contentInfo.contentType = Pkcs7Constants.OID_SIGNED_DATA; 1348 contentInfo.content = new Asn1OpaqueObject(Asn1DerEncoder.encode(signedData)); 1349 return Asn1DerEncoder.encode(contentInfo); 1350 } 1351 1352 /** 1353 * Picks the correct v2/v3 digest for v4 signature verification. 1354 * 1355 * Keep in sync with pickBestDigestForV4 in framework's ApkSigningBlockUtils. 1356 */ pickBestDigestForV4(Map<ContentDigestAlgorithm, byte[]> contentDigests)1357 public static byte[] pickBestDigestForV4(Map<ContentDigestAlgorithm, byte[]> contentDigests) { 1358 for (ContentDigestAlgorithm algo : V4_CONTENT_DIGEST_ALGORITHMS) { 1359 if (contentDigests.containsKey(algo)) { 1360 return contentDigests.get(algo); 1361 } 1362 } 1363 return null; 1364 } 1365 1366 /** 1367 * Signer configuration. 1368 */ 1369 public static class SignerConfig { 1370 /** Private key. */ 1371 public PrivateKey privateKey; 1372 1373 /** 1374 * Certificates, with the first certificate containing the public key corresponding to 1375 * {@link #privateKey}. 1376 */ 1377 public List<X509Certificate> certificates; 1378 1379 /** 1380 * List of signature algorithms with which to sign. 1381 */ 1382 public List<SignatureAlgorithm> signatureAlgorithms; 1383 1384 public int minSdkVersion; 1385 public int maxSdkVersion; 1386 public SigningCertificateLineage mSigningCertificateLineage; 1387 } 1388 1389 public static class Result { 1390 public final int signatureSchemeVersion; 1391 1392 /** Whether the APK's APK Signature Scheme signature verifies. */ 1393 public boolean verified; 1394 1395 public final List<Result.SignerInfo> signers = new ArrayList<>(); 1396 public SigningCertificateLineage signingCertificateLineage = null; 1397 private final List<ApkVerifier.IssueWithParams> mWarnings = new ArrayList<>(); 1398 private final List<ApkVerifier.IssueWithParams> mErrors = new ArrayList<>(); 1399 Result(int signatureSchemeVersion)1400 public Result(int signatureSchemeVersion) { 1401 this.signatureSchemeVersion = signatureSchemeVersion; 1402 } 1403 containsErrors()1404 public boolean containsErrors() { 1405 if (!mErrors.isEmpty()) { 1406 return true; 1407 } 1408 if (!signers.isEmpty()) { 1409 for (Result.SignerInfo signer : signers) { 1410 if (signer.containsErrors()) { 1411 return true; 1412 } 1413 } 1414 } 1415 return false; 1416 } 1417 containsWarnings()1418 public boolean containsWarnings() { 1419 if (!mWarnings.isEmpty()) { 1420 return true; 1421 } 1422 if (!signers.isEmpty()) { 1423 for (Result.SignerInfo signer : signers) { 1424 if (signer.containsWarnings()) { 1425 return true; 1426 } 1427 } 1428 } 1429 return false; 1430 } 1431 addError(ApkVerifier.Issue msg, Object... parameters)1432 public void addError(ApkVerifier.Issue msg, Object... parameters) { 1433 mErrors.add(new ApkVerifier.IssueWithParams(msg, parameters)); 1434 } 1435 addWarning(ApkVerifier.Issue msg, Object... parameters)1436 public void addWarning(ApkVerifier.Issue msg, Object... parameters) { 1437 mWarnings.add(new ApkVerifier.IssueWithParams(msg, parameters)); 1438 } 1439 getErrors()1440 public List<ApkVerifier.IssueWithParams> getErrors() { 1441 return mErrors; 1442 } 1443 getWarnings()1444 public List<ApkVerifier.IssueWithParams> getWarnings() { 1445 return mWarnings; 1446 } 1447 1448 public static class SignerInfo { 1449 public int index; 1450 public List<X509Certificate> certs = new ArrayList<>(); 1451 public List<ContentDigest> contentDigests = new ArrayList<>(); 1452 public Map<ContentDigestAlgorithm, byte[]> verifiedContentDigests = new HashMap<>(); 1453 public List<Signature> signatures = new ArrayList<>(); 1454 public Map<SignatureAlgorithm, byte[]> verifiedSignatures = new HashMap<>(); 1455 public List<AdditionalAttribute> additionalAttributes = new ArrayList<>(); 1456 public byte[] signedData; 1457 public int minSdkVersion; 1458 public int maxSdkVersion; 1459 public SigningCertificateLineage signingCertificateLineage; 1460 1461 private final List<ApkVerifier.IssueWithParams> mWarnings = new ArrayList<>(); 1462 private final List<ApkVerifier.IssueWithParams> mErrors = new ArrayList<>(); 1463 addError(ApkVerifier.Issue msg, Object... parameters)1464 public void addError(ApkVerifier.Issue msg, Object... parameters) { 1465 mErrors.add(new ApkVerifier.IssueWithParams(msg, parameters)); 1466 } 1467 addWarning(ApkVerifier.Issue msg, Object... parameters)1468 public void addWarning(ApkVerifier.Issue msg, Object... parameters) { 1469 mWarnings.add(new ApkVerifier.IssueWithParams(msg, parameters)); 1470 } 1471 containsErrors()1472 public boolean containsErrors() { 1473 return !mErrors.isEmpty(); 1474 } 1475 containsWarnings()1476 public boolean containsWarnings() { 1477 return !mWarnings.isEmpty(); 1478 } 1479 getErrors()1480 public List<ApkVerifier.IssueWithParams> getErrors() { 1481 return mErrors; 1482 } 1483 getWarnings()1484 public List<ApkVerifier.IssueWithParams> getWarnings() { 1485 return mWarnings; 1486 } 1487 1488 public static class ContentDigest { 1489 private final int mSignatureAlgorithmId; 1490 private final byte[] mValue; 1491 ContentDigest(int signatureAlgorithmId, byte[] value)1492 public ContentDigest(int signatureAlgorithmId, byte[] value) { 1493 mSignatureAlgorithmId = signatureAlgorithmId; 1494 mValue = value; 1495 } 1496 getSignatureAlgorithmId()1497 public int getSignatureAlgorithmId() { 1498 return mSignatureAlgorithmId; 1499 } 1500 getValue()1501 public byte[] getValue() { 1502 return mValue; 1503 } 1504 } 1505 1506 public static class Signature { 1507 private final int mAlgorithmId; 1508 private final byte[] mValue; 1509 Signature(int algorithmId, byte[] value)1510 public Signature(int algorithmId, byte[] value) { 1511 mAlgorithmId = algorithmId; 1512 mValue = value; 1513 } 1514 getAlgorithmId()1515 public int getAlgorithmId() { 1516 return mAlgorithmId; 1517 } 1518 getValue()1519 public byte[] getValue() { 1520 return mValue; 1521 } 1522 } 1523 1524 public static class AdditionalAttribute { 1525 private final int mId; 1526 private final byte[] mValue; 1527 AdditionalAttribute(int id, byte[] value)1528 public AdditionalAttribute(int id, byte[] value) { 1529 mId = id; 1530 mValue = value.clone(); 1531 } 1532 getId()1533 public int getId() { 1534 return mId; 1535 } 1536 getValue()1537 public byte[] getValue() { 1538 return mValue.clone(); 1539 } 1540 } 1541 } 1542 } 1543 1544 public static class SupportedSignature { 1545 public final SignatureAlgorithm algorithm; 1546 public final byte[] signature; 1547 SupportedSignature(SignatureAlgorithm algorithm, byte[] signature)1548 public SupportedSignature(SignatureAlgorithm algorithm, byte[] signature) { 1549 this.algorithm = algorithm; 1550 this.signature = signature; 1551 } 1552 } 1553 1554 public static class SigningSchemeBlockAndDigests { 1555 public final Pair<byte[], Integer> signingSchemeBlock; 1556 public final Map<ContentDigestAlgorithm, byte[]> digestInfo; 1557 SigningSchemeBlockAndDigests( Pair<byte[], Integer> signingSchemeBlock, Map<ContentDigestAlgorithm, byte[]> digestInfo)1558 public SigningSchemeBlockAndDigests( 1559 Pair<byte[], Integer> signingSchemeBlock, 1560 Map<ContentDigestAlgorithm, byte[]> digestInfo) { 1561 this.signingSchemeBlock = signingSchemeBlock; 1562 this.digestInfo = digestInfo; 1563 } 1564 } 1565 } 1566