TR-eenet
TR-eenet

Reputation: 33

How are enroll precalculated PCR 1 SHA256 value

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?

Upvotes: 0

Views: 43

Answers (1)

grawity_u1686
grawity_u1686

Reputation: 16572

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 <[email protected]>
# 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)

Upvotes: 1

Related Questions