diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index 97c2b7d11354a..142bb39fe8926 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -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 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 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(); - 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 sBuffer = new AtomicReference(); - - 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; diff --git a/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java b/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java index 180812340ba81..a74a8821eba61 100644 --- a/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java +++ b/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java @@ -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. */ diff --git a/core/java/android/util/apk/ApkSignatureVerifier.java b/core/java/android/util/apk/ApkSignatureVerifier.java new file mode 100644 index 0000000000000..73a9478509e69 --- /dev/null +++ b/core/java/android/util/apk/ApkSignatureVerifier.java @@ -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 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 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 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; + } + } +} diff --git a/core/java/android/util/apk/SignatureNotFoundException.java b/core/java/android/util/apk/SignatureNotFoundException.java new file mode 100644 index 0000000000000..9c7c7600cf732 --- /dev/null +++ b/core/java/android/util/apk/SignatureNotFoundException.java @@ -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); + } +}