diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index e10827bc6101b..10980b79f1f42 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -16004,7 +16004,8 @@ public class PackageManagerService extends IPackageManager.Stub } if (apkPath != null) { final VerityUtils.SetupResult result = - VerityUtils.generateApkVeritySetupData(apkPath); + VerityUtils.generateApkVeritySetupData(apkPath, null /* signaturePath */, + true /* skipSigningBlock */); if (result.isOk()) { if (Build.IS_DEBUGGABLE) Slog.i(TAG, "Enabling apk verity to " + apkPath); FileDescriptor fd = result.getUnownedFileDescriptor(); diff --git a/services/core/java/com/android/server/security/VerityUtils.java b/services/core/java/com/android/server/security/VerityUtils.java index 9f69702911c9c..37966108fe640 100644 --- a/services/core/java/com/android/server/security/VerityUtils.java +++ b/services/core/java/com/android/server/security/VerityUtils.java @@ -26,42 +26,76 @@ import android.system.Os; import android.util.Pair; import android.util.Slog; import android.util.apk.ApkSignatureVerifier; +import android.util.apk.ApkVerityBuilder; import android.util.apk.ByteBufferFactory; import android.util.apk.SignatureNotFoundException; +import libcore.util.HexEncoding; + import java.io.FileDescriptor; import java.io.IOException; +import java.io.RandomAccessFile; import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.security.DigestException; +import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; +import sun.security.pkcs.PKCS7; + /** Provides fsverity related operations. */ abstract public class VerityUtils { private static final String TAG = "VerityUtils"; + /** The maximum size of signature file. This is just to avoid potential abuse. */ + private static final int MAX_SIGNATURE_FILE_SIZE_BYTES = 8192; + private static final boolean DEBUG = false; /** - * Generates Merkle tree and fsverity metadata. + * Generates Merkle tree and fs-verity metadata. * - * @return {@code SetupResult} that contains the {@code EsetupResultCode}, and when success, the + * @return {@code SetupResult} that contains the result code, and when success, the * {@code FileDescriptor} to read all the data from. */ - public static SetupResult generateApkVeritySetupData(@NonNull String apkPath) { - if (DEBUG) Slog.d(TAG, "Trying to install apk verity to " + apkPath); + public static SetupResult generateApkVeritySetupData(@NonNull String apkPath, + String signaturePath, boolean skipSigningBlock) { + if (DEBUG) { + Slog.d(TAG, "Trying to install apk verity to " + apkPath + " with signature file " + + signaturePath); + } SharedMemory shm = null; try { - byte[] signedRootHash = ApkSignatureVerifier.getVerityRootHash(apkPath); - if (signedRootHash == null) { + byte[] signedVerityHash; + if (skipSigningBlock) { + signedVerityHash = ApkSignatureVerifier.getVerityRootHash(apkPath); + } else { + Path path = Paths.get(signaturePath); + if (Files.exists(path)) { + // TODO(112037636): fail early if the signing key is not in .fs-verity keyring. + PKCS7 pkcs7 = new PKCS7(Files.readAllBytes(path)); + signedVerityHash = pkcs7.getContentInfo().getContentBytes(); + if (DEBUG) { + Slog.d(TAG, "fs-verity measurement = " + bytesToString(signedVerityHash)); + } + } else { + signedVerityHash = null; + } + } + + if (signedVerityHash == null) { if (DEBUG) { - Slog.d(TAG, "Skip verity tree generation since there is no root hash"); + Slog.d(TAG, "Skip verity tree generation since there is no signed root hash"); } return SetupResult.skipped(); } - Pair result = generateApkVerityIntoSharedMemory(apkPath, - signedRootHash); + Pair result = generateFsVerityIntoSharedMemory(apkPath, + signaturePath, signedVerityHash, skipSigningBlock); shm = result.first; int contentSize = result.second; FileDescriptor rfd = shm.getFileDescriptor(); @@ -96,23 +130,115 @@ abstract public class VerityUtils { return ApkSignatureVerifier.getVerityRootHash(apkPath); } + /** + * Generates fs-verity metadata for {@code filePath} in the buffer created by {@code + * trackedBufferFactory}. The metadata contains the Merkle tree, fs-verity descriptor and + * extensions, including a PKCS#7 signature provided in {@code signaturePath}. + * + *

It is worthy to note that {@code trackedBufferFactory} generates a "tracked" {@code + * ByteBuffer}. The data will be used outside this method via the factory itself. + * + * @return fs-verity measurement of {@code filePath}, which is a SHA-256 of fs-verity descriptor + * and authenticated extensions. + */ + private static byte[] generateFsverityMetadata(String filePath, String signaturePath, + @NonNull TrackedShmBufferFactory trackedBufferFactory) + throws IOException, SignatureNotFoundException, SecurityException, DigestException, + NoSuchAlgorithmException { + try (RandomAccessFile file = new RandomAccessFile(filePath, "r")) { + ApkVerityBuilder.ApkVerityResult result = ApkVerityBuilder.generateFsVerityTree( + file, trackedBufferFactory); + + ByteBuffer buffer = result.verityData; + buffer.position(result.merkleTreeSize); + return generateFsverityDescriptorAndMeasurement(file, result.rootHash, signaturePath, + buffer); + } + } + + /** + * Generates fs-verity descriptor including the extensions to the {@code output} and returns the + * fs-verity measurement. + * + * @return fs-verity measurement, which is a SHA-256 of fs-verity descriptor and authenticated + * extensions. + */ + private static byte[] generateFsverityDescriptorAndMeasurement( + @NonNull RandomAccessFile file, @NonNull byte[] rootHash, + @NonNull String pkcs7SignaturePath, @NonNull ByteBuffer output) + throws IOException, NoSuchAlgorithmException, DigestException { + final short kRootHashExtensionId = 1; + final short kPkcs7SignatureExtensionId = 3; + final int origPosition = output.position(); + + // For generating fs-verity file measurement, which consists of the descriptor and + // authenticated extensions (but not unauthenticated extensions and the footer). + MessageDigest md = MessageDigest.getInstance("SHA-256"); + + // 1. Generate fs-verity descriptor. + final byte[] desc = constructFsverityDescriptorNative(file.length()); + output.put(desc); + md.update(desc); + + // 2. Generate authenticated extensions. + final byte[] authExt = + constructFsverityExtensionNative(kRootHashExtensionId, rootHash.length); + output.put(authExt); + output.put(rootHash); + md.update(authExt); + md.update(rootHash); + + // 3. Generate unauthenticated extensions. + ByteBuffer header = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN); + output.putShort((short) 1); // number of unauthenticated extensions below + output.position(output.position() + 6); + + // Generate PKCS#7 extension. NB: We do not verify agaist trusted certificate (should be + // done by the caller if needed). + Path path = Paths.get(pkcs7SignaturePath); + if (Files.size(path) > MAX_SIGNATURE_FILE_SIZE_BYTES) { + throw new IllegalArgumentException("Signature size is unexpectedly large: " + + pkcs7SignaturePath); + } + final byte[] pkcs7Signature = Files.readAllBytes(path); + output.put(constructFsverityExtensionNative(kPkcs7SignatureExtensionId, + pkcs7Signature.length)); + output.put(pkcs7Signature); + + // 4. Generate the footer. + output.put(constructFsverityFooterNative(output.position() - origPosition)); + + return md.digest(); + } + + private static native byte[] constructFsverityDescriptorNative(long fileSize); + private static native byte[] constructFsverityExtensionNative(short extensionId, + int extensionDataSize); + private static native byte[] constructFsverityFooterNative(int offsetToDescriptorHead); + /** * Returns a pair of {@code SharedMemory} and {@code Integer}. The {@code SharedMemory} contains * Merkle tree and fsverity headers for the given apk, in the form that can immediately be used * for fsverity setup. The data is aligned to the beginning of {@code SharedMemory}, and has * length equals to the returned {@code Integer}. */ - private static Pair generateApkVerityIntoSharedMemory( - String apkPath, byte[] expectedRootHash) + private static Pair generateFsVerityIntoSharedMemory( + String apkPath, String signaturePath, @NonNull byte[] expectedRootHash, + boolean skipSigningBlock) throws IOException, SecurityException, DigestException, NoSuchAlgorithmException, SignatureNotFoundException { TrackedShmBufferFactory shmBufferFactory = new TrackedShmBufferFactory(); - byte[] generatedRootHash = ApkSignatureVerifier.generateApkVerity(apkPath, - shmBufferFactory); + byte[] generatedRootHash; + if (skipSigningBlock) { + generatedRootHash = ApkSignatureVerifier.generateApkVerity(apkPath, shmBufferFactory); + } else { + generatedRootHash = generateFsverityMetadata(apkPath, signaturePath, shmBufferFactory); + } // We only generate Merkle tree once here, so it's important to make sure the root hash // matches the signed one in the apk. if (!Arrays.equals(expectedRootHash, generatedRootHash)) { - throw new SecurityException("Locally generated verity root hash does not match"); + throw new SecurityException("verity hash mismatch: " + + bytesToString(generatedRootHash) + " != " + bytesToString(expectedRootHash)); } int contentSize = shmBufferFactory.getBufferLimit(); @@ -126,11 +252,15 @@ abstract public class VerityUtils { return Pair.create(shm, contentSize); } + private static String bytesToString(byte[] bytes) { + return HexEncoding.encodeToString(bytes); + } + public static class SetupResult { /** Result code if verity is set up correctly. */ private static final int RESULT_OK = 1; - /** Result code if the apk does not contain a verity root hash. */ + /** Result code if signature is not provided. */ private static final int RESULT_SKIPPED = 2; /** Result code if the setup failed. */ diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp index becde7311607e..061f8e2ba0fb8 100644 --- a/services/core/jni/Android.bp +++ b/services/core/jni/Android.bp @@ -37,6 +37,7 @@ cc_library_static { "com_android_server_locksettings_SyntheticPasswordManager.cpp", "com_android_server_net_NetworkStatsService.cpp", "com_android_server_power_PowerManagerService.cpp", + "com_android_server_security_VerityUtils.cpp", "com_android_server_SerialService.cpp", "com_android_server_storage_AppFuseBridge.cpp", "com_android_server_SystemServer.cpp", diff --git a/services/core/jni/com_android_server_security_VerityUtils.cpp b/services/core/jni/com_android_server_security_VerityUtils.cpp new file mode 100644 index 0000000000000..d0f173b572c8e --- /dev/null +++ b/services/core/jni/com_android_server_security_VerityUtils.cpp @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2018 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. + */ + +#define LOG_TAG "VerityUtils" + +#include +#include "jni.h" +#include + +#include + +// TODO(112037636): Always include once fsverity.h is upstreamed and backported. +#define HAS_FSVERITY 0 + +#if HAS_FSVERITY +#include +#endif + +namespace android { + +namespace { + +class JavaByteArrayHolder { + public: + static JavaByteArrayHolder* newArray(JNIEnv* env, jsize size) { + return new JavaByteArrayHolder(env, size); + } + + jbyte* getRaw() { + return mElements; + } + + jbyteArray release() { + mEnv->ReleaseByteArrayElements(mBytes, mElements, 0); + mElements = nullptr; + return mBytes; + } + + private: + JavaByteArrayHolder(JNIEnv* env, jsize size) { + mEnv = env; + mBytes = mEnv->NewByteArray(size); + mElements = mEnv->GetByteArrayElements(mBytes, nullptr); + memset(mElements, 0, size); + } + + virtual ~JavaByteArrayHolder() { + LOG_ALWAYS_FATAL_IF(mElements == nullptr, "Elements are not released"); + } + + JNIEnv* mEnv; + jbyteArray mBytes; + jbyte* mElements; +}; + +jbyteArray constructFsverityDescriptor(JNIEnv* env, jobject /* clazz */, jlong fileSize) { +#if HAS_FSVERITY + auto raii = JavaByteArrayHolder::newArray(env, sizeof(fsverity_descriptor)); + fsverity_descriptor* desc = reinterpret_cast(raii->getRaw()); + + memcpy(desc->magic, FS_VERITY_MAGIC, sizeof(desc->magic)); + desc->major_version = 1; + desc->minor_version = 0; + desc->log_data_blocksize = 12; + desc->log_tree_blocksize = 12; + desc->data_algorithm = FS_VERITY_ALG_SHA256; + desc->tree_algorithm = FS_VERITY_ALG_SHA256; + desc->flags = 0; + desc->orig_file_size = fileSize; + desc->auth_ext_count = 1; + + return raii->release(); +#else + LOG_ALWAYS_FATAL("fs-verity is used while not enabled"); + return 0; +#endif // HAS_FSVERITY +} + +jbyteArray constructFsverityExtension(JNIEnv* env, jobject /* clazz */, jshort extensionId, + jint extensionDataSize) { +#if HAS_FSVERITY + auto raii = JavaByteArrayHolder::newArray(env, sizeof(fsverity_extension)); + fsverity_extension* ext = reinterpret_cast(raii->getRaw()); + + ext->length = sizeof(fsverity_extension) + extensionDataSize; + ext->type = extensionId; + + return raii->release(); +#else + LOG_ALWAYS_FATAL("fs-verity is used while not enabled"); + return 0; +#endif // HAS_FSVERITY +} + +jbyteArray constructFsverityFooter(JNIEnv* env, jobject /* clazz */, + jint offsetToDescriptorHead) { +#if HAS_FSVERITY + auto raii = JavaByteArrayHolder::newArray(env, sizeof(fsverity_footer)); + fsverity_footer* footer = reinterpret_cast(raii->getRaw()); + + footer->desc_reverse_offset = offsetToDescriptorHead + sizeof(fsverity_footer); + memcpy(footer->magic, FS_VERITY_MAGIC, sizeof(footer->magic)); + + return raii->release(); +#else + LOG_ALWAYS_FATAL("fs-verity is used while not enabled"); + return 0; +#endif // HAS_FSVERITY +} + +const JNINativeMethod sMethods[] = { + { "constructFsverityDescriptorNative", "(J)[B", (void *)constructFsverityDescriptor }, + { "constructFsverityExtensionNative", "(SI)[B", (void *)constructFsverityExtension }, + { "constructFsverityFooterNative", "(I)[B", (void *)constructFsverityFooter }, +}; + +} // namespace + +int register_android_server_security_VerityUtils(JNIEnv* env) { + return jniRegisterNativeMethods(env, + "com/android/server/security/VerityUtils", sMethods, NELEM(sMethods)); +} + +} // namespace android diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp index bb6e6840f3b41..918f57e2945e0 100644 --- a/services/core/jni/onload.cpp +++ b/services/core/jni/onload.cpp @@ -54,6 +54,7 @@ int register_android_server_SyntheticPasswordManager(JNIEnv* env); int register_android_server_GraphicsStatsService(JNIEnv* env); int register_android_hardware_display_DisplayViewport(JNIEnv* env); int register_android_server_net_NetworkStatsService(JNIEnv* env); +int register_android_server_security_VerityUtils(JNIEnv* env); }; using namespace android; @@ -101,5 +102,6 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) register_android_server_GraphicsStatsService(env); register_android_hardware_display_DisplayViewport(env); register_android_server_net_NetworkStatsService(env); + register_android_server_security_VerityUtils(env); return JNI_VERSION_1_4; }