kiwi-el8/kiwi/utils/veritysetup.py
Joerg Vehlow c33ec79ab2 Make verity metadata always readonly
If a partition is verity protected, it can never be writable,
since verity is by-definition read-only.
2023-08-22 09:41:58 +02:00

245 lines
8.9 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 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:
header_string = '{0} {1} ro verity'.format(
metadata_format_version, filesystem
)
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)