From 467d2bc5d2b60d475f5eee4af60f63806489b962 Mon Sep 17 00:00:00 2001 From: Daniel Zatovic Date: Tue, 16 Apr 2024 17:04:41 +0200 Subject: [PATCH] LuksScanner: Add LUKS dump scanner and models Add LuksScanner actor that runs 'cryptsetup luksDump' for all 'crypt' from lsblk output. The output is then parsed and filled into LuksDump and LuksToken models. The LuksDump model contains information about LUKS version, device UUID, corresponding device path, name of the backing device (which contains the LUKS header) and a list of LuksToken models. LuksToken model represents a token associated with the given LUKS device. It contains token ID, IDs of associated keyslot and token type. If the token type is "clevis", we use "clevis luks list" command to determine the clevis-specific subtype and append it to the token name. E.g. if there is a "clevis" token and "clevis luks list" returns "tpm2", the token type will be "clevis-tpm2". --- .../common/actors/luksscanner/actor.py | 23 +++ .../luksscanner/libraries/luksdump_parser.py | 195 ++++++++++++++++++ .../luksscanner/libraries/luksscanner.py | 124 +++++++++++ .../tests/files/luksDump_luks1.txt | 27 +++ .../tests/files/luksDump_nvme0n1p3_luks1.txt | 27 +++ .../tests/files/luksDump_nvme0n1p3_luks2.txt | 43 ++++ .../files/luksDump_nvme0n1p3_luks2_tokens.txt | 119 +++++++++++ .../luksscanner/tests/test_luksdump_parser.py | 147 +++++++++++++ .../luksscanner/tests/test_luksscaner.py | 142 +++++++++++++ .../system_upgrade/common/models/luksdump.py | 73 +++++++ 10 files changed, 920 insertions(+) create mode 100644 repos/system_upgrade/common/actors/luksscanner/actor.py create mode 100755 repos/system_upgrade/common/actors/luksscanner/libraries/luksdump_parser.py create mode 100644 repos/system_upgrade/common/actors/luksscanner/libraries/luksscanner.py create mode 100644 repos/system_upgrade/common/actors/luksscanner/tests/files/luksDump_luks1.txt create mode 100644 repos/system_upgrade/common/actors/luksscanner/tests/files/luksDump_nvme0n1p3_luks1.txt create mode 100644 repos/system_upgrade/common/actors/luksscanner/tests/files/luksDump_nvme0n1p3_luks2.txt create mode 100644 repos/system_upgrade/common/actors/luksscanner/tests/files/luksDump_nvme0n1p3_luks2_tokens.txt create mode 100644 repos/system_upgrade/common/actors/luksscanner/tests/test_luksdump_parser.py create mode 100644 repos/system_upgrade/common/actors/luksscanner/tests/test_luksscaner.py create mode 100644 repos/system_upgrade/common/models/luksdump.py diff --git a/repos/system_upgrade/common/actors/luksscanner/actor.py b/repos/system_upgrade/common/actors/luksscanner/actor.py new file mode 100644 index 0000000000..a163374be3 --- /dev/null +++ b/repos/system_upgrade/common/actors/luksscanner/actor.py @@ -0,0 +1,23 @@ +from leapp.actors import Actor +from leapp.libraries.actor import luksscanner +from leapp.models import LuksDumps, StorageInfo +from leapp.reporting import Report +from leapp.tags import FactsPhaseTag, IPUWorkflowTag + + +class LuksScanner(Actor): + """ + Provides data about active LUKS devices. + + Scans all block devices of 'crypt' type and attempts to run 'cryptsetup luksDump' on them. + For every 'crypt' device a LuksDump model is produced. Furthermore, if there is any LUKS token + of type clevis, the concrete subtype is determined using 'clevis luks list'. + """ + + name = 'luks_scanner' + consumes = (StorageInfo,) + produces = (Report, LuksDumps) + tags = (IPUWorkflowTag, FactsPhaseTag) + + def process(self): + self.produce(luksscanner.get_luks_dumps_model()) diff --git a/repos/system_upgrade/common/actors/luksscanner/libraries/luksdump_parser.py b/repos/system_upgrade/common/actors/luksscanner/libraries/luksdump_parser.py new file mode 100755 index 0000000000..3c61813e63 --- /dev/null +++ b/repos/system_upgrade/common/actors/luksscanner/libraries/luksdump_parser.py @@ -0,0 +1,195 @@ +class LuksDumpParser(object): + """ + Class for parsing "cryptsetup luksDump" output. Given a list of lines, it + generates a dictionary representing the dump. + """ + + class Node(object): + """ + Helper class, every line is represented as a node. The node depth is + based on the indentation of the line. A dictionary is produced after + all lines are inserted. + """ + + def __init__(self, indented_line): + self.children = [] + self.level = len(indented_line) - len(indented_line.lstrip()) + self.text = indented_line.strip() + + def add_children(self, nodes): + childlevel = nodes[0].level + while nodes: + node = nodes.pop(0) + if node.level == childlevel: # add node as a child + self.children.append(node) + elif node.level > childlevel: # add nodes as grandchildren of the last child + nodes.insert(0, node) + self.children[-1].add_children(nodes) + elif node.level <= self.level: # this node is a sibling, no more children + nodes.insert(0, node) + return + + def as_dict(self): + if len(self.children) > 1: + children = [node.as_dict() for node in self.children] + + return {self.text: LuksDumpParser._merge_list(children)} + if len(self.children) == 1: + return {self.text: self.children[0].as_dict()} + return self.text + + @staticmethod + def _count_type(elem_list, elem_type): + """ Count the number of items of elem_type inside the elem_list """ + return sum(isinstance(x, elem_type) for x in elem_list) + + @staticmethod + def _merge_list(elem_list): + """ + Given a list of elements merge them into a single element. If all + elements are strings, concatenate them into a single string. When all + the elements are dictionaries merge them into a single dictionary + containing the keys/values from all of the dictionaries. + """ + + dict_count = LuksDumpParser._count_type(elem_list, dict) + str_count = LuksDumpParser._count_type(elem_list, str) + + result = elem_list + if dict_count == len(elem_list): + result = {} + for element in elem_list: + result.update(element) + elif str_count == len(elem_list): + result = "".join(elem_list) + + return result + + @staticmethod + def _find_single_str(elem_list): + """ If the list contains exactly one string return it or return None otherwise. """ + + result = None + + for elem in elem_list: + if isinstance(elem, str): + if result is not None: + # more than one strings in the list + return None + result = elem + + return result + + @staticmethod + def _fixup_type(elem_list, type_string): + single_string = LuksDumpParser._find_single_str(elem_list) + + if single_string is not None: + elem_list.remove(single_string) + elem_list.append({type_string: single_string}) + + @staticmethod + def _fixup_section(section, type_string): + for key, value in section.items(): + LuksDumpParser._fixup_type(value, type_string) + section[key] = LuksDumpParser._merge_list(section[key]) + + @staticmethod + def _fixup_dict(parsed_dict): + """ Various fixups of the parsed dictionary """ + + if "Version" not in parsed_dict: + return + if parsed_dict["Version"] == "1": + for i in range(8): + keyslot = "Key Slot {}".format(i) + + if keyslot not in parsed_dict: + continue + + if parsed_dict[keyslot] in ["ENABLED", "DISABLED"]: + parsed_dict[keyslot] = {"enabled": parsed_dict[keyslot] == "ENABLED"} + + if not isinstance(parsed_dict[keyslot], list): + continue + + enabled = None + if "ENABLED" in parsed_dict[keyslot]: + enabled = True + parsed_dict[keyslot].remove("ENABLED") + if "DISABLED" in parsed_dict[keyslot]: + enabled = False + parsed_dict[keyslot].remove("DISABLED") + parsed_dict[keyslot] = LuksDumpParser._merge_list(parsed_dict[keyslot]) + if enabled is not None: + parsed_dict[keyslot]["enabled"] = enabled + elif parsed_dict["Version"] == "2": + for section in ["Keyslots", "Digests", "Data segments", "Tokens"]: + if section in parsed_dict: + LuksDumpParser._fixup_section(parsed_dict[section], "type") + + @staticmethod + def _fixup_dump(dump): + """ + Replace tabs with spaces, for lines with colon a move the text + after column on new line with the indent of the following line. + """ + + dump = [line.replace("\t", " "*8).replace("\n", "") for line in dump] + newdump = [] + + for i, line in enumerate(dump): + if not line.strip(): + continue + + if ':' in line: + first_half = line.split(":")[0] + second_half = ":".join(line.split(":")[1:]).lstrip() + + current_level = len(line) - len(line.lstrip()) + if i+1 < len(dump): + next_level = len(dump[i+1]) - len(dump[i+1].lstrip()) + else: + next_level = current_level + + if next_level > current_level: + second_half = " " * next_level + second_half + else: + second_half = " " * (current_level + 8) + second_half + + newdump.append(first_half) + if second_half.strip(): + newdump.append(second_half) + else: + newdump.append(line) + + return newdump + + @staticmethod + def parse(dump): + """ + Parse the output of "cryptsetup luksDump" command into a dictionary. + + :param dump: List of output lines of luksDump + :returns: Parsed dictionary + """ + + root = LuksDumpParser.Node('root') + + nodes = [] + for line in LuksDumpParser._fixup_dump(dump): + nodes.append(LuksDumpParser.Node(line)) + + root.add_children(nodes) + root = root.as_dict()['root'] + + if isinstance(root, list): + result = {} + for child in root: + if isinstance(child, str): + child = {child: {}} + result.update(child) + root = result + + LuksDumpParser._fixup_dict(root) + return root diff --git a/repos/system_upgrade/common/actors/luksscanner/libraries/luksscanner.py b/repos/system_upgrade/common/actors/luksscanner/libraries/luksscanner.py new file mode 100644 index 0000000000..b9967f4042 --- /dev/null +++ b/repos/system_upgrade/common/actors/luksscanner/libraries/luksscanner.py @@ -0,0 +1,124 @@ +import functools + +from leapp.exceptions import StopActorExecutionError +from leapp.libraries import stdlib +from leapp.libraries.actor.luksdump_parser import LuksDumpParser +from leapp.libraries.stdlib import api +from leapp.models import LuksDump, LuksDumps, LuksToken, StorageInfo + + +def aslist(f): + """ Decorator used to convert generator to list """ + @functools.wraps(f) + def inner(*args, **kwargs): + return list(f(*args, **kwargs)) + return inner + + +def _get_clevis_type(device_path, keyslot): + """ + Assuming the device is initialized using clevis, determine the type of + clevis token associated to the specified keyslot. + """ + try: + result = stdlib.run(["clevis", "luks", "list", "-d", device_path, "-s", str(keyslot)]) + except OSError: + message = ('A LUKS drive with clevis token was discovered, but there is ' + 'no clevis package installed. The clevis command is required ' + 'to determine clevis token type.') + details = {'hint': 'Use dnf to install the "clevis-luks" package.'} + raise StopActorExecutionError(message=message, details=details) + except stdlib.CalledProcessError as e: + api.current_logger().debug("clevis list command failed with an error code: {}".format(e.exit_code)) + + message = ('The "clevis luks list" command failed. This' + 'might be because the clevis-luks package is' + 'missing on your system.') + details = {'hint': 'Use dnf to install the "clevis-luks" package.'} + raise StopActorExecutionError(message=message, details=details) + + line = result["stdout"].split() + if len(line) != 3: + raise StopActorExecutionError( + 'Invalid "clevis list" output detected' + ) + + return "clevis-{}".format(line[1]) + + +@aslist +def _get_tokens(device_path, luksdump_dict): + """ Given a parsed LUKS dump, produce a list of tokens """ + if "Version" not in luksdump_dict or luksdump_dict["Version"] != '2': + return + if "Tokens" not in luksdump_dict: + raise StopActorExecutionError( + 'No tokens in cryptsetup luksDump output' + ) + + for token_id in luksdump_dict["Tokens"]: + token = luksdump_dict["Tokens"][token_id] + + if "Keyslot" not in token or "type" not in token: + raise StopActorExecutionError( + 'Token specification does not contain keyslot or type', + ) + keyslot = int(token["Keyslot"]) + token_type = token["type"] + + if token_type == "clevis": + token_type = _get_clevis_type(device_path, keyslot) + + yield LuksToken( + token_id=int(token_id), + keyslot=keyslot, + token_type=token_type + ) + + +def get_luks_dump_by_device(device_path, device_name): + """ Determine info about LUKS device using cryptsetup and clevis commands """ + + try: + result = stdlib.run(['cryptsetup', 'luksDump', device_path]) + luksdump_dict = LuksDumpParser.parse(result["stdout"].splitlines()) + + version = int(luksdump_dict["Version"]) if "Version" in luksdump_dict else None + uuid = luksdump_dict["UUID"] if "UUID" in luksdump_dict else None + if version is None or uuid is None: + api.current_logger().error( + 'Failed to detect UUID or version from the output "cryptsetup luksDump {}" command'.format(device_path) + ) + raise StopActorExecutionError( + 'Failed to detect UUID or version from the output "cryptsetup luksDump {}" command'.format(device_path) + ) + + return LuksDump( + version=version, + uuid=uuid, + device_path=device_path, + device_name=device_name, + tokens=_get_tokens(device_path, luksdump_dict) + ) + + except (OSError, stdlib.CalledProcessError) as ex: + api.current_logger().error( + 'Failed to execute "cryptsetup luksDump" command: {}'.format(ex) + ) + raise StopActorExecutionError( + 'Failed to execute "cryptsetup luksDump {}" command'.format(device_path) + ) + + +@aslist +def get_luks_dumps(): + """ Collect info abaout every active LUKS device """ + + for storage_info in api.consume(StorageInfo): + for blk in storage_info.lsblk: + if blk.tp == 'crypt' and blk.parent_path: + yield get_luks_dump_by_device(blk.parent_path, blk.parent_name) + + +def get_luks_dumps_model(): + return LuksDumps(dumps=get_luks_dumps()) diff --git a/repos/system_upgrade/common/actors/luksscanner/tests/files/luksDump_luks1.txt b/repos/system_upgrade/common/actors/luksscanner/tests/files/luksDump_luks1.txt new file mode 100644 index 0000000000..e22cc8cee5 --- /dev/null +++ b/repos/system_upgrade/common/actors/luksscanner/tests/files/luksDump_luks1.txt @@ -0,0 +1,27 @@ +LUKS header information for /dev/loop10 + +Version: 1 +Cipher name: aes +Cipher mode: xts-plain64 +Hash spec: sha256 +Payload offset: 4096 +MK bits: 512 +MK digest: fb ec 6b 31 ae e4 49 03 3e ad 43 22 02 cf a8 78 ad 3c d2 a8 +MK salt: 17 57 4e 2f ed 0b 5c 62 d5 de 54 f5 7f ab 60 68 + 71 d8 72 06 64 6c 81 05 39 55 3f 55 32 56 d9 da +MK iterations: 114573 +UUID: 90242257-d00a-4019-aba6-03083f89404b + +Key Slot 0: ENABLED + Iterations: 1879168 + Salt: fc 77 48 72 bd 31 ca 83 23 80 5a 5e b9 5b de bb + 55 ac d5 a9 3b 96 ad a5 82 bc 11 68 ba f8 87 56 + Key material offset: 8 + AF stripes: 4000 +Key Slot 1: DISABLED +Key Slot 2: DISABLED +Key Slot 3: DISABLED +Key Slot 4: DISABLED +Key Slot 5: DISABLED +Key Slot 6: DISABLED +Key Slot 7: DISABLED diff --git a/repos/system_upgrade/common/actors/luksscanner/tests/files/luksDump_nvme0n1p3_luks1.txt b/repos/system_upgrade/common/actors/luksscanner/tests/files/luksDump_nvme0n1p3_luks1.txt new file mode 100644 index 0000000000..e22cc8cee5 --- /dev/null +++ b/repos/system_upgrade/common/actors/luksscanner/tests/files/luksDump_nvme0n1p3_luks1.txt @@ -0,0 +1,27 @@ +LUKS header information for /dev/loop10 + +Version: 1 +Cipher name: aes +Cipher mode: xts-plain64 +Hash spec: sha256 +Payload offset: 4096 +MK bits: 512 +MK digest: fb ec 6b 31 ae e4 49 03 3e ad 43 22 02 cf a8 78 ad 3c d2 a8 +MK salt: 17 57 4e 2f ed 0b 5c 62 d5 de 54 f5 7f ab 60 68 + 71 d8 72 06 64 6c 81 05 39 55 3f 55 32 56 d9 da +MK iterations: 114573 +UUID: 90242257-d00a-4019-aba6-03083f89404b + +Key Slot 0: ENABLED + Iterations: 1879168 + Salt: fc 77 48 72 bd 31 ca 83 23 80 5a 5e b9 5b de bb + 55 ac d5 a9 3b 96 ad a5 82 bc 11 68 ba f8 87 56 + Key material offset: 8 + AF stripes: 4000 +Key Slot 1: DISABLED +Key Slot 2: DISABLED +Key Slot 3: DISABLED +Key Slot 4: DISABLED +Key Slot 5: DISABLED +Key Slot 6: DISABLED +Key Slot 7: DISABLED diff --git a/repos/system_upgrade/common/actors/luksscanner/tests/files/luksDump_nvme0n1p3_luks2.txt b/repos/system_upgrade/common/actors/luksscanner/tests/files/luksDump_nvme0n1p3_luks2.txt new file mode 100644 index 0000000000..407261f487 --- /dev/null +++ b/repos/system_upgrade/common/actors/luksscanner/tests/files/luksDump_nvme0n1p3_luks2.txt @@ -0,0 +1,43 @@ +LUKS header information +Version: 2 +Epoch: 3 +Metadata area: 16384 [bytes] +Keyslots area: 16744448 [bytes] +UUID: dfd8db30-2b65-4be9-8cae-65f5fac4a06f +Label: (no label) +Subsystem: (no subsystem) +Flags: (no flags) + +Data segments: + 0: crypt + offset: 16777216 [bytes] + length: (whole device) + cipher: aes-xts-plain64 + sector: 512 [bytes] + +Keyslots: + 0: luks2 + Key: 512 bits + Priority: normal + Cipher: aes-xts-plain64 + Cipher key: 512 bits + PBKDF: argon2id + Time cost: 7 + Memory: 1048576 + Threads: 4 + Salt: 1d d5 97 97 dd 45 e2 d7 2b a7 0b fa c4 7f b3 f4 + ef 4e 5f 95 e0 ba fd 7a 7e 36 02 69 f8 44 96 d8 + AF stripes: 4000 + AF hash: sha256 + Area offset:32768 [bytes] + Area length:258048 [bytes] + Digest ID: 0 +Tokens: +Digests: + 0: pbkdf2 + Hash: sha256 + Iterations: 99750 + Salt: 10 1d a1 21 8b 93 dc bb f1 ab 2b 1b 89 8e 3d c4 + 18 07 51 08 ef f5 95 da 9f 85 fa d7 de c9 c4 96 + Digest: 4f 27 4c 19 ae 72 b1 75 ef 53 c0 6d ff db 7f fe + f1 67 d0 c3 67 03 0c 14 3a 6f 6a 1a 87 a8 6f 32 diff --git a/repos/system_upgrade/common/actors/luksscanner/tests/files/luksDump_nvme0n1p3_luks2_tokens.txt b/repos/system_upgrade/common/actors/luksscanner/tests/files/luksDump_nvme0n1p3_luks2_tokens.txt new file mode 100644 index 0000000000..c2a7464c07 --- /dev/null +++ b/repos/system_upgrade/common/actors/luksscanner/tests/files/luksDump_nvme0n1p3_luks2_tokens.txt @@ -0,0 +1,119 @@ +LUKS header information +Version: 2 +Epoch: 9 +Metadata area: 16384 [bytes] +Keyslots area: 16744448 [bytes] +UUID: 6b929b85-b01e-4aa3-8ad2-a05decae6e3d +Label: (no label) +Subsystem: (no subsystem) +Flags: (no flags) + +Data segments: + 0: crypt + offset: 16777216 [bytes] + length: (whole device) + cipher: aes-xts-plain64 + sector: 512 [bytes] + +Keyslots: + 0: luks2 + Key: 512 bits + Priority: normal + Cipher: aes-xts-plain64 + Cipher key: 512 bits + PBKDF: argon2id + Time cost: 7 + Memory: 1048576 + Threads: 4 + Salt: de a1 b9 7f 03 cb b4 89 e2 52 20 fc e4 24 65 cd + de a1 b9 7f 03 cb b4 89 e2 52 20 fc e4 24 65 cd + AF stripes: 4000 + AF hash: sha256 + Area offset:32768 [bytes] + Area length:258048 [bytes] + Digest ID: 0 + 1: luks2 + Key: 512 bits + Priority: normal + Cipher: aes-xts-plain64 + Cipher key: 512 bits + PBKDF: pbkdf2 + Hash: sha256 + Iterations: 1000 + Salt: de a1 b9 7f 03 cb b4 89 e2 52 20 fc e4 24 65 cd + de a1 b9 7f 03 cb b4 89 e2 52 20 fc e4 24 65 cd + AF stripes: 4000 + AF hash: sha256 + Area offset:290816 [bytes] + Area length:258048 [bytes] + Digest ID: 0 + 2: luks2 + Key: 512 bits + Priority: normal + Cipher: aes-xts-plain64 + Cipher key: 512 bits + PBKDF: pbkdf2 + Hash: sha256 + Iterations: 1000 + Salt: de a1 b9 7f 03 cb b4 89 e2 52 20 fc e4 24 65 cd + de a1 b9 7f 03 cb b4 89 e2 52 20 fc e4 24 65 cd + AF stripes: 4000 + AF hash: sha256 + Area offset:548864 [bytes] + Area length:258048 [bytes] + Digest ID: 0 + 3: luks2 + Key: 512 bits + Priority: normal + Cipher: aes-xts-plain64 + Cipher key: 512 bits + PBKDF: pbkdf2 + Hash: sha512 + Iterations: 1000 + Salt: de a1 b9 7f 03 cb b4 89 e2 52 20 fc e4 24 65 cd + de a1 b9 7f 03 cb b4 89 e2 52 20 fc e4 24 65 cd + AF stripes: 4000 + AF hash: sha512 + Area offset:806912 [bytes] + Area length:258048 [bytes] + Digest ID: 0 +Tokens: + 0: clevis + Keyslot: 1 + 1: clevis + Keyslot: 2 + 2: systemd-tpm2 + tpm2-hash-pcrs: 7 + tpm2-pcr-bank: sha256 + tpm2-pubkey: + (null) + tpm2-pubkey-pcrs: n/a + tpm2-primary-alg: ecc + tpm2-blob: de a1 b9 7f 03 cb b4 89 e2 52 20 fc e4 24 65 cd + de a1 b9 7f 03 cb b4 89 e2 52 20 fc e4 24 65 cd + de a1 b9 7f 03 cb b4 89 e2 52 20 fc e4 24 65 cd + de a1 b9 7f 03 cb b4 89 e2 52 20 fc e4 24 65 cd + de a1 b9 7f 03 cb b4 89 e2 52 20 fc e4 24 65 cd + de a1 b9 7f 03 cb b4 89 e2 52 20 fc e4 24 65 cd + de a1 b9 7f 03 cb b4 89 e2 52 20 fc e4 24 65 cd + de a1 b9 7f 03 cb b4 89 e2 52 20 fc e4 24 65 cd + de a1 b9 7f 03 cb b4 89 e2 52 20 fc e4 24 65 cd + de a1 b9 7f 03 cb b4 89 e2 52 20 fc e4 24 65 cd + de a1 b9 7f 03 cb b4 89 e2 52 20 fc e4 24 65 cd + de a1 b9 7f 03 cb b4 89 e2 52 20 fc e4 24 65 cd + de a1 b9 7f 03 cb b4 89 e2 52 20 fc e4 24 65 cd + de a1 b9 7f 03 cb b4 89 e2 52 20 fc e4 24 65 cd + tpm2-policy-hash: + de a1 b9 7f 03 cb b4 89 e2 52 20 fc e4 24 65 cd + de a1 b9 7f 03 cb b4 89 e2 52 20 fc e4 24 65 cd + tpm2-pin: false + tpm2-salt: false + Keyslot: 3 +Digests: + 0: pbkdf2 + Hash: sha256 + Iterations: 117448 + Salt: de a1 b9 7f 03 cb b4 89 e2 52 20 fc e4 24 65 cd + de a1 b9 7f 03 cb b4 89 e2 52 20 fc e4 24 65 cd + Digest: de a1 b9 7f 03 cb b4 89 e2 52 20 fc e4 24 65 cd + de a1 b9 7f 03 cb b4 89 e2 52 20 fc e4 24 65 cd diff --git a/repos/system_upgrade/common/actors/luksscanner/tests/test_luksdump_parser.py b/repos/system_upgrade/common/actors/luksscanner/tests/test_luksdump_parser.py new file mode 100644 index 0000000000..4b19014926 --- /dev/null +++ b/repos/system_upgrade/common/actors/luksscanner/tests/test_luksdump_parser.py @@ -0,0 +1,147 @@ +import os + +from leapp.libraries.actor.luksdump_parser import LuksDumpParser +from leapp.snactor.fixture import current_actor_context + +CUR_DIR = os.path.dirname(os.path.abspath(__file__)) + + +def test_luksdump_parser_luks1(current_actor_context): + f = open(os.path.join(CUR_DIR, 'files/luksDump_nvme0n1p3_luks1.txt')) + parsed_dict = LuksDumpParser.parse(f.readlines()) + + assert parsed_dict["Version"] == "1" + assert parsed_dict["Cipher name"] == "aes" + assert parsed_dict["Cipher mode"] == "xts-plain64" + assert parsed_dict["Hash spec"] == "sha256" + assert parsed_dict["Payload offset"] == "4096" + assert parsed_dict["MK bits"] == "512" + assert parsed_dict["MK digest"].replace(" ", "") == "fbec6b31aee449033ead432202cfa878ad3cd2a8" + assert parsed_dict["MK salt"].replace(" ", "") == "17574e2fed0b5c62d5de54f57fab6068"\ + "71d87206646c810539553f553256d9da" + assert parsed_dict["MK iterations"] == "114573" + assert parsed_dict["UUID"] == "90242257-d00a-4019-aba6-03083f89404b" + + assert parsed_dict["Key Slot 0"]["enabled"] + assert parsed_dict["Key Slot 0"]["Iterations"] == "1879168" + assert parsed_dict["Key Slot 0"]["Salt"].replace(" ", "") == "fc774872bd31ca8323805a5eb95bdebb" \ + "55acd5a93b96ada582bc1168baf88756" + assert parsed_dict["Key Slot 0"]["Key material offset"] == "8" + assert parsed_dict["Key Slot 0"]["AF stripes"] == "4000" + + assert not parsed_dict["Key Slot 1"]["enabled"] + assert not parsed_dict["Key Slot 2"]["enabled"] + assert not parsed_dict["Key Slot 3"]["enabled"] + assert not parsed_dict["Key Slot 4"]["enabled"] + assert not parsed_dict["Key Slot 5"]["enabled"] + assert not parsed_dict["Key Slot 6"]["enabled"] + assert not parsed_dict["Key Slot 7"]["enabled"] + + +def test_luksdump_parser_luks2_tokens(current_actor_context): + f = open(os.path.join(CUR_DIR, 'files/luksDump_nvme0n1p3_luks2_tokens.txt')) + parsed_dict = LuksDumpParser.parse(f.readlines()) + + assert parsed_dict["Version"] == "2" + assert parsed_dict["Epoch"] == "9" + assert parsed_dict["Metadata area"] == "16384 [bytes]" + assert parsed_dict["Keyslots area"] == "16744448 [bytes]" + assert parsed_dict["UUID"] == "6b929b85-b01e-4aa3-8ad2-a05decae6e3d" + assert parsed_dict["Label"] == "(no label)" + assert parsed_dict["Subsystem"] == "(no subsystem)" + assert parsed_dict["Flags"] == "(no flags)" + + assert len(parsed_dict["Data segments"]) == 1 + assert parsed_dict["Data segments"]["0"]["type"] == "crypt" + assert parsed_dict["Data segments"]["0"]["offset"] == "16777216 [bytes]" + assert parsed_dict["Data segments"]["0"]["length"] == "(whole device)" + assert parsed_dict["Data segments"]["0"]["cipher"] == "aes-xts-plain64" + assert parsed_dict["Data segments"]["0"]["sector"] == "512 [bytes]" + + assert len(parsed_dict["Keyslots"]) == 4 + assert parsed_dict["Keyslots"]["0"]["type"] == "luks2" + assert parsed_dict["Keyslots"]["0"]["Key"] == "512 bits" + assert parsed_dict["Keyslots"]["0"]["Priority"] == "normal" + assert parsed_dict["Keyslots"]["0"]["Cipher"] == "aes-xts-plain64" + assert parsed_dict["Keyslots"]["0"]["Cipher key"] == "512 bits" + assert parsed_dict["Keyslots"]["0"]["PBKDF"] == "argon2id" + assert parsed_dict["Keyslots"]["0"]["Time cost"] == "7" + assert parsed_dict["Keyslots"]["0"]["Memory"] == "1048576" + assert parsed_dict["Keyslots"]["0"]["Threads"] == "4" + assert parsed_dict["Keyslots"]["0"]["Salt"].replace(" ", "") == 2*"dea1b97f03cbb489e25220fce42465cd" + assert parsed_dict["Keyslots"]["0"]["AF stripes"] == "4000" + assert parsed_dict["Keyslots"]["0"]["AF hash"] == "sha256" + assert parsed_dict["Keyslots"]["0"]["Area offset"] == "32768 [bytes]" + assert parsed_dict["Keyslots"]["0"]["Area length"] == "258048 [bytes]" + assert parsed_dict["Keyslots"]["0"]["Digest ID"] == "0" + + assert parsed_dict["Keyslots"]["1"]["type"] == "luks2" + assert parsed_dict["Keyslots"]["1"]["Key"] == "512 bits" + assert parsed_dict["Keyslots"]["1"]["Priority"] == "normal" + assert parsed_dict["Keyslots"]["1"]["Cipher"] == "aes-xts-plain64" + assert parsed_dict["Keyslots"]["1"]["Cipher key"] == "512 bits" + assert parsed_dict["Keyslots"]["1"]["PBKDF"] == "pbkdf2" + assert parsed_dict["Keyslots"]["1"]["Hash"] == "sha256" + assert parsed_dict["Keyslots"]["1"]["Iterations"] == "1000" + assert parsed_dict["Keyslots"]["1"]["Salt"].replace(" ", "") == 2*"dea1b97f03cbb489e25220fce42465cd" + assert parsed_dict["Keyslots"]["1"]["AF stripes"] == "4000" + assert parsed_dict["Keyslots"]["1"]["AF hash"] == "sha256" + assert parsed_dict["Keyslots"]["1"]["Area offset"] == "290816 [bytes]" + assert parsed_dict["Keyslots"]["1"]["Area length"] == "258048 [bytes]" + assert parsed_dict["Keyslots"]["1"]["Digest ID"] == "0" + + assert parsed_dict["Keyslots"]["2"]["type"] == "luks2" + assert parsed_dict["Keyslots"]["2"]["Key"] == "512 bits" + assert parsed_dict["Keyslots"]["2"]["Priority"] == "normal" + assert parsed_dict["Keyslots"]["2"]["Cipher"] == "aes-xts-plain64" + assert parsed_dict["Keyslots"]["2"]["Cipher key"] == "512 bits" + assert parsed_dict["Keyslots"]["2"]["PBKDF"] == "pbkdf2" + assert parsed_dict["Keyslots"]["2"]["Hash"] == "sha256" + assert parsed_dict["Keyslots"]["2"]["Iterations"] == "1000" + assert parsed_dict["Keyslots"]["2"]["Salt"].replace(" ", "") == 2*"dea1b97f03cbb489e25220fce42465cd" + assert parsed_dict["Keyslots"]["2"]["AF stripes"] == "4000" + assert parsed_dict["Keyslots"]["2"]["AF hash"] == "sha256" + assert parsed_dict["Keyslots"]["2"]["Area offset"] == "548864 [bytes]" + assert parsed_dict["Keyslots"]["2"]["Area length"] == "258048 [bytes]" + assert parsed_dict["Keyslots"]["2"]["Digest ID"] == "0" + + assert parsed_dict["Keyslots"]["3"]["type"] == "luks2" + assert parsed_dict["Keyslots"]["3"]["Key"] == "512 bits" + assert parsed_dict["Keyslots"]["3"]["Priority"] == "normal" + assert parsed_dict["Keyslots"]["3"]["Cipher"] == "aes-xts-plain64" + assert parsed_dict["Keyslots"]["3"]["Cipher key"] == "512 bits" + assert parsed_dict["Keyslots"]["3"]["PBKDF"] == "pbkdf2" + assert parsed_dict["Keyslots"]["3"]["Hash"] == "sha512" + assert parsed_dict["Keyslots"]["3"]["Iterations"] == "1000" + assert parsed_dict["Keyslots"]["3"]["Salt"].replace(" ", "") == 2*"dea1b97f03cbb489e25220fce42465cd" + assert parsed_dict["Keyslots"]["3"]["AF stripes"] == "4000" + assert parsed_dict["Keyslots"]["3"]["AF hash"] == "sha512" + assert parsed_dict["Keyslots"]["3"]["Area offset"] == "806912 [bytes]" + assert parsed_dict["Keyslots"]["3"]["Area length"] == "258048 [bytes]" + assert parsed_dict["Keyslots"]["3"]["Digest ID"] == "0" + + assert len(parsed_dict["Tokens"]) == 3 + assert parsed_dict["Tokens"]["0"]["type"] == "clevis" + assert parsed_dict["Tokens"]["0"]["Keyslot"] == "1" + + assert parsed_dict["Tokens"]["1"]["type"] == "clevis" + assert parsed_dict["Tokens"]["1"]["Keyslot"] == "2" + + assert parsed_dict["Tokens"]["2"]["type"] == "systemd-tpm2" + assert parsed_dict["Tokens"]["2"]["Keyslot"] == "3" + assert parsed_dict["Tokens"]["2"]["tpm2-hash-pcrs"] == "7" + assert parsed_dict["Tokens"]["2"]["tpm2-pcr-bank"] == "sha256" + assert parsed_dict["Tokens"]["2"]["tpm2-pubkey"] == "(null)" + assert parsed_dict["Tokens"]["2"]["tpm2-pubkey-pcrs"] == "n/a" + assert parsed_dict["Tokens"]["2"]["tpm2-primary-alg"] == "ecc" + assert parsed_dict["Tokens"]["2"]["tpm2-blob"].replace(" ", "") == 14*"dea1b97f03cbb489e25220fce42465cd" + assert parsed_dict["Tokens"]["2"]["tpm2-policy-hash"].replace(" ", "") == 2*"dea1b97f03cbb489e25220fce42465cd" + assert parsed_dict["Tokens"]["2"]["tpm2-pin"] == "false" + assert parsed_dict["Tokens"]["2"]["tpm2-salt"] == "false" + + assert len(parsed_dict["Digests"]) == 1 + assert parsed_dict["Digests"]["0"]["type"] == "pbkdf2" + assert parsed_dict["Digests"]["0"]["Hash"] == "sha256" + assert parsed_dict["Digests"]["0"]["Iterations"] == "117448" + assert parsed_dict["Digests"]["0"]["Salt"].replace(" ", "") == 2*"dea1b97f03cbb489e25220fce42465cd" + assert parsed_dict["Digests"]["0"]["Digest"].replace(" ", "") == 2*"dea1b97f03cbb489e25220fce42465cd" diff --git a/repos/system_upgrade/common/actors/luksscanner/tests/test_luksscaner.py b/repos/system_upgrade/common/actors/luksscanner/tests/test_luksscaner.py new file mode 100644 index 0000000000..22eb0946f1 --- /dev/null +++ b/repos/system_upgrade/common/actors/luksscanner/tests/test_luksscaner.py @@ -0,0 +1,142 @@ +import os + +import pytest + +from leapp.libraries.stdlib import api +from leapp.models import LsblkEntry, LuksDumps, StorageInfo +from leapp.snactor.fixture import current_actor_context + +CUR_DIR = os.path.dirname(os.path.abspath(__file__)) + +TOKENS_ASSERT = { + 0: { + "keyslot": 1, + "token_type": "clevis-tpm2" + }, + 1: { + "keyslot": 2, + "token_type": "clevis-tang" + }, + 2: { + "keyslot": 3, + "token_type": "systemd-tpm2" + }, +} + +CLEVIS_KEYSLOTS = { + 1: 'tpm2 \'{"hash":"sha256","key":"rsa","pcr_bank":"sha256","pcr_ids":"0,1,7"}\'', + 2: 'tang \'{"url":"http://localhost"}\'' +} + + +class MockedRun(object): + """Simple mock class for leapp.libraries.stdlib.run.""" + + def __init__(self, variant, clevis_keyslots): + """if exc_type provided, then it will be raised on + instance call. + + :type exc_type: None or BaseException + """ + self.logger = api.current_logger() + + self.commands = [] + self.variant = variant + self.clevis_keyslots = clevis_keyslots + + def __call__(self, cmd, *args, **kwargs): + self.commands.append(cmd) + + if len(cmd) == 3 and cmd[:2] == ['cryptsetup', 'luksDump']: + dev_path = cmd[2] + + # We cannot have the output in a list, since the command is called per device. Therefore, we have to map + # each device path to its output. + output_files_per_device = { + '/dev/nvme0n1p3': 'luksDump_nvme0n1p3{}.txt'.format(("_" + self.variant) if self.variant else "") + } + + if dev_path not in output_files_per_device: + raise ValueError( + 'Attempting to call "cryptsetup luksDump" on an unexpected device: {}'.format(dev_path) + ) + with open(os.path.join(CUR_DIR, 'files/{}'.format(output_files_per_device[dev_path]))) as f: + return {"stdout": f.read()} + elif len(cmd) >= 3 and cmd[:3] == ['clevis', 'luks', 'list']: + dev_path = None + keyslot = None + + device_flag = False + keyslot_flag = False + for element in cmd: + if device_flag: + dev_path = element + elif keyslot_flag: + keyslot = element + + device_flag = element == "-d" + keyslot_flag = element == "-s" + + if dev_path is None or keyslot is None: + raise ValueError('Attempting to call "clevis luks list" without specifying keyslot or device') + if dev_path is None or keyslot is None or dev_path != "/dev/nvme0n1p3": + raise ValueError('Attempting to call "clevis luks list" on invalid device') + + keyslot = int(keyslot) + + if keyslot in self.clevis_keyslots: + return {"stdout": "{}: {}".format(keyslot, self.clevis_keyslots[keyslot])} + + return {} + + +@pytest.mark.parametrize( + ("variant", "luks_version", "uuid", "tokens_assert"), + [ + ('luks1', 1, '90242257-d00a-4019-aba6-03083f89404b', {}), + ('luks2', 2, 'dfd8db30-2b65-4be9-8cae-65f5fac4a06f', {}), + ('luks2_tokens', 2, '6b929b85-b01e-4aa3-8ad2-a05decae6e3d', TOKENS_ASSERT), + ] +) +def test_actor_with_luks(monkeypatch, current_actor_context, variant, luks_version, uuid, tokens_assert): + mocked_run = MockedRun(variant, CLEVIS_KEYSLOTS) + monkeypatch.setattr('leapp.libraries.stdlib.run', mocked_run) + + with_luks = [ + LsblkEntry( + name='/dev/nvme0n1', kname='/dev/nvme0n1', maj_min='259:0', rm='0', size='10G', bsize=10*(1 << 39), + ro='0', tp='disk', parent_name='', parent_path='', mountpoint='' + ), + LsblkEntry( + name='/dev/nvme0n1p3', kname='/dev/nvme0n1p3', maj_min='259:3', rm='0', size='10G', bsize=10*(1 << 39), + ro='0', tp='part', parent_name='nvme0n1', parent_path='/dev/nvme0n1', mountpoint='' + ), + LsblkEntry( + name='/dev/mapper/tst1', kname='/dev/dm-0', maj_min='253:0', rm='0', size='9G', bsize=9*(1 << 39), ro='0', + tp='crypt', parent_name='nvme0n1p3', parent_path='/dev/nvme0n1p3', mountpoint='' + ), + # PKNAME is not set, so this crypt device will be ignored + LsblkEntry( + name='/dev/mapper/tst2', kname='/dev/dm-1', maj_min='253:0', rm='0', size='9G', bsize=9*(1 << 39), ro='0', + tp='crypt', parent_name='', parent_path='', mountpoint='' + ) + ] + + current_actor_context.feed(StorageInfo(lsblk=with_luks)) + current_actor_context.run() + + luks_dumps = current_actor_context.consume(LuksDumps) + assert len(luks_dumps) == 1 + assert len(luks_dumps[0].dumps) == 1 + luks_dump = luks_dumps[0].dumps[0] + + assert luks_dump.version == luks_version + assert luks_dump.uuid == uuid + assert luks_dump.device_name == "nvme0n1p3" + assert luks_dump.device_path == "/dev/nvme0n1p3" + assert len(luks_dump.tokens) == len(tokens_assert) + + for token in luks_dump.tokens: + assert token.token_id in tokens_assert + assert token.keyslot == tokens_assert[token.token_id]["keyslot"] + assert token.token_type == tokens_assert[token.token_id]["token_type"] diff --git a/repos/system_upgrade/common/models/luksdump.py b/repos/system_upgrade/common/models/luksdump.py new file mode 100644 index 0000000000..511c10cbc7 --- /dev/null +++ b/repos/system_upgrade/common/models/luksdump.py @@ -0,0 +1,73 @@ +from leapp.models import fields, Model +from leapp.topics import SystemInfoTopic + + +class LuksToken(Model): + """ + Represents a single token associated with the LUKS device. + + Note this model is supposed to be used just as part of the LuksDump msg. + """ + topic = SystemInfoTopic + + token_id = fields.Integer() + """ + Token ID (as seen in the luksDump) + """ + + keyslot = fields.Integer() + """ + ID of the associated keyslot + """ + + token_type = fields.String() + """ + Type of the token. For "clevis" type the concrete subtype (determined using + clevis luks list) is appended e.g. clevis-tpm2. clevis-tang, ... + """ + + +class LuksDump(Model): + """ + Information about a single LUKS-encrypted device. + + Note this model is supposed to be used as a part of LuksDumps msg. + """ + topic = SystemInfoTopic + + version = fields.Integer() + """ + LUKS version + """ + + uuid = fields.String() + """ + UUID of the LUKS device + """ + + device_path = fields.String() + """ + Full path to the backing device + """ + + device_name = fields.String() + """ + Device name of the backing device + """ + + tokens = fields.List(fields.Model(LuksToken), default=[]) + """ + List of LUKS2 tokens + """ + + +class LuksDumps(Model): + """ + Information about a all LUKS-encrypted devices on the system. + """ + topic = SystemInfoTopic + + dumps = fields.List(fields.Model(LuksDump)) + """ + List of LuksDump representing all the encrypted devices on the system. + """