blob: 666efd5d61d071410e071fdafea7f1462ae22847 [file] [log] [blame]
Inseob Kim9cda3972021-10-12 22:59:12 +09001#!/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(self, key):
59 self._key = key
60
61 def set_cert(self, cert):
62 self._cert = cert
63
64 def set_hash_alg(self, hash_alg):
65 self._hash_alg = hash_alg
66
67 def set_signature(self, signature):
68 self._signature = signature
69
70 def _raw_signature(pkcs7_sig_file):
71 """ Extracts raw signature from DER formatted PKCS#7 detached signature file
72
73 Do that by parsing the ASN.1 tree to get the location of the signature
74 in the file and then read the portion.
75 """
76
77 # Note: there seems to be no public python API (even in 3p modules) that
78 # provides direct access to the raw signature at this moment. So, `openssl
79 # asn1parse` commandline tool is used instead.
80 cmd = ['openssl', 'asn1parse']
81 cmd.extend(['-inform', 'DER'])
82 cmd.extend(['-in', pkcs7_sig_file])
83 out = subprocess.check_output(cmd, universal_newlines=True)
84
85 # The signature is the last element in the tree
86 last_line = out.splitlines()[-1]
87 m = re.search('(\d+):.*hl=\s*(\d+)\s*l=\s*(\d+)\s*.*OCTET STRING', last_line)
88 if not m:
89 raise RuntimeError("Failed to parse asn1parse output: " + out)
90 offset = int(m.group(1))
91 header_len = int(m.group(2))
92 size = int(m.group(3))
93 with open(pkcs7_sig_file, 'rb') as f:
94 f.seek(offset + header_len)
95 return f.read(size)
96
Inseob Kimf69346e2021-10-13 15:16:33 +090097 def digest(self, input_file):
98 cmd = [self._fsverity_path, 'digest', input_file]
99 cmd.extend(['--compact'])
100 cmd.extend(['--hash-alg', self._hash_alg])
101 out = subprocess.check_output(cmd, universal_newlines=True).strip()
102 return bytes(bytearray.fromhex(out))
103
Inseob Kim9cda3972021-10-12 22:59:12 +0900104 def generate(self, input_file, output_file=None):
105 if self._signature != 'none':
106 if not self._key:
107 raise RuntimeError("key must be specified.")
108 if not self._cert:
109 raise RuntimeError("cert must be specified.")
110
111 if not output_file:
112 output_file = input_file + '.fsv_meta'
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 # convert DER private key to PEM
134 pem_key = os.path.join(work_dir, 'key.pem')
135 key_cmd = ['openssl', 'pkcs8']
136 key_cmd.extend(['-inform', 'DER'])
137 key_cmd.extend(['-in', self._key])
138 key_cmd.extend(['-nocrypt'])
139 key_cmd.extend(['-out', pem_key])
140 subprocess.check_call(key_cmd)
141
142 cmd.extend(['--key', pem_key])
143 cmd.extend(['--cert', self._cert])
144 cmd.extend(['--hash-alg', self._hash_alg])
145 cmd.extend(['--block-size', '4096'])
146 cmd.extend(['--out-merkle-tree', merkletree_file])
147 cmd.extend(['--out-descriptor', desc_file])
148 subprocess.check_call(cmd, stdout=open(os.devnull, 'w'))
149
150 with open(output_file, 'wb') as out:
151 # 1. version
152 out.write(pack('<I', 1))
153
154 # 2. fsverity_descriptor
155 with open(desc_file, 'rb') as f:
156 out.write(f.read())
157
158 # 3. signature
159 SIG_TYPE_NONE = 0
160 SIG_TYPE_PKCS7 = 1
161 SIG_TYPE_RAW = 2
162 if self._signature == 'raw':
163 out.write(pack('<I', SIG_TYPE_RAW))
164 sig = self._raw_signature(sig_file)
165 out.write(pack('<I', len(sig)))
166 out.write(sig)
167 elif self._signature == 'pkcs7':
168 with open(sig_file, 'rb') as f:
169 out.write(pack('<I', SIG_TYPE_PKCS7))
170 sig = f.read()
171 out.write(pack('<I', len(sig)))
172 out.write(sig)
173 else:
174 out.write(pack('<I', SIG_TYPE_NONE))
175
176 # 4. merkle tree
177 with open(merkletree_file, 'rb') as f:
178 # merkle tree is placed at the next nearest page boundary to make
179 # mmapping possible
180 out.seek(next_page(out.tell()))
181 out.write(f.read())
182
183def next_page(n):
184 """ Returns the next nearest page boundary from `n` """
185 PAGE_SIZE = 4096
186 return (n + PAGE_SIZE - 1) // PAGE_SIZE * PAGE_SIZE
187
188if __name__ == '__main__':
189 p = argparse.ArgumentParser()
190 p.add_argument(
191 '--output',
192 help='output file. If omitted, print to <INPUT>.fsv_meta',
193 metavar='output',
194 default=None)
195 p.add_argument(
196 'input',
197 help='input file to be signed')
198 p.add_argument(
199 '--key',
200 help='PKCS#8 private key file in DER format')
201 p.add_argument(
202 '--cert',
203 help='x509 certificate file in PEM format')
204 p.add_argument(
205 '--hash-alg',
206 help='hash algorithm to use to build the merkle tree',
207 choices=['sha256', 'sha512'],
208 default='sha256')
209 p.add_argument(
210 '--signature',
211 help='format for signature',
212 choices=['none', 'raw', 'pkcs7'],
213 default='none')
214 p.add_argument(
215 '--fsverity-path',
216 help='path to the fsverity program',
217 required=True)
218 args = p.parse_args(sys.argv[1:])
219
220 generator = FSVerityMetadataGenerator(args.fsverity_path)
221 generator.set_signature(args.signature)
222 if args.signature == 'none':
223 if args.key or args.cert:
224 raise ValueError("When signature is none, key and cert can't be set")
225 else:
226 if not args.key or not args.cert:
227 raise ValueError("To generate signature, key and cert must be set")
228 generator.set_key(args.key)
229 generator.set_cert(args.cert)
230 generator.set_hash_alg(args.hash_alg)
231 generator.generate(args.input, args.output)