diff options
author | Eric Biggers <ebiggers@google.com> | 2018-06-14 11:11:47 -0700 |
---|---|---|
committer | Eric Biggers <ebiggers@google.com> | 2018-06-14 11:53:02 -0700 |
commit | 3a35ec38df2f4a10746436949a2ec4e51d39f3ec (patch) | |
tree | acc610ad4513f3a065bd194e5f6907923363b59b | |
parent | 2a7dbea90885dbd1dadc3d4a2873008ae618614e (diff) | |
download | fsverity-utils-3a35ec38df2f4a10746436949a2ec4e51d39f3ec.tar.gz |
fsveritysetup: support including the signed file measurement
Signed-off-by: Eric Biggers <ebiggers@google.com>
-rwxr-xr-x | fsveritysetup | 211 |
1 files changed, 163 insertions, 48 deletions
diff --git a/fsveritysetup b/fsveritysetup index 6d629e1..1e753b4 100755 --- a/fsveritysetup +++ b/fsveritysetup @@ -19,8 +19,12 @@ DATA_BLOCK_SIZE = 4096 HASH_BLOCK_SIZE = 4096 FS_VERITY_MAGIC = b'TrueBrew' FS_VERITY_SALT_SIZE = 8 + FS_VERITY_EXT_ELIDE = 1 FS_VERITY_EXT_PATCH = 2 +FS_VERITY_EXT_SALT = 3 +FS_VERITY_EXT_PKCS7_SIGNATURE = 4 + FS_VERITY_ALG_SHA256 = 1 FS_VERITY_ALG_CRC32 = 2 @@ -54,6 +58,7 @@ class CRC32Hash(object): class HashAlgorithm(object): + """A hash algorithm supported by fs-verity""" def __init__(self, code, name, digest_size): self.code = code @@ -76,8 +81,8 @@ HASH_ALGORITHMS = [ class fsverity_footer(ctypes.LittleEndianStructure): _fields_ = [ ('magic', ctypes.c_char * 8), # - ('maj_version', ctypes.c_uint8), - ('min_version', ctypes.c_uint8), + ('major_version', ctypes.c_uint8), + ('minor_version', ctypes.c_uint8), ('log_blocksize', ctypes.c_uint8), ('log_arity', ctypes.c_uint8), ('meta_algorithm', ctypes.c_uint16), @@ -114,6 +119,16 @@ class fsverity_extension_elide(ctypes.LittleEndianStructure): ] +class fsverity_measurement(ctypes.LittleEndianStructure): + _fields_ = [ + ('digest_algorithm', ctypes.c_uint16), # + ('digest_size', ctypes.c_uint16), + ('reserved1', ctypes.c_uint32), + ('reserved2', ctypes.c_uint64 * 3) + # followed by variable-length 'digest' + ] + + class FooterOffset(ctypes.LittleEndianStructure): _fields_ = [('ftr_offset', ctypes.c_uint32)] @@ -139,7 +154,7 @@ def copy(src, dst): def pad_to_block_boundary(f): - """Pads the file with zeroes to data block boundary.""" + """Pads the file with zeroes to the next data block boundary.""" f.write(b'\0' * (-f.tell() % DATA_BLOCK_SIZE)) @@ -191,6 +206,29 @@ def veritysetup(data_filename, tree_filename, salt, algorithm): class Extension(object): + """An fs-verity extension item.""" + + def serialize(self): + type_buf = self._serialize_impl() + hdr = fsverity_extension() + hdr.length = ctypes.sizeof(hdr) + len(type_buf) + hdr.type = self.TYPE_CODE + pad = -len(type_buf) % 8 + return serialize_struct(hdr) + type_buf + (b'\0' * pad) + + +class PKCS7SignatureExtension(Extension): + + TYPE_CODE = FS_VERITY_EXT_PKCS7_SIGNATURE + + def __init__(self, pkcs7_msg): + self.pkcs7_msg = pkcs7_msg + + def _serialize_impl(self): + return self.pkcs7_msg + + +class DataExtension(Extension): """An fs-verity patch or elide extension.""" def __init__(self, offset, length): @@ -205,20 +243,12 @@ class Extension(object): if self.offset < 0: raise ValueError('offset cannot be negative (got {})'.format(self.offset)) - def serialize(self): - type_buf = self._serialize_impl() - hdr = fsverity_extension() - pad = -len(type_buf) % 8 - hdr.length = ctypes.sizeof(hdr) + len(type_buf) - hdr.type = self.TYPE_CODE - return serialize_struct(hdr) + type_buf + (b'\0' * pad) - def __str__(self): return '{}(offset {}, length {})'.format(self.__class__.__name__, self.offset, self.length) -class ElideExtension(Extension): +class ElideExtension(DataExtension): """An fs-verity elide extension.""" TYPE_CODE = FS_VERITY_EXT_ELIDE @@ -226,7 +256,7 @@ class ElideExtension(Extension): MAX_LENGTH = (1 << 64) - 1 def __init__(self, offset, length): - Extension.__init__(self, offset, length) + DataExtension.__init__(self, offset, length) def apply(self, out_file): pass @@ -238,7 +268,7 @@ class ElideExtension(Extension): return serialize_struct(ext) -class PatchExtension(Extension): +class PatchExtension(DataExtension): """An fs-verity patch extension.""" TYPE_CODE = FS_VERITY_EXT_PATCH @@ -246,7 +276,7 @@ class PatchExtension(Extension): MAX_LENGTH = 255 def __init__(self, offset, data): - Extension.__init__(self, offset, len(data)) + DataExtension.__init__(self, offset, len(data)) self.data = data def apply(self, dst): @@ -273,27 +303,31 @@ class FSVerityGenerator(object): self.algorithm = algorithm assert len(salt) == FS_VERITY_SALT_SIZE - self.extensions = kwargs.get('extensions') - if self.extensions is None: - self.extensions = [] + self.patch_elide_exts = kwargs.get('patch_elide_exts') + if self.patch_elide_exts is None: + self.patch_elide_exts = [] self.builtin_veritysetup = kwargs.get('builtin_veritysetup') if self.builtin_veritysetup is None: self.builtin_veritysetup = False + self.signing_key_file = kwargs.get('signing_key_file') + self.signature_file = kwargs.get('signature_file') + self.tmp_filenames = [] # Patches and elisions must be within the file size and must not overlap. - self.extensions = sorted(self.extensions, key=lambda ext: ext.offset) - for i, ext in enumerate(self.extensions): + self.patch_elide_exts = sorted( + self.patch_elide_exts, key=lambda ext: ext.offset) + for i, ext in enumerate(self.patch_elide_exts): ext_end = ext.offset + ext.length if ext_end > self.original_size: raise BadExtensionListError( '{} extends beyond end of file!'.format(ext)) - if i + 1 < len( - self.extensions) and ext_end > self.extensions[i + 1].offset: + if i + 1 < len(self.patch_elide_exts + ) and ext_end > self.patch_elide_exts[i + 1].offset: raise BadExtensionListError('{} overlaps {}!'.format( - ext, self.extensions[i + 1])) + ext, self.patch_elide_exts[i + 1])) def _open_tmpfile(self, mode): f = tempfile.NamedTemporaryFile(mode, delete=False) @@ -304,11 +338,12 @@ class FSVerityGenerator(object): for filename in self.tmp_filenames: os.unlink(filename) - def _apply_extensions(self, data_filename): + def _apply_patch_elide_extensions(self, data_filename): + """Apply patch and elide extensions.""" with open(data_filename, 'rb') as src: with self._open_tmpfile('wb') as dst: src_pos = 0 - for ext in self.extensions: + for ext in self.patch_elide_exts: print('Applying {}'.format(ext)) copy_bytes(src, dst, ext.offset - src_pos) ext.apply(dst) @@ -334,8 +369,8 @@ class FSVerityGenerator(object): # If there are any patch or elide extensions, apply them to a temporary file # and use that to build the Merkle tree instead of the original data. - if self.extensions: - data_filename = self._apply_extensions(data_filename) + if self.patch_elide_exts: + data_filename = self._apply_patch_elide_extensions(data_filename) # Pad to a data block boundary before building the Merkle tree. # Note: elisions may result in padding being needed, even if the original @@ -381,17 +416,63 @@ class FSVerityGenerator(object): footer = fsverity_footer() assert ctypes.sizeof(footer) == 64 footer.magic = FS_VERITY_MAGIC - footer.maj_version = 1 - footer.min_version = 0 + footer.major_version = 1 + footer.minor_version = 0 footer.log_blocksize = ilog2(DATA_BLOCK_SIZE) footer.log_arity = ilog2(DATA_BLOCK_SIZE / self.algorithm.digest_size) footer.meta_algorithm = self.algorithm.code footer.data_algorithm = self.algorithm.code footer.size = self.original_size - footer.authenticated_ext_count = len(self.extensions) + footer.authenticated_ext_count = len(self.patch_elide_exts) + footer.unauthenticated_ext_count = 0 + if self.signing_key_file or self.signature_file: + footer.unauthenticated_ext_count += 1 footer.salt = self.salt return serialize_struct(footer) + def _sign_measurement(self, measurement): + """Sign the file's measurement using the given signing_key_file.""" + m = fsverity_measurement() + m.digest_algorithm = self.algorithm.code + m.digest_size = self.algorithm.digest_size + data_to_sign = serialize_struct(m) + binascii.unhexlify(measurement) + + with self._open_tmpfile('wb') as f: + f.write(data_to_sign) + data_to_sign_file = f.name + + with self._open_tmpfile('wb') as f: + pkcs7_msg_file = f.name + + cmd = [ + 'openssl', # + 'smime', + '-sign', + '-in', + data_to_sign_file, + '-signer', + self.signing_key_file, + '-inform', + 'pem', + '-md', + self.algorithm.name, + '-out', + pkcs7_msg_file, + '-outform', + 'der', + '-binary', + '-nodetach', + '-noattr' + ] + + print(' '.join(cmd)) + subprocess.check_call(cmd) + + with open(pkcs7_msg_file, 'rb') as f: + pkcs7_msg = f.read() + + return pkcs7_msg + def generate(self): """Sets up a file for fs-verity. @@ -423,28 +504,47 @@ class FSVerityGenerator(object): with open(tree_filename, 'rb') as treefile: copy(treefile, outfile) - # Append the fixed-size portion of the fs-verity footer. + # Generate the fixed-size portion of the fs-verity footer. footer = self._generate_footer() - outfile.write(footer) - # Append extension items, if any. - extensions = bytearray() - for ext in self.extensions: - extensions += ext.serialize() - outfile.write(extensions) - - # Finish the output file by writing the footer offset field. - ftr_offset = FooterOffset() - ftr_offset.ftr_offset = len(footer) + len(extensions) + ctypes.sizeof( - ftr_offset) - outfile.write(serialize_struct(ftr_offset)) + # Generate authenticated extension items, if any. + auth_extensions = bytearray() + for ext in self.patch_elide_exts: + auth_extensions += ext.serialize() # Compute the fs-verity measurement. measurement = self.algorithm.create() measurement.update(footer) - measurement.update(extensions) + measurement.update(auth_extensions) measurement.update(binascii.unhexlify(root_hash)) measurement = measurement.hexdigest() + + # Generate unauthenticated extension items, if any. + unauth_extensions = bytearray() + + pkcs7_msg = None + if self.signing_key_file: + pkcs7_msg = self._sign_measurement(measurement) + if self.signature_file: + with open(self.signature_file, 'wb') as f: + f.write(pkcs7_msg) + print('Wrote signed file measurement to "{}"'.format( + self.signature_file)) + elif self.signature_file: + with open(self.signature_file, 'rb') as f: + pkcs7_msg = f.read() + if pkcs7_msg: + unauth_extensions += PKCS7SignatureExtension(pkcs7_msg).serialize() + + # Write the footer to the output file. + outfile.write(footer) + outfile.write(auth_extensions) + outfile.write(unauth_extensions) + ftr_offset = FooterOffset() + ftr_offset.ftr_offset = len(footer) + len(auth_extensions) + len( + unauth_extensions) + ctypes.sizeof(ftr_offset) + outfile.write(serialize_struct(ftr_offset)) + finally: self._delete_tmpfiles() @@ -472,6 +572,7 @@ def convert_salt_argument(argstring): def convert_patch_argument(argstring): + """Parse a --patch argument into a PatchExtension.""" try: (offset, patchfile) = argstring.split(',') offset = int(offset) @@ -488,6 +589,7 @@ def convert_patch_argument(argstring): def convert_elide_argument(argstring): + """Parse an --elide argument into an ElideExtension.""" try: (offset, length) = argstring.split(',') offset = int(offset) @@ -533,7 +635,7 @@ def parse_args(): metavar='<offset,patchfile>', type=convert_patch_argument, action='append', - dest='extensions', + dest='patch_elide_exts', help="""Add a patch extension (not recommended). Data in the region beginning at <offset> in the original file and continuing for filesize(<patchfile>) bytes will be replaced with the contents of @@ -544,7 +646,7 @@ def parse_args(): metavar='<offset,length>', type=convert_elide_argument, action='append', - dest='extensions', + dest='patch_elide_exts', help="""Add an elide extension (not recommended). Data in the region beginning at <offset> in the original file and continuing for <length> bytes will not be verified.""") @@ -555,6 +657,17 @@ def parse_args(): help="""Use the built-in Merkle tree generation algorithm rather than invoking the external veritysetup program. They should produce the same result.""") + parser.add_argument( + '--signing-key', + metavar='<signing_key_file>', + type=str, + help='File containing signing key in PEM format') + parser.add_argument( + '--signature', + metavar='<signature_file>', + type=str, + help="""File containing signed measurement in PKCS#7 DER format. This is + an output file if --signing-key is given, or an input file otherwise.""") return parser.parse_args() @@ -566,8 +679,10 @@ def main(): args.out_filename, args.salt, args.hash, - extensions=args.extensions, - builtin_veritysetup=args.builtin_veritysetup) + patch_elide_exts=args.patch_elide_exts, + builtin_veritysetup=args.builtin_veritysetup, + signing_key_file=args.signing_key, + signature_file=args.signature) except BadExtensionListError as e: sys.stderr.write('ERROR: {}\n'.format(e)) sys.exit(1) |