Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add spec and parser for luksmeta command #3525

Merged
merged 16 commits into from
Oct 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/shared_combiners_catalog/cryptsetup.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.. automodule:: insights.combiners.cryptsetup
:members: LuksDevices
:show-inheritance:
3 changes: 3 additions & 0 deletions docs/shared_parsers_catalog/luksmeta.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.. automodule:: insights.parsers.luksmeta
:members:
:show-inheritance:
56 changes: 56 additions & 0 deletions insights/combiners/cryptsetup.py
Original file line number Diff line number Diff line change
@@ -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])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that the LuksMeta should not be an optional for this combiner, since it's required for the check of L77.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My reasoning behind this is that every LUKS1/2 device has to have one LuksDump object, whereas LuksMeta objects are created only for devices that are LUKS1 and are initialized using luksmeta. Therefore, if the device is not formatted using luksmeta, we just pass-through LuksDump dictionary, and if it is initialized using luksmeta we add a "luksmeta" entry into the LuksDump output. So the LuksDump must always be present, and optionally for luksmeta devices, we also add the information from LuksMeta parser.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just checked your latest update to the test of this combiner, it now looks good to me. Sorry for the late reply, I was just back from a long leave.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, thank you for the review.

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)
xiangce marked this conversation as resolved.
Show resolved Hide resolved

if not self:
raise SkipComponent
109 changes: 109 additions & 0 deletions insights/parsers/luksmeta.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""
luksmeta - command ``luksmeta show -d <device_name>``
=====================================================
This class provides parsing for the output of luksmeta <device_name>.
xiangce marked this conversation as resolved.
Show resolved Hide resolved
"""

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 <device>`` 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)
<class 'insights.parsers.luksmeta.LuksMeta'>

>>> 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)
1 change: 1 addition & 0 deletions insights/specs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
5 changes: 2 additions & 3 deletions insights/specs/datasources/luks_devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 2 additions & 0 deletions insights/specs/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
191 changes: 191 additions & 0 deletions insights/tests/combiners/test_cryptsetup.py
Original file line number Diff line number Diff line change
@@ -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
Loading