From 03ab8e0122b7d122f0ad1992ba4212be6a70bbdd Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 13 Oct 2022 05:04:05 +0200 Subject: [PATCH] Add spec and parser for luksmeta command (#3525) * Add spec and parser for luksmeta command Signed-off-by: daniel.zatovic * Add LuksDump parser to collect.py Signed-off-by: daniel.zatovic * Change luksmeta command documentation Signed-off-by: daniel.zatovic * Improve error handling in the luksmeta parser Signed-off-by: daniel.zatovic * Change LuksMeta parser to inherit from dict. Signed-off-by: daniel.zatovic * Extend test coverage Signed-off-by: daniel.zatovic * Fix luksmeta parser documentation Signed-off-by: daniel.zatovic * Specs class cannot be decorated using datasource Otherwise TypeError: LocalSpecs() takes no arguments is emitted. Signed-off-by: daniel.zatovic * Change luks1_block_devices to a combinder Signed-off-by: daniel.zatovic * Add device UUID to LuksMeta parser output. To make a combiner which combines LuksMeta and cryptsetup luksDump outputs, we need to include UUID information in LuksMeta parser. Signed-off-by: daniel.zatovic * Convert LuksMeta to CommandParser Signed-off-by: daniel.zatovic * Add LuksDevices combiner We match LuksDump and the associated LuksMeta based on the UUID of the device they were obtained from. Signed-off-by: daniel.zatovic * Don't run luks1_block_devices during collection Instead we run luksmeta against all devices in /dev/disk/by-uuid Signed-off-by: daniel.zatovic * Fix cryptsetup LocalSpecs to use full path Signed-off-by: daniel.zatovic * Remove luks1_block_devices combiner Signed-off-by: daniel.zatovic * Skip LuksDevices combiner if there are no results Signed-off-by: daniel.zatovic (cherry picked from commit d5b086237ac49cc09564f80e56641bf2043b8f72) --- docs/shared_combiners_catalog/cryptsetup.rst | 3 + docs/shared_parsers_catalog/luksmeta.rst | 3 + insights/combiners/cryptsetup.py | 56 ++++++ insights/parsers/luksmeta.py | 109 +++++++++++ insights/specs/__init__.py | 1 + insights/specs/datasources/luks_devices.py | 5 +- insights/specs/default.py | 2 + insights/tests/combiners/test_cryptsetup.py | 191 +++++++++++++++++++ insights/tests/parsers/test_luksmeta.py | 63 ++++++ 9 files changed, 430 insertions(+), 3 deletions(-) create mode 100644 docs/shared_combiners_catalog/cryptsetup.rst create mode 100644 docs/shared_parsers_catalog/luksmeta.rst create mode 100644 insights/combiners/cryptsetup.py create mode 100644 insights/parsers/luksmeta.py create mode 100644 insights/tests/combiners/test_cryptsetup.py create mode 100644 insights/tests/parsers/test_luksmeta.py diff --git a/docs/shared_combiners_catalog/cryptsetup.rst b/docs/shared_combiners_catalog/cryptsetup.rst new file mode 100644 index 0000000000..1bf56628c4 --- /dev/null +++ b/docs/shared_combiners_catalog/cryptsetup.rst @@ -0,0 +1,3 @@ +.. automodule:: insights.combiners.cryptsetup + :members: LuksDevices + :show-inheritance: diff --git a/docs/shared_parsers_catalog/luksmeta.rst b/docs/shared_parsers_catalog/luksmeta.rst new file mode 100644 index 0000000000..c1b1ddfe3c --- /dev/null +++ b/docs/shared_parsers_catalog/luksmeta.rst @@ -0,0 +1,3 @@ +.. automodule:: insights.parsers.luksmeta + :members: + :show-inheritance: diff --git a/insights/combiners/cryptsetup.py b/insights/combiners/cryptsetup.py new file mode 100644 index 0000000000..717750738a --- /dev/null +++ b/insights/combiners/cryptsetup.py @@ -0,0 +1,56 @@ +""" +Cryptsetup - combine metadata about LUKS devices +================================================ + +Combine outputs of LuksDump and LuksMeta parsers (with the same UUID) into a +single dictionary. +""" + +import copy + +from insights import SkipComponent +from insights.core.plugins import combiner +from insights.parsers.cryptsetup_luksDump import LuksDump +from insights.parsers.luksmeta import LuksMeta + + +@combiner(LuksDump, optional=[LuksMeta]) +class LuksDevices(list): + """ + Combiner for LUKS encrypted devices information. It uses the results of + the ``LuksDump`` and ``LuksMeta`` parser (they are matched based UUID of + the device they were collected from). + + + Examples: + >>> luks_devices[0]["header"]["Version"] + '1' + >>> "luksmeta" in luks_devices[0] + True + >>> "luksmeta" in luks_devices[1] + False + >>> luks_devices[0]["luksmeta"][0] + Keyslot on index 0 is 'active' with no embedded metadata + """ + + def __init__(self, luks_dumps, luks_metas): + luksmeta_by_uuid = {} + + if luks_metas: + for luks_meta in luks_metas: + if "device_uuid" not in luks_meta: + continue + + luksmeta_by_uuid[luks_meta["device_uuid"].lower()] = luks_meta + + for luks_dump in luks_dumps: + uuid = luks_dump.dump["header"]["UUID"].lower() + luks_dump_copy = copy.deepcopy(luks_dump.dump) + + if luks_metas and uuid in luksmeta_by_uuid: + luks_dump_copy["luksmeta"] = luksmeta_by_uuid[uuid] + + self.append(luks_dump_copy) + + if not self: + raise SkipComponent diff --git a/insights/parsers/luksmeta.py b/insights/parsers/luksmeta.py new file mode 100644 index 0000000000..822b510016 --- /dev/null +++ b/insights/parsers/luksmeta.py @@ -0,0 +1,109 @@ +""" +luksmeta - command ``luksmeta show -d `` +===================================================== +This class provides parsing for the output of luksmeta . +""" + +from insights import parser, CommandParser +from insights.specs import Specs + + +class KeyslotSpecification: + """ + Class ``KeyslotSpecification`` describes information about a keyslot + collected by the ``luksmeta show`` command. + + + Attributes: + index (int): the index of the described keyslot + state (str): the state of the described keyslot + metadata (str): the UUID of the application that stored metadata into + the described keyslot + """ + + def __init__(self, index, state, metadata): + self.index = index + self.state = state + self.metadata = metadata + + def __repr__(self): + ret = "Keyslot on index " + str(self.index) + " is '" + self.state + "' " + if self.metadata: + ret += "with metadata stored by application with UUID '" + self.metadata + "'" + else: + ret += "with no embedded metadata" + + return ret + + +@parser(Specs.luksmeta) +class LuksMeta(CommandParser, dict): + """ + Class ``LuksMeta`` parses the output of the ``luksmeta show -d `` command. + + This command prints information if the device has custom user-defined + metadata embedded in the keyslots (used e.g. by clevis). If the device was + not initialized using ``luksmeta``, the parser raises SkipComponent. + + The parser can be indexed by the keyslot index (in the range 0-7). + A KeyslotSpecification object is returned, which describes every LUKS + keyslot. The KeyslotSpecification contains the ``index``, ``state`` and + ``metadata`` fileds. Metadata field stores the UUID of the application that + has stored metadata in the keyslot. + + Sample input data is in the format:: + + 0 active empty + 1 active cb6e8904-81ff-40da-a84a-07ab9ab5715e + 2 active empty + 3 active empty + 4 inactive empty + 5 active empty + 6 active cb6e8904-81ff-40da-a84a-07ab9ab5715e + 7 active cb6e8904-81ff-40da-a84a-07ab9ab5715e + + + Examples: + >>> type(parsed_result) + + + >>> parsed_result[0].index + 0 + + >>> parsed_result[0].state + 'active' + + >>> parsed_result[4].state + 'inactive' + + >>> parsed_result[0].metadata is None + True + + >>> parsed_result[1].metadata + 'cb6e8904-81ff-40da-a84a-07ab9ab5715e' + """ # noqa + + BAD_LINES = [ + "device is not initialized", + "luksmeta data appears corrupt", + "unknown error", + "invalid slot", + "is not a luksv1 device", + "invalid argument", + "unable to read luksv1 header" + ] + + def __init__(self, context): + super(LuksMeta, self).__init__(context, LuksMeta.BAD_LINES) + + def parse_content(self, content): + filename_split = self.file_name.split(".") + + if len(filename_split) >= 4 and filename_split[-4] == "dev" and filename_split[-3] == "disk" and filename_split[-2] == "by-uuid": + self["device_uuid"] = self.file_name.split(".")[-1] if self.file_name else None + + for line in content: + index, state, metadata = line.split() + index = int(index) + metadata = None if metadata == "empty" else metadata + self[index] = KeyslotSpecification(index, state, metadata) diff --git a/insights/specs/__init__.py b/insights/specs/__init__.py index 1a2b74d0ba..3eb23dd0a0 100644 --- a/insights/specs/__init__.py +++ b/insights/specs/__init__.py @@ -363,6 +363,7 @@ class Specs(SpecSet): lssap = RegistryPoint() lsscsi = RegistryPoint() lsvmbus = RegistryPoint() + luksmeta = RegistryPoint(multi_output=True) lvdisplay = RegistryPoint() lvm_conf = RegistryPoint(filterable=True) lvm_system_devices = RegistryPoint() diff --git a/insights/specs/datasources/luks_devices.py b/insights/specs/datasources/luks_devices.py index ca411c885d..a180073ff6 100644 --- a/insights/specs/datasources/luks_devices.py +++ b/insights/specs/datasources/luks_devices.py @@ -37,11 +37,10 @@ def luks_block_devices(broker): raise SkipComponent -@datasource(luks_block_devices) class LocalSpecs(Specs): """ Local specs used only by LUKS_data_sources datasource. """ - cryptsetup_luks_dump_token_commands = foreach_execute(luks_block_devices, "cryptsetup luksDump --disable-external-tokens %s", deps=[HasCryptsetupWithTokens]) - cryptsetup_luks_dump_commands = foreach_execute(luks_block_devices, "cryptsetup luksDump %s", deps=[HasCryptsetupWithoutTokens]) + cryptsetup_luks_dump_token_commands = foreach_execute(luks_block_devices, "/usr/sbin/cryptsetup luksDump --disable-external-tokens %s", deps=[luks_block_devices, HasCryptsetupWithTokens]) + cryptsetup_luks_dump_commands = foreach_execute(luks_block_devices, "/usr/sbin/cryptsetup luksDump %s", deps=[luks_block_devices, HasCryptsetupWithoutTokens]) def line_indentation(line): diff --git a/insights/specs/default.py b/insights/specs/default.py index 445d790c51..ed9acc64c2 100644 --- a/insights/specs/default.py +++ b/insights/specs/default.py @@ -372,6 +372,8 @@ class DefaultSpecs(Specs): lspci_vmmkn = simple_command("/sbin/lspci -vmmkn") lsscsi = simple_command("/usr/bin/lsscsi") lsvmbus = simple_command("/usr/sbin/lsvmbus -vv") + block_devices_by_uuid = listdir("/dev/disk/by-uuid/", context=HostContext) + luksmeta = foreach_execute(block_devices_by_uuid, "/usr/bin/luksmeta show -d /dev/disk/by-uuid/%s", keep_rc=True) lvm_conf = simple_file("/etc/lvm/lvm.conf") lvmconfig = first_of([ simple_command("/usr/sbin/lvmconfig --type full"), diff --git a/insights/tests/combiners/test_cryptsetup.py b/insights/tests/combiners/test_cryptsetup.py new file mode 100644 index 0000000000..389d3b8869 --- /dev/null +++ b/insights/tests/combiners/test_cryptsetup.py @@ -0,0 +1,191 @@ +import doctest +import pytest + +from insights import SkipComponent +from insights.parsers.cryptsetup_luksDump import LuksDump +from insights.parsers import luksmeta +from insights.combiners.cryptsetup import LuksDevices +import insights.combiners.cryptsetup +from insights.tests import context_wrap + +LUKS1_DUMP = """LUKS header information for luks1 + +Version: 1 +Cipher name: aes +Cipher mode: xts-plain64 +Hash spec: sha256 +Payload offset: 4096 +MK bits: 512 +MK digest: ca fe ba be df 8c c4 b4 b8 0a cc dd 98 b5 d8 64 3a 95 3e 9e +MK salt: ca fe ba be 04 3b 77 d8 ff 08 1e 0a 41 68 45 a5 + ca fe ba be 7b 3f a9 69 9c 9b 51 24 58 47 8d a2 + ca fe ba be 7b 3f a9 69 9c 9b 51 24 58 47 8d a2 + ca fe ba be 7b 3f a9 69 9c 9b 51 24 58 47 8d a2 + ca fe ba be 7b 3f a9 69 9c 9b 51 24 58 47 8d a2 + ca fe ba be 7b 3f a9 69 9c de ad be ef +MK iterations: 106562 +UUID: 263902da-5f0c-43a9-82eb-cc6f14d90448 + +Key Slot 0: ENABLED + Iterations: 2099250 + Salt: de ad be ef + Salt: ca fe ba be a1 f3 ae cb 4a 3f f0 2d de ad be ef + de ad be ef + Key material offset: 8 + AF stripes: 4000 +Key Slot 1: ENABLED + Iterations: 1987820 + Salt: ca fe ba be f2 b7 7d f3 29 c2 c8 80 de ad be ef + ca fe ba be 9f a1 87 07 c6 4f aa cd de ad be ef + ca fe ba be 9f a1 87 07 c6 4f aa de ad be ef + Key material offset: 512 + AF stripes: 4000 +Key Slot 2: ENABLED + Iterations: 2052006 + Salt: ca fe ba be 47 94 e7 40 22 c1 bb 4a de ad be ef + ca fe ba be 52 e8 8d 70 b2 1e 9d 47 de ad be ef + Key material offset: 1016 + AF stripes: 4000 +Key Slot 3: DISABLED +Key Slot 4: DISABLED +Key Slot 5: DISABLED +Key Slot 6: DISABLED +Key Slot 7: DISABLED +""" # noqa + +LUKS2_DUMP = """LUKS header information +Version: 2 +Epoch: 6 +Metadata area: 16384 [bytes] +Keyslots area: 16744448 [bytes] +UUID: cfbcc942-e06b-4c4a-952f-e9c9b2011c27 +Label: (no label) +Subsystem: (no subsystem) +Flags: (no flags) + +Data segments: + 0: crypt + offset: 16777216 [bytes] + length: (whole device) + cipher: aes-xts-plain64 + sector: 4096 [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: ca fe ba be fe 1c 90 d8 2a 35 b2 b2 de ad be ef + ca fe ba be b2 dd 45 9e ed 9a 33 b2 de ad be ef + de ad be ef + 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: argon2id + Time cost: 7 + Memory: 1048576 + Threads: 4 + Salt: ca fe ba be c1 94 15 86 2a e9 26 f8 de ad be ef + ca fe ba be 05 2d 80 c9 56 e8 4d 6f de ad be ef + 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: sha512 + Iterations: 1000 + Salt: ca fe ba be d7 8f a6 de a0 cb a4 d1 de ad be ef + ca fe ba be fb 53 43 06 e8 83 90 93 de ad be ef + AF stripes: 4000 + AF hash: sha512 + Area offset:548864 [bytes] + Area length:258048 [bytes] + Digest ID: 0 +Tokens: + 0: systemd-tpm2 + tpm2-pcrs: 7 + tpm2-bank: sha256 + tpm2-primary-alg: ecc + tpm2-blob: 00 9e 00 20 bd 97 78 70 3f 3a 5b 93 d4 8f dc ed + 10 16 b2 ce f5 f7 a2 c8 63 f6 19 12 63 7a f2 94 + 26 f1 b6 1b 00 10 2e 36 26 c1 3b f7 1e 8d 86 55 + tpm2-policy-hash: + df 06 80 28 e7 67 b1 d0 34 f4 de 1b 8e ac 33 5a + df 06 80 28 e7 67 b1 d0 34 f4 de 1b 8e ac 33 5a + Keyslot: 2 +Digests: + 0: pbkdf2 + Hash: sha256 + Iterations: 129774 + Salt: ca fe ba be e0 65 83 82 35 03 29 56 de ad be ef + ca fe ba be de 69 39 97 d5 b3 ac c4 de ad be ef + de ad be ef + Digest: ca fe ba be 9d 46 9b 0f 3a 0f 57 13 de ad be ef + ca fe ba be ed 7d 09 2c 3d b6 fa f4 de ad be ef +""" # noqa + +LUKSMETA_OUTPUT = """0 active empty +1 active cb6e8904-81ff-40da-a84a-07ab9ab5715e +2 active empty +3 active empty +4 inactive empty +5 active empty +6 active cb6e8904-81ff-40da-a84a-07ab9ab5715e +7 active cb6e8904-81ff-40da-a84a-07ab9ab5715e +""" # noqa + +luks1_device = LuksDump(context_wrap(LUKS1_DUMP)) +luks2_device = LuksDump(context_wrap(LUKS2_DUMP)) +uuid = luks1_device.dump["header"]["UUID"] +luksmeta_parsed = luksmeta.LuksMeta(context_wrap(LUKSMETA_OUTPUT, path="/insights_commands/cryptsetup_luksDump_--disable-external-tokens_.dev.disk.by-uuid." + uuid)) +luksmeta_parsed_no_uuid = luksmeta.LuksMeta(context_wrap(LUKSMETA_OUTPUT)) + + +def test_luks_devices_combiner(): + with pytest.raises(SkipComponent): + luks_devices = LuksDevices([], None) + + luks_devices = LuksDevices([luks1_device, luks2_device], None) + for device in luks_devices: + assert "luksmeta" not in device + + luks_devices = LuksDevices([luks1_device, luks2_device], []) + for device in luks_devices: + assert "luksmeta" not in device + + luks_devices = LuksDevices([luks1_device, luks2_device], [luksmeta_parsed_no_uuid]) + for device in luks_devices: + assert "luksmeta" not in device + + luks_devices = LuksDevices([luks1_device, luks2_device], [luksmeta_parsed]) + for device in luks_devices: + if device["header"]["UUID"] == uuid: + assert "luksmeta" in device + else: + assert "luksmeta" not in device + + +def test_doc_examples(): + env = { + 'luks_devices': LuksDevices([luks1_device, luks2_device], [luksmeta_parsed]) + } + failed, total = doctest.testmod(insights.combiners.cryptsetup, globs=env) + assert failed == 0 diff --git a/insights/tests/parsers/test_luksmeta.py b/insights/tests/parsers/test_luksmeta.py new file mode 100644 index 0000000000..9461715651 --- /dev/null +++ b/insights/tests/parsers/test_luksmeta.py @@ -0,0 +1,63 @@ +import doctest +import pytest + +from insights import SkipComponent +from insights.parsers import luksmeta +from insights.tests import context_wrap + + +LUKSMETA_OUTPUT = """0 active empty +1 active cb6e8904-81ff-40da-a84a-07ab9ab5715e +2 active empty +3 active empty +4 inactive empty +5 active empty +6 active cb6e8904-81ff-40da-a84a-07ab9ab5715e +7 active cb6e8904-81ff-40da-a84a-07ab9ab5715e +""" # noqa + +LUKSMETA_NOT_FOUND = "bash: luksmeta: command not found..." +LUKSMETA_NOT_INITIALIZED = "Device is not initialized (./luks1)" +LUKSMETA_BAD_DEVICE = "./luks2 (LUKS2) is not a LUKSv1 device" + + +def test_luksmeta(): + luksmeta_parsed = luksmeta.LuksMeta(context_wrap(LUKSMETA_OUTPUT, path="/insights_commands/cryptsetup_luksDump_--disable-external-tokens_.dev.disk.by-uuid.d62357eb-ea88-4b13-b756-a24e91fbfe9a")) + + with pytest.raises(SkipComponent): + luksmeta.LuksMeta(context_wrap(LUKSMETA_NOT_FOUND)) + + with pytest.raises(SkipComponent): + luksmeta.LuksMeta(context_wrap(LUKSMETA_NOT_INITIALIZED)) + + with pytest.raises(SkipComponent): + luksmeta.LuksMeta(context_wrap(LUKSMETA_BAD_DEVICE)) + + # 8 keyslots and 1 device UUID + assert len(luksmeta_parsed) == 9 + assert "device_uuid" in luksmeta_parsed + assert luksmeta_parsed["device_uuid"] == "d62357eb-ea88-4b13-b756-a24e91fbfe9a" + + for i in range(8): + assert luksmeta_parsed[i].index == i + + assert str(luksmeta_parsed[0]) == "Keyslot on index 0 is 'active' with no embedded metadata" + assert str(luksmeta_parsed[1]) == "Keyslot on index 1 is 'active' with metadata stored by application with UUID 'cb6e8904-81ff-40da-a84a-07ab9ab5715e'" + + assert luksmeta_parsed[0].state == "active" + assert luksmeta_parsed[4].state == "inactive" + + assert luksmeta_parsed[0].metadata is None + assert luksmeta_parsed[1].metadata is not None + assert luksmeta_parsed[1].metadata == "cb6e8904-81ff-40da-a84a-07ab9ab5715e" + + luksmeta_parsed = luksmeta.LuksMeta(context_wrap(LUKSMETA_OUTPUT, path="/insights_commands/cryptsetup_luksDump_--disable-external-tokens_.dev.loop0")) + assert "device_uuid" not in luksmeta_parsed + + +def test_doc_examples(): + env = { + 'parsed_result': luksmeta.LuksMeta(context_wrap(LUKSMETA_OUTPUT)), + } + failed, total = doctest.testmod(luksmeta, globs=env) + assert failed == 0