From 1d85df2d5e94563ae35171c02421a813a04cd30a Mon Sep 17 00:00:00 2001 From: Alex Buynytskyy Date: Wed, 20 Jan 2021 21:07:08 -0800 Subject: [PATCH] Signature verification for checksums. Bug: 160605420 Test: atest ChecksumsTest Change-Id: Ia1cca0c678256defc2d3b781eb75494b9b17496d --- core/java/android/content/pm/Checksum.java | 38 +++- .../android/content/pm/PackageInstaller.java | 14 +- .../com/android/server/pm/ApkChecksums.java | 201 ++++++++++++++---- .../server/pm/PackageInstallerSession.java | 44 ++-- 4 files changed, 231 insertions(+), 66 deletions(-) diff --git a/core/java/android/content/pm/Checksum.java b/core/java/android/content/pm/Checksum.java index 318d3635ede1e..4f4898af278b1 100644 --- a/core/java/android/content/pm/Checksum.java +++ b/core/java/android/content/pm/Checksum.java @@ -23,6 +23,9 @@ import android.os.Parcelable; import com.android.internal.util.DataClass; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -122,6 +125,33 @@ public final class Checksum implements Parcelable { @Retention(RetentionPolicy.SOURCE) public @interface Type {} + /** + * Serialize checksum to the stream in binary format. + * @hide + */ + public static void writeToStream(@NonNull DataOutputStream dos, @NonNull Checksum checksum) + throws IOException { + dos.writeInt(checksum.getType()); + + final byte[] valueBytes = checksum.getValue(); + dos.writeInt(valueBytes.length); + dos.write(valueBytes); + } + + /** + * Deserialize checksum previously stored in + * {@link #writeToStream(DataOutputStream, Checksum)}. + * @hide + */ + public static @NonNull Checksum readFromStream(@NonNull DataInputStream dis) + throws IOException { + final int type = dis.readInt(); + + final byte[] valueBytes = new byte[dis.readInt()]; + dis.read(valueBytes); + return new Checksum(type, valueBytes); + } + /** * Checksum type. */ @@ -133,7 +163,7 @@ public final class Checksum implements Parcelable { - // Code below generated by codegen v1.0.15. + // Code below generated by codegen v1.0.22. // // DO NOT MODIFY! // CHECKSTYLE:OFF Generated code @@ -233,10 +263,10 @@ public final class Checksum implements Parcelable { }; @DataClass.Generated( - time = 1601955017774L, - codegenVersion = "1.0.15", + time = 1611601571576L, + codegenVersion = "1.0.22", sourceFile = "frameworks/base/core/java/android/content/pm/Checksum.java", - inputSignatures = "public static final int TYPE_WHOLE_MERKLE_ROOT_4K_SHA256\npublic static final @java.lang.Deprecated int TYPE_WHOLE_MD5\npublic static final @java.lang.Deprecated int TYPE_WHOLE_SHA1\npublic static final @java.lang.Deprecated int TYPE_WHOLE_SHA256\npublic static final @java.lang.Deprecated int TYPE_WHOLE_SHA512\npublic static final int TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256\npublic static final int TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512\nprivate final @android.content.pm.Checksum.Type int mType\nprivate final @android.annotation.NonNull byte[] mValue\nclass Checksum extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genConstDefs=false)") + inputSignatures = "public static final int TYPE_WHOLE_MERKLE_ROOT_4K_SHA256\npublic static final @java.lang.Deprecated int TYPE_WHOLE_MD5\npublic static final @java.lang.Deprecated int TYPE_WHOLE_SHA1\npublic static final @java.lang.Deprecated int TYPE_WHOLE_SHA256\npublic static final @java.lang.Deprecated int TYPE_WHOLE_SHA512\npublic static final int TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256\npublic static final int TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512\nprivate final @android.content.pm.Checksum.Type int mType\nprivate final @android.annotation.NonNull byte[] mValue\npublic static void writeToStream(java.io.DataOutputStream,android.content.pm.Checksum)\npublic static @android.annotation.NonNull android.content.pm.Checksum readFromStream(java.io.DataInputStream)\nclass Checksum extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genConstDefs=false)") @Deprecated private void __metadata() {} diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index 248be0f095107..f9980bc63d92f 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -1253,9 +1253,17 @@ public class PackageInstaller { * {@link #openWrite} * @param checksums installer intends to make available via * {@link PackageManager#requestChecksums}. - * @param signature PKCS#7 detached signature bytes over serialized checksums to enable - * fs-verity for the checksums or null if fs-verity should not be enabled. - * @see fs-verity + * @param signature DER PKCS#7 detached signature bytes over binary serialized checksums + * to enable integrity checking for the checksums or null for no integrity + * checking. {@link PackageManager#requestChecksums} will return + * the certificate used to create signature. + * Binary format for checksums: + *
{@code DataOutputStream dos;
+         *                  dos.writeInt(checksum.getType());
+         *                  dos.writeInt(checksum.getValue().length);
+         *                  dos.write(checksum.getValue());}
+ * If using openssl cms, make sure to specify -binary -nosmimecap. + * @see openssl cms * @throws SecurityException if called after the session has been * committed or abandoned. * @throws IllegalStateException if checksums for this file have already been added. diff --git a/services/core/java/com/android/server/pm/ApkChecksums.java b/services/core/java/com/android/server/pm/ApkChecksums.java index 08739cbeaee49..ff3a12ae73554 100644 --- a/services/core/java/com/android/server/pm/ApkChecksums.java +++ b/services/core/java/com/android/server/pm/ApkChecksums.java @@ -63,8 +63,10 @@ import com.android.server.pm.parsing.pkg.AndroidPackage; import com.android.server.security.VerityUtils; import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; +import java.io.EOFException; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -72,17 +74,24 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.RandomAccessFile; import java.nio.ByteBuffer; +import java.nio.file.Files; import java.security.DigestException; +import java.security.InvalidParameterException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; +import sun.security.pkcs.PKCS7; +import sun.security.pkcs.SignerInfo; + /** * Provides checksums for APK. */ @@ -90,6 +99,7 @@ public class ApkChecksums { static final String TAG = "ApkChecksums"; private static final String DIGESTS_FILE_EXTENSION = ".digests"; + private static final String DIGESTS_SIGNATURE_FILE_EXTENSION = ".signature"; // MessageDigest algorithms. static final String ALGO_MD5 = "MD5"; @@ -97,6 +107,8 @@ public class ApkChecksums { static final String ALGO_SHA256 = "SHA256"; static final String ALGO_SHA512 = "SHA512"; + private static final Certificate[] EMPTY_CERTIFICATE_ARRAY = {}; + /** * Check back in 1 second after we detected we needed to wait for the APK to be fully available. */ @@ -166,6 +178,21 @@ public class ApkChecksums { + DIGESTS_FILE_EXTENSION; } + /** + * Return the signature path associated with the given digests path. + * (appends '.signature' to the end) + */ + public static String buildSignaturePathForDigests(String digestsPath) { + return digestsPath + DIGESTS_SIGNATURE_FILE_EXTENSION; + } + + /** Returns true if the given file looks like containing digests or digests' signature. */ + public static boolean isDigestOrDigestSignatureFile(File file) { + final String name = file.getName(); + return name.endsWith(DIGESTS_FILE_EXTENSION) || name.endsWith( + DIGESTS_SIGNATURE_FILE_EXTENSION); + } + /** * Search for the digests file associated with the given target file. * If it exists, the method returns the digests file; otherwise it returns null. @@ -176,43 +203,93 @@ public class ApkChecksums { return digestsFile.exists() ? digestsFile : null; } + /** + * Search for the signature file associated with the given digests file. + * If it exists, the method returns the signature file; otherwise it returns null. + */ + public static File findSignatureForDigests(File digestsFile) { + String signaturePath = buildSignaturePathForDigests(digestsFile.getAbsolutePath()); + File signatureFile = new File(signaturePath); + return signatureFile.exists() ? signatureFile : null; + } + /** * Serialize checksums to the stream in binary format. */ public static void writeChecksums(OutputStream os, Checksum[] checksums) - throws IOException, CertificateException { + throws IOException { try (DataOutputStream dos = new DataOutputStream(os)) { - dos.writeInt(checksums.length); for (Checksum checksum : checksums) { - dos.writeInt(checksum.getType()); - - final byte[] valueBytes = checksum.getValue(); - dos.writeInt(valueBytes.length); - dos.write(valueBytes); + Checksum.writeToStream(dos, checksum); } } } + private static Checksum[] readChecksums(File file) throws IOException { + try (InputStream is = new FileInputStream(file)) { + return readChecksums(is); + } + } + /** * Deserialize array of checksums previously stored in * {@link #writeChecksums(OutputStream, Checksum[])}. */ - private static Checksum[] readChecksums(File file) throws IOException { - try (InputStream is = new FileInputStream(file); - DataInputStream dis = new DataInputStream(is)) { - final int size = dis.readInt(); - Checksum[] checksums = new Checksum[size]; - for (int i = 0; i < size; ++i) { - final int type = dis.readInt(); - - final byte[] valueBytes = new byte[dis.readInt()]; - dis.read(valueBytes); - checksums[i] = new Checksum(type, valueBytes); + public static Checksum[] readChecksums(InputStream is) throws IOException { + try (DataInputStream dis = new DataInputStream(is)) { + ArrayList checksums = new ArrayList<>(); + try { + // 100 is an arbitrary very big number. We should stop at EOF. + for (int i = 0; i < 100; ++i) { + checksums.add(Checksum.readFromStream(dis)); + } + } catch (EOFException e) { + // expected } - return checksums; + return checksums.toArray(new Checksum[checksums.size()]); } } + /** + * Verifies signature over binary serialized checksums. + * @param checksums array of checksums + * @param signature detached PKCS7 signature in DER format + * @return all certificates that passed verification + * @throws SignatureException if verification fails + */ + public static @NonNull Certificate[] verifySignature(Checksum[] checksums, byte[] signature) + throws NoSuchAlgorithmException, IOException, SignatureException { + final byte[] blob; + try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { + writeChecksums(os, checksums); + blob = os.toByteArray(); + } + + PKCS7 pkcs7 = new PKCS7(signature); + + final Certificate[] certs = pkcs7.getCertificates(); + if (certs == null || certs.length == 0) { + throw new SignatureException("Signature missing certificates"); + } + + final SignerInfo[] signerInfos = pkcs7.verify(blob); + if (signerInfos == null || signerInfos.length == 0) { + throw new SignatureException("Verification failed"); + } + + ArrayList certificates = new ArrayList<>(signerInfos.length); + for (SignerInfo signerInfo : signerInfos) { + ArrayList chain = signerInfo.getCertificateChain(pkcs7); + if (chain == null) { + throw new SignatureException( + "Verification passed, but certification chain is empty."); + } + certificates.addAll(chain); + } + + return certificates.toArray(new Certificate[certificates.size()]); + } + /** * Fetch or calculate checksums for the collection of files. * @@ -335,6 +412,8 @@ public class ApkChecksums { } } + // Note: this compares installer and system digests internally and + // has to be called right after all system digests are populated. getInstallerChecksums(split, file, types, installerPackageName, trustedInstallers, checksums, injector); } @@ -352,31 +431,52 @@ public class ApkChecksums { return; } - final File digestsFile = new File(buildDigestsPathForApk(file.getAbsolutePath())); - if (!digestsFile.exists()) { + final File digestsFile = findDigestsForFile(file); + if (digestsFile == null) { return; } - - final AndroidPackage installer = injector.getPackageManagerInternal().getPackage( - installerPackageName); - if (installer == null) { - Slog.e(TAG, "Installer package not found."); - return; - } - - // Obtaining array of certificates used for signing the installer package. - final Signature[] certs = installer.getSigningDetails().signatures; - final Signature[] pastCerts = installer.getSigningDetails().pastSigningCertificates; - if (certs == null || certs.length == 0 || certs[0] == null) { - Slog.e(TAG, "Can't obtain calling installer package's certificates."); - return; - } - // According to V2/V3 signing schema, the first certificate corresponds to the public key - // in the signing block. - byte[] trustedCertBytes = certs[0].toByteArray(); + final File signatureFile = findSignatureForDigests(digestsFile); try { final Checksum[] digests = readChecksums(digestsFile); + final Signature[] certs; + final Signature[] pastCerts; + + if (signatureFile != null) { + final Certificate[] certificates = verifySignature(digests, + Files.readAllBytes(signatureFile.toPath())); + if (certificates == null || certificates.length == 0) { + Slog.e(TAG, "Error validating signature"); + return; + } + + certs = new Signature[certificates.length]; + for (int i = 0, size = certificates.length; i < size; i++) { + certs[i] = new Signature(certificates[i].getEncoded()); + } + + pastCerts = null; + } else { + final AndroidPackage installer = injector.getPackageManagerInternal().getPackage( + installerPackageName); + if (installer == null) { + Slog.e(TAG, "Installer package not found."); + return; + } + + // Obtaining array of certificates used for signing the installer package. + certs = installer.getSigningDetails().signatures; + pastCerts = installer.getSigningDetails().pastSigningCertificates; + } + if (certs == null || certs.length == 0 || certs[0] == null) { + Slog.e(TAG, "Can't obtain certificates."); + return; + } + + // According to V2/V3 signing schema, the first certificate corresponds to the public + // key in the signing block. + byte[] trustedCertBytes = certs[0].toByteArray(); + final Set trusted = convertToSet(trustedInstallers); if (trusted != null && !trusted.isEmpty()) { @@ -391,6 +491,16 @@ public class ApkChecksums { trustedCertBytes = trustedCert.toByteArray(); } + // Compare OS-enforced digests. + for (Checksum digest : digests) { + final ApkChecksum system = checksums.get(digest.getType()); + if (system != null && !Arrays.equals(system.getValue(), digest.getValue())) { + throw new InvalidParameterException("System digest " + digest.getType() + + " mismatch, can't bind installer-provided digests to the APK."); + } + } + + // Append missing digests. for (Checksum digest : digests) { if (isRequired(digest.getType(), types, checksums)) { checksums.put(digest.getType(), @@ -398,7 +508,16 @@ public class ApkChecksums { } } } catch (IOException e) { - Slog.e(TAG, "Error reading .digests", e); + Slog.e(TAG, "Error reading .digests or .signature", e); + } catch (NoSuchAlgorithmException | SignatureException | InvalidParameterException e) { + Slog.e(TAG, "Error validating digests. Invalid digests will be removed", e); + try { + Files.deleteIfExists(digestsFile.toPath()); + if (signatureFile != null) { + Files.deleteIfExists(signatureFile.toPath()); + } + } catch (IOException ignored) { + } } catch (CertificateEncodingException e) { Slog.e(TAG, "Error encoding trustedInstallers", e); } diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 2b5c393e71595..e143bd0cb6241 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -27,7 +27,7 @@ import static android.content.pm.PackageManager.INSTALL_FAILED_INTERNAL_ERROR; import static android.content.pm.PackageManager.INSTALL_FAILED_INVALID_APK; import static android.content.pm.PackageManager.INSTALL_FAILED_MEDIA_UNAVAILABLE; import static android.content.pm.PackageManager.INSTALL_FAILED_MISSING_SPLIT; -import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING; +import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_NO_CERTIFICATES; import static android.content.pm.PackageManager.INSTALL_STAGED; import static android.content.pm.PackageManager.INSTALL_SUCCEEDED; import static android.system.OsConstants.O_CREAT; @@ -59,7 +59,6 @@ import android.content.IIntentReceiver; import android.content.IIntentSender; import android.content.Intent; import android.content.IntentSender; -import android.content.pm.ApkChecksum; import android.content.pm.ApplicationInfo; import android.content.pm.Checksum; import android.content.pm.DataLoaderManager; @@ -160,7 +159,9 @@ import java.io.FileDescriptor; import java.io.FileFilter; import java.io.FileOutputStream; import java.io.IOException; -import java.security.cert.CertificateException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; +import java.security.cert.Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -249,7 +250,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private static final String PROPERTY_NAME_INHERIT_NATIVE = "pi.inherit_native_on_dont_kill"; private static final int[] EMPTY_CHILD_SESSION_ARRAY = EmptyArray.INT; private static final InstallationFile[] EMPTY_INSTALLATION_FILE_ARRAY = {}; - private static final ApkChecksum[] EMPTY_FILE_CHECKSUM_ARRAY = {}; private static final String SYSTEM_DATA_LOADER_PACKAGE = "android"; private static final String APEX_FILE_EXTENSION = ".apex"; @@ -805,6 +805,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { if (file.getName().endsWith(REMOVE_MARKER_EXTENSION)) return false; if (DexMetadataHelper.isDexMetadataFile(file)) return false; if (VerityUtils.isFsveritySignatureFile(file)) return false; + if (ApkChecksums.isDigestOrDigestSignatureFile(file)) return false; return true; } }; @@ -1294,13 +1295,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } if (signature != null && signature.length != 0) { - final boolean standardMode = PackageManagerServiceUtils.isApkVerityEnabled(); - final boolean legacyMode = PackageManagerServiceUtils.isLegacyApkVerityEnabled(); - if (!standardMode || legacyMode) { - Slog.e(TAG, - "Can't enforce checksum's signature: Apk-Verity is disabled or in legacy " - + "mode."); - signature = null; + try { + Certificate[] ignored = ApkChecksums.verifySignature(checksums, signature); + } catch (IOException | NoSuchAlgorithmException | SignatureException e) { + throw new IllegalArgumentException("Can't verify signature", e); } } @@ -3093,30 +3091,35 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { final String targetDigestsPath = ApkChecksums.buildDigestsPathForApk(targetFile.getName()); final File targetDigestsFile = new File(stageDir, targetDigestsPath); try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { - // Storing and staging checksums. ApkChecksums.writeChecksums(os, checksums); + + final byte[] signature = perFileChecksum.getSignature(); + if (signature != null && signature.length > 0) { + Certificate[] ignored = ApkChecksums.verifySignature(checksums, signature); + } + + // Storing and staging checksums. storeBytesToInstallationFile(targetDigestsPath, targetDigestsFile.getAbsolutePath(), os.toByteArray()); stageFileLocked(targetDigestsFile, targetDigestsFile); - final byte[] signature = perFileChecksum.getSignature(); + // Storing and staging signature. if (signature == null || signature.length == 0) { return; } - // Storing and staging signature. - final String targetDigestsSignaturePath = VerityUtils.getFsveritySignatureFilePath( + final String targetDigestsSignaturePath = ApkChecksums.buildSignaturePathForDigests( targetDigestsPath); final File targetDigestsSignatureFile = new File(stageDir, targetDigestsSignaturePath); storeBytesToInstallationFile(targetDigestsSignaturePath, targetDigestsSignatureFile.getAbsolutePath(), signature); stageFileLocked(targetDigestsSignatureFile, targetDigestsSignatureFile); - } catch (CertificateException e) { - throw new PackageManagerException(INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING, - "Failed to encode certificate for " + mPackageName, e); } catch (IOException e) { throw new PackageManagerException(INSTALL_FAILED_INSUFFICIENT_STORAGE, "Failed to store digests for " + mPackageName, e); + } catch (NoSuchAlgorithmException | SignatureException e) { + throw new PackageManagerException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "Failed to verify digests' signature for " + mPackageName, e); } } @@ -3160,6 +3163,11 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { final File digestsFile = ApkChecksums.findDigestsForFile(origFile); if (digestsFile != null) { mResolvedInheritedFiles.add(digestsFile); + + final File signatureFile = ApkChecksums.findSignatureForDigests(digestsFile); + if (signatureFile != null) { + mResolvedInheritedFiles.add(signatureFile); + } } }