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..44113d0eb8 --- /dev/null +++ b/repos/system_upgrade/common/actors/luksscanner/libraries/luksdump_parser.py @@ -0,0 +1,199 @@ +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): + # NOTE(pstodulk): it's expected that nodes are non-empty list and + # having it empty is an error if it happens. So keeping a hard crash + # for now as having an empty list it's hypothetical now and I would + # probably end with en error anyway if discovered. + 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..1c7822a504 --- /dev/null +++ b/repos/system_upgrade/common/actors/luksscanner/libraries/luksscanner.py @@ -0,0 +1,125 @@ +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), + details={'details': str(ex)} + ) + + +@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..83b56ef814 --- /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 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. + """