systemdubuntu-22.04tpmubuntu-24.04luks

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?


Solution

  • 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)