Skip to content

Commit 493cccc

Browse files
authored
Added a HSM menu entry (#1196)
* Added a HSM menu entry, but also a safety check to make sure a FIDO device is connected * flake8 complaints * Adding FIDO lookup using cryptenroll listing * Added systemd-cryptenroll --fido2-device=list * Removed old _select_hsm call * Fixed flake8 complaints * Added support for locking and unlocking with a HSM * Removed hardcoded paths in favor of PR merge * Removed hardcoded paths in favor of PR merge * Fixed mypy complaint * Flake8 issue * Added sd-encrypt for HSM and revert back to encrypt when HSM is not used (stability reason) * Added /etc/vconsole.conf and tweaked fido2_enroll() to use the proper paths * Spelling error * Using UUID instead of PARTUUID when using HSM. I can't figure out how to get sd-encrypt to use PARTUUID instead. Added a Partition().part_uuid function. Actually renamed .uuid to .part_uuid and created a .uuid instead. * Adding missing package libfido2 and removed tpm2-device=auto as it overrides everything and forces password prompt to be used over FIDO2, no matter the order of the options. * Added some notes to clarify some choices. * Had to move libfido2 package install to later in the chain, as there's not even a base during mounting :P
1 parent 561ea7e commit 493cccc

17 files changed

Lines changed: 242 additions & 27 deletions

File tree

.flake8

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[flake8]
22
count = True
33
# Several of the following could be autofixed or improved by running the code through psf/black
4-
ignore = E123,E126,E128,E203,E231,E261,E302,E402,E722,F541,W191,W292,W293
4+
ignore = E123,E126,E128,E203,E231,E261,E302,E402,E722,F541,W191,W292,W293,W503
55
max-complexity = 40
66
max-line-length = 236
77
show-source = True

archinstall/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@
4545
from .lib.translation import Translation, DeferredTranslation
4646
from .lib.plugins import plugins, load_plugin # This initiates the plugin loading ceremony
4747
from .lib.configuration import *
48+
from .lib.udev import udevadm_info
49+
from .lib.hsm import (
50+
get_fido2_devices,
51+
fido2_enroll
52+
)
4853
parser = ArgumentParser()
4954

5055
__version__ = "2.4.2"

archinstall/lib/configuration.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
import json
22
import logging
3-
from pathlib import Path
3+
import pathlib
44
from typing import Optional, Dict
55

66
from .storage import storage
77
from .general import JSON, UNSAFE_JSON
88
from .output import log
9-
9+
from .exceptions import RequirementError
10+
from .hsm import get_fido2_devices
11+
12+
def configuration_sanity_check():
13+
if storage['arguments'].get('HSM'):
14+
if not get_fido2_devices():
15+
raise RequirementError(
16+
f"In order to use HSM to pair with the disk encryption,"
17+
+ f" one needs to be accessible through /dev/hidraw* and support"
18+
+ f" the FIDO2 protocol. You can check this by running"
19+
+ f" 'systemd-cryptenroll --fido2-device=list'."
20+
)
1021

1122
class ConfigurationOutput:
1223
def __init__(self, config: Dict):
@@ -21,7 +32,7 @@ def __init__(self, config: Dict):
2132
self._user_credentials = {}
2233
self._disk_layout = None
2334
self._user_config = {}
24-
self._default_save_path = Path(storage.get('LOG_PATH', '.'))
35+
self._default_save_path = pathlib.Path(storage.get('LOG_PATH', '.'))
2536
self._user_config_file = 'user_configuration.json'
2637
self._user_creds_file = "user_credentials.json"
2738
self._disk_layout_file = "user_disk_layout.json"
@@ -84,7 +95,7 @@ def show(self):
8495

8596
print()
8697

87-
def _is_valid_path(self, dest_path :Path) -> bool:
98+
def _is_valid_path(self, dest_path :pathlib.Path) -> bool:
8899
if (not dest_path.exists()) or not (dest_path.is_dir()):
89100
log(
90101
'Destination directory {} does not exist or is not a directory,\n Configuration files can not be saved'.format(dest_path.resolve()),
@@ -93,26 +104,26 @@ def _is_valid_path(self, dest_path :Path) -> bool:
93104
return False
94105
return True
95106

96-
def save_user_config(self, dest_path :Path = None):
107+
def save_user_config(self, dest_path :pathlib.Path = None):
97108
if self._is_valid_path(dest_path):
98109
with open(dest_path / self._user_config_file, 'w') as config_file:
99110
config_file.write(self.user_config_to_json())
100111

101-
def save_user_creds(self, dest_path :Path = None):
112+
def save_user_creds(self, dest_path :pathlib.Path = None):
102113
if self._is_valid_path(dest_path):
103114
if user_creds := self.user_credentials_to_json():
104115
target = dest_path / self._user_creds_file
105116
with open(target, 'w') as config_file:
106117
config_file.write(user_creds)
107118

108-
def save_disk_layout(self, dest_path :Path = None):
119+
def save_disk_layout(self, dest_path :pathlib.Path = None):
109120
if self._is_valid_path(dest_path):
110121
if disk_layout := self.disk_layout_to_json():
111122
target = dest_path / self._disk_layout_file
112123
with target.open('w') as config_file:
113124
config_file.write(disk_layout)
114125

115-
def save(self, dest_path :Path = None):
126+
def save(self, dest_path :pathlib.Path = None):
116127
if not dest_path:
117128
dest_path = self._default_save_path
118129

archinstall/lib/disk/blockdevice.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ def get_partition(self, uuid :str) -> Partition:
275275
count = 0
276276
while count < 5:
277277
for partition_uuid, partition in self.partitions.items():
278-
if partition.uuid.lower() == uuid.lower():
278+
if partition.part_uuid.lower() == uuid.lower():
279279
return partition
280280
else:
281281
log(f"uuid {uuid} not found. Waiting for {count +1} time",level=logging.DEBUG)

archinstall/lib/disk/filesystem.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def load_layout(self, layout :Dict[str, Any]) -> None:
150150

151151
if partition.get('boot', False):
152152
log(f"Marking partition {partition['device_instance']} as bootable.")
153-
self.set(self.partuuid_to_index(partition['device_instance'].uuid), 'boot on')
153+
self.set(self.partuuid_to_index(partition['device_instance'].part_uuid), 'boot on')
154154

155155
prev_partition = partition
156156

@@ -193,7 +193,7 @@ def use_entire_disk(self, root_filesystem_type :str = 'ext4') -> Partition:
193193
def add_partition(self, partition_type :str, start :str, end :str, partition_format :Optional[str] = None) -> Partition:
194194
log(f'Adding partition to {self.blockdevice}, {start}->{end}', level=logging.INFO)
195195

196-
previous_partition_uuids = {partition.uuid for partition in self.blockdevice.partitions.values()}
196+
previous_partition_uuids = {partition.part_uuid for partition in self.blockdevice.partitions.values()}
197197

198198
if self.mode == MBR:
199199
if len(self.blockdevice.partitions) > 3:
@@ -210,7 +210,7 @@ def add_partition(self, partition_type :str, start :str, end :str, partition_for
210210
count = 0
211211
while count < 10:
212212
new_uuid = None
213-
new_uuid_set = (previous_partition_uuids ^ {partition.uuid for partition in self.blockdevice.partitions.values()})
213+
new_uuid_set = (previous_partition_uuids ^ {partition.part_uuid for partition in self.blockdevice.partitions.values()})
214214

215215
if len(new_uuid_set) > 0:
216216
new_uuid = new_uuid_set.pop()
@@ -236,7 +236,7 @@ def add_partition(self, partition_type :str, start :str, end :str, partition_for
236236
# TODO: This should never be able to happen
237237
log(f"Could not find the new PARTUUID after adding the partition.", level=logging.ERROR, fg="red")
238238
log(f"Previous partitions: {previous_partition_uuids}", level=logging.ERROR, fg="red")
239-
log(f"New partitions: {(previous_partition_uuids ^ {partition.uuid for partition in self.blockdevice.partitions.values()})}", level=logging.ERROR, fg="red")
239+
log(f"New partitions: {(previous_partition_uuids ^ {partition.part_uuid for partition in self.blockdevice.partitions.values()})}", level=logging.ERROR, fg="red")
240240
raise DiskError(f"Could not add partition using: {parted_string}")
241241

242242
def set_name(self, partition: int, name: str) -> bool:

archinstall/lib/disk/partition.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ def partition_type(self) -> Optional[str]:
184184
return device['pttype']
185185

186186
@property
187-
def uuid(self) -> Optional[str]:
187+
def part_uuid(self) -> Optional[str]:
188188
"""
189189
Returns the PARTUUID as returned by lsblk.
190190
This is more reliable than relying on /dev/disk/by-partuuid as
@@ -197,6 +197,26 @@ def uuid(self) -> Optional[str]:
197197

198198
time.sleep(max(0.1, storage['DISK_TIMEOUTS'] * i))
199199

200+
partuuid = self._safe_part_uuid
201+
if partuuid:
202+
return partuuid
203+
204+
raise DiskError(f"Could not get PARTUUID for {self.path} using 'blkid -s PARTUUID -o value {self.path}'")
205+
206+
@property
207+
def uuid(self) -> Optional[str]:
208+
"""
209+
Returns the UUID as returned by lsblk for the **partition**.
210+
This is more reliable than relying on /dev/disk/by-uuid as
211+
it doesn't seam to be able to detect md raid partitions.
212+
For bind mounts all the subvolumes share the same uuid
213+
"""
214+
for i in range(storage['DISK_RETRY_ATTEMPTS']):
215+
if not self.partprobe():
216+
raise DiskError(f"Could not perform partprobe on {self.device_path}")
217+
218+
time.sleep(max(0.1, storage['DISK_TIMEOUTS'] * i))
219+
200220
partuuid = self._safe_uuid
201221
if partuuid:
202222
return partuuid
@@ -216,6 +236,28 @@ def _safe_uuid(self) -> Optional[str]:
216236

217237
log(f"Could not reliably refresh PARTUUID of partition {self.device_path} due to partprobe error.", level=logging.DEBUG)
218238

239+
try:
240+
return SysCommand(f'blkid -s UUID -o value {self.device_path}').decode('UTF-8').strip()
241+
except SysCallError as error:
242+
if self.block_device.info.get('TYPE') == 'iso9660':
243+
# Parent device is a Optical Disk (.iso dd'ed onto a device for instance)
244+
return None
245+
246+
log(f"Could not get PARTUUID of partition using 'blkid -s UUID -o value {self.device_path}': {error}")
247+
248+
@property
249+
def _safe_part_uuid(self) -> Optional[str]:
250+
"""
251+
A near copy of self.uuid but without any delays.
252+
This function should only be used where uuid is not crucial.
253+
For instance when you want to get a __repr__ of the class.
254+
"""
255+
if not self.partprobe():
256+
if self.block_device.info.get('TYPE') == 'iso9660':
257+
return None
258+
259+
log(f"Could not reliably refresh PARTUUID of partition {self.device_path} due to partprobe error.", level=logging.DEBUG)
260+
219261
try:
220262
return SysCommand(f'blkid -s PARTUUID -o value {self.device_path}').decode('UTF-8').strip()
221263
except SysCallError as error:

archinstall/lib/general.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ def _encode(obj :Any) -> Any:
135135
return obj.isoformat()
136136
elif isinstance(obj, (list, set, tuple)):
137137
return [json.loads(json.dumps(item, cls=JSON)) for item in obj]
138+
elif isinstance(obj, (pathlib.Path)):
139+
return str(obj)
138140
else:
139141
return obj
140142

archinstall/lib/hsm/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .fido import (
2+
get_fido2_devices,
3+
fido2_enroll
4+
)

archinstall/lib/hsm/fido.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import typing
2+
import pathlib
3+
from ..general import SysCommand, SysCommandWorker, clear_vt100_escape_codes
4+
from ..disk.partition import Partition
5+
6+
def get_fido2_devices() -> typing.Dict[str, typing.Dict[str, str]]:
7+
"""
8+
Uses systemd-cryptenroll to list the FIDO2 devices
9+
connected that supports FIDO2.
10+
Some devices might show up in udevadm as FIDO2 compliant
11+
when they are in fact not.
12+
13+
The drawback of systemd-cryptenroll is that it uses human readable format.
14+
That means we get this weird table like structure that is of no use.
15+
16+
So we'll look for `MANUFACTURER` and `PRODUCT`, we take their index
17+
and we split each line based on those positions.
18+
"""
19+
worker = clear_vt100_escape_codes(SysCommand(f"systemd-cryptenroll --fido2-device=list").decode('UTF-8'))
20+
21+
MANUFACTURER_POS = 0
22+
PRODUCT_POS = 0
23+
devices = {}
24+
for line in worker.split('\r\n'):
25+
if '/dev' not in line:
26+
MANUFACTURER_POS = line.find('MANUFACTURER')
27+
PRODUCT_POS = line.find('PRODUCT')
28+
continue
29+
30+
path = line[:MANUFACTURER_POS].rstrip()
31+
manufacturer = line[MANUFACTURER_POS:PRODUCT_POS].rstrip()
32+
product = line[PRODUCT_POS:]
33+
34+
devices[path] = {
35+
'manufacturer' : manufacturer,
36+
'product' : product
37+
}
38+
39+
return devices
40+
41+
def fido2_enroll(hsm_device_path :pathlib.Path, partition :Partition, password :str) -> bool:
42+
worker = SysCommandWorker(f"systemd-cryptenroll --fido2-device={hsm_device_path} {partition.real_device}", peak_output=True)
43+
pw_inputted = False
44+
while worker.is_alive():
45+
if pw_inputted is False and bytes(f"please enter current passphrase for disk {partition.real_device}", 'UTF-8') in worker._trace_log.lower():
46+
worker.write(bytes(password, 'UTF-8'))
47+
pw_inputted = True

0 commit comments

Comments
 (0)