| /* | |
| * Copyright (C) 2018 The Android Open Source Project | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| package android.util.apk; | |
| import android.util.ArrayMap; | |
| import android.util.Pair; | |
| import java.io.ByteArrayInputStream; | |
| import java.io.FileDescriptor; | |
| import java.io.IOException; | |
| import java.io.RandomAccessFile; | |
| import java.nio.BufferUnderflowException; | |
| import java.nio.ByteBuffer; | |
| import java.nio.ByteOrder; | |
| import java.security.DigestException; | |
| import java.security.InvalidAlgorithmParameterException; | |
| import java.security.InvalidKeyException; | |
| import java.security.MessageDigest; | |
| import java.security.NoSuchAlgorithmException; | |
| import java.security.PublicKey; | |
| import java.security.Signature; | |
| import java.security.SignatureException; | |
| import java.security.cert.CertificateException; | |
| import java.security.cert.CertificateFactory; | |
| import java.security.cert.X509Certificate; | |
| import java.security.spec.AlgorithmParameterSpec; | |
| import java.security.spec.MGF1ParameterSpec; | |
| import java.security.spec.PSSParameterSpec; | |
| import java.util.ArrayList; | |
| import java.util.Arrays; | |
| import java.util.HashSet; | |
| import java.util.List; | |
| import java.util.Map; | |
| /** | |
| * Utility class for an APK Signature Scheme using the APK Signing Block. | |
| * | |
| * @hide for internal use only. | |
| */ | |
| public final class ApkSigningBlockUtils { | |
| private ApkSigningBlockUtils() { | |
| } | |
| /** | |
| * Returns the APK Signature Scheme block contained in the provided APK file and the | |
| * additional information relevant for verifying the block against the file. | |
| * | |
| * @param blockId the ID value in the APK Signing Block's sequence of ID-value pairs | |
| * identifying the appropriate block to find, e.g. the APK Signature Scheme v2 | |
| * block ID. | |
| * @throws SignatureNotFoundException if the APK is not signed using this scheme. | |
| * @throws IOException if an I/O error occurs while reading the APK file. | |
| */ | |
| static SignatureInfo findSignature(RandomAccessFile apk, int blockId) | |
| throws IOException, SignatureNotFoundException { | |
| // Find the ZIP End of Central Directory (EoCD) record. | |
| Pair<ByteBuffer, Long> eocdAndOffsetInFile = getEocd(apk); | |
| ByteBuffer eocd = eocdAndOffsetInFile.first; | |
| long eocdOffset = eocdAndOffsetInFile.second; | |
| if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(apk, eocdOffset)) { | |
| throw new SignatureNotFoundException("ZIP64 APK not supported"); | |
| } | |
| // Find the APK Signing Block. The block immediately precedes the Central Directory. | |
| long centralDirOffset = getCentralDirOffset(eocd, eocdOffset); | |
| Pair<ByteBuffer, Long> apkSigningBlockAndOffsetInFile = | |
| findApkSigningBlock(apk, centralDirOffset); | |
| ByteBuffer apkSigningBlock = apkSigningBlockAndOffsetInFile.first; | |
| long apkSigningBlockOffset = apkSigningBlockAndOffsetInFile.second; | |
| // Find the APK Signature Scheme Block inside the APK Signing Block. | |
| ByteBuffer apkSignatureSchemeBlock = findApkSignatureSchemeBlock(apkSigningBlock, | |
| blockId); | |
| return new SignatureInfo( | |
| apkSignatureSchemeBlock, | |
| apkSigningBlockOffset, | |
| centralDirOffset, | |
| eocdOffset, | |
| eocd); | |
| } | |
| static void verifyIntegrity( | |
| Map<Integer, byte[]> expectedDigests, | |
| RandomAccessFile apk, | |
| SignatureInfo signatureInfo) throws SecurityException { | |
| if (expectedDigests.isEmpty()) { | |
| throw new SecurityException("No digests provided"); | |
| } | |
| boolean neverVerified = true; | |
| Map<Integer, byte[]> expected1MbChunkDigests = new ArrayMap<>(); | |
| if (expectedDigests.containsKey(CONTENT_DIGEST_CHUNKED_SHA256)) { | |
| expected1MbChunkDigests.put(CONTENT_DIGEST_CHUNKED_SHA256, | |
| expectedDigests.get(CONTENT_DIGEST_CHUNKED_SHA256)); | |
| } | |
| if (expectedDigests.containsKey(CONTENT_DIGEST_CHUNKED_SHA512)) { | |
| expected1MbChunkDigests.put(CONTENT_DIGEST_CHUNKED_SHA512, | |
| expectedDigests.get(CONTENT_DIGEST_CHUNKED_SHA512)); | |
| } | |
| if (!expected1MbChunkDigests.isEmpty()) { | |
| try { | |
| verifyIntegrityFor1MbChunkBasedAlgorithm(expected1MbChunkDigests, apk.getFD(), | |
| signatureInfo); | |
| neverVerified = false; | |
| } catch (IOException e) { | |
| throw new SecurityException("Cannot get FD", e); | |
| } | |
| } | |
| if (expectedDigests.containsKey(CONTENT_DIGEST_VERITY_CHUNKED_SHA256)) { | |
| verifyIntegrityForVerityBasedAlgorithm( | |
| expectedDigests.get(CONTENT_DIGEST_VERITY_CHUNKED_SHA256), apk, signatureInfo); | |
| neverVerified = false; | |
| } | |
| if (neverVerified) { | |
| throw new SecurityException("No known digest exists for integrity check"); | |
| } | |
| } | |
| static boolean isSupportedSignatureAlgorithm(int sigAlgorithm) { | |
| switch (sigAlgorithm) { | |
| case SIGNATURE_RSA_PSS_WITH_SHA256: | |
| case SIGNATURE_RSA_PSS_WITH_SHA512: | |
| case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256: | |
| case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512: | |
| case SIGNATURE_ECDSA_WITH_SHA256: | |
| case SIGNATURE_ECDSA_WITH_SHA512: | |
| case SIGNATURE_DSA_WITH_SHA256: | |
| case SIGNATURE_VERITY_RSA_PKCS1_V1_5_WITH_SHA256: | |
| case SIGNATURE_VERITY_ECDSA_WITH_SHA256: | |
| case SIGNATURE_VERITY_DSA_WITH_SHA256: | |
| return true; | |
| default: | |
| return false; | |
| } | |
| } | |
| private static void verifyIntegrityFor1MbChunkBasedAlgorithm( | |
| Map<Integer, byte[]> expectedDigests, | |
| FileDescriptor apkFileDescriptor, | |
| SignatureInfo signatureInfo) throws SecurityException { | |
| int[] digestAlgorithms = new int[expectedDigests.size()]; | |
| int digestAlgorithmCount = 0; | |
| for (int digestAlgorithm : expectedDigests.keySet()) { | |
| digestAlgorithms[digestAlgorithmCount] = digestAlgorithm; | |
| digestAlgorithmCount++; | |
| } | |
| byte[][] actualDigests; | |
| try { | |
| actualDigests = computeContentDigestsPer1MbChunk(digestAlgorithms, apkFileDescriptor, | |
| signatureInfo); | |
| } catch (DigestException e) { | |
| throw new SecurityException("Failed to compute digest(s) of contents", e); | |
| } | |
| for (int i = 0; i < digestAlgorithms.length; i++) { | |
| int digestAlgorithm = digestAlgorithms[i]; | |
| byte[] expectedDigest = expectedDigests.get(digestAlgorithm); | |
| byte[] actualDigest = actualDigests[i]; | |
| if (!MessageDigest.isEqual(expectedDigest, actualDigest)) { | |
| throw new SecurityException( | |
| getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm) | |
| + " digest of contents did not verify"); | |
| } | |
| } | |
| } | |
| /** | |
| * Calculate digests using digestAlgorithms for apkFileDescriptor. | |
| * This will skip signature block described by signatureInfo. | |
| */ | |
| public static byte[][] computeContentDigestsPer1MbChunk(int[] digestAlgorithms, | |
| FileDescriptor apkFileDescriptor, SignatureInfo signatureInfo) throws DigestException { | |
| // We need to verify the integrity of the following three sections of the file: | |
| // 1. Everything up to the start of the APK Signing Block. | |
| // 2. ZIP Central Directory. | |
| // 3. ZIP End of Central Directory (EoCD). | |
| // Each of these sections is represented as a separate DataSource instance below. | |
| // To handle large APKs, these sections are read in 1 MB chunks using memory-mapped I/O to | |
| // avoid wasting physical memory. In most APK verification scenarios, the contents of the | |
| // APK are already there in the OS's page cache and thus mmap does not use additional | |
| // physical memory. | |
| DataSource beforeApkSigningBlock = | |
| DataSource.create(apkFileDescriptor, 0, signatureInfo.apkSigningBlockOffset); | |
| DataSource centralDir = | |
| DataSource.create( | |
| apkFileDescriptor, signatureInfo.centralDirOffset, | |
| signatureInfo.eocdOffset - signatureInfo.centralDirOffset); | |
| // For the purposes of integrity verification, ZIP End of Central Directory's field Start of | |
| // Central Directory must be considered to point to the offset of the APK Signing Block. | |
| ByteBuffer eocdBuf = signatureInfo.eocd.duplicate(); | |
| eocdBuf.order(ByteOrder.LITTLE_ENDIAN); | |
| ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, signatureInfo.apkSigningBlockOffset); | |
| DataSource eocd = new ByteBufferDataSource(eocdBuf); | |
| return computeContentDigestsPer1MbChunk(digestAlgorithms, | |
| new DataSource[]{beforeApkSigningBlock, centralDir, eocd}); | |
| } | |
| private static byte[][] computeContentDigestsPer1MbChunk( | |
| int[] digestAlgorithms, | |
| DataSource[] contents) throws DigestException { | |
| // For each digest algorithm the result is computed as follows: | |
| // 1. Each segment of contents is split into consecutive chunks of 1 MB in size. | |
| // The final chunk will be shorter iff the length of segment is not a multiple of 1 MB. | |
| // No chunks are produced for empty (zero length) segments. | |
| // 2. The digest of each chunk is computed over the concatenation of byte 0xa5, the chunk's | |
| // length in bytes (uint32 little-endian) and the chunk's contents. | |
| // 3. The output digest is computed over the concatenation of the byte 0x5a, the number of | |
| // chunks (uint32 little-endian) and the concatenation of digests of chunks of all | |
| // segments in-order. | |
| long totalChunkCountLong = 0; | |
| for (DataSource input : contents) { | |
| totalChunkCountLong += getChunkCount(input.size()); | |
| } | |
| if (totalChunkCountLong >= Integer.MAX_VALUE / 1024) { | |
| throw new DigestException("Too many chunks: " + totalChunkCountLong); | |
| } | |
| int totalChunkCount = (int) totalChunkCountLong; | |
| byte[][] digestsOfChunks = new byte[digestAlgorithms.length][]; | |
| for (int i = 0; i < digestAlgorithms.length; i++) { | |
| int digestAlgorithm = digestAlgorithms[i]; | |
| int digestOutputSizeBytes = getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm); | |
| byte[] concatenationOfChunkCountAndChunkDigests = | |
| new byte[5 + totalChunkCount * digestOutputSizeBytes]; | |
| concatenationOfChunkCountAndChunkDigests[0] = 0x5a; | |
| setUnsignedInt32LittleEndian( | |
| totalChunkCount, | |
| concatenationOfChunkCountAndChunkDigests, | |
| 1); | |
| digestsOfChunks[i] = concatenationOfChunkCountAndChunkDigests; | |
| } | |
| byte[] chunkContentPrefix = new byte[5]; | |
| chunkContentPrefix[0] = (byte) 0xa5; | |
| int chunkIndex = 0; | |
| MessageDigest[] mds = new MessageDigest[digestAlgorithms.length]; | |
| for (int i = 0; i < digestAlgorithms.length; i++) { | |
| String jcaAlgorithmName = | |
| getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithms[i]); | |
| try { | |
| mds[i] = MessageDigest.getInstance(jcaAlgorithmName); | |
| } catch (NoSuchAlgorithmException e) { | |
| throw new RuntimeException(jcaAlgorithmName + " digest not supported", e); | |
| } | |
| } | |
| // TODO: Compute digests of chunks in parallel when beneficial. This requires some research | |
| // into how to parallelize (if at all) based on the capabilities of the hardware on which | |
| // this code is running and based on the size of input. | |
| DataDigester digester = new MultipleDigestDataDigester(mds); | |
| int dataSourceIndex = 0; | |
| for (DataSource input : contents) { | |
| long inputOffset = 0; | |
| long inputRemaining = input.size(); | |
| while (inputRemaining > 0) { | |
| int chunkSize = (int) Math.min(inputRemaining, CHUNK_SIZE_BYTES); | |
| setUnsignedInt32LittleEndian(chunkSize, chunkContentPrefix, 1); | |
| for (int i = 0; i < mds.length; i++) { | |
| mds[i].update(chunkContentPrefix); | |
| } | |
| try { | |
| input.feedIntoDataDigester(digester, inputOffset, chunkSize); | |
| } catch (IOException e) { | |
| throw new DigestException( | |
| "Failed to digest chunk #" + chunkIndex + " of section #" | |
| + dataSourceIndex, | |
| e); | |
| } | |
| for (int i = 0; i < digestAlgorithms.length; i++) { | |
| int digestAlgorithm = digestAlgorithms[i]; | |
| byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i]; | |
| int expectedDigestSizeBytes = | |
| getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm); | |
| MessageDigest md = mds[i]; | |
| int actualDigestSizeBytes = | |
| md.digest( | |
| concatenationOfChunkCountAndChunkDigests, | |
| 5 + chunkIndex * expectedDigestSizeBytes, | |
| expectedDigestSizeBytes); | |
| if (actualDigestSizeBytes != expectedDigestSizeBytes) { | |
| throw new RuntimeException( | |
| "Unexpected output size of " + md.getAlgorithm() + " digest: " | |
| + actualDigestSizeBytes); | |
| } | |
| } | |
| inputOffset += chunkSize; | |
| inputRemaining -= chunkSize; | |
| chunkIndex++; | |
| } | |
| dataSourceIndex++; | |
| } | |
| byte[][] result = new byte[digestAlgorithms.length][]; | |
| for (int i = 0; i < digestAlgorithms.length; i++) { | |
| int digestAlgorithm = digestAlgorithms[i]; | |
| byte[] input = digestsOfChunks[i]; | |
| String jcaAlgorithmName = getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm); | |
| MessageDigest md; | |
| try { | |
| md = MessageDigest.getInstance(jcaAlgorithmName); | |
| } catch (NoSuchAlgorithmException e) { | |
| throw new RuntimeException(jcaAlgorithmName + " digest not supported", e); | |
| } | |
| byte[] output = md.digest(input); | |
| result[i] = output; | |
| } | |
| return result; | |
| } | |
| /** | |
| * Return the verity digest only if the length of digest content looks correct. | |
| * When verity digest is generated, the last incomplete 4k chunk is padded with 0s before | |
| * hashing. This means two almost identical APKs with different number of 0 at the end will have | |
| * the same verity digest. To avoid this problem, the length of the source content (excluding | |
| * Signing Block) is appended to the verity digest, and the digest is returned only if the | |
| * length is consistent to the current APK. | |
| */ | |
| static byte[] parseVerityDigestAndVerifySourceLength( | |
| byte[] data, long fileSize, SignatureInfo signatureInfo) throws SecurityException { | |
| // FORMAT: | |
| // OFFSET DATA TYPE DESCRIPTION | |
| // * @+0 bytes uint8[32] Merkle tree root hash of SHA-256 | |
| // * @+32 bytes int64 Length of source data | |
| int kRootHashSize = 32; | |
| int kSourceLengthSize = 8; | |
| if (data.length != kRootHashSize + kSourceLengthSize) { | |
| throw new SecurityException("Verity digest size is wrong: " + data.length); | |
| } | |
| ByteBuffer buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN); | |
| buffer.position(kRootHashSize); | |
| long expectedSourceLength = buffer.getLong(); | |
| long signingBlockSize = signatureInfo.centralDirOffset | |
| - signatureInfo.apkSigningBlockOffset; | |
| if (expectedSourceLength != fileSize - signingBlockSize) { | |
| throw new SecurityException("APK content size did not verify"); | |
| } | |
| return Arrays.copyOfRange(data, 0, kRootHashSize); | |
| } | |
| private static void verifyIntegrityForVerityBasedAlgorithm( | |
| byte[] expectedDigest, | |
| RandomAccessFile apk, | |
| SignatureInfo signatureInfo) throws SecurityException { | |
| try { | |
| byte[] expectedRootHash = parseVerityDigestAndVerifySourceLength(expectedDigest, | |
| apk.getChannel().size(), signatureInfo); | |
| VerityBuilder.VerityResult verity = VerityBuilder.generateApkVerityTree(apk, | |
| signatureInfo, new ByteBufferFactory() { | |
| @Override | |
| public ByteBuffer create(int capacity) { | |
| return ByteBuffer.allocate(capacity); | |
| } | |
| }); | |
| if (!Arrays.equals(expectedRootHash, verity.rootHash)) { | |
| throw new SecurityException("APK verity digest of contents did not verify"); | |
| } | |
| } catch (DigestException | IOException | NoSuchAlgorithmException e) { | |
| throw new SecurityException("Error during verification", e); | |
| } | |
| } | |
| /** | |
| * Returns the ZIP End of Central Directory (EoCD) and its offset in the file. | |
| * | |
| * @throws IOException if an I/O error occurs while reading the file. | |
| * @throws SignatureNotFoundException if the EoCD could not be found. | |
| */ | |
| static Pair<ByteBuffer, Long> getEocd(RandomAccessFile apk) | |
| throws IOException, SignatureNotFoundException { | |
| Pair<ByteBuffer, Long> eocdAndOffsetInFile = | |
| ZipUtils.findZipEndOfCentralDirectoryRecord(apk); | |
| if (eocdAndOffsetInFile == null) { | |
| throw new SignatureNotFoundException( | |
| "Not an APK file: ZIP End of Central Directory record not found"); | |
| } | |
| return eocdAndOffsetInFile; | |
| } | |
| static long getCentralDirOffset(ByteBuffer eocd, long eocdOffset) | |
| throws SignatureNotFoundException { | |
| // Look up the offset of ZIP Central Directory. | |
| long centralDirOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocd); | |
| if (centralDirOffset > eocdOffset) { | |
| throw new SignatureNotFoundException( | |
| "ZIP Central Directory offset out of range: " + centralDirOffset | |
| + ". ZIP End of Central Directory offset: " + eocdOffset); | |
| } | |
| long centralDirSize = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocd); | |
| if (centralDirOffset + centralDirSize != eocdOffset) { | |
| throw new SignatureNotFoundException( | |
| "ZIP Central Directory is not immediately followed by End of Central" | |
| + " Directory"); | |
| } | |
| return centralDirOffset; | |
| } | |
| private static long getChunkCount(long inputSizeBytes) { | |
| return (inputSizeBytes + CHUNK_SIZE_BYTES - 1) / CHUNK_SIZE_BYTES; | |
| } | |
| private static final int CHUNK_SIZE_BYTES = 1024 * 1024; | |
| static final int SIGNATURE_RSA_PSS_WITH_SHA256 = 0x0101; | |
| static final int SIGNATURE_RSA_PSS_WITH_SHA512 = 0x0102; | |
| static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256 = 0x0103; | |
| static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512 = 0x0104; | |
| static final int SIGNATURE_ECDSA_WITH_SHA256 = 0x0201; | |
| static final int SIGNATURE_ECDSA_WITH_SHA512 = 0x0202; | |
| static final int SIGNATURE_DSA_WITH_SHA256 = 0x0301; | |
| static final int SIGNATURE_VERITY_RSA_PKCS1_V1_5_WITH_SHA256 = 0x0421; | |
| static final int SIGNATURE_VERITY_ECDSA_WITH_SHA256 = 0x0423; | |
| static final int SIGNATURE_VERITY_DSA_WITH_SHA256 = 0x0425; | |
| public static final int CONTENT_DIGEST_CHUNKED_SHA256 = 1; | |
| public static final int CONTENT_DIGEST_CHUNKED_SHA512 = 2; | |
| public static final int CONTENT_DIGEST_VERITY_CHUNKED_SHA256 = 3; | |
| public static final int CONTENT_DIGEST_SHA256 = 4; | |
| static int compareSignatureAlgorithm(int sigAlgorithm1, int sigAlgorithm2) { | |
| int digestAlgorithm1 = getSignatureAlgorithmContentDigestAlgorithm(sigAlgorithm1); | |
| int digestAlgorithm2 = getSignatureAlgorithmContentDigestAlgorithm(sigAlgorithm2); | |
| return compareContentDigestAlgorithm(digestAlgorithm1, digestAlgorithm2); | |
| } | |
| private static int compareContentDigestAlgorithm(int digestAlgorithm1, int digestAlgorithm2) { | |
| switch (digestAlgorithm1) { | |
| case CONTENT_DIGEST_CHUNKED_SHA256: | |
| switch (digestAlgorithm2) { | |
| case CONTENT_DIGEST_CHUNKED_SHA256: | |
| return 0; | |
| case CONTENT_DIGEST_CHUNKED_SHA512: | |
| case CONTENT_DIGEST_VERITY_CHUNKED_SHA256: | |
| return -1; | |
| default: | |
| throw new IllegalArgumentException( | |
| "Unknown digestAlgorithm2: " + digestAlgorithm2); | |
| } | |
| case CONTENT_DIGEST_CHUNKED_SHA512: | |
| switch (digestAlgorithm2) { | |
| case CONTENT_DIGEST_CHUNKED_SHA256: | |
| case CONTENT_DIGEST_VERITY_CHUNKED_SHA256: | |
| return 1; | |
| case CONTENT_DIGEST_CHUNKED_SHA512: | |
| return 0; | |
| default: | |
| throw new IllegalArgumentException( | |
| "Unknown digestAlgorithm2: " + digestAlgorithm2); | |
| } | |
| case CONTENT_DIGEST_VERITY_CHUNKED_SHA256: | |
| switch (digestAlgorithm2) { | |
| case CONTENT_DIGEST_CHUNKED_SHA512: | |
| return -1; | |
| case CONTENT_DIGEST_VERITY_CHUNKED_SHA256: | |
| return 0; | |
| case CONTENT_DIGEST_CHUNKED_SHA256: | |
| return 1; | |
| default: | |
| throw new IllegalArgumentException( | |
| "Unknown digestAlgorithm2: " + digestAlgorithm2); | |
| } | |
| default: | |
| throw new IllegalArgumentException("Unknown digestAlgorithm1: " + digestAlgorithm1); | |
| } | |
| } | |
| static int getSignatureAlgorithmContentDigestAlgorithm(int sigAlgorithm) { | |
| switch (sigAlgorithm) { | |
| case SIGNATURE_RSA_PSS_WITH_SHA256: | |
| case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256: | |
| case SIGNATURE_ECDSA_WITH_SHA256: | |
| case SIGNATURE_DSA_WITH_SHA256: | |
| return CONTENT_DIGEST_CHUNKED_SHA256; | |
| case SIGNATURE_RSA_PSS_WITH_SHA512: | |
| case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512: | |
| case SIGNATURE_ECDSA_WITH_SHA512: | |
| return CONTENT_DIGEST_CHUNKED_SHA512; | |
| case SIGNATURE_VERITY_RSA_PKCS1_V1_5_WITH_SHA256: | |
| case SIGNATURE_VERITY_ECDSA_WITH_SHA256: | |
| case SIGNATURE_VERITY_DSA_WITH_SHA256: | |
| return CONTENT_DIGEST_VERITY_CHUNKED_SHA256; | |
| default: | |
| throw new IllegalArgumentException( | |
| "Unknown signature algorithm: 0x" | |
| + Long.toHexString(sigAlgorithm & 0xffffffff)); | |
| } | |
| } | |
| static String getContentDigestAlgorithmJcaDigestAlgorithm(int digestAlgorithm) { | |
| switch (digestAlgorithm) { | |
| case CONTENT_DIGEST_CHUNKED_SHA256: | |
| case CONTENT_DIGEST_VERITY_CHUNKED_SHA256: | |
| return "SHA-256"; | |
| case CONTENT_DIGEST_CHUNKED_SHA512: | |
| return "SHA-512"; | |
| default: | |
| throw new IllegalArgumentException( | |
| "Unknown content digest algorthm: " + digestAlgorithm); | |
| } | |
| } | |
| private static int getContentDigestAlgorithmOutputSizeBytes(int digestAlgorithm) { | |
| switch (digestAlgorithm) { | |
| case CONTENT_DIGEST_CHUNKED_SHA256: | |
| case CONTENT_DIGEST_VERITY_CHUNKED_SHA256: | |
| return 256 / 8; | |
| case CONTENT_DIGEST_CHUNKED_SHA512: | |
| return 512 / 8; | |
| default: | |
| throw new IllegalArgumentException( | |
| "Unknown content digest algorthm: " + digestAlgorithm); | |
| } | |
| } | |
| static String getSignatureAlgorithmJcaKeyAlgorithm(int sigAlgorithm) { | |
| switch (sigAlgorithm) { | |
| case SIGNATURE_RSA_PSS_WITH_SHA256: | |
| case SIGNATURE_RSA_PSS_WITH_SHA512: | |
| case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256: | |
| case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512: | |
| case SIGNATURE_VERITY_RSA_PKCS1_V1_5_WITH_SHA256: | |
| return "RSA"; | |
| case SIGNATURE_ECDSA_WITH_SHA256: | |
| case SIGNATURE_ECDSA_WITH_SHA512: | |
| case SIGNATURE_VERITY_ECDSA_WITH_SHA256: | |
| return "EC"; | |
| case SIGNATURE_DSA_WITH_SHA256: | |
| case SIGNATURE_VERITY_DSA_WITH_SHA256: | |
| return "DSA"; | |
| default: | |
| throw new IllegalArgumentException( | |
| "Unknown signature algorithm: 0x" | |
| + Long.toHexString(sigAlgorithm & 0xffffffff)); | |
| } | |
| } | |
| static Pair<String, ? extends AlgorithmParameterSpec> | |
| getSignatureAlgorithmJcaSignatureAlgorithm(int sigAlgorithm) { | |
| switch (sigAlgorithm) { | |
| case SIGNATURE_RSA_PSS_WITH_SHA256: | |
| return Pair.create( | |
| "SHA256withRSA/PSS", | |
| new PSSParameterSpec( | |
| "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1)); | |
| case SIGNATURE_RSA_PSS_WITH_SHA512: | |
| return Pair.create( | |
| "SHA512withRSA/PSS", | |
| new PSSParameterSpec( | |
| "SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1)); | |
| case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256: | |
| case SIGNATURE_VERITY_RSA_PKCS1_V1_5_WITH_SHA256: | |
| return Pair.create("SHA256withRSA", null); | |
| case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512: | |
| return Pair.create("SHA512withRSA", null); | |
| case SIGNATURE_ECDSA_WITH_SHA256: | |
| case SIGNATURE_VERITY_ECDSA_WITH_SHA256: | |
| return Pair.create("SHA256withECDSA", null); | |
| case SIGNATURE_ECDSA_WITH_SHA512: | |
| return Pair.create("SHA512withECDSA", null); | |
| case SIGNATURE_DSA_WITH_SHA256: | |
| case SIGNATURE_VERITY_DSA_WITH_SHA256: | |
| return Pair.create("SHA256withDSA", null); | |
| default: | |
| throw new IllegalArgumentException( | |
| "Unknown signature algorithm: 0x" | |
| + Long.toHexString(sigAlgorithm & 0xffffffff)); | |
| } | |
| } | |
| /** | |
| * Returns new byte buffer whose content is a shared subsequence of this buffer's content | |
| * between the specified start (inclusive) and end (exclusive) positions. As opposed to | |
| * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source | |
| * buffer's byte order. | |
| */ | |
| static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) { | |
| if (start < 0) { | |
| throw new IllegalArgumentException("start: " + start); | |
| } | |
| if (end < start) { | |
| throw new IllegalArgumentException("end < start: " + end + " < " + start); | |
| } | |
| int capacity = source.capacity(); | |
| if (end > source.capacity()) { | |
| throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity); | |
| } | |
| int originalLimit = source.limit(); | |
| int originalPosition = source.position(); | |
| try { | |
| source.position(0); | |
| source.limit(end); | |
| source.position(start); | |
| ByteBuffer result = source.slice(); | |
| result.order(source.order()); | |
| return result; | |
| } finally { | |
| source.position(0); | |
| source.limit(originalLimit); | |
| source.position(originalPosition); | |
| } | |
| } | |
| /** | |
| * Relative <em>get</em> method for reading {@code size} number of bytes from the current | |
| * position of this buffer. | |
| * | |
| * <p>This method reads the next {@code size} bytes at this buffer's current position, | |
| * returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to | |
| * {@code size}, byte order set to this buffer's byte order; and then increments the position by | |
| * {@code size}. | |
| */ | |
| static ByteBuffer getByteBuffer(ByteBuffer source, int size) | |
| throws BufferUnderflowException { | |
| if (size < 0) { | |
| throw new IllegalArgumentException("size: " + size); | |
| } | |
| int originalLimit = source.limit(); | |
| int position = source.position(); | |
| int limit = position + size; | |
| if ((limit < position) || (limit > originalLimit)) { | |
| throw new BufferUnderflowException(); | |
| } | |
| source.limit(limit); | |
| try { | |
| ByteBuffer result = source.slice(); | |
| result.order(source.order()); | |
| source.position(limit); | |
| return result; | |
| } finally { | |
| source.limit(originalLimit); | |
| } | |
| } | |
| static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws IOException { | |
| if (source.remaining() < 4) { | |
| throw new IOException( | |
| "Remaining buffer too short to contain length of length-prefixed field." | |
| + " Remaining: " + source.remaining()); | |
| } | |
| int len = source.getInt(); | |
| if (len < 0) { | |
| throw new IllegalArgumentException("Negative length"); | |
| } else if (len > source.remaining()) { | |
| throw new IOException("Length-prefixed field longer than remaining buffer." | |
| + " Field length: " + len + ", remaining: " + source.remaining()); | |
| } | |
| return getByteBuffer(source, len); | |
| } | |
| static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws IOException { | |
| int len = buf.getInt(); | |
| if (len < 0) { | |
| throw new IOException("Negative length"); | |
| } else if (len > buf.remaining()) { | |
| throw new IOException("Underflow while reading length-prefixed value. Length: " + len | |
| + ", available: " + buf.remaining()); | |
| } | |
| byte[] result = new byte[len]; | |
| buf.get(result); | |
| return result; | |
| } | |
| static void setUnsignedInt32LittleEndian(int value, byte[] result, int offset) { | |
| result[offset] = (byte) (value & 0xff); | |
| result[offset + 1] = (byte) ((value >>> 8) & 0xff); | |
| result[offset + 2] = (byte) ((value >>> 16) & 0xff); | |
| result[offset + 3] = (byte) ((value >>> 24) & 0xff); | |
| } | |
| private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L; | |
| private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L; | |
| private static final int APK_SIG_BLOCK_MIN_SIZE = 32; | |
| static Pair<ByteBuffer, Long> findApkSigningBlock( | |
| RandomAccessFile apk, long centralDirOffset) | |
| throws IOException, SignatureNotFoundException { | |
| // FORMAT: | |
| // OFFSET DATA TYPE DESCRIPTION | |
| // * @+0 bytes uint64: size in bytes (excluding this field) | |
| // * @+8 bytes payload | |
| // * @-24 bytes uint64: size in bytes (same as the one above) | |
| // * @-16 bytes uint128: magic | |
| if (centralDirOffset < APK_SIG_BLOCK_MIN_SIZE) { | |
| throw new SignatureNotFoundException( | |
| "APK too small for APK Signing Block. ZIP Central Directory offset: " | |
| + centralDirOffset); | |
| } | |
| // Read the magic and offset in file from the footer section of the block: | |
| // * uint64: size of block | |
| // * 16 bytes: magic | |
| ByteBuffer footer = ByteBuffer.allocate(24); | |
| footer.order(ByteOrder.LITTLE_ENDIAN); | |
| apk.seek(centralDirOffset - footer.capacity()); | |
| apk.readFully(footer.array(), footer.arrayOffset(), footer.capacity()); | |
| if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO) | |
| || (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) { | |
| throw new SignatureNotFoundException( | |
| "No APK Signing Block before ZIP Central Directory"); | |
| } | |
| // Read and compare size fields | |
| long apkSigBlockSizeInFooter = footer.getLong(0); | |
| if ((apkSigBlockSizeInFooter < footer.capacity()) | |
| || (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) { | |
| throw new SignatureNotFoundException( | |
| "APK Signing Block size out of range: " + apkSigBlockSizeInFooter); | |
| } | |
| int totalSize = (int) (apkSigBlockSizeInFooter + 8); | |
| long apkSigBlockOffset = centralDirOffset - totalSize; | |
| if (apkSigBlockOffset < 0) { | |
| throw new SignatureNotFoundException( | |
| "APK Signing Block offset out of range: " + apkSigBlockOffset); | |
| } | |
| ByteBuffer apkSigBlock = ByteBuffer.allocate(totalSize); | |
| apkSigBlock.order(ByteOrder.LITTLE_ENDIAN); | |
| apk.seek(apkSigBlockOffset); | |
| apk.readFully(apkSigBlock.array(), apkSigBlock.arrayOffset(), apkSigBlock.capacity()); | |
| long apkSigBlockSizeInHeader = apkSigBlock.getLong(0); | |
| if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) { | |
| throw new SignatureNotFoundException( | |
| "APK Signing Block sizes in header and footer do not match: " | |
| + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter); | |
| } | |
| return Pair.create(apkSigBlock, apkSigBlockOffset); | |
| } | |
| static ByteBuffer findApkSignatureSchemeBlock(ByteBuffer apkSigningBlock, int blockId) | |
| throws SignatureNotFoundException { | |
| checkByteOrderLittleEndian(apkSigningBlock); | |
| // FORMAT: | |
| // OFFSET DATA TYPE DESCRIPTION | |
| // * @+0 bytes uint64: size in bytes (excluding this field) | |
| // * @+8 bytes pairs | |
| // * @-24 bytes uint64: size in bytes (same as the one above) | |
| // * @-16 bytes uint128: magic | |
| ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24); | |
| int entryCount = 0; | |
| while (pairs.hasRemaining()) { | |
| entryCount++; | |
| if (pairs.remaining() < 8) { | |
| throw new SignatureNotFoundException( | |
| "Insufficient data to read size of APK Signing Block entry #" + entryCount); | |
| } | |
| long lenLong = pairs.getLong(); | |
| if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) { | |
| throw new SignatureNotFoundException( | |
| "APK Signing Block entry #" + entryCount | |
| + " size out of range: " + lenLong); | |
| } | |
| int len = (int) lenLong; | |
| int nextEntryPos = pairs.position() + len; | |
| if (len > pairs.remaining()) { | |
| throw new SignatureNotFoundException( | |
| "APK Signing Block entry #" + entryCount + " size out of range: " + len | |
| + ", available: " + pairs.remaining()); | |
| } | |
| int id = pairs.getInt(); | |
| if (id == blockId) { | |
| return getByteBuffer(pairs, len - 4); | |
| } | |
| pairs.position(nextEntryPos); | |
| } | |
| throw new SignatureNotFoundException( | |
| "No block with ID " + blockId + " in APK Signing Block."); | |
| } | |
| private static void checkByteOrderLittleEndian(ByteBuffer buffer) { | |
| if (buffer.order() != ByteOrder.LITTLE_ENDIAN) { | |
| throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); | |
| } | |
| } | |
| /** | |
| * {@link DataDigester} that updates multiple {@link MessageDigest}s whenever data is fed. | |
| */ | |
| private static class MultipleDigestDataDigester implements DataDigester { | |
| private final MessageDigest[] mMds; | |
| MultipleDigestDataDigester(MessageDigest[] mds) { | |
| mMds = mds; | |
| } | |
| @Override | |
| public void consume(ByteBuffer buffer) { | |
| buffer = buffer.slice(); | |
| for (MessageDigest md : mMds) { | |
| buffer.position(0); | |
| md.update(buffer); | |
| } | |
| } | |
| } | |
| static VerifiedProofOfRotation verifyProofOfRotationStruct( | |
| ByteBuffer porBuf, | |
| CertificateFactory certFactory) | |
| throws SecurityException, IOException { | |
| int levelCount = 0; | |
| int lastSigAlgorithm = -1; | |
| X509Certificate lastCert = null; | |
| List<X509Certificate> certs = new ArrayList<>(); | |
| List<Integer> flagsList = new ArrayList<>(); | |
| // Proof-of-rotation struct: | |
| // A uint32 version code followed by basically a singly linked list of nodes, called levels | |
| // here, each of which have the following structure: | |
| // * length-prefix for the entire level | |
| // - length-prefixed signed data (if previous level exists) | |
| // * length-prefixed X509 Certificate | |
| // * uint32 signature algorithm ID describing how this signed data was signed | |
| // - uint32 flags describing how to treat the cert contained in this level | |
| // - uint32 signature algorithm ID to use to verify the signature of the next level. The | |
| // algorithm here must match the one in the signed data section of the next level. | |
| // - length-prefixed signature over the signed data in this level. The signature here | |
| // is verified using the certificate from the previous level. | |
| // The linking is provided by the certificate of each level signing the one of the next. | |
| try { | |
| // get the version code, but don't do anything with it: creator knew about all our flags | |
| porBuf.getInt(); | |
| HashSet<X509Certificate> certHistorySet = new HashSet<>(); | |
| while (porBuf.hasRemaining()) { | |
| levelCount++; | |
| ByteBuffer level = getLengthPrefixedSlice(porBuf); | |
| ByteBuffer signedData = getLengthPrefixedSlice(level); | |
| int flags = level.getInt(); | |
| int sigAlgorithm = level.getInt(); | |
| byte[] signature = readLengthPrefixedByteArray(level); | |
| if (lastCert != null) { | |
| // Use previous level cert to verify current level | |
| Pair<String, ? extends AlgorithmParameterSpec> sigAlgParams = | |
| getSignatureAlgorithmJcaSignatureAlgorithm(lastSigAlgorithm); | |
| PublicKey publicKey = lastCert.getPublicKey(); | |
| Signature sig = Signature.getInstance(sigAlgParams.first); | |
| sig.initVerify(publicKey); | |
| if (sigAlgParams.second != null) { | |
| sig.setParameter(sigAlgParams.second); | |
| } | |
| sig.update(signedData); | |
| if (!sig.verify(signature)) { | |
| throw new SecurityException("Unable to verify signature of certificate #" | |
| + levelCount + " using " + sigAlgParams.first + " when verifying" | |
| + " Proof-of-rotation record"); | |
| } | |
| } | |
| signedData.rewind(); | |
| byte[] encodedCert = readLengthPrefixedByteArray(signedData); | |
| int signedSigAlgorithm = signedData.getInt(); | |
| if (lastCert != null && lastSigAlgorithm != signedSigAlgorithm) { | |
| throw new SecurityException("Signing algorithm ID mismatch for certificate #" | |
| + levelCount + " when verifying Proof-of-rotation record"); | |
| } | |
| lastCert = (X509Certificate) | |
| certFactory.generateCertificate(new ByteArrayInputStream(encodedCert)); | |
| lastCert = new VerbatimX509Certificate(lastCert, encodedCert); | |
| lastSigAlgorithm = sigAlgorithm; | |
| if (certHistorySet.contains(lastCert)) { | |
| throw new SecurityException("Encountered duplicate entries in " | |
| + "Proof-of-rotation record at certificate #" + levelCount + ". All " | |
| + "signing certificates should be unique"); | |
| } | |
| certHistorySet.add(lastCert); | |
| certs.add(lastCert); | |
| flagsList.add(flags); | |
| } | |
| } catch (IOException | BufferUnderflowException e) { | |
| throw new IOException("Failed to parse Proof-of-rotation record", e); | |
| } catch (NoSuchAlgorithmException | InvalidKeyException | |
| | InvalidAlgorithmParameterException | SignatureException e) { | |
| throw new SecurityException( | |
| "Failed to verify signature over signed data for certificate #" | |
| + levelCount + " when verifying Proof-of-rotation record", e); | |
| } catch (CertificateException e) { | |
| throw new SecurityException("Failed to decode certificate #" + levelCount | |
| + " when verifying Proof-of-rotation record", e); | |
| } | |
| return new VerifiedProofOfRotation(certs, flagsList); | |
| } | |
| /** | |
| * Verified processed proof of rotation. | |
| * | |
| * @hide for internal use only. | |
| */ | |
| public static class VerifiedProofOfRotation { | |
| public final List<X509Certificate> certs; | |
| public final List<Integer> flagsList; | |
| public VerifiedProofOfRotation(List<X509Certificate> certs, List<Integer> flagsList) { | |
| this.certs = certs; | |
| this.flagsList = flagsList; | |
| } | |
| } | |
| } |