There is support in kiwi to use dm_integrity in combination
with the LUKS header and dm_crypt. However there is also the
use case to setup dm_integrity in standalone mode. This commit
allows to create the dm_integrity layer outside of LUKS using
/etc/integritytab to activate the map through a systemd
generator if systemd is used.
Regarding systemd it's required to use a version of system which
provides: system-generators/systemd-integritysetup-generator.
If this generator does not exist in the distribution it will
also be missing in the dracut generated initrd and the boot
will not be able to succeed. It's mentioned here because even
newer distributions might be missing the generator
Along with the implementation there are two new optional
attributes in the <type> section:
standalone_integrity="true|false"
embed_integrity_metadata="true|false"
standalone_integrity activates/deactivates the dm_integrity map
on top of the root filesystem. Similar to the veritysetup support
there is the opportunity to create an embedded magic metadata
block at the end of the device containing the root
filesystem via embed_integrity_metadata
247 lines
9.0 KiB
Python
247 lines
9.0 KiB
Python
# Copyright (c) 2022 Marcus Schäfer. All rights reserved.
|
|
#
|
|
# This file is part of kiwi.
|
|
#
|
|
# kiwi is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# kiwi is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with kiwi. If not, see <http://www.gnu.org/licenses/>
|
|
#
|
|
import os
|
|
from typing import (
|
|
Optional, Dict, IO
|
|
)
|
|
|
|
# project
|
|
import kiwi.defaults as defaults
|
|
|
|
from kiwi.utils.signature import Signature
|
|
from kiwi.utils.temporary import Temporary
|
|
from kiwi.command import Command
|
|
from kiwi.utils.block import BlockID
|
|
|
|
from kiwi.exceptions import KiwiOffsetError
|
|
|
|
|
|
class VeritySetup:
|
|
"""
|
|
**Create block level verification data on file or device**
|
|
"""
|
|
def __init__(
|
|
self, image_filepath: str, data_blocks: Optional[int] = None,
|
|
hash_offset: int = 0
|
|
) -> None:
|
|
"""
|
|
Construct new VeritySetup
|
|
|
|
:param str image_filepath: block device node or filename
|
|
:param int data_blocks:
|
|
Number of blocks to verify, if not provided the whole
|
|
image_filepath is used
|
|
:param int hash_offset:
|
|
Optional offset to start writing verity hash.
|
|
If not specified it is assumed image_filepath
|
|
is a file and not a block special such that the
|
|
offset is calculated from the size of the file
|
|
"""
|
|
self.image_filepath = image_filepath
|
|
self.data_blocks = data_blocks
|
|
self.verity_hash_offset = \
|
|
hash_offset or os.path.getsize(self.image_filepath)
|
|
self.verity_dict: Dict[str, str] = {}
|
|
self.verification_metadata_file: Optional[IO[bytes]] = None
|
|
|
|
def format(self) -> Dict[str, str]:
|
|
"""
|
|
Run veritysetup on the given device node or filename
|
|
and store the verification information onto the same
|
|
device node or filename
|
|
|
|
:return: veritysetup result information in key/value format
|
|
|
|
:rtype: dict
|
|
"""
|
|
verity_call = Command.run(
|
|
[
|
|
'veritysetup', 'format',
|
|
self.image_filepath, self.image_filepath,
|
|
'--no-superblock',
|
|
f'--hash-offset={self.verity_hash_offset}',
|
|
f'--hash-block-size={defaults.VERITY_HASH_BLOCKSIZE}'
|
|
] + (
|
|
[
|
|
f'--data-blocks={self.data_blocks}',
|
|
f'--data-block-size={defaults.VERITY_DATA_BLOCKSIZE}'
|
|
] if self.data_blocks else []
|
|
)
|
|
)
|
|
for line in verity_call.output.split(os.linesep):
|
|
try:
|
|
# strip out any space, tabs, newlines
|
|
line = ''.join(line.split())
|
|
# split out key:value based data
|
|
(key, value) = line.split(':', 2)
|
|
self.verity_dict[key] = value
|
|
except ValueError:
|
|
# ignore any occurrence for which split failed
|
|
pass
|
|
return self.verity_dict
|
|
|
|
def get_hash_byte_size(self) -> int:
|
|
"""
|
|
Run veritysetup into a temporary file to estimate
|
|
the required bytesize
|
|
|
|
:return: a byte value
|
|
|
|
:rtype: int
|
|
"""
|
|
temp_file = Temporary().new_file()
|
|
Command.run(
|
|
[
|
|
'veritysetup', 'format',
|
|
self.image_filepath, temp_file.name,
|
|
'--no-superblock',
|
|
f'--hash-block-size={defaults.VERITY_HASH_BLOCKSIZE}'
|
|
] + (
|
|
[
|
|
f'--data-blocks={self.data_blocks}',
|
|
f'--data-block-size={defaults.VERITY_DATA_BLOCKSIZE}'
|
|
] if self.data_blocks else []
|
|
)
|
|
)
|
|
return os.path.getsize(temp_file.name)
|
|
|
|
def get_block_storage_filesystem(self) -> str:
|
|
"""
|
|
Retrieve filesystem type from image_filepath. The method
|
|
only returns a value if image_filepath at construction
|
|
time of the VeritySetup object is a block device containing
|
|
a filesystem
|
|
|
|
:rtype: blkid TYPE value or empty string
|
|
|
|
:return: str
|
|
"""
|
|
try:
|
|
return BlockID(self.image_filepath).get_filesystem()
|
|
except Exception:
|
|
return ''
|
|
|
|
def write_verification_metadata(self, device_node: str) -> None:
|
|
"""
|
|
Write metadata block beginning at
|
|
getsize64() - defaults.DM_METADATA_OFFSET
|
|
of the given device_node
|
|
|
|
:param str device_node: block device node name
|
|
"""
|
|
if self.verification_metadata_file:
|
|
meta_data_size = os.path.getsize(
|
|
self.verification_metadata_file.name
|
|
)
|
|
if meta_data_size > defaults.DM_METADATA_OFFSET:
|
|
raise KiwiOffsetError(
|
|
'Metadata size of {0}b exceeds {1}b limit'.format(
|
|
meta_data_size, defaults.DM_METADATA_OFFSET
|
|
)
|
|
)
|
|
with open(self.verification_metadata_file.name, 'rb') as meta:
|
|
with open(device_node, 'r+b') as target:
|
|
# seek --defaults.DM_METADATA_OFFSET from the
|
|
# end to reach the metadata start
|
|
# Please note, writing of the metadata block can destroy
|
|
# the filesystem on the device_node if it was not created
|
|
# with a smaller size than the device_node, you have been
|
|
# warned.
|
|
target.seek(-defaults.DM_METADATA_OFFSET, 2)
|
|
target.write(meta.read())
|
|
|
|
def create_verity_verification_metadata(self) -> None:
|
|
"""
|
|
Create a metadata block containing information for
|
|
dm_verity verification in the following format:
|
|
|
|
|header_string|0xFF|dm_verity_credentials|0xFF|0x0|
|
|
|
|
header_string:
|
|
'{version} {fstype} {ro|rw} verity'
|
|
|
|
dm_verity_credentials:
|
|
'{hash_type} {data_blksize} {hash_blksize}
|
|
{data_blocks} {hash_start_block} {algorithm} {root_hash} {salt}'
|
|
|
|
Please note, writing of the metadata block can destroy
|
|
the filesystem on the device_node if it was not created
|
|
with a smaller size than the device_node !
|
|
"""
|
|
metadata_format_version = defaults.DM_METADATA_FORMAT_VERSION
|
|
filesystem = self.get_block_storage_filesystem()
|
|
if filesystem and self.verity_dict:
|
|
filesystem_mode = 'ro' if filesystem == 'squashfs' else 'rw'
|
|
|
|
header_string = '{0} {1} {2} verity'.format(
|
|
metadata_format_version, filesystem, filesystem_mode
|
|
)
|
|
|
|
hash_start_block = int(
|
|
self.verity_hash_offset / int(
|
|
self.verity_dict['Hashblocksize']
|
|
)
|
|
)
|
|
dm_verity_credentials = '{0} {1} {2} {3} {4} {5} {6} {7}'.format(
|
|
self.verity_dict['Hashtype'],
|
|
self.verity_dict['Datablocksize'],
|
|
self.verity_dict['Hashblocksize'],
|
|
self.verity_dict['Datablocks'],
|
|
hash_start_block,
|
|
self.verity_dict['Hashalgorithm'],
|
|
self.verity_dict['Roothash'],
|
|
self.verity_dict['Salt']
|
|
)
|
|
|
|
self.verification_metadata_file = Temporary().new_file()
|
|
with open(self.verification_metadata_file.name, 'wb') as meta:
|
|
meta.write(header_string.encode("ascii"))
|
|
meta.write(b'\xFF')
|
|
meta.write(dm_verity_credentials.encode("ascii"))
|
|
meta.write(b'\xFF')
|
|
meta.write(b'\0')
|
|
|
|
def sign_verification_metadata(self) -> None:
|
|
"""
|
|
Create an openssl based signature from the metadata block
|
|
and attach it at the end of the block.
|
|
"""
|
|
if self.verification_metadata_file:
|
|
Signature(self.verification_metadata_file.name).sign()
|
|
|
|
def store_credentials(
|
|
self, credentials_filepath: str, target_block_id: BlockID
|
|
) -> None:
|
|
"""
|
|
Store verification credentials and other metadata to
|
|
the given credentials_filepath
|
|
|
|
:param str credentials_filepath: file path name
|
|
:param BlockID target_block_id:
|
|
instance of BlockID of the target storage device
|
|
"""
|
|
if self.verity_dict:
|
|
with open(credentials_filepath, 'w') as verity:
|
|
for key in sorted(self.verity_dict.keys()):
|
|
verity.write(f'{key}: {self.verity_dict[key]}{os.linesep}')
|
|
verity.write(
|
|
f'Root hashoffset: {self.verity_hash_offset}')
|
|
verity.write(os.linesep)
|
|
verity.write('Superblock: --no-superblock')
|
|
verity.write(os.linesep)
|