/* * Copyright (C) 2016 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.system.ErrnoException; import android.system.OsConstants; 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.math.BigInteger; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.DirectByteBuffer; import java.security.DigestException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.KeyFactory; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.Principal; import java.security.PublicKey; import java.security.Signature; import java.security.SignatureException; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.CertificateExpiredException; import java.security.cert.CertificateFactory; import java.security.cert.CertificateNotYetValidException; import java.security.cert.X509Certificate; import java.security.spec.AlgorithmParameterSpec; import java.security.spec.InvalidKeySpecException; import java.security.spec.MGF1ParameterSpec; import java.security.spec.PSSParameterSpec; import java.security.spec.X509EncodedKeySpec; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Set; import libcore.io.Libcore; import libcore.io.Os; /** * APK Signature Scheme v2 verifier. * * @hide for internal use only. */ public class ApkSignatureSchemeV2Verifier { /** * {@code .SF} file header section attribute indicating that the APK is signed not just with * JAR signature scheme but also with APK Signature Scheme v2 or newer. This attribute * facilitates v2 signature stripping detection. * *

The attribute contains a comma-separated set of signature scheme IDs. */ public static final String SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME = "X-Android-APK-Signed"; public static final int SF_ATTRIBUTE_ANDROID_APK_SIGNED_ID = 2; /** * Returns {@code true} if the provided APK contains an APK Signature Scheme V2 signature. * *

NOTE: This method does not verify the signature. */ public static boolean hasSignature(String apkFile) throws IOException { try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) { findSignature(apk); return true; } catch (SignatureNotFoundException e) { return false; } } /** * Verifies APK Signature Scheme v2 signatures of the provided APK and returns the certificates * associated with each signer. * * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2. * @throws SecurityException if a APK Signature Scheme v2 signature of this APK does not verify. * @throws IOException if an I/O error occurs while reading the APK file. */ public static X509Certificate[][] verify(String apkFile) throws SignatureNotFoundException, SecurityException, IOException { try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) { return verify(apk); } } /** * Verifies APK Signature Scheme v2 signatures of the provided APK and returns the certificates * associated with each signer. * * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2. * @throws SecurityException if an APK Signature Scheme v2 signature of this APK does not * verify. * @throws IOException if an I/O error occurs while reading the APK file. */ private static X509Certificate[][] verify(RandomAccessFile apk) throws SignatureNotFoundException, SecurityException, IOException { SignatureInfo signatureInfo = findSignature(apk); return verify(apk.getFD(), signatureInfo); } /** * APK Signature Scheme v2 block and additional information relevant to verifying the signatures * contained in the block against the file. */ private static class SignatureInfo { /** Contents of APK Signature Scheme v2 block. */ private final ByteBuffer signatureBlock; /** Position of the APK Signing Block in the file. */ private final long apkSigningBlockOffset; /** Position of the ZIP Central Directory in the file. */ private final long centralDirOffset; /** Position of the ZIP End of Central Directory (EoCD) in the file. */ private final long eocdOffset; /** Contents of ZIP End of Central Directory (EoCD) of the file. */ private final ByteBuffer eocd; private SignatureInfo( ByteBuffer signatureBlock, long apkSigningBlockOffset, long centralDirOffset, long eocdOffset, ByteBuffer eocd) { this.signatureBlock = signatureBlock; this.apkSigningBlockOffset = apkSigningBlockOffset; this.centralDirOffset = centralDirOffset; this.eocdOffset = eocdOffset; this.eocd = eocd; } } /** * Returns the APK Signature Scheme v2 block contained in the provided APK file and the * additional information relevant for verifying the block against the file. * * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2. * @throws IOException if an I/O error occurs while reading the APK file. */ private static SignatureInfo findSignature(RandomAccessFile apk) throws IOException, SignatureNotFoundException { // Find the ZIP End of Central Directory (EoCD) record. Pair 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 apkSigningBlockAndOffsetInFile = findApkSigningBlock(apk, centralDirOffset); ByteBuffer apkSigningBlock = apkSigningBlockAndOffsetInFile.first; long apkSigningBlockOffset = apkSigningBlockAndOffsetInFile.second; // Find the APK Signature Scheme v2 Block inside the APK Signing Block. ByteBuffer apkSignatureSchemeV2Block = findApkSignatureSchemeV2Block(apkSigningBlock); return new SignatureInfo( apkSignatureSchemeV2Block, apkSigningBlockOffset, centralDirOffset, eocdOffset, eocd); } /** * Verifies the contents of the provided APK file against the provided APK Signature Scheme v2 * Block. * * @param signatureInfo APK Signature Scheme v2 Block and information relevant for verifying it * against the APK file. */ private static X509Certificate[][] verify( FileDescriptor apkFileDescriptor, SignatureInfo signatureInfo) throws SecurityException { int signerCount = 0; Map contentDigests = new ArrayMap<>(); List signerCerts = new ArrayList<>(); CertificateFactory certFactory; try { certFactory = CertificateFactory.getInstance("X.509"); } catch (CertificateException e) { throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e); } ByteBuffer signers; try { signers = getLengthPrefixedSlice(signatureInfo.signatureBlock); } catch (IOException e) { throw new SecurityException("Failed to read list of signers", e); } while (signers.hasRemaining()) { signerCount++; try { ByteBuffer signer = getLengthPrefixedSlice(signers); X509Certificate[] certs = verifySigner(signer, contentDigests, certFactory); signerCerts.add(certs); } catch (IOException | BufferUnderflowException | SecurityException e) { throw new SecurityException( "Failed to parse/verify signer #" + signerCount + " block", e); } } if (signerCount < 1) { throw new SecurityException("No signers found"); } if (contentDigests.isEmpty()) { throw new SecurityException("No content digests found"); } verifyIntegrity( contentDigests, apkFileDescriptor, signatureInfo.apkSigningBlockOffset, signatureInfo.centralDirOffset, signatureInfo.eocdOffset, signatureInfo.eocd); return signerCerts.toArray(new X509Certificate[signerCerts.size()][]); } private static X509Certificate[] verifySigner( ByteBuffer signerBlock, Map contentDigests, CertificateFactory certFactory) throws SecurityException, IOException { ByteBuffer signedData = getLengthPrefixedSlice(signerBlock); ByteBuffer signatures = getLengthPrefixedSlice(signerBlock); byte[] publicKeyBytes = readLengthPrefixedByteArray(signerBlock); int signatureCount = 0; int bestSigAlgorithm = -1; byte[] bestSigAlgorithmSignatureBytes = null; List signaturesSigAlgorithms = new ArrayList<>(); while (signatures.hasRemaining()) { signatureCount++; try { ByteBuffer signature = getLengthPrefixedSlice(signatures); if (signature.remaining() < 8) { throw new SecurityException("Signature record too short"); } int sigAlgorithm = signature.getInt(); signaturesSigAlgorithms.add(sigAlgorithm); if (!isSupportedSignatureAlgorithm(sigAlgorithm)) { continue; } if ((bestSigAlgorithm == -1) || (compareSignatureAlgorithm(sigAlgorithm, bestSigAlgorithm) > 0)) { bestSigAlgorithm = sigAlgorithm; bestSigAlgorithmSignatureBytes = readLengthPrefixedByteArray(signature); } } catch (IOException | BufferUnderflowException e) { throw new SecurityException( "Failed to parse signature record #" + signatureCount, e); } } if (bestSigAlgorithm == -1) { if (signatureCount == 0) { throw new SecurityException("No signatures found"); } else { throw new SecurityException("No supported signatures found"); } } String keyAlgorithm = getSignatureAlgorithmJcaKeyAlgorithm(bestSigAlgorithm); Pair signatureAlgorithmParams = getSignatureAlgorithmJcaSignatureAlgorithm(bestSigAlgorithm); String jcaSignatureAlgorithm = signatureAlgorithmParams.first; AlgorithmParameterSpec jcaSignatureAlgorithmParams = signatureAlgorithmParams.second; boolean sigVerified; try { PublicKey publicKey = KeyFactory.getInstance(keyAlgorithm) .generatePublic(new X509EncodedKeySpec(publicKeyBytes)); Signature sig = Signature.getInstance(jcaSignatureAlgorithm); sig.initVerify(publicKey); if (jcaSignatureAlgorithmParams != null) { sig.setParameter(jcaSignatureAlgorithmParams); } sig.update(signedData); sigVerified = sig.verify(bestSigAlgorithmSignatureBytes); } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException | InvalidAlgorithmParameterException | SignatureException e) { throw new SecurityException( "Failed to verify " + jcaSignatureAlgorithm + " signature", e); } if (!sigVerified) { throw new SecurityException(jcaSignatureAlgorithm + " signature did not verify"); } // Signature over signedData has verified. byte[] contentDigest = null; signedData.clear(); ByteBuffer digests = getLengthPrefixedSlice(signedData); List digestsSigAlgorithms = new ArrayList<>(); int digestCount = 0; while (digests.hasRemaining()) { digestCount++; try { ByteBuffer digest = getLengthPrefixedSlice(digests); if (digest.remaining() < 8) { throw new IOException("Record too short"); } int sigAlgorithm = digest.getInt(); digestsSigAlgorithms.add(sigAlgorithm); if (sigAlgorithm == bestSigAlgorithm) { contentDigest = readLengthPrefixedByteArray(digest); } } catch (IOException | BufferUnderflowException e) { throw new IOException("Failed to parse digest record #" + digestCount, e); } } if (!signaturesSigAlgorithms.equals(digestsSigAlgorithms)) { throw new SecurityException( "Signature algorithms don't match between digests and signatures records"); } int digestAlgorithm = getSignatureAlgorithmContentDigestAlgorithm(bestSigAlgorithm); byte[] previousSignerDigest = contentDigests.put(digestAlgorithm, contentDigest); if ((previousSignerDigest != null) && (!MessageDigest.isEqual(previousSignerDigest, contentDigest))) { throw new SecurityException( getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm) + " contents digest does not match the digest specified by a preceding signer"); } ByteBuffer certificates = getLengthPrefixedSlice(signedData); List certs = new ArrayList<>(); int certificateCount = 0; while (certificates.hasRemaining()) { certificateCount++; byte[] encodedCert = readLengthPrefixedByteArray(certificates); X509Certificate certificate; try { certificate = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(encodedCert)); } catch (CertificateException e) { throw new SecurityException("Failed to decode certificate #" + certificateCount, e); } certificate = new VerbatimX509Certificate(certificate, encodedCert); certs.add(certificate); } if (certs.isEmpty()) { throw new SecurityException("No certificates listed"); } X509Certificate mainCertificate = certs.get(0); byte[] certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded(); if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) { throw new SecurityException( "Public key mismatch between certificate and signature record"); } return certs.toArray(new X509Certificate[certs.size()]); } private static void verifyIntegrity( Map expectedDigests, FileDescriptor apkFileDescriptor, long apkSigningBlockOffset, long centralDirOffset, long eocdOffset, ByteBuffer eocdBuf) throws SecurityException { if (expectedDigests.isEmpty()) { throw new SecurityException("No digests provided"); } // 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 = new MemoryMappedFileDataSource(apkFileDescriptor, 0, apkSigningBlockOffset); DataSource centralDir = new MemoryMappedFileDataSource( apkFileDescriptor, centralDirOffset, eocdOffset - 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. eocdBuf = eocdBuf.duplicate(); eocdBuf.order(ByteOrder.LITTLE_ENDIAN); ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, apkSigningBlockOffset); DataSource eocd = new ByteBufferDataSource(eocdBuf); int[] digestAlgorithms = new int[expectedDigests.size()]; int digestAlgorithmCount = 0; for (int digestAlgorithm : expectedDigests.keySet()) { digestAlgorithms[digestAlgorithmCount] = digestAlgorithm; digestAlgorithmCount++; } byte[][] actualDigests; try { actualDigests = computeContentDigests( digestAlgorithms, new DataSource[] {beforeApkSigningBlock, centralDir, eocd}); } 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"); } } } private static byte[][] computeContentDigests( 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. 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.feedIntoMessageDigests(mds, 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; } /** * 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. */ private static Pair getEocd(RandomAccessFile apk) throws IOException, SignatureNotFoundException { Pair eocdAndOffsetInFile = ZipUtils.findZipEndOfCentralDirectoryRecord(apk); if (eocdAndOffsetInFile == null) { throw new SignatureNotFoundException( "Not an APK file: ZIP End of Central Directory record not found"); } return eocdAndOffsetInFile; } private 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 final long getChunkCount(long inputSizeBytes) { return (inputSizeBytes + CHUNK_SIZE_BYTES - 1) / CHUNK_SIZE_BYTES; } private static final int CHUNK_SIZE_BYTES = 1024 * 1024; private static final int SIGNATURE_RSA_PSS_WITH_SHA256 = 0x0101; private static final int SIGNATURE_RSA_PSS_WITH_SHA512 = 0x0102; private static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256 = 0x0103; private static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512 = 0x0104; private static final int SIGNATURE_ECDSA_WITH_SHA256 = 0x0201; private static final int SIGNATURE_ECDSA_WITH_SHA512 = 0x0202; private static final int SIGNATURE_DSA_WITH_SHA256 = 0x0301; private static final int CONTENT_DIGEST_CHUNKED_SHA256 = 1; private static final int CONTENT_DIGEST_CHUNKED_SHA512 = 2; private 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: return true; default: return false; } } private 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: return -1; default: throw new IllegalArgumentException( "Unknown digestAlgorithm2: " + digestAlgorithm2); } case CONTENT_DIGEST_CHUNKED_SHA512: switch (digestAlgorithm2) { case CONTENT_DIGEST_CHUNKED_SHA256: return 1; case CONTENT_DIGEST_CHUNKED_SHA512: return 0; default: throw new IllegalArgumentException( "Unknown digestAlgorithm2: " + digestAlgorithm2); } default: throw new IllegalArgumentException("Unknown digestAlgorithm1: " + digestAlgorithm1); } } private 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; default: throw new IllegalArgumentException( "Unknown signature algorithm: 0x" + Long.toHexString(sigAlgorithm & 0xffffffff)); } } private static String getContentDigestAlgorithmJcaDigestAlgorithm(int digestAlgorithm) { switch (digestAlgorithm) { case CONTENT_DIGEST_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: return 256 / 8; case CONTENT_DIGEST_CHUNKED_SHA512: return 512 / 8; default: throw new IllegalArgumentException( "Unknown content digest algorthm: " + digestAlgorithm); } } private 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: return "RSA"; case SIGNATURE_ECDSA_WITH_SHA256: case SIGNATURE_ECDSA_WITH_SHA512: return "EC"; case SIGNATURE_DSA_WITH_SHA256: return "DSA"; default: throw new IllegalArgumentException( "Unknown signature algorithm: 0x" + Long.toHexString(sigAlgorithm & 0xffffffff)); } } private static Pair 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: return Pair.create("SHA256withRSA", null); case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512: return Pair.create("SHA512withRSA", null); case SIGNATURE_ECDSA_WITH_SHA256: return Pair.create("SHA256withECDSA", null); case SIGNATURE_ECDSA_WITH_SHA512: return Pair.create("SHA512withECDSA", null); case SIGNATURE_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. */ private 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 get method for reading {@code size} number of bytes from the current * position of this buffer. * *

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}. */ private 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); } } private 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); } private 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; } private 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; private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a; private static Pair 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); } private static ByteBuffer findApkSignatureSchemeV2Block(ByteBuffer apkSigningBlock) 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 == APK_SIGNATURE_SCHEME_V2_BLOCK_ID) { return getByteBuffer(pairs, len - 4); } pairs.position(nextEntryPos); } throw new SignatureNotFoundException( "No APK Signature Scheme v2 block 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"); } } public static class SignatureNotFoundException extends Exception { private static final long serialVersionUID = 1L; public SignatureNotFoundException(String message) { super(message); } public SignatureNotFoundException(String message, Throwable cause) { super(message, cause); } } /** * Source of data to be digested. */ private static interface DataSource { /** * Returns the size (in bytes) of the data offered by this source. */ long size(); /** * Feeds the specified region of this source's data into the provided digests. Each digest * instance gets the same data. * * @param offset offset of the region inside this data source. * @param size size (in bytes) of the region. */ void feedIntoMessageDigests(MessageDigest[] mds, long offset, int size) throws IOException; } /** * {@link DataSource} which provides data from a file descriptor by memory-mapping the sections * of the file requested by * {@link DataSource#feedIntoMessageDigests(MessageDigest[], long, int) feedIntoMessageDigests}. */ private static final class MemoryMappedFileDataSource implements DataSource { private static final Os OS = Libcore.os; private static final long MEMORY_PAGE_SIZE_BYTES = OS.sysconf(OsConstants._SC_PAGESIZE); private final FileDescriptor mFd; private final long mFilePosition; private final long mSize; /** * Constructs a new {@code MemoryMappedFileDataSource} for the specified region of the file. * * @param position start position of the region in the file. * @param size size (in bytes) of the region. */ public MemoryMappedFileDataSource(FileDescriptor fd, long position, long size) { mFd = fd; mFilePosition = position; mSize = size; } @Override public long size() { return mSize; } @Override public void feedIntoMessageDigests( MessageDigest[] mds, long offset, int size) throws IOException { // IMPLEMENTATION NOTE: After a lot of experimentation, the implementation of this // method was settled on a straightforward mmap with prefaulting. // // This method is not using FileChannel.map API because that API does not offset a way // to "prefault" the resulting memory pages. Without prefaulting, performance is about // 10% slower on small to medium APKs, but is significantly worse for APKs in 500+ MB // range. FileChannel.load (which currently uses madvise) doesn't help. Finally, // invoking madvise (MADV_SEQUENTIAL) after mmap with prefaulting wastes quite a bit of // time, which is not compensated for by faster reads. // We mmap the smallest region of the file containing the requested data. mmap requires // that the start offset in the file must be a multiple of memory page size. We thus may // need to mmap from an offset less than the requested offset. long filePosition = mFilePosition + offset; long mmapFilePosition = (filePosition / MEMORY_PAGE_SIZE_BYTES) * MEMORY_PAGE_SIZE_BYTES; int dataStartOffsetInMmapRegion = (int) (filePosition - mmapFilePosition); long mmapRegionSize = size + dataStartOffsetInMmapRegion; long mmapPtr = 0; try { mmapPtr = OS.mmap( 0, // let the OS choose the start address of the region in memory mmapRegionSize, OsConstants.PROT_READ, OsConstants.MAP_SHARED | OsConstants.MAP_POPULATE, // "prefault" all pages mFd, mmapFilePosition); // Feeding a memory region into MessageDigest requires the region to be represented // as a direct ByteBuffer. ByteBuffer buf = new DirectByteBuffer( size, mmapPtr + dataStartOffsetInMmapRegion, mFd, // not really needed, but just in case null, // no need to clean up -- it's taken care of by the finally block true // read only buffer ); for (MessageDigest md : mds) { buf.position(0); md.update(buf); } } catch (ErrnoException e) { throw new IOException("Failed to mmap " + mmapRegionSize + " bytes", e); } finally { if (mmapPtr != 0) { try { OS.munmap(mmapPtr, mmapRegionSize); } catch (ErrnoException ignored) {} } } } } /** * {@link DataSource} which provides data from a {@link ByteBuffer}. */ private static final class ByteBufferDataSource implements DataSource { /** * Underlying buffer. The data is stored between position 0 and the buffer's capacity. * The buffer's position is 0 and limit is equal to capacity. */ private final ByteBuffer mBuf; public ByteBufferDataSource(ByteBuffer buf) { // Defensive copy, to avoid changes to mBuf being visible in buf. mBuf = buf.slice(); } @Override public long size() { return mBuf.capacity(); } @Override public void feedIntoMessageDigests( MessageDigest[] mds, long offset, int size) throws IOException { // There's no way to tell MessageDigest to read data from ByteBuffer from a position // other than the buffer's current position. We thus need to change the buffer's // position to match the requested offset. // // In the future, it may be necessary to compute digests of multiple regions in // parallel. Given that digest computation is a slow operation, we enable multiple // such requests to be fulfilled by this instance. This is achieved by serially // creating a new ByteBuffer corresponding to the requested data range and then, // potentially concurrently, feeding these buffers into MessageDigest instances. ByteBuffer region; synchronized (mBuf) { mBuf.position((int) offset); mBuf.limit((int) offset + size); region = mBuf.slice(); } for (MessageDigest md : mds) { // Need to reset position to 0 at the start of each iteration because // MessageDigest.update below sets it to the buffer's limit. region.position(0); md.update(region); } } } /** * For legacy reasons we need to return exactly the original encoded certificate bytes, instead * of letting the underlying implementation have a shot at re-encoding the data. */ private static class VerbatimX509Certificate extends WrappedX509Certificate { private byte[] encodedVerbatim; public VerbatimX509Certificate(X509Certificate wrapped, byte[] encodedVerbatim) { super(wrapped); this.encodedVerbatim = encodedVerbatim; } @Override public byte[] getEncoded() throws CertificateEncodingException { return encodedVerbatim; } } private static class WrappedX509Certificate extends X509Certificate { private final X509Certificate wrapped; public WrappedX509Certificate(X509Certificate wrapped) { this.wrapped = wrapped; } @Override public Set getCriticalExtensionOIDs() { return wrapped.getCriticalExtensionOIDs(); } @Override public byte[] getExtensionValue(String oid) { return wrapped.getExtensionValue(oid); } @Override public Set getNonCriticalExtensionOIDs() { return wrapped.getNonCriticalExtensionOIDs(); } @Override public boolean hasUnsupportedCriticalExtension() { return wrapped.hasUnsupportedCriticalExtension(); } @Override public void checkValidity() throws CertificateExpiredException, CertificateNotYetValidException { wrapped.checkValidity(); } @Override public void checkValidity(Date date) throws CertificateExpiredException, CertificateNotYetValidException { wrapped.checkValidity(date); } @Override public int getVersion() { return wrapped.getVersion(); } @Override public BigInteger getSerialNumber() { return wrapped.getSerialNumber(); } @Override public Principal getIssuerDN() { return wrapped.getIssuerDN(); } @Override public Principal getSubjectDN() { return wrapped.getSubjectDN(); } @Override public Date getNotBefore() { return wrapped.getNotBefore(); } @Override public Date getNotAfter() { return wrapped.getNotAfter(); } @Override public byte[] getTBSCertificate() throws CertificateEncodingException { return wrapped.getTBSCertificate(); } @Override public byte[] getSignature() { return wrapped.getSignature(); } @Override public String getSigAlgName() { return wrapped.getSigAlgName(); } @Override public String getSigAlgOID() { return wrapped.getSigAlgOID(); } @Override public byte[] getSigAlgParams() { return wrapped.getSigAlgParams(); } @Override public boolean[] getIssuerUniqueID() { return wrapped.getIssuerUniqueID(); } @Override public boolean[] getSubjectUniqueID() { return wrapped.getSubjectUniqueID(); } @Override public boolean[] getKeyUsage() { return wrapped.getKeyUsage(); } @Override public int getBasicConstraints() { return wrapped.getBasicConstraints(); } @Override public byte[] getEncoded() throws CertificateEncodingException { return wrapped.getEncoded(); } @Override public void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException { wrapped.verify(key); } @Override public void verify(PublicKey key, String sigProvider) throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException { wrapped.verify(key, sigProvider); } @Override public String toString() { return wrapped.toString(); } @Override public PublicKey getPublicKey() { return wrapped.getPublicKey(); } } }