aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEric Biggers <ebiggers@google.com>2018-06-14 11:11:47 -0700
committerEric Biggers <ebiggers@google.com>2018-06-14 11:53:02 -0700
commit3a35ec38df2f4a10746436949a2ec4e51d39f3ec (patch)
treeacc610ad4513f3a065bd194e5f6907923363b59b
parent2a7dbea90885dbd1dadc3d4a2873008ae618614e (diff)
downloadfsverity-utils-3a35ec38df2f4a10746436949a2ec4e51d39f3ec.tar.gz
fsveritysetup: support including the signed file measurement
Signed-off-by: Eric Biggers <ebiggers@google.com>
-rwxr-xr-xfsveritysetup211
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)