APK digest API and initial implementation.

Bug: 160605420
Test: atest ChecksumsTest

Change-Id: I08ef0b131c44313f2c6acddad00dfa03598cc1ff
This commit is contained in:
Alex Buynytskyy
2020-08-06 15:16:38 -07:00
parent 5be7e28816
commit ec6f3e189c
17 changed files with 1076 additions and 61 deletions

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;
}
}
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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,