-
Notifications
You must be signed in to change notification settings - Fork 184
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feat: Add spec and parser for cryptsetup luksDump (#3504)
* Feat: Add spec and parser for cryptsetup luksDump Signed-off-by: daniel.zatovic <[email protected]> * Fix flake errors in cryptsetup parser test Signed-off-by: daniel.zatovic <[email protected]> * Rework luksDump parser and test. Signed-off-by: daniel.zatovic <[email protected]> * Fix documentation test and fix examples Signed-off-by: daniel.zatovic <[email protected]> * Add docs for cryptsetup parser and datasource Signed-off-by: daniel.zatovic <[email protected]> * Replace bash filtering by a custom datasource Signed-off-by: daniel.zatovic <[email protected]> * Add tests for the new cryptstup datasources Signed-off-by: daniel.zatovic <[email protected]> * Fix comment style Signed-off-by: daniel.zatovic <[email protected]> * Fix doc test inconsistency between Python 2 and 3 Signed-off-by: daniel.zatovic <[email protected]> * Fix title length Signed-off-by: daniel.zatovic <[email protected]> * Fix LUKS capitalisation Signed-off-by: daniel.zatovic <[email protected]> * Remove stray print command Signed-off-by: daniel.zatovic <[email protected]> * Fix names formatting according to PEP8. Signed-off-by: daniel.zatovic <[email protected]> * Change variable names in docs Signed-off-by: daniel.zatovic <[email protected]> * Filter tokens manually instead of using an option Signed-off-by: daniel.zatovic <[email protected]> * Add component to detect cryptsetup token support If the tokens are supported (cryptsetup package version at least 2.4.0 is installed), add --disable-external-tokens option to luksDump spec. Signed-off-by: daniel.zatovic <[email protected]> * Add tests for the cryptsetup component Signed-off-by: daniel.zatovic <[email protected]> * Enable dependent components in the config Signed-off-by: daniel.zatovic <[email protected]> * Extend test coverage Signed-off-by: daniel.zatovic <[email protected]> Signed-off-by: daniel.zatovic <[email protected]> (cherry picked from commit 484befc)
- Loading branch information
Showing
12 changed files
with
804 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
.. automodule:: insights.components.cryptsetup | ||
:members: | ||
:show-inheritance: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
.. automodule:: insights.parsers.cryptsetup_luksDump | ||
:members: | ||
:show-inheritance: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
""" | ||
HasCryptsetupWithTokens, HasCryptsetupWithoutTokens | ||
=================================================== | ||
The ``HasCryptsetupWithTokens``/``HasCryptsetupWithoutTokens`` component uses | ||
``InstalledRpms`` parser to determine if cryptsetup package is installed and if | ||
it has tokens support (since version 2.4.0), if not it raises ``SkipComponent`` | ||
so that the dependent component will not fire. Can be added as a dependency of | ||
a parser so that the parser only fires if the ``cryptsetup`` dependency and | ||
token support is met. | ||
""" | ||
from insights.core.plugins import component | ||
from insights.parsers.installed_rpms import InstalledRpms, InstalledRpm | ||
from insights.core.dr import SkipComponent | ||
|
||
|
||
@component(InstalledRpms) | ||
class HasCryptsetupWithTokens(object): | ||
"""The ``HasCryptsetupWithTokens`` component uses ``InstalledRpms`` parser | ||
to determine if cryptsetup package is installed and if it has tokens | ||
support (since version 2.4.0), if not it raises ``SkipComponent`` | ||
Raises: | ||
SkipComponent: When ``cryptsetup`` package is strictly less than 2.4.0, | ||
or when cryptsetup package is not installed | ||
""" | ||
def __init__(self, rpms): | ||
rpm = rpms.get_max("cryptsetup") | ||
|
||
if rpm is None: | ||
raise SkipComponent("cryptsetup package is not installed") | ||
|
||
if rpm < InstalledRpm("cryptsetup-2.4.0-0"): | ||
raise SkipComponent("cryptsetup package with token support is not installed") | ||
|
||
|
||
@component(InstalledRpms) | ||
class HasCryptsetupWithoutTokens(object): | ||
"""The ``HasCryptsetupWithoutTokens`` component uses ``InstalledRpms`` | ||
parser to determine if cryptsetup package is installed and if it does not | ||
have tokens support (below version 2.4.0), if not it raises | ||
``SkipComponent`` | ||
Raises: | ||
SkipComponent: When ``cryptsetup`` package is at least 2.4.0, or when | ||
cryptsetup package is not installed | ||
""" | ||
def __init__(self, rpms): | ||
rpm = rpms.get_max("cryptsetup") | ||
|
||
if rpm is None: | ||
raise SkipComponent("cryptsetup package is not installed") | ||
|
||
if rpm >= InstalledRpm("cryptsetup-2.4.0-0"): | ||
raise SkipComponent("cryptsetup package with token support is installed") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
""" | ||
LuksDump - command ``cryptsetup luksDump`` | ||
========================================== | ||
This class provides parsing for the output of cryptsetup luksDump | ||
<device_name>. Outputs from LUKS1 and LUKS2 are supported. | ||
""" | ||
|
||
from insights import parser, Parser, SkipComponent | ||
from insights.parsers import ParseException | ||
from insights.parsr import (Literal, AnyChar, Char, Opt, String, WS, | ||
HangingString, WithIndent, Many, EOF, Lift) | ||
from insights.specs import Specs | ||
|
||
import string | ||
|
||
|
||
class DocParser(object): | ||
def __init__(self): | ||
value_chars = set(string.printable) - set("\n\r") | ||
|
||
FirstLine = Literal("LUKS header information", value="header") << AnyChar.until(Char("\n")) + Opt(Many(Char("\n"))) | ||
FirstIndent = Literal(" ") | ||
# we need to replace the \t by 8 spaces in the input, | ||
# otherwise WithIndent does not work properly | ||
# SecondIndent = Literal("\t") | ||
SecondIndent = Literal(" " * 8) | ||
|
||
Key = String(value_chars - set(":")) << Char(":") % "Key" | ||
Value = WS >> HangingString(value_chars) % "Value" | ||
|
||
MultilineContinuation = Many((Char("\n") + Literal(" " * 15) + SecondIndent) >> String(value_chars)) | ||
Value1 = WS >> String(value_chars) + Opt(MultilineContinuation).map(lambda x: "".join(x)) << Char("\n") | ||
Value1 = Value1.map(lambda x: ("".join(x)).strip()) | ||
|
||
ZeroLevelKVPair = Key + Value1 | ||
FirstLevelKVPair = FirstIndent >> Key + Value1 | ||
SecondLevelKVPair = SecondIndent >> WithIndent(Key + Value) | ||
|
||
Luks2SectionName = Key << Char("\n") | ||
Luks2SectionEntry = (FirstLevelKVPair + Many(SecondLevelKVPair).map(dict)).map(self.convert_type) | ||
Luks2Section = Luks2SectionName + Many(Luks2SectionEntry).map(dict) << Opt(Many(Char("\n"))) | ||
Luks2Body = Many(Luks2Section, lower=1) | ||
|
||
Luks1Section = ZeroLevelKVPair + Many(SecondLevelKVPair).map(dict) << Opt(Many(Char("\n"))) | ||
Luks1Body = Many(Luks1Section.map(self.convert_status), lower=1) | ||
|
||
KVBlock = Many(Key + Value1).map(dict) | ||
LuksHeader = (FirstLine + KVBlock) << Opt(Many(Char("\n"))) | ||
|
||
# Luks2Body has to go first, because Luks1Body consumes also valid Luks2 bodies | ||
self.Top = Lift(self.join_header_and_body) * LuksHeader * (Luks2Body | Luks1Body) << EOF | ||
|
||
def join_header_and_body(self, header, body): | ||
return dict([header] + body) | ||
|
||
def convert_type(self, section): | ||
section[1]["type"] = section[0][1] | ||
return [section[0][0], section[1]] | ||
|
||
def convert_status(self, section): | ||
section[2]["status"] = section[1] | ||
return [section[0], section[2]] | ||
|
||
def __call__(self, content): | ||
try: | ||
return self.Top(content) | ||
except Exception: | ||
raise ParseException("There was an exception when parsing one of the outputs of cryptsetup luksDump commands.") | ||
|
||
|
||
@parser(Specs.cryptsetup_luksDump) | ||
class LuksDump(Parser): | ||
""" | ||
Sample input data is in the format:: | ||
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: 3d c4 1b 52 fe 1c 90 d8 2a 35 b2 62 34 e9 0a 59 | ||
e9 0e 48 57 b2 dd 45 | ||
AF stripes: 4000 | ||
AF hash: sha256 | ||
Area offset:32768 [bytes] | ||
Area length:258048 [bytes] | ||
Digest ID: 0 | ||
Tokens: | ||
0: systemd-tpm2 | ||
Keyslot: 2 | ||
Digests: | ||
0: pbkdf2 | ||
Hash: sha256 | ||
Iterations: 129774 | ||
Salt: e6 31 d5 74 e0 65 83 82 35 03 29 56 0e 80 36 5c | ||
4d cd 4d f9 de 69 39 97 | ||
Digest: 21 aa b3 dc 9d 46 9b 0f 3a 0f 57 13 80 c6 0b bf | ||
67 66 9e 73 ed 7d 09 2c | ||
Examples: | ||
>>> type(parsed_result) | ||
<class 'insights.parsers.cryptsetup_luksDump.LuksDump'> | ||
>>> from pprint import pprint | ||
>>> pprint(parsed_result.dump["header"]) | ||
{'Epoch': '6', | ||
'Flags': '(no flags)', | ||
'Keyslots area': '16744448 [bytes]', | ||
'Label': '(no label)', | ||
'Metadata area': '16384 [bytes]', | ||
'Subsystem': '(no subsystem)', | ||
'UUID': 'cfbcc942-e06b-4c4a-952f-e9c9b2011c27', | ||
'Version': '2'} | ||
>>> pprint(parsed_result.dump["Keyslots"]["0"]) | ||
{'AF hash': 'sha256', | ||
'AF stripes': '4000', | ||
'Area length': '258048 [bytes]', | ||
'Area offset': '32768 [bytes]', | ||
'Cipher': 'aes-xts-plain64', | ||
'Cipher key': '512 bits', | ||
'Digest ID': '0', | ||
'Key': '512 bits', | ||
'Memory': '1048576', | ||
'PBKDF': 'argon2id', | ||
'Priority': 'normal', | ||
'Salt': '3d c4 1b 52 fe 1c 90 d8 2a 35 b2 62 34 e9 0a 59 e9 0e 48 57 b2 dd 45', | ||
'Threads': '4', | ||
'Time cost': '7', | ||
'type': 'luks2'} | ||
>>> parsed_result.dump["Tokens"]["0"]["type"] | ||
'systemd-tpm2' | ||
Attributes: | ||
dump(dict of dicts): A top level dict containing the dictionaries | ||
representing the header, data segments, keyslots, digests | ||
and tokens. | ||
""" # noqa | ||
|
||
def __init__(self, context): | ||
self.parse_dump = DocParser() | ||
super(LuksDump, self).__init__(context) | ||
|
||
def parse_content(self, content): | ||
if len(content) == 0 or (len(content) == 1 and "not a valid LUKS" in content[0]): | ||
raise SkipComponent | ||
self.dump = self.parse_dump("\n".join(content).replace("\t", " " * 8) + "\n") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
""" | ||
Custom datasource for gathering a list of encrypted LUKS block devices and their properties. | ||
""" | ||
from insights.components.cryptsetup import HasCryptsetupWithTokens, HasCryptsetupWithoutTokens | ||
from insights.core.context import HostContext | ||
from insights.core.dr import SkipComponent | ||
from insights.core.plugins import datasource | ||
from insights.core.spec_factory import DatasourceProvider, foreach_execute | ||
from insights.parsers.blkid import BlockIDInfo | ||
from insights.specs import Specs | ||
import re | ||
|
||
|
||
@datasource(BlockIDInfo, HostContext) | ||
def luks_block_devices(broker): | ||
""" | ||
This datasource provides a list of LUKS encrypted device. | ||
Sample data returned:: | ||
['/dev/sda', '/dev/nvme0n1p3'] | ||
Returns: | ||
list: List of the LUKS encrypted block devices. | ||
Raises: | ||
SkipComponent: When there is not any LUKS encrypted block device on the | ||
system. | ||
""" | ||
|
||
block_id = broker[BlockIDInfo] | ||
if block_id: | ||
devices = block_id.filter_by_type("crypto_LUKS") | ||
if devices: | ||
return sorted(map(lambda x: x["NAME"], devices)) | ||
|
||
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]) | ||
|
||
|
||
def line_indentation(line): | ||
""" | ||
Compute line indentation level | ||
Arguments: | ||
line(str): The whole line | ||
Returns: | ||
int: the number of spaces the line is indentated by | ||
""" | ||
line = line.replace("\t", " " * 8) | ||
return len(line) - len(line.lstrip()) | ||
|
||
|
||
def filter_token_lines(lines): | ||
""" | ||
Filter out token descriptions to keep just the Keyslot filed | ||
Arguments: | ||
lines(list): List of lines of the luksDump output | ||
Returns: | ||
list: The original lines, except the tokens section only contains only token name and associated keyslot | ||
""" | ||
in_tokens = False | ||
remove_indices = [] | ||
|
||
for i, line in enumerate(lines): | ||
if line == "Tokens:": | ||
in_tokens = True | ||
continue | ||
|
||
if in_tokens and line_indentation(line) == 0: | ||
in_tokens = False | ||
|
||
if not in_tokens or line_indentation(line) == 2 or line.startswith("\tKeyslot:"): | ||
continue | ||
|
||
remove_indices.append(i) | ||
|
||
return [i for j, i in enumerate(lines) if j not in remove_indices] | ||
|
||
|
||
@datasource(HostContext, [LocalSpecs.cryptsetup_luks_dump_token_commands, LocalSpecs.cryptsetup_luks_dump_commands]) | ||
def luks_data_sources(broker): | ||
""" | ||
This datasource provides the output of 'cryptsetup luksDump' command for | ||
every LUKS encrypted device on the system. The digest and salt fields are | ||
filtered out as they can be potentially sensitive. | ||
Returns: | ||
list: List of outputs of the cryptsetup luksDump command. | ||
Raises: | ||
SkipComponent: When there is not any LUKS encrypted block device on the | ||
system. | ||
""" | ||
datasources = [] | ||
|
||
commands = [] | ||
if LocalSpecs.cryptsetup_luks_dump_token_commands in broker: | ||
commands.extend(broker[LocalSpecs.cryptsetup_luks_dump_token_commands]) | ||
if LocalSpecs.cryptsetup_luks_dump_commands in broker: | ||
commands.extend(broker[LocalSpecs.cryptsetup_luks_dump_commands]) | ||
|
||
for command in commands: | ||
lines_without_tokens = filter_token_lines(command.content) | ||
|
||
regex = re.compile(r'[\t ]*(MK digest:|MK salt:|Salt:|Digest:)(\s*([a-z0-9][a-z0-9] ){16}\n)*(\s*([a-z0-9][a-z0-9] )+\n)?', flags=re.IGNORECASE) | ||
filtered_content = regex.sub("", "\n".join(lines_without_tokens) + "\n") | ||
|
||
datasources.append( | ||
DatasourceProvider(content=filtered_content, relative_path="insights_commands/" + command.cmd.replace("/", ".").replace(" ", "_")) | ||
) | ||
|
||
if datasources: | ||
return datasources | ||
|
||
raise SkipComponent |
Oops, something went wrong.