Merge "Unbreak verifying v2 signatures of large APKs." into nyc-dev

This commit is contained in:
Alex Klyubin
2016-03-22 19:40:31 +00:00
committed by Android (Google) Code Review
2 changed files with 491 additions and 204 deletions

View File

@@ -16,17 +16,20 @@
package android.util.apk; package android.util.apk;
import android.system.ErrnoException;
import android.system.OsConstants;
import android.util.ArrayMap;
import android.util.Pair; import android.util.Pair;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.FileDescriptor;
import java.io.IOException; import java.io.IOException;
import java.io.RandomAccessFile; import java.io.RandomAccessFile;
import java.math.BigInteger; import java.math.BigInteger;
import java.nio.BufferUnderflowException; import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.nio.MappedByteBuffer; import java.nio.DirectByteBuffer;
import java.nio.channels.FileChannel;
import java.security.DigestException; import java.security.DigestException;
import java.security.InvalidAlgorithmParameterException; import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
@@ -52,11 +55,13 @@ import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import libcore.io.Libcore;
import libcore.io.Os;
/** /**
* APK Signature Scheme v2 verifier. * APK Signature Scheme v2 verifier.
* *
@@ -75,44 +80,17 @@ public class ApkSignatureSchemeV2Verifier {
public static final int SF_ATTRIBUTE_ANDROID_APK_SIGNED_ID = 2; public static final int SF_ATTRIBUTE_ANDROID_APK_SIGNED_ID = 2;
/** /**
* Returns {@code true} if the provided APK contains an APK Signature Scheme V2 * Returns {@code true} if the provided APK contains an APK Signature Scheme V2 signature.
* signature. The signature will not be verified. *
* <p><b>NOTE: This method does not verify the signature.</b>
*/ */
public static boolean hasSignature(String apkFile) throws IOException { public static boolean hasSignature(String apkFile) throws IOException {
try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) { try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) {
long fileSize = apk.length(); findSignature(apk);
if (fileSize > Integer.MAX_VALUE) {
return false;
}
MappedByteBuffer apkContents;
try {
apkContents = apk.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, fileSize);
} catch (IOException e) {
if (e.getCause() instanceof OutOfMemoryError) {
// TODO: Remove this temporary workaround once verifying large APKs is
// supported. Very large APKs cannot be memory-mapped. This verification code
// needs to change to use a different approach for verifying such APKs.
return false; // Pretend that this APK does not have a v2 signature.
} else {
throw new IOException("Failed to memory-map APK", e);
}
}
// ZipUtils and APK Signature Scheme v2 verifier expect little-endian byte order.
apkContents.order(ByteOrder.LITTLE_ENDIAN);
final int centralDirOffset =
(int) getCentralDirOffset(apkContents, getEocdOffset(apkContents));
// Find the APK Signing Block.
int apkSigningBlockOffset = findApkSigningBlock(apkContents, centralDirOffset);
ByteBuffer apkSigningBlock =
sliceFromTo(apkContents, apkSigningBlockOffset, centralDirOffset);
// Find the APK Signature Scheme v2 Block inside the APK Signing Block.
findApkSignatureSchemeV2Block(apkSigningBlock);
return true; return true;
} catch (SignatureNotFoundException e) { } catch (SignatureNotFoundException e) {
return false;
} }
return false;
} }
/** /**
@@ -135,90 +113,97 @@ public class ApkSignatureSchemeV2Verifier {
* associated with each signer. * associated with each signer.
* *
* @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2. * @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 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. * @throws IOException if an I/O error occurs while reading the APK file.
*/ */
public static X509Certificate[][] verify(RandomAccessFile apk) private static X509Certificate[][] verify(RandomAccessFile apk)
throws SignatureNotFoundException, SecurityException, IOException { throws SignatureNotFoundException, SecurityException, IOException {
SignatureInfo signatureInfo = findSignature(apk);
long fileSize = apk.length(); return verify(apk.getFD(), signatureInfo);
if (fileSize > Integer.MAX_VALUE) {
throw new IOException("File too large: " + apk.length() + " bytes");
}
MappedByteBuffer apkContents;
try {
apkContents = apk.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, fileSize);
// Attempt to preload the contents into memory for faster overall verification (v2 and
// older) at the expense of somewhat increased latency for rejecting malformed APKs.
apkContents.load();
} catch (IOException e) {
if (e.getCause() instanceof OutOfMemoryError) {
// TODO: Remove this temporary workaround once verifying large APKs is supported.
// Very large APKs cannot be memory-mapped. This verification code needs to change
// to use a different approach for verifying such APKs.
// This workaround pretends that this APK does not have a v2 signature. This works
// fine provided the APK is not actually v2-signed. If the APK is v2 signed, v2
// signature stripping protection inside v1 signature verification code will reject
// this APK.
throw new SignatureNotFoundException("Failed to memory-map APK", e);
} else {
throw new IOException("Failed to memory-map APK", e);
}
}
return verify(apkContents);
} }
/** /**
* Verifies APK Signature Scheme v2 signatures of the provided APK and returns the certificates * APK Signature Scheme v2 block and additional information relevant to verifying the signatures
* associated with each signer. * contained in the block against the file.
* */
* @param apkContents contents of the APK. The contents start at the current position and end private static class SignatureInfo {
* at the limit of the buffer. /** 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 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(ByteBuffer apkContents) private static SignatureInfo findSignature(RandomAccessFile apk)
throws SignatureNotFoundException, SecurityException { throws IOException, SignatureNotFoundException {
// Avoid modifying byte order, position, limit, and mark of the original apkContents. // Find the ZIP End of Central Directory (EoCD) record.
apkContents = apkContents.slice(); 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");
}
// ZipUtils and APK Signature Scheme v2 verifier expect little-endian byte order. // Find the APK Signing Block. The block immediately precedes the Central Directory.
apkContents.order(ByteOrder.LITTLE_ENDIAN); long centralDirOffset = getCentralDirOffset(eocd, eocdOffset);
Pair<ByteBuffer, Long> apkSigningBlockAndOffsetInFile =
final int eocdOffset = getEocdOffset(apkContents); findApkSigningBlock(apk, centralDirOffset);
final int centralDirOffset = (int) getCentralDirOffset(apkContents, eocdOffset); ByteBuffer apkSigningBlock = apkSigningBlockAndOffsetInFile.first;
long apkSigningBlockOffset = apkSigningBlockAndOffsetInFile.second;
// Find the APK Signing Block.
int apkSigningBlockOffset = findApkSigningBlock(apkContents, centralDirOffset);
ByteBuffer apkSigningBlock =
sliceFromTo(apkContents, apkSigningBlockOffset, centralDirOffset);
// Find the APK Signature Scheme v2 Block inside the APK Signing Block. // Find the APK Signature Scheme v2 Block inside the APK Signing Block.
ByteBuffer apkSignatureSchemeV2Block = findApkSignatureSchemeV2Block(apkSigningBlock); ByteBuffer apkSignatureSchemeV2Block = findApkSignatureSchemeV2Block(apkSigningBlock);
// Verify the contents of the APK outside of the APK Signing Block using the APK Signature return new SignatureInfo(
// Scheme v2 Block.
return verify(
apkContents,
apkSignatureSchemeV2Block, apkSignatureSchemeV2Block,
apkSigningBlockOffset, apkSigningBlockOffset,
centralDirOffset, centralDirOffset,
eocdOffset); eocdOffset,
eocd);
} }
/** /**
* Verifies the contents outside of the APK Signing Block using the provided APK Signature * Verifies the contents of the provided APK file against the provided APK Signature Scheme v2
* Scheme v2 Block. * Block.
*
* @param signatureInfo APK Signature Scheme v2 Block and information relevant for verifying it
* against the APK file.
*/ */
private static X509Certificate[][] verify( private static X509Certificate[][] verify(
ByteBuffer apkContents, FileDescriptor apkFileDescriptor,
ByteBuffer v2Block, SignatureInfo signatureInfo) throws SecurityException {
int apkSigningBlockOffset,
int centralDirOffset,
int eocdOffset) throws SecurityException {
int signerCount = 0; int signerCount = 0;
Map<Integer, byte[]> contentDigests = new HashMap<>(); Map<Integer, byte[]> contentDigests = new ArrayMap<>();
List<X509Certificate[]> signerCerts = new ArrayList<>(); List<X509Certificate[]> signerCerts = new ArrayList<>();
CertificateFactory certFactory; CertificateFactory certFactory;
try { try {
@@ -228,7 +213,7 @@ public class ApkSignatureSchemeV2Verifier {
} }
ByteBuffer signers; ByteBuffer signers;
try { try {
signers = getLengthPrefixedSlice(v2Block); signers = getLengthPrefixedSlice(signatureInfo.signatureBlock);
} catch (IOException e) { } catch (IOException e) {
throw new SecurityException("Failed to read list of signers", e); throw new SecurityException("Failed to read list of signers", e);
} }
@@ -255,10 +240,11 @@ public class ApkSignatureSchemeV2Verifier {
verifyIntegrity( verifyIntegrity(
contentDigests, contentDigests,
apkContents, apkFileDescriptor,
apkSigningBlockOffset, signatureInfo.apkSigningBlockOffset,
centralDirOffset, signatureInfo.centralDirOffset,
eocdOffset); signatureInfo.eocdOffset,
signatureInfo.eocd);
return signerCerts.toArray(new X509Certificate[signerCerts.size()][]); return signerCerts.toArray(new X509Certificate[signerCerts.size()][]);
} }
@@ -401,25 +387,38 @@ public class ApkSignatureSchemeV2Verifier {
private static void verifyIntegrity( private static void verifyIntegrity(
Map<Integer, byte[]> expectedDigests, Map<Integer, byte[]> expectedDigests,
ByteBuffer apkContents, FileDescriptor apkFileDescriptor,
int apkSigningBlockOffset, long apkSigningBlockOffset,
int centralDirOffset, long centralDirOffset,
int eocdOffset) throws SecurityException { long eocdOffset,
ByteBuffer eocdBuf) throws SecurityException {
if (expectedDigests.isEmpty()) { if (expectedDigests.isEmpty()) {
throw new SecurityException("No digests provided"); throw new SecurityException("No digests provided");
} }
ByteBuffer beforeApkSigningBlock = sliceFromTo(apkContents, 0, apkSigningBlockOffset); // We need to verify the integrity of the following three sections of the file:
ByteBuffer centralDir = sliceFromTo(apkContents, centralDirOffset, eocdOffset); // 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 // 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. // Central Directory must be considered to point to the offset of the APK Signing Block.
byte[] eocdBytes = new byte[apkContents.capacity() - eocdOffset]; eocdBuf = eocdBuf.duplicate();
apkContents.position(eocdOffset); eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
apkContents.get(eocdBytes); ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, apkSigningBlockOffset);
ByteBuffer eocd = ByteBuffer.wrap(eocdBytes); DataSource eocd = new ByteBufferDataSource(eocdBuf);
eocd.order(apkContents.order());
ZipUtils.setZipEocdCentralDirectoryOffset(eocd, apkSigningBlockOffset);
int[] digestAlgorithms = new int[expectedDigests.size()]; int[] digestAlgorithms = new int[expectedDigests.size()];
int digestAlgorithmCount = 0; int digestAlgorithmCount = 0;
@@ -427,30 +426,30 @@ public class ApkSignatureSchemeV2Verifier {
digestAlgorithms[digestAlgorithmCount] = digestAlgorithm; digestAlgorithms[digestAlgorithmCount] = digestAlgorithm;
digestAlgorithmCount++; digestAlgorithmCount++;
} }
Map<Integer, byte[]> actualDigests; byte[][] actualDigests;
try { try {
actualDigests = actualDigests =
computeContentDigests( computeContentDigests(
digestAlgorithms, digestAlgorithms,
new ByteBuffer[] {beforeApkSigningBlock, centralDir, eocd}); new DataSource[] {beforeApkSigningBlock, centralDir, eocd});
} catch (DigestException e) { } catch (DigestException e) {
throw new SecurityException("Failed to compute digest(s) of contents", e); throw new SecurityException("Failed to compute digest(s) of contents", e);
} }
for (Map.Entry<Integer, byte[]> entry : expectedDigests.entrySet()) { for (int i = 0; i < digestAlgorithms.length; i++) {
int digestAlgorithm = entry.getKey(); int digestAlgorithm = digestAlgorithms[i];
byte[] expectedDigest = entry.getValue(); byte[] expectedDigest = expectedDigests.get(digestAlgorithm);
byte[] actualDigest = actualDigests.get(digestAlgorithm); byte[] actualDigest = actualDigests[i];
if (!MessageDigest.isEqual(expectedDigest, actualDigest)) { if (!MessageDigest.isEqual(expectedDigest, actualDigest)) {
throw new SecurityException( throw new SecurityException(
getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm) getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm)
+ " digest of contents did not verify"); + " digest of contents did not verify");
} }
} }
} }
private static Map<Integer, byte[]> computeContentDigests( private static byte[][] computeContentDigests(
int[] digestAlgorithms, int[] digestAlgorithms,
ByteBuffer[] contents) throws DigestException { DataSource[] contents) throws DigestException {
// For each digest algorithm the result is computed as follows: // 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. // 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. // The final chunk will be shorter iff the length of segment is not a multiple of 1 MB.
@@ -461,13 +460,18 @@ public class ApkSignatureSchemeV2Verifier {
// chunks (uint32 little-endian) and the concatenation of digests of chunks of all // chunks (uint32 little-endian) and the concatenation of digests of chunks of all
// segments in-order. // segments in-order.
int totalChunkCount = 0; long totalChunkCountLong = 0;
for (ByteBuffer input : contents) { for (DataSource input : contents) {
totalChunkCount += getChunkCount(input.remaining()); totalChunkCountLong += getChunkCount(input.size());
} }
if (totalChunkCountLong >= Integer.MAX_VALUE / 1024) {
throw new DigestException("Too many chunks: " + totalChunkCountLong);
}
int totalChunkCount = (int) totalChunkCountLong;
Map<Integer, byte[]> digestsOfChunks = new HashMap<>(totalChunkCount); byte[][] digestsOfChunks = new byte[digestAlgorithms.length][];
for (int digestAlgorithm : digestAlgorithms) { for (int i = 0; i < digestAlgorithms.length; i++) {
int digestAlgorithm = digestAlgorithms[i];
int digestOutputSizeBytes = getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm); int digestOutputSizeBytes = getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm);
byte[] concatenationOfChunkCountAndChunkDigests = byte[] concatenationOfChunkCountAndChunkDigests =
new byte[5 + totalChunkCount * digestOutputSizeBytes]; new byte[5 + totalChunkCount * digestOutputSizeBytes];
@@ -476,49 +480,71 @@ public class ApkSignatureSchemeV2Verifier {
totalChunkCount, totalChunkCount,
concatenationOfChunkCountAndChunkDigests, concatenationOfChunkCountAndChunkDigests,
1); 1);
digestsOfChunks.put(digestAlgorithm, concatenationOfChunkCountAndChunkDigests); digestsOfChunks[i] = concatenationOfChunkCountAndChunkDigests;
} }
byte[] chunkContentPrefix = new byte[5]; byte[] chunkContentPrefix = new byte[5];
chunkContentPrefix[0] = (byte) 0xa5; chunkContentPrefix[0] = (byte) 0xa5;
int chunkIndex = 0; int chunkIndex = 0;
for (ByteBuffer input : contents) { MessageDigest[] mds = new MessageDigest[digestAlgorithms.length];
while (input.hasRemaining()) { for (int i = 0; i < digestAlgorithms.length; i++) {
int chunkSize = Math.min(input.remaining(), CHUNK_SIZE_BYTES); String jcaAlgorithmName =
ByteBuffer chunk = getByteBuffer(input, chunkSize); getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithms[i]);
for (int digestAlgorithm : digestAlgorithms) { try {
String jcaAlgorithmName = mds[i] = MessageDigest.getInstance(jcaAlgorithmName);
getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm); } catch (NoSuchAlgorithmException e) {
MessageDigest md; throw new RuntimeException(jcaAlgorithmName + " digest not supported", e);
try { }
md = MessageDigest.getInstance(jcaAlgorithmName); }
} catch (NoSuchAlgorithmException e) { // TODO: Compute digests of chunks in parallel when beneficial. This requires some research
throw new RuntimeException(jcaAlgorithmName + " digest not supported", e); // 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.
chunk.clear(); int dataSourceIndex = 0;
setUnsignedInt32LittleEndian(chunk.remaining(), chunkContentPrefix, 1); for (DataSource input : contents) {
md.update(chunkContentPrefix); long inputOffset = 0;
md.update(chunk); long inputRemaining = input.size();
byte[] concatenationOfChunkCountAndChunkDigests = while (inputRemaining > 0) {
digestsOfChunks.get(digestAlgorithm); 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 = int expectedDigestSizeBytes =
getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm); getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm);
int actualDigestSizeBytes = md.digest(concatenationOfChunkCountAndChunkDigests, MessageDigest md = mds[i];
5 + chunkIndex * expectedDigestSizeBytes, expectedDigestSizeBytes); int actualDigestSizeBytes =
md.digest(
concatenationOfChunkCountAndChunkDigests,
5 + chunkIndex * expectedDigestSizeBytes,
expectedDigestSizeBytes);
if (actualDigestSizeBytes != expectedDigestSizeBytes) { if (actualDigestSizeBytes != expectedDigestSizeBytes) {
throw new RuntimeException( throw new RuntimeException(
"Unexpected output size of " + md.getAlgorithm() + " digest: " "Unexpected output size of " + md.getAlgorithm() + " digest: "
+ actualDigestSizeBytes); + actualDigestSizeBytes);
} }
} }
inputOffset += chunkSize;
inputRemaining -= chunkSize;
chunkIndex++; chunkIndex++;
} }
dataSourceIndex++;
} }
Map<Integer, byte[]> result = new HashMap<>(digestAlgorithms.length); byte[][] result = new byte[digestAlgorithms.length][];
for (Map.Entry<Integer, byte[]> entry : digestsOfChunks.entrySet()) { for (int i = 0; i < digestAlgorithms.length; i++) {
int digestAlgorithm = entry.getKey(); int digestAlgorithm = digestAlgorithms[i];
byte[] input = entry.getValue(); byte[] input = digestsOfChunks[i];
String jcaAlgorithmName = getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm); String jcaAlgorithmName = getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm);
MessageDigest md; MessageDigest md;
try { try {
@@ -527,49 +553,47 @@ public class ApkSignatureSchemeV2Verifier {
throw new RuntimeException(jcaAlgorithmName + " digest not supported", e); throw new RuntimeException(jcaAlgorithmName + " digest not supported", e);
} }
byte[] output = md.digest(input); byte[] output = md.digest(input);
result.put(digestAlgorithm, output); result[i] = output;
} }
return result; return result;
} }
/** /**
* Finds the offset of ZIP End of Central Directory (EoCD). * Returns the ZIP End of Central Directory (EoCD) and its offset in the file.
* *
* @throws SignatureNotFoundException If the EoCD could not be found * @throws IOException if an I/O error occurs while reading the file.
* @throws SignatureNotFoundException if the EoCD could not be found.
*/ */
private static int getEocdOffset(ByteBuffer apkContents) throws SignatureNotFoundException { private static Pair<ByteBuffer, Long> getEocd(RandomAccessFile apk)
int eocdOffset = ZipUtils.findZipEndOfCentralDirectoryRecord(apkContents); throws IOException, SignatureNotFoundException {
if (eocdOffset == -1) { Pair<ByteBuffer, Long> eocdAndOffsetInFile =
ZipUtils.findZipEndOfCentralDirectoryRecord(apk);
if (eocdAndOffsetInFile == null) {
throw new SignatureNotFoundException( throw new SignatureNotFoundException(
"Not an APK file: ZIP End of Central Directory record not found"); "Not an APK file: ZIP End of Central Directory record not found");
} }
return eocdOffset; return eocdAndOffsetInFile;
} }
private static long getCentralDirOffset(ByteBuffer apkContents, int eocdOffset) private static long getCentralDirOffset(ByteBuffer eocd, long eocdOffset)
throws SignatureNotFoundException { throws SignatureNotFoundException {
if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(apkContents, eocdOffset)) {
throw new SignatureNotFoundException("ZIP64 APK not supported");
}
ByteBuffer eocd = sliceFromTo(apkContents, eocdOffset, apkContents.capacity());
// Look up the offset of ZIP Central Directory. // Look up the offset of ZIP Central Directory.
long centralDirOffsetLong = ZipUtils.getZipEocdCentralDirectoryOffset(eocd); long centralDirOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocd);
if (centralDirOffsetLong >= eocdOffset) { if (centralDirOffset >= eocdOffset) {
throw new SignatureNotFoundException( throw new SignatureNotFoundException(
"ZIP Central Directory offset out of range: " + centralDirOffsetLong "ZIP Central Directory offset out of range: " + centralDirOffset
+ ". ZIP End of Central Directory offset: " + eocdOffset); + ". ZIP End of Central Directory offset: " + eocdOffset);
} }
long centralDirSizeLong = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocd); long centralDirSize = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocd);
if (centralDirOffsetLong + centralDirSizeLong != eocdOffset) { if (centralDirOffset + centralDirSize != eocdOffset) {
throw new SignatureNotFoundException( throw new SignatureNotFoundException(
"ZIP Central Directory is not immediately followed by End of Central" "ZIP Central Directory is not immediately followed by End of Central"
+ " Directory"); + " Directory");
} }
return centralDirOffsetLong; return centralDirOffset;
} }
private static final int getChunkCount(int inputSizeBytes) { private static final long getChunkCount(long inputSizeBytes) {
return (inputSizeBytes + CHUNK_SIZE_BYTES - 1) / CHUNK_SIZE_BYTES; return (inputSizeBytes + CHUNK_SIZE_BYTES - 1) / CHUNK_SIZE_BYTES;
} }
@@ -837,10 +861,9 @@ public class ApkSignatureSchemeV2Verifier {
private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a; private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
private static int findApkSigningBlock(ByteBuffer apkContents, int centralDirOffset) private static Pair<ByteBuffer, Long> findApkSigningBlock(
throws SignatureNotFoundException { RandomAccessFile apk, long centralDirOffset)
checkByteOrderLittleEndian(apkContents); throws IOException, SignatureNotFoundException {
// FORMAT: // FORMAT:
// OFFSET DATA TYPE DESCRIPTION // OFFSET DATA TYPE DESCRIPTION
// * @+0 bytes uint64: size in bytes (excluding this field) // * @+0 bytes uint64: size in bytes (excluding this field)
@@ -853,32 +876,42 @@ public class ApkSignatureSchemeV2Verifier {
"APK too small for APK Signing Block. ZIP Central Directory offset: " "APK too small for APK Signing Block. ZIP Central Directory offset: "
+ centralDirOffset); + centralDirOffset);
} }
// Check magic field present // Read the magic and offset in file from the footer section of the block:
if ((apkContents.getLong(centralDirOffset - 16) != APK_SIG_BLOCK_MAGIC_LO) // * uint64: size of block
|| (apkContents.getLong(centralDirOffset - 8) != APK_SIG_BLOCK_MAGIC_HI)) { // * 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( throw new SignatureNotFoundException(
"No APK Signing Block before ZIP Central Directory"); "No APK Signing Block before ZIP Central Directory");
} }
// Read and compare size fields // Read and compare size fields
long apkSigBlockSizeLong = apkContents.getLong(centralDirOffset - 24); long apkSigBlockSizeInFooter = footer.getLong(0);
if ((apkSigBlockSizeLong < 24) || (apkSigBlockSizeLong > Integer.MAX_VALUE - 8)) { if ((apkSigBlockSizeInFooter < footer.capacity())
|| (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) {
throw new SignatureNotFoundException( throw new SignatureNotFoundException(
"APK Signing Block size out of range: " + apkSigBlockSizeLong); "APK Signing Block size out of range: " + apkSigBlockSizeInFooter);
} }
int apkSigBlockSizeFromFooter = (int) apkSigBlockSizeLong; int totalSize = (int) (apkSigBlockSizeInFooter + 8);
int totalSize = apkSigBlockSizeFromFooter + 8; long apkSigBlockOffset = centralDirOffset - totalSize;
int apkSigBlockOffset = centralDirOffset - totalSize;
if (apkSigBlockOffset < 0) { if (apkSigBlockOffset < 0) {
throw new SignatureNotFoundException( throw new SignatureNotFoundException(
"APK Signing Block offset out of range: " + apkSigBlockOffset); "APK Signing Block offset out of range: " + apkSigBlockOffset);
} }
long apkSigBlockSizeFromHeader = apkContents.getLong(apkSigBlockOffset); ByteBuffer apkSigBlock = ByteBuffer.allocate(totalSize);
if (apkSigBlockSizeFromHeader != apkSigBlockSizeFromFooter) { 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( throw new SignatureNotFoundException(
"APK Signing Block sizes in header and footer do not match: " "APK Signing Block sizes in header and footer do not match: "
+ apkSigBlockSizeFromHeader + " vs " + apkSigBlockSizeFromFooter); + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter);
} }
return apkSigBlockOffset; return Pair.create(apkSigBlock, apkSigBlockOffset);
} }
private static ByteBuffer findApkSignatureSchemeV2Block(ByteBuffer apkSigningBlock) private static ByteBuffer findApkSignatureSchemeV2Block(ByteBuffer apkSigningBlock)
@@ -930,6 +963,8 @@ public class ApkSignatureSchemeV2Verifier {
} }
public static class SignatureNotFoundException extends Exception { public static class SignatureNotFoundException extends Exception {
private static final long serialVersionUID = 1L;
public SignatureNotFoundException(String message) { public SignatureNotFoundException(String message) {
super(message); super(message);
} }
@@ -939,6 +974,159 @@ public class ApkSignatureSchemeV2Verifier {
} }
} }
/**
* 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 * 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. * of letting the underlying implementation have a shot at re-encoding the data.

View File

@@ -16,13 +16,17 @@
package android.util.apk; package android.util.apk;
import android.util.Pair;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
/** /**
* Assorted ZIP format helpers. * Assorted ZIP format helpers.
* *
* <p>NOTE: Most helper methods operating on {@code ByteBuffer} instances except that the byte * <p>NOTE: Most helper methods operating on {@code ByteBuffer} instances expect that the byte
* order of these buffers is little-endian. * order of these buffers is little-endian.
*/ */
abstract class ZipUtils { abstract class ZipUtils {
@@ -35,9 +39,101 @@ abstract class ZipUtils {
private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20; private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20;
private static final int ZIP64_EOCD_LOCATOR_SIZE = 20; private static final int ZIP64_EOCD_LOCATOR_SIZE = 20;
private static final int ZIP64_EOCD_LOCATOR_SIG = 0x07064b50; private static final int ZIP64_EOCD_LOCATOR_SIG_REVERSE_BYTE_ORDER = 0x504b0607;
private static final int UINT32_MAX_VALUE = 0xffff; private static final int UINT16_MAX_VALUE = 0xffff;
/**
* Returns the ZIP End of Central Directory record of the provided ZIP file.
*
* @return contents of the ZIP End of Central Directory record and the record's offset in the
* file or {@code null} if the file does not contain the record.
*
* @throws IOException if an I/O error occurs while reading the file.
*/
static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(RandomAccessFile zip)
throws IOException {
// ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
// The record can be identified by its 4-byte signature/magic which is located at the very
// beginning of the record. A complication is that the record is variable-length because of
// the comment field.
// The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
// end of the buffer for the EOCD record signature. Whenever we find a signature, we check
// the candidate record's comment length is such that the remainder of the record takes up
// exactly the remaining bytes in the buffer. The search is bounded because the maximum
// size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
long fileSize = zip.length();
if (fileSize < ZIP_EOCD_REC_MIN_SIZE) {
return null;
}
// Optimization: 99.99% of APKs have a zero-length comment field in the EoCD record and thus
// the EoCD record offset is known in advance. Try that offset first to avoid unnecessarily
// reading more data.
Pair<ByteBuffer, Long> result = findZipEndOfCentralDirectoryRecord(zip, 0);
if (result != null) {
return result;
}
// EoCD does not start where we expected it to. Perhaps it contains a non-empty comment
// field. Expand the search. The maximum size of the comment field in EoCD is 65535 because
// the comment length field is an unsigned 16-bit number.
return findZipEndOfCentralDirectoryRecord(zip, UINT16_MAX_VALUE);
}
/**
* Returns the ZIP End of Central Directory record of the provided ZIP file.
*
* @param maxCommentSize maximum accepted size (in bytes) of EoCD comment field. The permitted
* value is from 0 to 65535 inclusive. The smaller the value, the faster this method
* locates the record, provided its comment field is no longer than this value.
*
* @return contents of the ZIP End of Central Directory record and the record's offset in the
* file or {@code null} if the file does not contain the record.
*
* @throws IOException if an I/O error occurs while reading the file.
*/
private static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(
RandomAccessFile zip, int maxCommentSize) throws IOException {
// ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
// The record can be identified by its 4-byte signature/magic which is located at the very
// beginning of the record. A complication is that the record is variable-length because of
// the comment field.
// The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
// end of the buffer for the EOCD record signature. Whenever we find a signature, we check
// the candidate record's comment length is such that the remainder of the record takes up
// exactly the remaining bytes in the buffer. The search is bounded because the maximum
// size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
if ((maxCommentSize < 0) || (maxCommentSize > UINT16_MAX_VALUE)) {
throw new IllegalArgumentException("maxCommentSize: " + maxCommentSize);
}
long fileSize = zip.length();
if (fileSize < ZIP_EOCD_REC_MIN_SIZE) {
// No space for EoCD record in the file.
return null;
}
// Lower maxCommentSize if the file is too small.
maxCommentSize = (int) Math.min(maxCommentSize, fileSize - ZIP_EOCD_REC_MIN_SIZE);
ByteBuffer buf = ByteBuffer.allocate(ZIP_EOCD_REC_MIN_SIZE + maxCommentSize);
buf.order(ByteOrder.LITTLE_ENDIAN);
long bufOffsetInFile = fileSize - buf.capacity();
zip.seek(bufOffsetInFile);
zip.readFully(buf.array(), buf.arrayOffset(), buf.capacity());
int eocdOffsetInBuf = findZipEndOfCentralDirectoryRecord(buf);
if (eocdOffsetInBuf == -1) {
// No EoCD record found in the buffer
return null;
}
// EoCD found
buf.position(eocdOffsetInBuf);
ByteBuffer eocd = buf.slice();
eocd.order(ByteOrder.LITTLE_ENDIAN);
return Pair.create(eocd, bufOffsetInFile + eocdOffsetInBuf);
}
/** /**
* Returns the position at which ZIP End of Central Directory record starts in the provided * Returns the position at which ZIP End of Central Directory record starts in the provided
@@ -45,7 +141,7 @@ abstract class ZipUtils {
* *
* <p>NOTE: Byte order of {@code zipContents} must be little-endian. * <p>NOTE: Byte order of {@code zipContents} must be little-endian.
*/ */
public static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) { private static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) {
assertByteOrderLittleEndian(zipContents); assertByteOrderLittleEndian(zipContents);
// ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
@@ -56,14 +152,13 @@ abstract class ZipUtils {
// end of the buffer for the EOCD record signature. Whenever we find a signature, we check // end of the buffer for the EOCD record signature. Whenever we find a signature, we check
// the candidate record's comment length is such that the remainder of the record takes up // the candidate record's comment length is such that the remainder of the record takes up
// exactly the remaining bytes in the buffer. The search is bounded because the maximum // exactly the remaining bytes in the buffer. The search is bounded because the maximum
// size of the comment field is 65535 bytes because the field is an unsigned 32-bit number. // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
int archiveSize = zipContents.capacity(); int archiveSize = zipContents.capacity();
if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) { if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) {
System.out.println("File size smaller than EOCD min size");
return -1; return -1;
} }
int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT32_MAX_VALUE); int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE);
int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE; int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE;
for (int expectedCommentLength = 0; expectedCommentLength < maxCommentLength; for (int expectedCommentLength = 0; expectedCommentLength < maxCommentLength;
expectedCommentLength++) { expectedCommentLength++) {
@@ -82,24 +177,28 @@ abstract class ZipUtils {
} }
/** /**
* Returns {@code true} if the provided buffer contains a ZIP64 End of Central Directory * Returns {@code true} if the provided file contains a ZIP64 End of Central Directory
* Locator. * Locator.
* *
* <p>NOTE: Byte order of {@code zipContents} must be little-endian. * @param zipEndOfCentralDirectoryPosition offset of the ZIP End of Central Directory record
* in the file.
*
* @throws IOException if an I/O error occurs while reading the file.
*/ */
public static final boolean isZip64EndOfCentralDirectoryLocatorPresent( public static final boolean isZip64EndOfCentralDirectoryLocatorPresent(
ByteBuffer zipContents, int zipEndOfCentralDirectoryPosition) { RandomAccessFile zip, long zipEndOfCentralDirectoryPosition) throws IOException {
assertByteOrderLittleEndian(zipContents);
// ZIP64 End of Central Directory Locator immediately precedes the ZIP End of Central // ZIP64 End of Central Directory Locator immediately precedes the ZIP End of Central
// Directory Record. // Directory Record.
long locatorPosition = zipEndOfCentralDirectoryPosition - ZIP64_EOCD_LOCATOR_SIZE;
int locatorPosition = zipEndOfCentralDirectoryPosition - ZIP64_EOCD_LOCATOR_SIZE;
if (locatorPosition < 0) { if (locatorPosition < 0) {
return false; return false;
} }
return zipContents.getInt(locatorPosition) == ZIP64_EOCD_LOCATOR_SIG; zip.seek(locatorPosition);
// RandomAccessFile.readInt assumes big-endian byte order, but ZIP format uses
// little-endian.
return zip.readInt() == ZIP64_EOCD_LOCATOR_SIG_REVERSE_BYTE_ORDER;
} }
/** /**