Merge "Refactor PackageParser.collectCerts() to hide signature scheme."

This commit is contained in:
TreeHugger Robot
2017-12-14 00:04:18 +00:00
committed by Android (Google) Code Review
4 changed files with 333 additions and 207 deletions

View File

@@ -32,7 +32,6 @@ import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_ACTIVITIES_RESIZE_
import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_UNRESIZEABLE;
import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST;
import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME;
import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING;
import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES;
import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_NOT_APK;
@@ -87,7 +86,8 @@ import android.util.Slog;
import android.util.SparseArray;
import android.util.TypedValue;
import android.util.apk.ApkSignatureSchemeV2Verifier;
import android.util.jar.StrictJarFile;
import android.util.apk.ApkSignatureVerifier;
import android.util.apk.SignatureNotFoundException;
import android.view.Gravity;
import com.android.internal.R;
@@ -106,12 +106,10 @@ import java.io.File;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Constructor;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
@@ -129,8 +127,6 @@ import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.zip.ZipEntry;
/**
* Parser for package files (APKs) on disk. This supports apps packaged either
@@ -173,7 +169,7 @@ public class PackageParser {
// TODO: refactor "codePath" to "apkPath"
/** File name in an APK for the Android manifest. */
private static final String ANDROID_MANIFEST_FILENAME = "AndroidManifest.xml";
public static final String ANDROID_MANIFEST_FILENAME = "AndroidManifest.xml";
/** Path prefix for apps on expanded storage */
private static final String MNT_EXPAND = "/mnt/expand/";
@@ -821,23 +817,6 @@ public class PackageParser {
return pi;
}
private static Certificate[][] loadCertificates(StrictJarFile jarFile, ZipEntry entry)
throws PackageParserException {
InputStream is = null;
try {
// We must read the stream for the JarEntry to retrieve
// its certificates.
is = jarFile.getInputStream(entry);
readFullyIgnoringContents(is);
return jarFile.getCertificateChains(entry);
} catch (IOException | RuntimeException e) {
throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,
"Failed reading " + entry.getName() + " in " + jarFile, e);
} finally {
IoUtils.closeQuietly(is);
}
}
public static final int PARSE_MUST_BE_APK = 1 << 0;
public static final int PARSE_IGNORE_PROCESSES = 1 << 1;
public static final int PARSE_FORWARD_LOCK = 1 << 2;
@@ -1517,7 +1496,7 @@ public class PackageParser {
pkg.mCertificates = certificates;
try {
pkg.mSignatures = convertToSignatures(certificates);
pkg.mSignatures = ApkSignatureVerifier.convertToSignatures(certificates);
} catch (CertificateEncodingException e) {
// certificates weren't encoded properly; something went wrong
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
@@ -1580,157 +1559,46 @@ public class PackageParser {
throws PackageParserException {
final String apkPath = apkFile.getAbsolutePath();
// Try to verify the APK using APK Signature Scheme v2.
boolean verified = false;
{
Certificate[][] allSignersCerts = null;
Signature[] signatures = null;
try {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "verifyV2");
allSignersCerts = ApkSignatureSchemeV2Verifier.verify(apkPath);
signatures = convertToSignatures(allSignersCerts);
// APK verified using APK Signature Scheme v2.
verified = true;
} catch (ApkSignatureSchemeV2Verifier.SignatureNotFoundException e) {
// No APK Signature Scheme v2 signature found
if ((parseFlags & PARSE_IS_EPHEMERAL) != 0) {
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
boolean untrusted = (parseFlags & PARSE_IS_SYSTEM_DIR) == 0;
int minSignatureScheme = ApkSignatureVerifier.VERSION_JAR_SIGNATURE_SCHEME;
if ((parseFlags & PARSE_IS_EPHEMERAL) != 0 || pkg.applicationInfo.isStaticSharedLibrary()) {
// must use v2 signing scheme
minSignatureScheme = ApkSignatureVerifier.VERSION_APK_SIGNATURE_SCHEME_V2;
}
try {
ApkSignatureVerifier.Result verified =
ApkSignatureVerifier.verify(apkPath, minSignatureScheme, untrusted);
if (pkg.mCertificates == null) {
pkg.mCertificates = verified.certs;
pkg.mSignatures = verified.sigs;
pkg.mSigningKeys = new ArraySet<>(verified.certs.length);
for (int i = 0; i < verified.certs.length; i++) {
Certificate[] signerCerts = verified.certs[i];
Certificate signerCert = signerCerts[0];
pkg.mSigningKeys.add(signerCert.getPublicKey());
}
} else {
if (!Signature.areExactMatch(pkg.mSignatures, verified.sigs)) {
throw new PackageParserException(
INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES,
apkPath + " has mismatched certificates");
}
}
} catch (SignatureNotFoundException e) {
if ((parseFlags & PARSE_IS_EPHEMERAL) != 0) {
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"No APK Signature Scheme v2 signature in ephemeral package " + apkPath,
e);
}
// Static shared libraries must use only the V2 signing scheme
if (pkg.applicationInfo.isStaticSharedLibrary()) {
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"Static shared libs must use v2 signature scheme " + apkPath);
}
} catch (Exception e) {
// APK Signature Scheme v2 signature was found but did not verify
}
if (pkg.applicationInfo.isStaticSharedLibrary()) {
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"Failed to collect certificates from " + apkPath
+ " using APK Signature Scheme v2",
e);
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
"Static shared libs must use v2 signature scheme " + apkPath);
}
if (verified) {
if (pkg.mCertificates == null) {
pkg.mCertificates = allSignersCerts;
pkg.mSignatures = signatures;
pkg.mSigningKeys = new ArraySet<>(allSignersCerts.length);
for (int i = 0; i < allSignersCerts.length; i++) {
Certificate[] signerCerts = allSignersCerts[i];
Certificate signerCert = signerCerts[0];
pkg.mSigningKeys.add(signerCert.getPublicKey());
}
} else {
if (!Signature.areExactMatch(pkg.mSignatures, signatures)) {
throw new PackageParserException(
INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES,
apkPath + " has mismatched certificates");
}
}
// Not yet done, because we need to confirm that AndroidManifest.xml exists and,
// if requested, that classes.dex exists.
}
}
StrictJarFile jarFile = null;
try {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "strictJarFileCtor");
// Ignore signature stripping protections when verifying APKs from system partition.
// For those APKs we only care about extracting signer certificates, and don't care
// about verifying integrity.
boolean signatureSchemeRollbackProtectionsEnforced =
(parseFlags & PARSE_IS_SYSTEM_DIR) == 0;
jarFile = new StrictJarFile(
apkPath,
!verified, // whether to verify JAR signature
signatureSchemeRollbackProtectionsEnforced);
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
// Always verify manifest, regardless of source
final ZipEntry manifestEntry = jarFile.findEntry(ANDROID_MANIFEST_FILENAME);
if (manifestEntry == null) {
throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST,
"Package " + apkPath + " has no manifest");
}
// Optimization: early termination when APK already verified
if (verified) {
return;
}
// APK's integrity needs to be verified using JAR signature scheme.
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "verifyV1");
final List<ZipEntry> toVerify = new ArrayList<>();
toVerify.add(manifestEntry);
// If we're parsing an untrusted package, verify all contents
if ((parseFlags & PARSE_IS_SYSTEM_DIR) == 0) {
final Iterator<ZipEntry> i = jarFile.iterator();
while (i.hasNext()) {
final ZipEntry entry = i.next();
if (entry.isDirectory()) continue;
final String entryName = entry.getName();
if (entryName.startsWith("META-INF/")) continue;
if (entryName.equals(ANDROID_MANIFEST_FILENAME)) continue;
toVerify.add(entry);
}
}
// Verify that entries are signed consistently with the first entry
// we encountered. Note that for splits, certificates may have
// already been populated during an earlier parse of a base APK.
for (ZipEntry entry : toVerify) {
final Certificate[][] entryCerts = loadCertificates(jarFile, entry);
if (ArrayUtils.isEmpty(entryCerts)) {
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"Package " + apkPath + " has no certificates at entry "
+ entry.getName());
}
final Signature[] entrySignatures = convertToSignatures(entryCerts);
if (pkg.mCertificates == null) {
pkg.mCertificates = entryCerts;
pkg.mSignatures = entrySignatures;
pkg.mSigningKeys = new ArraySet<PublicKey>();
for (int i=0; i < entryCerts.length; i++) {
pkg.mSigningKeys.add(entryCerts[i][0].getPublicKey());
}
} else {
if (!Signature.areExactMatch(pkg.mSignatures, entrySignatures)) {
throw new PackageParserException(
INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES, "Package " + apkPath
+ " has mismatched certificates at entry "
+ entry.getName());
}
}
}
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
} catch (GeneralSecurityException e) {
throw new PackageParserException(INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING,
"Failed to collect certificates from " + apkPath, e);
} catch (IOException | RuntimeException e) {
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"Failed to collect certificates from " + apkPath, e);
} finally {
closeQuietly(jarFile);
"No APK Signature Scheme v2 signature in package " + apkPath, e);
}
}
private static Signature[] convertToSignatures(Certificate[][] certs)
throws CertificateEncodingException {
final Signature[] res = new Signature[certs.length];
for (int i = 0; i < certs.length; i++) {
res[i] = new Signature(certs[i]);
}
return res;
}
private static AssetManager newConfiguredAssetManager() {
AssetManager assetManager = new AssetManager();
assetManager.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
@@ -7647,33 +7515,6 @@ public class PackageParser {
sCompatibilityModeEnabled = compatibilityModeEnabled;
}
private static AtomicReference<byte[]> sBuffer = new AtomicReference<byte[]>();
public static long readFullyIgnoringContents(InputStream in) throws IOException {
byte[] buffer = sBuffer.getAndSet(null);
if (buffer == null) {
buffer = new byte[4096];
}
int n = 0;
int count = 0;
while ((n = in.read(buffer, 0, buffer.length)) != -1) {
count += n;
}
sBuffer.set(buffer);
return count;
}
public static void closeQuietly(StrictJarFile jarFile) {
if (jarFile != null) {
try {
jarFile.close();
} catch (Exception ignored) {
}
}
}
public static class PackageParserException extends Exception {
public final int error;

View File

@@ -917,18 +917,6 @@ public class ApkSignatureSchemeV2Verifier {
}
}
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);
}
}
/**
* {@link DataDigester} that updates multiple {@link MessageDigest}s whenever data is feeded.
*/

View File

@@ -0,0 +1,263 @@
/*
* Copyright (C) 2017 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 static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST;
import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING;
import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES;
import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_NO_CERTIFICATES;
import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION;
import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER;
import android.content.pm.PackageParser;
import android.content.pm.PackageParser.PackageParserException;
import android.content.pm.Signature;
import android.os.Trace;
import android.util.jar.StrictJarFile;
import com.android.internal.util.ArrayUtils;
import libcore.io.IoUtils;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.zip.ZipEntry;
/**
* Facade class that takes care of the details of APK verification on
* behalf of PackageParser.
*
* @hide for internal use only.
*/
public class ApkSignatureVerifier {
public static final int VERSION_JAR_SIGNATURE_SCHEME = 1;
public static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2;
private static final AtomicReference<byte[]> sBuffer = new AtomicReference<>();
/**
* Verifies the provided APK and returns the certificates associated with each signer. Also
* ensures that the provided APK contains an AndroidManifest.xml file.
*
* @param systemDir systemDir apk contents are already trusted, so we don't need to enforce
* v2 stripping rollback protection, or verify integrity of the APK.
*
* @throws PackageParserException if the APK's signature failed to verify.
* @throws SignatureNotFoundException if a signature corresponding to minLevel or greater
* is not found, except in the case of no JAR signature.
*/
public static Result verify(String apkPath, int minSignatureSchemeVersion, boolean systemDir)
throws PackageParserException, SignatureNotFoundException {
boolean verified = false;
Certificate[][] signerCerts;
int level = VERSION_APK_SIGNATURE_SCHEME_V2;
// first try v2
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "verifyV2");
try {
signerCerts = ApkSignatureSchemeV2Verifier.verify(apkPath);
Signature[] signerSigs = convertToSignatures(signerCerts);
// sanity check - must have an AndroidManifest file
StrictJarFile jarFile = null;
try {
jarFile = new StrictJarFile(apkPath, false, false);
final ZipEntry manifestEntry =
jarFile.findEntry(PackageParser.ANDROID_MANIFEST_FILENAME);
if (manifestEntry == null) {
throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST,
"Package " + apkPath + " has no manifest");
}
} finally {
closeQuietly(jarFile);
}
return new Result(signerCerts, signerSigs);
} catch (SignatureNotFoundException e) {
// not signed with v2, try older if allowed
if (minSignatureSchemeVersion >= VERSION_APK_SIGNATURE_SCHEME_V2) {
throw new SignatureNotFoundException(
"No APK Signature Scheme v2 signature found for " + apkPath, e);
}
} catch (PackageParserException e) {
// preserve any new exceptions explicitly thrown here
throw e;
} catch (Exception e) {
// APK Signature Scheme v2 signature found but did not verify
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"Failed to collect certificates from " + apkPath
+ " using APK Signature Scheme v2", e);
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
// v2 didn't work, try jarsigner
return verifyV1Signature(apkPath, systemDir);
}
private static Result verifyV1Signature(String apkPath, boolean systemDir)
throws PackageParserException {
StrictJarFile jarFile = null;
try {
final Certificate[][] lastCerts;
final Signature[] lastSigs;
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "strictJarFileCtor");
jarFile = new StrictJarFile(apkPath, true, !systemDir);
final List<ZipEntry> toVerify = new ArrayList<>();
// Always verify manifest, regardless of source
final ZipEntry manifestEntry = jarFile.findEntry(
PackageParser.ANDROID_MANIFEST_FILENAME);
if (manifestEntry == null) {
throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST,
"Package " + apkPath + " has no manifest");
}
lastCerts = loadCertificates(jarFile, manifestEntry);
if (ArrayUtils.isEmpty(lastCerts)) {
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, "Package "
+ apkPath + " has no certificates at entry "
+ PackageParser.ANDROID_MANIFEST_FILENAME);
}
lastSigs = convertToSignatures(lastCerts);
// don't waste time on already-trusted packages
if (!systemDir) {
final Iterator<ZipEntry> i = jarFile.iterator();
while (i.hasNext()) {
final ZipEntry entry = i.next();
if (entry.isDirectory()) continue;
final String entryName = entry.getName();
if (entryName.startsWith("META-INF/")) continue;
if (entryName.equals(PackageParser.ANDROID_MANIFEST_FILENAME)) continue;
toVerify.add(entry);
}
// Verify that entries are signed consistently with the first entry
// we encountered. Note that for splits, certificates may have
// already been populated during an earlier parse of a base APK.;
for (ZipEntry entry : toVerify) {
final Certificate[][] entryCerts = loadCertificates(jarFile, entry);
if (ArrayUtils.isEmpty(entryCerts)) {
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"Package " + apkPath + " has no certificates at entry "
+ entry.getName());
}
// make sure all entries use the same signing certs
final Signature[] entrySigs = convertToSignatures(entryCerts);
if (!Signature.areExactMatch(lastSigs, entrySigs)) {
throw new PackageParserException(
INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES,
"Package " + apkPath + " has mismatched certificates at entry "
+ entry.getName());
}
}
}
return new Result(lastCerts, lastSigs);
} catch (GeneralSecurityException e) {
throw new PackageParserException(INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING,
"Failed to collect certificates from " + apkPath, e);
} catch (IOException | RuntimeException e) {
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"Failed to collect certificates from " + apkPath, e);
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
closeQuietly(jarFile);
}
}
private static Certificate[][] loadCertificates(StrictJarFile jarFile, ZipEntry entry)
throws PackageParserException {
InputStream is = null;
try {
// We must read the stream for the JarEntry to retrieve
// its certificates.
is = jarFile.getInputStream(entry);
readFullyIgnoringContents(is);
return jarFile.getCertificateChains(entry);
} catch (IOException | RuntimeException e) {
throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,
"Failed reading " + entry.getName() + " in " + jarFile, e);
} finally {
IoUtils.closeQuietly(is);
}
}
private static void readFullyIgnoringContents(InputStream in) throws IOException {
byte[] buffer = sBuffer.getAndSet(null);
if (buffer == null) {
buffer = new byte[4096];
}
int n = 0;
int count = 0;
while ((n = in.read(buffer, 0, buffer.length)) != -1) {
count += n;
}
sBuffer.set(buffer);
return;
}
/**
* Converts an array of certificate chains into the {@code Signature} equivalent used by the
* PackageManager.
*
* @throws CertificateEncodingException if it is unable to create a Signature object.
*/
public static Signature[] convertToSignatures(Certificate[][] certs)
throws CertificateEncodingException {
final Signature[] res = new Signature[certs.length];
for (int i = 0; i < certs.length; i++) {
res[i] = new Signature(certs[i]);
}
return res;
}
private static void closeQuietly(StrictJarFile jarFile) {
if (jarFile != null) {
try {
jarFile.close();
} catch (Exception ignored) {
}
}
}
/**
* Result of a successful APK verification operation.
*/
public static class Result {
public final Certificate[][] certs;
public final Signature[] sigs;
public Result(Certificate[][] certs, Signature[] sigs) {
this.certs = certs;
this.sigs = sigs;
}
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (C) 2017 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;
/**
* Indicates that the APK is missing a signature.
*
* @hide
*/
public 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);
}
}