Implement fsverity metadata generator
Using fsverity tool, fsverity metadata for specific artifacts in system
mage can be generated. Users can do that by setting a makefile variable
PRODUCT_SYSTEM_FSVERITY_GENERATE_METADATA to true.
If set to true, the following artifacts will be signed.
- system/framework/*.jar
- system/framework/oat/<arch>/*.{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".
Bug: 193113311
Test: build with PRODUCT_SYSTEM_FSVERITY_GENERATE_METADATA := true
Change-Id: Ib70d591a72d23286b5debcb05fbad799dfd79b94
diff --git a/tools/releasetools/Android.bp b/tools/releasetools/Android.bp
index 827aaac..4e628cf 100644
--- a/tools/releasetools/Android.bp
+++ b/tools/releasetools/Android.bp
@@ -50,6 +50,7 @@
],
libs: [
"releasetools_common",
+ "releasetools_fsverity_metadata_generator",
"releasetools_verity_utils",
],
required: [
@@ -260,6 +261,16 @@
}
python_library_host {
+ name: "releasetools_fsverity_metadata_generator",
+ srcs: [
+ "fsverity_metadata_generator.py",
+ ],
+ required: [
+ "fsverity",
+ ],
+}
+
+python_library_host {
name: "releasetools_verity_utils",
srcs: [
"verity_utils.py",
diff --git a/tools/releasetools/build_image.py b/tools/releasetools/build_image.py
index 38104af..8e8bcde 100755
--- a/tools/releasetools/build_image.py
+++ b/tools/releasetools/build_image.py
@@ -24,6 +24,7 @@
from __future__ import print_function
+import glob
import logging
import os
import os.path
@@ -34,6 +35,8 @@
import common
import verity_utils
+from fsverity_metadata_generator import FSVerityMetadataGenerator
+
logger = logging.getLogger(__name__)
OPTIONS = common.OPTIONS
@@ -475,6 +478,24 @@
elif fs_type.startswith("f2fs") and prop_dict.get("f2fs_compress") == "true":
fs_spans_partition = False
+ if "fsverity_generate_metadata" in prop_dict:
+ 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(prop_dict["fsverity"])
+ for f in files:
+ generator.generate(f)
+
# 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 +610,6 @@
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 +745,8 @@
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")
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 0000000..439e484
--- /dev/null
+++ b/tools/releasetools/fsverity_metadata_generator.py
@@ -0,0 +1,224 @@
+#!/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 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('<I', 1))
+
+ # 2. fsverity_descriptor
+ with open(desc_file, 'rb') as f:
+ out.write(f.read())
+
+ # 3. signature
+ SIG_TYPE_NONE = 0
+ SIG_TYPE_PKCS7 = 1
+ SIG_TYPE_RAW = 2
+ if self._signature == 'raw':
+ out.write(pack('<I', SIG_TYPE_RAW))
+ sig = self._raw_signature(sig_file)
+ out.write(pack('<I', len(sig)))
+ out.write(sig)
+ elif self._signature == 'pkcs7':
+ with open(sig_file, 'rb') as f:
+ out.write(pack('<I', SIG_TYPE_PKCS7))
+ sig = f.read()
+ out.write(pack('<I', len(sig)))
+ out.write(sig)
+ else:
+ out.write(pack('<I', SIG_TYPE_NONE))
+
+ # 4. merkle tree
+ with open(merkletree_file, 'rb') as f:
+ # merkle tree is placed at the next nearest page boundary to make
+ # mmapping possible
+ out.seek(next_page(out.tell()))
+ out.write(f.read())
+
+def next_page(n):
+ """ Returns the next nearest page boundary from `n` """
+ PAGE_SIZE = 4096
+ return (n + PAGE_SIZE - 1) // PAGE_SIZE * PAGE_SIZE
+
+if __name__ == '__main__':
+ p = argparse.ArgumentParser()
+ p.add_argument(
+ '--output',
+ help='output file. If omitted, print to <INPUT>.fsv_meta',
+ metavar='output',
+ default=None)
+ p.add_argument(
+ 'input',
+ help='input file to be signed')
+ p.add_argument(
+ '--key',
+ help='PKCS#8 private key file in DER format')
+ p.add_argument(
+ '--cert',
+ help='x509 certificate file in PEM format')
+ p.add_argument(
+ '--hash-alg',
+ help='hash algorithm to use to build the merkle tree',
+ choices=['sha256', 'sha512'],
+ default='sha256')
+ p.add_argument(
+ '--signature',
+ help='format for signature',
+ choices=['none', 'raw', 'pkcs7'],
+ default='none')
+ p.add_argument(
+ '--fsverity-path',
+ help='path to the fsverity program',
+ required=True)
+ args = p.parse_args(sys.argv[1:])
+
+ generator = FSVerityMetadataGenerator(args.fsverity_path)
+ generator.set_signature(args.signature)
+ if args.signature == 'none':
+ if args.key or args.cert:
+ raise ValueError("When signature is none, key and cert can't be set")
+ else:
+ if not args.key or not args.cert:
+ raise ValueError("To generate signature, key and cert must be set")
+ generator.set_key(args.key)
+ generator.set_cert(args.cert)
+ generator.set_hash_alg(args.hash_alg)
+ generator.generate(args.input, args.output)