1#!/usr/bin/env python 2# 3# Copyright 2021 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_metadata_generator` generates fsverity metadata and signature to a 19container file 20 21This actually is a simple wrapper around the `fsverity` program. A file is 22signed by the program which produces the PKCS#7 signature file, merkle tree file 23, and the fsverity_descriptor file. Then the files are packed into a single 24output file so that the information about the signing stays together. 25 26Currently, the output of this script is used by `fd_server` which is the host- 27side backend of an authfs filesystem. `fd_server` uses this file in case when 28the underlying filesystem (ext4, etc.) on the device doesn't support the 29fsverity feature natively in which case the information is read directly from 30the filesystem using ioctl. 31""" 32 33import argparse 34import os 35import re 36import shutil 37import subprocess 38import sys 39import tempfile 40from struct import * 41 42class TempDirectory(object): 43 def __enter__(self): 44 self.name = tempfile.mkdtemp() 45 return self.name 46 47 def __exit__(self, *unused): 48 shutil.rmtree(self.name) 49 50class FSVerityMetadataGenerator: 51 def __init__(self, fsverity_path): 52 self._fsverity_path = fsverity_path 53 54 # Default values for some properties 55 self.set_hash_alg("sha256") 56 self.set_signature('none') 57 58 def set_key_format(self, key_format): 59 self._key_format = key_format 60 61 def set_key(self, key): 62 self._key = key 63 64 def set_cert(self, cert): 65 self._cert = cert 66 67 def set_hash_alg(self, hash_alg): 68 self._hash_alg = hash_alg 69 70 def set_signature(self, signature): 71 self._signature = signature 72 73 def _raw_signature(pkcs7_sig_file): 74 """ Extracts raw signature from DER formatted PKCS#7 detached signature file 75 76 Do that by parsing the ASN.1 tree to get the location of the signature 77 in the file and then read the portion. 78 """ 79 80 # Note: there seems to be no public python API (even in 3p modules) that 81 # provides direct access to the raw signature at this moment. So, `openssl 82 # asn1parse` commandline tool is used instead. 83 cmd = ['openssl', 'asn1parse'] 84 cmd.extend(['-inform', 'DER']) 85 cmd.extend(['-in', pkcs7_sig_file]) 86 out = subprocess.check_output(cmd, universal_newlines=True) 87 88 # The signature is the last element in the tree 89 last_line = out.splitlines()[-1] 90 m = re.search('(\d+):.*hl=\s*(\d+)\s*l=\s*(\d+)\s*.*OCTET STRING', last_line) 91 if not m: 92 raise RuntimeError("Failed to parse asn1parse output: " + out) 93 offset = int(m.group(1)) 94 header_len = int(m.group(2)) 95 size = int(m.group(3)) 96 with open(pkcs7_sig_file, 'rb') as f: 97 f.seek(offset + header_len) 98 return f.read(size) 99 100 def digest(self, input_file): 101 cmd = [self._fsverity_path, 'digest', input_file] 102 cmd.extend(['--compact']) 103 cmd.extend(['--hash-alg', self._hash_alg]) 104 out = subprocess.check_output(cmd, universal_newlines=True).strip() 105 return bytes(bytearray.fromhex(out)) 106 107 def generate(self, input_file, output_file): 108 if self._signature != 'none': 109 if not self._key: 110 raise RuntimeError("key must be specified.") 111 if not self._cert: 112 raise RuntimeError("cert must be specified.") 113 114 with TempDirectory() as temp_dir: 115 self._do_generate(input_file, output_file, temp_dir) 116 117 def _do_generate(self, input_file, output_file, work_dir): 118 # temporary files 119 desc_file = os.path.join(work_dir, 'desc') 120 merkletree_file = os.path.join(work_dir, 'merkletree') 121 sig_file = os.path.join(work_dir, 'signature') 122 123 # run the fsverity util to create the temporary files 124 cmd = [self._fsverity_path] 125 if self._signature == 'none': 126 cmd.append('digest') 127 cmd.append(input_file) 128 else: 129 cmd.append('sign') 130 cmd.append(input_file) 131 cmd.append(sig_file) 132 133 # If key is DER, convert DER private key to PEM 134 if self._key_format == 'der': 135 pem_key = os.path.join(work_dir, 'key.pem') 136 key_cmd = ['openssl', 'pkcs8'] 137 key_cmd.extend(['-inform', 'DER']) 138 key_cmd.extend(['-in', self._key]) 139 key_cmd.extend(['-nocrypt']) 140 key_cmd.extend(['-out', pem_key]) 141 subprocess.check_call(key_cmd) 142 else: 143 pem_key = self._key 144 145 cmd.extend(['--key', pem_key]) 146 cmd.extend(['--cert', self._cert]) 147 cmd.extend(['--hash-alg', self._hash_alg]) 148 cmd.extend(['--block-size', '4096']) 149 cmd.extend(['--out-merkle-tree', merkletree_file]) 150 cmd.extend(['--out-descriptor', desc_file]) 151 subprocess.check_call(cmd, stdout=open(os.devnull, 'w')) 152 153 with open(output_file, 'wb') as out: 154 # 1. version 155 out.write(pack('<I', 1)) 156 157 # 2. fsverity_descriptor 158 with open(desc_file, 'rb') as f: 159 out.write(f.read()) 160 161 # 3. signature 162 SIG_TYPE_NONE = 0 163 SIG_TYPE_PKCS7 = 1 164 SIG_TYPE_RAW = 2 165 if self._signature == 'raw': 166 out.write(pack('<I', SIG_TYPE_RAW)) 167 sig = self._raw_signature(sig_file) 168 out.write(pack('<I', len(sig))) 169 out.write(sig) 170 elif self._signature == 'pkcs7': 171 with open(sig_file, 'rb') as f: 172 out.write(pack('<I', SIG_TYPE_PKCS7)) 173 sig = f.read() 174 out.write(pack('<I', len(sig))) 175 out.write(sig) 176 else: 177 out.write(pack('<I', SIG_TYPE_NONE)) 178 out.write(pack('<I', 0)) 179 180 # 4. merkle tree 181 with open(merkletree_file, 'rb') as f: 182 # merkle tree is placed at the next nearest page boundary to make 183 # mmapping possible 184 out.seek(next_page(out.tell())) 185 out.write(f.read()) 186 187def next_page(n): 188 """ Returns the next nearest page boundary from `n` """ 189 PAGE_SIZE = 4096 190 return (n + PAGE_SIZE - 1) // PAGE_SIZE * PAGE_SIZE 191 192if __name__ == '__main__': 193 p = argparse.ArgumentParser() 194 p.add_argument( 195 '--output', 196 help='output file. If omitted, print to <INPUT>.fsv_meta', 197 metavar='output', 198 default=None) 199 p.add_argument( 200 'input', 201 help='input file to be signed') 202 p.add_argument( 203 '--key-format', 204 choices=['pem', 'der'], 205 default='der', 206 help='format of the input key. Default is der') 207 p.add_argument( 208 '--key', 209 help='PKCS#8 private key file') 210 p.add_argument( 211 '--cert', 212 help='x509 certificate file in PEM format') 213 p.add_argument( 214 '--hash-alg', 215 help='hash algorithm to use to build the merkle tree', 216 choices=['sha256', 'sha512'], 217 default='sha256') 218 p.add_argument( 219 '--signature', 220 help='format for signature', 221 choices=['none', 'raw', 'pkcs7'], 222 default='none') 223 p.add_argument( 224 '--fsverity-path', 225 help='path to the fsverity program', 226 required=True) 227 args = p.parse_args(sys.argv[1:]) 228 229 output_file = args.output 230 if not output_file: 231 output_file = input_file + '.fsv_meta' 232 233 # remove the output file first, as switching between a file and a symlink can be complicated 234 try: 235 os.remove(output_file) 236 except FileNotFoundError: 237 pass 238 239 if os.path.islink(args.input): 240 target = os.readlink(args.input) + '.fsv_meta' 241 os.symlink(target, output_file) 242 sys.exit(0) 243 244 generator = FSVerityMetadataGenerator(args.fsverity_path) 245 generator.set_signature(args.signature) 246 if args.signature == 'none': 247 if args.key or args.cert: 248 raise ValueError("When signature is none, key and cert can't be set") 249 else: 250 if not args.key or not args.cert: 251 raise ValueError("To generate signature, key and cert must be set") 252 generator.set_key(args.key) 253 generator.set_cert(args.cert) 254 generator.set_key_format(args.key_format) 255 generator.set_hash_alg(args.hash_alg) 256 generator.generate(args.input, output_file) 257