├── .gitignore ├── LICENSE ├── README.md ├── doap.turtle ├── requirements.txt ├── setup.py └── tpm_futurepcr ├── __init__.py ├── __main__.py ├── binary_reader.py ├── device_path.py ├── event_log.py ├── pcr_bank.py ├── systemd_boot.py ├── tpm_constants.py └── util.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | /*.egg-info 3 | /build 4 | /dist 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License (also available at ) 2 | 3 | (c) 2019 Mantas Mikulėnas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included 14 | in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | The `tpm_futurepcr` script allows pre-calculating what the future PCR4 value will be after a kernel upgrade, before you reboot. This is useful when your rootfs is LUKS-encrypted with a key sealed by the TPM against PCR4 (among others). 2 | 3 | This script only recognizes measurements done by native UEFI LoadImage() – i.e. hashes of PE/COFF executables such as vmlinuz.efi. (Although it does parse the TPM 1.2 event log, it does not (yet) recognize measurements done by TrustedGRUB on BIOS systems, and in fact I'm not entirely sure whether the entire premise of sealing data against user-specified PCR values is even _possible_ in the TPM 1.2 API.) 4 | 5 | As an additional hack, this script also recognizes systemd-boot and updates its EV\_IPL event according to the future kernel command line. 6 | 7 | This script will understand the event log in both SHA1-only (TPM 1.2) and Crypto-Agile (TPM 2.0, Linux kernel 5.3+) formats. 8 | 9 | ### Other similar projects 10 | 11 | - 12 | 13 | ### Warning 14 | 15 | Until Linux 5.17, neither systemd-boot nor EFISTUB measure the loaded initrd images, making it unsafe to rely on PCR4 alone. (Starting with Linux 5.17, the initrd measurements are now stored in PCR9; this script does not yet support pre-calculating it.) Additionally, only systemd-boot measures the _command line_ into PCR8; EFISTUB on its own does not. 16 | 17 | It is recommended to use PCR-based sealing (whether it is PCR4 with tpm\_futurepcr or PCR7 with Secure Boot) only with a combined [systemd-stub][] "kernel + initramfs" image, such as the one produced by `mkinitcpio -U`. 18 | 19 | [systemd-stub]: https://www.freedesktop.org/software/systemd/man/systemd-stub.html 20 | 21 | ### Dependencies 22 | 23 | - python-signify (for calculating Authenticode digests) 24 | - tpm2-tools (for reading current PCR values in kernels older than v5.12) 25 | 26 | ### Installation 27 | 28 | `python setup.py install` 29 | 30 | ### Usage 31 | 32 | Normally sealing data against PCRs starts by creating a "policy" which specifies the PCR values. In the Intel TPM 2.0 stack, this is done with *tpm2_createpolicy*: 33 | 34 | tpm2_createpolicy --policy-pcr --pcr-list=sha256:0,2,4,7 --policy=policy.bin 35 | 36 | This automatically uses current PCR values, and can be written to do so explicitly: 37 | 38 | tpm2_pcrread sha256:0,2,4,7 -Q -o pcrvalues.bin 39 | tpm2_createpolicy --policy-pcr --pcr-list=sha256:0,2,4,7 --pcr=pcrvalues.bin --policy=policy.bin 40 | 41 | To do the same with *future* PCR values, use tpm\_futurepcr: 42 | 43 | tpm_futurepcr -L 0,2,4,7 -o pcrvalues.bin 44 | tpm2_createpolicy --policy-pcr --pcr-list=sha256:0,2,4,7 --pcr=pcrvalues.bin --policy=policy.bin 45 | -------------------------------------------------------------------------------- /doap.turtle: -------------------------------------------------------------------------------- 1 | # rapper -q -i turtle -o rdfxml-abbrev doap.turtle 2 | 3 | @prefix doap: . 4 | @prefix foaf: . 5 | 6 | <> 7 | a doap:Project; 8 | doap:name "tpm_futurepcr"; 9 | doap:homepage ; 10 | doap:shortdesc "Tool to replay TPM event logs and predict future PCR values"@en; 11 | doap:created "2019-02-22"; 12 | doap:programming-language "Python"; 13 | doap:license ; 14 | doap:repository [ 15 | a doap:GitRepository; 16 | doap:location 17 | ]; 18 | doap:bug-database ; 19 | doap:maintainer . 20 | 21 | 22 | a foaf:Person; 23 | foaf:name "Mantas Mikulėnas"; 24 | foaf:mbox . 25 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | signify 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | setup(name="tpm_futurepcr", 3 | version="1.5", 4 | description="Calculate future TPM PCR[4] after a kernel upgrade", 5 | url="https://github.com/grawity/tpm_futurepcr", 6 | author="Mantas Mikulėnas", 7 | author_email="grawity@gmail.com", 8 | license="MIT", 9 | packages=["tpm_futurepcr"], 10 | install_requires=["signify"], 11 | entry_points={ 12 | "console_scripts": [ 13 | "tpm_futurepcr = tpm_futurepcr:main", 14 | ], 15 | }) 16 | -------------------------------------------------------------------------------- /tpm_futurepcr/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import sys 4 | 5 | from .event_log import * 6 | from .pcr_bank import * 7 | from .systemd_boot import ( 8 | loader_encode_pcr8, 9 | loader_decode_pcr8, 10 | loader_get_next_cmdline, 11 | ) 12 | from .tpm_constants import TpmEventType 13 | from .util import * 14 | 15 | hash_algs_to_tpm = { 16 | "sha1": TpmAlgorithm.SHA1, 17 | "sha256": TpmAlgorithm.SHA256, 18 | } 19 | 20 | def parse_pcr_list(args): 21 | hash_alg = None 22 | if args.pcr_list: 23 | verbose_all_pcrs = False 24 | pcr_list = args.pcr_list 25 | if "+" in pcr_list: 26 | exit("error: PCR specifier may only contain one bank.") 27 | if ":" in pcr_list: 28 | bank_spec = pcr_list.split(":") 29 | hash_alg = bank_spec[0] 30 | pcr_list = bank_spec[1] 31 | wanted_pcrs = [int(idx) for idx in pcr_list.split(",")] 32 | else: 33 | verbose_all_pcrs = True 34 | wanted_pcrs = [*range(NUM_PCRS)] 35 | 36 | if args.hash_alg: 37 | if not hash_alg: 38 | hash_alg = args.hash_alg 39 | elif hash_alg != args.hash_alg: 40 | exit("error: Conflicting PCR hash algorithm specifications given.") 41 | 42 | if not hash_alg: 43 | print("warning: PCR hash algorithm now defaults to sha256, not sha1", 44 | file=sys.stderr) 45 | hash_alg = "sha256" 46 | 47 | return hash_alg, wanted_pcrs, verbose_all_pcrs 48 | 49 | def main(): 50 | parser = argparse.ArgumentParser() 51 | parser.add_argument("-L", "--pcr-list", 52 | help="limit output to specified PCR indexes") 53 | parser.add_argument("-H", "--hash-alg", 54 | help="specify the hash algorithm (sha1 or sha256)") 55 | parser.add_argument("-o", "--output", 56 | help="write binary PCR values to specified file") 57 | parser.add_argument("--allow-unexpected-bsa", action="store_true", 58 | help="accept BOOT_SERVICES_APPLICATION events with weird paths") 59 | parser.add_argument("--substitute-bsa-unix-path", action=KeyValueAction, 60 | help="substitute BOOT_SERVICES_APPLICATION path (syntax: =)") 61 | parser.add_argument("--compare", action="store_true", 62 | help="compare computed PCRs against live values") 63 | parser.add_argument("-v", "--verbose", action="store_true", 64 | help="show verbose information about log parsing") 65 | parser.add_argument("--log-path", 66 | help="read binary log from an alternative path") 67 | args = parser.parse_args() 68 | 69 | hash_alg, wanted_pcrs, verbose_all_pcrs = parse_pcr_list(args) 70 | 71 | try: 72 | tpm_hash_alg = hash_algs_to_tpm[hash_alg] 73 | except KeyError: 74 | exit("error: Unsupported hash algorithm %r" % hash_alg) 75 | 76 | this_pcrs = PcrBank(hash_alg) 77 | next_pcrs = PcrBank(hash_alg) 78 | last_efi_binary = None 79 | errors = 0 80 | 81 | for event in enum_log_entries(args.log_path): 82 | idx = event["pcr_idx"] 83 | 84 | _verbose_pcr = (args.verbose and (verbose_all_pcrs or idx in wanted_pcrs)) 85 | if _verbose_pcr: 86 | show_log_entry(event) 87 | 88 | if idx == 0xFFFFFFFF: 89 | if _verbose_pcr: 90 | print("event updates Windows virtual PCR[-1], skipping") 91 | continue 92 | 93 | this_extend_value = event["pcr_extend_values"].get(tpm_hash_alg) 94 | next_extend_value = this_extend_value 95 | 96 | if this_extend_value is None: 97 | if _verbose_pcr: 98 | print("event does not update the specified PCR bank, skipping") 99 | continue 100 | 101 | if event["event_type"] == TpmEventType.EFI_BOOT_SERVICES_APPLICATION: 102 | event_data = parse_efi_bsa_event(event["event_data"]) 103 | try: 104 | unix_path = device_path_to_unix_path(event_data["device_path_vec"]) 105 | if args.substitute_bsa_unix_path: 106 | unix_path = args.substitute_bsa_unix_path.get(unix_path, unix_path) 107 | except Exception as e: 108 | print(e) 109 | errors = 1 110 | unix_path = None 111 | 112 | if unix_path: 113 | file_hash = hash_pecoff(unix_path, hash_alg) 114 | next_extend_value = file_hash 115 | last_efi_binary = unix_path 116 | if _verbose_pcr: 117 | print("-- extending with coff hash --") 118 | print("file path =", unix_path) 119 | print("file hash =", to_hex(file_hash)) 120 | print("this event extend value =", to_hex(this_extend_value)) 121 | print("guessed extend value =", to_hex(next_extend_value)) 122 | else: 123 | # This might be a firmware item such as the boot menu. 124 | if not args.allow_unexpected_bsa: 125 | exit("error: Unexpected boot events found. Binding to these PCR values " 126 | "is not advised, as it might be difficult to reproduce this state " 127 | "later. Exiting.") 128 | print("warning: couldn't map EfiBootServicesApplication event to a Linux path", 129 | file=sys.stderr) 130 | 131 | # Handle systemd EFI stub "kernel command line" measurements (found in 132 | # PCR 8 up to systemd v250, but PCR 12 afterwards). 133 | if event["event_type"] == TpmEventType.IPL and (idx in wanted_pcrs): 134 | try: 135 | cmdline = loader_get_next_cmdline(last_efi_binary) 136 | if args.verbose: 137 | old_cmdline = event["event_data"] 138 | # 2022-03-19 grawity: In the past, we had to strip away the last \0 byte for 139 | # some reason (which I don't remember)... but apparently now we don't? Let's 140 | # add a warning so that hopefully I remember why it was necessary. 141 | if len(old_cmdline) % 2 != 0: 142 | print("warning: Expecting EV_IPL data to contain UTF-16, but length isn't a multiple of 2", 143 | file=sys.stderr) 144 | old_cmdline += b'\0' 145 | old_cmdline = loader_decode_pcr8(old_cmdline) 146 | print("-- extending with systemd-boot cmdline --") 147 | print("this cmdline:", repr(old_cmdline)) 148 | print("next cmdline:", repr(cmdline)) 149 | cmdline = loader_encode_pcr8(cmdline) 150 | next_extend_value = hash_bytes(cmdline, hash_alg) 151 | except FileNotFoundError: 152 | # Either some of the EFI variables, or the ESP, or the .conf, are missing. 153 | # It's probably not a systemd-boot environment, so PCR[8] meaning is undefined. 154 | if args.verbose: 155 | print("-- not touching non-systemd IPL event --") 156 | 157 | if event["event_type"] != TpmEventType.NO_ACTION: 158 | this_pcrs.extend_with_hash(idx, this_extend_value) 159 | next_pcrs.extend_with_hash(idx, next_extend_value) 160 | 161 | if _verbose_pcr: 162 | print("--> after this event, PCR %d contains value %s" % (idx, to_hex(this_pcrs[idx]))) 163 | print("--> after reboot, PCR %d will contain value %s" % (idx, to_hex(next_pcrs[idx]))) 164 | print() 165 | 166 | if errors: 167 | print("fatal errors occured", file=sys.stderr) 168 | exit(1) 169 | 170 | if args.compare: 171 | print("== Real vs computed PCR values ==") 172 | real_pcrs = read_current_pcrs(hash_alg) 173 | errors = 0 174 | print(" "*7, "%-*s" % (this_pcrs.pcr_size*2, "REAL"), "|", "%-*s" % (next_pcrs.pcr_size*2, "COMPUTED")) 175 | for idx in wanted_pcrs: 176 | if real_pcrs[idx] == this_pcrs[idx]: 177 | status = "+" 178 | else: 179 | errors += 1 180 | status = "" 181 | print("PCR %2d:" % idx, to_hex(real_pcrs[idx]), "|", to_hex(this_pcrs[idx]), status) 182 | exit(errors > 0) 183 | 184 | for idx in wanted_pcrs: 185 | if idx <= 7 and this_pcrs.count[idx] == 0: 186 | # The first 8 PCRs always have an EV_SEPARATOR logged to them at the very least, 187 | # and the first 3 or so will almost always have other boot events. If we never saw 188 | # anything then the whole bank might be unused (and an all-zeros PCR value is 189 | # obviously unsafe to bind against). 190 | exit("error: Log contains no entries for PCR %d in the %r bank." % (idx, hash_alg)) 191 | 192 | if args.verbose or (not args.output): 193 | print("== Final computed & predicted PCR values ==") 194 | print(" "*7, "%-*s" % (this_pcrs.pcr_size*2, "CURRENT"), "|", "%-*s" % (next_pcrs.pcr_size*2, "PREDICTED NEXT")) 195 | for idx in wanted_pcrs: 196 | print("PCR %2d:" % idx, to_hex(this_pcrs[idx]), "|", to_hex(next_pcrs[idx])) 197 | 198 | if args.output: 199 | with open(args.output, "wb") as fh: 200 | for idx in wanted_pcrs: 201 | fh.write(next_pcrs[idx]) 202 | -------------------------------------------------------------------------------- /tpm_futurepcr/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import tpm_futurepcr 3 | tpm_futurepcr.main() 4 | -------------------------------------------------------------------------------- /tpm_futurepcr/binary_reader.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | class BinaryReader(): 4 | def __init__(self, fh): 5 | self.fh = fh 6 | self._N_size = struct.calcsize("@N") 7 | self._P_size = struct.calcsize("@P") 8 | 9 | def seek(self, pos, whence=0): 10 | return self.fh.seek(pos, whence) 11 | 12 | def read(self, length): 13 | buf = self.fh.read(length) 14 | if len(buf) < length: 15 | if len(buf) == 0: 16 | raise EOFError("Hit EOF after 0/%d bytes" % length) 17 | else: 18 | raise IOError("Hit EOF after %d/%d bytes" % (len(buf), length)) 19 | return buf 20 | 21 | def _read_fmt(self, length, fmt): 22 | buf = self.fh.read(length) 23 | if len(buf) < length: 24 | if len(buf) == 0: 25 | raise EOFError("Hit EOF after 0/%d bytes" % length) 26 | else: 27 | raise IOError("Hit EOF after %d/%d bytes" % (len(buf), length)) 28 | data, = struct.unpack(fmt, buf) 29 | return data 30 | 31 | def read_u8(self): 32 | return self._read_fmt(1, ">B") 33 | 34 | def read_u16_le(self): 35 | return self._read_fmt(2, "\033[m" % (e["pcr_idx"], event_type, event_type_str)) 60 | event_data = e["event_data"] 61 | if event_type == TpmEventType.EFI_BOOT_SERVICES_APPLICATION: 62 | if verbose: 63 | hexdump(event_data) 64 | ed = parse_efi_bsa_event(event_data) 65 | pprint(ed) 66 | else: 67 | ed = parse_efi_bsa_event(event_data) 68 | print("Path vector:") 69 | for p in ed["device_path_vec"]: 70 | type_name = getattr(p["type"], "name", str(p["type"])) 71 | subtype_name = getattr(p["subtype"], "name", str(p["subtype"])) 72 | file_path = p.get("file_path", p["data"]) 73 | print(" * %-20s %-20s %s" % (type_name, subtype_name, file_path)) 74 | elif event_type in {TpmEventType.EFI_VARIABLE_AUTHORITY, 75 | TpmEventType.EFI_VARIABLE_BOOT, 76 | TpmEventType.EFI_VARIABLE_DRIVER_CONFIG}: 77 | if verbose: 78 | hexdump(event_data, 64) 79 | ed = parse_efi_variable_event(event_data) 80 | pprint(ed) 81 | else: 82 | ed = parse_efi_variable_event(event_data) 83 | print("Variable: %r {%s}" % (ed["unicode_name"], ed["variable_name_uuid"])) 84 | else: 85 | hexdump(event_data, 64) 86 | 87 | # ~/src/linux/include/linux/tpm_eventlog.h 88 | # TPMv1: https://sources.debian.org/src/golang-github-coreos-go-tspi/0.1.1-2/tspi/tpm.go/?hl=44#L44 89 | # TPMv2: https://trustedcomputinggroup.org/wp-content/uploads/EFI-Protocol-Specification-rev13-160330final.pdf 90 | 91 | def enum_log_entries(path=None): 92 | tpm_ver = 1 # the first entry is always in old format 93 | tcg_hdr = None 94 | with open(path or "/sys/kernel/security/tpm0/binary_bios_measurements", "rb") as fh: 95 | rd = BinaryReader(fh) 96 | while True: 97 | event = {} 98 | try: 99 | # same across both formats 100 | event["pcr_idx"] = rd.read_u32_le() 101 | event["event_type"] = rd.read_u32_le() 102 | event["event_type"] = TpmEventType(event["event_type"]) 103 | event["pcr_extend_values"] = {} 104 | if tpm_ver == 1: 105 | # section 5.1, SHA1 Event Log Entry Format 106 | event["pcr_extend_values"][TpmAlgorithm.SHA1] = rd.read(20) 107 | elif tpm_ver == 2: 108 | # section 5.2, Crypto Agile Log Entry Format 109 | pcr_count = rd.read_u32_le() 110 | for i in range(pcr_count): 111 | # Spec says it should be safe to just iter over hdr[digest_sizes], 112 | # as all entries must have the same algorithms in the same order, 113 | # but it does recommend alg_id lookup as the preferred method. 114 | alg_id = TpmAlgorithm(rd.read_u16_le()) 115 | digest = rd.read(tcg_hdr["digest_sizes_dict"][alg_id]) 116 | event["pcr_extend_values"][alg_id] = digest 117 | # same across both formats 118 | event["event_size"] = rd.read_u32_le() 119 | event["event_data"] = rd.read(event["event_size"]) 120 | yield event 121 | except EOFError: 122 | break 123 | 124 | # section 5.3, Event Log Header 125 | if tpm_ver == 1 \ 126 | and event["pcr_idx"] == 0 \ 127 | and event["event_type"] == TpmEventType.NO_ACTION \ 128 | and event["event_data"][0:15] == b"Spec ID Event03": 129 | tpm_ver = 2 130 | tcg_hdr = parse_efi_tcg2_header_event(event["event_data"]) 131 | -------------------------------------------------------------------------------- /tpm_futurepcr/pcr_bank.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import os 3 | import subprocess 4 | 5 | from .util import is_tpm2, in_path 6 | 7 | NUM_PCRS = 24 8 | 9 | def read_current_pcrs(alg="sha1"): 10 | pcr_size = hashlib.new(alg).digest_size 11 | pcrs = {} 12 | if os.path.exists("/sys/class/tpm/tpm0/pcr-%s/0" % alg): 13 | # New sysfs exports in kernel v5.12 14 | for idx in range(NUM_PCRS): 15 | with open("/sys/class/tpm/tpm0/pcr-%s/%d" % (alg, idx), "r") as fh: 16 | buf = fh.read().strip() 17 | pcrs[idx] = bytes.fromhex(buf) 18 | elif is_tpm2(): 19 | if in_path("tpm2_pcrread"): # tpm2-utils 4.0 or later 20 | cmd = ["tpm2_pcrread", alg, "-Q", "-o", "/dev/stdout"] 21 | elif in_path("tpm2_pcrlist"): # tpm2-utils 3.x 22 | cmd = ["tpm2_pcrlist", "-L", alg, "-Q", "-o", "/dev/stdout"] 23 | else: 24 | # TODO: try using IBM TSS tools 25 | raise Exception("tpm2_pcrread or tpm2_pcrlist not found") 26 | with subprocess.Popen(cmd, stdout=subprocess.PIPE) as proc: 27 | if proc.wait() != 0: 28 | raise subprocess.CalledProcessError(proc.returncode, proc.args) 29 | for idx in range(NUM_PCRS): 30 | pcrs[idx] = proc.stdout.read(pcr_size) 31 | else: 32 | if alg != "sha1": 33 | raise Exception("TPM v1 only supports the SHA1 PCR bank") 34 | with open("/sys/class/tpm/tpm0/pcrs", "r") as fh: 35 | for line in fh: 36 | if line.startswith("PCR-"): 37 | idx, buf = line.strip().split(": ") 38 | idx = int(idx[4:], 10) 39 | pcrs[idx] = bytes.fromhex(buf) 40 | return pcrs 41 | 42 | def extend_pcr_with_hash(pcr_value, extend_value, alg="sha1"): 43 | pcr_value = hashlib.new(alg, pcr_value + extend_value).digest() 44 | return pcr_value 45 | 46 | def extend_pcr_with_data(pcr_value, extend_data, alg="sha1"): 47 | extend_value = hashlib.new(alg, extend_data).digest() 48 | return extend_pcr_with_hash(pcr_value, extend_value) 49 | 50 | class PcrBank(): 51 | NUM_PCRS = 24 52 | 53 | def __init__(self, alg="sha1"): 54 | self.hash_alg = alg 55 | self.pcr_size = hashlib.new(alg).digest_size 56 | self.pcrs = {idx: (b"\xFF" if (17 <= idx <= 22) else b"\x00") * self.pcr_size 57 | for idx in range(self.NUM_PCRS)} 58 | self.count = {idx: 0 for idx in range(self.NUM_PCRS)} 59 | 60 | def extend_with_hash(self, idx, extend_value): 61 | self.pcrs[idx] = extend_pcr_with_hash(self.pcrs[idx], extend_value, self.hash_alg) 62 | self.count[idx] += 1 63 | return self.pcrs[idx] 64 | 65 | def extend_with_data(self, idx, extend_data): 66 | self.pcrs[idx] = extend_pcr_with_data(self.pcrs[idx], extend_data, self.hash_alg) 67 | self.count[idx] += 1 68 | return self.pcrs[idx] 69 | 70 | def __getitem__(self, idx): 71 | return self.pcrs[idx] 72 | -------------------------------------------------------------------------------- /tpm_futurepcr/systemd_boot.py: -------------------------------------------------------------------------------- 1 | import os 2 | from .util import ( 3 | find_mountpoint_by_partuuid, 4 | read_efi_variable, 5 | read_pecoff_section, 6 | ) 7 | 8 | EFIVAR_GUID_REDHAT = "4a67b082-0a4c-41cf-b6c7-440b29bb8c4f" 9 | 10 | def loader_get_esp_partuuid(): 11 | buf = read_efi_variable("LoaderDevicePartUUID", EFIVAR_GUID_REDHAT) 12 | return buf.decode("utf-16le").rstrip("\0") 13 | 14 | def loader_get_current_entry(): 15 | buf = read_efi_variable("LoaderEntrySelected", EFIVAR_GUID_REDHAT) 16 | return buf.decode("utf-16le").rstrip("\0") 17 | 18 | def _to_efi_path(path): 19 | # kinda match systemd.git:src/boot/efi/util.c:stra_to_path() 20 | # (except for the utf16 part, that'll be done on the whole cmdline) 21 | path = path.replace("/", "\\") 22 | if not path.startswith("\\"): 23 | path = "\\" + path 24 | return path 25 | 26 | def loader_parse_config(name, esp=None): 27 | # match systemd.git:src/boot/efi/boot.c:line_get_key_value() 28 | esp = esp or "/boot" 29 | path = os.path.join(esp, "loader/entries/%s.conf" % name) 30 | config = [] 31 | with open(path, "r") as fh: 32 | for line in fh: 33 | line = line.strip() 34 | if (not line) or (line[0] == "#"): 35 | continue 36 | try: 37 | key, val = line.split(None, 1) 38 | except ValueError: 39 | continue 40 | if val[0] == "\"" and val[-1] == "\"": 41 | val = val[1:-1] 42 | config.append((key, val)) 43 | return config 44 | 45 | def loader_get_cmdline(entry, esp=None): 46 | # match systemd.git:src/boot/efi/boot.c:config_entry_add_from_file() 47 | config = loader_parse_config(entry, esp) 48 | initrd = [] 49 | options = [] 50 | for key, val in config: 51 | if key == "initrd": 52 | initrd.append("initrd=" + _to_efi_path(val)) 53 | elif key == "options": 54 | options.append(val) 55 | return " ".join([*initrd, *options]) 56 | 57 | def sd_stub_get_cmdline(path): 58 | """ 59 | Get the .cmdline section from a systemd EFI stub "unified image". 60 | """ 61 | # Match systemd.git:src/boot/efi/boot.c:config_entry_add_linux() 62 | cmdline = read_pecoff_section(path, ".cmdline").decode("utf-8") 63 | # Chomp a single trailing newline 64 | if cmdline[-1] == "\n": 65 | cmdline = cmdline[:-1] 66 | return cmdline 67 | 68 | def loader_get_next_cmdline(last_efi_binary=None): 69 | try: 70 | sd_stub_present = read_efi_variable("StubInfo", EFIVAR_GUID_REDHAT) 71 | except FileNotFoundError: 72 | sd_stub_present = None 73 | 74 | if sd_stub_present: 75 | # Booted using mksignkernels/systemd-stub, so the cmdline is embedded in 76 | # the kernel .efi binary. We don't know its path so assume it's the last 77 | # binary we've seen in the event log. 78 | if last_efi_binary: 79 | return sd_stub_get_cmdline(last_efi_binary) 80 | else: 81 | raise Exception("systemd-stub is present, but no EFI binary traced") 82 | else: 83 | entry = loader_get_current_entry() 84 | esp = find_mountpoint_by_partuuid(loader_get_esp_partuuid()) 85 | return loader_get_cmdline(entry, esp) 86 | 87 | def loader_encode_pcr8(cmdline): 88 | """ 89 | Encode kernel command line the same way systemd-stub does it before measuring. 90 | """ 91 | return (cmdline + "\0").encode("utf-16le") 92 | 93 | def loader_decode_pcr8(cmdline): 94 | """ 95 | Reverse the encoding for a kernel command line we've read from EV_IPL. 96 | """ 97 | assert(cmdline.endswith(b"\0\0")) 98 | return cmdline.decode("utf-16le")[:-1] 99 | -------------------------------------------------------------------------------- /tpm_futurepcr/tpm_constants.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | class TpmAlgorithm(enum.IntEnum): 4 | # https://trustedcomputinggroup.org/resource/tcg-algorithm-registry/ 5 | RSA = 0x0001 6 | TDES = 0x0003 7 | SHA1 = 0x0004 8 | HMAC = 0x0005 9 | AES = 0x0006 10 | MGF1 = 0x0007 11 | KEYEDHASH = 0x0008 12 | XOR = 0x000A 13 | SHA256 = 0x000B 14 | SHA384 = 0x000C 15 | SHA512 = 0x000D 16 | NULL = 0x0010 17 | SM3_256 = 0x0012 18 | SM4 = 0x0013 19 | RSASSA = 0x0014 20 | RSAES = 0x0015 21 | RSAPSS = 0x0016 22 | OAEP = 0x0017 23 | ECDSA = 0x0018 24 | ECDH = 0x0019 25 | ECDAA = 0x001A 26 | SM2 = 0x001B 27 | ECSCHNORR = 0x001C 28 | ECMQV = 0x001D 29 | KDF1_SP800_56A = 0x0020 30 | KDF2 = 0x0021 31 | KDF1_SP800_108 = 0x0022 32 | ECC = 0x0023 33 | SYMCIPHER = 0x0025 34 | CAMELLIA = 0x0026 35 | SHA3_256 = 0x0027 36 | SHA3_384 = 0x0028 37 | SHA3_512 = 0x0029 38 | CMAC = 0x003F 39 | CTR = 0x0040 40 | OFB = 0x0041 41 | CBC = 0x0042 42 | CFB = 0x0043 43 | ECB = 0x0044 44 | 45 | class TpmEventType(enum.IntEnum): 46 | # BIOS events 47 | PREBOOT_CERT = 0x00000000 # deprecated 48 | POST_CODE = 0x00000001 49 | UNUSED = 0x00000002 # reserved 50 | NO_ACTION = 0x00000003 # noextend 51 | SEPARATOR = 0x00000004 # BIOS: extend 0-7 52 | ACTION = 0x00000005 53 | EVENT_TAG = 0x00000006 54 | S_CRTM_CONTENTS = 0x00000007 55 | S_CRTM_VERSION = 0x00000008 56 | CPU_MICROCODE = 0x00000009 57 | PLATFORM_CONFIG_FLAGS = 0x0000000A 58 | TABLE_OF_DEVICES = 0x0000000B 59 | COMPACT_HASH = 0x0000000C 60 | IPL = 0x0000000D 61 | IPL_PARTITION_DATA = 0x0000000E 62 | NONHOST_CODE = 0x0000000F 63 | NONHOST_CONFIG = 0x00000010 64 | NONHOST_INFO = 0x00000011 65 | OMIT_BOOT_DEVICE_EVENTS = 0x00000012 66 | 67 | # UEFI events 68 | # https://trustedcomputinggroup.org/wp-content/uploads/TCG_EFI_Platform_1_22_Final_-v15.pdf#page=32 69 | # https://github.com/canonical/tcglog-parser/blob/master/constants.go 70 | EFI_EVENT_BASE = 0x80000000 71 | EFI_VARIABLE_DRIVER_CONFIG = 0x80000001 72 | EFI_VARIABLE_BOOT = 0x80000002 73 | EFI_BOOT_SERVICES_APPLICATION = 0x80000003 74 | EFI_BOOT_SERVICES_DRIVER = 0x80000004 75 | EFI_RUNTIME_SERVICES_DRIVER = 0x80000005 76 | EFI_GPT_EVENT = 0x80000006 77 | EFI_ACTION = 0x80000007 78 | EFI_PLATFORM_FIRMWARE_BLOB = 0x80000008 79 | EFI_HANDOFF_TABLES = 0x80000009 80 | EFI_PLATFORM_FIRMWARE_BLOB2 = 0x8000000A 81 | EFI_HANDOFF_TABLES2 = 0x8000000B 82 | EFI_VARIABLE_BOOT2 = 0x8000000C 83 | EFI_HCRTM_EVENT = 0x80000010 84 | EFI_VARIABLE_AUTHORITY = 0x800000E0 85 | EFI_SPDM_FIRMWARE_BLOB = 0x800000E1 86 | EFI_SPDM_FIRMWARE_CONFIG = 0x800000E2 87 | 88 | class TpmPostCode(): 89 | POST_CODE = b"POST CODE" 90 | SMM_CODE = b"SMM CODE" 91 | ACPI_DATA = b"ACPI DATA" 92 | BIS_CODE = b"BIS CODE" 93 | UEFI_PI = b"UEFI PI" 94 | OPROM = b"Embedded Option ROM" 95 | 96 | class TpmEfiActionString(): 97 | CALLING_EFI_APPLICATION = b"Calling EFI Application from Boot Option" 98 | RETURNING_FROM_EFI_APPLICATION = b"Returning from EFI Application from Boot Option" 99 | EXIT_BOOT_SERVICES_INVOCATION = b"Exit Boot Services Invocation" 100 | EXIT_BOOT_SERVICES_FAILED = b"Exit Boot Services Returned with Failure" 101 | EXIT_BOOT_SERVICES_SUCCEEDED = b"Exit Boot Services Returned with Success" 102 | 103 | class DevicePathType(enum.IntEnum): 104 | HardwareDevice = 0x01 105 | ACPIDevice = 0x02 106 | MessagingDevice = 0x03 107 | MediaDevice = 0x04 108 | BIOSBootDevice = 0x05 109 | End = 0x7F 110 | 111 | class HardwareDevicePathSubtype(enum.IntEnum): 112 | PCI = 0x01 113 | PCCARD = 0x02 114 | MemoryMapped = 0x03 115 | Vendor = 0x04 116 | Controller = 0x05 117 | BMC = 0x06 118 | 119 | class ACPIDevicePathSubtype(enum.IntEnum): 120 | ACPI = 0x01 121 | ACPIExtended = 0x02 122 | ACPI_ADR = 0x03 123 | 124 | class MessagingDevicePathSubtype(enum.IntEnum): 125 | ATAPI = 0x01 126 | SCSI = 0x02 127 | FibreChannel = 0x03 128 | IEEE1394 = 0x04 129 | USB = 0x05 130 | I2O = 0x06 131 | InfiniBand = 0x09 132 | Vendor = 0x0A 133 | MACAddress = 0x0B 134 | IPv4 = 0x0C 135 | IPv6 = 0x0D 136 | UART = 0x0E 137 | USBClass = 0x0F 138 | USBWWID = 0x10 139 | DeviceLUN = 0x11 140 | SATA = 0x12 141 | iSCSI = 0x13 142 | VLAN = 0x14 143 | FibreChannelEx = 0x15 144 | SASEx = 0x16 145 | NVMe = 0x17 146 | URI = 0x18 147 | UniversalFlashUFS = 0x19 148 | SecureDigital = 0x1A 149 | Bluetooth = 0x1B 150 | WiFi = 0x1C 151 | eMMC = 0x1D 152 | BluetoothLE = 0x1E 153 | DNS = 0x1F 154 | 155 | class MediaDevicePathSubtype(enum.IntEnum): 156 | HardDrive = 0x01 157 | CDROM = 0x02 158 | Vendor = 0x03 159 | FilePath = 0x04 160 | MediaProtocol = 0x05 161 | PIWGFirmware = 0x06 162 | PIWGFirmwareVolume = 0x07 163 | RelativeOffsetRange = 0x08 164 | RAMDisk = 0x09 165 | 166 | class BiosBootDevicePathSubtype(enum.IntEnum): 167 | BiosBootDevice = 0x01 168 | 169 | class BiosBootDeviceType(enum.IntEnum): 170 | Floppy = 0x01 171 | HardDrive = 0x02 172 | CDROM = 0x03 173 | PCMCIA = 0x04 174 | USB = 0x05 175 | EmbeddedNetwork = 0x06 176 | BEV = 0x80 177 | Unknown = 0xFF 178 | -------------------------------------------------------------------------------- /tpm_futurepcr/util.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import os 3 | import signify.fingerprinter 4 | import subprocess 5 | import argparse 6 | 7 | def to_hex(buf): 8 | import binascii 9 | return binascii.hexlify(buf).decode() 10 | 11 | def hexdump(buf, max_len=None): 12 | if max_len is None: 13 | max_len = len(buf) 14 | else: 15 | max_len = min(max_len, len(buf)) 16 | for i in range(0, max_len, 16): 17 | row = buf[i:i+16] 18 | offs = "0x%08x:" % i 19 | hexs = ["%02X" % b for b in row] + [" "] * 16 20 | text = [chr(b) if 0x20 < b < 0x7f else "." for b in row] + [" "] * 16 21 | print(offs, " ".join(hexs[:16]), "|%s|" % "".join(text[:16])) 22 | if len(buf) > max_len: 23 | print("(%d more bytes)" % (len(buf) - max_len)) 24 | 25 | def guid_to_UUID(buf): 26 | import struct 27 | import uuid 28 | buf = struct.pack(">LHH8B", *struct.unpack("