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