1#!/usr/bin/env python3 2# 3# Copyright 2022 Google Inc. All rights reserved. 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17""" 18`fsverity_manifest_generator` generates the a manifest file containing digests 19of target files. 20""" 21 22import argparse 23import os 24import subprocess 25import sys 26from fsverity_digests_pb2 import FSVerityDigests 27 28HASH_ALGORITHM = 'sha256' 29 30def _digest(fsverity_path, input_file): 31 cmd = [fsverity_path, 'digest', input_file] 32 cmd.extend(['--compact']) 33 cmd.extend(['--hash-alg', HASH_ALGORITHM]) 34 out = subprocess.check_output(cmd, text=True).strip() 35 return bytes(bytearray.fromhex(out)) 36 37if __name__ == '__main__': 38 p = argparse.ArgumentParser(fromfile_prefix_chars='@') 39 p.add_argument( 40 '--output', 41 help='Path to the output manifest', 42 required=True) 43 p.add_argument( 44 '--fsverity-path', 45 help='path to the fsverity program', 46 required=True) 47 p.add_argument( 48 '--base-dir', 49 help='directory to use as a relative root for the inputs. Also see the documentation of ' 50 'inputs') 51 p.add_argument( 52 'inputs', 53 nargs='*', 54 help='input file for the build manifest. It can be in either of two forms: <file> or ' 55 '<file>,<path_on_device>. If the first form is used, --base-dir must be provided, and the ' 56 'path on device will be the filepath relative to the base dir') 57 args = p.parse_args() 58 59 links = {} 60 digests = FSVerityDigests() 61 for f in sorted(args.inputs): 62 if args.base_dir: 63 # f is a full path for now; make it relative so it starts with {mount_point}/ 64 rel = os.path.relpath(f, args.base_dir) 65 else: 66 parts = f.split(',') 67 if len(parts) != 2 or not parts[0] or not parts[1]: 68 sys.exit("Since --base-path wasn't provided, all inputs must be pairs separated by commas " 69 "but this input wasn't: " + f) 70 f, rel = parts 71 72 # Some fsv_meta files are links to other ones. Don't read through the link, because the 73 # layout of files in the build system may not match the layout of files on the device. 74 # Instead, read its target and use it to copy the digest from the real file after all files 75 # are processed. 76 if os.path.islink(f): 77 links[rel] = os.path.normpath(os.path.join(os.path.dirname(rel), os.readlink(f))) 78 else: 79 digest = digests.digests[rel] 80 digest.digest = _digest(args.fsverity_path, f) 81 digest.hash_alg = HASH_ALGORITHM 82 83 for link_rel, real_rel in links.items(): 84 if real_rel not in digests.digests: 85 sys.exit(f'There was a fsv_meta symlink to {real_rel}, but that file was not a fsv_meta file') 86 link_digest = digests.digests[link_rel] 87 real_digest = digests.digests[real_rel] 88 link_digest.CopyFrom(real_digest) 89 90 # Serialize with deterministic=True for reproducible builds and build caching. 91 # The serialized contents will still change across different versions of protobuf. 92 manifest = digests.SerializeToString(deterministic=True) 93 94 with open(args.output, "wb") as f: 95 f.write(manifest) 96