APK digest API and initial implementation.
Bug: 160605420 Test: atest ChecksumsTest Change-Id: I08ef0b131c44313f2c6acddad00dfa03598cc1ff
This commit is contained in:
@@ -149,7 +149,7 @@ public class ApkSignatureSchemeV2Verifier {
|
||||
* @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)
|
||||
public static SignatureInfo findSignature(RandomAccessFile apk)
|
||||
throws IOException, SignatureNotFoundException {
|
||||
return ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ public class ApkSignatureSchemeV3Verifier {
|
||||
* @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v3.
|
||||
* @throws IOException if an I/O error occurs while reading the APK file.
|
||||
*/
|
||||
private static SignatureInfo findSignature(RandomAccessFile apk)
|
||||
public static SignatureInfo findSignature(RandomAccessFile apk)
|
||||
throws IOException, SignatureNotFoundException {
|
||||
return ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
|
||||
}
|
||||
|
||||
@@ -92,6 +92,20 @@ public class ApkSignatureVerifier {
|
||||
private static PackageParser.SigningDetails verifySignatures(String apkPath,
|
||||
@SignatureSchemeVersion int minSignatureSchemeVersion, boolean verifyFull)
|
||||
throws PackageParserException {
|
||||
return verifySignaturesInternal(apkPath, minSignatureSchemeVersion,
|
||||
verifyFull).signingDetails;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the provided APK using all allowed signing schemas.
|
||||
* @return the certificates associated with each signer and content digests.
|
||||
* @param verifyFull whether to verify all contents of this APK or just collect certificates.
|
||||
* @throws PackageParserException if there was a problem collecting certificates
|
||||
* @hide
|
||||
*/
|
||||
public static SigningDetailsWithDigests verifySignaturesInternal(String apkPath,
|
||||
@SignatureSchemeVersion int minSignatureSchemeVersion, boolean verifyFull)
|
||||
throws PackageParserException {
|
||||
|
||||
if (minSignatureSchemeVersion > SignatureSchemeVersion.SIGNING_BLOCK_V4) {
|
||||
// V3 and before are older than the requested minimum signing version
|
||||
@@ -121,7 +135,7 @@ public class ApkSignatureVerifier {
|
||||
return verifyV3AndBelowSignatures(apkPath, minSignatureSchemeVersion, verifyFull);
|
||||
}
|
||||
|
||||
private static PackageParser.SigningDetails verifyV3AndBelowSignatures(String apkPath,
|
||||
private static SigningDetailsWithDigests verifyV3AndBelowSignatures(String apkPath,
|
||||
@SignatureSchemeVersion int minSignatureSchemeVersion, boolean verifyFull)
|
||||
throws PackageParserException {
|
||||
// try v3
|
||||
@@ -174,7 +188,7 @@ public class ApkSignatureVerifier {
|
||||
* @throws SignatureNotFoundException if there are no V4 signatures in the APK
|
||||
* @throws PackageParserException if there was a problem collecting certificates
|
||||
*/
|
||||
private static PackageParser.SigningDetails verifyV4Signature(String apkPath,
|
||||
private static SigningDetailsWithDigests verifyV4Signature(String apkPath,
|
||||
@SignatureSchemeVersion int minSignatureSchemeVersion, boolean verifyFull)
|
||||
throws SignatureNotFoundException, PackageParserException {
|
||||
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, verifyFull ? "verifyV4" : "certsOnlyV4");
|
||||
@@ -234,8 +248,8 @@ public class ApkSignatureVerifier {
|
||||
}
|
||||
}
|
||||
|
||||
return new PackageParser.SigningDetails(signerSigs,
|
||||
SignatureSchemeVersion.SIGNING_BLOCK_V4);
|
||||
return new SigningDetailsWithDigests(new PackageParser.SigningDetails(signerSigs,
|
||||
SignatureSchemeVersion.SIGNING_BLOCK_V4), vSigner.contentDigests);
|
||||
} catch (SignatureNotFoundException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
@@ -256,8 +270,8 @@ public class ApkSignatureVerifier {
|
||||
* @throws SignatureNotFoundException if there are no V3 signatures in the APK
|
||||
* @throws PackageParserException if there was a problem collecting certificates
|
||||
*/
|
||||
private static PackageParser.SigningDetails verifyV3Signature(String apkPath,
|
||||
boolean verifyFull) throws SignatureNotFoundException, PackageParserException {
|
||||
private static SigningDetailsWithDigests verifyV3Signature(String apkPath, boolean verifyFull)
|
||||
throws SignatureNotFoundException, PackageParserException {
|
||||
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, verifyFull ? "verifyV3" : "certsOnlyV3");
|
||||
try {
|
||||
ApkSignatureSchemeV3Verifier.VerifiedSigner vSigner =
|
||||
@@ -275,8 +289,9 @@ public class ApkSignatureVerifier {
|
||||
pastSignerSigs[i].setFlags(vSigner.por.flagsList.get(i));
|
||||
}
|
||||
}
|
||||
return new PackageParser.SigningDetails(signerSigs,
|
||||
SignatureSchemeVersion.SIGNING_BLOCK_V3, pastSignerSigs);
|
||||
return new SigningDetailsWithDigests(new PackageParser.SigningDetails(signerSigs,
|
||||
SignatureSchemeVersion.SIGNING_BLOCK_V3, pastSignerSigs),
|
||||
vSigner.contentDigests);
|
||||
} catch (SignatureNotFoundException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
@@ -297,15 +312,16 @@ public class ApkSignatureVerifier {
|
||||
* @throws SignatureNotFoundException if there are no V2 signatures in the APK
|
||||
* @throws PackageParserException if there was a problem collecting certificates
|
||||
*/
|
||||
private static PackageParser.SigningDetails verifyV2Signature(String apkPath,
|
||||
boolean verifyFull) throws SignatureNotFoundException, PackageParserException {
|
||||
private static SigningDetailsWithDigests verifyV2Signature(String apkPath, boolean verifyFull)
|
||||
throws SignatureNotFoundException, PackageParserException {
|
||||
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, verifyFull ? "verifyV2" : "certsOnlyV2");
|
||||
try {
|
||||
Certificate[][] signerCerts = verifyFull ? ApkSignatureSchemeV2Verifier.verify(apkPath)
|
||||
: ApkSignatureSchemeV2Verifier.unsafeGetCertsWithoutVerification(apkPath);
|
||||
ApkSignatureSchemeV2Verifier.VerifiedSigner vSigner =
|
||||
ApkSignatureSchemeV2Verifier.verify(apkPath, verifyFull);
|
||||
Certificate[][] signerCerts = vSigner.certs;
|
||||
Signature[] signerSigs = convertToSignatures(signerCerts);
|
||||
return new PackageParser.SigningDetails(signerSigs,
|
||||
SignatureSchemeVersion.SIGNING_BLOCK_V2);
|
||||
return new SigningDetailsWithDigests(new PackageParser.SigningDetails(signerSigs,
|
||||
SignatureSchemeVersion.SIGNING_BLOCK_V2), vSigner.contentDigests);
|
||||
} catch (SignatureNotFoundException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
@@ -324,8 +340,7 @@ public class ApkSignatureVerifier {
|
||||
* @param verifyFull whether to verify all contents of this APK or just collect certificates.
|
||||
* @throws PackageParserException if there was a problem collecting certificates
|
||||
*/
|
||||
private static PackageParser.SigningDetails verifyV1Signature(
|
||||
String apkPath, boolean verifyFull)
|
||||
private static SigningDetailsWithDigests verifyV1Signature(String apkPath, boolean verifyFull)
|
||||
throws PackageParserException {
|
||||
StrictJarFile jarFile = null;
|
||||
|
||||
@@ -391,7 +406,8 @@ public class ApkSignatureVerifier {
|
||||
}
|
||||
}
|
||||
}
|
||||
return new PackageParser.SigningDetails(lastSigs, SignatureSchemeVersion.JAR);
|
||||
return new SigningDetailsWithDigests(
|
||||
new PackageParser.SigningDetails(lastSigs, SignatureSchemeVersion.JAR), null);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new PackageParserException(INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING,
|
||||
"Failed to collect certificates from " + apkPath, e);
|
||||
@@ -542,4 +558,27 @@ public class ApkSignatureVerifier {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended signing details.
|
||||
* @hide for internal use only.
|
||||
*/
|
||||
public static class SigningDetailsWithDigests {
|
||||
public final PackageParser.SigningDetails signingDetails;
|
||||
|
||||
/**
|
||||
* APK Signature Schemes v2/v3/v4 might contain multiple content digests.
|
||||
* SignatureVerifier usually chooses one of them to verify.
|
||||
* For certain signature schemes, e.g. v4, this digest is verified continuously.
|
||||
* For others, e.g. v2, the caller has to specify if they want to verify.
|
||||
* Please refer to documentation for more details.
|
||||
*/
|
||||
public final Map<Integer, byte[]> contentDigests;
|
||||
|
||||
SigningDetailsWithDigests(PackageParser.SigningDetails signingDetails,
|
||||
Map<Integer, byte[]> contentDigests) {
|
||||
this.signingDetails = signingDetails;
|
||||
this.contentDigests = contentDigests;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ import java.util.Map;
|
||||
*
|
||||
* @hide for internal use only.
|
||||
*/
|
||||
final class ApkSigningBlockUtils {
|
||||
public final class ApkSigningBlockUtils {
|
||||
|
||||
private ApkSigningBlockUtils() {
|
||||
}
|
||||
@@ -146,6 +146,37 @@ final class ApkSigningBlockUtils {
|
||||
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.
|
||||
@@ -156,6 +187,7 @@ final class ApkSigningBlockUtils {
|
||||
// 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,
|
||||
signatureInfo.apkSigningBlockOffset);
|
||||
@@ -171,31 +203,8 @@ final class ApkSigningBlockUtils {
|
||||
ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, signatureInfo.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 =
|
||||
computeContentDigestsPer1MbChunk(
|
||||
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");
|
||||
}
|
||||
}
|
||||
return computeContentDigestsPer1MbChunk(digestAlgorithms,
|
||||
new DataSource[]{beforeApkSigningBlock, centralDir, eocd});
|
||||
}
|
||||
|
||||
private static byte[][] computeContentDigestsPer1MbChunk(
|
||||
@@ -417,14 +426,10 @@ final class ApkSigningBlockUtils {
|
||||
static final int SIGNATURE_VERITY_ECDSA_WITH_SHA256 = 0x0423;
|
||||
static final int SIGNATURE_VERITY_DSA_WITH_SHA256 = 0x0425;
|
||||
|
||||
static final int CONTENT_DIGEST_CHUNKED_SHA256 = 1;
|
||||
static final int CONTENT_DIGEST_CHUNKED_SHA512 = 2;
|
||||
static final int CONTENT_DIGEST_VERITY_CHUNKED_SHA256 = 3;
|
||||
static final int CONTENT_DIGEST_SHA256 = 4;
|
||||
|
||||
private static final int[] V4_CONTENT_DIGEST_ALGORITHMS =
|
||||
{CONTENT_DIGEST_CHUNKED_SHA512, CONTENT_DIGEST_VERITY_CHUNKED_SHA256,
|
||||
CONTENT_DIGEST_CHUNKED_SHA256};
|
||||
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);
|
||||
|
||||
@@ -16,15 +16,18 @@
|
||||
|
||||
package android.util.apk;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* APK Signature Scheme v2 block and additional information relevant to verifying the signatures
|
||||
* contained in the block against the file.
|
||||
* @hide
|
||||
*/
|
||||
class SignatureInfo {
|
||||
public class SignatureInfo {
|
||||
/** Contents of APK Signature Scheme v2 block. */
|
||||
public final ByteBuffer signatureBlock;
|
||||
public final @NonNull ByteBuffer signatureBlock;
|
||||
|
||||
/** Position of the APK Signing Block in the file. */
|
||||
public final long apkSigningBlockOffset;
|
||||
@@ -36,10 +39,10 @@ class SignatureInfo {
|
||||
public final long eocdOffset;
|
||||
|
||||
/** Contents of ZIP End of Central Directory (EoCD) of the file. */
|
||||
public final ByteBuffer eocd;
|
||||
public final @NonNull ByteBuffer eocd;
|
||||
|
||||
SignatureInfo(ByteBuffer signatureBlock, long apkSigningBlockOffset, long centralDirOffset,
|
||||
long eocdOffset, ByteBuffer eocd) {
|
||||
SignatureInfo(@NonNull ByteBuffer signatureBlock, long apkSigningBlockOffset,
|
||||
long centralDirOffset, long eocdOffset, @NonNull ByteBuffer eocd) {
|
||||
this.signatureBlock = signatureBlock;
|
||||
this.apkSigningBlockOffset = apkSigningBlockOffset;
|
||||
this.centralDirOffset = centralDirOffset;
|
||||
|
||||
@@ -115,6 +115,34 @@ public abstract class VerityBuilder {
|
||||
signingBlockSize, signatureInfo.eocdOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the fs-verity hash tree. It is the actual verity tree format on disk, as is
|
||||
* re-generated on device.
|
||||
*
|
||||
* The tree is built bottom up. The bottom level has 256-bit digest for each 4 KB block in the
|
||||
* input file. If the total size is larger than 4 KB, take this level as input and repeat the
|
||||
* same procedure, until the level is within 4 KB. If salt is given, it will apply to each
|
||||
* digestion before the actual data.
|
||||
*
|
||||
* The returned root hash is calculated from the last level of 4 KB chunk, similarly with salt.
|
||||
*
|
||||
* @return the root hash of the generated hash tree.
|
||||
*/
|
||||
public static byte[] generateFsVerityRootHash(@NonNull String apkPath, byte[] salt,
|
||||
@NonNull ByteBufferFactory bufferFactory)
|
||||
throws IOException, NoSuchAlgorithmException, DigestException {
|
||||
try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {
|
||||
int[] levelOffset = calculateVerityLevelOffset(apk.length());
|
||||
int merkleTreeSize = levelOffset[levelOffset.length - 1];
|
||||
|
||||
ByteBuffer output = bufferFactory.create(
|
||||
merkleTreeSize
|
||||
+ CHUNK_SIZE_BYTES); // maximum size of apk-verity metadata
|
||||
output.order(ByteOrder.LITTLE_ENDIAN);
|
||||
ByteBuffer tree = slice(output, 0, merkleTreeSize);
|
||||
return generateFsVerityTreeInternal(apk, salt, levelOffset, tree);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Calculates the apk-verity root hash for integrity measurement. This needs to be consistent
|
||||
* to what kernel returns.
|
||||
@@ -259,9 +287,10 @@ public abstract class VerityBuilder {
|
||||
// thus the syscall overhead is not too big.
|
||||
private static final int MMAP_REGION_SIZE_BYTES = 1024 * 1024;
|
||||
|
||||
private static void generateFsVerityDigestAtLeafLevel(RandomAccessFile file, ByteBuffer output)
|
||||
private static void generateFsVerityDigestAtLeafLevel(RandomAccessFile file,
|
||||
@Nullable byte[] salt, ByteBuffer output)
|
||||
throws IOException, NoSuchAlgorithmException, DigestException {
|
||||
BufferedDigester digester = new BufferedDigester(null /* salt */, output);
|
||||
BufferedDigester digester = new BufferedDigester(salt, output);
|
||||
|
||||
// 1. Digest the whole file by chunks.
|
||||
consumeByChunk(digester,
|
||||
@@ -324,6 +353,35 @@ public abstract class VerityBuilder {
|
||||
digester.fillUpLastOutputChunk();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static byte[] generateFsVerityTreeInternal(@NonNull RandomAccessFile apk,
|
||||
@Nullable byte[] salt, @NonNull int[] levelOffset, @NonNull ByteBuffer output)
|
||||
throws IOException, NoSuchAlgorithmException, DigestException {
|
||||
// 1. Digest the apk to generate the leaf level hashes.
|
||||
generateFsVerityDigestAtLeafLevel(apk, salt,
|
||||
slice(output, levelOffset[levelOffset.length - 2],
|
||||
levelOffset[levelOffset.length - 1]));
|
||||
|
||||
// 2. Digest the lower level hashes bottom up.
|
||||
for (int level = levelOffset.length - 3; level >= 0; level--) {
|
||||
ByteBuffer inputBuffer = slice(output, levelOffset[level + 1], levelOffset[level + 2]);
|
||||
ByteBuffer outputBuffer = slice(output, levelOffset[level], levelOffset[level + 1]);
|
||||
|
||||
DataSource source = new ByteBufferDataSource(inputBuffer);
|
||||
BufferedDigester digester = new BufferedDigester(salt, outputBuffer);
|
||||
consumeByChunk(digester, source, CHUNK_SIZE_BYTES);
|
||||
digester.assertEmptyBuffer();
|
||||
digester.fillUpLastOutputChunk();
|
||||
}
|
||||
|
||||
// 3. Digest the first block (i.e. first level) to generate the root hash.
|
||||
byte[] rootHash = new byte[DIGEST_SIZE_BYTES];
|
||||
BufferedDigester digester = new BufferedDigester(salt, ByteBuffer.wrap(rootHash));
|
||||
digester.consume(slice(output, 0, CHUNK_SIZE_BYTES));
|
||||
digester.assertEmptyBuffer();
|
||||
return rootHash;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static byte[] generateVerityTreeInternal(@NonNull RandomAccessFile apk,
|
||||
@Nullable SignatureInfo signatureInfo, @Nullable byte[] salt,
|
||||
|
||||
Reference in New Issue
Block a user