The problem:
On Ubuntu 24.04 i use the command:
systemd-cryptenroll \
--tpm2-device=auto \
--tpm2-pcrs=0:sha256=60d6401905da60ba1610c36a6c7e21e4f6fb75750e0bcc80049d29eb7c01d8a4 \
--tpm2-pcrs=1:sha256=4975eb86a8ec8053c321adcb2aea978a840fd9304c6076cd2bdccac387bd7904 \
--tpm2-pcrs=4:sha256=7f0326c0506de8253b1842c01124d36ef400a42e9ee0966917135568aaa31728 \
--tpm2-pcrs=5:sha256=7787a346d056e0c2cd99be78f2152cba61a592f0f594c9e719029cb6631c77e6 \
--tpm2-pcrs=7:sha256=0d6f5076088673485ad84f4010b87acb7d50fb38642c948a2d7db3c29d1270e3 \
--unlock-key-file=/root/keyfile \
/dev/mmcblk1p2
to set precalculated PCR-Values and register an TPM2-Keyslot.
On Ubuntu 22.04 it's no possible:
systemd 249 (249.11-0ubuntu3.12)
Failed to parse PCR number: 0:sha256=60d6401905da60ba1610c36a6c7e21e4f6fb75750e0bcc80049d29eb7c01d8a4
The interface seams only expect:
[...] --tpm2-pcrs=0,1,2,3,... [...]
It's not possible to set hash values.
My question:
Is there another way or application to do that?
Is there another way or application to do that?
Sure – upgrade to a more recent systemd version (e.g. maybe Ubuntu 22.04 has it in backports). You can even build systemd from tarball/git locally, and use just the updated 'systemd-cryptenroll' binary out of the build directory without having to "install" it over the main systemd.
Alternatively, write your own tool which seals a keyslot password to the custom PCRs using generic TPM2 tools (e.g. tpm2_create
from Intel TSS). Systemd-cryptenroll stores the sealed data as LUKS2 "token" – you can enroll a disk, then use cryptsetup token export
to see what format it uses (it's JSON), and just replace it with your custom-generated token via cryptsetup token import
.
I wrote such a tool a few years ago. Note that this code does not implement any of the "anti-snooping" protections that later systemd versions have introduced. But if you're using an internal fTPM (not a discrete TPM chip), then it's fine.
#!/usr/bin/env python3
# (c) 2021 Mantas Mikulėnas <grawity@gmail.com>
# Released under the MIT license <https://spdx.org/licenses/MIT>
from pprint import pprint
import argparse
import base64
import binascii
import os
import json
import subprocess
import tempfile
DEFAULT_PCR_SPEC = "sha256:0,2,4,7,9,12"
def is_tpm2():
return os.path.exists("/dev/tpmrm0")
def is_efi():
var = "BootCurrent-8be4df61-93ca-11d2-aa0d-00e098032b8c"
return os.path.exists(f"/sys/firmware/efi/efivars/{var}")
def is_systemd_boot():
var = "LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
return os.path.exists(f"/sys/firmware/efi/efivars/{var}")
def check_sd_loader(wanted):
var = "LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
path = f"/sys/firmware/efi/efivars/{var}"
if os.path.exists(path):
with open(path, "rb") as fh:
_ = fh.read(4)
current = fh.read().decode("utf-16le").rstrip("\0")
print(f"booted from loader entry {current!r}")
return (current == wanted)
else:
print("booted via unknown (non-systemd-boot) loader")
return False
def is_service_active(name):
res = subprocess.run(["systemctl", "is-active", "--quiet", name])
return (res.returncode == 0)
def tpm2_create_ecc_primary():
out_context = tempfile.NamedTemporaryFile()
attributes = ["fixedtpm", "fixedparent", "sensitivedataorigin",
"userwithauth", "restricted", "decrypt"]
subprocess.run(["tpm2_createprimary",
"--quiet",
"--hierarchy=o",
"--hash-algorithm=sha256",
"--key-algorithm=ecc256:null:aes128cfb",
"--attributes=" + "|".join(attributes),
"--key-context=" + out_context.name],
check=True)
return out_context
def tpm2_create_pcr_policy(pcr_spec, pcr_data_file):
out_policy = tempfile.NamedTemporaryFile()
subprocess.run(["tpm2_createpolicy",
"--quiet",
"--policy-pcr",
"--pcr-list=" + pcr_spec,
"--pcr=" + pcr_data_file,
"--policy=" + out_policy.name],
check=True)
return out_policy
def tpm2_create_sealed(parent_context, input_file, policy_file):
out_private = tempfile.NamedTemporaryFile()
out_public = tempfile.NamedTemporaryFile()
attributes = ["fixedtpm", "fixedparent", "adminwithpolicy", "noda"]
subprocess.run(["tpm2_create",
"--quiet",
"--parent-context=" + parent_context,
"--hash-algorithm=sha256",
"--attributes=" + "|".join(attributes),
"--sealing-input=" + input_file,
"--policy=" + policy_file,
"--private=" + out_private.name,
"--public=" + out_public.name],
check=True)
return out_private, out_public
def luks2_read_header(device):
# https://habd.as/post/external-backup-drive-encryption/assets/luks2_doc_wip.pdf
with open(device, "rb") as fh:
magic = fh.read(6)
version = fh.read(2) # u16
hdr_size = fh.read(8) # u64
seqid = fh.read(8) # u64
label = fh.read(48)
csum_alg = fh.read(32)
salt = fh.read(64)
uuid = fh.read(40)
subsys = fh.read(48)
hdr_offset = fh.read(8) # u64
_ = fh.read(184)
csum = fh.read(64)
_ = fh.read(7*512)
fixed_hdr_size = 4096
hdr_size = int.from_bytes(hdr_size, "big")
json_data = fh.read(hdr_size - fixed_hdr_size)
json_data, _, _ = json_data.partition(b"\0")
json_data = json.loads(json_data)
return None, json_data
def luks_list_tokens(device):
_, json_data = luks2_read_header(device)
return json_data["tokens"]
def luks_remove_token(device, token_id):
subprocess.run(["cryptsetup", "token", "remove",
device,
"--token-id=" + str(token_id)],
check=True)
def luks_import_token(device, token, token_id, keyslot):
# TODO: With next cryptsetup, use --token-replace instead of explicitly deleting the old one.
subprocess.run(["cryptsetup", "token", "import",
device,
#"--debug",
"--token-id=" + str(token_id),
"--key-slot=" + str(keyslot)],
input=json.dumps(token).encode(),
check=True)
return token_id
def luks_export_token(device, token_id):
res = subprocess.run(["cryptsetup", "token", "export",
device,
"--token-id=" + str(token_id)],
stdout=subprocess.PIPE,
check=True)
return json.loads(res.stdout)
def seal_key_to_pcrs(key_data, pcr_spec):
temp_pcr = tempfile.NamedTemporaryFile()
if args.future:
print("Generating future PCR values...")
subprocess.run(["tpm_futurepcr", "-L", pcr_spec, "-o", temp_pcr.name], check=True)
if args.verbose:
subprocess.run(["tpm_futurepcr", "-L", pcr_spec])
else:
print("Reading current PCR values...")
subprocess.run(["tpm2_pcrread", "-Q", "-o", temp_pcr.name, pcr_spec], check=True)
if args.verbose:
subprocess.run(["tpm2_pcrread", pcr_spec])
print("Creating primary object...")
temp_primary = tpm2_create_ecc_primary()
print("Creating trial policy...")
temp_policy = tpm2_create_pcr_policy(pcr_spec, temp_pcr.name)
with open(temp_policy.name, "rb") as fh:
policy_hash = fh.read()
print("Policy hash is", binascii.hexlify(policy_hash).decode())
print("Creating sealed object...")
parent_context = temp_primary.name
temp_input = tempfile.NamedTemporaryFile()
with open(temp_input.name, "wb") as fh:
fh.write(key_data)
temp_private, temp_public = tpm2_create_sealed(parent_context,
temp_input.name,
temp_policy.name)
with open(temp_private.name, "rb") as fh:
priv_data = fh.read()
with open(temp_public.name, "rb") as fh:
pub_data = fh.read()
return priv_data, pub_data, policy_hash
test_unseal = False
if not is_tpm2():
exit("error: non-TPM2 systems are not supported")
parser = argparse.ArgumentParser()
parser.add_argument("--keyslot", default="2")
parser.add_argument("-d", "--device", default="/dev/nvme0n1p2")
parser.add_argument("-F", "--future", action="store_true",
help="use future PCRs instead of current")
parser.add_argument("-m", "--mkinitcpio", action="store_true",
help="internal option")
parser.add_argument("-L", "--pcr-spec", default=DEFAULT_PCR_SPEC)
parser.add_argument("-v", "--verbose", action="store_true")
args = parser.parse_args()
luks_keyslot = args.keyslot
luks_device = args.device
key_file = f"/etc/luks/system_slot{luks_keyslot}.key"
pcr_spec = args.pcr_spec
hash_alg, pcr_list = pcr_spec.split(":")
if hash_alg != "sha256":
exit("Only the sha256 PCR bank is supported.")
if is_service_active("tpm2-abrmd.service"):
os.environ["TPM2TOOLS_TCTI"] = "tabrmd"
if args.verbose:
print("Using userspace TPM resource manager (tpm2-abrmd)")
else:
os.environ["TPM2TOOLS_TCTI"] = "device:/dev/tpmrm0"
print("Using in-kernel TPM resource manager")
_, luks_hdr = luks2_read_header(luks_device)
if args.verbose:
print("Current LUKS extended header:")
pprint(luks_hdr)
if os.path.exists(key_file):
with open(key_file, "rb") as fh:
print(f"Reading key from {key_file!r}")
key_data = base64.b64decode(fh.read())
elif luks_keyslot in luks_hdr["keyslots"]:
exit(f"error: Keyslot {luks_keyslot!r} exists but no key file was found.")
else:
print(f"Generating a new key")
key_data = os.urandom(32)
with open(key_file, "xb") as fh:
print(f"Storing the key in {key_file!r}")
fh.write(base64.b64encode(key_data))
if luks_keyslot in luks_hdr["keyslots"]:
print(f"Keyslot {luks_keyslot!r} exists in LUKS header, assuming it matches keyfile")
else:
print(f"Adding key {key_file!r} as keyslot {luks_keyslot!r}")
subprocess.run(["cryptsetup", "luksAddKey",
"--key-slot", luks_keyslot,
luks_device, key_file],
check=True)
priv_data, pub_data, policy_hash = seal_key_to_pcrs(key_data, pcr_spec)
new_token = {"type": "systemd-tpm2",
"keyslots": [luks_keyslot],
"tpm2-blob": base64.b64encode(priv_data + pub_data).decode(),
"tpm2-pcrs": [int(p) for p in pcr_list.split(",")],
"tpm2-policy-hash": binascii.hexlify(policy_hash).decode()}
if args.verbose:
print("New token:")
pprint(new_token)
overwrite_token_id = None
for id, old_token in luks_hdr["tokens"].items():
if args.verbose:
print(f"Found old token {id!r}:")
pprint(old_token)
if old_token["type"] == "systemd-tpm2":
if old_token["keyslots"] == [luks_keyslot]:
print(f"Found matching token {id!r} (a systemd TPM2 token), will overwrite.")
overwrite_token_id = id
if overwrite_token_id is not None:
token_id = overwrite_token_id
print(f"Removing old token {token_id!r}")
luks_remove_token(luks_device, token_id)
else:
token_id = None
max_tokens = 10
for id in map(str, range(max_tokens)):
if id not in luks_hdr["tokens"]:
token_id = id
break
if token_id is None:
exit("Error: Could not find a free token slot")
print(f"Using free token slot {token_id!r}")
print(f"Importing new token {token_id!r}")
luks_import_token(luks_device, new_token, token_id, luks_keyslot)