diff --git a/core/Makefile b/core/Makefile index b26f635cb6..44d4a9e578 100644 --- a/core/Makefile +++ b/core/Makefile @@ -526,6 +526,16 @@ $(foreach kmd,$(BOARD_KERNEL_MODULE_DIRS), \ $(eval ALL_DEFAULT_INSTALLED_MODULES += $(call build-recovery-as-boot-load,$(kmd))),\ $(eval ALL_DEFAULT_INSTALLED_MODULES += $(call build-image-kernel-modules-dir,GENERIC_RAMDISK,$(TARGET_RAMDISK_OUT),,modules.load,,$(kmd))))) +# ----------------------------------------------------------------- +# FSVerity metadata generation +ifeq ($(PRODUCT_SYSTEM_FSVERITY_GENERATE_METADATA),true) + +FSVERITY_APK_KEY_PATH := $(DEFAULT_SYSTEM_DEV_CERTIFICATE) +FSVERITY_APK_OUT := system/etc/security/fsverity/BuildManifest.apk +FSVERITY_APK_MANIFEST_PATH := system/security/fsverity/AndroidManifest.xml + +endif # PRODUCT_SYSTEM_FSVERITY_GENERATE_METADATA + # ----------------------------------------------------------------- # Cert-to-package mapping. Used by the post-build signing tools. # Use a macro to add newline to each echo command @@ -575,6 +585,8 @@ $(APKCERTS_FILE): $(if $(PACKAGES.$(p).EXTERNAL_KEY),\ $(call _apkcerts_write_line,$(PACKAGES.$(p).STEM),EXTERNAL,,$(PACKAGES.$(p).COMPRESSED),$(PACKAGES.$(p).PARTITION),$@),\ $(call _apkcerts_write_line,$(PACKAGES.$(p).STEM),$(PACKAGES.$(p).CERTIFICATE),$(PACKAGES.$(p).PRIVATE_KEY),$(PACKAGES.$(p).COMPRESSED),$(PACKAGES.$(p).PARTITION),$@)))) + $(if $(filter true,$(PRODUCT_SYSTEM_FSVERITY_GENERATE_METADATA)),\ + $(call _apkcerts_write_line,$(notdir $(basename $(FSVERITY_APK_OUT))),$(FSVERITY_APK_KEY_PATH).x509.pem,$(FSVERITY_APK_KEY_PATH).pk8,,system,$@)) # In case value of PACKAGES is empty. $(hide) touch $@ @@ -1672,6 +1684,11 @@ define generate-image-prop-dictionary $(if $(filter $(2),system),\ $(if $(INTERNAL_SYSTEM_OTHER_PARTITION_SIZE),$(hide) echo "system_other_size=$(INTERNAL_SYSTEM_OTHER_PARTITION_SIZE)" >> $(1)) $(if $(PRODUCT_SYSTEM_HEADROOM),$(hide) echo "system_headroom=$(PRODUCT_SYSTEM_HEADROOM)" >> $(1)) + $(if $(filter true,$(PRODUCT_SYSTEM_FSVERITY_GENERATE_METADATA)),$(hide) echo "fsverity=$(HOST_OUT_EXECUTABLES)/fsverity" >> $(1)) + $(if $(filter true,$(PRODUCT_SYSTEM_FSVERITY_GENERATE_METADATA)),$(hide) echo "fsverity_generate_metadata=true" >> $(1)) + $(if $(filter true,$(PRODUCT_SYSTEM_FSVERITY_GENERATE_METADATA)),$(hide) echo "fsverity_apk_key=$(FSVERITY_APK_KEY_PATH)" >> $(1)) + $(if $(filter true,$(PRODUCT_SYSTEM_FSVERITY_GENERATE_METADATA)),$(hide) echo "fsverity_apk_manifest=$(FSVERITY_APK_MANIFEST_PATH)" >> $(1)) + $(if $(filter true,$(PRODUCT_SYSTEM_FSVERITY_GENERATE_METADATA)),$(hide) echo "fsverity_apk_out=$(FSVERITY_APK_OUT)" >> $(1)) $(call add-common-ro-flags-to-image-props,system,$(1)) ) $(if $(filter $(2),system_other),\ @@ -2773,6 +2790,10 @@ endef ifeq ($(BOARD_AVB_ENABLE),true) $(BUILT_SYSTEMIMAGE): $(BOARD_AVB_SYSTEM_KEY_PATH) endif +ifeq ($(PRODUCT_SYSTEM_FSVERITY_GENERATE_METADATA),true) +$(BUILT_SYSTEMIMAGE): $(HOST_OUT_EXECUTABLES)/fsverity $(HOST_OUT_EXECUTABLES)/aapt2 \ + $(FSVERITY_APK_MANIFEST_PATH) $(FSVERITY_APK_KEY_PATH).x509.pem $(FSVERITY_APK_KEY_PATH).pk8 +endif $(BUILT_SYSTEMIMAGE): $(FULL_SYSTEMIMAGE_DEPS) $(INSTALLED_FILES_FILE) $(call build-systemimage-target,$@) diff --git a/core/product.mk b/core/product.mk index 23fb93929a..683c429a7a 100644 --- a/core/product.mk +++ b/core/product.mk @@ -440,6 +440,16 @@ _product_single_value_vars += PRODUCT_INSTALL_EXTRA_FLATTENED_APEXES # This option is only meant to be set by GSI products. _product_single_value_vars += PRODUCT_INSTALL_DEBUG_POLICY_TO_SYSTEM_EXT +# If set, metadata files for the following artifacts will be generated. +# - system/framework/*.jar +# - system/framework/oat//*.{oat,vdex,art} +# - system/etc/boot-image.prof +# - system/etc/dirty-image-objects +# One fsverity metadata container file per one input file will be generated in +# system.img, with a suffix ".fsv_meta". e.g. a container file for +# "/system/framework/foo.jar" will be "system/framework/foo.jar.fsv_meta". +_product_single_value_vars += PRODUCT_SYSTEM_FSVERITY_GENERATE_METADATA + .KATI_READONLY := _product_single_value_vars _product_list_vars _product_var_list :=$= $(_product_single_value_vars) $(_product_list_vars) diff --git a/tools/releasetools/Android.bp b/tools/releasetools/Android.bp index 827aaac8f1..a979a8ece5 100644 --- a/tools/releasetools/Android.bp +++ b/tools/releasetools/Android.bp @@ -50,6 +50,7 @@ python_defaults { ], libs: [ "releasetools_common", + "releasetools_fsverity_metadata_generator", "releasetools_verity_utils", ], required: [ @@ -259,6 +260,16 @@ python_library_host { ], } +python_library_host { + name: "releasetools_fsverity_metadata_generator", + srcs: [ + "fsverity_metadata_generator.py", + ], + libs: [ + "fsverity_digests_proto_python", + ], +} + python_library_host { name: "releasetools_verity_utils", srcs: [ diff --git a/tools/releasetools/build_image.py b/tools/releasetools/build_image.py index 38104affa4..8a5d627b36 100755 --- a/tools/releasetools/build_image.py +++ b/tools/releasetools/build_image.py @@ -24,6 +24,7 @@ Usage: build_image input_directory properties_file output_image \\ from __future__ import print_function +import glob import logging import os import os.path @@ -34,6 +35,9 @@ import sys import common import verity_utils +from fsverity_digests_pb2 import FSVerityDigests +from fsverity_metadata_generator import FSVerityMetadataGenerator + logger = logging.getLogger(__name__) OPTIONS = common.OPTIONS @@ -447,6 +451,68 @@ def BuildImageMkfs(in_dir, prop_dict, out_file, target_out, fs_config): return mkfs_output +def GenerateFSVerityMetadata(in_dir, fsverity_path, apk_key_path, apk_manifest_path, apk_out_path): + """Generates fsverity metadata files. + + By setting PRODUCT_SYSTEM_FSVERITY_GENERATE_METADATA := true, fsverity + metadata files will be generated. For the input files, see `patterns` below. + + One metadata file per one input file will be generated with the suffix + .fsv_meta. e.g. system/framework/foo.jar -> system/framework/foo.jar.fsv_meta + Also a mapping file containing fsverity digests will be generated to + system/etc/security/fsverity/BuildManifest.apk. + + Args: + in_dir: temporary working directory (same as BuildImage) + fsverity_path: path to host tool fsverity + apk_key_path: path to key (e.g. build/make/target/product/security/platform) + apk_manifest_path: path to AndroidManifest.xml for APK + apk_out_path: path to the output APK + + Returns: + None. The files are generated directly under in_dir. + """ + + patterns = [ + "system/framework/*.jar", + "system/framework/oat/*/*.oat", + "system/framework/oat/*/*.vdex", + "system/framework/oat/*/*.art", + "system/etc/boot-image.prof", + "system/etc/dirty-image-objects", + ] + files = [] + for pattern in patterns: + files += glob.glob(os.path.join(in_dir, pattern)) + files = sorted(set(files)) + + generator = FSVerityMetadataGenerator(fsverity_path) + generator.set_hash_alg("sha256") + + digests = FSVerityDigests() + for f in files: + generator.generate(f) + # f is a full path for now; make it relative so it starts with {mount_point}/ + digest = digests.digests[os.path.relpath(f, in_dir)] + digest.digest = generator.digest(f) + digest.hash_alg = "sha256" + + temp_dir = common.MakeTempDir() + + os.mkdir(os.path.join(temp_dir, "assets")) + metadata_path = os.path.join(temp_dir, "assets", "build_manifest") + with open(metadata_path, "wb") as f: + f.write(digests.SerializeToString()) + + apk_path = os.path.join(in_dir, apk_out_path) + + common.RunAndCheckOutput(["aapt2", "link", + "-A", os.path.join(temp_dir, "assets"), + "-o", apk_path, + "--manifest", apk_manifest_path]) + common.RunAndCheckOutput(["apksigner", "sign", "--in", apk_path, + "--cert", apk_key_path + ".x509.pem", + "--key", apk_key_path + ".pk8"]) def BuildImage(in_dir, prop_dict, out_file, target_out=None): """Builds an image for the files under in_dir and writes it to out_file. @@ -475,6 +541,13 @@ def BuildImage(in_dir, prop_dict, out_file, target_out=None): elif fs_type.startswith("f2fs") and prop_dict.get("f2fs_compress") == "true": fs_spans_partition = False + if "fsverity_generate_metadata" in prop_dict: + GenerateFSVerityMetadata(in_dir, + fsverity_path=prop_dict["fsverity"], + apk_key_path=prop_dict["fsverity_apk_key"], + apk_manifest_path=prop_dict["fsverity_apk_manifest"], + apk_out_path=prop_dict["fsverity_apk_out"]) + # Get a builder for creating an image that's to be verified by Verified Boot, # or None if not applicable. verity_image_builder = verity_utils.CreateVerityImageBuilder(prop_dict) @@ -589,7 +662,6 @@ def BuildImage(in_dir, prop_dict, out_file, target_out=None): if verity_image_builder: verity_image_builder.Build(out_file) - def ImagePropFromGlobalDict(glob_dict, mount_point): """Build an image property dictionary from the global dictionary. @@ -725,6 +797,11 @@ def ImagePropFromGlobalDict(glob_dict, mount_point): copy_prop("system_root_image", "system_root_image") copy_prop("root_dir", "root_dir") copy_prop("root_fs_config", "root_fs_config") + copy_prop("fsverity", "fsverity") + copy_prop("fsverity_generate_metadata", "fsverity_generate_metadata") + copy_prop("fsverity_apk_key","fsverity_apk_key") + copy_prop("fsverity_apk_manifest","fsverity_apk_manifest") + copy_prop("fsverity_apk_out","fsverity_apk_out") elif mount_point == "data": # Copy the generic fs type first, override with specific one if available. copy_prop("flash_logical_block_size", "flash_logical_block_size") diff --git a/tools/releasetools/fsverity_metadata_generator.py b/tools/releasetools/fsverity_metadata_generator.py new file mode 100644 index 0000000000..666efd5d61 --- /dev/null +++ b/tools/releasetools/fsverity_metadata_generator.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python +# +# Copyright 2021 Google Inc. All rights reserved. +# +# 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. + +""" +`fsverity_metadata_generator` generates fsverity metadata and signature to a +container file + +This actually is a simple wrapper around the `fsverity` program. A file is +signed by the program which produces the PKCS#7 signature file, merkle tree file +, and the fsverity_descriptor file. Then the files are packed into a single +output file so that the information about the signing stays together. + +Currently, the output of this script is used by `fd_server` which is the host- +side backend of an authfs filesystem. `fd_server` uses this file in case when +the underlying filesystem (ext4, etc.) on the device doesn't support the +fsverity feature natively in which case the information is read directly from +the filesystem using ioctl. +""" + +import argparse +import os +import re +import shutil +import subprocess +import sys +import tempfile +from struct import * + +class TempDirectory(object): + def __enter__(self): + self.name = tempfile.mkdtemp() + return self.name + + def __exit__(self, *unused): + shutil.rmtree(self.name) + +class FSVerityMetadataGenerator: + def __init__(self, fsverity_path): + self._fsverity_path = fsverity_path + + # Default values for some properties + self.set_hash_alg("sha256") + self.set_signature('none') + + def set_key(self, key): + self._key = key + + def set_cert(self, cert): + self._cert = cert + + def set_hash_alg(self, hash_alg): + self._hash_alg = hash_alg + + def set_signature(self, signature): + self._signature = signature + + def _raw_signature(pkcs7_sig_file): + """ Extracts raw signature from DER formatted PKCS#7 detached signature file + + Do that by parsing the ASN.1 tree to get the location of the signature + in the file and then read the portion. + """ + + # Note: there seems to be no public python API (even in 3p modules) that + # provides direct access to the raw signature at this moment. So, `openssl + # asn1parse` commandline tool is used instead. + cmd = ['openssl', 'asn1parse'] + cmd.extend(['-inform', 'DER']) + cmd.extend(['-in', pkcs7_sig_file]) + out = subprocess.check_output(cmd, universal_newlines=True) + + # The signature is the last element in the tree + last_line = out.splitlines()[-1] + m = re.search('(\d+):.*hl=\s*(\d+)\s*l=\s*(\d+)\s*.*OCTET STRING', last_line) + if not m: + raise RuntimeError("Failed to parse asn1parse output: " + out) + offset = int(m.group(1)) + header_len = int(m.group(2)) + size = int(m.group(3)) + with open(pkcs7_sig_file, 'rb') as f: + f.seek(offset + header_len) + return f.read(size) + + def digest(self, input_file): + cmd = [self._fsverity_path, 'digest', input_file] + cmd.extend(['--compact']) + cmd.extend(['--hash-alg', self._hash_alg]) + out = subprocess.check_output(cmd, universal_newlines=True).strip() + return bytes(bytearray.fromhex(out)) + + def generate(self, input_file, output_file=None): + if self._signature != 'none': + if not self._key: + raise RuntimeError("key must be specified.") + if not self._cert: + raise RuntimeError("cert must be specified.") + + if not output_file: + output_file = input_file + '.fsv_meta' + + with TempDirectory() as temp_dir: + self._do_generate(input_file, output_file, temp_dir) + + def _do_generate(self, input_file, output_file, work_dir): + # temporary files + desc_file = os.path.join(work_dir, 'desc') + merkletree_file = os.path.join(work_dir, 'merkletree') + sig_file = os.path.join(work_dir, 'signature') + + # run the fsverity util to create the temporary files + cmd = [self._fsverity_path] + if self._signature == 'none': + cmd.append('digest') + cmd.append(input_file) + else: + cmd.append('sign') + cmd.append(input_file) + cmd.append(sig_file) + + # convert DER private key to PEM + pem_key = os.path.join(work_dir, 'key.pem') + key_cmd = ['openssl', 'pkcs8'] + key_cmd.extend(['-inform', 'DER']) + key_cmd.extend(['-in', self._key]) + key_cmd.extend(['-nocrypt']) + key_cmd.extend(['-out', pem_key]) + subprocess.check_call(key_cmd) + + cmd.extend(['--key', pem_key]) + cmd.extend(['--cert', self._cert]) + cmd.extend(['--hash-alg', self._hash_alg]) + cmd.extend(['--block-size', '4096']) + cmd.extend(['--out-merkle-tree', merkletree_file]) + cmd.extend(['--out-descriptor', desc_file]) + subprocess.check_call(cmd, stdout=open(os.devnull, 'w')) + + with open(output_file, 'wb') as out: + # 1. version + out.write(pack('