diff --git a/TODO b/TODO index 97466c81b8..7348c23556 100644 --- a/TODO +++ b/TODO @@ -147,6 +147,23 @@ Features: root=nvme::::: to boot directly from nvme-oF +* pcrlock: + - make signed PCR work together with pcrlock + - add kernel-install plugin that automatically creates UKI .pcrlock file when + UKI is installed, and removes it when it is removed again + - automatically install PE measurement of sd-boot on "bootctl install" + - write generated pcrlock signature files to the ESP as credential, one for + each installed OS & pick up generated pcrlock signature file in sd-stub, + pass it via initrd to OS + - pre-calc sysext + kernel cmdline measurements + - pre-calc cryptsetup root key measurement + - maybe make systemd-repart generate .pcrlock for old and new GPT header in + /run? + - Add support for more than 8 branches per PCR OR + - add "systemd-pcrlock lock-kernel-current" or so which synthesizes .pcrlock + policy from currently booted kernel/event log, to close gap for first boot + for pre-built images + * add a new systemd-project@.service that is very similar to user@.service but uses DynamicUser=1 and no PAMName= to invoke an unprivileged somewhat light-weight service manager. Use HOME=/var/lib/systemd/projects/%i as home @@ -299,54 +316,6 @@ Features: * systemd-mount should only consider modern file systems when mounting, similar to systemd-dissect -* new "systemd-pcrlock" component for dealing with PCR4. Design idea: - 1. define /{etc,usr,var/lib}/pcrlock.d//.pcrlock - 2. these files contain list of hashes that will be measured when component is - run, per PCR - 3. each component involved in the boot that is deterministically measured can - place one or more of these files in those dirs (shim, sd-boot, - sd-stub/UKI, cryptsetup, pcrphase, pcrfs, …) - 4. since each component has its own dir, with multiple files in them, package - such as kernels (of which there can be multiple installed at the same - time) can be grouped together: only one of them is measured at a time. - 5. whenever a new component is added or an old one removed, or the PCR lock - shall be relaxed or tightened the systemd-pcrlock tool is invoked. - 6. tool iterates through all these files, orders them alphabetically by - component, then matches them up with current measurements (as per uefi - event log), identifying by hash, accepting that the "beginning" of the - measurements might not be recognizable. - 7. Then calculates expected PCR values starting with the "unrecognized - head" from the event log, then continuing with all of components - defined via the .pcrlock files (but dropping out the "recognized tail" - from the uefi event log). (This might mean combinatorial explosion, if - there are multiple shims, multiple sd-boot, and so on.) - 8. Generates a public/private key pair on the TPM - 9. Generates a counter object in the TPM, with a policy that allows only - one-by-one increase with signature policy by the public/private key pair. - 10. now signs policies of all expected PCR values with the generated keypair, - using all combinations of components defined in the .pcrlock files - restricting it to the counter + 1. - 11. locks down the keypair with a signed policy with its own public key - 12. generates JSON file of all these policies with their signatures, drops - them as singleton in ESP - 13. increases the counter by one. - 14. after boot sd-stub picks JSON up from ESP, passes it to userspace via - .extra - 15. JSON contained policies can now be used to unlock disk as well as the - public/key itself for signing further policies, as well as increment for - the counter - 16. whenever any of the components above is added/removed new JSON file with - signatures for counter + 1 is generated, dropped in ESP, then counter - increased. (i.e. this means the "recognized tail" of the event log is - deterministically swapped out) - 17. when firmware update is expected, relaxed signed policy is generated for - next boot only valid if counter is increased (this means the - "unrecognized head" for the event log can change without losing access) - 18. on every boot checks if releaxed policy is in effect, if so, new strict - policy is generated and counter increased. - Net result: Removes downgrade attack surface + Locks OS to firmware + Allows - downgrades within bounds - * add another PE section ".fname" or so that encodes the intended filename for PE file, and validate that when loading add-ons and similar before using it. This is particularly relevant when we load multiple add-ons and want to @@ -469,30 +438,6 @@ Features: * SIGRTMIN+18 and memory pressure handling should still be added to: hostnamed, localed, oomd, timedated. -* in order to make binding to PCR 4 realistic: - - generate one keypair "U" and store it in a tpm2 nvindex. - - Generate another keypair "P" and store it in a second tpm2 nvindex. - - allocate a persistent counter object "C" in the tpm2 - - Enroll all user objects (i.e. luks volumes, creds, …) to a tpm2 policy - signed by U. - - Lock both U and P down with a tpm2 policy signed by P (yes, P can only be - used if a signature by P itself can be provided) - - For regular reboots generate a signature for a restrictive PCR4 + counter C - based policy with key P. Place signature in EFI var, so it can be found on - next boot - - For reboots where a firmware update is expected generate a signature with a - more open policy against just counter C. Place signature in same EFI var. - - Increase C whenever switching between these two signature types. - - During early boot, use the signature from the EFI var to unlock U and P. - Use it to generate a signature for unlocking user objects given the current - PCR 4 value, store that away into /run somewhere, for user during the whole - later boot. - - When booting up automatically update the mentioned efi var so that it - contains the restrictive signature. But also generate a signature ahead of - time that could be used in case during the current boot we later detect we might - need to reboot for a firmware update. Store that in /run somewhere, so that - it can be placed in the EFI var, if needed. - * repart/gpt-auto/DDIs: maybe introduce a concept of "extension" partitions, that have a new type uuid and can "extend" earlier partitions, to work around the fact that systemd-repart can only grow the last partition defined. During @@ -1027,12 +972,6 @@ Features: set up the directory so that it can only be accessed if host and app are in order. -* TPM2: extend unlock policy to protect against version downgrades in signed - policies: policy probably must take some nvram based generation counter into - account that can only monotonically increase and can be used to invalidate - old PCR signatures. Otherwise people could downgrade to old signed PCR sets - whenever they want. - * update HACKING.md to suggest developing systemd with the ideas from: https://0pointer.net/blog/testing-my-system-code-in-usr-without-modifying-usr.html https://0pointer.net/blog/running-an-container-off-the-host-usr.html diff --git a/man/crypttab.xml b/man/crypttab.xml index 3e003156d2..fa60599301 100644 --- a/man/crypttab.xml +++ b/man/crypttab.xml @@ -808,6 +808,22 @@ + + + + Takes an absolute path to a TPM2 pcrlock policy file, as produced by the + systemd-pcrlock1 + tool. This permits locking LUKS2 volumes to a local policy of allowed PCR values with + variants. See + systemd-cryptenroll1 + for details on enrolling TPM2 pcrlock policies. If this option is not specified but it is attempted + to unlock a LUKS2 volume with a TPM2 pcrlock enrollment a suitable signature file + pcrlock.json is searched for in /run/systemd/ and + /var/lib/systemd/ (in this order). + + + + diff --git a/man/rules/meson.build b/man/rules/meson.build index 67a40d4605..5dc3e08896 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1004,6 +1004,16 @@ manpages = [ ['systemd-nspawn', '1', [], ''], ['systemd-oomd.service', '8', ['systemd-oomd'], 'ENABLE_OOMD'], ['systemd-path', '1', [], ''], + ['systemd-pcrlock', + '8', + ['systemd-pcrlock-file-system.service', + 'systemd-pcrlock-firmware-code.service', + 'systemd-pcrlock-firmware-config.service', + 'systemd-pcrlock-machine-id.service', + 'systemd-pcrlock-make-policy.service', + 'systemd-pcrlock-secureboot-authority.service', + 'systemd-pcrlock-secureboot-policy.service'], + 'ENABLE_BOOTLOADER'], ['systemd-pcrphase.service', '8', ['systemd-pcrextend', @@ -1139,6 +1149,7 @@ manpages = [ ['systemd.nspawn', '5', [], ''], ['systemd.offline-updates', '7', [], ''], ['systemd.path', '5', [], ''], + ['systemd.pcrlock', '5', ['systemd.pcrlock.d'], ''], ['systemd.preset', '5', [], ''], ['systemd.resource-control', '5', [], ''], ['systemd.scope', '5', [], ''], diff --git a/man/systemd-cryptenroll.xml b/man/systemd-cryptenroll.xml index 836538be4c..3d0efc9bf7 100644 --- a/man/systemd-cryptenroll.xml +++ b/man/systemd-cryptenroll.xml @@ -519,6 +519,20 @@ + + PATH + + Configures a TPM2 pcrlock policy to bind encryption to. Expects a path to a pcrlock + policy file as generated by the + systemd-pcrlock1 + tool. If a TPM2 device is enrolled and this option is not used but a file + pcrlock.json is found in /run/systemd/ or + /var/lib/systemd/ it is automatically used. Assign an empty string to turn this + behaviour off. + + + + SLOT diff --git a/man/systemd-pcrlock.xml b/man/systemd-pcrlock.xml new file mode 100644 index 0000000000..3c4d9bb0e7 --- /dev/null +++ b/man/systemd-pcrlock.xml @@ -0,0 +1,559 @@ + + + + + + + + systemd-pcrlock + systemd + + + + systemd-pcrlock + 8 + + + + systemd-pcrlock + systemd-pcrlock-file-system.service + systemd-pcrlock-firmware-code.service + systemd-pcrlock-firmware-config.service + systemd-pcrlock-machine-id.service + systemd-pcrlock-make-policy.service + systemd-pcrlock-secureboot-authority.service + systemd-pcrlock-secureboot-policy.service + Analyze and predict TPM2 PCR states and generate an access policy from the prediction + + + + + /usr/lib/systemd/systemd-pcrlock OPTIONS + + + + + Description + + Note: this command is experimental for now. While it is likely to become a regular component of + systemd, it might still change in behaviour and interface. + + systemd-pcrlock is a tool that may be used to analyze and predict TPM2 PCR + measurements, and generate TPM2 access policies from the prediction which it stores in a TPM2 NV index + (i.e. in the TPM2 non-volatile memory). This may then be used to restrict access to TPM2 objects (such as + disk encryption keys) to system boot-ups in which only specific, trusted components are used. + + systemd-pcrlock uses as input for its analysis and prediction: + + + The UEFI firmware TPM2 event log + (i.e. /sys/kernel/security/tpm0/binary_bios_measurements) of the current + boot. + + The userspace TPM2 event log + (i.e. /run/log/systemd/tpm2-measure.log) of the current + boot. + + The current PCR state of the TPM2 chip. + + Boot component definition files (*.pcrlock and + *.pcrlock.d/*.pcrlock, see + systemd.pcrlock5) + that each define expected measurements for one component of the boot process, permitting alternative + variants for each. (Variants may be used used to bless multiple kernel versions or boot loader versions + at the same time.) + + + It uses these inputs to generate a combined event log, validating it against the PCR states. It + then attempts to recognize event log records and matches them against the defined components. For each PCR + where this can be done comprehensively (i.e. where all listed records and all defined components have + been matched) this may then be used to predict future PCR measurements, taking the alternative variants + defined for each component into account. This prediction may then be converted into a TPM2 access policy + (consisting of TPM2 PolicyPCR and PolicyOR items), which is + then stored in an NV index in the TPM2. This may be used to then lock secrets (such as disk encryption + keys) to these policies (via a TPM2 PolicyAuthorizeNV policy). + + Use tools such as + systemd-cryptenroll1 + or systemd-repart8 to + bind disk encryption to such a systemd-pcrlock TPM2 policy. Specifically, see the + switches of these tools. + + The access policy logic requires a TPM2 device that implements the + PolicyAuthorizeNV command, i.e. implements TPM 2.0 version 1.38 or newer. + + + + Commands + + The following commands are understood: + + + + log + + This reads the combined TPM2 event log, validates it, matches it against the current + PCR values, and outputs both in tabular form. Combine with to generate + output in JSON format. + + + + + + cel + + This reads the combined TPM2 event log and writes it to STDOUT in TCG Common Event Log + Format (CEL-JSON) format. + + + + + + list-components + + Shows a list of component definitions and their variants, i.e. the + *.pcrlock files discovered in /var/lib/pcrlock.d/, + /usr/lib/pcrlock.d/, and the other supported directories. See + systemd.pcrlock5 + for details on these files and the full list of directories searched. + + + + + + predict + + Predicts the PCR state on future boots. This will analyze the TPM2 event log as + described above, recognize components, and then generate all possible resulting PCR values for all + combinations of component variants. Note that no prediction is made for PCRs whose value does not + match the event log records, for which unrecognized measurements are discovered or for which + components are defined that cannot be found in the event log. This is a safety measure to ensure that + any generated access policy can be fulfilled correctly on current and future boots. + + + + + + + make-policy + + This predicts the PCR state for future boots, much like the + predict command above. It then uses this data to generate a TPM2 access policy + which it stores in a TPM2 NV index. The prediction and information about the used TPM2 and its NV + index are written to /var/lib/systemd/pcrlock.json. + + The NV index is allocated on first invocation, and updated on subsequent invocations. + + The NV index contents may be changed (and thus the policy stored in it updated) by providing an + access PIN. This PIN is normally generated automatically and stored in encrypted form (with an access + policy binding it to the NV index itself) in the aforementioned JSON policy file. This PIN may be + chosen by the user, via the switch. If specified it may be used as + alternative path of access to update the policy. + + If the new prediction matches the old this command terminates quickly and executes no further + operation. (Unless is specified, see below.) + + + + + + + remove-policy + + Removes a previously generated policy. Deletes the + /var/lib/systemd/pcrlock.json file, and deallocates the NV index. + + + + + + + lock-firmware-code + unlock-firmware-code + + Generates/removes .pcrlock files based on the TPM2 event log of + the current boot covering all records for PCRs 0 ("platform-code") and 2 ("external-code"). + + This operation allows locking the boot process to the current version of the firmware of the + system and its extension cards. This operation should only be used if the system vendor does not + provide suitable pcrlock data ahead of time. + + Note that this data only matches the current version of the firmware. If a firmware update is + applied this data will be out-of-date and any access policy generated from it will no longer pass. It + is thus recommended to invoke unlock-firmware-code before doing a firmware update, + followed by make-policy to refresh the policy. + + systemd-pcrlock lock-firmware-code is invoked automatically at boot via the + systemd-pcrlock-firmware-code.service unit, if enabled. This ensures that an + access policy managed by systemd-pcrlock is automatically locked to the new + firmware version whenever the policy has been relaxed temporarily, in order to cover for firmware + updates, as described above. + + The files are only generated from the event log if the event log matches the current TPM2 PCR + state. + + This writes/removes the files + /var/lib/pcrlock.d/250-firmware-code-early.pcrlock.d/generated.pcrlock and + /var/lib/pcrlock.d/550-firmware-code-late.pcrlock.d/generated.pcrlock. + + + + + + + lock-firmware-config + unlock-firmware-config + + This is similar to + lock-firmware-code/unlock-firmware-code but locks down the + firmware configuration, i.e. PCRs 1 ("platform-config") and 3 ("external-config"). + + This functionality should be used with care as in most scenarios a minor firmware configuration + change should not invalidate access policies to TPM2 objects. Also note that some systems measure + unstable and unpredictable information (e.g. current CPU voltages, temperatures, as part of SMBIOS + data) to these PCRs, which means this form of lockdown cannot be used reliably on such systems. Use + this functionality only if the system and hardware is well known and does not suffer by these + limitations, for example in virtualized environments. + + Use unlock-firmware-config before making firmware configuration changes. If + the systemd-pcrlock-firmware-config.service unit is enabled it will + automatically generate a pcrlock file from the new measurements. + + This writes/removes the files + /var/lib/pcrlock.d/250-firmware-config-early.pcrlock.d/generated.pcrlock and + /var/lib/pcrlock.d/550-firmware-config-late.pcrlock.d/generated.pcrlock. + + + + + + + lock-secureboot-policy + unlock-secureboot-policy + + Generates/removes a .pcrlock file based on the SecureBoot policy + currently enforced. This looks at the SecureBoot, PK, KEK, db, dbx, dbt, dbr EFI variables and + predicts their measurements to PCR 7 ("secure-boot-policy") on the next boot. + + Use unlock-firmware-config before applying SecureBoot policy updates. If + the systemd-pcrlock-secureboot-policy.service unit is enabled it will + automatically generate a pcrlock file from the policy discovered. + + This writes/removes the file + /var/lib/pcrlock.d/230-secureboot-policy.pcrlock.d/generated.pcrlock. + + + + + + + lock-secureboot-authority + unlock-secureboot-authority + + Generates/removes a .pcrlock file based on the SecureBoot + authorities used to validate the boot path. SecureBoot authorities are the specific SecureBoot + database entries that where used to validate the UEFI PE binaries executed at boot. This looks at the + event log of the current boot, and uses relevant measurements on PCR 7 + ("secure-boot-policy"). + + This writes/removes the file + /var/lib/pcrlock.d/620-secureboot-authority.pcrlock.d/generated.pcrlock. + + + + + + + lock-gpt DEVICE + unlock-gpt + + Generates/removes a .pcrlock file based on the GPT partition + table of the specified disk. If no disk is specified automatically determines the block device + backing the root file system. This locks the state of the disk partitioning of the booted medium, + which firmware measures to PCR 5 ("boot-loader-config"). + + This writes/removes the file + /var/lib/pcrlock.d/600-gpt.pcrlock.d/generated.pcrlock. + + + + + + + lock-pe BINARY + unlock-pe + + Generates/removes a .pcrlock file based on the specified PE + binary. This is useful for predicting measurements the firmware makes to PCR 4 ("boot-loader-code") + if the specified binary is part of the UEFI boot process. Use this on boot loader binaries and + suchlike. Use lock-uki (see below) for PE binaries that are unified kernel images + (UKIs). + + Expects a path to the PE binary as argument. If not specified, reads the binary from STDIN + instead. + + The pcrlock file to write must be specified via the switch. + + + + + + + lock-uki UKI + unlock-uki + + Generates/removes a .pcrlock file based on the specified UKI PE + binary. This is useful for predicting measurements the firmware makes to PCR 4 ("boot-loader-code"), + and systemd-stub7 + makes to PCR 11 ("kernel-boot"), if the specified UKI is booted. This is a superset of + lock-pe. + + Expects a path to the UKI PE binary as argument. If not specified, reads the binary from STDIN + instead. + + The pcrlock file to write must be specified via the switch. + + + + + + + lock-machine-id + unlock-machine-id + + Generates/removes a .pcrlock file based on + /etc/machine-id. This is useful for predicting measurements + systemd-pcrmachine.service8 + makes to PCR 15 ("system-identity"). + + This writes/removes the file + /var/lib/pcrlock.d/820-machine-id.pcrlock. + + + + + + + lock-file-system PATH + unlock-file-system PATH + + Generates/removes a .pcrlock file based on file system + identity. This is useful for predicting measurements + systemd-pcrfs@.service8 + makes to PCR 15 ("system-identity") for the root and /var/ file systems. + + This writes/removes the files + /var/lib/pcrlock.d/830-root-file-system.pcrlock and + /var/lib/pcrlock.d/840-file-system-path.pcrlock. + + + + + + + lock-kernel-cmdline FILE + unlock-kernel-cmdline + + Generates/removes a .pcrlock file based on + /proc/cmdline (or the specified file if given). This is useful for predicting + measurements the Linux kernel makes to PCR 9 ("kernel-initrd"). + + This writes/removes the file + /var/lib/pcrlock.d/710-kernel-cmdline.pcrlock/generated.pcrlock. + + + + + + + lock-kernel-initrd FILE + unlock-kernel-initrd + + Generates/removes a .pcrlock file based on a kernel initrd cpio + archive. This is useful for predicting measurements the Linux kernel makes to PCR 9 + ("kernel-initrd"). Do not use for systemd-stub UKIs, as the initrd is combined + dynamically from various sources and hence does not take a single input, like this command. + + This writes/removes the file + /var/lib/pcrlock.d/720-kernel-initrd.pcrlock/generated.pcrlock. + + + + + + + lock-raw FILE + unlock-raw + + Generates/removes a .pcrlock file based on raw binary data. The + data is either read from the specified file or from STDIN (if none is specified). This requires that + is specified. The generated pcrlock file is written to the file specified + via or to STDOUT (if none is specified). + + + + + + + + + + Options + + The following options are understood: + + + + + + When displaying the TPM2 event log do not attempt to decode the records to provide a + friendly event log description string. Instead, show the binary payload data in escaped form. + + + + + + + + Specifies the PCR number to use. May be specified more than once to select multiple + PCRs. + + This is used by lock-raw and lock-pe to select the + PCR to lock against. + + If used with predict and make-policy this will override + which PCRs to include in the prediction and policy. If unspecified this defaults to PCRs 0-5, 7, + 11-15. Note that these commands will not include any PCRs in the prediction/policy (even if specified + explicitly) if there are measurements in the event log that do not match the current PCR value, or + there are unrecognized measurements in the event log, or components define measurements not seen in + the event log. + + + + + + + + Specifies to NV index to store the policy in. Honoured by + make-policy. If not specified the command will automatically pick a free NV + index. + + + + + + + + Takes a path to read *.pcrlock and + *.pcrlock.d/*.pcrlock files from. May be used more than once to specify multiple + such directories. If not specified defaults to /etc/pcrlock.d/, + /run/pcrlock.d/, /var/lib/pcrlock.d/, + /usr/local/pcrlock.d/, /usr/lib/pcrlock.d/. + + + + + + + + Takes either a string or a colon-separated pair of strings. Configures up to which + point in the sorted list of defined components to analyze/predict PCRs to. Typically, the + systemd-pcrlock tool is invoked from a fully booted system after boot-up and + before shutdown. This means various components that are defined for shutdown have not been measured + yet, and should not be searched for. This option allows to restrict which components are considered + for analysis (taking only components before some point into account, ignoring components after + them). The expected string is ordered against the filenames of the components defined. Any components + with a lexicographically later name are ignored. This logic applies to the log, + predict, and make-policy verbs. If a colon-separated pair of + strings are specified then they select which phases of the boot to include in the + prediction/policy. The first string defines where the first prediction shall be made, and the second + string defines where the last prediction shall be made. All such predictions are then combined into + one set. + + If used with list-components the selected location range will be highlighted + in the component list. + + Defaults to 760-:940-, which means the policies generated by default will + basically cover the whole runtime of the OS userspace, from the initrd (as 760- + closely follows 750-enter-initrd.pcrlock) until (and including) the main runtime + of the system (as 940- is closely followed by + 950-shutdown.pcrlock). See + systemd.pcrlock5 + for a full list of well-known components, that illustrate where this range is placed by + default. + + + + + + + + Takes a boolean. Defaults to false. Honoured by make-policy. If + true, will query the user for a PIN to unlock the TPM2 NV index with. If no policy was created before + this PIN is used to protect the newly allocated NV index. If a policy has been created before the PIN + is used to unlock write access to the NV index. If this option is not used a PIN is automatically + generated. Regardless if user supplied or automatically generated, it is stored in encrypted form in + the policy metadata file. The recovery PIN may be used to regain write access to an NV index in case + the access policy became out of date. + + + + + + + + Takes a file system path as argument. If specified overrides where to write the + generated pcrlock data to. Honoured by the various lock-* commands. If not + specified, a default path is generally used, as documented above. + + + + + + + + Takes a file system path as argument. If specified overrides where to write pcrlock + policy metadata to. If not specified defaults to + /var/lib/systemd/pcrlock.json. + + + + + + + + If specified with make-policy, the predicted policy will be + written to the NV index even if it is detected to be the same as the previously stored + one. + + + + + + + + + + + + + Exit status + + On success, 0 is returned, a non-zero failure code otherwise. + + + + See Also + + systemd1, + systemd.pcrlock5, + systemd-cryptenroll1, + systemd-cryptsetup@.service8, + systemd-repart8, + systemd-pcrmachine.service8 + + + + diff --git a/man/systemd-repart.xml b/man/systemd-repart.xml index 8517f24d67..70f8346e80 100644 --- a/man/systemd-repart.xml +++ b/man/systemd-repart.xml @@ -388,6 +388,16 @@ + + PATH + + Configures a TPM2 pcrlock policy to bind encryption to. See + systemd-cryptenroll1 + for details on this option. + + + + BOOL diff --git a/man/systemd.pcrlock.xml b/man/systemd.pcrlock.xml new file mode 100644 index 0000000000..070bed2e11 --- /dev/null +++ b/man/systemd.pcrlock.xml @@ -0,0 +1,298 @@ + + + + + + systemd.pcrlock + systemd + + + + systemd.pcrlock + 5 + + + + systemd.pcrlock + systemd.pcrlock.d + PCR measurement prediction files + + + + +/etc/pcrlock.d/*.pcrlock +/etc/pcrlock.d/*.pcrlock.d/*.pcrlock +/run/pcrlock.d/*.pcrlock +/run/pcrlock.d/*.pcrlock.d/*.pcrlock +/var/lib/pcrlock.d/*.pcrlock +/var/lib/pcrlock.d/*.pcrlock.d/*.pcrlock +/usr/local/pcrlock.d/*.pcrlock +/usr/local/pcrlock.d/*.pcrlock.d/*.pcrlock +/usr/lib/pcrlock.d/*.pcrlock +/usr/lib/pcrlock.d/*.pcrlock.d/*.pcrlock + + + + Description + + *.pcrlock files define expected TPM2 PCR measurements of components involved + in the boot + process. systemd-pcrlock1 + uses such pcrlock files to analyze and predict TPM2 PCR measurements. The pcrlock files are JSON arrays + that follow a subset of the TCG Common Event Log Format + (CEL-JSON) specification. Specifically the recnum, content, + and content_type record fields are not used and ignored if present. Each pcrlock file + defines one set of expected, ordered PCR measurements of a specific component of the boot. + + *.pcrlock files may be placed in various .d/ drop-in directores (see above for + a full list). All matching files discovered in these directories are sorted alphabetically by their file + name (without taking the actual directory they were found in into account): pcrlock files with + alphabetically earlier names are expected to cover measurements done before those with alphabetically + later names. In order to make positioning pcrlock files in the boot process convenient the files are + expected (by convention, this is not enforced) to be named + NNN-component.pcrlock (where + NNN is a three-digit decimal number), for example + 750-enter-initrd.pcrlock. + + For various components of the boot process more than one alternative pcrlock file shall be + supported (i.e. "variants"). For example to cover multiple kernels installed in parallel in the access + policy, or multiple versions of the boot loader. This can be done by placing + *.pcrlock.d/*.pcrlock in the drop-in dirs, i.e. a common directory for a specific + component, that contains one or more pcrlock files each covering one variant of the + component. Example: 650-kernel.pcrlock.d/6.5.5-200.fc38.x86_64.pcrlock and + 650-kernel.pcrlock.d/6.5.7-100.fc38.x86_64.pcrlock + + Use systemd-pcrlock list-components to list all pcrlock files currently + installed. + + Use the various lock-* commands of systemd-pcrlock to + automatically generate suitable pcrlock files for various types of resources. + + + + Well-known Components + + Components of the boot process may be defined freely by the administrator or OS vendor. The + following components are well-known however, and are defined by systemd. The list below is useful for + ordering local pcrlock files properly against these components of the boot. + + + + + 240-secureboot-policy.pcrlock + + The SecureBoot policy, as recorded to PCR 7. May be generated via + systemd-pcrlock lock-secureboot-policy. + + + + + + 250-firmware-code-early.pcrlock + + Firmware code measurements, as recorded to PCR 0 and 2, up to the separator + measurement (see 400-secureboot-separator.pcrlock. below). May be generated via + systemd-pcrlock lock-firmware-code. + + + + + + 250-firmware-config-early.pcrlock + + Firmware configuration measurements, as recorded to PCR 1 and 3, up to the separator + measurement (see 400-secureboot-separator.pcrlock. below). May be generated via + systemd-pcrlock lock-firmware-config. + + + + + + 350-action-efi-application.pcrlock + + The EFI "Application" measurement done once by the firmware. Statically defined. + + + + + + 400-secureboot-separator.pcrlock + + The EFI "separator" measurement on PCR 7 done once by the firmware to indicate where + firmware control transitions into boot loader/OS control. Statically defined. + + + + + + 500-separator.pcrlock + + The EFI "separator" measurements on PCRs 0-6 done once by the firmware to indicate + where firmware control transitions into boot loader/OS control. Statically defined. + + + + + + 550-firmware-code-late.pcrlock + + Firmware code measurements, as recorded to PCR 0 and 2, after the separator + measurement (see 400-secureboot-separator.pcrlock. above). May be generated via + systemd-pcrlock lock-firmware-code. + + + + + + 550-firmware-config-late.pcrlock + + Firmware configuration measurements, as recorded to PCR 1 and 3, after the separator + measurement (see 400-secureboot-separator.pcrlock. above). May be generated via + systemd-pcrlock lock-firmware-config. + + + + + + 600-gpt.pcrlock + + The GPT partition table of the booted medium, as recorded to PCR 5 by the + firmware. May be generated via systemd-pcrlock lock-gpt. + + + + + + 620-secureboot-authority.pcrlock + + The SecureBoot authority, as recorded to PCR 7. May be generated via + systemd-pcrlock lock-secureboot-authority. + + + + + + 700-action-efi-exit-boot-services.pcrlock + + The EFI action generated when ExitBootServices() is generated, + i.e. the UEFI environment is left and the OS takes over. Covers the PCR 5 measurement. Statically + defined. + + + + + + 710-kernel-cmdline.pcrlock + + The kernel command line, as measured by the Linux kernel to PCR 9. May be generated + via systemd-pcrlock lock-kernel-cmdline. + + + + + + 720-kernel-initrd.pcrlock + + The kernel initrd, as measured by the Linux kernel to PCR 9. May be generated + via systemd-pcrlock lock-kernel-initrd. + + + + + + 750-enter-initrd.pcrlock + + The measurement to PCR 11 + systemd-pcrphase-initrd.service8 + makes when the initrd initializes. Statically defined. + + + + + + 800-leave-initrd.pcrlock + + The measurement to PCR 11 + systemd-pcrphase-initrd.service8 + makes when the initrd finishes. Statically defined. + + + + + + 820-machine-id.pcrlock + + The measurement to PCR 15 + systemd-pcrmachine.service8 + makes at boot, covering /etc/machine-id contents. May be generated via + systemd-pcrlock lock-machine-id. + + + + + + 830-root-file-system.pcrlock + + The measurement to PCR 15 + systemd-pcrfs-root.service8 + makes at boot, covering the root file system identity. May be generated + via systemd-pcrlock lock-file-system. + + + + + + 850-sysinit.pcrlock + + The measurement to PCR 11 + systemd-pcrphase-sysinit.service8 + makes when the main userspace did basic initialization and will now proceed to start regular system + services. Statically defined. + + + + + + 900-ready.pcrlock + + The measurement to PCR 11 + systemd-pcrphase.service8 + makes when the system fully booted up. Statically defined. + + + + + + 950-shutdown.pcrlock + + The measurement to PCR 11 + systemd-pcrphase.service8 + makes when the system begins shutdown. Statically defined. + + + + + + 990-final.pcrlock + + The measurement to PCR 11 + systemd-pcrphase-sysinit.service8 + makes when the system is close to finishing shutdown. Statically defined. + + + + + + + + See Also + + systemd1, + systemd-pcrlock1 + + + + diff --git a/meson.build b/meson.build index 7f71114494..3e68f43ea6 100644 --- a/meson.build +++ b/meson.build @@ -174,6 +174,7 @@ profiledir = libexecdir / 'portable' / 'profile' repartdefinitionsdir = libexecdir / 'repart/definitions' ntpservicelistdir = prefixdir / 'lib/systemd/ntp-units.d' credstoredir = prefixdir / 'lib/credstore' +pcrlockdir = prefixdir / 'lib/pcrlock.d' configfiledir = get_option('configfiledir') if configfiledir == '' @@ -2176,6 +2177,7 @@ subdir('src/oom') subdir('src/partition') subdir('src/path') subdir('src/pcrextend') +subdir('src/pcrlock') subdir('src/portable') subdir('src/pstore') subdir('src/quotacheck') diff --git a/src/basic/efivars.h b/src/basic/efivars.h index bafe2d3128..34d697fbff 100644 --- a/src/basic/efivars.h +++ b/src/basic/efivars.h @@ -13,12 +13,14 @@ #include "efivars-fundamental.h" #include "time-util.h" -#define EFI_VENDOR_LOADER SD_ID128_MAKE(4a,67,b0,82,0a,4c,41,cf,b6,c7,44,0b,29,bb,8c,4f) -#define EFI_VENDOR_LOADER_STR SD_ID128_MAKE_UUID_STR(4a,67,b0,82,0a,4c,41,cf,b6,c7,44,0b,29,bb,8c,4f) -#define EFI_VENDOR_GLOBAL SD_ID128_MAKE(8b,e4,df,61,93,ca,11,d2,aa,0d,00,e0,98,03,2b,8c) -#define EFI_VENDOR_GLOBAL_STR SD_ID128_MAKE_UUID_STR(8b,e4,df,61,93,ca,11,d2,aa,0d,00,e0,98,03,2b,8c) -#define EFI_VENDOR_SYSTEMD SD_ID128_MAKE(8c,f2,64,4b,4b,0b,42,8f,93,87,6d,87,60,50,dc,67) -#define EFI_VENDOR_SYSTEMD_STR SD_ID128_MAKE_UUID_STR(8c,f2,64,4b,4b,0b,42,8f,93,87,6d,87,60,50,dc,67) +#define EFI_VENDOR_LOADER SD_ID128_MAKE(4a,67,b0,82,0a,4c,41,cf,b6,c7,44,0b,29,bb,8c,4f) +#define EFI_VENDOR_LOADER_STR SD_ID128_MAKE_UUID_STR(4a,67,b0,82,0a,4c,41,cf,b6,c7,44,0b,29,bb,8c,4f) +#define EFI_VENDOR_GLOBAL SD_ID128_MAKE(8b,e4,df,61,93,ca,11,d2,aa,0d,00,e0,98,03,2b,8c) +#define EFI_VENDOR_GLOBAL_STR SD_ID128_MAKE_UUID_STR(8b,e4,df,61,93,ca,11,d2,aa,0d,00,e0,98,03,2b,8c) +#define EFI_VENDOR_DATABASE SD_ID128_MAKE(d7,19,b2,cb,3d,3a,45,96,a3,bc,da,d0,0e,67,65,6f) +#define EFI_VENDOR_DATABASE_STR SD_ID128_MAKE_UUID_STR(d7,19,b2,cb,3d,3a,45,96,a3,bc,da,d0,0e,67,65,6f) +#define EFI_VENDOR_SYSTEMD SD_ID128_MAKE(8c,f2,64,4b,4b,0b,42,8f,93,87,6d,87,60,50,dc,67) +#define EFI_VENDOR_SYSTEMD_STR SD_ID128_MAKE_UUID_STR(8c,f2,64,4b,4b,0b,42,8f,93,87,6d,87,60,50,dc,67) #define EFI_VARIABLE_NON_VOLATILE UINT32_C(0x00000001) #define EFI_VARIABLE_BOOTSERVICE_ACCESS UINT32_C(0x00000002) diff --git a/src/cryptenroll/cryptenroll-tpm2.c b/src/cryptenroll/cryptenroll-tpm2.c index 4b8f3cc4f8..1273822d3a 100644 --- a/src/cryptenroll/cryptenroll-tpm2.c +++ b/src/cryptenroll/cryptenroll-tpm2.c @@ -139,7 +139,8 @@ int enroll_tpm2(struct crypt_device *cd, const char *pubkey_path, uint32_t pubkey_pcr_mask, const char *signature_path, - bool use_pin) { + bool use_pin, + const char *pcrlock_path) { _cleanup_(erase_and_freep) void *secret = NULL; _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *signature_json = NULL; @@ -207,6 +208,15 @@ int enroll_tpm2(struct crypt_device *cd, return log_debug_errno(r, "Failed to read TPM PCR signature: %m"); } + _cleanup_(tpm2_pcrlock_policy_done) Tpm2PCRLockPolicy pcrlock_policy = {}; + if (pcrlock_path) { + r = tpm2_pcrlock_policy_load(pcrlock_path, &pcrlock_policy); + if (r < 0) + return r; + + flags |= TPM2_FLAGS_USE_PCRLOCK; + } + _cleanup_(tpm2_context_unrefp) Tpm2Context *tpm2_context = NULL; r = tpm2_context_new(device, &tpm2_context); if (r < 0) @@ -248,6 +258,7 @@ int enroll_tpm2(struct crypt_device *cd, n_hash_pcr_values, pubkey ? &public : NULL, use_pin, + pcrlock_path ? &pcrlock_policy : NULL, &policy); if (r < 0) return r; @@ -288,6 +299,7 @@ int enroll_tpm2(struct crypt_device *cd, pubkey_pcr_mask, signature_json, pin_str, + pcrlock_path ? &pcrlock_policy : NULL, /* primary_alg= */ 0, blob, blob_size, policy.buffer, policy.size, diff --git a/src/cryptenroll/cryptenroll-tpm2.h b/src/cryptenroll/cryptenroll-tpm2.h index 8a57bdda01..6439a08df4 100644 --- a/src/cryptenroll/cryptenroll-tpm2.h +++ b/src/cryptenroll/cryptenroll-tpm2.h @@ -8,9 +8,9 @@ #include "tpm2-util.h" #if HAVE_TPM2 -int enroll_tpm2(struct crypt_device *cd, const void *volume_key, size_t volume_key_size, const char *device, uint32_t seal_key_handle, Tpm2PCRValue *hash_pcrs, size_t n_hash_pcrs, const char *pubkey_path, uint32_t pubkey_pcr_mask, const char *signature_path, bool use_pin); +int enroll_tpm2(struct crypt_device *cd, const void *volume_key, size_t volume_key_size, const char *device, uint32_t seal_key_handle, Tpm2PCRValue *hash_pcrs, size_t n_hash_pcrs, const char *pubkey_path, uint32_t pubkey_pcr_mask, const char *signature_path, bool use_pin, const char *pcrlock_path); #else -static inline int enroll_tpm2(struct crypt_device *cd, const void *volume_key, size_t volume_key_size, const char *device, uint32_t seal_key_handle, Tpm2PCRValue *hash_pcrs, size_t n_hash_pcrs, const char *pubkey_path, uint32_t pubkey_pcr_mask, const char *signature_path, bool use_pin) { +static inline int enroll_tpm2(struct crypt_device *cd, const void *volume_key, size_t volume_key_size, const char *device, uint32_t seal_key_handle, Tpm2PCRValue *hash_pcrs, size_t n_hash_pcrs, const char *pubkey_path, uint32_t pubkey_pcr_mask, const char *signature_path, bool use_pin, const char *pcrlock_path) { return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "TPM2 key enrollment not supported."); } diff --git a/src/cryptenroll/cryptenroll.c b/src/cryptenroll/cryptenroll.c index 8b65485a29..20d6bc2680 100644 --- a/src/cryptenroll/cryptenroll.c +++ b/src/cryptenroll/cryptenroll.c @@ -43,6 +43,7 @@ static bool arg_tpm2_pin = false; static char *arg_tpm2_public_key = NULL; static uint32_t arg_tpm2_public_key_pcr_mask = 0; static char *arg_tpm2_signature = NULL; +static char *arg_tpm2_pcrlock = NULL; static char *arg_node = NULL; static int *arg_wipe_slots = NULL; static size_t arg_n_wipe_slots = 0; @@ -65,6 +66,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_hash_pcr_values, freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_public_key, freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_signature, freep); +STATIC_DESTRUCTOR_REGISTER(arg_tpm2_pcrlock, freep); STATIC_DESTRUCTOR_REGISTER(arg_node, freep); STATIC_DESTRUCTOR_REGISTER(arg_wipe_slots, freep); @@ -144,6 +146,8 @@ static int help(void) { " --tpm2-signature=PATH\n" " Validate public key enrollment works with JSON signature\n" " file\n" + " --tpm2-pcrlock=PATH\n" + " Specify pcrlock policy to lock against\n" " --tpm2-with-pin=BOOL\n" " Whether to require entering a PIN to unlock the volume\n" "\nSee the %2$s for details.\n", @@ -173,6 +177,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_TPM2_PUBLIC_KEY_PCRS, ARG_TPM2_SIGNATURE, ARG_TPM2_PIN, + ARG_TPM2_PCRLOCK, ARG_WIPE_SLOT, ARG_FIDO2_WITH_PIN, ARG_FIDO2_WITH_UP, @@ -200,11 +205,12 @@ static int parse_argv(int argc, char *argv[]) { { "tpm2-public-key-pcrs", required_argument, NULL, ARG_TPM2_PUBLIC_KEY_PCRS }, { "tpm2-signature", required_argument, NULL, ARG_TPM2_SIGNATURE }, { "tpm2-with-pin", required_argument, NULL, ARG_TPM2_PIN }, + { "tpm2-pcrlock", required_argument, NULL, ARG_TPM2_PCRLOCK }, { "wipe-slot", required_argument, NULL, ARG_WIPE_SLOT }, {} }; - bool auto_hash_pcr_values = true, auto_public_key_pcr_mask = true; + bool auto_hash_pcr_values = true, auto_public_key_pcr_mask = true, auto_pcrlock = true; int c, r; assert(argc >= 0); @@ -412,6 +418,14 @@ static int parse_argv(int argc, char *argv[]) { break; + case ARG_TPM2_PCRLOCK: + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_tpm2_pcrlock); + if (r < 0) + return r; + + auto_pcrlock = false; + break; + case ARG_WIPE_SLOT: { const char *p = optarg; @@ -500,12 +514,23 @@ static int parse_argv(int argc, char *argv[]) { } } + if (auto_pcrlock) { + assert(!arg_tpm2_pcrlock); + + r = tpm2_pcrlock_search_file(NULL, NULL, &arg_tpm2_pcrlock); + if (r < 0) { + if (r != -ENOENT) + log_warning_errno(r, "Search for pcrlock.json failed, assuming it does not exist: %m"); + } else + log_info("Automatically using pcrlock policy '%s'.", arg_tpm2_pcrlock); + } + if (auto_public_key_pcr_mask) { assert(arg_tpm2_public_key_pcr_mask == 0); arg_tpm2_public_key_pcr_mask = INDEX_TO_MASK(uint32_t, TPM2_PCR_KERNEL_BOOT); } - if (auto_hash_pcr_values) { + if (auto_hash_pcr_values && !arg_tpm2_pcrlock) { /* Only lock to PCR 7 by default if no pcrlock policy is around (which is a better replacement) */ assert(arg_tpm2_n_hash_pcr_values == 0); if (!GREEDY_REALLOC_APPEND( @@ -690,7 +715,7 @@ static int run(int argc, char *argv[]) { break; case ENROLL_TPM2: - slot = enroll_tpm2(cd, vk, vks, arg_tpm2_device, arg_tpm2_seal_key_handle, arg_tpm2_hash_pcr_values, arg_tpm2_n_hash_pcr_values, arg_tpm2_public_key, arg_tpm2_public_key_pcr_mask, arg_tpm2_signature, arg_tpm2_pin); + slot = enroll_tpm2(cd, vk, vks, arg_tpm2_device, arg_tpm2_seal_key_handle, arg_tpm2_hash_pcr_values, arg_tpm2_n_hash_pcr_values, arg_tpm2_public_key, arg_tpm2_public_key_pcr_mask, arg_tpm2_signature, arg_tpm2_pin, arg_tpm2_pcrlock); break; case _ENROLL_TYPE_INVALID: diff --git a/src/cryptsetup/cryptsetup-tokens/cryptsetup-token-systemd-tpm2.c b/src/cryptsetup/cryptsetup-tokens/cryptsetup-token-systemd-tpm2.c index 94d568c17f..6da80896cd 100644 --- a/src/cryptsetup/cryptsetup-tokens/cryptsetup-token-systemd-tpm2.c +++ b/src/cryptsetup/cryptsetup-tokens/cryptsetup-token-systemd-tpm2.c @@ -109,6 +109,7 @@ _public_ int cryptsetup_token_open_pin( pubkey_pcr_mask, params.signature_path, pin_string, + params.pcrlock_path, primary_alg, blob, blob_size, @@ -239,6 +240,7 @@ _public_ void cryptsetup_token_dump( crypt_log(cd, "\ttpm2-blob: %s\n", blob_str); crypt_log(cd, "\ttpm2-policy-hash:" CRYPT_DUMP_LINE_SEP "%s\n", policy_hash_str); crypt_log(cd, "\ttpm2-pin: %s\n", true_false(flags & TPM2_FLAGS_USE_PIN)); + crypt_log(cd, "\ttpm2-pcrlock: %s\n", true_false(flags & TPM2_FLAGS_USE_PCRLOCK)); crypt_log(cd, "\ttpm2-salt: %s\n", true_false(salt)); crypt_log(cd, "\ttpm2-srk: %s\n", true_false(srk_buf)); } diff --git a/src/cryptsetup/cryptsetup-tokens/luks2-tpm2.c b/src/cryptsetup/cryptsetup-tokens/luks2-tpm2.c index 34bf92c750..72be5cc71d 100644 --- a/src/cryptsetup/cryptsetup-tokens/luks2-tpm2.c +++ b/src/cryptsetup/cryptsetup-tokens/luks2-tpm2.c @@ -22,6 +22,7 @@ int acquire_luks2_key( uint32_t pubkey_pcr_mask, const char *signature_path, const char *pin, + const char *pcrlock_path, uint16_t primary_alg, const void *key_data, size_t key_data_size, @@ -76,6 +77,13 @@ int acquire_luks2_key( return log_error_errno(r, "Failed to load PCR signature: %m"); } + _cleanup_(tpm2_pcrlock_policy_done) Tpm2PCRLockPolicy pcrlock_policy = {}; + if (FLAGS_SET(flags, TPM2_FLAGS_USE_PCRLOCK)) { + r = tpm2_pcrlock_policy_load(pcrlock_path, &pcrlock_policy); + if (r < 0) + return r; + } + _cleanup_(tpm2_context_unrefp) Tpm2Context *tpm2_context = NULL; r = tpm2_context_new(device, &tpm2_context); if (r < 0) @@ -88,6 +96,7 @@ int acquire_luks2_key( pubkey_pcr_mask, signature_json, pin, + FLAGS_SET(flags, TPM2_FLAGS_USE_PCRLOCK) ? &pcrlock_policy : NULL, primary_alg, key_data, key_data_size, policy_hash, policy_hash_size, diff --git a/src/cryptsetup/cryptsetup-tokens/luks2-tpm2.h b/src/cryptsetup/cryptsetup-tokens/luks2-tpm2.h index 1143f5fd9f..d84e5a3c3b 100644 --- a/src/cryptsetup/cryptsetup-tokens/luks2-tpm2.h +++ b/src/cryptsetup/cryptsetup-tokens/luks2-tpm2.h @@ -14,6 +14,7 @@ int acquire_luks2_key( size_t pubkey_size, uint32_t pubkey_pcr_mask, const char *signature_path, + const char *pcrlock_path, const char *pin, uint16_t primary_alg, const void *key_data, diff --git a/src/cryptsetup/cryptsetup-tpm2.c b/src/cryptsetup/cryptsetup-tpm2.c index 41dca776d1..f59d5f9d1d 100644 --- a/src/cryptsetup/cryptsetup-tpm2.c +++ b/src/cryptsetup/cryptsetup-tpm2.c @@ -62,6 +62,7 @@ int acquire_tpm2_key( size_t pubkey_size, uint32_t pubkey_pcr_mask, const char *signature_path, + const char *pcrlock_path, uint16_t primary_alg, const char *key_file, size_t key_file_size, @@ -129,6 +130,14 @@ int acquire_tpm2_key( return log_error_errno(r, "Failed to load pcr signature: %m"); } + _cleanup_(tpm2_pcrlock_policy_done) Tpm2PCRLockPolicy pcrlock_policy = {}; + + if (FLAGS_SET(flags, TPM2_FLAGS_USE_PCRLOCK)) { + r = tpm2_pcrlock_policy_load(pcrlock_path, &pcrlock_policy); + if (r < 0) + return r; + } + _cleanup_(tpm2_context_unrefp) Tpm2Context *tpm2_context = NULL; r = tpm2_context_new(device, &tpm2_context); if (r < 0) @@ -142,6 +151,7 @@ int acquire_tpm2_key( pubkey_pcr_mask, signature_json, /* pin= */ NULL, + FLAGS_SET(flags, TPM2_FLAGS_USE_PCRLOCK) ? &pcrlock_policy : NULL, primary_alg, blob, blob_size, @@ -189,6 +199,7 @@ int acquire_tpm2_key( pubkey_pcr_mask, signature_json, b64_salted_pin, + pcrlock_path ? &pcrlock_policy : NULL, primary_alg, blob, blob_size, diff --git a/src/cryptsetup/cryptsetup-tpm2.h b/src/cryptsetup/cryptsetup-tpm2.h index a510ac6257..a50a9435a9 100644 --- a/src/cryptsetup/cryptsetup-tpm2.h +++ b/src/cryptsetup/cryptsetup-tpm2.h @@ -20,6 +20,7 @@ int acquire_tpm2_key( size_t pubkey_size, uint32_t pubkey_pcr_mask, const char *signature_path, + const char *pcrlock_path, uint16_t primary_alg, const char *key_file, size_t key_file_size, @@ -72,6 +73,7 @@ static inline int acquire_tpm2_key( size_t pubkey_size, uint32_t pubkey_pcr_mask, const char *signature_path, + const char *pcrlock_path, uint16_t primary_alg, const char *key_file, size_t key_file_size, diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c index 1ebebcb203..1da7bff049 100644 --- a/src/cryptsetup/cryptsetup.c +++ b/src/cryptsetup/cryptsetup.c @@ -100,6 +100,7 @@ static bool arg_tpm2_device_auto = false; static uint32_t arg_tpm2_pcr_mask = UINT32_MAX; static char *arg_tpm2_signature = NULL; static bool arg_tpm2_pin = false; +static char *arg_tpm2_pcrlock = NULL; static bool arg_headless = false; static usec_t arg_token_timeout_usec = 30*USEC_PER_SEC; static unsigned arg_tpm2_measure_pcr = UINT_MAX; /* This and the following field is about measuring the unlocked volume key to the local TPM */ @@ -116,6 +117,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_fido2_rp_id, freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_signature, freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_measure_banks, strv_freep); +STATIC_DESTRUCTOR_REGISTER(arg_tpm2_pcrlock, freep); static const char* const passphrase_type_table[_PASSPHRASE_TYPE_MAX] = { [PASSPHRASE_REGULAR] = "passphrase", @@ -424,6 +426,16 @@ static int parse_one_option(const char *option) { arg_tpm2_pin = r; + } else if ((val = startswith(option, "tpm2-pcrlock="))) { + + if (!path_is_absolute(val)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "TPM2 pcrlock policy path \"%s\" is not absolute, refusing.", val); + + r = free_and_strdup(&arg_tpm2_pcrlock, val); + if (r < 0) + return log_oom(); + } else if ((val = startswith(option, "tpm2-measure-pcr="))) { unsigned pcr; @@ -1605,6 +1617,7 @@ static int attach_luks2_by_tpm2_via_plugin( .search_pcr_mask = arg_tpm2_pcr_mask, .device = arg_tpm2_device, .signature_path = arg_tpm2_signature, + .pcrlock_path = arg_tpm2_pcrlock, }; if (!libcryptsetup_plugins_support()) @@ -1664,6 +1677,7 @@ static int attach_luks_or_plain_or_bitlk_by_tpm2( /* pubkey= */ NULL, /* pubkey_size= */ 0, /* pubkey_pcr_mask= */ 0, /* signature_path= */ NULL, + /* pcrlock_path= */ NULL, /* primary_alg= */ 0, key_file, arg_keyfile_size, arg_keyfile_offset, key_data, key_data_size, @@ -1761,6 +1775,7 @@ static int attach_luks_or_plain_or_bitlk_by_tpm2( pubkey, pubkey_size, pubkey_pcr_mask, arg_tpm2_signature, + arg_tpm2_pcrlock, primary_alg, /* key_file= */ NULL, /* key_file_size= */ 0, /* key_file_offset= */ 0, /* no key file */ blob, blob_size, diff --git a/src/partition/repart.c b/src/partition/repart.c index c99168b08f..01cfc46f1c 100644 --- a/src/partition/repart.c +++ b/src/partition/repart.c @@ -152,6 +152,7 @@ static Tpm2PCRValue *arg_tpm2_hash_pcr_values = NULL; static size_t arg_tpm2_n_hash_pcr_values = 0; static char *arg_tpm2_public_key = NULL; static uint32_t arg_tpm2_public_key_pcr_mask = 0; +static char *arg_tpm2_pcrlock = NULL; static bool arg_split = false; static GptPartitionType *arg_filter_partitions = NULL; static size_t arg_n_filter_partitions = 0; @@ -175,6 +176,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_certificate, X509_freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_hash_pcr_values, freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_public_key, freep); +STATIC_DESTRUCTOR_REGISTER(arg_tpm2_pcrlock, freep); STATIC_DESTRUCTOR_REGISTER(arg_filter_partitions, freep); STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep); STATIC_DESTRUCTOR_REGISTER(arg_copy_from, strv_freep); @@ -3821,8 +3823,21 @@ static int partition_encrypt(Context *context, Partition *p, PartitionTarget *ta return log_error_errno(r, "Could not get hash mask: %m"); } + _cleanup_(tpm2_pcrlock_policy_done) Tpm2PCRLockPolicy pcrlock_policy = {}; + if (arg_tpm2_pcrlock) { + r = tpm2_pcrlock_policy_load(arg_tpm2_pcrlock, &pcrlock_policy); + if (r < 0) + return r; + } + TPM2B_DIGEST policy = TPM2B_DIGEST_MAKE(NULL, TPM2_SHA256_DIGEST_SIZE); - r = tpm2_calculate_sealing_policy(arg_tpm2_hash_pcr_values, arg_tpm2_n_hash_pcr_values, pubkey ? &public : NULL, /* use_pin= */ false, &policy); + r = tpm2_calculate_sealing_policy( + arg_tpm2_hash_pcr_values, + arg_tpm2_n_hash_pcr_values, + pubkey ? &public : NULL, + /* use_pin= */ false, + arg_tpm2_pcrlock ? &pcrlock_policy : NULL, + &policy); if (r < 0) return log_error_errno(r, "Could not calculate sealing policy digest: %m"); @@ -6435,6 +6450,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_TPM2_PCRS, ARG_TPM2_PUBLIC_KEY, ARG_TPM2_PUBLIC_KEY_PCRS, + ARG_TPM2_PCRLOCK, ARG_SPLIT, ARG_INCLUDE_PARTITIONS, ARG_EXCLUDE_PARTITIONS, @@ -6472,6 +6488,7 @@ static int parse_argv(int argc, char *argv[]) { { "tpm2-pcrs", required_argument, NULL, ARG_TPM2_PCRS }, { "tpm2-public-key", required_argument, NULL, ARG_TPM2_PUBLIC_KEY }, { "tpm2-public-key-pcrs", required_argument, NULL, ARG_TPM2_PUBLIC_KEY_PCRS }, + { "tpm2-pcrlock", required_argument, NULL, ARG_TPM2_PCRLOCK }, { "split", required_argument, NULL, ARG_SPLIT }, { "include-partitions", required_argument, NULL, ARG_INCLUDE_PARTITIONS }, { "exclude-partitions", required_argument, NULL, ARG_EXCLUDE_PARTITIONS }, @@ -6485,7 +6502,7 @@ static int parse_argv(int argc, char *argv[]) { {} }; - bool auto_hash_pcr_values = true, auto_public_key_pcr_mask = true; + bool auto_hash_pcr_values = true, auto_public_key_pcr_mask = true, auto_pcrlock = true; int c, r; assert(argc >= 0); @@ -6728,6 +6745,14 @@ static int parse_argv(int argc, char *argv[]) { break; + case ARG_TPM2_PCRLOCK: + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_tpm2_pcrlock); + if (r < 0) + return r; + + auto_pcrlock = false; + break; + case ARG_SPLIT: r = parse_boolean_argument("--split=", optarg, NULL); if (r < 0) @@ -6935,12 +6960,23 @@ static int parse_argv(int argc, char *argv[]) { return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "A path to an image file must be specified when --split is used."); + if (auto_pcrlock) { + assert(!arg_tpm2_pcrlock); + + r = tpm2_pcrlock_search_file(NULL, NULL, &arg_tpm2_pcrlock); + if (r < 0) { + if (r != -ENOENT) + log_warning_errno(r, "Search for pcrlock.json failed, assuming it does not exist: %m"); + } else + log_info("Automatically using pcrlock policy '%s'.", arg_tpm2_pcrlock); + } + if (auto_public_key_pcr_mask) { assert(arg_tpm2_public_key_pcr_mask == 0); arg_tpm2_public_key_pcr_mask = INDEX_TO_MASK(uint32_t, TPM2_PCR_KERNEL_BOOT); } - if (auto_hash_pcr_values) { + if (auto_hash_pcr_values && !arg_tpm2_pcrlock) { /* Only lock to PCR 7 if no pcr policy is specified. */ assert(arg_tpm2_n_hash_pcr_values == 0); if (!GREEDY_REALLOC_APPEND( diff --git a/src/pcrlock/meson.build b/src/pcrlock/meson.build new file mode 100644 index 0000000000..a31b30bb15 --- /dev/null +++ b/src/pcrlock/meson.build @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +executables += [ + libexec_template + { + 'name' : 'systemd-pcrlock', + 'conditions' : [ + 'HAVE_OPENSSL', + 'HAVE_TPM2' + ], + 'sources' : files( + 'pcrlock.c', + 'pcrlock-firmware.c', + 'pehash.c', + ), + 'dependencies' : [ + libm, + libopenssl, + tpm2, + ], + }, +] + +if conf.get('HAVE_OPENSSL') == 1 and conf.get('HAVE_TPM2') == 1 + install_data('pcrlock.d/350-action-efi-application.pcrlock', install_dir : pcrlockdir) + install_data('pcrlock.d/400-secureboot-separator.pcrlock.d/300-0x00000000.pcrlock', install_dir : pcrlockdir / '400-secureboot-separator.pcrlock.d') + install_data('pcrlock.d/400-secureboot-separator.pcrlock.d/600-0xffffffff.pcrlock', install_dir : pcrlockdir / '400-secureboot-separator.pcrlock.d') + install_data('pcrlock.d/500-separator.pcrlock.d/300-0x00000000.pcrlock', install_dir : pcrlockdir / '500-separator.pcrlock.d') + install_data('pcrlock.d/500-separator.pcrlock.d/600-0xffffffff.pcrlock', install_dir : pcrlockdir / '500-separator.pcrlock.d') + install_data('pcrlock.d/700-action-efi-exit-boot-services.pcrlock.d/300-present.pcrlock', install_dir : pcrlockdir / '700-action-efi-exit-boot-services.pcrlock.d') + install_data('pcrlock.d/700-action-efi-exit-boot-services.pcrlock.d/600-absent.pcrlock', install_dir : pcrlockdir / '700-action-efi-exit-boot-services.pcrlock.d') + install_data('pcrlock.d/750-enter-initrd.pcrlock', install_dir : pcrlockdir) + install_data('pcrlock.d/800-leave-initrd.pcrlock', install_dir : pcrlockdir) + install_data('pcrlock.d/850-sysinit.pcrlock', install_dir : pcrlockdir) + install_data('pcrlock.d/900-ready.pcrlock', install_dir : pcrlockdir) + install_data('pcrlock.d/950-shutdown.pcrlock', install_dir : pcrlockdir) + install_data('pcrlock.d/990-final.pcrlock', install_dir : pcrlockdir) +endif diff --git a/src/pcrlock/pcrlock-firmware.c b/src/pcrlock/pcrlock-firmware.c new file mode 100644 index 0000000000..73c68c2237 --- /dev/null +++ b/src/pcrlock/pcrlock-firmware.c @@ -0,0 +1,168 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "pcrlock-firmware.h" +#include "unaligned.h" + +static int tcg_pcr_event2_digests_size( + const TCG_EfiSpecIdEventAlgorithmSize *algorithms, + size_t n_algorithms, + size_t *ret) { + + size_t m = 0; + + assert(algorithms || n_algorithms == 0); + assert(ret); + + FOREACH_ARRAY(a, algorithms, n_algorithms) { + + if (a->digestSize > UINT32_MAX - offsetof(TPMT_HA, digest) - m) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Accumulated hash size too large"); + + m += offsetof(TPMT_HA, digest) + a->digestSize; + } + + *ret = m; + return 0; +} + +int validate_firmware_event( + const TCG_PCR_EVENT2 *event, + size_t left, + const TCG_EfiSpecIdEventAlgorithmSize *algorithms, + size_t n_algorithms, + const TCG_PCR_EVENT2 **ret_next_event, + size_t *ret_left, + const void **ret_payload, + size_t *ret_payload_size) { + + size_t digests_size; + int r; + + assert(event); + assert(algorithms || n_algorithms == 0); + assert(ret_next_event); + assert(ret_left); + + if (left == 0) { + *ret_next_event = NULL; + *ret_left = 0; + return 0; + } + + r = tcg_pcr_event2_digests_size(algorithms, n_algorithms, &digests_size); + if (r < 0) + return r; + + if (left < (uint64_t) offsetof(TCG_PCR_EVENT2, digests.digests) + (uint64_t) digests_size + sizeof(uint32_t)) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Event header too short."); + + if (event->digests.count != n_algorithms) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Number of digests in event doesn't match log."); + + uint32_t eventSize = unaligned_read_ne32((const uint8_t*) &event->digests.digests + digests_size); + uint64_t size = (uint64_t) offsetof(TCG_PCR_EVENT2, digests.digests) + (uint64_t) digests_size + sizeof(uint32_t) + eventSize; + + if (size > left) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Event header too short."); + + *ret_next_event = (const TCG_PCR_EVENT2*) ((const uint8_t*) event + size); + *ret_left = left - size; + + if (ret_payload) + *ret_payload = (const uint8_t*) &event->digests.digests + digests_size + sizeof(uint32_t); + if (ret_payload_size) + *ret_payload_size = eventSize; + + return 1; +} + +int validate_firmware_header( + const void *start, + size_t size, + const TCG_EfiSpecIdEventAlgorithmSize **ret_algorithms, + size_t *ret_n_algorithms, + const TCG_PCR_EVENT2 **ret_first, + size_t *ret_left) { + + assert(start || size == 0); + assert(ret_algorithms); + assert(ret_n_algorithms); + assert(ret_first); + assert(ret_left); + + if (size < offsetof(TCG_PCClientPCREvent, event)) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Event log too short for TCG_PCClientPCREvent."); + + const TCG_PCClientPCREvent *h = start; + + if (size < (uint64_t) offsetof(TCG_PCClientPCREvent, event) + (uint64_t) h->eventDataSize) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Event log too short for TCG_PCClientPCREvent events data."); + + if (h->pcrIndex != 0) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Event log header has unexpected PCR index %" PRIu32, h->pcrIndex); + if (h->eventType != EV_NO_ACTION) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Event log header has unexpected event type 0x%" PRIx32, h->eventType); + if (!memeqzero(h->digest, sizeof(h->digest))) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Event log header has unexpected non-zero digest."); + + if (h->eventDataSize < offsetof(TCG_EfiSpecIDEvent, digestSizes)) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Event log header too short for TCG_EfiSpecIdEvent."); + + const TCG_EfiSpecIDEvent *id = (const TCG_EfiSpecIDEvent*) h->event; + + /* Signature as per "TCG PC Client Specific Platform Firmware Profile Specification" + * (https://trustedcomputinggroup.org/resource/pc-client-specific-platform-firmware-profile-specification/), + * section 10.4.5.1 "Specification ID Version Event" (at least in version 1.05 Revision 23 of the + * spec) */ + if (memcmp(id->signature, + (const uint8_t[]) { 0x53, 0x70, 0x65, 0x63, 0x20, 0x49, 0x44, 0x20, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x33, 0x00 }, + sizeof(id->signature)) != 0) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Missing TPM2 event log signature."); + + if (id->numberOfAlgorithms <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Number of advertised hash algorithms is zero."); + if (id->numberOfAlgorithms > UINT32_MAX / sizeof(TCG_EfiSpecIdEventAlgorithmSize)) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Number of advertised hash algorithms too large."); + + log_debug("TPM PC Client Platform Firmware Profile: family %u.%u, revision %u.%u", + id->specVersionMajor, id->specVersionMinor, + id->specErrata / 100, id->specErrata % 100); + + if (h->eventDataSize < (uint64_t) offsetof(TCG_EfiSpecIDEvent, digestSizes) + (uint64_t) (id->numberOfAlgorithms * sizeof(TCG_EfiSpecIdEventAlgorithmSize)) + 1U) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Event log header doesn't fit all algorithms."); + + uint8_t vendorInfoSize = *((const uint8_t*) id + offsetof(TCG_EfiSpecIDEvent, digestSizes) + (id->numberOfAlgorithms * sizeof(TCG_EfiSpecIdEventAlgorithmSize))); + if (h->eventDataSize != offsetof(TCG_EfiSpecIDEvent, digestSizes) + (id->numberOfAlgorithms * sizeof(TCG_EfiSpecIdEventAlgorithmSize)) + 1U + vendorInfoSize) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Event log header doesn't fit vendor info."); + + for (size_t i = 0; i < id->numberOfAlgorithms; i++) { + const EVP_MD *implementation; + const char *a; + + a = tpm2_hash_alg_to_string(id->digestSizes[i].algorithmId); + if (!a) { + log_notice("Event log advertises unknown hash algorithm 0x%4x, can't validate.", id->digestSizes[i].algorithmId); + continue; + } + + implementation = EVP_get_digestbyname(a); + if (!implementation) { + log_notice("Event log advertises hash algorithm '%s' we don't implement, can't validate.", a); + continue; + } + + if (EVP_MD_size(implementation) != id->digestSizes[i].digestSize) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Advertised digest size for '%s' is wrong, refusing.", a); + } + + *ret_algorithms = id->digestSizes; + *ret_n_algorithms = id->numberOfAlgorithms; + + size_t offset = offsetof(TCG_PCClientPCREvent, event) + h->eventDataSize; + *ret_first = (TCG_PCR_EVENT2*) ((const uint8_t*) h + offset); + *ret_left = size - offset; + + return 0; +} diff --git a/src/pcrlock/pcrlock-firmware.h b/src/pcrlock/pcrlock-firmware.h new file mode 100644 index 0000000000..169666ede9 --- /dev/null +++ b/src/pcrlock/pcrlock-firmware.h @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include + +#include "tpm2-event-log.h" +#include "tpm2-util.h" + +int validate_firmware_event( + const TCG_PCR_EVENT2 *event, + size_t left, + const TCG_EfiSpecIdEventAlgorithmSize *algorithms, + size_t n_algorithms, + const TCG_PCR_EVENT2 **ret_next_event, + size_t *ret_left, + const void **ret_payload, + size_t *ret_payload_size); + +int validate_firmware_header( + const void *start, + size_t size, + const TCG_EfiSpecIdEventAlgorithmSize **ret_algorithms, + size_t *ret_n_algorithms, + const TCG_PCR_EVENT2 **ret_first, + size_t *ret_left); diff --git a/src/pcrlock/pcrlock.c b/src/pcrlock/pcrlock.c new file mode 100644 index 0000000000..cd2a6a6071 --- /dev/null +++ b/src/pcrlock/pcrlock.c @@ -0,0 +1,4992 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include +#include +#include + +#include "sd-device.h" + +#include "ask-password-api.h" +#include "blockdev-util.h" +#include "build.h" +#include "chase.h" +#include "conf-files.h" +#include "efi-api.h" +#include "env-util.h" +#include "escape.h" +#include "fd-util.h" +#include "fileio.h" +#include "format-table.h" +#include "format-util.h" +#include "fs-util.h" +#include "gpt.h" +#include "hash-funcs.h" +#include "hexdecoct.h" +#include "initrd-util.h" +#include "main-func.h" +#include "mkdir-label.h" +#include "openssl-util.h" +#include "ordered-set.h" +#include "parse-argument.h" +#include "parse-util.h" +#include "path-util.h" +#include "pcrextend-util.h" +#include "pcrlock-firmware.h" +#include "pehash.h" +#include "pretty-print.h" +#include "proc-cmdline.h" +#include "random-util.h" +#include "recovery-key.h" +#include "sort-util.h" +#include "terminal-util.h" +#include "tpm2-util.h" +#include "unaligned.h" +#include "unit-name.h" +#include "utf8.h" +#include "verbs.h" + +static PagerFlags arg_pager_flags = 0; +static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF|JSON_FORMAT_NEWLINE; +static char **arg_components = NULL; +static uint32_t arg_pcr_mask = 0; +static char *arg_pcrlock_path = NULL; +static bool arg_pcrlock_auto = true; +static bool arg_raw_description = false; +static char *arg_location_start = NULL; +static char *arg_location_end = NULL; +static TPM2_HANDLE arg_nv_index = 0; +static bool arg_recovery_pin = false; +static char *arg_policy_path = NULL; +static bool arg_force = false; + +STATIC_DESTRUCTOR_REGISTER(arg_components, strv_freep); +STATIC_DESTRUCTOR_REGISTER(arg_pcrlock_path, freep); +STATIC_DESTRUCTOR_REGISTER(arg_location_start, freep); +STATIC_DESTRUCTOR_REGISTER(arg_location_end, freep); +STATIC_DESTRUCTOR_REGISTER(arg_policy_path, freep); + +#define PCRLOCK_SECUREBOOT_POLICY_PATH "/var/lib/pcrlock.d/240-secureboot-policy.pcrlock.d/generated.pcrlock" +#define PCRLOCK_FIRMWARE_CODE_EARLY_PATH "/var/lib/pcrlock.d/250-firmware-code-early.pcrlock.d/generated.pcrlock" +#define PCRLOCK_FIRMWARE_CONFIG_EARLY_PATH "/var/lib/pcrlock.d/250-firmware-config-early.pcrlock.d/generated.pcrlock" +#define PCRLOCK_FIRMWARE_CODE_LATE_PATH "/var/lib/pcrlock.d/550-firmware-code-late.pcrlock.d/generated.pcrlock" +#define PCRLOCK_FIRMWARE_CONFIG_LATE_PATH "/var/lib/pcrlock.d/550-firmware-config-late.pcrlock.d/generated.pcrlock" +#define PCRLOCK_GPT_PATH "/var/lib/pcrlock.d/600-gpt.pcrlock.d/generated.pcrlock" +#define PCRLOCK_SECUREBOOT_AUTHORITY_PATH "/var/lib/pcrlock.d/620-secureboot-authority.pcrlock.d/generated.pcrlock" +#define PCRLOCK_KERNEL_CMDLINE_PATH "/var/lib/pcrlock.d/710-kernel-cmdline.pcrlock/generated.pcrlock" +#define PCRLOCK_KERNEL_INITRD_PATH "/var/lib/pcrlock.d/720-kernel-initrd.pcrlock/generated.pcrlock" +#define PCRLOCK_MACHINE_ID_PATH "/var/lib/pcrlock.d/820-machine-id.pcrlock" +#define PCRLOCK_ROOT_FILE_SYSTEM_PATH "/var/lib/pcrlock.d/830-root-file-system.pcrlock" +#define PCRLOCK_FILE_SYSTEM_PATH_PREFIX "/var/lib/pcrlock.d/840-file-system-" + +/* The default set of PCRs to lock to */ +#define DEFAULT_PCR_MASK \ + ((UINT32_C(1) << TPM2_PCR_PLATFORM_CODE) | \ + (UINT32_C(1) << TPM2_PCR_PLATFORM_CONFIG) | \ + (UINT32_C(1) << TPM2_PCR_EXTERNAL_CODE) | \ + (UINT32_C(1) << TPM2_PCR_EXTERNAL_CONFIG) | \ + (UINT32_C(1) << TPM2_PCR_BOOT_LOADER_CODE) | \ + (UINT32_C(1) << TPM2_PCR_BOOT_LOADER_CONFIG) | \ + (UINT32_C(1) << TPM2_PCR_SECURE_BOOT_POLICY) | \ + (UINT32_C(1) << TPM2_PCR_KERNEL_BOOT) | \ + (UINT32_C(1) << TPM2_PCR_KERNEL_CONFIG) | \ + (UINT32_C(1) << TPM2_PCR_SYSEXTS) | \ + (UINT32_C(1) << TPM2_PCR_SHIM_POLICY) | \ + (UINT32_C(1) << TPM2_PCR_SYSTEM_IDENTITY)) + +typedef struct EventLogRecordBank EventLogRecordBank; +typedef struct EventLogRecord EventLogRecord; +typedef struct EventLogRegisterBank EventLogRegisterBank; +typedef struct EventLogRegister EventLogRegister; +typedef struct EventLogComponentVariant EventLogComponentVariant; +typedef struct EventLogComponent EventLogComponent; +typedef struct EventLog EventLog; + +struct EventLogRecordBank { + uint16_t algorithm; + TPM2B_DIGEST hash; + LIST_FIELDS(EventLogRecordBank, banks); +}; + +typedef enum EventPayloadValid { + EVENT_PAYLOAD_VALID_YES, + EVENT_PAYLOAD_VALID_NO, + EVENT_PAYLOAD_VALID_DONT_KNOW, + _EVENT_PAYLOAD_VALID_MAX, + _EVENT_PAYLOAD_VALID_INVALID = -EINVAL, +} EventPayloadValid; + +struct EventLogRecord { + EventLog *event_log; + uint32_t pcr; + + const char *source; + char *description; + + /* Data for firmware events (i.e. "TCG PC Client Platform Firmware Profile Specification" events) */ + uint32_t firmware_event_type; + void *firmware_payload; + size_t firmware_payload_size; + + /* Data for userspace events (i.e. those generated by systemd in userspace */ + Tpm2UserspaceEventType userspace_event_type; + JsonVariant *userspace_content; + + /* Validation result for the event payload itself, if the record contains enough information to validate the hash */ + EventPayloadValid event_payload_valid; + + /* If this record matches an variant of one of our defined components */ + EventLogComponentVariant **mapped; + size_t n_mapped; + + /* If this record is part of an EventLogComponentVariant */ + EventLogComponentVariant *owning_component_variant; + + LIST_HEAD(EventLogRecordBank, banks); +}; + +#define EVENT_LOG_RECORD_IS_FIRMWARE(record) ((record)->firmware_event_type != UINT32_MAX) +#define EVENT_LOG_RECORD_IS_USERSPACE(record) ((record)->userspace_event_type >= 0) + +struct EventLogRegisterBank { + TPM2B_DIGEST observed; + TPM2B_DIGEST calculated; +}; + +struct EventLogRegister { + char *color; + unsigned n_measurements; + bool fully_recognized; /* true if all measurements in this register have been recognized to match components */ + EventLogRegisterBank *banks; +}; + +struct EventLogComponentVariant { + EventLogComponent *component; + + char *id; + char *path; + + EventLogRecord **records; + size_t n_records; +}; + +struct EventLogComponent { + char *id; + + EventLogComponentVariant **variants; + size_t n_variants; +}; + +struct EventLog { + EventLogRecord **records; + size_t n_records; + + uint16_t *algorithms; + size_t n_algorithms; + bool algorithms_locked; /* if algorithms where set explicitly by user, and we should not determine them automatically */ + + const EVP_MD **mds; + + /* The hash algorithm which we focus on for matching up components */ + uint16_t primary_algorithm; + + uint8_t startup_locality; + bool startup_locality_found; + + EventLogRegister registers[TPM2_PCRS_MAX]; + + EventLogComponent **components; + size_t n_components; + + /* Number of components which we couldn't find in the event log */ + size_t n_missing_components; + + /* PCRs mask indicating all PCRs touched by unrecognized components */ + uint32_t missing_component_pcrs; +}; + +static EventLogRecordBank *event_log_record_bank_free(EventLogRecordBank *bank) { + return mfree(bank); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(EventLogRecordBank*, event_log_record_bank_free); + +static EventLogRecord *event_log_record_free(EventLogRecord *record) { + EventLogRecordBank *bank; + + if (!record) + return NULL; + + free(record->description); + free(record->firmware_payload); + json_variant_unref(record->userspace_content); + + while ((bank = LIST_POP(banks, record->banks))) + event_log_record_bank_free(bank); + + free(record->mapped); + + return mfree(record); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(EventLogRecord*, event_log_record_free); + +static void event_log_register_done(EventLog *el, EventLogRegister *reg) { + assert(reg); + + free(reg->color); + free(reg->banks); +} + +static EventLogComponentVariant* event_log_component_variant_free(EventLogComponentVariant *variant) { + if (!variant) + return NULL; + + free(variant->id); + free(variant->path); + + FOREACH_ARRAY(record, variant->records, variant->n_records) + event_log_record_free(*record); + + free(variant->records); + + return mfree(variant); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(EventLogComponentVariant*, event_log_component_variant_free); + +static EventLogComponent* event_log_component_free(EventLogComponent *component) { + if (!component) + return NULL; + + FOREACH_ARRAY(variant, component->variants, component->n_variants) + event_log_component_variant_free(*variant); + free(component->variants); + + free(component->id); + + return mfree(component); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(EventLogComponent*, event_log_component_free); + +static EventLog* event_log_free(EventLog *el) { + if (!el) + return NULL; + + FOREACH_ARRAY(p, el->registers, TPM2_PCRS_MAX) + event_log_register_done(el, p); + + FOREACH_ARRAY(rr, el->records, el->n_records) + event_log_record_free(*rr); + free(el->records); + + FOREACH_ARRAY(c, el->components, el->n_components) + event_log_component_free(*c); + free(el->components); + + free(el->algorithms); + free(el->mds); + + return mfree(el); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(EventLog*, event_log_free); + +static EventLogRecord* event_log_record_new(EventLog *el) { + EventLogRecord *record; + + record = new(EventLogRecord, 1); + if (!record) + return NULL; + + *record = (EventLogRecord) { + .event_log = el, + .firmware_event_type = UINT32_MAX, + .userspace_event_type = _TPM2_USERSPACE_EVENT_TYPE_INVALID, + .event_payload_valid = _EVENT_PAYLOAD_VALID_INVALID, + }; + + return record; +} + +static int event_log_add_record( + EventLog *el, + EventLogRecord **ret) { + + _cleanup_(event_log_record_freep) EventLogRecord *record = NULL; + + assert(el); + + if (!GREEDY_REALLOC(el->records, el->n_records+1)) + return -ENOMEM; + + record = event_log_record_new(el); + if (!record) + return -ENOMEM; + + el->records[el->n_records++] = record; + + if (ret) + *ret = record; + + TAKE_PTR(record); + + return 0; +} + +static int event_log_add_algorithm(EventLog *el, uint16_t alg) { + assert(el); + + if (el->algorithms_locked) /* algorithms configured via env var, don't add any further automatically */ + return 0; + + if (typesafe_bsearch(&alg, el->algorithms, el->n_algorithms, cmp_uint16)) + return 0; + + if (!GREEDY_REALLOC(el->algorithms, el->n_algorithms+1)) + return -ENOMEM; + + el->algorithms[el->n_algorithms++] = alg; + + typesafe_qsort(el->algorithms, el->n_algorithms, cmp_uint16); + + return 1; +} + +static int event_log_add_algorithms_from_environment(EventLog *el) { + const char *e; + int r; + + assert(el); + + e = secure_getenv("SYSTEMD_TPM2_HASH_ALGORITHMS"); + if (!e) + return 0; + + for (;;) { + _cleanup_free_ char *word = NULL; + + r = extract_first_word(&e, &word, ":", 0); + if (r < 0) + return r; + if (r == 0) + break; + + r = tpm2_hash_alg_from_string(word); + if (r < 0) + return log_error_errno(r, "Unknown hash algorithm '%s'.", word); + + r = event_log_add_algorithm(el, r); + if (r < 0) + return log_error_errno(r, "Failed to add hash algorithm '%s'.", word); + } + + if (el->n_algorithms > 0) + el->algorithms_locked = true; + + return 0; +} + +static EventLogRecordBank *event_log_record_find_bank( + const EventLogRecord *record, + uint16_t alg) { + + assert(record); + + LIST_FOREACH(banks, i, record->banks) + if (i->algorithm == alg) + return i; + + return NULL; +} + +static int event_log_record_add_bank( + EventLogRecord *record, + uint16_t algorithm, + const void *hash, + size_t hash_size, + EventLogRecordBank **ret) { + + _cleanup_(event_log_record_bank_freep) EventLogRecordBank *bank = NULL; + _cleanup_free_ void *h = NULL; + + assert(record); + assert(hash || hash_size == 0); + + if (event_log_record_find_bank(record, algorithm)) + return -EEXIST; + + if (hash_size > sizeof_field(TPM2B_DIGEST, buffer)) + return -E2BIG; + + h = memdup(hash, hash_size); + if (!h) + return -ENOMEM; + + bank = new(EventLogRecordBank, 1); + if (!bank) + return -ENOMEM; + + *bank = (EventLogRecordBank) { + .algorithm = algorithm, + .hash = TPM2B_DIGEST_MAKE(hash, hash_size), + }; + + LIST_PREPEND(banks, record->banks, bank); + + if (ret) + *ret = bank; + + TAKE_PTR(bank); + + return 0; +} + +static bool event_log_record_is_stub(EventLogRecord *rec) { + assert(rec); + + /* Recognizes the special EV_IPL events systemd-stub generates. Since EV_IPL can be used by almost + * anything, we'll check for the PCR values, to see if it's one of ours. */ + + if (rec->firmware_event_type != EV_IPL) + return false; + + if (!EVENT_LOG_RECORD_IS_FIRMWARE(rec)) + return false; + + if (!IN_SET(rec->pcr, + TPM2_PCR_KERNEL_BOOT, /* 11 */ + TPM2_PCR_KERNEL_CONFIG, /* 12 */ + TPM2_PCR_SYSEXTS)) /* 13 */ + return false; + + return true; +} + +static int event_log_record_parse_variable_data( + EventLogRecord *rec, + sd_id128_t *ret_variable_uuid, + char **ret_variable_name) { + + _cleanup_free_ char16_t *p16 = NULL; + _cleanup_free_ char *p = NULL; + + assert(rec); + assert(ret_variable_uuid); + assert(ret_variable_name); + + if (rec->firmware_payload_size < sizeof(UEFI_VARIABLE_DATA)) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "EFI variable field too short."); + + const UEFI_VARIABLE_DATA *vdata = rec->firmware_payload; + + if (vdata->unicodeNameLength > (SIZE_MAX - offsetof(UEFI_VARIABLE_DATA, unicodeNameLength)) / 2) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Unicode name length too large."); + + size_t m = offsetof(UEFI_VARIABLE_DATA, unicodeName) + vdata->unicodeNameLength * 2; + + if (vdata->variableDataLength > SIZE_MAX - m) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Oversize EFI variable data size."); + + if (rec->firmware_payload_size != m + vdata->variableDataLength) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "EFI variable data has wrong size."); + + p16 = memdup(vdata->unicodeName, vdata->unicodeNameLength * 2); /* Copy out, to align properly */ + if (!p16) + return log_oom_debug(); + + p = utf16_to_utf8(p16, vdata->unicodeNameLength * 2); + if (!p) + return log_oom_debug(); + + if (!string_is_safe(p)) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Unsafe EFI variable string in record."); + + *ret_variable_uuid = efi_guid_to_id128(vdata->variableName); + *ret_variable_name = TAKE_PTR(p); + + return 0; +} + +static int event_log_record_extract_firmware_description(EventLogRecord *rec) { + _cleanup_free_ char *fallback = NULL; + int r; + + assert(rec); + + if (!EVENT_LOG_RECORD_IS_FIRMWARE(rec)) + return 0; + + if (arg_raw_description) + goto catchall; + + switch (rec->firmware_event_type) { + + case EV_EFI_VARIABLE_DRIVER_CONFIG: + case EV_EFI_VARIABLE_BOOT: + case EV_EFI_VARIABLE_BOOT2: + case EV_EFI_VARIABLE_AUTHORITY: { + _cleanup_free_ char *p = NULL; + sd_id128_t uuid; + + r = event_log_record_parse_variable_data(rec, &uuid, &p); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_warning_errno(r, "EFI variable data invalid, ignoring."); + goto invalid; + } + + if (asprintf(&rec->description, "%s: %s-" SD_ID128_UUID_FORMAT_STR, + rec->firmware_event_type == EV_EFI_VARIABLE_AUTHORITY ? "Authority" : "Variable", + p, + SD_ID128_FORMAT_VAL(uuid)) < 0) + return log_oom(); + + return 1; + } + + case EV_SEPARATOR: { + if (rec->firmware_payload_size != sizeof(uint32_t)) { + log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "EFI separator field has wrong size, ignoring."); + goto invalid; + } + + uint32_t val = unaligned_read_ne32(rec->firmware_payload); + + switch (val) { + + case 0: + case UINT32_C(0xffffffff): + (void) asprintf(&rec->description, "Separator: Success (0x%02" PRIx32 ")", val); + break; + + case 1: + rec->description = strdup("Separator: Error (0x01)"); + break; + + default: + log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "Unexpected separator payload %" PRIu32 ".", val); + goto invalid; + } + + if (!rec->description) + return log_oom(); + + return 1; + } + + case EV_EFI_ACTION: { + _cleanup_free_ char *d = NULL; + + r = make_cstring(rec->firmware_payload, rec->firmware_payload_size, MAKE_CSTRING_ALLOW_TRAILING_NUL, &d); + if (r < 0) + return log_error_errno(r, "Failed to make C string from EFI action string: %m"); + + if (!string_is_safe(d)) { + log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "Unsafe EFI action string in record, ignoring."); + goto invalid; + } + + rec->description = strjoin("Action: ", d); + if (!rec->description) + return log_oom(); + return 1; + } + + case EV_EFI_GPT_EVENT: { + if (rec->firmware_payload_size < sizeof(GptHeader)) { + log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "GPT measurement too short, ignoring."); + goto invalid; + } + + const GptHeader *h = rec->firmware_payload; + + if (!gpt_header_has_signature(h)) { + log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "GPT measurement does not cover a GPT partition table header, ignoring."); + goto invalid; + } + + if (asprintf(&rec->description, "GPT: disk " SD_ID128_UUID_FORMAT_STR, SD_ID128_FORMAT_VAL(efi_guid_to_id128(h->disk_guid))) < 0) + return log_oom(); + + return 1; + } + + case EV_IPL: { + _cleanup_free_ char *d = NULL; + + /* EV_IPL can be anything, only try to parse the description on PCRs we "own" */ + if (!event_log_record_is_stub(rec)) + break; + + /* sd-stub always sets a description string as text for these */ + + d = utf16_to_utf8(rec->firmware_payload, rec->firmware_payload_size); + if (!d) + return log_oom(); + + if (string_has_cc(d, NULL)) { + log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "Unsafe EFI action string in record, ignoring."); + goto invalid; + } + + rec->description = strjoin("String: ", d); + if (!rec->description) + return log_oom(); + + return 1; + } + + case EV_EVENT_TAG: { + TCG_PCClientTaggedEvent *tag = rec->firmware_payload; + size_t left = rec->firmware_payload_size; + + if (left == 0) { + log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "Empty tagged PC client event, ignoring."); + goto invalid; + } + + for (;;) { + uint64_t m; + + if (left < offsetof(TCG_PCClientTaggedEvent, taggedEventData)) { + log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "Tagged PC client event too short, ignoring."); + goto invalid; + } + + m = offsetof(TCG_PCClientTaggedEvent, taggedEventData) + (uint64_t) tag->taggedEventDataSize; + if (left < m) { + log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "Tagged PC client event data too short, ignoring."); + goto invalid; + } + + switch (tag->taggedEventID) { + + /* Linux kernel's own measurements: */ + case INITRD_EVENT_TAG_ID: + /* The tagged event payload is just a constant string, hence don't show it */ + if (!strextend_with_separator(&rec->description, ", ", "Linux: initrd")) + return log_oom(); + break; + + case LOAD_OPTIONS_EVENT_TAG_ID: + /* As above. */ + if (!strextend_with_separator(&rec->description, ", ", "Linux: kernel command line")) + return log_oom(); + break; + + /* systemd's measurements: */ + case LOADER_CONF_EVENT_TAG_ID: + /* As above. */ + if (!strextend_with_separator(&rec->description, ", ", "systemd-boot: loader.conf")) + return log_oom(); + break; + + case DEVICETREE_ADDON_EVENT_TAG_ID: { + _cleanup_free_ char *raw = NULL, *s = NULL; + + raw = utf16_to_utf8((const char16_t*) tag->taggedEventData, tag->taggedEventDataSize); + if (!raw) + return log_oom(); + + s = cescape(raw); + if (!s) + return log_oom(); + + r = strextendf_with_separator(&rec->description, ", ", "systemd-stub: devicetree addon %s", s); + if (r < 0) + return log_error_errno(r, "Failed to format EV_EVENT_TAG description string: %m"); + break; + } + + default: { + _cleanup_free_ char *s = NULL; + + s = cescape_length((char*) tag->taggedEventData, tag->taggedEventDataSize); + if (!s) + return log_oom(); + + r = strextendf_with_separator(&rec->description, ", ", "Tag 0x%" PRIx32 ": %s", tag->taggedEventID, s); + if (r < 0) + return log_error_errno(r, "Failed to format EV_EVENT_TAG description string: %m"); + + break; + }} + + tag = (TCG_PCClientTaggedEvent*) ((uint8_t*) tag + m); + left -= m; + + if (left == 0) + break; + } + + return 1; + } + + case EV_EFI_PLATFORM_FIRMWARE_BLOB: { + const UEFI_PLATFORM_FIRMWARE_BLOB *blob; + if (rec->firmware_payload_size != sizeof(UEFI_PLATFORM_FIRMWARE_BLOB)) { + log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "EV_EFI_PLATFORM_FIRMWARE_BLOB of wrong size, ignoring."); + goto invalid; + } + + blob = rec->firmware_payload; + if (asprintf(&rec->description, "Blob: %s @ 0x%" PRIx64, FORMAT_BYTES(blob->blobLength), blob->blobBase) < 0) + return log_oom(); + + return 1; + } + + case EV_EFI_BOOT_SERVICES_APPLICATION: + case EV_EFI_BOOT_SERVICES_DRIVER: + case EV_EFI_RUNTIME_SERVICES_DRIVER: { + const UEFI_IMAGE_LOAD_EVENT *load; + _cleanup_free_ char *fn = NULL; + bool end = false; + + if (rec->firmware_payload_size < offsetof(UEFI_IMAGE_LOAD_EVENT, devicePath)) { + log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "Device path too short, ignoring."); + goto invalid; + } + + load = rec->firmware_payload; + if (load->lengthOfDevicePath != + rec->firmware_payload_size - offsetof(UEFI_IMAGE_LOAD_EVENT, devicePath)) { + log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "Device path size does not match, ignoring."); + goto invalid; + } + + const packed_EFI_DEVICE_PATH *dp = (const packed_EFI_DEVICE_PATH*) load->devicePath; + size_t left = load->lengthOfDevicePath; + + for (;;) { + if (left == 0) { + if (!end) { + log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "Garbage after device path end, ignoring."); + goto invalid; + } + + break; + } + + if (end) { + log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "Garbage after device path end, ignoring."); + goto invalid; + } + + if (left < offsetof(packed_EFI_DEVICE_PATH, path) || left < dp->length) { + log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "Device path element too short, ignoring."); + goto invalid; + } + + if (dp->type == 4 && dp->subType == 4) { + /* Filename, store the last node of this type as description, it should contain the file name */ + + free(fn); + fn = utf16_to_utf8((void*) dp->path, dp->length - offsetof(packed_EFI_DEVICE_PATH, path)); + if (!fn) + return log_oom(); + + } else if (dp->type == 0x7F && dp->subType == 0xFF) + /* End of Hardware Device Path */ + end = true; + else + log_debug("Ignoring device path element type=0x%02x subtype=0x%02x", dp->type, dp->subType); + + left -= dp->length; + dp = (packed_EFI_DEVICE_PATH*) ((uint8_t*) dp + dp->length); + } + + if (fn) { + rec->description = strjoin("File: ", fn); + if (!rec->description) + return log_oom(); + + return 1; + } + + break; + }} + +catchall: + /* Catchall: show binary data */ + fallback = cescape_length(rec->firmware_payload, rec->firmware_payload_size); + if (!fallback) + return log_oom(); + + rec->description = strjoin("Raw: ", fallback); + if (!rec->description) + return log_oom(); + return 1; + + +invalid: + /* Mark the payload as invalid, so that we do not bother parsing/validating it any further */ + rec->event_payload_valid = EVENT_PAYLOAD_VALID_NO; + return 0; +} + +static int event_log_add_algorithms_from_record(EventLog *el, EventLogRecord *record) { + int r; + + assert(el); + assert(record); + + if (el->algorithms_locked) + return 0; + + LIST_FOREACH(banks, i, record->banks) { + r = event_log_add_algorithm(el, i->algorithm); + if (r < 0) + return r; + } + + return 0; +} + +static int event_log_load_firmware(EventLog *el) { + const TCG_EfiSpecIdEventAlgorithmSize *algorithms; + size_t bufsize = 0, n_algorithms = 0, left = 0; + _cleanup_free_ void *buf = NULL; + const TCG_PCR_EVENT2 *event; + const char *path; + int r; + + assert(el); + + path = tpm2_firmware_log_path(); + + r = read_full_file(path, (char**) &buf, &bufsize); + if (r < 0) + return log_error_errno(r, "Failed to open TPM2 event log '%s': %m", path); + + if (bufsize == 0) { + /* Sometimes it's useful to invoke things with SYSTEMD_MEASURE_LOG_FIRMWARE=/dev/null, let's allow that, and proceed */ + log_warning("Empty firmware event log file, not loading."); + return 0; + } + + r = validate_firmware_header(buf, bufsize, &algorithms, &n_algorithms, &event, &left); + if (r < 0) + return r; + + for (const TCG_PCR_EVENT2 *next_event = NULL;; event = next_event) { + EventLogRecord *record = NULL; + const void *payload; + size_t payload_size; + + r = validate_firmware_event( + event, + left, + algorithms, + n_algorithms, + &next_event, + &left, + &payload, + &payload_size); + if (r < 0) + return r; + if (r == 0) + break; + + if (event->eventType == EV_NO_ACTION && + event->pcrIndex == 0 && + payload_size == 17 && + memcmp(payload, "StartupLocality", sizeof("StartupLocality")) == 0) { + if (el->startup_locality_found) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "StartupLocality event found twice!"); + + el->startup_locality = ((const uint8_t*) payload)[sizeof("StartupLocality")]; + el->startup_locality_found = true; + log_debug("Found StartupLocality event: %u", el->startup_locality); + continue; + } + + if (event->eventType == EV_NO_ACTION) { /* Ignore pseudo events, that don't result in a measurement */ + log_debug("Skipping NO_ACTION event."); + continue; + } + + r = event_log_add_record(el, &record); + if (r < 0) + return log_error_errno(r, "Failed to add record to event log: %m"); + + record->pcr = event->pcrIndex; + record->source = path; + record->firmware_event_type = event->eventType; + record->firmware_payload = memdup(payload, payload_size); + if (!record->firmware_payload) + return log_oom(); + record->firmware_payload_size = payload_size; + + const void *ha, *ha_next = NULL; + ha = (const uint8_t*) event + offsetof(TCG_PCR_EVENT2, digests.digests); + assert(event->digests.count == n_algorithms); + + for (size_t i = 0; i < n_algorithms; i++, ha = ha_next) { + ha_next = (const uint8_t*) ha + offsetof(TPMT_HA, digest) + algorithms[i].digestSize; + + /* The TPMT_HA is not aligned in the record, hence read the hashAlg field via an unaligned read */ + assert_cc(__builtin_types_compatible_p(uint16_t, typeof(TPMI_ALG_HASH))); + uint16_t hash_alg = unaligned_read_ne16((const uint8_t*) ha + offsetof(TPMT_HA, hashAlg)); + + if (hash_alg != algorithms[i].algorithmId) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Hash algorithms in event log record don't match log."); + + if (!tpm2_hash_alg_to_string(algorithms[i].algorithmId)) + continue; + + r = event_log_record_add_bank( + record, + algorithms[i].algorithmId, + (const uint8_t*) ha + offsetof(TPMT_HA, digest), + algorithms[i].digestSize, + /* ret= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to add bank to event log record: %m"); + } + + /* Try to extract a descriptive text */ + r = event_log_record_extract_firmware_description(record); + if (r < 0) + return r; + + r = event_log_add_algorithms_from_record(el, record); + if (r < 0) + return r; + } + + return 0; +} + +static int event_log_record_parse_json(EventLogRecord *record, JsonVariant *j) { + const char *rectype = NULL; + JsonVariant *x, *k; + uint64_t u; + int r; + + assert(record); + assert(j); + + if (!json_variant_is_object(j)) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "record object is not an object."); + + x = json_variant_by_key(j, "pcr"); + if (!x) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "'pcr' field missing from TPM measurement log file entry."); + if (!json_variant_is_unsigned(x)) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "'pcr' field is not an integer."); + + u = json_variant_unsigned(x); + if (u >= TPM2_PCRS_MAX) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "'pcr' field is out of range."); + record->pcr = json_variant_unsigned(x); + + x = json_variant_by_key(j, "digests"); + if (!x) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "'digests' field missing from TPM measurement log file entry."); + if (!json_variant_is_array(x)) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "'digests' field is not an array."); + + JSON_VARIANT_ARRAY_FOREACH(k, x) { + _cleanup_free_ void *hash = NULL; + size_t hash_size; + JsonVariant *a, *h; + int na; + + a = json_variant_by_key(k, "hashAlg"); + if (!a) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "'digests' field element lacks 'hashAlg' field."); + if (!json_variant_is_string(a)) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "'hashAlg' field is not a string."); + + na = tpm2_hash_alg_from_string(json_variant_string(a)); + if (na < 0) { + log_debug_errno(na, "Unsupported hash '%s' in userspace event log, ignoring: %m", json_variant_string(a)); + continue; + } + + h = json_variant_by_key(k, "digest"); + if (!h) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "'digests' field lacks 'digest' field"); + + r = json_variant_unhex(h, &hash, &hash_size); + if (r < 0) + return log_error_errno(r, "Failed to decode digest: %m"); + + r = event_log_record_add_bank( + record, + na, + hash, + hash_size, + /* ret= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to add bank to event log record: %m"); + } + + x = json_variant_by_key(j, "content_type"); + if (!x) + log_debug("'content_type' missing from TPM measurement log file entry, ignoring."); + else { + if (!json_variant_is_string(x)) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "'content_type' field is not a string."); + + rectype = json_variant_string(x); + } + + if (streq_ptr(rectype, "systemd")) { + JsonVariant *y; + + x = json_variant_by_key(j, "content"); + if (!x) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "'content' field missing from TPM measurement log file entry."); + if (!json_variant_is_object(x)) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "'content' sub-object is not an object."); + + y = json_variant_by_key(x, "string"); + if (y) { + if (!json_variant_is_string(y)) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "'string' field is not a string."); + + r = free_and_strdup_warn(&record->description, json_variant_string(y)); + if (r < 0) + return r; + } + + y = json_variant_by_key(x, "eventType"); + if (y) { + if (!json_variant_is_string(y)) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "'eventType' field is not a string."); + + record->userspace_event_type = tpm2_userspace_event_type_from_string(json_variant_string(y)); + if (record->userspace_event_type < 0) + log_debug_errno(record->userspace_event_type, "Unknown userspace event type '%s', ignoring.", json_variant_string(y)); + } + + json_variant_unref(record->userspace_content); + record->userspace_content = json_variant_ref(x); + } + + return 0; +} + +static int event_log_load_userspace(EventLog *el) { + _cleanup_fclose_ FILE *f = NULL; + _cleanup_free_ char *b = NULL; + bool beginning = true; + const char *path; + size_t bn = 0; + int r; + + assert(el); + + path = tpm2_userspace_log_path(); + + f = fopen(path, "re"); + if (!f) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to open userspace TPM measurement log file: %m"); + + return 0; + } + + if (flock(fileno(f), LOCK_SH) < 0) + return log_error_errno(errno, "Failed to lock userspace TPM measurement log file: %m"); + + for (;;) { + _cleanup_(json_variant_unrefp) JsonVariant *j = NULL; + EventLogRecord *record; + int ch; + + ch = fgetc(f); + if (ch == EOF) { + if (ferror(f)) + return log_error_errno(errno, "Failed to read local TPM measurement log file: %m"); + + if (beginning) + break; + } else if (ch != 0x1EU) { + if (!GREEDY_REALLOC(b, bn + 2)) + return log_oom(); + + b[bn++] = (char) ch; + continue; + } + + if (beginning) { + beginning = false; + continue; + } + + b[bn] = 0; + r = json_parse(b, 0, &j, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to parse local TPM measurement log file: %m"); + + r = event_log_add_record(el, &record); + if (r < 0) + return log_error_errno(r, "Failed to add record to event log: %m"); + + record->source = path; + + r = event_log_record_parse_json(record, j); + if (r < 0) + return r; + + r = event_log_add_algorithms_from_record(el, record); + if (r < 0) + return r; + + if (ch == EOF) + break; + + b = mfree(b); + bn = 0; + } + + return 0; +} + +static EventLog *event_log_new(void) { + _cleanup_(event_log_freep) EventLog *el = NULL; + + el = new(EventLog, 1); + if (!el) + return NULL; + + *el = (EventLog) { + .primary_algorithm = UINT16_MAX, + }; + + return TAKE_PTR(el); +} + +static int event_log_load(EventLog *el) { + int r; + + assert(el); + + r = event_log_load_firmware(el); + if (r < 0) + return r; + + r = event_log_load_userspace(el); + if (r < 0) + return r; + + return 0; +} + +static int event_log_read_pcrs(EventLog *el) { + _cleanup_(tpm2_context_unrefp) Tpm2Context *tc = NULL; + int r; + + assert(el); + + r = tpm2_context_new(NULL, &tc); + if (r < 0) + return r; + + FOREACH_ARRAY(rr, el->registers, TPM2_PCRS_MAX) { + if (rr->banks) + continue; + + rr->banks = new0(EventLogRegisterBank, el->n_algorithms); + if (!rr->banks) + return log_oom(); + } + + for (size_t a = 0; a < el->n_algorithms; a++) { + _cleanup_free_ Tpm2PCRValue *pcr_values = NULL; + size_t n_pcr_values; + TPML_PCR_SELECTION selection; + + tpm2_tpml_pcr_selection_from_mask(TPM2_PCRS_MASK, el->algorithms[a], &selection); + r = tpm2_pcr_read(tc, &selection, &pcr_values, &n_pcr_values); + if (r < 0) + return r; + + FOREACH_ARRAY(v, pcr_values, n_pcr_values) { + assert(v->hash == el->algorithms[a]); + el->registers[v->index].banks[a].observed = v->value; + } + } + + return 0; +} + +static void event_log_initial_pcr_state(EventLog *el, uint32_t pcr, size_t size, TPM2B_DIGEST *ret) { + assert(el); + assert(pcr < TPM2_PCRS_MAX); + assert(size > 0); + assert(size <= sizeof_field(TPM2B_DIGEST, buffer)); + assert(ret); + + ret->size = size; + + switch (pcr) { + + case 0: + memzero(ret->buffer, ret->size-1); + ((uint8_t*) ret->buffer)[ret->size-1] = el->startup_locality_found ? el->startup_locality : 0; + break; + + case 1 ... 16: + case 23: + memzero(ret->buffer, ret->size); + break; + + case 17 ... 22: + memset(ret->buffer, 0xffu, ret->size); + break; + + default: + assert_not_reached(); + } +} + +static int event_log_calculate_pcrs(EventLog *el) { + assert(el); + + /* Iterates through the event log an calculates the expected hash values based on all listed records */ + + assert(!el->mds); + el->mds = new(const EVP_MD*, el->n_algorithms); + if (!el->mds) + return log_oom(); + + for (size_t i = 0; i < el->n_algorithms; i++) { + const EVP_MD *md; + const char *a; + + assert_se(a = tpm2_hash_alg_to_string(el->algorithms[i])); + assert_se(md = EVP_get_digestbyname(a)); + + el->mds[i] = md; + } + + for (uint32_t pcr = 0; pcr < TPM2_PCRS_MAX; pcr++) + for (size_t i = 0; i < el->n_algorithms; i++) { + EventLogRegisterBank *b = el->registers[pcr].banks + i; + event_log_initial_pcr_state(el, pcr, EVP_MD_size(el->mds[i]), &b->calculated); + } + + FOREACH_ARRAY(rr, el->records, el->n_records) { + EventLogRegister *reg = el->registers + (*rr)->pcr; + + for (size_t i = 0; i < el->n_algorithms; i++) { + const char *n = tpm2_hash_alg_to_string(el->algorithms[i]); + _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *mc = NULL; + EventLogRegisterBank *reg_b; + EventLogRecordBank *rec_b; + unsigned sz; + + rec_b = event_log_record_find_bank(*rr, el->algorithms[i]); + if (!rec_b) { + log_warning_errno(SYNTHETIC_ERRNO(ENXIO), "Record with missing bank '%s', ignoring.", n); + continue; + } + + reg_b = reg->banks + i; + + mc = EVP_MD_CTX_new(); + if (!mc) + return log_oom(); + + if (EVP_DigestInit_ex(mc, el->mds[i], NULL) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize %s message digest context.", n); + + if (EVP_DigestUpdate(mc, reg_b->calculated.buffer, reg_b->calculated.size) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to run digest."); + + if (EVP_DigestUpdate(mc, rec_b->hash.buffer, rec_b->hash.size) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to run digest."); + + if (EVP_DigestFinal_ex(mc, reg_b->calculated.buffer, &sz) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to finalize hash context."); + + assert(sz == reg_b->calculated.size); + } + + reg->n_measurements++; + } + + return 0; +} + +static int event_log_record_validate_hash_firmware( + EventLogRecord *record, + EventLogRecordBank *bank, + const EVP_MD *md) { + + _cleanup_free_ void *hdata_alternative = NULL; + size_t hsz, hsz_alternative = 0; + bool strict = false; + const void *hdata; + + assert(record); + assert(bank); + assert(md); + + if (!EVENT_LOG_RECORD_IS_FIRMWARE(record)) + return 0; + + switch (record->firmware_event_type) { + + case EV_EFI_ACTION: + case EV_EFI_GPT_EVENT: + case EV_EFI_VARIABLE_BOOT2: + case EV_EFI_VARIABLE_DRIVER_CONFIG: + case EV_EFI_VARIABLE_AUTHORITY: + case EV_SEPARATOR: + case EV_S_CRTM_VERSION: + /* Here the extended hash value is the hash value of the event payload. Note that + * EV_PLATFORM_CONFIG_FLAGS (according to the TCG PC Client Platform Firmware Profile + * Specification) is also supposed to be like this. But ovmf doesn't follow this requirement, + * hence be lenient on that one, and don't include it here. */ + hdata = record->firmware_payload; + hsz = record->firmware_payload_size; + strict = true; + break; + + case EV_EFI_VARIABLE_BOOT: { + const UEFI_VARIABLE_DATA *vdata = record->firmware_payload; + size_t skip; + + /* Here the extended hash value is the hash value of the variable data (i.e. excluding the + * name). + * + * Note: we already checked the general validity of the UEFI_VARIABLE_DATA structure, hence + * no need to do so again. */ + + assert(record->firmware_payload_size >= offsetof(UEFI_VARIABLE_DATA, unicodeName)); + skip = offsetof(UEFI_VARIABLE_DATA, unicodeName) + vdata->unicodeNameLength * 2; + + assert(record->firmware_payload_size >= skip); + hdata = (const uint8_t*) record->firmware_payload + skip; + hsz = record->firmware_payload_size - skip; + strict = true; + break; + } + + case EV_IPL: + if (event_log_record_is_stub(record)) { + /* The PE section names have a descriptive string in UTF-16 in the payload, but the + * hash is over the UTF-8 version (with suffixing 0), hence let's convert the payload + * into that format here, and see if it checks out. */ + hdata_alternative = utf16_to_utf8(record->firmware_payload, record->firmware_payload_size); + if (!hdata_alternative) + return log_oom(); + + hsz_alternative = strlen(hdata_alternative) + 1; /* with NUL byte */ + } + + _fallthrough_; + + default: + /* For the others check the data too, just in case. But usually this will not match, hence + * only report if the checksum matches, but don't complain if it does not. */ + hdata = record->firmware_payload; + hsz = record->firmware_payload_size; + strict = false; + break; + } + + int mdsz = EVP_MD_size(md); + assert(mdsz > 0); + assert((size_t) mdsz <= sizeof_field(TPM2B_DIGEST, buffer)); + + TPM2B_DIGEST payload_hash = { + .size = mdsz, + }; + + unsigned dsz = mdsz; + + if (EVP_Digest(hdata, hsz, payload_hash.buffer, &dsz, md, NULL) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to calculate event payload hash."); + assert(dsz == (unsigned) mdsz); + + /* If this didn't match then let's try the alternative format here, if we have one, and check things then. */ + if (memcmp_nn(bank->hash.buffer, bank->hash.size, payload_hash.buffer, payload_hash.size) != 0 && hdata_alternative) { + if (EVP_Digest(hdata_alternative, hsz_alternative, payload_hash.buffer, &dsz, md, NULL) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to calculate event payload hash."); + assert(dsz == (unsigned) mdsz); + } + + if (memcmp_nn(bank->hash.buffer, bank->hash.size, payload_hash.buffer, payload_hash.size) != 0) { + if (strict) + record->event_payload_valid = EVENT_PAYLOAD_VALID_NO; + else if (record->event_payload_valid != EVENT_PAYLOAD_VALID_NO) + record->event_payload_valid = EVENT_PAYLOAD_VALID_DONT_KNOW; + } else if (record->event_payload_valid < 0) + record->event_payload_valid = EVENT_PAYLOAD_VALID_YES; + + return 1; +} + +static int event_log_record_validate_hash_userspace( + EventLogRecord *record, + EventLogRecordBank *bank, + const EVP_MD *md) { + + _cleanup_free_ unsigned char *payload_hash = NULL; + unsigned payload_hash_size; + JsonVariant *js; + const char *s; + int mdsz; + + assert(record); + assert(bank); + assert(md); + + if (!EVENT_LOG_RECORD_IS_USERSPACE(record)) + return 0; + + if (!record->userspace_content) + return 0; + + js = json_variant_by_key(record->userspace_content, "string"); + if (!js) + return 0; + + assert(json_variant_is_string(js)); + s = json_variant_string(js); + + mdsz = EVP_MD_size(md); + assert(mdsz > 0); + + payload_hash_size = mdsz; + payload_hash = malloc(payload_hash_size); + if (!payload_hash) + return log_oom(); + + if (EVP_Digest(s, strlen(s), payload_hash, &payload_hash_size, md, NULL) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to calculate event payload hash."); + + assert((int) payload_hash_size == mdsz); + if (memcmp_nn(bank->hash.buffer, bank->hash.size, payload_hash, payload_hash_size) != 0) + record->event_payload_valid = EVENT_PAYLOAD_VALID_NO; + else if (record->event_payload_valid < 0) + record->event_payload_valid = EVENT_PAYLOAD_VALID_YES; + + return 0; +} + +static int event_log_validate_record_hashes(EventLog *el) { + int r; + + assert(el); + + /* For records which contain the full data to validate the hashes, do so. */ + + FOREACH_ARRAY(rr, el->records, el->n_records) { + + LIST_FOREACH(banks, bank, (*rr)->banks) { + const EVP_MD *md; + const char *a; + + assert_se(a = tpm2_hash_alg_to_string(bank->algorithm)); + assert_se(md = EVP_get_digestbyname(a)); + + r = event_log_record_validate_hash_firmware(*rr, bank, md); + if (r < 0) + return r; + + r = event_log_record_validate_hash_userspace(*rr, bank, md); + if (r < 0) + return r; + } + } + + return 0; +} + +static int event_log_component_cmp(EventLogComponent *const*a, EventLogComponent *const*b) { + const EventLogComponent *x = ASSERT_PTR(*ASSERT_PTR(a)), *y = ASSERT_PTR(*ASSERT_PTR(b)); + + return strcmp(x->id, y->id); +} + +static EventLogComponent *event_log_find_component(EventLog *el, const char *id) { + EventLogComponent k = { + .id = (char*) id, + }; + EventLogComponent *kk = &k, **found; + + assert(el); + assert(id); + + found = typesafe_bsearch( + &kk, + el->components, + el->n_components, + event_log_component_cmp); + if (!found) + return NULL; + + return *found; +} + +static int event_log_add_component(EventLog *el, const char *id, EventLogComponent **ret) { + _cleanup_(event_log_component_freep) EventLogComponent *component = NULL; + _cleanup_free_ char *id_copy = NULL; + EventLogComponent *found; + + assert(el); + assert(ret); + + found = event_log_find_component(el, id); + if (found) { + *ret = found; + return 0; + } + + if (!GREEDY_REALLOC(el->components, el->n_components+1)) + return log_oom(); + + id_copy = strdup(id); + if (!id_copy) + return log_oom(); + + component = new(EventLogComponent, 1); + if (!component) + return log_oom(); + + *component = (EventLogComponent) { + .id = TAKE_PTR(id_copy), + }; + + if (ret) + *ret = component; + + el->components[el->n_components++] = TAKE_PTR(component); + return 1; +} + +static int event_log_record_equal(const EventLogRecord *a, const EventLogRecord *b) { + EventLogRecordBank *x, *y; + + assert(a); + assert(a->event_log); + assert(b); + assert(b->event_log); + assert(a->event_log == b->event_log); + + if (a->pcr != b->pcr) + return false; + + x = event_log_record_find_bank(a, a->event_log->primary_algorithm); + y = event_log_record_find_bank(b, b->event_log->primary_algorithm); + if (!x || !y) + return false; + + assert(x->algorithm == a->event_log->primary_algorithm); + assert(y->algorithm == b->event_log->primary_algorithm); + + return memcmp_nn(x->hash.buffer, x->hash.size, y->hash.buffer, y->hash.size) == 0; +} + +static int event_log_add_component_file(EventLog *el, EventLogComponent *component, const char *path) { + _cleanup_(event_log_component_variant_freep) EventLogComponentVariant *variant = NULL; + _cleanup_free_ char *fname = NULL, *id = NULL, *path_copy = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *j = NULL; + JsonVariant *records; + const char *e; + int r; + + assert(el); + + r = path_extract_filename(path, &fname); + if (r < 0) + return log_error_errno(r, "Failed to extract basename from path %s: %m", path); + + e = endswith(fname, ".pcrlock"); + if (!e) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Bad suffix: %s", fname); + + id = strndup(fname, e - fname); + if (!id) + return log_oom(); + + if (!component) { + r = event_log_add_component(el, id, &component); + if (r < 0) + return r; + } + + if (!GREEDY_REALLOC(component->variants, component->n_variants+1)) + return log_oom(); + + r = json_parse_file( + /* f= */ NULL, + path, + /* flags= */ 0, + &j, + /* ret_line= */ NULL, + /* ret_column= */ NULL); + if (r < 0) { + log_warning_errno(r, "Failed to parse component file %s, ignoring: %m", path); + return 0; + } + + if (!json_variant_is_object(j)) { + log_warning_errno(r, "Component file %s does not contain JSON object, ignoring.", path); + return 0; + } + + path_copy = strdup(path); + if (!path_copy) + return log_oom(); + + variant = new(EventLogComponentVariant, 1); + if (!variant) + return log_oom(); + + *variant = (EventLogComponentVariant) { + .component = component, + .path = TAKE_PTR(path_copy), + .id = TAKE_PTR(id), + }; + + records = json_variant_by_key(j, "records"); + if (records) { + JsonVariant *rj; + + if (!json_variant_is_array(records)) { + log_warning_errno(r, "Component records field of file %s is not an array, ignoring.", path); + return 0; + } + + JSON_VARIANT_ARRAY_FOREACH(rj, records) { + _cleanup_(event_log_record_freep) EventLogRecord *record = NULL; + + if (!GREEDY_REALLOC(variant->records, variant->n_records+1)) + return log_oom(); + + record = event_log_record_new(el); + if (!record) + return log_oom(); + + r = event_log_record_parse_json(record, rj); + if (r < 0) + return r; + + record->owning_component_variant = variant; + variant->records[variant->n_records++] = TAKE_PTR(record); + } + } + + component->variants[component->n_variants++] = TAKE_PTR(variant); + return 1; +} + +static int event_log_add_component_dir(EventLog *el, const char *path, char **base_search) { + _cleanup_free_ char *fname = NULL, *id = NULL; + _cleanup_strv_free_ char **files = NULL; + EventLogComponent *component; + const char *e; + int r; + + assert(el); + + r = path_extract_filename(path, &fname); + if (r < 0) + return log_error_errno(r, "Failed to extract basename from path %s: %m", path); + + e = endswith(fname, ".pcrlock.d"); + if (!e) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Bad suffix: %s", fname); + + id = strndup(fname, e - fname); + if (!id) + return log_oom(); + + r = event_log_add_component(el, id, &component); + if (r < 0) + return r; + + _cleanup_strv_free_ char **search = NULL; + + STRV_FOREACH(b, base_search) { + _cleanup_free_ char *q = NULL; + + q = path_join(*b, fname); + if (!q) + return log_oom(); + + r = strv_consume(&search, TAKE_PTR(q)); + if (r < 0) + return log_oom(); + } + + r = conf_files_list_strv(&files, ".pcrlock", /* root= */ NULL, CONF_FILES_REGULAR, (const char*const*) search); + if (r < 0) + return log_error_errno(r, "Failed to enumerate .pcrlock files for component '%s': %m", id); + + STRV_FOREACH(f, files) { + r = event_log_add_component_file(el, component, *f); + if (r < 0) + return r; + } + + return 0; +} + +static int event_log_load_components(EventLog *el) { + _cleanup_strv_free_ char **files = NULL; + char **dirs; + int r; + + assert(el); + + dirs = arg_components ?: + STRV_MAKE("/etc/pcrlock.d", + "/run/pcrlock.d", + "/var/lib/pcrlock.d", + "/usr/local/lib/pcrlock.d", + "/usr/lib/pcrlock.d"); + + r = conf_files_list_strv(&files, NULL, NULL, CONF_FILES_REGULAR|CONF_FILES_DIRECTORY|CONF_FILES_FILTER_MASKED, (const char*const*) dirs); + if (r < 0) + return log_error_errno(r, "Failed to enumerate .pcrlock files: %m"); + + STRV_FOREACH(f, files) { + if (endswith(*f, ".pcrlock.d")) + r = event_log_add_component_dir(el, *f, dirs); + else if (endswith(*f, ".pcrlock")) + r = event_log_add_component_file(el, NULL, *f); + else + continue; + if (r < 0) + return r; + } + + return 0; +} + +static int event_log_validate_fully_recognized(EventLog *el) { + + for (uint32_t pcr = 0; pcr < ELEMENTSOF(el->registers); pcr++) { + bool fully_recognized = true; + + FOREACH_ARRAY(rr, el->records, el->n_records) { + EventLogRecord *rec = *rr; + + if (rec->pcr != pcr) + continue; + + if (rec->n_mapped == 0) { + log_notice("Event log record %zu (PCR %" PRIu32 ", \"%s\") not matching any component.", + (size_t) (rr - el->records), rec->pcr, strna(rec->description)); + fully_recognized = false; + break; + } + } + + el->registers[pcr].fully_recognized = fully_recognized; + } + + return 0; +} + +static int event_log_match_component_variant( + EventLog *el, + size_t i, + EventLogComponentVariant *variant, + size_t j, + bool assign) { + + int r; + + assert(el); + assert(variant); + + /* It's OK to point immediately after the last record, but not further */ + assert(i <= el->n_records); + assert(j <= variant->n_records); + + /* All entries in the variant checked out? Yippieh! */ + if (j == variant->n_records) + return true; + + /* If the remainder of the variant is longer than the remainder of the event log, it cannot possibly fit. */ + if (el->n_records - i < variant->n_records - j) + return false; + + /* Does this record match? If not, let's try at the next place in the logs. */ + if (!event_log_record_equal(el->records[i], variant->records[j])) + return event_log_match_component_variant(el, i + 1, variant, j, assign); /* Recursion! */ + + /* This one matches. Good. Let's see if the rest also matches. (Recursion!) */ + r = event_log_match_component_variant(el, i + 1, variant, j + 1, assign); + if (r <= 0) + return r; + + if (assign) { + /* Take ownership (Note we allow multiple components and variants to take owneship of the same record!) */ + if (!GREEDY_REALLOC(el->records[i]->mapped, el->records[i]->n_mapped+1)) + return log_oom(); + + el->records[i]->mapped[el->records[i]->n_mapped++] = variant; + } + + return true; +} + +static uint32_t event_log_component_variant_pcrs(EventLogComponentVariant *i) { + uint32_t mask = 0; + + assert(i); + + /* returns mask of PCRs touched by this variant */ + + FOREACH_ARRAY(rr, i->records, i->n_records) + mask |= UINT32_C(1) << (*rr)->pcr; + + return mask; +} + +static uint32_t event_log_component_pcrs(EventLogComponent *c) { + uint32_t mask = 0; + + assert(c); + + /* Returns mask of PCRs touched by this component */ + + FOREACH_ARRAY(ii, c->variants, c->n_variants) + mask |= event_log_component_variant_pcrs(*ii); + + return mask; +} + +static int event_log_map_components(EventLog *el) { + _cleanup_free_ char *skipped_ids = NULL; + unsigned n_skipped = 0; + int r; + + assert(el); + + FOREACH_ARRAY(cc, el->components, el->n_components) { + _cleanup_free_ char *matching_ids = NULL; + unsigned n_matching = 0, n_empty = 0; + EventLogComponent *c = *cc; + + if (arg_location_end && strcmp(c->id, arg_location_end) > 0) { + n_skipped++; + + if (!strextend_with_separator(&skipped_ids, ", ", c->id)) + return log_oom(); + + continue; + } + + FOREACH_ARRAY(ii, c->variants, c->n_variants) { + EventLogComponentVariant *i = *ii; + + if (i->n_records == 0) { + /* The empty variant always matches */ + n_empty++; + continue; + } + + r = event_log_match_component_variant(el, 0, i, 0, n_matching + n_empty == 0); + if (r < 0) + return r; + if (r > 0) { + n_matching++; + + if (!strextend_with_separator(&matching_ids, ", ", i->id)) + return log_oom(); + } + } + + if (n_matching + n_empty == 0) { + + if (arg_location_start && strcmp(c->id, arg_location_start) >= 0) + log_info("Didn't find component '%s' in event log, assuming system hasn't reached it yet.", c->id); + else { + log_notice("Couldn't find component '%s' in event log.", c->id); + el->n_missing_components++; + el->missing_component_pcrs |= event_log_component_pcrs(c); + } + } else if (n_matching > 1) + log_debug("Found %u possible variants of component '%s' in event log (%s). Proceeding.", n_matching, c->id, matching_ids); + } + + if (n_skipped > 0) + log_notice("Skipped %u components after location '%s' (%s).", n_skipped, arg_location_end, skipped_ids); + if (el->n_missing_components > 0) + log_notice("Unable to recognize %zu components in event log.", el->n_missing_components); + + return event_log_validate_fully_recognized(el); +} + +static void hsv_to_rgb( + double h, double s, double v, + uint8_t* ret_r, uint8_t *ret_g, uint8_t *ret_b) { + + double c, x, m, r, g, b; + + assert(s >= 0 && s <= 100); + assert(v >= 0 && v <= 100); + assert(ret_r); + assert(ret_g); + assert(ret_b); + + c = (s / 100.0) * (v / 100.0); + x = c * (1 - fabs(fmod(h / 60.0, 2) - 1)); + m = (v / 100) - c; + + if (h >= 0 && h < 60) + r = c, g = x, b = 0.0; + else if (h >= 60 && h < 120) + r = x, g = c, b = 0.0; + else if (h >= 120 && h < 180) + r = 0.0, g = c, b = x; + else if (h >= 180 && h < 240) + r = 0.0, g = x, b = c; + else if (h >= 240 && h < 300) + r = x, g = 0.0, b = c; + else + r = c, g = 0.0, b = x; + + *ret_r = (uint8_t) ((r + m) * 255); + *ret_g = (uint8_t) ((g + m) * 255); + *ret_b = (uint8_t) ((b + m) * 255); +} + +#define ANSI_TRUE_COLOR_MAX (7U + 3U + 1U + 3U + 1U + 3U + 2U) + +static const char *ansi_true_color(uint8_t r, uint8_t g, uint8_t b, char ret[static ANSI_TRUE_COLOR_MAX]) { + snprintf(ret, ANSI_TRUE_COLOR_MAX, "\x1B[38;2;%u;%u;%um", r, g, b); + return ret; +} + +static char *color_for_pcr(EventLog *el, uint32_t pcr) { + char color[ANSI_TRUE_COLOR_MAX]; + uint8_t r, g, b; + + assert(el); + assert(pcr < TPM2_PCRS_MAX); + + if (el->registers[pcr].color) + return el->registers[pcr].color; + + hsv_to_rgb(360.0 / (TPM2_PCRS_MAX - 1) * pcr, 100, 90, &r, &g, &b); + ansi_true_color(r, g, b, color); + + el->registers[pcr].color = strdup(color); + return el->registers[pcr].color; +} + +static int add_algorithm_columns( + EventLog *el, + Table *table, + const char *prefix, + const char *json_field_prefix) { + + int r; + + assert(el); + assert(table); + + FOREACH_ARRAY(alg, el->algorithms, el->n_algorithms) { + const char *n = tpm2_hash_alg_to_string(*alg); + _cleanup_free_ char *v = NULL; + + if (prefix) { + v = strjoin(prefix, " ", n); + if (!v) + return log_oom(); + } + + size_t c = table_get_current_column(table); + + r = table_add_cell(table, NULL, TABLE_HEADER, v ?: n); + if (r < 0) + return table_log_add_error(r); + + if (FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF) && + el->primary_algorithm != UINT16_MAX && + *alg != el->primary_algorithm) + (void) table_hide_column_from_display(table, c); + + _cleanup_free_ char *j = NULL; + if (json_field_prefix) { + _cleanup_free_ char *m = strdup(n); + if (!m) + return log_oom(); + + j = strjoin(json_field_prefix, ascii_strupper(m)); + if (!j) + return log_oom(); + } + + (void) table_set_json_field_name(table, c, j ?: n); + } + + return 0; +} + +static int show_log_table(EventLog *el, JsonVariant **ret_variant) { + _cleanup_(table_unrefp) Table *table = NULL; + int r; + + assert(el); + + table = table_new_raw(5 + el->n_algorithms + 4); + if (!table) + return log_oom(); + + (void) table_set_ersatz_string(table, TABLE_ERSATZ_DASH); + + r = table_add_many(table, + TABLE_HEADER, "pcr", + TABLE_SET_ALIGN_PERCENT, 100, + TABLE_HEADER, "", + TABLE_HEADER, "pcrname", + TABLE_HEADER, "event", + TABLE_HEADER, "match", + TABLE_SET_ALIGN_PERCENT, 100); + if (r < 0) + return table_log_add_error(r); + + r = add_algorithm_columns(el, table, NULL, NULL); + if (r < 0) + return r; + + size_t phase_column = table_get_current_column(table); + + r = table_add_many(table, + TABLE_HEADER, "F/U", + TABLE_HEADER, "source", + TABLE_HEADER, "component", + TABLE_HEADER, "description"); + if (r < 0) + return table_log_add_error(r); + + (void) table_hide_column_from_display(table, table_get_columns(table) - 3); /* hide source */ + + if (!FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) + (void) table_hide_column_from_display(table, (size_t) 1); /* hide color block column */ + + (void) table_set_json_field_name(table, phase_column, "phase"); + + FOREACH_ARRAY(rr, el->records, el->n_records) { + EventLogRecord *record = *rr; + + r = table_add_many(table, + TABLE_UINT32, record->pcr, + TABLE_STRING, special_glyph(SPECIAL_GLYPH_FULL_BLOCK), + TABLE_SET_COLOR, color_for_pcr(el, record->pcr), + TABLE_STRING, tpm2_pcr_index_to_string(record->pcr)); + if (r < 0) + return table_log_add_error(r); + + if (EVENT_LOG_RECORD_IS_FIRMWARE(record)) { + const char *et; + + et = tpm2_log_event_type_to_string(record->firmware_event_type); + if (et) + r = table_add_cell(table, NULL, TABLE_STRING, et); + else + r = table_add_cell(table, NULL, TABLE_UINT32_HEX, &record->firmware_event_type); + } else if (EVENT_LOG_RECORD_IS_USERSPACE(record)) + r = table_add_cell(table, NULL, TABLE_STRING, tpm2_userspace_event_type_to_string(record->userspace_event_type)); + else + r = table_add_cell(table, NULL, TABLE_EMPTY, NULL); + if (r < 0) + return table_log_add_error(r); + + if (record->event_payload_valid < 0 || record->event_payload_valid == EVENT_PAYLOAD_VALID_DONT_KNOW) + r = table_add_cell(table, NULL, TABLE_EMPTY, NULL); + else + r = table_add_many(table, + TABLE_BOOLEAN_CHECKMARK, record->event_payload_valid == EVENT_PAYLOAD_VALID_YES, + TABLE_SET_COLOR, ansi_highlight_green_red(record->event_payload_valid == EVENT_PAYLOAD_VALID_YES)); + if (r < 0) + return table_log_add_error(r); + + FOREACH_ARRAY(alg, el->algorithms, el->n_algorithms) { + EventLogRecordBank *bank; + + bank = event_log_record_find_bank(record, *alg); + if (bank) { + _cleanup_free_ char *hex = NULL; + + hex = hexmem(bank->hash.buffer, bank->hash.size); + if (!hex) + return log_oom(); + + r = table_add_cell(table, NULL, TABLE_STRING, hex); + } else + r = table_add_cell(table, NULL, TABLE_EMPTY, NULL); + if (r < 0) + return table_log_add_error(r); + } + + r = table_add_many(table, + TABLE_STRING, EVENT_LOG_RECORD_IS_FIRMWARE(record) ? "F" : + EVENT_LOG_RECORD_IS_USERSPACE(record) ? "U" : NULL, + TABLE_PATH_BASENAME, record->source, + TABLE_PATH_BASENAME, record->n_mapped > 0 ? record->mapped[0]->component->id : NULL, + TABLE_STRING, record->description); + if (r < 0) + return table_log_add_error(r); + } + + if (ret_variant) { + r = table_to_json(table, ret_variant); + if (r < 0) + return log_error_errno(r, "Failed to format table to JSON: %m"); + + return 0; + } + + r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, /* show_header= */true); + if (r < 0) + return log_error_errno(r, "Failed to output table: %m"); + + return 0; +} + +static bool is_unset_pcr(const void *value, size_t size) { + return memeqzero(value, size) || memeqbyte(0xffu, value, size); +} + +static bool event_log_pcr_checks_out(const EventLog *el, const EventLogRegister *reg) { + assert(el); + assert(reg); + + for (size_t i = 0; i < el->n_algorithms; i++) + if (memcmp_nn(reg->banks[i].calculated.buffer, reg->banks[i].calculated.size, + reg->banks[i].observed.buffer, reg->banks[i].observed.size) != 0) + return false; + + return true; +} + +static int show_pcr_table(EventLog *el, JsonVariant **ret_variant) { + _cleanup_(table_unrefp) Table *table = NULL; + int r; + + assert(el); + + table = table_new_raw(7 + el->n_algorithms*2); + if (!table) + return log_oom(); + + (void) table_set_ersatz_string(table, TABLE_ERSATZ_DASH); + + r = table_add_many(table, + TABLE_HEADER, "pcr", + TABLE_SET_ALIGN_PERCENT, 100, + TABLE_HEADER, "", + TABLE_HEADER, "pcrname", + TABLE_HEADER, "count", + TABLE_SET_ALIGN_PERCENT, 100, + TABLE_HEADER, "h", + TABLE_SET_ALIGN_PERCENT, 100, + TABLE_HEADER, "r", + TABLE_SET_ALIGN_PERCENT, 100, + TABLE_HEADER, "c", + TABLE_SET_ALIGN_PERCENT, 100); + if (r < 0) + return table_log_add_error(r); + + r = add_algorithm_columns(el, table, "Calculated", "calculated"); + if (r < 0) + return r; + + r = add_algorithm_columns(el, table, "Observed", "observed"); + if (r < 0) + return r; + + if (!FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) + (void) table_hide_column_from_display(table, (size_t) 1); /* hide color block column */ + + (void) table_set_json_field_name(table, 4, "hashMatchesEventLog"); + (void) table_set_json_field_name(table, 5, "allEventsMatched"); + (void) table_set_json_field_name(table, 6, "noMissingComponents"); + + for (uint32_t pcr = 0; pcr < TPM2_PCRS_MAX; pcr++) { + r = table_add_many(table, + TABLE_UINT32, pcr, + TABLE_STRING, special_glyph(SPECIAL_GLYPH_FULL_BLOCK), + TABLE_SET_COLOR, color_for_pcr(el, pcr), + TABLE_STRING, tpm2_pcr_index_to_string(pcr)); + if (r < 0) + return table_log_add_error(r); + + if (el->registers[pcr].n_measurements > 0) + r = table_add_cell(table, NULL, TABLE_UINT, &el->registers[pcr].n_measurements); + else + r = table_add_cell(table, NULL, TABLE_EMPTY, NULL); + if (r < 0) + return table_log_add_error(r); + + /* Check if the PCR hash value matches the event log data */ + bool hash_match = event_log_pcr_checks_out(el, el->registers + pcr); + + /* Whether all records in this PCR have a matching component */ + bool fully_recognized = el->registers[pcr].fully_recognized; + + /* Whether any unmatched components touch this PCR */ + bool missing_components = FLAGS_SET(el->missing_component_pcrs, UINT32_C(1) << pcr); + + r = table_add_many(table, + TABLE_BOOLEAN_CHECKMARK, hash_match, + TABLE_SET_COLOR, ansi_highlight_green_red(hash_match), + TABLE_BOOLEAN_CHECKMARK, fully_recognized, + TABLE_SET_COLOR, ansi_highlight_green_red(fully_recognized), + TABLE_BOOLEAN_CHECKMARK, !missing_components, + TABLE_SET_COLOR, ansi_highlight_green_red(!missing_components)); + if (r < 0) + return table_log_add_error(r); + + for (size_t i = 0; i < el->n_algorithms; i++) { + const char *color; + + color = is_unset_pcr(el->registers[pcr].banks[i].calculated.buffer, el->registers[pcr].banks[i].calculated.size) ? ANSI_GREY : NULL; + + if (el->registers[pcr].banks[i].calculated.size > 0) { + _cleanup_free_ char *hex = NULL; + + hex = hexmem(el->registers[pcr].banks[i].calculated.buffer, el->registers[pcr].banks[i].calculated.size); + if (!hex) + return log_oom(); + + r = table_add_many(table, + TABLE_STRING, hex, + TABLE_SET_COLOR, color); + } else + r = table_add_many(table, + TABLE_EMPTY, + TABLE_SET_COLOR, color); + if (r < 0) + return table_log_add_error(r); + } + + for (size_t i = 0; i < el->n_algorithms; i++) { + _cleanup_free_ char *hex = NULL; + const char *color; + + hex = hexmem(el->registers[pcr].banks[i].observed.buffer, el->registers[pcr].banks[i].observed.size); + if (!hex) + return log_oom(); + + color = !hash_match ? ANSI_HIGHLIGHT_RED : + is_unset_pcr(el->registers[pcr].banks[i].observed.buffer, el->registers[pcr].banks[i].observed.size) ? ANSI_GREY : NULL; + + r = table_add_many(table, + TABLE_STRING, hex, + TABLE_SET_COLOR, color); + if (r < 0) + return table_log_add_error(r); + } + } + + if (ret_variant) { + r = table_to_json(table, ret_variant); + if (r < 0) + return log_error_errno(r, "Failed to format table to JSON: %m"); + + return 0; + } + + r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, /* show_header= */ true); + if (r < 0) + return log_error_errno(r, "Failed to output table: %m"); + + if (FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) + printf("\n" + "%sLegend: H → PCR hash value matches event log%s\n" + "%s R → All event log records for this PCR have a matching component%s\n" + "%s C → No components that couldn't be matched with log records affect this PCR%s\n", + ansi_grey(), ansi_normal(), /* less on small screens automatically resets the color after long lines, hence we set it anew for each line */ + ansi_grey(), ansi_normal(), + ansi_grey(), ansi_normal()); + + return 0; +} + +static int event_determine_primary_algorithm(EventLog *el) { + assert(el); + + if (el->n_algorithms == 0) { + /* Nothing loaded to make the decision on? Then pick SHA256 */ + el->primary_algorithm = TPM2_ALG_SHA256; + return 0; + } + + FOREACH_ARRAY(alg, el->algorithms, el->n_algorithms) { + /* If we have SHA256, focus on that that */ + + if (*alg == TPM2_ALG_SHA256) { + el->primary_algorithm = *alg; + return 0; + } + } + + /* Otherwise show the "best" (i.e. the one with the highest id value) */ + el->primary_algorithm = el->algorithms[el->n_algorithms-1]; + return 0; +} + +static int event_log_load_and_process(EventLog **ret) { + _cleanup_(event_log_freep) EventLog *el = NULL; + int r; + + el = event_log_new(); + if (!el) + return log_oom(); + + r = event_log_add_algorithms_from_environment(el); + if (r < 0) + return r; + + r = event_log_load(el); + if (r < 0) + return r; + + r = event_log_read_pcrs(el); + if (r < 0) + return r; + + r = event_log_calculate_pcrs(el); + if (r < 0) + return r; + + r = event_log_validate_record_hashes(el); + if (r < 0) + return r; + + r = event_determine_primary_algorithm(el); + if (r < 0) + return r; + + r = event_log_load_components(el); + if (r < 0) + return r; + + r = event_log_map_components(el); + if (r < 0) + return r; + + *ret = TAKE_PTR(el); + return 0; +} + +static int verb_show_log(int argc, char *argv[], void *userdata) { + _cleanup_(json_variant_unrefp) JsonVariant *log_table = NULL, *pcr_table = NULL; + _cleanup_(event_log_freep) EventLog *el = NULL; + bool want_json = !FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF); + int r; + + r = event_log_load_and_process(&el); + if (r < 0) + return r; + + if (!want_json) + putchar('\n'); + + r = show_log_table(el, want_json ? &log_table : NULL); + if (r < 0) + return r; + + if (!want_json) + putchar('\n'); + + r = show_pcr_table(el, want_json ? &pcr_table : NULL); + if (r < 0) + return r; + + if (want_json) { + _cleanup_(json_variant_unrefp) JsonVariant *object = NULL; + + r = json_build(&object, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_VARIANT("log", log_table), + JSON_BUILD_PAIR_VARIANT("pcrs", pcr_table))); + if (r < 0) + return log_error_errno(r, "Failed to generate combined object: %m"); + + r = json_variant_dump(object, arg_json_format_flags, stdout, /* prefix= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to dump JSON object: %m"); + } + + return 0; +} + +static int verb_show_cel(int argc, char *argv[], void *userdata) { + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL; + _cleanup_(event_log_freep) EventLog *el = NULL; + uint64_t recnum = 0; + int r; + + el = event_log_new(); + if (!el) + return log_oom(); + + r = event_log_load(el); + if (r < 0) + return r; + + /* Output the event log in TCG CEL-JSON. */ + + FOREACH_ARRAY(rr, el->records, el->n_records) { + _cleanup_(json_variant_unrefp) JsonVariant *ja = NULL, *fj = NULL; + EventLogRecord *record = *rr; + JsonVariant *cd = NULL; + const char *ct = NULL; + + LIST_FOREACH(banks, bank, record->banks) { + r = json_variant_append_arrayb( + &ja, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_STRING("hashAlg", tpm2_hash_alg_to_string(bank->algorithm)), + JSON_BUILD_PAIR_HEX("digest", bank->hash.buffer, bank->hash.size))); + if (r < 0) + return log_error_errno(r, "Failed to append CEL digest entry: %m"); + } + + if (!ja) { + r = json_variant_new_array(&ja, NULL, 0); + if (r < 0) + return log_error_errno(r, "Failed to allocate JSON array: %m"); + } + + if (EVENT_LOG_RECORD_IS_FIRMWARE(record)) { + _cleanup_free_ char *et = NULL; + const char *z; + + z = tpm2_log_event_type_to_string(record->firmware_event_type); + if (z) { + _cleanup_free_ char *b = NULL; + + b = strreplace(z, "-", "_"); + if (!b) + return log_oom(); + + et = strjoin("EV_", ascii_strupper(b)); + if (!et) + return log_oom(); + } else if (asprintf(&et, "%" PRIu32, record->firmware_event_type) < 0) + return log_oom(); + + r = json_build(&fj, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_STRING("event_type", et), + JSON_BUILD_PAIR_HEX("event_data", record->firmware_payload, record->firmware_payload_size))); + if (r < 0) + return log_error_errno(r, "Failed to build firmware event data: %m"); + + cd = fj; + ct = "pcclient_std"; + } else if (EVENT_LOG_RECORD_IS_USERSPACE(record)) { + cd = record->userspace_content; + ct = "systemd"; + } + + r = json_variant_append_arrayb(&array, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_UNSIGNED("pcr", record->pcr), + JSON_BUILD_PAIR_UNSIGNED("recnum", ++recnum), + JSON_BUILD_PAIR_VARIANT("digests", ja), + JSON_BUILD_PAIR_CONDITION(ct, "content_type", JSON_BUILD_STRING(ct)), + JSON_BUILD_PAIR_CONDITION(cd, "content", JSON_BUILD_VARIANT(cd)))); + if (r < 0) + return log_error_errno(r, "Failed to append CEL record: %m"); + } + + if (arg_json_format_flags & (JSON_FORMAT_PRETTY|JSON_FORMAT_PRETTY_AUTO)) + pager_open(arg_pager_flags); + + json_variant_dump(array, arg_json_format_flags|JSON_FORMAT_EMPTY_ARRAY, stdout, NULL); + return 0; +} + +static int verb_list_components(int argc, char *argv[], void *userdata) { + _cleanup_(event_log_freep) EventLog *el = NULL; + _cleanup_(table_unrefp) Table *table = NULL; + enum { + BEFORE_LOCATION, + BETWEEN_LOCATION, + AFTER_LOCATION, + } loc = BEFORE_LOCATION; + int r; + + el = event_log_new(); + if (!el) + return log_oom(); + + r = event_log_add_algorithms_from_environment(el); + if (r < 0) + return r; + + r = event_determine_primary_algorithm(el); + if (r < 0) + return r; + + r = event_log_load_components(el); + if (r < 0) + return r; + + table = table_new("id", "variants"); + if (!table) + return log_oom(); + + FOREACH_ARRAY(c, el->components, el->n_components) { + + if (FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) { + _cleanup_free_ char *marker = NULL; + + switch (loc) { + + case BEFORE_LOCATION: + if (arg_location_end && strcmp((*c)->id, arg_location_end) >= 0) { + loc = AFTER_LOCATION; + marker = strjoin(special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), " location '", arg_location_end, "' ", special_glyph(SPECIAL_GLYPH_ARROW_LEFT)); + } else if (arg_location_start && strcmp((*c)->id, arg_location_start) >= 0) { + loc = BETWEEN_LOCATION; + marker = strjoin(special_glyph(SPECIAL_GLYPH_TREE_TOP), " start location '", arg_location_start, "' ", special_glyph(SPECIAL_GLYPH_ARROW_DOWN)); + } + + break; + + case BETWEEN_LOCATION: + if (arg_location_end && strcmp((*c)->id, arg_location_end) >= 0) { + loc = AFTER_LOCATION; + marker = strjoin(special_glyph(SPECIAL_GLYPH_TREE_RIGHT), " end location '", arg_location_end, "' ", special_glyph(SPECIAL_GLYPH_ARROW_UP)); + } + break; + + case AFTER_LOCATION: + break; + } + + if (marker) { + r = table_add_many(table, + TABLE_STRING, marker, + TABLE_SET_COLOR, ANSI_GREY, + TABLE_EMPTY); + if (r < 0) + return table_log_add_error(r); + } + } + + FOREACH_ARRAY(variant, (*c)->variants, (*c)->n_variants) { + r = table_add_many(table, + TABLE_STRING, (*c)->id, + TABLE_PATH, (*variant)->path); + if (r < 0) + return table_log_add_error(r); + } + } + + if (table_get_rows(table) > 1 || !FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) { + r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, /* show_header= */ true); + if (r < 0) + return log_error_errno(r, "Failed to output table: %m"); + } + + if (FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) { + if (table_get_rows(table) > 1) + printf("\n%zu components listed.\n", table_get_rows(table) - 1); + else + printf("No components defined.\n"); + } + + return 0; +} + +static int event_log_pcr_mask_checks_out(EventLog *el, uint32_t mask) { + assert(el); + + for (uint32_t pcr = 0; pcr < TPM2_PCRS_MAX; pcr++) { + + if (!FLAGS_SET(mask, UINT32_C(1) << pcr)) + continue; + + if (!event_log_pcr_checks_out(el, el->registers + pcr)) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Event log for PCR %" PRIu32 " does not match PCR state, refusing.", pcr); + } + + return 0; +} + +static int make_pcrlock_record( + uint32_t pcr, + const void *data, + size_t data_size, + JsonVariant **ret_record) { + + _cleanup_(json_variant_unrefp) JsonVariant *digests = NULL; + int r; + + assert(data || data_size == 0); + assert(ret_record); + + if (data_size == SIZE_MAX) + data_size = strlen(data); + + /* Generates a .pcrlock record for the given PCR and data/data size. This is a subset of TCG CEL. */ + + FOREACH_ARRAY(pa, tpm2_hash_algorithms, TPM2_N_HASH_ALGORITHMS) { + _cleanup_free_ unsigned char *hash = NULL; + int hash_ssize; + unsigned hash_usize; + const EVP_MD *md; + const char *a; + + assert_se(a = tpm2_hash_alg_to_string(*pa)); + assert_se(md = EVP_get_digestbyname(a)); + hash_ssize = EVP_MD_size(md); + assert_se(hash_ssize > 0); + hash_usize = hash_ssize; + + hash = malloc(hash_usize); + if (!hash) + return log_oom(); + + if (EVP_Digest(data, data_size, hash, &hash_usize, md, NULL) != 1) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to hash data with algorithm '%s'.", a); + + r = json_variant_append_arrayb( + &digests, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("hashAlg", JSON_BUILD_STRING(a)), + JSON_BUILD_PAIR("digest", JSON_BUILD_HEX(hash, hash_usize)))); + if (r < 0) + return log_error_errno(r, "Failed to build JSON digest object: %m"); + } + + r = json_build(ret_record, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("pcr", JSON_BUILD_UNSIGNED(pcr)), + JSON_BUILD_PAIR("digests", JSON_BUILD_VARIANT(digests)))); + if (r < 0) + return log_error_errno(r, "Failed to build record object: %m"); + + return 0; +} + +static const char *pcrlock_path(const char *default_pcrlock_path) { + return arg_pcrlock_path ?: arg_pcrlock_auto ? default_pcrlock_path : NULL; +} + +static int write_pcrlock(JsonVariant *array, const char *default_pcrlock_path) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *a = NULL; + _cleanup_fclose_ FILE *f = NULL; + const char *p; + int r; + + if (!array) { + r = json_variant_new_array(&a, NULL, 0); + if (r < 0) + return log_error_errno(r, "Failed to allocate empty array: %m"); + + array = a; + } + + r = json_build(&v, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("records", JSON_BUILD_VARIANT(array)))); + if (r < 0) + return log_error_errno(r, "Failed to build JSON object: %m"); + + p = pcrlock_path(default_pcrlock_path); + if (p) { + (void) mkdir_parents_label(p, 0755); + + f = fopen(p, "we"); + if (!f) + return log_error_errno(errno, "Failed to open %s for writing: %m", p); + } + + r = json_variant_dump(v, arg_json_format_flags, f ?: stdout, /* prefix= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to output JSON object: %m"); + + if (p) + log_info("%s written.", p); + + return 0; +} + +static int unlink_pcrlock(const char *default_pcrlock_path) { + const char *p; + + p = pcrlock_path(default_pcrlock_path); + if (!p) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No .pcrlock path specified, refusing."); + + if (unlink(p) < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to delete %s: %m", p); + + log_info("%s already deleted.", p); + } else + log_info("%s deleted.", p); + + (void) rmdir_parents(p, "/var/lib"); + + return 0; +} + +static int verb_lock_raw(int argc, char *argv[], void *userdata) { + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL; + _cleanup_free_ char *data = NULL; + _cleanup_fclose_ FILE *f = NULL; + size_t size; + int r; + + if (arg_pcr_mask == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No PCR specified, refusing."); + + if (argc >= 2) { + f = fopen(argv[1], "re"); + if (!f) + return log_error_errno(errno, "Failed to open '%s': %m", argv[1]); + } + + r = read_full_stream(f ?: stdin, &data, &size); + if (r < 0) + return log_error_errno(r, "Failed to read data from stdin: %m"); + + for (uint32_t i = 0; i < TPM2_PCRS_MAX; i++) { + _cleanup_(json_variant_unrefp) JsonVariant *record = NULL; + + if (!FLAGS_SET(arg_pcr_mask, UINT32_C(1) << i)) + continue; + + r = make_pcrlock_record(i, data, size, &record); + if (r < 0) + return r; + + r = json_variant_append_array(&array, record); + if (r < 0) + return log_error_errno(r, "Failed to append to JSON array: %m"); + } + + return write_pcrlock(array, NULL); +} + +static int verb_unlock_simple(int argc, char *argv[], void *userdata) { + return unlink_pcrlock(NULL); +} + +static int verb_lock_secureboot_policy(int argc, char *argv[], void *userdata) { + static const struct { + sd_id128_t id; + const char *name; + int synthesize_empty; /* 0 → fail, > 0 → synthesize empty db, < 0 → skip */ + } variables[] = { + { EFI_VENDOR_GLOBAL, "SecureBoot", 0 }, + { EFI_VENDOR_GLOBAL, "PK", 0 }, + { EFI_VENDOR_GLOBAL, "KEK", 0 }, + { EFI_VENDOR_DATABASE, "db", 1 }, + { EFI_VENDOR_DATABASE, "dbx", 1 }, + { EFI_VENDOR_DATABASE, "dbt", -1 }, + { EFI_VENDOR_DATABASE, "dbr", -1 }, + }; + + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL; + int r; + + /* Generates expected records from the current SecureBoot state, as readable in the EFI variables + * right now. */ + + FOREACH_ARRAY(vv, variables, ELEMENTSOF(variables)) { + _cleanup_(json_variant_unrefp) JsonVariant *record = NULL; + + _cleanup_free_ char *name = NULL; + if (asprintf(&name, "%s-" SD_ID128_UUID_FORMAT_STR, vv->name, SD_ID128_FORMAT_VAL(vv->id)) < 0) + return log_oom(); + + _cleanup_free_ void *data = NULL; + size_t data_size; + r = efi_get_variable(name, NULL, &data, &data_size); + if (r < 0) { + if (r != -ENOENT || vv->synthesize_empty == 0) + return log_error_errno(r, "Failed to read EFI variable '%s': %m", name); + if (vv->synthesize_empty < 0) + continue; + + /* If the main database variables are not set we don't consider this an error, but + * measure an empty database instead. */ + log_debug("EFI variable %s is not set, synthesizing empty variable for measurement.", name); + data_size = 0; + } + + _cleanup_free_ char16_t* name16 = utf8_to_utf16(vv->name, SIZE_MAX); + if (!name16) + return log_oom(); + size_t name16_bytes = char16_strlen(name16) * 2; + + size_t vdata_size = offsetof(UEFI_VARIABLE_DATA, unicodeName) + name16_bytes + data_size; + _cleanup_free_ UEFI_VARIABLE_DATA *vdata = malloc(vdata_size); + if (!vdata) + return log_oom(); + + *vdata = (UEFI_VARIABLE_DATA) { + .unicodeNameLength = name16_bytes / 2, + .variableDataLength = data_size, + }; + + efi_id128_to_guid(vv->id, vdata->variableName); + memcpy(mempcpy(vdata->unicodeName, name16, name16_bytes), data, data_size); + + r = make_pcrlock_record(TPM2_PCR_SECURE_BOOT_POLICY /* =7 */, vdata, vdata_size, &record); + if (r < 0) + return r; + + r = json_variant_append_array(&array, record); + if (r < 0) + return log_error_errno(r, "Failed to append to JSON array: %m"); + } + + return write_pcrlock(array, PCRLOCK_SECUREBOOT_POLICY_PATH); +} + +static int verb_unlock_secureboot_policy(int argc, char *argv[], void *userdata) { + return unlink_pcrlock(PCRLOCK_SECUREBOOT_POLICY_PATH); +} + +static int event_log_record_is_secureboot_variable(EventLogRecord *rec, sd_id128_t uuid, const char *name) { + _cleanup_free_ char *found_name = NULL; + sd_id128_t found_uuid; + int r; + + assert(rec); + assert(name); + + if (!EVENT_LOG_RECORD_IS_FIRMWARE(rec)) + return false; + + if (rec->pcr != TPM2_PCR_SECURE_BOOT_POLICY) + return false; + + if (rec->event_payload_valid != EVENT_PAYLOAD_VALID_YES) + return false; + + if (rec->firmware_event_type != EV_EFI_VARIABLE_DRIVER_CONFIG) + return false; + + r = event_log_record_parse_variable_data(rec, &found_uuid, &found_name); + if (r == -EBADMSG) + return false; + if (r < 0) + return r; + + if (!sd_id128_equal(found_uuid, uuid)) + return false; + + return streq(found_name, name); +} + +static bool event_log_record_is_secureboot_authority(EventLogRecord *rec) { + assert(rec); + + if (!EVENT_LOG_RECORD_IS_FIRMWARE(rec)) + return false; + + if (rec->pcr != TPM2_PCR_SECURE_BOOT_POLICY) + return false; + + if (rec->event_payload_valid != EVENT_PAYLOAD_VALID_YES) + return false; + + return rec->firmware_event_type == EV_EFI_VARIABLE_AUTHORITY; +} + +static int event_log_ensure_secureboot_consistency(EventLog *el) { + static const struct { + sd_id128_t id; + const char *name; + bool required; + } table[] = { + { EFI_VENDOR_GLOBAL, "SecureBoot", true }, + { EFI_VENDOR_GLOBAL, "PK", true }, + { EFI_VENDOR_GLOBAL, "KEK", true }, + { EFI_VENDOR_DATABASE, "db", true }, + { EFI_VENDOR_DATABASE, "dbx", true }, + { EFI_VENDOR_DATABASE, "dbt", false }, + { EFI_VENDOR_DATABASE, "dbr", false }, + // FIXME: ensure we also find the separator here + }; + + EventLogRecord *records[ELEMENTSOF(table)] = {}; + EventLogRecord *first_authority = NULL; + + assert(el); + + /* Ensures that the PCR 7 records are complete and in order. Before we lock down PCR 7 we want to + * ensure its state is actually consistent. */ + + FOREACH_ARRAY(rr, el->records, el->n_records) { + EventLogRecord *rec = *rr; + size_t found = SIZE_MAX; + + if (event_log_record_is_secureboot_authority(rec)) { + if (first_authority) + continue; + + first_authority = rec; + // FIXME: also check that each authority record's data is also listed in 'db' + continue; + } + + for (size_t i = 0; i < ELEMENTSOF(table); i++) + if (event_log_record_is_secureboot_variable(rec, table[i].id, table[i].name)) { + found = i; + break; + } + if (found == SIZE_MAX) + continue; + + /* Require the authority records always come *after* database measurements */ + if (first_authority) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "SecureBoot authority before variable, refusing."); + + /* Check for duplicates */ + if (records[found]) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Duplicate '%s' record, refusing.", rec->description); + + /* Check for order */ + for (size_t j = found + 1; j < ELEMENTSOF(table); j++) + if (records[j]) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "'%s' record before '%s' record, refusing.", records[j]->description, rec->description); + + records[found] = rec; + } + + /* Check for existance */ + for (size_t i = 0; i < ELEMENTSOF(table); i++) + if (table[i].required && !records[i]) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Required record '%s' not found, refusing.", table[i].name); + + /* At this point we know that all required variables have been measured, in the right order. */ + return 0; +} + +static int verb_lock_secureboot_authority(int argc, char *argv[], void *userdata) { + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL; + _cleanup_(event_log_freep) EventLog *el = NULL; + int r; + + /* Lock down the EV_EFI_VARIABLE_AUTHORITY records from the existing log. Note that there's not too + * much value in locking this down too much, since it stores only the result of the primary database + * checks, and that's what we should bind policy to. Moreover it's hard to predict, since extension + * card firmware validation will result in additional records here. */ + + if (!is_efi_secure_boot()) { + log_info("SecureBoot disabled, not generating authority .pcrlock file."); + return unlink_pcrlock(PCRLOCK_SECUREBOOT_AUTHORITY_PATH); + } + + el = event_log_new(); + if (!el) + return log_oom(); + + r = event_log_add_algorithms_from_environment(el); + if (r < 0) + return r; + + r = event_log_load(el); + if (r < 0) + return r; + + r = event_log_read_pcrs(el); + if (r < 0) + return r; + + r = event_log_calculate_pcrs(el); + if (r < 0) + return r; + + /* Before we base anything on the event log records, let's check that the event log state checks + * out. */ + + r = event_log_pcr_mask_checks_out(el, UINT32_C(1) << TPM2_PCR_SECURE_BOOT_POLICY); + if (r < 0) + return r; + + r = event_log_validate_record_hashes(el); + if (r < 0) + return r; + + r = event_log_ensure_secureboot_consistency(el); + if (r < 0) + return r; + + FOREACH_ARRAY(rr, el->records, el->n_records) { + _cleanup_(json_variant_unrefp) JsonVariant *digests = NULL; + EventLogRecord *rec = *rr; + + if (!event_log_record_is_secureboot_authority(rec)) + continue; + + log_debug("Locking down authority '%s'.", strna(rec->description)); + + LIST_FOREACH(banks, bank, rec->banks) { + r = json_variant_append_arrayb( + &digests, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("hashAlg", JSON_BUILD_STRING(tpm2_hash_alg_to_string(bank->algorithm))), + JSON_BUILD_PAIR("digest", JSON_BUILD_HEX(bank->hash.buffer, bank->hash.size)))); + if (r < 0) + return log_error_errno(r, "Failed to build digests array: %m"); + } + + r = json_variant_append_arrayb( + &array, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("pcr", JSON_BUILD_UNSIGNED(rec->pcr)), + JSON_BUILD_PAIR("digests", JSON_BUILD_VARIANT(digests)))); + if (r < 0) + return log_error_errno(r, "Failed to build record array: %m"); + } + + return write_pcrlock(array, PCRLOCK_SECUREBOOT_AUTHORITY_PATH); +} + +static int verb_unlock_secureboot_authority(int argc, char *argv[], void *userdata) { + return unlink_pcrlock(PCRLOCK_SECUREBOOT_AUTHORITY_PATH); +} + +static int verb_lock_gpt(int argc, char *argv[], void *userdata) { + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL, *record = NULL; + _cleanup_(sd_device_unrefp) sd_device *d = NULL; + uint8_t h[2 * 4096]; /* space for at least two 4K sectors. GPT header should definitely be in here */ + uint64_t start, n_members, member_size; + _cleanup_close_ int fd = -EBADF; + const GptHeader *p; + size_t found = 0; + ssize_t n; + int r; + + r = block_device_new_from_path( + argc >= 2 ? argv[1] : "/", + BLOCK_DEVICE_LOOKUP_WHOLE_DISK|BLOCK_DEVICE_LOOKUP_BACKING|BLOCK_DEVICE_LOOKUP_ORIGINATING, + &d); + if (r < 0) + return log_error_errno(r, "Failed to determine root block device: %m"); + + fd = sd_device_open(d, O_CLOEXEC|O_RDONLY|O_NOCTTY); + if (fd < 0) + return log_error_errno(fd, "Failed to open root block device: %m"); + + n = pread(fd, &h, sizeof(h), 0); + if (n < 0) + return log_error_errno(errno, "Failed to read GPT header of block device: %m"); + if ((size_t) n != sizeof(h)) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short read trying to read GPT header: %m"); + + /* Try a couple of sector sizes */ + for (size_t sz = 512; sz <= 4096; sz <<= 1) { + assert(sizeof(h) >= sz * 2); + p = (const GptHeader*) (h + sz); /* 2nd sector */ + + if (!gpt_header_has_signature(p)) + continue; + + if (found != 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), + "Disk has partition table for multiple sector sizes, refusing."); + + found = sz; + } + + if (found == 0) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), + "Disk does not have GPT partition table, refusing."); + + p = (const GptHeader*) (h + found); + + if (le32toh(p->header_size) > found) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), + "GPT header size over long (%" PRIu32 "), refusing.", le32toh(p->header_size)); + + start = le64toh(p->partition_entry_lba); + if (start > UINT64_MAX / found) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), + "Partition table start offset overflow, refusing."); + + member_size = le32toh(p->size_of_partition_entry); + if (member_size < sizeof(GptPartitionEntry)) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), + "Partition entry size too short, refusing."); + + n_members = le32toh(p->number_of_partition_entries); + uint64_t member_bufsz = n_members * member_size; + if (member_bufsz > 1U*1024U*1024U) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), + "Partition table size too large, refusing."); + + member_bufsz = ROUND_UP(member_bufsz, found); + + _cleanup_free_ void *members = malloc(member_bufsz); + if (!members) + return log_oom(); + + n = pread(fd, members, member_bufsz, start * found); + if (n < 0) + return log_error_errno(errno, "Failed to read GPT partition table entries: %m"); + if ((size_t) n != member_bufsz) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short read while reading GPT partition table entries: %m"); + + size_t vdata_size = le32toh(p->header_size) + sizeof(le64_t) + member_size * n_members; + _cleanup_free_ void *vdata = malloc0(vdata_size); + if (!vdata) + return log_oom(); + + void *n_measured_entries = mempcpy(vdata, p, sizeof(GptHeader)); /* n_measured_entries is a 64bit value */ + + void *qq = (uint8_t*) n_measured_entries + sizeof(le64_t); + + for (uint64_t i = 0; i < n_members; i++) { + const GptPartitionEntry *entry = (const GptPartitionEntry*) ((const uint8_t*) members + (member_size * i)); + + if (memeqzero(entry->partition_type_guid, sizeof(entry->partition_type_guid))) + continue; + + qq = mempcpy(qq, entry, member_size); + unaligned_write_le64(n_measured_entries, unaligned_read_le64(n_measured_entries) + 1); + } + + vdata_size = (uint8_t*) qq - (uint8_t*) vdata; + + r = make_pcrlock_record(TPM2_PCR_BOOT_LOADER_CONFIG /* =5 */, vdata, vdata_size, &record); + if (r < 0) + return r; + + r = json_variant_new_array(&array, &record, 1); + if (r < 0) + return log_error_errno(r, "Failed to append to JSON array: %m"); + + return write_pcrlock(array, PCRLOCK_GPT_PATH); +} + +static int verb_unlock_gpt(int argc, char *argv[], void *userdata) { + return unlink_pcrlock(PCRLOCK_GPT_PATH); +} + +static bool event_log_record_is_separator(const EventLogRecord *rec) { + assert(rec); + + /* Recognizes EV_SEPARATOR events */ + + if (!EVENT_LOG_RECORD_IS_FIRMWARE(rec)) + return false; + + if (rec->firmware_event_type != EV_SEPARATOR) + return false; + + return rec->event_payload_valid == EVENT_PAYLOAD_VALID_YES; /* Insist the record is consistent */ +} + +static int event_log_record_is_action_calling_efi_app(const EventLogRecord *rec) { + _cleanup_free_ char *d = NULL; + int r; + + assert(rec); + + /* Recognizes the special EV_EFI_ACTION that is issues when the firmware passes control to the boot loader. */ + + if (!EVENT_LOG_RECORD_IS_FIRMWARE(rec)) + return false; + + if (rec->pcr != TPM2_PCR_BOOT_LOADER_CODE) + return false; + + if (rec->firmware_event_type != EV_EFI_ACTION) + return false; + + if (rec->event_payload_valid != EVENT_PAYLOAD_VALID_YES) /* Insist the record is consistent */ + return false; + + r = make_cstring(rec->firmware_payload, rec->firmware_payload_size, MAKE_CSTRING_ALLOW_TRAILING_NUL, &d); + if (r < 0) + return r; + + return streq(d, "Calling EFI Application from Boot Option"); +} + +static void enable_json_sse(void) { + /* We shall write this to a single output stream? We have to output two files, hence try to be smart + * and enable JSON SSE */ + + if (!arg_pcrlock_path && arg_pcrlock_auto) + return; + + if (FLAGS_SET(arg_json_format_flags, JSON_FORMAT_SSE)) + return; + + log_notice("Enabling JSON_SEQ mode, since writing two .pcrlock files to single output."); + arg_json_format_flags |= JSON_FORMAT_SSE; +} + +static int verb_lock_firmware(int argc, char *argv[], void *userdata) { + _cleanup_(json_variant_unrefp) JsonVariant *array_early = NULL, *array_late = NULL; + _cleanup_(event_log_freep) EventLog *el = NULL; + uint32_t always_mask, separator_mask, separator_seen_mask = 0, action_seen_mask = 0; + const char *default_pcrlock_early_path, *default_pcrlock_late_path; + int r; + + enable_json_sse(); + + /* The PCRs we intend to cover. Note that we measure firmware, external *and* boot loader code/config + * here – but the latter only until the "separator" events are seen, which tell us where transition + * into OS boot loader happens. This reflects the fact that on some systems the firmware already + * measures some firmware-supplied apps into PCR 4. (e.g. Thinkpad X1 Gen9) */ + if (endswith(argv[0], "firmware-code")) { + always_mask = (UINT32_C(1) << TPM2_PCR_PLATFORM_CODE) | /* → 0 */ + (UINT32_C(1) << TPM2_PCR_EXTERNAL_CODE); /* → 2 */ + + separator_mask = UINT32_C(1) << TPM2_PCR_BOOT_LOADER_CODE; /* → 4 */ + + default_pcrlock_early_path = PCRLOCK_FIRMWARE_CODE_EARLY_PATH; + default_pcrlock_late_path = PCRLOCK_FIRMWARE_CODE_LATE_PATH; + } else { + assert(endswith(argv[0], "firmware-config")); + always_mask = (UINT32_C(1) << TPM2_PCR_PLATFORM_CONFIG) | /* → 1 */ + (UINT32_C(1) << TPM2_PCR_EXTERNAL_CONFIG); /* → 3 */ + + separator_mask = UINT32_C(1) << TPM2_PCR_BOOT_LOADER_CONFIG; /* → 5 */ + + default_pcrlock_early_path = PCRLOCK_FIRMWARE_CONFIG_EARLY_PATH; + default_pcrlock_late_path = PCRLOCK_FIRMWARE_CONFIG_LATE_PATH; + } + + el = event_log_new(); + if (!el) + return log_oom(); + + r = event_log_add_algorithms_from_environment(el); + if (r < 0) + return r; + + r = event_log_load(el); + if (r < 0) + return r; + + r = event_log_read_pcrs(el); + if (r < 0) + return r; + + r = event_log_calculate_pcrs(el); + if (r < 0) + return r; + + r = event_log_validate_record_hashes(el); + if (r < 0) + return r; + + /* Before we base anything on the event log records for any of the selected PCRs, let's check that + * the event log state checks out for them. */ + + r = event_log_pcr_mask_checks_out(el, always_mask|separator_mask); + if (r < 0) + return r; + + // FIXME: before doing this, validate ahead-of-time that EV_SEPARATOR records exist for all entries, + // and exactly once + + FOREACH_ARRAY(rr, el->records, el->n_records) { + _cleanup_(json_variant_unrefp) JsonVariant *digests = NULL; + EventLogRecord *rec = *rr; + uint32_t bit = UINT32_C(1) << rec->pcr; + + if (!EVENT_LOG_RECORD_IS_FIRMWARE(rec)) + continue; + + if (!FLAGS_SET(always_mask, bit) && + !(FLAGS_SET(separator_mask, bit) && !FLAGS_SET(separator_seen_mask|action_seen_mask, bit))) + continue; + + /* If we hit the separator record, we stop processing the PCRs listed in `separator_mask` */ + if (event_log_record_is_separator(rec)) { + separator_seen_mask |= bit; + continue; + } + + /* If we hit the special "Calling EFI Application from Boot Option" action we treat this the + * same as a separator here, as that's where firmware passes control to boot loader. Note + * that some EFI implementations forget to generate one of them. */ + r = event_log_record_is_action_calling_efi_app(rec); + if (r < 0) + return log_error_errno(r, "Failed to check if event is 'Calling EFI Application from Boot Option' action: %m"); + if (r > 0) { + action_seen_mask |= bit; + continue; + } + + LIST_FOREACH(banks, bank, rec->banks) { + r = json_variant_append_arrayb( + &digests, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("hashAlg", JSON_BUILD_STRING(tpm2_hash_alg_to_string(bank->algorithm))), + JSON_BUILD_PAIR("digest", JSON_BUILD_HEX(bank->hash.buffer, bank->hash.size)))); + if (r < 0) + return log_error_errno(r, "Failed to build digests array: %m"); + } + + r = json_variant_append_arrayb( + FLAGS_SET(separator_seen_mask, bit) ? &array_late : &array_early, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("pcr", JSON_BUILD_UNSIGNED(rec->pcr)), + JSON_BUILD_PAIR("digests", JSON_BUILD_VARIANT(digests)))); + if (r < 0) + return log_error_errno(r, "Failed to build record array: %m"); + } + + r = write_pcrlock(array_early, default_pcrlock_early_path); + if (r < 0) + return r; + + return write_pcrlock(array_late, default_pcrlock_late_path); +} + +static int verb_unlock_firmware(int argc, char *argv[], void *userdata) { + const char *default_pcrlock_early_path, *default_pcrlock_late_path; + int r; + + if (endswith(argv[0], "firmware-code")) { + default_pcrlock_early_path = PCRLOCK_FIRMWARE_CODE_EARLY_PATH; + default_pcrlock_late_path = PCRLOCK_FIRMWARE_CODE_LATE_PATH; + } else { + default_pcrlock_early_path = PCRLOCK_FIRMWARE_CONFIG_EARLY_PATH; + default_pcrlock_late_path = PCRLOCK_FIRMWARE_CONFIG_LATE_PATH; + } + + r = unlink_pcrlock(default_pcrlock_early_path); + if (r < 0) + return r; + + if (arg_pcrlock_path) /* if the path is specified don't delete the same thing twice */ + return 0; + + r = unlink_pcrlock(default_pcrlock_late_path); + if (r < 0) + return r; + + return 0; +} + +static int verb_lock_machine_id(int argc, char *argv[], void *userdata) { + _cleanup_(json_variant_unrefp) JsonVariant *record = NULL, *array = NULL; + _cleanup_free_ char *word = NULL; + int r; + + r = pcrextend_machine_id_word(&word); + if (r < 0) + return r; + + r = make_pcrlock_record(TPM2_PCR_SYSTEM_IDENTITY /* = 15 */, word, SIZE_MAX, &record); + if (r < 0) + return r; + + r = json_variant_new_array(&array, &record, 1); + if (r < 0) + return log_error_errno(r, "Failed to create record array: %m"); + + return write_pcrlock(array, PCRLOCK_MACHINE_ID_PATH); +} + +static int verb_unlock_machine_id(int argc, char *argv[], void *userdata) { + return unlink_pcrlock(PCRLOCK_MACHINE_ID_PATH); +} + +static int pcrlock_file_system_path(const char *normalized_path, char **ret) { + _cleanup_free_ char *s = NULL; + + assert(normalized_path); + + if (path_equal(normalized_path, "/")) + s = strdup(PCRLOCK_ROOT_FILE_SYSTEM_PATH); + else { + /* We reuse the escaping we use for turning paths into unit names */ + _cleanup_free_ char *escaped = NULL; + + assert(normalized_path[0] == '/'); + assert(normalized_path[1] != '/'); + + escaped = unit_name_escape(normalized_path + 1); + if (!escaped) + return log_oom(); + + s = strjoin(PCRLOCK_FILE_SYSTEM_PATH_PREFIX, escaped, ".pcrlock"); + } + if (!s) + return log_oom(); + + *ret = TAKE_PTR(s); + return 0; +} + +static int verb_lock_file_system(int argc, char *argv[], void *userdata) { + const char* paths[3] = {}; + int r; + + if (argc > 1) + paths[0] = argv[1]; + else { + dev_t a, b; + paths[0] = "/"; + + r = get_block_device("/", &a); + if (r < 0) + return log_error_errno(r, "Failed to get device of root file system: %m"); + + r = get_block_device("/var", &b); + if (r < 0) + return log_error_errno(r, "Failed to get device of /var/ file system: %m"); + + /* if backing device is distinct, then measure /var/ too */ + if (a != b) + paths[1] = "/var"; + + enable_json_sse(); + } + + STRV_FOREACH(p, paths) { + _cleanup_free_ char *word = NULL, *normalized_path = NULL, *pcrlock_file = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *record = NULL, *array = NULL; + + r = pcrextend_file_system_word(*p, &word, &normalized_path); + if (r < 0) + return r; + + r = pcrlock_file_system_path(normalized_path, &pcrlock_file); + if (r < 0) + return r; + + r = make_pcrlock_record(TPM2_PCR_SYSTEM_IDENTITY /* = 15 */, word, SIZE_MAX, &record); + if (r < 0) + return r; + + r = json_variant_new_array(&array, &record, 1); + if (r < 0) + return log_error_errno(r, "Failed to create record array: %m"); + + r = write_pcrlock(array, pcrlock_file); + if (r < 0) + return r; + } + + return 0; +} + +static int verb_unlock_file_system(int argc, char *argv[], void *userdata) { + const char* paths[3] = {}; + int r; + + if (argc > 1) + paths[0] = argv[1]; + else { + paths[0] = "/"; + paths[1] = "/var"; + } + + STRV_FOREACH(p, paths) { + _cleanup_free_ char *normalized_path = NULL, *pcrlock_file = NULL; + + r = chase(*p, NULL, 0, &normalized_path, NULL); + if (r < 0) + return log_error_errno(r, "Failed to normal path '%s': %m", argv[1]); + + r = pcrlock_file_system_path(normalized_path, &pcrlock_file); + if (r < 0) + return r; + + r = unlink_pcrlock(pcrlock_file); + if (r < 0) + return r; + } + + return 0; +} + +static int verb_lock_pe(int argc, char *argv[], void *userdata) { + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL; + _cleanup_close_ int fd = -EBADF; + int r; + + // FIXME: Maybe also generate a matching EV_EFI_VARIABLE_AUTHORITY records here for each signature that + // covers this PE plus its hash, as alternatives under the same component name + + if (argc >= 2) { + fd = open(argv[1], O_RDONLY|O_CLOEXEC); + if (fd < 0) + return log_error_errno(errno, "Failed to open '%s': %m", argv[1]); + } + + if (arg_pcr_mask == 0) + arg_pcr_mask = UINT32_C(1) << TPM2_PCR_BOOT_LOADER_CODE; + + for (uint32_t i = 0; i < TPM2_PCRS_MAX; i++) { + _cleanup_(json_variant_unrefp) JsonVariant *digests = NULL; + + if (!FLAGS_SET(arg_pcr_mask, UINT32_C(1) << i)) + continue; + + FOREACH_ARRAY(pa, tpm2_hash_algorithms, TPM2_N_HASH_ALGORITHMS) { + _cleanup_free_ void *hash = NULL; + size_t hash_size; + const EVP_MD *md; + const char *a; + + assert_se(a = tpm2_hash_alg_to_string(*pa)); + assert_se(md = EVP_get_digestbyname(a)); + + r = pe_hash(fd < 0 ? STDIN_FILENO : fd, md, &hash, &hash_size); + if (r < 0) + return log_error_errno(r, "Failed to hash PE binary: %m"); + + r = json_variant_append_arrayb(&digests, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("hashAlg", JSON_BUILD_STRING(a)), + JSON_BUILD_PAIR("digest", JSON_BUILD_HEX(hash, hash_size)))); + if (r < 0) + return log_error_errno(r, "Failed to build JSON digest object: %m"); + } + + r = json_variant_append_arrayb( + &array, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("pcr", JSON_BUILD_UNSIGNED(i)), + JSON_BUILD_PAIR("digests", JSON_BUILD_VARIANT(digests)))); + if (r < 0) + return log_error_errno(r, "Failed to append record object: %m"); + } + + return write_pcrlock(array, NULL); +} + +typedef void* SectionHashArray[_UNIFIED_SECTION_MAX * TPM2_N_HASH_ALGORITHMS]; + +static void section_hashes_array_done(SectionHashArray *array) { + assert(array); + + for (size_t i = 0; i < _UNIFIED_SECTION_MAX * TPM2_N_HASH_ALGORITHMS; i++) + free((*array)[i]); +} + +static int verb_lock_uki(int argc, char *argv[], void *userdata) { + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL, *pe_digests = NULL; + _cleanup_(section_hashes_array_done) SectionHashArray section_hashes = {}; + size_t hash_sizes[TPM2_N_HASH_ALGORITHMS]; + _cleanup_close_ int fd = -EBADF; + int r; + + if (arg_pcr_mask != 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "PCR not configurable for UKI lock down."); + + if (argc >= 2) { + fd = open(argv[1], O_RDONLY|O_CLOEXEC); + if (fd < 0) + return log_error_errno(errno, "Failed to open '%s': %m", argv[1]); + } + + for (size_t i = 0; i < TPM2_N_HASH_ALGORITHMS; i++) { + _cleanup_free_ void *peh = NULL; + const EVP_MD *md; + const char *a; + + assert_se(a = tpm2_hash_alg_to_string(tpm2_hash_algorithms[i])); + assert_se(md = EVP_get_digestbyname(a)); + + r = pe_hash(fd < 0 ? STDIN_FILENO : fd, md, &peh, hash_sizes + i); + if (r < 0) + return log_error_errno(r, "Failed to hash PE binary: %m"); + + r = json_variant_append_arrayb( + &pe_digests, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("hashAlg", JSON_BUILD_STRING(a)), + JSON_BUILD_PAIR("digest", JSON_BUILD_HEX(peh, hash_sizes[i])))); + if (r < 0) + return log_error_errno(r, "Failed to build JSON digest object: %m"); + + r = uki_hash(fd < 0 ? STDIN_FILENO : fd, md, section_hashes + (i * _UNIFIED_SECTION_MAX), hash_sizes + i); + if (r < 0) + return log_error_errno(r, "Failed to UKI hash PE binary: %m"); + } + + r = json_variant_append_arrayb( + &array, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("pcr", JSON_BUILD_UNSIGNED(TPM2_PCR_BOOT_LOADER_CODE)), + JSON_BUILD_PAIR("digests", JSON_BUILD_VARIANT(pe_digests)))); + if (r < 0) + return log_error_errno(r, "Failed to append record object: %m"); + + for (UnifiedSection section = 0; section < _UNIFIED_SECTION_MAX; section++) { + _cleanup_(json_variant_unrefp) JsonVariant *section_digests = NULL, *record = NULL; + + if (!unified_section_measure(section)) + continue; + + for (size_t i = 0; i < TPM2_N_HASH_ALGORITHMS; i++) { + const char *a; + void *hash; + + hash = section_hashes[i * _UNIFIED_SECTION_MAX + section]; + if (!hash) + continue; + + assert_se(a = tpm2_hash_alg_to_string(tpm2_hash_algorithms[i])); + + r = json_variant_append_arrayb( + §ion_digests, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("hashAlg", JSON_BUILD_STRING(a)), + JSON_BUILD_PAIR("digest", JSON_BUILD_HEX(hash, hash_sizes[i])))); + if (r < 0) + return log_error_errno(r, "Failed to build JSON digest object: %m"); + } + + if (!section_digests) + continue; + + /* So we have digests for this section, hence generate a record for the section name first. */ + r = make_pcrlock_record(TPM2_PCR_KERNEL_BOOT /* =11 */, unified_sections[section], strlen(unified_sections[section]) + 1, &record); + if (r < 0) + return r; + + r = json_variant_append_array(&array, record); + if (r < 0) + return log_error_errno(r, "Failed to append JSON record array: %m"); + + /* And then apppend a record for the section contents digests as well */ + r = json_variant_append_arrayb( + &array, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("pcr", JSON_BUILD_UNSIGNED(TPM2_PCR_KERNEL_BOOT /* =11 */)), + JSON_BUILD_PAIR("digests", JSON_BUILD_VARIANT(section_digests)))); + if (r < 0) + return log_error_errno(r, "Failed to append record object: %m"); + } + + return write_pcrlock(array, NULL); +} + +static int event_log_reduce_to_safe_pcrs(EventLog *el, uint32_t *pcrs) { + _cleanup_free_ char *dropped = NULL, *kept = NULL; + + assert(el); + assert(pcrs); + + /* When we compile a new PCR policy we don't want to bind to PCRs which are fishy for one of three + * reasons: + * + * 1. The PCR value doesn't match the event log + * 2. The event log for the PCR contains measurements we don't know responsible components for + * 3. The event log for the PCR does not contain measurements for components we know + * + * This function checks for the three conditions and drops the PCR from the mask. + */ + + for (uint32_t pcr = 0; pcr < TPM2_PCRS_MAX; pcr++) { + + if (!FLAGS_SET(*pcrs, UINT32_C(1) << pcr)) + continue; + + if (!event_log_pcr_checks_out(el, el->registers + pcr)) { + log_notice("PCR %" PRIu32 " (%s) value does not match event log. Removing from set of PCRs.", pcr, strna(tpm2_pcr_index_to_string(pcr))); + goto drop; + } + + if (!el->registers[pcr].fully_recognized) { + log_notice("PCR %" PRIu32 " (%s) event log contains unrecognized measurements. Removing from set of PCRs.", pcr, strna(tpm2_pcr_index_to_string(pcr))); + goto drop; + } + + if (FLAGS_SET(el->missing_component_pcrs, UINT32_C(1) << pcr)) { + log_notice("PCR %" PRIu32 " (%s) is touched by component we can't find in event log. Removing from set of PCRs.", pcr, strna(tpm2_pcr_index_to_string(pcr))); + goto drop; + } + + log_info("PCR %" PRIu32 " (%s) matches event log and fully consists of recognized measurements. Including in set of PCRs.", pcr, strna(tpm2_pcr_index_to_string(pcr))); + + if (strextendf_with_separator(&kept, ", ", "%" PRIu32 " (%s)", pcr, tpm2_pcr_index_to_string(pcr)) < 0) + return log_oom(); + + continue; + + drop: + *pcrs &= ~(UINT32_C(1) << pcr); + + if (strextendf_with_separator(&dropped, ", ", "%" PRIu32 " (%s)", pcr, tpm2_pcr_index_to_string(pcr)) < 0) + return log_oom(); + } + + if (dropped) + log_notice("PCRs dropped from protection mask: %s", dropped); + else + log_debug("No PCRs dropped from protection mask."); + + if (kept) + log_notice("PCRs in protection mask: %s", kept); + else + log_notice("No PCRs kept in protection mask."); + + return 0; +} + +static int verb_lock_kernel_cmdline(int argc, char *argv[], void *userdata) { + _cleanup_(json_variant_unrefp) JsonVariant *record = NULL, *array = NULL; + _cleanup_free_ char *cmdline = NULL; + int r; + + if (argc > 1) { + if (empty_or_dash(argv[1])) + r = read_full_stream(stdin, &cmdline, NULL); + else + r = read_full_file(argv[1], &cmdline, NULL); + } else + r = proc_cmdline(&cmdline); + if (r < 0) + return log_error_errno(r, "Failed to read cmdline: %m"); + + delete_trailing_chars(cmdline, "\n"); + + _cleanup_free_ char16_t *u = NULL; + u = utf8_to_utf16(cmdline, SIZE_MAX); + if (!u) + return log_oom(); + + r = make_pcrlock_record(TPM2_PCR_KERNEL_INITRD /* = 9 */, u, char16_strlen(u)*2+2, &record); + if (r < 0) + return r; + + r = json_variant_new_array(&array, &record, 1); + if (r < 0) + return log_error_errno(r, "Failed to create record array: %m"); + + r = write_pcrlock(array, PCRLOCK_KERNEL_CMDLINE_PATH); + if (r < 0) + return r; + + return 0; +} + +static int verb_unlock_kernel_cmdline(int argc, char *argv[], void *userdata) { + return unlink_pcrlock(PCRLOCK_KERNEL_CMDLINE_PATH); +} + +static int verb_lock_kernel_initrd(int argc, char *argv[], void *userdata) { + _cleanup_(json_variant_unrefp) JsonVariant *record = NULL, *array = NULL; + _cleanup_free_ void *data = NULL; + _cleanup_fclose_ FILE *f = NULL; + size_t size; + int r; + + if (argc >= 2) { + f = fopen(argv[1], "re"); + if (!f) + return log_error_errno(errno, "Failed to open '%s': %m", argv[1]); + } + + r = read_full_stream(f ?: stdin, (char**) &data, &size); + if (r < 0) + return log_error_errno(r, "Failed to read data from stdin: %m"); + + r = make_pcrlock_record(TPM2_PCR_KERNEL_INITRD /* = 9 */, data, size, &record); + if (r < 0) + return r; + + r = json_variant_new_array(&array, &record, 1); + if (r < 0) + return log_error_errno(r, "Failed to create record array: %m"); + + r = write_pcrlock(array, PCRLOCK_KERNEL_INITRD_PATH); + if (r < 0) + return r; + + return 0; +} + +static int verb_unlock_kernel_initrd(int argc, char *argv[], void *userdata) { + return unlink_pcrlock(PCRLOCK_KERNEL_INITRD_PATH); +} + +static int pcr_prediction_add_result( + Tpm2PCRPrediction *context, + Tpm2PCRPredictionResult *result, + uint32_t pcr, + const char *path, + size_t offset) { + + _cleanup_free_ Tpm2PCRPredictionResult *copy = NULL; + int r; + + assert(context); + assert(result); + + copy = newdup(Tpm2PCRPredictionResult, result, 1); + if (!copy) + return log_oom(); + + r = ordered_set_ensure_put(context->results + pcr, &tpm2_pcr_prediction_result_hash_ops, copy); + if (r == -EEXIST) /* Multiple identical results for the same PCR are totally expected */ + return 0; + if (r < 0) + return log_error_errno(r, "Failed to insert result into set: %m"); + + log_debug("Added prediction result %u for PCR %" PRIu32 " (path: %s)", ordered_set_size(context->results[pcr]), pcr, strempty(path)); + + TAKE_PTR(copy); + return 0; +} + +static const EVP_MD* evp_from_tpm2_alg(uint16_t alg) { + const char *name; + + name = tpm2_hash_alg_to_string(alg); + if (!name) + return NULL; + + return EVP_get_digestbyname(name); +} + +static int event_log_component_variant_calculate( + Tpm2PCRPrediction *context, + Tpm2PCRPredictionResult *result, + EventLogComponent *component, + EventLogComponentVariant *variant, + uint32_t pcr, + const char *path) { + + int r; + + assert(context); + assert(result); + assert(component); + assert(variant); + + FOREACH_ARRAY(rr, variant->records, variant->n_records) { + EventLogRecord *rec = *rr; + + if (rec->pcr != pcr) + continue; + + for (size_t i = 0; i < TPM2_N_HASH_ALGORITHMS; i++) { + _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *md_ctx = NULL; + EventLogRecordBank *b; + + if (result->hash[i].size <= 0) /* already invalidated */ + continue; + + b = event_log_record_find_bank(rec, tpm2_hash_algorithms[i]); + if (!b) { + /* Can't calculate, hence invalidate */ + result->hash[i] = (TPM2B_DIGEST) {}; + continue; + } + + md_ctx = EVP_MD_CTX_new(); + if (!md_ctx) + return log_oom(); + + const EVP_MD *md = ASSERT_PTR(evp_from_tpm2_alg(tpm2_hash_algorithms[i])); + + int sz = EVP_MD_size(md); + assert(sz > 0); + assert((size_t) sz <= sizeof_field(TPM2B_DIGEST, buffer)); + + assert(sz == tpm2_hash_alg_to_size(tpm2_hash_algorithms[i])); + + assert(result->hash[i].size == (size_t) sz); + assert(b->hash.size == (size_t) sz); + + if (EVP_DigestInit_ex(md_ctx, md, NULL) != 1) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to initialize message digest."); + + if (EVP_DigestUpdate(md_ctx, result->hash[i].buffer, sz) != 1) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to hash bank value."); + + if (EVP_DigestUpdate(md_ctx, b->hash.buffer, sz) != 1) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to hash data value."); + + unsigned l = (unsigned) sz; + if (EVP_DigestFinal_ex(md_ctx, result->hash[i].buffer, &l) != 1) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to finalize message digest."); + + assert(l == (unsigned) sz); + } + + /* This is a valid result once we hit the start location */ + if (arg_location_start && strcmp(component->id, arg_location_start) >= 0) { + r = pcr_prediction_add_result(context, result, pcr, path, rr - variant->records); + if (r < 0) + return r; + } + } + + return 0; +} + +static int event_log_predict_pcrs( + EventLog *el, + Tpm2PCRPrediction *context, + Tpm2PCRPredictionResult *parent_result, + size_t component_index, + uint32_t pcr, + const char *path) { + + EventLogComponent *component; + int count = 0, r; + + assert(el); + assert(context); + assert(parent_result); + + /* Check if we reached the end of the components, generate a result, and backtrack */ + if (component_index >= el->n_components || + (arg_location_end && strcmp(el->components[component_index]->id, arg_location_end) > 0)) { + r = pcr_prediction_add_result(context, parent_result, pcr, path, /* offset= */ 0); + if (r < 0) + return r; + + return 1; + } + + component = ASSERT_PTR(el->components[component_index]); + + FOREACH_ARRAY(ii, component->variants, component->n_variants) { + _cleanup_free_ Tpm2PCRPredictionResult *result = NULL; + EventLogComponentVariant *variant = *ii; + _cleanup_free_ char *subpath = NULL; + + /* Operate on a copy of the result */ + + if (path) + subpath = strjoin(path, ":", component->id); + else + subpath = strdup(component->id); + if (!subpath) + return log_oom(); + + if (!streq(component->id, variant->id)) + if (!strextend(&subpath, "@", variant->id)) + return log_oom(); + + result = newdup(Tpm2PCRPredictionResult, parent_result, 1); + if (!result) + return log_oom(); + + r = event_log_component_variant_calculate( + context, + result, + component, + variant, + pcr, + subpath); + if (r < 0) + return r; + + r = event_log_predict_pcrs( + el, + context, + result, + component_index + 1, /* Next component */ + pcr, + subpath); + if (r < 0) + return r; + + count += r; + } + + return count; +} + +static ssize_t event_log_calculate_component_combinations(EventLog *el) { + ssize_t count = 1; + assert(el); + + FOREACH_ARRAY(cc, el->components, el->n_components) { + EventLogComponent *c = *cc; + + /* Overflow check */ + if (c->n_variants > (size_t) (SSIZE_MAX/count)) + return log_error_errno(SYNTHETIC_ERRNO(E2BIG), "Too many component combinations."); + + count *= c->n_variants; + } + + return count; +} + +static int event_log_show_predictions(Tpm2PCRPrediction *context, uint16_t alg) { + int r; + + assert(context); + + pager_open(arg_pager_flags); + + if (!FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF)) { + _cleanup_(json_variant_unrefp) JsonVariant *j = NULL; + + for (size_t i = 0; i < TPM2_N_HASH_ALGORITHMS; i++) { + _cleanup_(json_variant_unrefp) JsonVariant *aj = NULL; + + r = tpm2_pcr_prediction_to_json( + context, + tpm2_hash_algorithms[i], + &aj); + if (r < 0) + return r; + + if (json_variant_elements(aj) == 0) + continue; + + r = json_variant_set_field( + &j, + tpm2_hash_alg_to_string(tpm2_hash_algorithms[i]), + aj); + if (r < 0) + return log_error_errno(r, "Failed to add prediction bank to object: %m"); + } + + if (!j) { + r = json_variant_new_object(&j, NULL, 0); + if (r < 0) + return log_error_errno(r, "Failed to allocated empty object: %m"); + } + + json_variant_dump(j, arg_json_format_flags, /* f= */ NULL, /* prefix= */ NULL); + return 0; + } + + for (uint32_t pcr = 0; pcr < TPM2_PCRS_MAX; pcr++) { + Tpm2PCRPredictionResult *result; + if (!FLAGS_SET(context->pcrs, UINT32_C(1) << pcr)) + continue; + + if (ordered_set_isempty(context->results[pcr])) { + printf("No results for PCR %u (%s).\n", pcr, tpm2_pcr_index_to_string(pcr)); + continue; + } + + printf("%sResults for PCR %u (%s):%s\n", ansi_underline(), pcr, tpm2_pcr_index_to_string(pcr), ansi_normal()); + + ORDERED_SET_FOREACH(result, context->results[pcr]) { + + _cleanup_free_ char *aa = NULL, *h = NULL; + const char *a; + + TPM2B_DIGEST *hash = tpm2_pcr_prediction_result_get_hash(result, alg); + if (!hash) + continue; + + a = ASSERT_PTR(tpm2_hash_alg_to_string(alg)); + aa = strdup(a); + if (!aa) + return log_oom(); + + ascii_strlower(aa); + + h = hexmem(hash->buffer, hash->size); + if (!h) + return log_oom(); + + printf(" %s%-6s:%s %s\n", ansi_grey(), aa, ansi_normal(), h); + } + } + + return 0; +} + +static int tpm2_pcr_prediction_run( + EventLog *el, + Tpm2PCRPrediction *context) { + + int r; + + assert(el); + assert(context); + + for (uint32_t pcr = 0; pcr < TPM2_PCRS_MAX; pcr++) { + _cleanup_free_ Tpm2PCRPredictionResult *result = NULL; + + if (!FLAGS_SET(context->pcrs, UINT32_C(1) << pcr)) + continue; + + result = new0(Tpm2PCRPredictionResult, 1); + if (!result) + return log_oom(); + + for (size_t i = 0; i < TPM2_N_HASH_ALGORITHMS; i++) + event_log_initial_pcr_state(el, pcr, tpm2_hash_alg_to_size(tpm2_hash_algorithms[i]), result->hash + i); + + r = event_log_predict_pcrs( + el, + context, + result, + /* component_index= */ 0, + pcr, + /* path= */ NULL); + if (r < 0) + return r; + } + + return 0; +} + +static int verb_predict(int argc, char *argv[], void *userdata) { + _cleanup_(tpm2_pcr_prediction_done) Tpm2PCRPrediction context = { + arg_pcr_mask != 0 ? arg_pcr_mask : DEFAULT_PCR_MASK, + }; + _cleanup_(event_log_freep) EventLog *el = NULL; + ssize_t count; + int r; + + r = event_log_load_and_process(&el); + if (r < 0) + return r; + + count = event_log_calculate_component_combinations(el); + if (count < 0) + return count; + + log_info("%zi combinations of components.", count); + + r = event_log_reduce_to_safe_pcrs(el, &context.pcrs); + if (r < 0) + return r; + + r = tpm2_pcr_prediction_run(el, &context); + if (r < 0) + return r; + + return event_log_show_predictions(&context, el->primary_algorithm); +} + +static int remove_policy_file(const char *path) { + assert(path); + + if (unlink(path) < 0) { + if (errno == ENOENT) + return 0; + + return log_error_errno(errno, "Failed to remove policy file '%s': %m", path); + } + + log_info("Removed policy file '%s'.", path); + return 1; +} + +static int verb_make_policy(int argc, char *argv[], void *userdata) { + int r; + + /* Here's how this all works: after predicting all possible PCR values for next boot (with + * alternatives) we'll calculate a policy from it as a combination of PolicyPCR + PolicyOR + * expressions. This is then stored in an NV index. When a component of the boot process is changed a + * new prediction is made and the NV index updated (which automatically invalidates any older + * policies). + * + * Whenever we want to lock an encrypted object (for example FDE) against this policy, we'll use a + * PolicyAuthorizeNV epxression that pins the NV index in the policy, and permits access to any + * policies matching the current NV index contents. + * + * We grant world-readable read access to the NV index. Write access is controlled by a PIN (which we + * either generate locally or which the user can provide us with) which can also be used for + * recovery. This PIN is sealed to the TPM and is locked via PolicyAuthorizeNV to the NV index it + * protects (i.e. we dogfood 🌭 🐶 hard here). This means in order to update such a policy we need + * the policy to pass. + * + * Information about the used NV Index, the SRK of the TPM, the sealed PIN and the current PCR + * prediction data are stored in a JSON file in /var/lib/. In order to be able to unlock root disks + * this data must be also copied to the ESP so that it is available to the initrd. The data is not + * sensitive, as SRK and NV index are pinned by it, and the prediction data must match the NV index + * to be useful. */ + + usec_t start_usec = now(CLOCK_MONOTONIC); + + _cleanup_(event_log_freep) EventLog *el = NULL; + r = event_log_load_and_process(&el); + if (r < 0) + return r; + + _cleanup_(tpm2_pcr_prediction_done) Tpm2PCRPrediction new_prediction = { + arg_pcr_mask != 0 ? arg_pcr_mask : DEFAULT_PCR_MASK, + }; + r = event_log_reduce_to_safe_pcrs(el, &new_prediction.pcrs); + if (r < 0) + return r; + + usec_t predict_start_usec = now(CLOCK_MONOTONIC); + + r = tpm2_pcr_prediction_run(el, &new_prediction); + if (r < 0) + return r; + + log_info("Predicted future PCRs in %s.", FORMAT_TIMESPAN(usec_sub_unsigned(now(CLOCK_MONOTONIC), predict_start_usec), 1)); + + _cleanup_(json_variant_unrefp) JsonVariant *new_prediction_json = NULL; + r = tpm2_pcr_prediction_to_json(&new_prediction, el->primary_algorithm, &new_prediction_json); + if (r < 0) + return r; + + if (DEBUG_LOGGING) + (void) json_variant_dump(new_prediction_json, JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO, stderr, NULL); + + _cleanup_(tpm2_pcrlock_policy_done) Tpm2PCRLockPolicy old_policy = {}; + + r = tpm2_pcrlock_policy_load(arg_pcrlock_path, &old_policy); + if (r < 0) + return r; + + bool have_old_policy = r > 0; + + /* When we update the policy the old serializations for NV, SRK, PIN remain the same */ + _cleanup_(iovec_done) struct iovec + nv_blob = TAKE_STRUCT(old_policy.nv_handle), + nv_public_blob = TAKE_STRUCT(old_policy.nv_public), + srk_blob = TAKE_STRUCT(old_policy.srk_handle), + pin_public = TAKE_STRUCT(old_policy.pin_public), + pin_private = TAKE_STRUCT(old_policy.pin_private); + + if (have_old_policy) { + if (arg_nv_index != 0 && old_policy.nv_index != arg_nv_index) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Stored policy references different NV index (0x%x) than specified (0x%x), refusing.", old_policy.nv_index, arg_nv_index); + + if (!arg_force && + old_policy.algorithm == el->primary_algorithm && + tpm2_pcr_prediction_equal(&old_policy.prediction, &new_prediction, el->primary_algorithm)) { + log_info("Prediction is identical to current policy, skipping update."); + return EXIT_SUCCESS; + } + } + + _cleanup_(tpm2_context_unrefp) Tpm2Context *tc = NULL; + r = tpm2_context_new(NULL, &tc); + if (r < 0) + return log_error_errno(r, "Failed to allocate TPM2 context: %m"); + + if (!tpm2_supports_command(tc, TPM2_CC_PolicyAuthorizeNV)) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "TPM2 does not support PolicyAuthorizeNV command, refusing."); + + _cleanup_(tpm2_handle_freep) Tpm2Handle *srk_handle = NULL; + + if (iovec_is_set(&srk_blob)) { + r = tpm2_deserialize( + tc, + srk_blob.iov_base, + srk_blob.iov_len, + &srk_handle); + if (r < 0) + return log_error_errno(r, "Failed to deserialize SRK TR: %m"); + } else { + r = tpm2_get_or_create_srk( + tc, + /* session= */ NULL, + /* ret_public= */ NULL, + /* ret_name= */ NULL, + /* ret_qname= */ NULL, + &srk_handle); + if (r < 0) + return log_error_errno(r, "Failed to install SRK: %m"); + } + + _cleanup_(tpm2_handle_freep) Tpm2Handle *encryption_session = NULL; + r = tpm2_make_encryption_session( + tc, + srk_handle, + /* bind_key= */ &TPM2_HANDLE_NONE, + &encryption_session); + if (r < 0) + return log_error_errno(r, "Failed to allocate encryption session: %m"); + + /* Acquire a recovery PIN, either from the user, or create a randomized one */ + _cleanup_(erase_and_freep) char *pin = NULL; + if (arg_recovery_pin) { + r = getenv_steal_erase("PIN", &pin); + if (r < 0) + return log_error_errno(r, "Failed to acquire PIN from environment: %m"); + if (r == 0) { + _cleanup_(strv_free_erasep) char **l = NULL; + + r = ask_password_auto( + "Recovery PIN", + /* icon= */ NULL, + /* id= */ "pcrlock-recovery-pin", + /* key_name= */ NULL, + /* credential_name= */ "systemd-pcrlock.recovery-pin", + /* until= */ 0, + /* flags= */ 0, + &l); + if (r < 0) + return log_error_errno(r, "Failed to query for recovery PIN: %m"); + + if (strv_length(l) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected a single PIN only."); + + pin = TAKE_PTR(l[0]); + l = mfree(l); + } + + } else if (!have_old_policy) { + char rnd[256]; + + r = crypto_random_bytes(rnd, sizeof(rnd)); + if (r < 0) + return log_error_errno(r, "Failed to generate a randomized recovery PIN: %m"); + + (void) base64mem(rnd, sizeof(rnd), &pin); + explicit_bzero_safe(rnd, sizeof(rnd)); + if (!pin) + return log_oom(); + } + + _cleanup_(tpm2_handle_freep) Tpm2Handle *nv_handle = NULL; + TPM2_HANDLE nv_index = 0; + + if (iovec_is_set(&nv_blob)) { + r = tpm2_deserialize(tc, nv_blob.iov_base, nv_blob.iov_len, &nv_handle); + if (r < 0) + return log_error_errno(r, "Failed to deserialize NV index TR: %m"); + + nv_index = old_policy.nv_index; + } + + TPM2B_AUTH auth = {}; + CLEANUP_ERASE(auth); + + if (pin) { + r = tpm2_get_pin_auth(TPM2_ALG_SHA256, pin, &auth); + if (r < 0) + return log_error_errno(r, "Failed to hash PIN: %m"); + } else { + assert(iovec_is_set(&pin_public)); + assert(iovec_is_set(&pin_private)); + + log_debug("Retrieving PIN from sealed data."); + + usec_t pin_start_usec = now(CLOCK_MONOTONIC); + + _cleanup_(iovec_done_erase) struct iovec secret = {}; + for (unsigned attempt = 0;; attempt++) { + _cleanup_(tpm2_handle_freep) Tpm2Handle *policy_session = NULL; + + r = tpm2_make_policy_session( + tc, + srk_handle, + encryption_session, + &policy_session); + if (r < 0) + return log_error_errno(r, "Failed to allocate policy session: %m"); + + r = tpm2_policy_super_pcr( + tc, + policy_session, + &old_policy.prediction, + old_policy.algorithm); + if (r < 0) + return log_error_errno(r, "Failed to submit super PCR policy: %m"); + + r = tpm2_policy_authorize_nv( + tc, + policy_session, + nv_handle, + NULL); + if (r < 0) + return log_error_errno(r, "Failed to submit AuthorizeNV policy: %m"); + + r = tpm2_unseal_data( + tc, + &pin_public, + &pin_private, + srk_handle, + policy_session, + encryption_session, + &secret); + if (r < 0 && (r != -ESTALE || attempt >= 16)) + return log_error_errno(r, "Failed to unseal PIN: %m"); + if (r == 0) + break; + + log_debug("Trying again (attempt %u), as PCR values changed during unlock attempt.", attempt+1); + } + + if (secret.iov_len > sizeof_field(TPM2B_AUTH, buffer)) + return log_error_errno(SYNTHETIC_ERRNO(E2BIG), "Decrypted PIN too large."); + + auth = (TPM2B_AUTH) { + .size = secret.iov_len, + }; + + memcpy_safe(auth.buffer, secret.iov_base, secret.iov_len); + + log_info("Retrieved PIN from TPM2 in %s.", FORMAT_TIMESPAN(usec_sub_unsigned(now(CLOCK_MONOTONIC), pin_start_usec), 1)); + } + + TPM2B_NV_PUBLIC nv_public = {}; + + usec_t nv_index_start_usec = now(CLOCK_MONOTONIC); + + if (!iovec_is_set(&nv_blob)) { + TPM2B_DIGEST recovery_policy_digest = TPM2B_DIGEST_MAKE(NULL, TPM2_SHA256_DIGEST_SIZE); + r = tpm2_calculate_policy_auth_value(&recovery_policy_digest); + if (r < 0) + return log_error_errno(r, "Failed to calculate authentication value policy: %m"); + + log_debug("Allocating NV index to write PCR policy to..."); + r = tpm2_define_policy_nv_index( + tc, + encryption_session, + arg_nv_index, + &recovery_policy_digest, + pin, + &auth, + &nv_index, + &nv_handle, + &nv_public); + if (r == -EEXIST) + return log_error_errno(r, "NV index 0x%" PRIx32 " already allocated.", arg_nv_index); + if (r < 0) + return log_error_errno(r, "Failed to allocate NV index: %m"); + } + + r = tpm2_set_auth_binary(tc, nv_handle, &auth); + if (r < 0) + return log_error_errno(r, "Failed to set authentication value on NV index: %m"); + + _cleanup_(tpm2_handle_freep) Tpm2Handle *policy_session = NULL; + r = tpm2_make_policy_session( + tc, + srk_handle, + encryption_session, + &policy_session); + if (r < 0) + return log_error_errno(r, "Failed to allocate policy session: %m"); + + r = tpm2_policy_auth_value( + tc, + policy_session, + /* ret_policy_digest= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to submit authentication value policy: %m"); + + log_debug("Calculating new PCR policy to write..."); + TPM2B_DIGEST new_super_pcr_policy_digest = TPM2B_DIGEST_MAKE(NULL, TPM2_SHA256_DIGEST_SIZE); + + usec_t pcr_policy_start_usec = now(CLOCK_MONOTONIC); + + r = tpm2_calculate_policy_super_pcr( + &new_prediction, + el->primary_algorithm, + &new_super_pcr_policy_digest); + if (r < 0) + return log_error_errno(r, "Failed to calculate super PCR policy: %m"); + + log_info("Calculated new PCR policy in %s.", FORMAT_TIMESPAN(usec_sub_unsigned(now(CLOCK_MONOTONIC), pcr_policy_start_usec), 1)); + + log_debug("Writing new PCR policy to NV index..."); + r = tpm2_write_policy_nv_index( + tc, + policy_session, + nv_index, + nv_handle, + &new_super_pcr_policy_digest); + if (r < 0) + return log_error_errno(r, "Failed to write to NV index: %m"); + + log_info("Updated NV index in %s.", FORMAT_TIMESPAN(usec_sub_unsigned(now(CLOCK_MONOTONIC), nv_index_start_usec), 1)); + + assert(iovec_is_set(&pin_public) == iovec_is_set(&pin_private)); + if (!iovec_is_set(&pin_public)) { + TPM2B_DIGEST authnv_policy_digest = TPM2B_DIGEST_MAKE(NULL, TPM2_SHA256_DIGEST_SIZE); + + r = tpm2_calculate_policy_authorize_nv(&nv_public, &authnv_policy_digest); + if (r < 0) + return log_error_errno(r, "Failed to calculate AuthorizeNV policy: %m"); + + struct iovec data = { + .iov_base = auth.buffer, + .iov_len = auth.size, + }; + + usec_t pin_seal_start_usec = now(CLOCK_MONOTONIC); + + log_debug("Sealing PIN to NV index policy..."); + r = tpm2_seal_data( + tc, + &data, + srk_handle, + encryption_session, + &authnv_policy_digest, + &pin_public, + &pin_private); + if (r < 0) + return log_error_errno(r, "Failed to seal PIN to NV auth policy: %m"); + + log_info("Sealed PIN in %s.", FORMAT_TIMESPAN(usec_sub_unsigned(now(CLOCK_MONOTONIC), pin_seal_start_usec), 1)); + } + + if (!iovec_is_set(&nv_blob)) { + r = tpm2_serialize(tc, nv_handle, &nv_blob.iov_base, &nv_blob.iov_len); + if (r < 0) + return log_error_errno(r, "Failed to serialize NV index TR: %m"); + } + + if (!iovec_is_set(&srk_blob)) { + r = tpm2_serialize(tc, srk_handle, &srk_blob.iov_base, &srk_blob.iov_len); + if (r < 0) + return log_error_errno(r, "Failed to serialize SRK index TR: %m"); + } + + if (!iovec_is_set(&nv_public_blob)) { + r = tpm2_marshal_nv_public(&nv_public, &nv_public_blob.iov_base, &nv_public_blob.iov_len); + if (r < 0) + return log_error_errno(r, "Failed to marshal NV public area: %m"); + } + + _cleanup_(json_variant_unrefp) JsonVariant *new_configuration_json = NULL; + r = json_build(&new_configuration_json, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_STRING("pcrBank", tpm2_hash_alg_to_string(el->primary_algorithm)), + JSON_BUILD_PAIR_VARIANT("pcrValues", new_prediction_json), + JSON_BUILD_PAIR_INTEGER("nvIndex", nv_index), + JSON_BUILD_PAIR_IOVEC_BASE64("nvHandle", &nv_blob), + JSON_BUILD_PAIR_IOVEC_BASE64("nvPublic", &nv_public_blob), + JSON_BUILD_PAIR_IOVEC_BASE64("srkHandle", &srk_blob), + JSON_BUILD_PAIR_IOVEC_BASE64("pinPublic", &pin_public), + JSON_BUILD_PAIR_IOVEC_BASE64("pinPrivate", &pin_private))); + if (r < 0) + return log_error_errno(r, "Failed to generate JSON: %m"); + + _cleanup_free_ char *text = NULL; + r = json_variant_format(new_configuration_json, 0, &text); + if (r < 0) + return log_error_errno(r, "Failed to format new configuration to JSON: %m"); + + const char *path = arg_pcrlock_path ?: (in_initrd() ? "/run/systemd/pcrlock.json" : "/var/lib/systemd/pcrlock.json"); + r = write_string_file(path, text, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_SYNC|WRITE_STRING_FILE_MKDIR_0755); + if (r < 0) + return log_error_errno(r, "Failed to write new configuration to '%s': %m", path); + + if (!arg_pcrlock_path && !in_initrd()) { + r = remove_policy_file("/run/systemd/pcrlock.json"); + if (r < 0) + return r; + } + + log_info("Written new policy to '%s' and digest to TPM2 NV index 0x%" PRIu32 ".", path, nv_index); + + log_info("Overall time spent: %s", FORMAT_TIMESPAN(usec_sub_unsigned(now(CLOCK_MONOTONIC), start_usec), 1)); + + return 0; +} + +static int undefine_policy_nv_index( + uint32_t nv_index, + const struct iovec *nv_blob, + const struct iovec *srk_blob) { + int r; + + assert(nv_blob); + assert(srk_blob); + + _cleanup_(tpm2_context_unrefp) Tpm2Context *tc = NULL; + r = tpm2_context_new(NULL, &tc); + if (r < 0) + return r; + + _cleanup_(tpm2_handle_freep) Tpm2Handle *srk_handle = NULL; + r = tpm2_deserialize( + tc, + srk_blob->iov_base, + srk_blob->iov_len, + &srk_handle); + if (r < 0) + return log_error_errno(r, "Failed to deserialize SRK TR: %m"); + + _cleanup_(tpm2_handle_freep) Tpm2Handle *nv_handle = NULL; + r = tpm2_deserialize( + tc, + nv_blob->iov_base, + nv_blob->iov_len, + &nv_handle); + if (r < 0) + return log_error_errno(r, "Failed to deserialize NV TR: %m"); + + _cleanup_(tpm2_handle_freep) Tpm2Handle *encryption_session = NULL; + r = tpm2_make_encryption_session( + tc, + srk_handle, + /* bind_key= */ &TPM2_HANDLE_NONE, + &encryption_session); + if (r < 0) + return r; + + r = tpm2_undefine_policy_nv_index( + tc, + encryption_session, + nv_index, + nv_handle); + if (r < 0) + return r; + + log_info("Removed NV index 0x%x", nv_index); + return 0; +} + +static int verb_remove_policy(int argc, char *argv[], void *userdata) { + int r; + + _cleanup_(tpm2_pcrlock_policy_done) Tpm2PCRLockPolicy policy = {}; + r = tpm2_pcrlock_policy_load(arg_policy_path, &policy); + if (r == 0) { + log_info("No policy found."); + return 0; + } + + if (r < 0) + log_notice("Failed to load old policy file, assuming it is corrupted, removing."); + else { + r = undefine_policy_nv_index(policy.nv_index, &policy.nv_handle, &policy.srk_handle); + if (r < 0) + log_notice("Failed to remove NV index, assuming data out of date, removing policy file."); + } + + if (arg_policy_path) { + r = remove_policy_file(arg_policy_path); + if (r < 0) + return r; + + return 0; + } else { + int ret = 0; + + RET_GATHER(ret, remove_policy_file("/var/lib/systemd/pcrlock.json")); + RET_GATHER(ret, remove_policy_file("/run/systemd/pcrlock.json")); + + return ret; + } +} + +static int help(int argc, char *argv[], void *userdata) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("systemd-pcrlock", "8", &link); + if (r < 0) + return log_oom(); + + printf("%1$s [OPTIONS...] COMMAND ...\n" + "\n%5$sManage a TPM2 PCR lock.%6$s\n" + "\n%3$sCommands:%4$s\n" + " log Show measurement log\n" + " cel Show measurement log in TCG CEL-JSON format\n" + " list-components List defined .pcrlock components\n" + " predict Predict PCR values\n" + " make-policy Predict PCR values and generate TPM2 policy from it\n" + " remove-policy Remove TPM2 policy\n" + "\n%3$sProtections:%4$s\n" + " lock-firmware-code Generate a .pcrlock file from current firmware code\n" + " unlock-firmware-code Remove .pcrlock file for firmware code\n" + " lock-firmware-config Generate a .pcrlock file from current firmware configuration\n" + " unlock-firmware-config Remove .pcrlock file for firmware configuration\n" + " lock-secureboot-policy Generate a .pcrlock file from current SecureBoot policy\n" + " unlock-secureboot-policy Remove .pcrlock file for SecureBoot policy\n" + " lock-secureboot-authority Generate a .pcrlock file from current SecureBoot authority\n" + " unlock-secureboot-authority Remove .pcrlock file for SecureBoot authority\n" + " lock-gpt [DISK] Generate a .pcrlock file from GPT header\n" + " unlock-gpt Remove .pcrlock file for GPT header\n" + " lock-pe [BINARY] Generate a .pcrlock file from PE binary\n" + " unlock-pe Remove .pcrlock file for PE binary\n" + " lock-uki [UKI] Generate a .pcrlock file from UKI PE binary\n" + " unlock-uki Remove .pcrlock file for UKI PE binary\n" + " lock-machine-id Generate a .pcrlock file from current machine ID\n" + " unlock-machine-id Remove .pcrlock file for machine ID\n" + " lock-file-system [PATH] Generate a .pcrlock file from current root fs + /var/\n" + " unlock-file-system [PATH] Remove .pcrlock file for root fs + /var/\n" + " lock-kernel-cmdline [FILE] Generate a .pcrlock file from kernel command line\n" + " unlock-kernel-cmdline Remove .pcrlock file for kernel command line\n" + " lock-kernel-initrd FILE Generate a .pcrlock file from an initrd file\n" + " unlock-kernel-initrd Remove .pcrlock file for an initrd file\n" + " lock-raw [FILE] Generate a .pcrlock file from raw data\n" + " unlock-raw Remove .pcrlock file for raw data\n" + "\n%3$sOptions:%4$s\n" + " -h --help Show this help\n" + " --version Print version\n" + " --no-pager Do not pipe output into a pager\n" + " --json=pretty|short|off Generate JSON output\n" + " --raw-description Show raw firmware record data as description in table\n" + " --pcr=NR Generate .pcrlock for specified PCR\n" + " --nv-index=NUMBER Use the specified NV index, instead of a random one\n" + " --components=PATH Directory to read .pcrlock files from\n" + " --location=STRING[:STRING]\n" + " Do not process components beyond this component name\n" + " --recovery-pin=yes Ask for a recovery PIN\n" + " --pcrlock=PATH .pcrlock file to write expected PCR measurement to\n" + " --policy=PATH JSON file to write policy output to\n" + " --force Write policy even if it matches existing policy\n" + "\nSee the %2$s for details.\n", + program_invocation_short_name, + link, + ansi_underline(), + ansi_normal(), + ansi_highlight(), + ansi_normal()); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + enum { + ARG_VERSION = 0x100, + ARG_NO_PAGER, + ARG_JSON, + ARG_RAW_DESCRIPTION, + ARG_PCR, + ARG_NV_INDEX, + ARG_COMPONENTS, + ARG_LOCATION, + ARG_RECOVERY_PIN, + ARG_PCRLOCK, + ARG_POLICY, + ARG_FORCE, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "no-pager", no_argument, NULL, ARG_NO_PAGER }, + { "json", required_argument, NULL, ARG_JSON }, + { "raw-description", no_argument, NULL, ARG_RAW_DESCRIPTION }, + { "pcr", required_argument, NULL, ARG_PCR }, + { "nv-index", required_argument, NULL, ARG_NV_INDEX }, + { "components", required_argument, NULL, ARG_COMPONENTS }, + { "location", required_argument, NULL, ARG_LOCATION }, + { "recovery-pin", required_argument, NULL, ARG_RECOVERY_PIN }, + { "pcrlock", required_argument, NULL, ARG_PCRLOCK }, + { "policy", required_argument, NULL, ARG_POLICY }, + { "force", no_argument, NULL, ARG_FORCE }, + {} + }; + + bool auto_location = true; + int c, r; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) + switch (c) { + + case 'h': + help(0, NULL, NULL); + return 0; + + case ARG_VERSION: + return version(); + + case ARG_NO_PAGER: + arg_pager_flags |= PAGER_DISABLE; + break; + + case ARG_JSON: + r = parse_json_argument(optarg, &arg_json_format_flags); + if (r <= 0) + return r; + break; + + case ARG_RAW_DESCRIPTION: + arg_raw_description = true; + break; + + case ARG_PCR: { + r = tpm2_parse_pcr_argument_to_mask(optarg, &arg_pcr_mask); + if (r < 0) + return log_error_errno(r, "Failed to parse PCR specification: %s", optarg); + + break; + } + + case ARG_NV_INDEX: + if (isempty(optarg)) + arg_nv_index = 0; + else { + uint32_t u; + + r = safe_atou32_full(optarg, 16, &u); + if (r < 0) + return log_error_errno(r, "Failed to parse --nv-index= argument: %s", optarg); + + if (u < TPM2_NV_INDEX_FIRST || u > TPM2_NV_INDEX_LAST) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Argument for --nv-index= outside of valid range 0x%" PRIx32 "…0x%" PRIx32 ": 0x%" PRIx32, + TPM2_NV_INDEX_FIRST, TPM2_NV_INDEX_LAST, u); + + arg_nv_index = u; + } + break; + + case ARG_COMPONENTS: { + _cleanup_free_ char *p = NULL; + + r = parse_path_argument(optarg, /* suppress_root= */ false, &p); + if (r < 0) + return r; + + r = strv_consume(&arg_components, TAKE_PTR(p)); + if (r < 0) + return log_oom(); + + break; + } + + case ARG_LOCATION: { + _cleanup_free_ char *start = NULL, *end = NULL; + const char *e; + + auto_location = false; + + if (isempty(optarg)) { + arg_location_start = mfree(arg_location_start); + arg_location_end = mfree(arg_location_end); + break; + } + + e = strchr(optarg, ':'); + if (e) { + start = strndup(optarg, e - optarg); + if (!start) + return log_oom(); + + end = strdup(e + 1); + if (!end) + return log_oom(); + } else { + start = strdup(optarg); + if (!start) + return log_oom(); + + end = strdup(optarg); + if (!end) + return log_oom(); + } + + if (!filename_is_valid(start)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Location string invalid, refusing: %s", start); + if (!filename_is_valid(end)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Location string invalid, refusing: %s", end); + + free_and_replace(arg_location_start, start); + free_and_replace(arg_location_end, end); + break; + } + + case ARG_RECOVERY_PIN: + r = parse_boolean_argument("--recovery-pin", optarg, &arg_recovery_pin); + if (r < 0) + return r; + break; + + case ARG_PCRLOCK: + if (isempty(optarg) || streq(optarg, "-")) + arg_pcrlock_path = mfree(arg_pcrlock_path); + else { + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_pcrlock_path); + if (r < 0) + return r; + } + + arg_pcrlock_auto = false; + break; + + case ARG_POLICY: + if (isempty(optarg) || streq(optarg, "-")) + arg_policy_path = mfree(arg_policy_path); + else { + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_policy_path); + if (r < 0) + return r; + } + + break; + + case ARG_FORCE: + arg_force = true; + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + + if (auto_location) { + assert(!arg_location_start); + assert(!arg_location_end); + + arg_location_start = strdup("760-"); + if (!arg_location_start) + return log_oom(); + + arg_location_end = strdup("940-"); + if (!arg_location_end) + return log_oom(); + } + + return 1; +} + +static int pcrlock_main(int argc, char *argv[]) { + static const Verb verbs[] = { + { "help", VERB_ANY, VERB_ANY, 0, help }, + { "log", VERB_ANY, 1, VERB_DEFAULT, verb_show_log }, + { "cel", VERB_ANY, 1, 0, verb_show_cel }, + { "list-components", VERB_ANY, 1, 0, verb_list_components }, + { "predict", VERB_ANY, 1, 0, verb_predict }, + { "lock-firmware-code", VERB_ANY, 2, 0, verb_lock_firmware }, + { "unlock-firmware-code", VERB_ANY, 1, 0, verb_unlock_firmware }, + { "lock-firmware-config", VERB_ANY, 2, 0, verb_lock_firmware }, + { "unlock-firmware-config", VERB_ANY, 1, 0, verb_unlock_firmware }, + { "lock-secureboot-policy", VERB_ANY, 1, 0, verb_lock_secureboot_policy }, + { "unlock-secureboot-policy", VERB_ANY, 1, 0, verb_unlock_secureboot_policy }, + { "lock-secureboot-authority", VERB_ANY, 1, 0, verb_lock_secureboot_authority }, + { "unlock-secureboot-authority", VERB_ANY, 1, 0, verb_unlock_secureboot_authority }, + { "lock-gpt", VERB_ANY, 2, 0, verb_lock_gpt }, + { "unlock-gpt", VERB_ANY, 1, 0, verb_unlock_gpt }, + { "lock-pe", VERB_ANY, 2, 0, verb_lock_pe }, + { "unlock-pe", VERB_ANY, 1, 0, verb_unlock_simple }, + { "lock-uki", VERB_ANY, 2, 0, verb_lock_uki }, + { "unlock-uki", VERB_ANY, 1, 0, verb_unlock_simple }, + { "lock-machine-id", VERB_ANY, 1, 0, verb_lock_machine_id }, + { "unlock-machine-id", VERB_ANY, 1, 0, verb_unlock_machine_id }, + { "lock-file-system", VERB_ANY, 2, 0, verb_lock_file_system }, + { "unlock-file-system", VERB_ANY, 2, 0, verb_unlock_file_system }, + { "lock-kernel-cmdline", VERB_ANY, 2, 0, verb_lock_kernel_cmdline }, + { "unlock-kernel-cmdline", VERB_ANY, 1, 0, verb_unlock_kernel_cmdline }, + { "lock-kernel-initrd", VERB_ANY, 2, 0, verb_lock_kernel_initrd }, + { "unlock-kernel-initrd", VERB_ANY, 1, 0, verb_unlock_kernel_initrd }, + { "lock-raw", VERB_ANY, 2, 0, verb_lock_raw }, + { "unlock-raw", VERB_ANY, 1, 0, verb_unlock_simple }, + { "make-policy", VERB_ANY, 1, 0, verb_make_policy }, + { "remove-policy", VERB_ANY, 1, 0, verb_remove_policy }, + {} + }; + + return dispatch_verb(argc, argv, verbs, NULL); +} + +static int run(int argc, char *argv[]) { + int r; + + log_show_color(true); + log_parse_environment(); + log_open(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + return pcrlock_main(argc, argv); +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/pcrlock/pcrlock.d/350-action-efi-application.pcrlock b/src/pcrlock/pcrlock.d/350-action-efi-application.pcrlock new file mode 100644 index 0000000000..2baaa9ceb1 --- /dev/null +++ b/src/pcrlock/pcrlock.d/350-action-efi-application.pcrlock @@ -0,0 +1 @@ +{"records":[{"pcr":4,"digests":[{"hashAlg":"sha1","digest":"cd0fdb4531a6ec41be2753ba042637d6e5f7f256"},{"hashAlg":"sha256","digest":"3d6772b4f84ed47595d72a2c4c5ffd15f5bb72c7507fe26f2aaee2c69d5633ba"},{"hashAlg":"sha384","digest":"77a0dab2312b4e1e57a84d865a21e5b2ee8d677a21012ada819d0a98988078d3d740f6346bfe0abaa938ca20439a8d71"},{"hashAlg":"sha512","digest":"03020279c5ea3676d6630c82a9931343225e8eab81529b65c786aeb6a445d3852a34dd193178f938b6b47345a72d4b647df309c971f7c02f0ede296a136a1086"}]}]} diff --git a/src/pcrlock/pcrlock.d/400-secureboot-separator.pcrlock.d/300-0x00000000.pcrlock b/src/pcrlock/pcrlock.d/400-secureboot-separator.pcrlock.d/300-0x00000000.pcrlock new file mode 100644 index 0000000000..c577c9874b --- /dev/null +++ b/src/pcrlock/pcrlock.d/400-secureboot-separator.pcrlock.d/300-0x00000000.pcrlock @@ -0,0 +1 @@ +{"records":[{"pcr":7,"digests":[{"hashAlg":"sha1","digest":"9069ca78e7450a285173431b3e52c5c25299e473"},{"hashAlg":"sha256","digest":"df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119"},{"hashAlg":"sha384","digest":"394341b7182cd227c5c6b07ef8000cdfd86136c4292b8e576573ad7ed9ae41019f5818b4b971c9effc60e1ad9f1289f0"},{"hashAlg":"sha512","digest":"ec2d57691d9b2d40182ac565032054b7d784ba96b18bcb5be0bb4e70e3fb041eff582c8af66ee50256539f2181d7f9e53627c0189da7e75a4d5ef10ea93b20b3"}]}]} diff --git a/src/pcrlock/pcrlock.d/400-secureboot-separator.pcrlock.d/600-0xffffffff.pcrlock b/src/pcrlock/pcrlock.d/400-secureboot-separator.pcrlock.d/600-0xffffffff.pcrlock new file mode 100644 index 0000000000..2e86898c99 --- /dev/null +++ b/src/pcrlock/pcrlock.d/400-secureboot-separator.pcrlock.d/600-0xffffffff.pcrlock @@ -0,0 +1 @@ +{"records":[{"pcr":7,"digests":[{"hashAlg":"sha1","digest":"d9be6524a5f5047db5866813acf3277892a7a30a"},{"hashAlg":"sha256","digest":"ad95131bc0b799c0b1af477fb14fcf26a6a9f76079e48bf090acb7e8367bfd0e"},{"hashAlg":"sha384","digest":"4a06b879c7eedbe01c945d46b5bd785b59203dce81ea6a1206c28091ca285365f760d9167778f0dc1763d4854aafd40a"},{"hashAlg":"sha512","digest":"ea71bb243b0b2db729b9eb88e3c55a3f490fbff23457825051224a1fe6e6d3f480590cfa3a4a6b12c622d6ac366feb03cd17004ed004cb3f0d52731626946679"}]}]} diff --git a/src/pcrlock/pcrlock.d/500-separator.pcrlock.d/300-0x00000000.pcrlock b/src/pcrlock/pcrlock.d/500-separator.pcrlock.d/300-0x00000000.pcrlock new file mode 100644 index 0000000000..f1e473f1a3 --- /dev/null +++ b/src/pcrlock/pcrlock.d/500-separator.pcrlock.d/300-0x00000000.pcrlock @@ -0,0 +1 @@ +{"records":[{"pcr":0,"digests":[{"hashAlg":"sha1","digest":"9069ca78e7450a285173431b3e52c5c25299e473"},{"hashAlg":"sha256","digest":"df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119"},{"hashAlg":"sha384","digest":"394341b7182cd227c5c6b07ef8000cdfd86136c4292b8e576573ad7ed9ae41019f5818b4b971c9effc60e1ad9f1289f0"},{"hashAlg":"sha512","digest":"ec2d57691d9b2d40182ac565032054b7d784ba96b18bcb5be0bb4e70e3fb041eff582c8af66ee50256539f2181d7f9e53627c0189da7e75a4d5ef10ea93b20b3"}]},{"pcr":1,"digests":[{"hashAlg":"sha1","digest":"9069ca78e7450a285173431b3e52c5c25299e473"},{"hashAlg":"sha256","digest":"df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119"},{"hashAlg":"sha384","digest":"394341b7182cd227c5c6b07ef8000cdfd86136c4292b8e576573ad7ed9ae41019f5818b4b971c9effc60e1ad9f1289f0"},{"hashAlg":"sha512","digest":"ec2d57691d9b2d40182ac565032054b7d784ba96b18bcb5be0bb4e70e3fb041eff582c8af66ee50256539f2181d7f9e53627c0189da7e75a4d5ef10ea93b20b3"}]},{"pcr":2,"digests":[{"hashAlg":"sha1","digest":"9069ca78e7450a285173431b3e52c5c25299e473"},{"hashAlg":"sha256","digest":"df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119"},{"hashAlg":"sha384","digest":"394341b7182cd227c5c6b07ef8000cdfd86136c4292b8e576573ad7ed9ae41019f5818b4b971c9effc60e1ad9f1289f0"},{"hashAlg":"sha512","digest":"ec2d57691d9b2d40182ac565032054b7d784ba96b18bcb5be0bb4e70e3fb041eff582c8af66ee50256539f2181d7f9e53627c0189da7e75a4d5ef10ea93b20b3"}]},{"pcr":3,"digests":[{"hashAlg":"sha1","digest":"9069ca78e7450a285173431b3e52c5c25299e473"},{"hashAlg":"sha256","digest":"df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119"},{"hashAlg":"sha384","digest":"394341b7182cd227c5c6b07ef8000cdfd86136c4292b8e576573ad7ed9ae41019f5818b4b971c9effc60e1ad9f1289f0"},{"hashAlg":"sha512","digest":"ec2d57691d9b2d40182ac565032054b7d784ba96b18bcb5be0bb4e70e3fb041eff582c8af66ee50256539f2181d7f9e53627c0189da7e75a4d5ef10ea93b20b3"}]},{"pcr":4,"digests":[{"hashAlg":"sha1","digest":"9069ca78e7450a285173431b3e52c5c25299e473"},{"hashAlg":"sha256","digest":"df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119"},{"hashAlg":"sha384","digest":"394341b7182cd227c5c6b07ef8000cdfd86136c4292b8e576573ad7ed9ae41019f5818b4b971c9effc60e1ad9f1289f0"},{"hashAlg":"sha512","digest":"ec2d57691d9b2d40182ac565032054b7d784ba96b18bcb5be0bb4e70e3fb041eff582c8af66ee50256539f2181d7f9e53627c0189da7e75a4d5ef10ea93b20b3"}]},{"pcr":5,"digests":[{"hashAlg":"sha1","digest":"9069ca78e7450a285173431b3e52c5c25299e473"},{"hashAlg":"sha256","digest":"df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119"},{"hashAlg":"sha384","digest":"394341b7182cd227c5c6b07ef8000cdfd86136c4292b8e576573ad7ed9ae41019f5818b4b971c9effc60e1ad9f1289f0"},{"hashAlg":"sha512","digest":"ec2d57691d9b2d40182ac565032054b7d784ba96b18bcb5be0bb4e70e3fb041eff582c8af66ee50256539f2181d7f9e53627c0189da7e75a4d5ef10ea93b20b3"}]},{"pcr":6,"digests":[{"hashAlg":"sha1","digest":"9069ca78e7450a285173431b3e52c5c25299e473"},{"hashAlg":"sha256","digest":"df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119"},{"hashAlg":"sha384","digest":"394341b7182cd227c5c6b07ef8000cdfd86136c4292b8e576573ad7ed9ae41019f5818b4b971c9effc60e1ad9f1289f0"},{"hashAlg":"sha512","digest":"ec2d57691d9b2d40182ac565032054b7d784ba96b18bcb5be0bb4e70e3fb041eff582c8af66ee50256539f2181d7f9e53627c0189da7e75a4d5ef10ea93b20b3"}]}]} diff --git a/src/pcrlock/pcrlock.d/500-separator.pcrlock.d/600-0xffffffff.pcrlock b/src/pcrlock/pcrlock.d/500-separator.pcrlock.d/600-0xffffffff.pcrlock new file mode 100644 index 0000000000..0b8d20b9c0 --- /dev/null +++ b/src/pcrlock/pcrlock.d/500-separator.pcrlock.d/600-0xffffffff.pcrlock @@ -0,0 +1 @@ +{"records":[{"pcr":0,"digests":[{"hashAlg":"sha1","digest":"d9be6524a5f5047db5866813acf3277892a7a30a"},{"hashAlg":"sha256","digest":"ad95131bc0b799c0b1af477fb14fcf26a6a9f76079e48bf090acb7e8367bfd0e"},{"hashAlg":"sha384","digest":"4a06b879c7eedbe01c945d46b5bd785b59203dce81ea6a1206c28091ca285365f760d9167778f0dc1763d4854aafd40a"},{"hashAlg":"sha512","digest":"ea71bb243b0b2db729b9eb88e3c55a3f490fbff23457825051224a1fe6e6d3f480590cfa3a4a6b12c622d6ac366feb03cd17004ed004cb3f0d52731626946679"}]},{"pcr":1,"digests":[{"hashAlg":"sha1","digest":"d9be6524a5f5047db5866813acf3277892a7a30a"},{"hashAlg":"sha256","digest":"ad95131bc0b799c0b1af477fb14fcf26a6a9f76079e48bf090acb7e8367bfd0e"},{"hashAlg":"sha384","digest":"4a06b879c7eedbe01c945d46b5bd785b59203dce81ea6a1206c28091ca285365f760d9167778f0dc1763d4854aafd40a"},{"hashAlg":"sha512","digest":"ea71bb243b0b2db729b9eb88e3c55a3f490fbff23457825051224a1fe6e6d3f480590cfa3a4a6b12c622d6ac366feb03cd17004ed004cb3f0d52731626946679"}]},{"pcr":2,"digests":[{"hashAlg":"sha1","digest":"d9be6524a5f5047db5866813acf3277892a7a30a"},{"hashAlg":"sha256","digest":"ad95131bc0b799c0b1af477fb14fcf26a6a9f76079e48bf090acb7e8367bfd0e"},{"hashAlg":"sha384","digest":"4a06b879c7eedbe01c945d46b5bd785b59203dce81ea6a1206c28091ca285365f760d9167778f0dc1763d4854aafd40a"},{"hashAlg":"sha512","digest":"ea71bb243b0b2db729b9eb88e3c55a3f490fbff23457825051224a1fe6e6d3f480590cfa3a4a6b12c622d6ac366feb03cd17004ed004cb3f0d52731626946679"}]},{"pcr":3,"digests":[{"hashAlg":"sha1","digest":"d9be6524a5f5047db5866813acf3277892a7a30a"},{"hashAlg":"sha256","digest":"ad95131bc0b799c0b1af477fb14fcf26a6a9f76079e48bf090acb7e8367bfd0e"},{"hashAlg":"sha384","digest":"4a06b879c7eedbe01c945d46b5bd785b59203dce81ea6a1206c28091ca285365f760d9167778f0dc1763d4854aafd40a"},{"hashAlg":"sha512","digest":"ea71bb243b0b2db729b9eb88e3c55a3f490fbff23457825051224a1fe6e6d3f480590cfa3a4a6b12c622d6ac366feb03cd17004ed004cb3f0d52731626946679"}]},{"pcr":4,"digests":[{"hashAlg":"sha1","digest":"d9be6524a5f5047db5866813acf3277892a7a30a"},{"hashAlg":"sha256","digest":"ad95131bc0b799c0b1af477fb14fcf26a6a9f76079e48bf090acb7e8367bfd0e"},{"hashAlg":"sha384","digest":"4a06b879c7eedbe01c945d46b5bd785b59203dce81ea6a1206c28091ca285365f760d9167778f0dc1763d4854aafd40a"},{"hashAlg":"sha512","digest":"ea71bb243b0b2db729b9eb88e3c55a3f490fbff23457825051224a1fe6e6d3f480590cfa3a4a6b12c622d6ac366feb03cd17004ed004cb3f0d52731626946679"}]},{"pcr":5,"digests":[{"hashAlg":"sha1","digest":"d9be6524a5f5047db5866813acf3277892a7a30a"},{"hashAlg":"sha256","digest":"ad95131bc0b799c0b1af477fb14fcf26a6a9f76079e48bf090acb7e8367bfd0e"},{"hashAlg":"sha384","digest":"4a06b879c7eedbe01c945d46b5bd785b59203dce81ea6a1206c28091ca285365f760d9167778f0dc1763d4854aafd40a"},{"hashAlg":"sha512","digest":"ea71bb243b0b2db729b9eb88e3c55a3f490fbff23457825051224a1fe6e6d3f480590cfa3a4a6b12c622d6ac366feb03cd17004ed004cb3f0d52731626946679"}]},{"pcr":6,"digests":[{"hashAlg":"sha1","digest":"d9be6524a5f5047db5866813acf3277892a7a30a"},{"hashAlg":"sha256","digest":"ad95131bc0b799c0b1af477fb14fcf26a6a9f76079e48bf090acb7e8367bfd0e"},{"hashAlg":"sha384","digest":"4a06b879c7eedbe01c945d46b5bd785b59203dce81ea6a1206c28091ca285365f760d9167778f0dc1763d4854aafd40a"},{"hashAlg":"sha512","digest":"ea71bb243b0b2db729b9eb88e3c55a3f490fbff23457825051224a1fe6e6d3f480590cfa3a4a6b12c622d6ac366feb03cd17004ed004cb3f0d52731626946679"}]}]} diff --git a/src/pcrlock/pcrlock.d/700-action-efi-exit-boot-services.pcrlock.d/300-present.pcrlock b/src/pcrlock/pcrlock.d/700-action-efi-exit-boot-services.pcrlock.d/300-present.pcrlock new file mode 100644 index 0000000000..d7012df2e7 --- /dev/null +++ b/src/pcrlock/pcrlock.d/700-action-efi-exit-boot-services.pcrlock.d/300-present.pcrlock @@ -0,0 +1 @@ +{"records":[{"pcr":5,"digests":[{"hashAlg":"sha1","digest":"443a6b7b82b7af564f2e393cd9d5a388b7fa4a98"},{"hashAlg":"sha256","digest":"d8043d6b7b85ad358eb3b6ae6a873ab7ef23a26352c5dc4faa5aeedacf5eb41b"},{"hashAlg":"sha384","digest":"214b0bef1379756011344877743fdc2a5382bac6e70362d624ccf3f654407c1b4badf7d8f9295dd3dabdef65b27677e0"},{"hashAlg":"sha512","digest":"0fed3a4c9552021436534d27f3adb481e22b50b29e4b37a63f518540a651a174f149b69f500b0bdb2cb3bf4e0e21e0781451090af33e88f6bee4cbebd15c1668"}]},{"pcr":5,"digests":[{"hashAlg":"sha1","digest":"475545ddc978d7bfd036facc7e2e987f48189f0d"},{"hashAlg":"sha256","digest":"b54f7542cbd872a81a9d9dea839b2b8d747c7ebd5ea6615c40f42f44a6dbeba0"},{"hashAlg":"sha384","digest":"0a2e01c85deae718a530ad8c6d20a84009babe6c8989269e950d8cf440c6e997695e64d455c4174a652cd080f6230b74"},{"hashAlg":"sha512","digest":"1bb30cdbd6da78fe2a8a161ef51176e22d64dce305b40b47243673af64a2b16fca6182116433e3891be94773f6d7d411275721d5bf7d40ea51a274d5c891637c"}]}]} diff --git a/src/pcrlock/pcrlock.d/700-action-efi-exit-boot-services.pcrlock.d/600-absent.pcrlock b/src/pcrlock/pcrlock.d/700-action-efi-exit-boot-services.pcrlock.d/600-absent.pcrlock new file mode 100644 index 0000000000..a16142b6e9 --- /dev/null +++ b/src/pcrlock/pcrlock.d/700-action-efi-exit-boot-services.pcrlock.d/600-absent.pcrlock @@ -0,0 +1 @@ +{"records":[]} diff --git a/src/pcrlock/pcrlock.d/750-enter-initrd.pcrlock b/src/pcrlock/pcrlock.d/750-enter-initrd.pcrlock new file mode 100644 index 0000000000..a2332dc324 --- /dev/null +++ b/src/pcrlock/pcrlock.d/750-enter-initrd.pcrlock @@ -0,0 +1 @@ +{"records":[{"pcr":11,"digests":[{"hashAlg":"sha1","digest":"b1b01d5f73f321eb70e76f8a0e241ac0a3fa4a6e"},{"hashAlg":"sha256","digest":"51e6b92f405d1f98d96e3de343d61d420ad6923b25de21d766f9298192f14fed"},{"hashAlg":"sha384","digest":"687eef3a3a8c716439b5ed583657e8668401630c321f2f35d19b953ddf20b68a96474d0c2e5f0e1757bfa5ba70b9fc32"},{"hashAlg":"sha512","digest":"ab0ddfdabe43f1d06b3e58fbe17439a0f7f552e9e228d85665d485ececf7e733bae4cd7e0a17e5456e2ee7e412f5a0f37de05a782cce781e173ee26958de7f30"}]}]} diff --git a/src/pcrlock/pcrlock.d/800-leave-initrd.pcrlock b/src/pcrlock/pcrlock.d/800-leave-initrd.pcrlock new file mode 100644 index 0000000000..bd8f436a7e --- /dev/null +++ b/src/pcrlock/pcrlock.d/800-leave-initrd.pcrlock @@ -0,0 +1 @@ +{"records":[{"pcr":11,"digests":[{"hashAlg":"sha1","digest":"865e1ff2cc5b8db815313b23fe3d8b561212f5d1"},{"hashAlg":"sha256","digest":"3be261aff7db92bf507eae947f4003ffa2bcad0bffe3524601d62d0bc8be7135"},{"hashAlg":"sha384","digest":"9c0743b7a2e1ee06c70b7137b763cd2205c26ced274149959b05bd5a51bfa96b4fedaa4f87398b5c88986d1ff0879910"},{"hashAlg":"sha512","digest":"01b8ca86b9f8fac967f383380aff7cdffd2ef0c496574517c25398f7c74aa611821dd469ba021b2aa9b9a7232865708ca45c79368f2e7fffda3dd6b308264008"}]}]} diff --git a/src/pcrlock/pcrlock.d/850-sysinit.pcrlock b/src/pcrlock/pcrlock.d/850-sysinit.pcrlock new file mode 100644 index 0000000000..3bae4452f6 --- /dev/null +++ b/src/pcrlock/pcrlock.d/850-sysinit.pcrlock @@ -0,0 +1 @@ +{"records":[{"pcr":11,"digests":[{"hashAlg":"sha1","digest":"aeabcf402223916e804cce79778a55d5a9276983"},{"hashAlg":"sha256","digest":"730bb5a583ba880c277e656d2dc8aba1a314a11b14d25b05153d2bab82567a48"},{"hashAlg":"sha384","digest":"955cc8939f81d862b3119aabe612fd36bf91668bb62397f5e4126085d79ba6d7cbfa4e3a2345747f0b476ce4b1cbc2c9"},{"hashAlg":"sha512","digest":"a9eb62cdd1cd8292b6325a8ee3770d6f1b613426a749e17ffba8f90bdd6c41806468fb79d01276de7cc791877dfebae165d4ed07585154acf96652c6db92acc1"}]}]} diff --git a/src/pcrlock/pcrlock.d/900-ready.pcrlock b/src/pcrlock/pcrlock.d/900-ready.pcrlock new file mode 100644 index 0000000000..9a0e82f6e5 --- /dev/null +++ b/src/pcrlock/pcrlock.d/900-ready.pcrlock @@ -0,0 +1 @@ +{"records":[{"pcr":11,"digests":[{"hashAlg":"sha1","digest":"75c0533730caf1f78561c0883fb87bc8d98ef04b"},{"hashAlg":"sha256","digest":"b24d6d33736ecd5604a4b17bc9c6481039fac362bb7df044ef1c10a2bfd21db6"},{"hashAlg":"sha384","digest":"23ed5781da39fe6dc17f79478aeeb9eb2bca1d776061da188e10f9c85f7933fb39cfdba50f39af8aed24e5b45b80d006"},{"hashAlg":"sha512","digest":"ca6616f94a209e53f6fdc526b473172eb4b2157cf4809c31e36ad52db614ed352e68407be53c238ba17a561c4fde43f4a859aa8711f9781a0c934296d4d7571b"}]}]} diff --git a/src/pcrlock/pcrlock.d/950-shutdown.pcrlock b/src/pcrlock/pcrlock.d/950-shutdown.pcrlock new file mode 100644 index 0000000000..1bc3f76e0a --- /dev/null +++ b/src/pcrlock/pcrlock.d/950-shutdown.pcrlock @@ -0,0 +1 @@ +{"records":[{"pcr":11,"digests":[{"hashAlg":"sha1","digest":"53669f193b2174641c72654b5c3e5b67950334ae"},{"hashAlg":"sha256","digest":"08434ba9cdf55a02284e2913400586cd289878e0f055f7bb0b07ce392caeb989"},{"hashAlg":"sha384","digest":"186e2d6603b9755221b7ef894dd52b1154b48ef4786aec06ab6f7709e639715e89bd59fa80736bb45f0ca88583c212c1"},{"hashAlg":"sha512","digest":"9e5549deb36fc48768cb80e03bc91c36cf549ff5921e05bab5b68faefda7fac8c8a0755db783cbf1c1b98c80dc22ef06ff3f4a0a16704749f5cd4acf40e42a94"}]}]} diff --git a/src/pcrlock/pcrlock.d/990-final.pcrlock b/src/pcrlock/pcrlock.d/990-final.pcrlock new file mode 100644 index 0000000000..77081ae754 --- /dev/null +++ b/src/pcrlock/pcrlock.d/990-final.pcrlock @@ -0,0 +1 @@ +{"records":[{"pcr":11,"digests":[{"hashAlg":"sha1","digest":"d594c2cc0a53025004791399d80e20852af4c988"},{"hashAlg":"sha256","digest":"2443630b4620165c8b173e7265e17526fe2787ae594364dd6d839ad58f2fc007"},{"hashAlg":"sha384","digest":"90697eec39ed47f2b7ed278aa6fe6a1c073fcc7f3af54299fb95ac8a18c771acbac71e25b5a5639554943bfdfab76737"},{"hashAlg":"sha512","digest":"b3d9598ca0aa5da28be1c97a45d53cc5c72a80e61c439c8bf3e89c5c0661f49df8fa34019a21cd5e31261ae3a3a87ef4592d8010aad6a5ecdc9dbaae38cd1470"}]}]} diff --git a/src/pcrlock/pehash.c b/src/pcrlock/pehash.c new file mode 100644 index 0000000000..06d1f6afc7 --- /dev/null +++ b/src/pcrlock/pehash.c @@ -0,0 +1,246 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include + +#include "alloc-util.h" +#include "hexdecoct.h" +#include "pe-binary.h" +#include "pehash.h" +#include "sort-util.h" +#include "stat-util.h" +#include "string-table.h" + +/* Implements: + * + * https://download.microsoft.com/download/9/c/5/9c5b2167-8017-4bae-9fde-d599bac8184a/authenticode_pe.docx + * → Section "Calculating the PE Image Hash" + */ + +#define IMAGE_DATA_DIRECTORY_INDEX_CERTIFICATION_TABLE 4U + +static int hash_file(int fd, EVP_MD_CTX *md_ctx, uint64_t offset, uint64_t size) { + uint8_t buffer[64*1024]; + + log_debug("Hashing %" PRIu64 " @ %" PRIu64 " → %" PRIu64, size, offset, offset + size); + + while (size > 0) { + size_t m = MIN(size, sizeof(buffer)); + ssize_t n; + + n = pread(fd, buffer, m, offset); + if (n < 0) + return log_debug_errno(errno, "Failed to read file for hashing: %m"); + if ((size_t) n != m) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Short read while hashing."); + + if (EVP_DigestUpdate(md_ctx, buffer, m) != 1) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Unable to hash data."); + + offset += m; + size -= m; + } + + return 0; +} + +static int section_offset_cmp(const IMAGE_SECTION_HEADER *a, const IMAGE_SECTION_HEADER *b) { + return CMP(ASSERT_PTR(a)->PointerToRawData, ASSERT_PTR(b)->PointerToRawData); +} + +int pe_hash(int fd, + const EVP_MD *md, + void **ret_hash, + size_t *ret_hash_size) { + + _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *mdctx = NULL; + _cleanup_free_ IMAGE_SECTION_HEADER *sections = NULL; + _cleanup_free_ IMAGE_DOS_HEADER *dos_header = NULL; + _cleanup_free_ PeHeader *pe_header = NULL; + const IMAGE_DATA_DIRECTORY *certificate_table; + struct stat st; + uint64_t p, q; + int r; + + assert(fd >= 0); + assert(md); + assert(ret_hash_size); + assert(ret_hash); + + if (fstat(fd, &st) < 0) + return log_debug_errno(errno, "Failed to stat file: %m"); + r = stat_verify_regular(&st); + if (r < 0) + return log_debug_errno(r, "Not a regular file: %m"); + + r = pe_load_headers(fd, &dos_header, &pe_header); + if (r < 0) + return r; + + r = pe_load_sections(fd, dos_header, pe_header, §ions); + if (r < 0) + return r; + + certificate_table = pe_header_get_data_directory(pe_header, IMAGE_DATA_DIRECTORY_INDEX_CERTIFICATION_TABLE); + if (!certificate_table) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "File lacks certificate table."); + + mdctx = EVP_MD_CTX_new(); + if (!mdctx) + return log_oom_debug(); + + if (EVP_DigestInit_ex(mdctx, md, NULL) != 1) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to allocate message digest."); + + /* Everything from beginning of file to CheckSum field in PE header */ + p = (uint64_t) dos_header->e_lfanew + + offsetof(PeHeader, optional.CheckSum); + r = hash_file(fd, mdctx, 0, p); + if (r < 0) + return r; + p += sizeof(le32_t); + + /* Everything between the CheckSum field and the Image Data Directory Entry for the Certification Table */ + q = (uint64_t) dos_header->e_lfanew + + PE_HEADER_OPTIONAL_FIELD_OFFSET(pe_header, DataDirectory[IMAGE_DATA_DIRECTORY_INDEX_CERTIFICATION_TABLE]); + r = hash_file(fd, mdctx, p, q - p); + if (r < 0) + return r; + q += sizeof(IMAGE_DATA_DIRECTORY); + + /* The rest of the header + the section table */ + p = pe_header->optional.SizeOfHeaders; + if (p < q) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "SizeOfHeaders too short."); + r = hash_file(fd, mdctx, q, p - q); + if (r < 0) + return r; + + /* Sort by location in file */ + typesafe_qsort(sections, pe_header->pe.NumberOfSections, section_offset_cmp); + + FOREACH_ARRAY(section, sections, pe_header->pe.NumberOfSections) { + r = hash_file(fd, mdctx, section->PointerToRawData, section->SizeOfRawData); + if (r < 0) + return r; + + p += section->SizeOfRawData; + } + + if ((uint64_t) st.st_size > p) { + + if (st.st_size - p < certificate_table->Size) + return log_debug_errno(errno, "No space for certificate table, refusing."); + + r = hash_file(fd, mdctx, p, st.st_size - p - certificate_table->Size); + if (r < 0) + return r; + } + + int hsz = EVP_MD_CTX_size(mdctx); + if (hsz < 0) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to get hash size."); + + unsigned hash_size = (unsigned) hsz; + _cleanup_free_ void *hash = malloc(hsz); + if (!hash) + return log_oom_debug(); + + if (EVP_DigestFinal_ex(mdctx, hash, &hash_size) != 1) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to finalize hash function."); + + assert(hash_size == (unsigned) hsz); + + *ret_hash = TAKE_PTR(hash); + *ret_hash_size = hash_size; + + return 0; +} + +typedef void* SectionHashArray[_UNIFIED_SECTION_MAX]; + +static void section_hash_array_done(SectionHashArray *array) { + assert(array); + + for (size_t i = 0; i < _UNIFIED_SECTION_MAX; i++) + free((*array)[i]); +} + +int uki_hash(int fd, + const EVP_MD *md, + void* ret_hashes[static _UNIFIED_SECTION_MAX], + size_t *ret_hash_size) { + + _cleanup_(section_hash_array_done) SectionHashArray hashes = {}; + _cleanup_free_ IMAGE_SECTION_HEADER *sections = NULL; + _cleanup_free_ IMAGE_DOS_HEADER *dos_header = NULL; + _cleanup_free_ PeHeader *pe_header = NULL; + int r; + + assert(fd >= 0); + assert(ret_hashes); + assert(ret_hash_size); + + r = pe_load_headers(fd, &dos_header, &pe_header); + if (r < 0) + return r; + + r = pe_load_sections(fd, dos_header, pe_header, §ions); + if (r < 0) + return r; + + int hsz = EVP_MD_size(md); + if (hsz < 0) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to get hash size."); + + FOREACH_ARRAY(section, sections, pe_header->pe.NumberOfSections) { + _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *mdctx = NULL; + _cleanup_free_ char *n = NULL; + ssize_t i; + + n = memdup_suffix0(section->Name, sizeof(section->Name)); + if (!n) + return log_oom_debug(); + + i = string_table_lookup(unified_sections, _UNIFIED_SECTION_MAX, n); + if (i < 0) + continue; + + if (hashes[i]) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Duplicate section"); + + mdctx = EVP_MD_CTX_new(); + if (!mdctx) + return log_oom_debug(); + + if (EVP_DigestInit_ex(mdctx, md, NULL) != 1) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to allocate message digest."); + + r = hash_file(fd, mdctx, section->PointerToRawData, section->VirtualSize); + if (r < 0) + return r; + + hashes[i] = malloc(hsz); + if (!hashes[i]) + return log_oom_debug(); + + unsigned hash_size = (unsigned) hsz; + if (EVP_DigestFinal_ex(mdctx, hashes[i], &hash_size) != 1) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to finalize hash function."); + + assert(hash_size == (unsigned) hsz); + + if (DEBUG_LOGGING) { + _cleanup_free_ char *hs = NULL; + + hs = hexmem(hashes[i], hsz); + log_debug("Section %s with %s is %s.", n, EVP_MD_name(md), strna(hs)); + } + } + + memcpy(ret_hashes, hashes, sizeof(hashes)); + zero(hashes); + *ret_hash_size = (unsigned) hsz; + + return 0; +} diff --git a/src/pcrlock/pehash.h b/src/pcrlock/pehash.h new file mode 100644 index 0000000000..26f2fb1e7b --- /dev/null +++ b/src/pcrlock/pehash.h @@ -0,0 +1,11 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include + +#include "openssl-util.h" +#include "uki.h" + +int pe_hash(int fd, const EVP_MD *md, void **ret_hash, size_t *ret_hash_size); + +int uki_hash(int fd, const EVP_MD *md, void *ret_hashes[static _UNIFIED_SECTION_MAX], size_t *ret_hash_size); diff --git a/src/shared/creds-util.c b/src/shared/creds-util.c index 6e84cc87a1..7cc8889595 100644 --- a/src/shared/creds-util.c +++ b/src/shared/creds-util.c @@ -856,6 +856,7 @@ int encrypt_credential_and_warn( tpm2_n_hash_pcr_values, pubkey ? &public : NULL, /* use_pin= */ false, + /* pcrlock_policy= */ NULL, &tpm2_policy); if (r < 0) return log_error_errno(r, "Could not calculate sealing policy digest: %m"); @@ -1219,6 +1220,7 @@ int decrypt_credential_and_warn( z ? le64toh(z->pcr_mask) : 0, signature_json, /* pin= */ NULL, + /* pcrlock_policy= */ NULL, le16toh(t->primary_alg), t->policy_hash_and_blob, le32toh(t->blob_size), diff --git a/src/shared/meson.build b/src/shared/meson.build index 003158e75a..fc5cc1c59c 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -161,6 +161,7 @@ shared_sources = files( 'tmpfile-util-label.c', 'tomoyo-util.c', 'tpm2-util.c', + 'tpm2-event-log.c', 'udev-util.c', 'user-record-nss.c', 'user-record-show.c', diff --git a/src/shared/tpm2-event-log.c b/src/shared/tpm2-event-log.c new file mode 100644 index 0000000000..2e238468ae --- /dev/null +++ b/src/shared/tpm2-event-log.c @@ -0,0 +1,67 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "tpm2-event-log.h" + +#include "sort-util.h" + +typedef struct tpm2_log_event_type_info { + uint32_t event_type; + const char *name; +} tpm2_log_event_type_info; + +static tpm2_log_event_type_info tpm2_log_event_type_table[] = { + /* Unfortunately the types are defined all over the place, hence we are not using a dense table + * here. + * + * Keep this sorted by event type, so that we can do bisection! */ + { EV_PREBOOT_CERT, "preboot-cert" }, + { EV_POST_CODE, "post-code" }, + { EV_NO_ACTION, "no-action" }, + { EV_SEPARATOR, "separator" }, + { EV_ACTION, "action" }, + { EV_EVENT_TAG, "event-tag" }, + { EV_S_CRTM_CONTENTS, "s-crtm-contents" }, + { EV_S_CRTM_VERSION, "s-crtm-version" }, + { EV_CPU_MICROCODE, "cpu-microcode" }, + { EV_PLATFORM_CONFIG_FLAGS, "platform-config-flags" }, + { EV_TABLE_OF_DEVICES, "table-of-devices" }, + { EV_COMPACT_HASH, "compact-hash" }, + { EV_IPL, "ipl" }, + { EV_IPL_PARTITION_DATA, "ipl-partition-data" }, + { EV_NONHOST_CODE, "nonhost-code" }, + { EV_NONHOST_CONFIG, "nonhost-config" }, + { EV_NONHOST_INFO, "nonhost-info" }, + { EV_OMIT_BOOT_DEVICE_EVENTS, "omit-boot-device-events" }, + /* omitting EV_EFI_EVENT_BASE, since its not an event, but just a base value for other events */ + { EV_EFI_VARIABLE_DRIVER_CONFIG, "efi-variable-driver-config" }, + { EV_EFI_VARIABLE_BOOT, "efi-variable-boot" }, + { EV_EFI_BOOT_SERVICES_APPLICATION, "efi-boot-services-application" }, + { EV_EFI_BOOT_SERVICES_DRIVER, "efi-boot-services-driver" }, + { EV_EFI_RUNTIME_SERVICES_DRIVER, "efi-runtime-services-driver" }, + { EV_EFI_GPT_EVENT, "efi-gpt-event" }, + { EV_EFI_ACTION, "efi-action" }, + { EV_EFI_PLATFORM_FIRMWARE_BLOB, "efi-platform-firmware-blob" }, + { EV_EFI_HANDOFF_TABLES, "efi-handoff-tables" }, + { EV_EFI_PLATFORM_FIRMWARE_BLOB2, "efi-platform-firmware-blob2" }, + { EV_EFI_HANDOFF_TABLES2, "efi-handoff-tables" }, + { EV_EFI_VARIABLE_BOOT2, "efi-variable-boot2" }, + { EV_EFI_HCRTM_EVENT, "efi-hcrtm-event" }, + { EV_EFI_VARIABLE_AUTHORITY, "efi-variable-authority" }, + { EV_EFI_SPDM_FIRMWARE_BLOB, "efi-spdm-firmware-blob" }, + { EV_EFI_SPDM_FIRMWARE_CONFIG, "efi-spdm-firmware-config" }, +}; + +static int tpm2_log_event_type_info_cmp(const tpm2_log_event_type_info *a, const tpm2_log_event_type_info *b) { + return CMP(ASSERT_PTR(a)->event_type, ASSERT_PTR(b)->event_type); +} + +const char *tpm2_log_event_type_to_string(uint32_t type) { + + tpm2_log_event_type_info *found, key = { + .event_type = type, + }; + + found = typesafe_bsearch(&key, tpm2_log_event_type_table, ELEMENTSOF(tpm2_log_event_type_table), tpm2_log_event_type_info_cmp); + + return found ? found->name : NULL; +} diff --git a/src/shared/tpm2-event-log.h b/src/shared/tpm2-event-log.h new file mode 100644 index 0000000000..916b805bc2 --- /dev/null +++ b/src/shared/tpm2-event-log.h @@ -0,0 +1,139 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include + +#include "tpm2-util.h" + +/* Definitions as per "TCG PC Client Specific Platform Firmware Profile Specification" + * (https://trustedcomputinggroup.org/resource/pc-client-specific-platform-firmware-profile-specification/), + * section 10.4.1 "Event Types" (at least in version 1.05 Revision 23 of the spec) */ +#ifndef EV_PREBOOT_CERT +#define EV_PREBOOT_CERT UINT32_C(0x00000000) +#define EV_POST_CODE UINT32_C(0x00000001) +#define EV_NO_ACTION UINT32_C(0x00000003) +#define EV_SEPARATOR UINT32_C(0x00000004) +#define EV_ACTION UINT32_C(0x00000005) +#define EV_EVENT_TAG UINT32_C(0x00000006) +#define EV_S_CRTM_CONTENTS UINT32_C(0x00000007) +#define EV_S_CRTM_VERSION UINT32_C(0x00000008) +#define EV_CPU_MICROCODE UINT32_C(0x00000009) +#define EV_PLATFORM_CONFIG_FLAGS UINT32_C(0x0000000a) +#define EV_TABLE_OF_DEVICES UINT32_C(0x0000000b) +#define EV_COMPACT_HASH UINT32_C(0x0000000c) +#define EV_IPL UINT32_C(0x0000000d) +#define EV_IPL_PARTITION_DATA UINT32_C(0x0000000e) +#define EV_NONHOST_CODE UINT32_C(0x0000000f) +#define EV_NONHOST_CONFIG UINT32_C(0x00000010) +#define EV_NONHOST_INFO UINT32_C(0x00000011) +#define EV_OMIT_BOOT_DEVICE_EVENTS UINT32_C(0x00000012) +#define EV_EFI_EVENT_BASE UINT32_C(0x80000000) +#define EV_EFI_VARIABLE_DRIVER_CONFIG UINT32_C(0x80000001) +#define EV_EFI_VARIABLE_BOOT UINT32_C(0x80000002) +#define EV_EFI_BOOT_SERVICES_APPLICATION UINT32_C(0x80000003) +#define EV_EFI_BOOT_SERVICES_DRIVER UINT32_C(0x80000004) +#define EV_EFI_RUNTIME_SERVICES_DRIVER UINT32_C(0x80000005) +#define EV_EFI_GPT_EVENT UINT32_C(0x80000006) +#define EV_EFI_ACTION UINT32_C(0x80000007) +#define EV_EFI_PLATFORM_FIRMWARE_BLOB UINT32_C(0x80000008) +#define EV_EFI_HANDOFF_TABLES UINT32_C(0x80000009) +#define EV_EFI_PLATFORM_FIRMWARE_BLOB2 UINT32_C(0x8000000A) +#define EV_EFI_HANDOFF_TABLES2 UINT32_C(0x8000000B) +#define EV_EFI_VARIABLE_BOOT2 UINT32_C(0x8000000C) +#define EV_EFI_HCRTM_EVENT UINT32_C(0x80000010) +#define EV_EFI_VARIABLE_AUTHORITY UINT32_C(0x800000E0) +#define EV_EFI_SPDM_FIRMWARE_BLOB UINT32_C(0x800000E1) +#define EV_EFI_SPDM_FIRMWARE_CONFIG UINT32_C(0x800000E2) +#endif + +/* Defined in drivers/firmware/efi/libstub/efistub.h in the Linux kernel sources */ +#ifndef INITRD_EVENT_TAG_ID +#define INITRD_EVENT_TAG_ID UINT32_C(0x8F3B22EC) +#endif + +#ifndef LOAD_OPTIONS_EVENT_TAG_ID +#define LOAD_OPTIONS_EVENT_TAG_ID UINT32_C(0x8F3B22ED) +#endif + +const char *tpm2_log_event_type_to_string(uint32_t type) _const_; + +#if HAVE_TPM2 + +/* UEFI event log data structures */ +typedef struct _packed_ TCG_PCClientPCREvent { + uint32_t pcrIndex; + uint32_t eventType; + uint8_t digest[20]; + uint32_t eventDataSize; + uint32_t event[]; +} TCG_PCClientPCREvent; + +typedef struct _packed_ packed_TPMT_HA { + uint16_t hashAlg; + TPMU_HA digest; +} packed_TPMT_HA; + +typedef struct _packed_ packed_TPML_DIGEST_VALUES { + uint32_t count; + packed_TPMT_HA digests[]; +} packed_TPML_DIGEST_VALUES; + +typedef struct _packed_ TCG_PCR_EVENT2 { + uint32_t pcrIndex; + uint32_t eventType; + packed_TPML_DIGEST_VALUES digests; + /* … */ +} TCG_PCR_EVENT2; + +typedef struct _packed_ TCG_EfiSpecIdEventAlgorithmSize { + uint16_t algorithmId; + uint16_t digestSize; +} TCG_EfiSpecIdEventAlgorithmSize; + +typedef struct _packed_ tdTCG_EfiSpecIdEvent { + uint8_t signature[16]; + uint32_t platformClass; + uint8_t specVersionMinor; + uint8_t specVersionMajor; + uint8_t specErrata; + uint8_t uintnSize; + uint32_t numberOfAlgorithms; + TCG_EfiSpecIdEventAlgorithmSize digestSizes[]; + /* … */ +} TCG_EfiSpecIDEvent; + +typedef struct _packed_ UEFI_VARIABLE_DATA { + uint8_t variableName[16]; + uint64_t unicodeNameLength; + uint64_t variableDataLength; + char16_t unicodeName[]; + /* … */ +} UEFI_VARIABLE_DATA; + +typedef struct _packed_ TCG_PCClientTaggedEvent{ + uint32_t taggedEventID; + uint32_t taggedEventDataSize; + uint8_t taggedEventData[]; +} TCG_PCClientTaggedEvent; + +typedef struct _packed_ packed_EFI_DEVICE_PATH { + uint8_t type; + uint8_t subType; + uint16_t length; + uint8_t path[]; +} packed_EFI_DEVICE_PATH; + +typedef struct _packed_ UEFI_IMAGE_LOAD_EVENT { + uint64_t imageLocationInMemory; + uint64_t imageLengthInMemory; + uint64_t imageLinkTimeAddress; + uint64_t lengthOfDevicePath; + packed_EFI_DEVICE_PATH devicePath[]; +} UEFI_IMAGE_LOAD_EVENT; + +typedef struct _packed_ UEFI_PLATFORM_FIRMWARE_BLOB { + uint64_t blobBase; + uint64_t blobLength; +} UEFI_PLATFORM_FIRMWARE_BLOB; + +#endif diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index b640de19c8..70879a0220 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -51,11 +51,16 @@ static TSS2_RC (*sym_Esys_GetRandom)(ESYS_CONTEXT *esysContext, ESYS_TR shandle1 static TSS2_RC (*sym_Esys_Initialize)(ESYS_CONTEXT **esys_context, TSS2_TCTI_CONTEXT *tcti, TSS2_ABI_VERSION *abiVersion) = NULL; static TSS2_RC (*sym_Esys_Load)(ESYS_CONTEXT *esysContext, ESYS_TR parentHandle, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_PRIVATE *inPrivate, const TPM2B_PUBLIC *inPublic, ESYS_TR *objectHandle) = NULL; static TSS2_RC (*sym_Esys_LoadExternal)(ESYS_CONTEXT *esysContext, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_SENSITIVE *inPrivate, const TPM2B_PUBLIC *inPublic, ESYS_TR hierarchy, ESYS_TR *objectHandle) = NULL; +static TSS2_RC (*sym_Esys_NV_DefineSpace)(ESYS_CONTEXT *esysContext, ESYS_TR authHandle, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_AUTH *auth, const TPM2B_NV_PUBLIC *publicInfo, ESYS_TR *nvHandle); +static TSS2_RC (*sym_Esys_NV_UndefineSpace)(ESYS_CONTEXT *esysContext, ESYS_TR authHandle, ESYS_TR nvIndex, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3); +static TSS2_RC (*sym_Esys_NV_Write)(ESYS_CONTEXT *esysContext, ESYS_TR authHandle, ESYS_TR nvIndex, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_MAX_NV_BUFFER *data, UINT16 offset); static TSS2_RC (*sym_Esys_PCR_Extend)(ESYS_CONTEXT *esysContext, ESYS_TR pcrHandle, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPML_DIGEST_VALUES *digests) = NULL; static TSS2_RC (*sym_Esys_PCR_Read)(ESYS_CONTEXT *esysContext, ESYS_TR shandle1,ESYS_TR shandle2, ESYS_TR shandle3, const TPML_PCR_SELECTION *pcrSelectionIn, UINT32 *pcrUpdateCounter, TPML_PCR_SELECTION **pcrSelectionOut, TPML_DIGEST **pcrValues) = NULL; -static TSS2_RC (*sym_Esys_PolicyAuthorize)(ESYS_CONTEXT *esysContext, ESYS_TR policySession, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_DIGEST *approvedPolicy, const TPM2B_NONCE *policyRef, const TPM2B_NAME *keySign, const TPMT_TK_VERIFIED *checkTicket) = NULL; static TSS2_RC (*sym_Esys_PolicyAuthValue)(ESYS_CONTEXT *esysContext, ESYS_TR policySession, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3) = NULL; +static TSS2_RC (*sym_Esys_PolicyAuthorize)(ESYS_CONTEXT *esysContext, ESYS_TR policySession, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_DIGEST *approvedPolicy, const TPM2B_NONCE *policyRef, const TPM2B_NAME *keySign, const TPMT_TK_VERIFIED *checkTicket) = NULL; +static TSS2_RC (*sym_Esys_PolicyAuthorizeNV)(ESYS_CONTEXT *esysContext, ESYS_TR authHandle, ESYS_TR nvIndex, ESYS_TR policySession, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3); static TSS2_RC (*sym_Esys_PolicyGetDigest)(ESYS_CONTEXT *esysContext, ESYS_TR policySession, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, TPM2B_DIGEST **policyDigest) = NULL; +static TSS2_RC (*sym_Esys_PolicyOR)(ESYS_CONTEXT *esysContext, ESYS_TR policySession, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPML_DIGEST *pHashList) = NULL; static TSS2_RC (*sym_Esys_PolicyPCR)(ESYS_CONTEXT *esysContext, ESYS_TR policySession, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_DIGEST *pcrDigest, const TPML_PCR_SELECTION *pcrs) = NULL; static TSS2_RC (*sym_Esys_ReadPublic)(ESYS_CONTEXT *esysContext, ESYS_TR objectHandle, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, TPM2B_PUBLIC **outPublic, TPM2B_NAME **name, TPM2B_NAME **qualifiedName) = NULL; static TSS2_RC (*sym_Esys_StartAuthSession)(ESYS_CONTEXT *esysContext, ESYS_TR tpmKey, ESYS_TR bind, ESYS_TR shandle1, ESYS_TR shandle2, ESYS_TR shandle3, const TPM2B_NONCE *nonceCaller, TPM2_SE sessionType, const TPMT_SYM_DEF *symmetric, TPMI_ALG_HASH authHash, ESYS_TR *sessionHandle) = NULL; @@ -79,6 +84,9 @@ static TSS2_RC (*sym_Tss2_MU_TPM2B_PRIVATE_Unmarshal)(uint8_t const buffer[], si static TSS2_RC (*sym_Tss2_MU_TPM2B_PUBLIC_Marshal)(TPM2B_PUBLIC const *src, uint8_t buffer[], size_t buffer_size, size_t *offset) = NULL; static TSS2_RC (*sym_Tss2_MU_TPM2B_PUBLIC_Unmarshal)(uint8_t const buffer[], size_t buffer_size, size_t *offset, TPM2B_PUBLIC *dest) = NULL; static TSS2_RC (*sym_Tss2_MU_TPML_PCR_SELECTION_Marshal)(TPML_PCR_SELECTION const *src, uint8_t buffer[], size_t buffer_size, size_t *offset) = NULL; +static TSS2_RC (*sym_Tss2_MU_TPMS_NV_PUBLIC_Marshal)(TPMS_NV_PUBLIC const *src, uint8_t buffer[], size_t buffer_size, size_t *offset) = NULL; +static TSS2_RC (*sym_Tss2_MU_TPM2B_NV_PUBLIC_Marshal)(TPM2B_NV_PUBLIC const *src, uint8_t buffer[], size_t buffer_size, size_t *offset) = NULL; +static TSS2_RC (*sym_Tss2_MU_TPM2B_NV_PUBLIC_Unmarshal)(uint8_t const buffer[], size_t buffer_size, size_t *offset, TPM2B_NV_PUBLIC *dest) = NULL; static TSS2_RC (*sym_Tss2_MU_TPMT_HA_Marshal)(TPMT_HA const *src, uint8_t buffer[], size_t buffer_size, size_t *offset) = NULL; static TSS2_RC (*sym_Tss2_MU_TPMT_PUBLIC_Marshal)(TPMT_PUBLIC const *src, uint8_t buffer[], size_t buffer_size, size_t *offset) = NULL; @@ -101,11 +109,16 @@ int dlopen_tpm2(void) { DLSYM_ARG(Esys_Initialize), DLSYM_ARG(Esys_Load), DLSYM_ARG(Esys_LoadExternal), + DLSYM_ARG(Esys_NV_DefineSpace), + DLSYM_ARG(Esys_NV_UndefineSpace), + DLSYM_ARG(Esys_NV_Write), DLSYM_ARG(Esys_PCR_Extend), DLSYM_ARG(Esys_PCR_Read), - DLSYM_ARG(Esys_PolicyAuthorize), DLSYM_ARG(Esys_PolicyAuthValue), + DLSYM_ARG(Esys_PolicyAuthorize), + DLSYM_ARG(Esys_PolicyAuthorizeNV), DLSYM_ARG(Esys_PolicyGetDigest), + DLSYM_ARG(Esys_PolicyOR), DLSYM_ARG(Esys_PolicyPCR), DLSYM_ARG(Esys_ReadPublic), DLSYM_ARG(Esys_StartAuthSession), @@ -144,6 +157,9 @@ int dlopen_tpm2(void) { DLSYM_ARG(Tss2_MU_TPM2B_PUBLIC_Marshal), DLSYM_ARG(Tss2_MU_TPM2B_PUBLIC_Unmarshal), DLSYM_ARG(Tss2_MU_TPML_PCR_SELECTION_Marshal), + DLSYM_ARG(Tss2_MU_TPMS_NV_PUBLIC_Marshal), + DLSYM_ARG(Tss2_MU_TPM2B_NV_PUBLIC_Marshal), + DLSYM_ARG(Tss2_MU_TPM2B_NV_PUBLIC_Unmarshal), DLSYM_ARG(Tss2_MU_TPMT_HA_Marshal), DLSYM_ARG(Tss2_MU_TPMT_PUBLIC_Marshal)); } @@ -2114,7 +2130,7 @@ int tpm2_create(Tpm2Context *c, return 0; } -static int tpm2_load( +int tpm2_load( Tpm2Context *c, const Tpm2Handle *parent, const Tpm2Handle *session, @@ -2329,6 +2345,135 @@ int tpm2_create_loaded( return 0; } +static int tpm2_marshal_private(const TPM2B_PRIVATE *private, void **ret, size_t *ret_size) { + size_t max_size = sizeof(*private), blob_size = 0; + _cleanup_free_ void *blob = NULL; + TSS2_RC rc; + + assert(private); + assert(ret); + assert(ret_size); + + blob = malloc0(max_size); + if (!blob) + return log_oom_debug(); + + rc = sym_Tss2_MU_TPM2B_PRIVATE_Marshal(private, blob, max_size, &blob_size); + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to marshal private key: %s", sym_Tss2_RC_Decode(rc)); + + *ret = TAKE_PTR(blob); + *ret_size = blob_size; + return 0; +} + +static int tpm2_unmarshal_private(const void *data, size_t size, TPM2B_PRIVATE *ret_private) { + TPM2B_PRIVATE private = {}; + size_t offset = 0; + TSS2_RC rc; + + assert(data || size == 0); + assert(ret_private); + + rc = sym_Tss2_MU_TPM2B_PRIVATE_Unmarshal(data, size, &offset, &private); + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to unmarshal private key: %s", sym_Tss2_RC_Decode(rc)); + if (offset != size) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Garbage at end of private key marshal data."); + + *ret_private = private; + return 0; +} + +static int tpm2_marshal_public(const TPM2B_PUBLIC *public, void **ret, size_t *ret_size) { + size_t max_size = sizeof(*public), blob_size = 0; + _cleanup_free_ void *blob = NULL; + TSS2_RC rc; + + assert(public); + assert(ret); + assert(ret_size); + + blob = malloc0(max_size); + if (!blob) + return log_oom_debug(); + + rc = sym_Tss2_MU_TPM2B_PUBLIC_Marshal(public, blob, max_size, &blob_size); + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to marshal public key: %s", sym_Tss2_RC_Decode(rc)); + + *ret = TAKE_PTR(blob); + *ret_size = blob_size; + return 0; +} + +static int tpm2_unmarshal_public(const void *data, size_t size, TPM2B_PUBLIC *ret_public) { + TPM2B_PUBLIC public = {}; + size_t offset = 0; + TSS2_RC rc; + + assert(data || size == 0); + assert(ret_public); + + rc = sym_Tss2_MU_TPM2B_PUBLIC_Unmarshal(data, size, &offset, &public); + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to unmarshal public key: %s", sym_Tss2_RC_Decode(rc)); + if (offset != size) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Garbage at end of public key marshal data."); + + *ret_public = public; + return 0; +} + +int tpm2_marshal_nv_public(const TPM2B_NV_PUBLIC *nv_public, void **ret, size_t *ret_size) { + size_t max_size = sizeof(*nv_public), blob_size = 0; + _cleanup_free_ void *blob = NULL; + TSS2_RC rc; + + assert(nv_public); + assert(ret); + assert(ret_size); + + blob = malloc0(max_size); + if (!blob) + return log_oom_debug(); + + rc = sym_Tss2_MU_TPM2B_NV_PUBLIC_Marshal(nv_public, blob, max_size, &blob_size); + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to marshal NV public structure: %s", sym_Tss2_RC_Decode(rc)); + + *ret = TAKE_PTR(blob); + *ret_size = blob_size; + return 0; +} + +int tpm2_unmarshal_nv_public(const void *data, size_t size, TPM2B_NV_PUBLIC *ret_nv_public) { + TPM2B_NV_PUBLIC nv_public = {}; + size_t offset = 0; + TSS2_RC rc; + + assert(data || size == 0); + assert(ret_nv_public); + + rc = sym_Tss2_MU_TPM2B_NV_PUBLIC_Unmarshal(data, size, &offset, &nv_public); + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to unmarshal NV public structure: %s", sym_Tss2_RC_Decode(rc)); + if (offset != size) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Garbage at end of NV public structure marshal data."); + + *ret_nv_public = nv_public; + return 0; +} + /* Read hash values from the specified PCR selection. Provides a Tpm2PCRValue array that contains all * requested PCR values, in the order provided by the TPM. Normally, the provided pcr values will match * exactly what is in the provided selection, but the TPM may ignore some selected PCRs (for example, if an @@ -2837,7 +2982,7 @@ static void tpm2_trim_auth_value(TPM2B_AUTH *auth) { log_debug("authValue ends in 0, trimming as required by the TPM2 specification Part 1 section 'HMAC Computation' authValue Note 2."); } -static int tpm2_get_pin_auth(TPMI_ALG_HASH hash, const char *pin, TPM2B_AUTH *ret_auth) { +int tpm2_get_pin_auth(TPMI_ALG_HASH hash, const char *pin, TPM2B_AUTH *ret_auth) { TPM2B_AUTH auth = {}; int r; @@ -2855,9 +3000,25 @@ static int tpm2_get_pin_auth(TPMI_ALG_HASH hash, const char *pin, TPM2B_AUTH *re return 0; } -static int tpm2_set_auth(Tpm2Context *c, const Tpm2Handle *handle, const char *pin) { - TPM2B_AUTH auth = {}; +int tpm2_set_auth_binary(Tpm2Context *c, const Tpm2Handle *handle, const TPM2B_AUTH *auth) { TSS2_RC rc; + + assert(c); + assert(handle); + + if (!auth) + return 0; + + rc = sym_Esys_TR_SetAuth(c->esys_context, handle->esys_handle, auth); + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to load PIN in TPM: %s", sym_Tss2_RC_Decode(rc)); + + return 0; +} + +int tpm2_set_auth(Tpm2Context *c, const Tpm2Handle *handle, const char *pin) { + TPM2B_AUTH auth = {}; int r; assert(c); @@ -2872,12 +3033,7 @@ static int tpm2_set_auth(Tpm2Context *c, const Tpm2Handle *handle, const char *p if (r < 0) return r; - rc = sym_Esys_TR_SetAuth(c->esys_context, handle->esys_handle, &auth); - if (rc != TSS2_RC_SUCCESS) - return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), - "Failed to load PIN in TPM: %s", sym_Tss2_RC_Decode(rc)); - - return 0; + return tpm2_set_auth_binary(c, handle, &auth); } static bool tpm2_is_encryption_session(Tpm2Context *c, const Tpm2Handle *session) { @@ -2894,7 +3050,7 @@ static bool tpm2_is_encryption_session(Tpm2Context *c, const Tpm2Handle *session return (flags & TPMA_SESSION_DECRYPT) && (flags & TPMA_SESSION_ENCRYPT); } -static int tpm2_make_encryption_session( +int tpm2_make_encryption_session( Tpm2Context *c, const Tpm2Handle *primary, const Tpm2Handle *bind_key, @@ -2948,7 +3104,7 @@ static int tpm2_make_encryption_session( return 0; } -static int tpm2_make_policy_session( +int tpm2_make_policy_session( Tpm2Context *c, const Tpm2Handle *primary, const Tpm2Handle *encryption_session, @@ -3147,7 +3303,7 @@ int tpm2_calculate_pubkey_name(const TPMT_PUBLIC *public, TPM2B_NAME *ret_name) "Failed to marshal key name: %s", sym_Tss2_RC_Decode(rc)); name.size = size; - tpm2_log_debug_name(&name, "Calculated name"); + tpm2_log_debug_name(&name, "Calculated public key name"); *ret_name = name; @@ -3184,6 +3340,60 @@ static int tpm2_get_name( return 0; } +int tpm2_calculate_nv_index_name(const TPMS_NV_PUBLIC *nvpublic, TPM2B_NAME *ret_name) { + TSS2_RC rc; + int r; + + assert(nvpublic); + assert(ret_name); + + r = dlopen_tpm2(); + if (r < 0) + return log_debug_errno(r, "TPM2 support not installed: %m"); + + if (nvpublic->nameAlg != TPM2_ALG_SHA256) + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "Unsupported nameAlg: 0x%x", + nvpublic->nameAlg); + + _cleanup_free_ uint8_t *buf = NULL; + size_t size = 0; + + buf = (uint8_t*) new(TPMS_NV_PUBLIC, 1); + if (!buf) + return log_oom_debug(); + + rc = sym_Tss2_MU_TPMS_NV_PUBLIC_Marshal(nvpublic, buf, sizeof(TPMS_NV_PUBLIC), &size); + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to marshal NV index: %s", sym_Tss2_RC_Decode(rc)); + + TPM2B_DIGEST digest = {}; + r = tpm2_digest_buffer(TPM2_ALG_SHA256, &digest, buf, size, /* extend= */ false); + if (r < 0) + return r; + + TPMT_HA ha = { + .hashAlg = TPM2_ALG_SHA256, + }; + assert(digest.size <= sizeof(ha.digest.sha256)); + memcpy_safe(ha.digest.sha256, digest.buffer, digest.size); + + TPM2B_NAME name; + size = 0; + rc = sym_Tss2_MU_TPMT_HA_Marshal(&ha, name.name, sizeof(name.name), &size); + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to marshal NV index name: %s", sym_Tss2_RC_Decode(rc)); + name.size = size; + + tpm2_log_debug_name(&name, "Calculated NV index name"); + + *ret_name = name; + + return 0; +} + /* Extend 'digest' with the PolicyAuthValue calculated hash. */ int tpm2_calculate_policy_auth_value(TPM2B_DIGEST *digest) { TPM2_CC command = TPM2_CC_PolicyAuthValue; @@ -3218,7 +3428,7 @@ int tpm2_calculate_policy_auth_value(TPM2B_DIGEST *digest) { return 0; } -static int tpm2_policy_auth_value( +int tpm2_policy_auth_value( Tpm2Context *c, const Tpm2Handle *session, TPM2B_DIGEST **ret_policy_digest) { @@ -3228,7 +3438,7 @@ static int tpm2_policy_auth_value( assert(c); assert(session); - log_debug("Adding authValue policy."); + log_debug("Submitting AuthValue policy."); rc = sym_Esys_PolicyAuthValue( c->esys_context, @@ -3244,6 +3454,185 @@ static int tpm2_policy_auth_value( return tpm2_get_policy_digest(c, session, ret_policy_digest); } +int tpm2_calculate_policy_authorize_nv( + const TPM2B_NV_PUBLIC *public_info, + TPM2B_DIGEST *digest) { + TPM2_CC command = TPM2_CC_PolicyAuthorizeNV; + TSS2_RC rc; + int r; + + assert(public_info); + assert(digest); + assert(digest->size == SHA256_DIGEST_SIZE); + + r = dlopen_tpm2(); + if (r < 0) + return log_debug_errno(r, "TPM2 support not installed: %m"); + + uint8_t buf[sizeof(command)]; + size_t offset = 0; + + rc = sym_Tss2_MU_TPM2_CC_Marshal(command, buf, sizeof(buf), &offset); + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to marshal PolicyAuthorizeNV command: %s", sym_Tss2_RC_Decode(rc)); + + if (offset != sizeof(command)) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Offset 0x%zx wrong after marshalling PolicyAuthorizeNV command", offset); + + TPM2B_NV_PUBLIC public_info_copy = *public_info; /* Make a copy, since we must set TPMA_NV_WRITTEN for the calculation */ + public_info_copy.nvPublic.attributes |= TPMA_NV_WRITTEN; + + TPM2B_NAME name = {}; + r = tpm2_calculate_nv_index_name(&public_info_copy.nvPublic, &name); + if (r < 0) + return r; + + struct iovec data[] = { + IOVEC_MAKE(buf, offset), + IOVEC_MAKE(name.name, name.size), + }; + + r = tpm2_digest_many(TPM2_ALG_SHA256, digest, data, ELEMENTSOF(data), /* extend= */ true); + if (r < 0) + return r; + + tpm2_log_debug_digest(digest, "PolicyAuthorizeNV calculated digest"); + + return 0; +} + +int tpm2_policy_authorize_nv( + Tpm2Context *c, + const Tpm2Handle *session, + const Tpm2Handle *nv_handle, + TPM2B_DIGEST **ret_policy_digest) { + + TSS2_RC rc; + + assert(c); + assert(session); + + log_debug("Submitting AuthorizeNV policy."); + + rc = sym_Esys_PolicyAuthorizeNV( + c->esys_context, + ESYS_TR_RH_OWNER, + nv_handle->esys_handle, + session->esys_handle, + ESYS_TR_PASSWORD, + ESYS_TR_NONE, + ESYS_TR_NONE); + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to add AuthorizeNV policy to TPM: %s", + sym_Tss2_RC_Decode(rc)); + + return tpm2_get_policy_digest(c, session, ret_policy_digest); +} + +int tpm2_policy_or( + Tpm2Context *c, + const Tpm2Handle *session, + const TPM2B_DIGEST *branches, size_t n_branches, + TPM2B_DIGEST **ret_policy_digest) { + + TPML_DIGEST hash_list; + TSS2_RC rc; + + assert(c); + assert(session); + + if (n_branches > ELEMENTSOF(hash_list.digests)) + return -EOPNOTSUPP; + + log_debug("Submitting OR policy."); + + hash_list = (TPML_DIGEST) { + .count = n_branches, + }; + + memcpy(hash_list.digests, branches, n_branches * sizeof(TPM2B_DIGEST)); + + if (DEBUG_LOGGING) + for (size_t i = 0; i < hash_list.count; i++) { + _cleanup_free_ char *h = hexmem(hash_list.digests[i].buffer, hash_list.digests[i].size); + log_debug("Submitting OR Branch #%zu: %s", i, h); + } + + rc = sym_Esys_PolicyOR( + c->esys_context, + session->esys_handle, + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + &hash_list); + if (rc != TSS2_RC_SUCCESS) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to add OR policy to TPM: %s", + sym_Tss2_RC_Decode(rc)); + + return tpm2_get_policy_digest(c, session, ret_policy_digest); +} + +/* Extend 'digest' with the PolicyOR calculated hash. */ +int tpm2_calculate_policy_or(const TPM2B_DIGEST *branches, size_t n_branches, TPM2B_DIGEST *digest) { + TPM2_CC command = TPM2_CC_PolicyOR; + TSS2_RC rc; + int r; + + assert(digest); + assert(digest->size == SHA256_DIGEST_SIZE); + + if (n_branches == 0) + return -EINVAL; + if (n_branches == 1) + log_warning("PolicyOR with a single branch submitted, this is weird."); + if (n_branches > 8) + return -E2BIG; + + r = dlopen_tpm2(); + if (r < 0) + return log_error_errno(r, "TPM2 support not installed: %m"); + + uint8_t buf[sizeof(command)]; + size_t offset = 0; + + rc = sym_Tss2_MU_TPM2_CC_Marshal(command, buf, sizeof(buf), &offset); + if (rc != TSS2_RC_SUCCESS) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to marshal PolicyOR command: %s", sym_Tss2_RC_Decode(rc)); + + if (offset != sizeof(command)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Offset 0x%zx wrong after marshalling PolicyOR command", offset); + _cleanup_free_ struct iovec *data = new(struct iovec, 1 + n_branches); + if (!data) + return log_oom(); + + data[0] = IOVEC_MAKE(buf, offset); + for (size_t i = 0; i < n_branches; i++) { + data[1 + i] = IOVEC_MAKE((void*) branches[i].buffer, branches[i].size); + + if (DEBUG_LOGGING) { + _cleanup_free_ char *h = hexmem(branches[i].buffer, branches[i].size); + log_debug("OR Branch #%zu: %s", i, h); + } + } + + /* PolicyOR does not use the previous hash value; we must zero and then extend it. */ + zero(digest->buffer); + + r = tpm2_digest_many(TPM2_ALG_SHA256, digest, data, 1 + n_branches, /* extend= */ true); + if (r < 0) + return r; + + tpm2_log_debug_digest(digest, "PolicyOR calculated digest"); + + return 0; +} + /* Extend 'digest' with the PolicyPCR calculated hash. */ int tpm2_calculate_policy_pcr( const Tpm2PCRValue *pcr_values, @@ -3304,7 +3693,7 @@ int tpm2_calculate_policy_pcr( return 0; } -static int tpm2_policy_pcr( +int tpm2_policy_pcr( Tpm2Context *c, const Tpm2Handle *session, const TPML_PCR_SELECTION *pcr_selection, @@ -3316,7 +3705,7 @@ static int tpm2_policy_pcr( assert(session); assert(pcr_selection); - log_debug("Adding PCR hash policy."); + log_debug("Submitting PCR hash policy."); rc = sym_Esys_PolicyPCR( c->esys_context, @@ -3517,6 +3906,7 @@ int tpm2_calculate_sealing_policy( size_t n_pcr_values, const TPM2B_PUBLIC *public, bool use_pin, + const Tpm2PCRLockPolicy *pcrlock_policy, TPM2B_DIGEST *digest) { int r; @@ -3524,12 +3914,30 @@ int tpm2_calculate_sealing_policy( assert(pcr_values || n_pcr_values == 0); assert(digest); + if (public && pcrlock_policy) + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Policies with both signed PCR and pcrlock are currently not supported."); + if (public) { r = tpm2_calculate_policy_authorize(public, NULL, digest); if (r < 0) return r; } + if (pcrlock_policy) { + TPM2B_NV_PUBLIC nv_public; + + r = tpm2_unmarshal_nv_public( + pcrlock_policy->nv_public.iov_base, + pcrlock_policy->nv_public.iov_len, + &nv_public); + if (r < 0) + return r; + + r = tpm2_calculate_policy_authorize_nv(&nv_public, digest); + if (r < 0) + return r; + } + if (n_pcr_values > 0) { r = tpm2_calculate_policy_pcr(pcr_values, n_pcr_values, digest); if (r < 0) @@ -3556,6 +3964,7 @@ static int tpm2_build_sealing_policy( uint32_t pubkey_pcr_mask, JsonVariant *signature_json, bool use_pin, + const Tpm2PCRLockPolicy *pcrlock_policy, TPM2B_DIGEST **ret_policy_digest) { int r; @@ -3574,6 +3983,9 @@ static int tpm2_build_sealing_policy( log_debug("Selected TPM2 PCRs are not initialized on this system."); } + if (pubkey_pcr_mask != 0 && pcrlock_policy) + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Policies with both signed PCR and pcrlock are currently not supported."); + if (pubkey_pcr_mask != 0) { TPML_PCR_SELECTION pcr_selection; tpm2_tpml_pcr_selection_from_mask(pubkey_pcr_mask, (TPMI_ALG_HASH)pcr_bank, &pcr_selection); @@ -3582,6 +3994,34 @@ static int tpm2_build_sealing_policy( return r; } + if (pcrlock_policy) { + _cleanup_(tpm2_handle_freep) Tpm2Handle *nv_handle = NULL; + + r = tpm2_policy_super_pcr( + c, + session, + &pcrlock_policy->prediction, + pcrlock_policy->algorithm); + if (r < 0) + return r; + + r = tpm2_deserialize( + c, + pcrlock_policy->nv_handle.iov_base, + pcrlock_policy->nv_handle.iov_len, + &nv_handle); + if (r < 0) + return r; + + r = tpm2_policy_authorize_nv( + c, + session, + nv_handle, + NULL); + if (r < 0) + return r; + } + if (hash_pcr_mask != 0) { TPML_PCR_SELECTION pcr_selection; tpm2_tpml_pcr_selection_from_mask(hash_pcr_mask, (TPMI_ALG_HASH)pcr_bank, &pcr_selection); @@ -3899,7 +4339,7 @@ static int tpm2_unmarshal_blob( /* Serialize a handle. This produces a binary object that can be later deserialized (by the same TPM), even * across restarts of the TPM or reboots (assuming the handle is persistent). */ -static int tpm2_serialize( +int tpm2_serialize( Tpm2Context *c, const Tpm2Handle *handle, void **ret_serialized, @@ -3925,7 +4365,7 @@ static int tpm2_serialize( return 0; } -static int tpm2_deserialize( +int tpm2_deserialize( Tpm2Context *c, const void *serialized, size_t serialized_size, @@ -4169,6 +4609,7 @@ int tpm2_unseal(Tpm2Context *c, uint32_t pubkey_pcr_mask, JsonVariant *signature, const char *pin, + const Tpm2PCRLockPolicy *pcrlock_policy, uint16_t primary_alg, const void *blob, size_t blob_size, @@ -4306,6 +4747,7 @@ int tpm2_unseal(Tpm2Context *c, pubkey_pcr_mask, signature, !!pin, + pcrlock_policy, &policy_digest); if (r < 0) return r; @@ -4350,7 +4792,312 @@ int tpm2_unseal(Tpm2Context *c, return 0; } -#endif +static TPM2_HANDLE generate_random_nv_index(void) { + return TPM2_NV_INDEX_FIRST + (TPM2_HANDLE) random_u64_range(TPM2_NV_INDEX_LAST - TPM2_NV_INDEX_FIRST + 1); +} + +int tpm2_define_policy_nv_index( + Tpm2Context *c, + const Tpm2Handle *session, + TPM2_HANDLE requested_nv_index, + const TPM2B_DIGEST *write_policy, + const char *pin, + const TPM2B_AUTH *auth, + TPM2_HANDLE *ret_nv_index, + Tpm2Handle **ret_nv_handle, + TPM2B_NV_PUBLIC *ret_nv_public) { + + _cleanup_(tpm2_handle_freep) Tpm2Handle *new_handle = NULL; + TSS2_RC rc; + int r; + + assert(c); + assert(pin || auth); + + r = tpm2_handle_new(c, &new_handle); + if (r < 0) + return r; + + new_handle->flush = false; /* This is a persistent NV index, don't flush hence */ + + TPM2B_AUTH _auth = {}; + CLEANUP_ERASE(_auth); + + if (!auth) { + r = tpm2_get_pin_auth(TPM2_ALG_SHA256, pin, &_auth); + if (r < 0) + return r; + + auth = &_auth; + } + + for (unsigned try = 0; try < 25U; try++) { + TPM2_HANDLE nv_index; + + if (requested_nv_index != 0) + nv_index = requested_nv_index; + else + nv_index = generate_random_nv_index(); + + TPM2B_NV_PUBLIC public_info = { + .size = sizeof_field(TPM2B_NV_PUBLIC, nvPublic), + .nvPublic = { + .nvIndex = nv_index, + .nameAlg = TPM2_ALG_SHA256, + .attributes = TPM2_NT_ORDINARY | TPMA_NV_WRITEALL | TPMA_NV_POLICYWRITE | TPMA_NV_OWNERREAD, + .dataSize = offsetof(TPMT_HA, digest) + tpm2_hash_alg_to_size(TPM2_ALG_SHA256), + }, + }; + + if (write_policy) + public_info.nvPublic.authPolicy = *write_policy; + + rc = sym_Esys_NV_DefineSpace( + c->esys_context, + /* authHandle= */ ESYS_TR_RH_OWNER, + /* shandle1= */ session ? session->esys_handle : ESYS_TR_PASSWORD, + /* shandle2= */ ESYS_TR_NONE, + /* shandle3= */ ESYS_TR_NONE, + auth, + &public_info, + &new_handle->esys_handle); + + if (rc == TSS2_RC_SUCCESS) { + log_debug("NV Index 0x%" PRIx32 " successfully allocated.", nv_index); + + if (ret_nv_index) + *ret_nv_index = nv_index; + + if (ret_nv_handle) + *ret_nv_handle = TAKE_PTR(new_handle); + + if (ret_nv_public) + *ret_nv_public = public_info; + + return 0; + } + if (rc != TPM2_RC_NV_DEFINED) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to allocate NV index: %s", sym_Tss2_RC_Decode(rc)); + + if (requested_nv_index != 0) { + assert(nv_index == requested_nv_index); + return log_debug_errno(SYNTHETIC_ERRNO(EEXIST), + "Requested NV index 0x%" PRIx32 " already taken.", requested_nv_index); + } + + log_debug("NV index 0x%" PRIu32 " already taken, trying another one (%u tries left)", nv_index, try); + } + + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Too many attempts trying to allocate NV index: %s", sym_Tss2_RC_Decode(rc)); +} + +int tpm2_write_policy_nv_index( + Tpm2Context *c, + const Tpm2Handle *policy_session, + TPM2_HANDLE nv_index, + const Tpm2Handle *nv_handle, + const TPM2B_DIGEST *policy_digest) { + + TSS2_RC rc; + + assert(c); + assert(policy_session); + assert(nv_handle); + assert(policy_digest); + + if (policy_digest->size != tpm2_hash_alg_to_size(TPM2_ALG_SHA256)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Policy to store in NV index has wrong size."); + + TPMT_HA ha = { + .hashAlg = TPM2_ALG_SHA256, + }; + assert(policy_digest->size <= sizeof_field(TPMT_HA, digest)); + memcpy_safe(&ha.digest, policy_digest->buffer, policy_digest->size); + + TPM2B_MAX_NV_BUFFER buffer = {}; + size_t written = 0; + rc = sym_Tss2_MU_TPMT_HA_Marshal(&ha, buffer.buffer, sizeof(buffer.buffer), &written); + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to marshal policy digest."); + + buffer.size = written; + + rc = sym_Esys_NV_Write( + c->esys_context, + /* authHandle= */ nv_handle->esys_handle, + /* nvIndex= */ nv_handle->esys_handle, + /* shandle1= */ policy_session->esys_handle, + /* shandle2= */ ESYS_TR_NONE, + /* shandle3= */ ESYS_TR_NONE, + &buffer, + /* offset= */ 0); + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to write NV index: %s", sym_Tss2_RC_Decode(rc)); + + if (DEBUG_LOGGING) { + _cleanup_free_ char *h = NULL; + h = hexmem(policy_digest->buffer, policy_digest->size); + log_debug("Written policy digest %s to NV index 0x%x", strnull(h), nv_index); + } + + return 0; +} + +int tpm2_undefine_policy_nv_index( + Tpm2Context *c, + const Tpm2Handle *session, + TPM2_HANDLE nv_index, + const Tpm2Handle *nv_handle) { + + TSS2_RC rc; + + assert(c); + assert(nv_handle); + + rc = sym_Esys_NV_UndefineSpace( + c->esys_context, + /* authHandle= */ ESYS_TR_RH_OWNER, + /* nvIndex= */ nv_handle->esys_handle, + /* shandle1= */ session ? session->esys_handle : ESYS_TR_NONE, + /* shandle2= */ ESYS_TR_NONE, + /* shandle3= */ ESYS_TR_NONE); + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to undefine NV index: %s", sym_Tss2_RC_Decode(rc)); + + log_debug("Undefined NV index 0x%x", nv_index); + return 0; +} + +int tpm2_seal_data( + Tpm2Context *c, + const struct iovec *data, + const Tpm2Handle *primary_handle, + const Tpm2Handle *encryption_session, + const TPM2B_DIGEST *policy, + struct iovec *ret_public, + struct iovec *ret_private) { + + int r; + + assert(c); + assert(data); + assert(primary_handle); + + /* This is a generic version of tpm2_seal(), that doesn't imply any policy or any specific + * combination of the two keypairs in their marshalling. tpm2_seal() is somewhat specific to the FDE + * usecase. We probably should migrate tpm2_seal() to use tpm2_seal_data() eventually. */ + + if (data->iov_len >= sizeof_field(TPMS_SENSITIVE_CREATE, data.buffer)) + return -E2BIG; + + TPMT_PUBLIC hmac_template = { + .type = TPM2_ALG_KEYEDHASH, + .nameAlg = TPM2_ALG_SHA256, + .objectAttributes = TPMA_OBJECT_FIXEDTPM | TPMA_OBJECT_FIXEDPARENT, + .parameters.keyedHashDetail.scheme.scheme = TPM2_ALG_NULL, + .unique.keyedHash.size = data->iov_len, + .authPolicy = policy ? *policy : TPM2B_DIGEST_MAKE(NULL, TPM2_SHA256_DIGEST_SIZE), + }; + + TPMS_SENSITIVE_CREATE hmac_sensitive = { + .data.size = hmac_template.unique.keyedHash.size, + }; + + CLEANUP_ERASE(hmac_sensitive); + + memcpy_safe(hmac_sensitive.data.buffer, data->iov_base, data->iov_len); + + _cleanup_(Esys_Freep) TPM2B_PUBLIC *public = NULL; + _cleanup_(Esys_Freep) TPM2B_PRIVATE *private = NULL; + r = tpm2_create(c, primary_handle, encryption_session, &hmac_template, &hmac_sensitive, &public, &private); + if (r < 0) + return r; + + _cleanup_(iovec_done) struct iovec public_blob = {}, private_blob = {}; + + r = tpm2_marshal_private(private, &private_blob.iov_base, &private_blob.iov_len); + if (r < 0) + return r; + + r = tpm2_marshal_public(public, &public_blob.iov_base, &public_blob.iov_len); + if (r < 0) + return r; + + if (ret_public) + *ret_public = TAKE_STRUCT(public_blob); + if (ret_private) + *ret_private = TAKE_STRUCT(private_blob); + + return 0; +} + +int tpm2_unseal_data( + Tpm2Context *c, + const struct iovec *public_blob, + const struct iovec *private_blob, + const Tpm2Handle *primary_handle, + const Tpm2Handle *policy_session, + const Tpm2Handle *encryption_session, + struct iovec *ret_data) { + + TSS2_RC rc; + int r; + + assert(c); + assert(public_blob); + assert(private_blob); + assert(primary_handle); + + TPM2B_PUBLIC public; + r = tpm2_unmarshal_public(public_blob->iov_base, public_blob->iov_len, &public); + if (r < 0) + return r; + + TPM2B_PRIVATE private; + r = tpm2_unmarshal_private(private_blob->iov_base, private_blob->iov_len, &private); + if (r < 0) + return r; + + _cleanup_(tpm2_handle_freep) Tpm2Handle *what = NULL; + r = tpm2_load(c, primary_handle, NULL, &public, &private, &what); + if (r < 0) + return r; + + _cleanup_(Esys_Freep) TPM2B_SENSITIVE_DATA* unsealed = NULL; + rc = sym_Esys_Unseal( + c->esys_context, + what->esys_handle, + policy_session ? policy_session->esys_handle : ESYS_TR_NONE, + encryption_session ? encryption_session->esys_handle : ESYS_TR_NONE, + ESYS_TR_NONE, + &unsealed); + if (rc == TPM2_RC_PCR_CHANGED) + return log_debug_errno(SYNTHETIC_ERRNO(ESTALE), + "PCR changed while unsealing."); + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to unseal data: %s", sym_Tss2_RC_Decode(rc)); + + _cleanup_(iovec_done) struct iovec d = {}; + d = (struct iovec) { + .iov_base = memdup(unsealed->buffer, unsealed->size), + .iov_len = unsealed->size, + }; + + explicit_bzero_safe(unsealed->buffer, unsealed->size); + + if (!d.iov_base) + return log_oom_debug(); + + *ret_data = TAKE_STRUCT(d); + return 0; +} +#endif /* HAVE_TPM2 */ int tpm2_list_devices(void) { #if HAVE_TPM2 @@ -4487,6 +5234,10 @@ const char *tpm2_userspace_log_path(void) { return secure_getenv("SYSTEMD_MEASURE_LOG_USERSPACE") ?: "/run/log/systemd/tpm2-measure.log"; } +const char *tpm2_firmware_log_path(void) { + return secure_getenv("SYSTEMD_MEASURE_LOG_FIRMWARE") ?: "/sys/kernel/security/tpm0/binary_bios_measurements"; +} + #if HAVE_OPENSSL static int tpm2_userspace_log_open(void) { _cleanup_close_ int fd = -EBADF; @@ -4719,6 +5470,574 @@ int tpm2_extend_bytes( return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "OpenSSL support is disabled."); #endif } + +const uint16_t tpm2_hash_algorithms[] = { + TPM2_ALG_SHA1, + TPM2_ALG_SHA256, + TPM2_ALG_SHA384, + TPM2_ALG_SHA512, + 0, +}; + +assert_cc(ELEMENTSOF(tpm2_hash_algorithms) == TPM2_N_HASH_ALGORITHMS + 1); + +static size_t tpm2_hash_algorithm_index(uint16_t algorithm) { + for (size_t i = 0; i < TPM2_N_HASH_ALGORITHMS; i++) + if (tpm2_hash_algorithms[i] == algorithm) + return i; + + return SIZE_MAX; +} + +TPM2B_DIGEST *tpm2_pcr_prediction_result_get_hash(Tpm2PCRPredictionResult *result, uint16_t alg) { + size_t alg_idx; + + assert(result); + + alg_idx = tpm2_hash_algorithm_index(alg); + if (alg_idx == SIZE_MAX) /* Algorithm not known? */ + return NULL; + + if (result->hash[alg_idx].size <= 0) /* No hash value for this algorithm? */ + return NULL; + + return result->hash + alg_idx; +} + +void tpm2_pcr_prediction_done(Tpm2PCRPrediction *p) { + assert(p); + + for (uint32_t pcr = 0; pcr < TPM2_PCRS_MAX; pcr++) + ordered_set_free(p->results[pcr]); +} + +static void tpm2_pcr_prediction_result_hash_func(const Tpm2PCRPredictionResult *banks, struct siphash *state) { + assert(banks); + + for (size_t i = 0; i < TPM2_N_HASH_ALGORITHMS; i++) + siphash24_compress_safe(banks->hash[i].buffer, banks->hash[i].size, state); +} + +static int tpm2_pcr_prediction_result_compare_func(const Tpm2PCRPredictionResult *a, const Tpm2PCRPredictionResult *b) { + int r; + + assert(a); + assert(b); + + for (size_t i = 0; i < TPM2_N_HASH_ALGORITHMS; i++) { + r = memcmp_nn(a->hash[i].buffer, a->hash[i].size, + b->hash[i].buffer, b->hash[i].size); + if (r != 0) + return r; + } + + return 0; +} + +DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR( + tpm2_pcr_prediction_result_hash_ops, + Tpm2PCRPredictionResult, + tpm2_pcr_prediction_result_hash_func, + tpm2_pcr_prediction_result_compare_func, + Tpm2PCRPredictionResult, + free); + +static Tpm2PCRPredictionResult *find_prediction_result_by_algorithm(OrderedSet *set, Tpm2PCRPredictionResult *result, size_t alg_idx) { + Tpm2PCRPredictionResult *f; + + assert(result); + assert(alg_idx != SIZE_MAX); + + f = ordered_set_get(set, result); /* Full match? */ + if (f) + return f; + + /* If this doesn't match full, then see if there an entry that at least matches by the relevant + * algorithm (we are fine if predictions are "incomplete" in some algorithms) */ + + ORDERED_SET_FOREACH(f, set) + if (memcmp_nn(result->hash[alg_idx].buffer, result->hash[alg_idx].size, + f->hash[alg_idx].buffer, f->hash[alg_idx].size) == 0) + return f; + + return NULL; +} + +bool tpm2_pcr_prediction_equal( + Tpm2PCRPrediction *a, + Tpm2PCRPrediction *b, + uint16_t algorithm) { + + if (a == b) + return true; + if (!a || !b) + return false; + + if (a->pcrs != b->pcrs) + return false; + + size_t alg_idx = tpm2_hash_algorithm_index(algorithm); + if (alg_idx == SIZE_MAX) + return false; + + for (uint32_t pcr = 0; pcr < TPM2_PCRS_MAX; pcr++) { + Tpm2PCRPredictionResult *banks; + + ORDERED_SET_FOREACH(banks, a->results[pcr]) + if (!find_prediction_result_by_algorithm(b->results[pcr], banks, alg_idx)) + return false; + + ORDERED_SET_FOREACH(banks, b->results[pcr]) + if (!find_prediction_result_by_algorithm(a->results[pcr], banks, alg_idx)) + return false; + } + + return true; +} + +int tpm2_pcr_prediction_to_json( + const Tpm2PCRPrediction *prediction, + uint16_t algorithm, + JsonVariant **ret) { + + _cleanup_(json_variant_unrefp) JsonVariant *aj = NULL; + int r; + + assert(prediction); + assert(ret); + + for (uint32_t pcr = 0; pcr < TPM2_PCRS_MAX; pcr++) { + _cleanup_(json_variant_unrefp) JsonVariant *vj = NULL; + Tpm2PCRPredictionResult *banks; + + if (!FLAGS_SET(prediction->pcrs, UINT32_C(1) << pcr)) + continue; + + ORDERED_SET_FOREACH(banks, prediction->results[pcr]) { + + TPM2B_DIGEST *hash = tpm2_pcr_prediction_result_get_hash(banks, algorithm); + if (!hash) + continue; + + r = json_variant_append_arrayb( + &vj, + JSON_BUILD_HEX(hash->buffer, hash->size)); + if (r < 0) + return log_error_errno(r, "Failed to append hash variant to JSON array: %m"); + } + + if (!vj) + continue; + + r = json_variant_append_arrayb( + &aj, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_INTEGER("pcr", pcr), + JSON_BUILD_PAIR_VARIANT("values", vj))); + if (r < 0) + return log_error_errno(r, "Failed to append PCR variants to JSON array: %m"); + } + + if (!aj) { + r = json_variant_new_array(&aj, NULL, 0); + if (r < 0) + return r; + } + + *ret = TAKE_PTR(aj); + return 0; +} + +int tpm2_pcr_prediction_from_json( + Tpm2PCRPrediction *prediction, + uint16_t algorithm, + JsonVariant *aj) { + + int r; + + assert(prediction); + + size_t alg_index = tpm2_hash_algorithm_index(algorithm); + assert(alg_index < TPM2_N_HASH_ALGORITHMS); + + if (!json_variant_is_array(aj)) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "PCR variant array is not an array."); + + JsonVariant *pcr; + JSON_VARIANT_ARRAY_FOREACH(pcr, aj) { + JsonVariant *nr, *values; + + nr = json_variant_by_key(pcr, "pcr"); + if (!nr) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "PCR array entry lacks PCR index field"); + + if (!json_variant_is_unsigned(nr) || + json_variant_unsigned(nr) >= TPM2_PCRS_MAX) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "PCR array entry PCR index is not an integer in the range 0…23"); + + values = json_variant_by_key(pcr, "values"); + if (!values) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "PCR array entry lacks values field"); + + if (!json_variant_is_array(values)) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "PCR array entry values field is not an array"); + + prediction->pcrs |= UINT32_C(1) << json_variant_unsigned(nr); + + JsonVariant *v; + JSON_VARIANT_ARRAY_FOREACH(v, values) { + _cleanup_free_ void *buffer = NULL; + size_t size; + + r = json_variant_unhex(v, &buffer, &size); + if (r < 0) + return log_error_errno(r, "Failed to decode PCR policy array hash value"); + + if (size <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "PCR policy array hash value is zero."); + + if (size > sizeof_field(TPM2B_DIGEST, buffer)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "PCR policy array hash value is too large."); + + _cleanup_free_ Tpm2PCRPredictionResult *banks = new0(Tpm2PCRPredictionResult, 1); + if (!banks) + return log_oom(); + + memcpy(banks->hash[alg_index].buffer, buffer, size); + banks->hash[alg_index].size = size; + + r = ordered_set_ensure_put(prediction->results + json_variant_unsigned(nr), &tpm2_pcr_prediction_result_hash_ops, banks); + if (r == -EEXIST) /* Let's allow duplicates */ + continue; + if (r < 0) + return log_error_errno(r, "Failed to insert result into set: %m"); + + TAKE_PTR(banks); + } + } + + return 0; +} + +int tpm2_calculate_policy_super_pcr( + Tpm2PCRPrediction *prediction, + uint16_t algorithm, + TPM2B_DIGEST *pcr_policy) { + + int r; + + assert_se(prediction); + assert_se(pcr_policy); + + /* Start with a zero policy if not specified otheriwse */ + TPM2B_DIGEST super_pcr_policy_digest = *pcr_policy; + + /* First we look for all PCRs that have exactly one allowed hash value, and generate a single PolicyPCR policy from them */ + _cleanup_free_ Tpm2PCRValue *single_values = NULL; + size_t n_single_values = 0; + for (uint32_t pcr = 0; pcr < TPM2_PCRS_MAX; pcr++) { + if (!FLAGS_SET(prediction->pcrs, UINT32_C(1) << pcr)) + continue; + + if (ordered_set_size(prediction->results[pcr]) != 1) + continue; + + log_debug("Including PCR %" PRIu32 " in single value PolicyPCR expression", pcr); + + Tpm2PCRPredictionResult *banks = ASSERT_PTR(ordered_set_first(prediction->results[pcr])); + + TPM2B_DIGEST *hash = tpm2_pcr_prediction_result_get_hash(banks, algorithm); + if (!hash) + continue; + + if (!GREEDY_REALLOC(single_values, n_single_values + 1)) + return -ENOMEM; + + single_values[n_single_values++] = TPM2_PCR_VALUE_MAKE(pcr, algorithm, *hash); + } + + if (n_single_values > 0) { + /* Evolve policy based on the expected PCR value for what we found. */ + r = tpm2_calculate_policy_pcr( + single_values, + n_single_values, + &super_pcr_policy_digest); + if (r < 0) + return r; + } + + /* Now deal with the PCRs for which we have variants, i.e. more than one allowed values */ + for (uint32_t pcr = 0; pcr < TPM2_PCRS_MAX; pcr++) { + _cleanup_free_ TPM2B_DIGEST *pcr_policy_digest_variants = NULL; + size_t n_pcr_policy_digest_variants = 0; + Tpm2PCRPredictionResult *banks; + + if (!FLAGS_SET(prediction->pcrs, UINT32_C(1) << pcr)) + continue; + + if (ordered_set_size(prediction->results[pcr]) <= 1) /* We only care for PCRs with 2 or more variants in this loop */ + continue; + + if (ordered_set_size(prediction->results[pcr]) > 8) + return log_error_errno(SYNTHETIC_ERRNO(E2BIG), "PCR policies with more than 8 alternatives per PCR are currently not supported."); + + ORDERED_SET_FOREACH(banks, prediction->results[pcr]) { + /* Start from the super PCR policy from the previous PCR we looked at so far. */ + TPM2B_DIGEST pcr_policy_digest = super_pcr_policy_digest; + + TPM2B_DIGEST *hash = tpm2_pcr_prediction_result_get_hash(banks, algorithm); + if (!hash) + continue; + + /* Evolve it based on the expected PCR value for this PCR */ + r = tpm2_calculate_policy_pcr( + &TPM2_PCR_VALUE_MAKE( + pcr, + algorithm, + *hash), + /* n_pcr_values= */ 1, + &pcr_policy_digest); + if (r < 0) + return r; + + /* Store away this new variant */ + if (!GREEDY_REALLOC(pcr_policy_digest_variants, n_pcr_policy_digest_variants + 1)) + return log_oom(); + + pcr_policy_digest_variants[n_pcr_policy_digest_variants++] = pcr_policy_digest; + + log_debug("Calculated PCR policy variant %zu for PCR %" PRIu32, n_pcr_policy_digest_variants, pcr); + } + + assert_se(n_pcr_policy_digest_variants >= 2); + assert_se(n_pcr_policy_digest_variants <= 8); + + /* Now combine all our variant into one OR policy */ + r = tpm2_calculate_policy_or( + pcr_policy_digest_variants, + n_pcr_policy_digest_variants, + &super_pcr_policy_digest); + if (r < 0) + return r; + + log_debug("Combined %zu variants in OR policy.", n_pcr_policy_digest_variants); + } + + *pcr_policy = super_pcr_policy_digest; + return 0; +} + +int tpm2_policy_super_pcr( + Tpm2Context *c, + const Tpm2Handle *session, + const Tpm2PCRPrediction *prediction, + uint16_t algorithm) { + + int r; + + assert_se(c); + assert_se(session); + assert_se(prediction); + + TPM2B_DIGEST previous_policy_digest = TPM2B_DIGEST_MAKE(NULL, TPM2_SHA256_DIGEST_SIZE); + + uint32_t single_value_pcrs = 0; + + /* Look for all PCRs that have only a singled allowed hash value, and synthesize a single PolicyPCR policy item for them */ + for (uint32_t pcr = 0; pcr < TPM2_PCRS_MAX; pcr++) { + if (!FLAGS_SET(prediction->pcrs, UINT32_C(1) << pcr)) + continue; + + if (ordered_set_size(prediction->results[pcr]) != 1) + continue; + + log_debug("Including PCR %" PRIu32 " in single value PolicyPCR expression", pcr); + + single_value_pcrs |= UINT32_C(1) << pcr; + } + + if (single_value_pcrs != 0) { + TPML_PCR_SELECTION pcr_selection; + tpm2_tpml_pcr_selection_from_mask(single_value_pcrs, algorithm, &pcr_selection); + + _cleanup_free_ TPM2B_DIGEST *current_policy_digest = NULL; + r = tpm2_policy_pcr( + c, + session, + &pcr_selection, + ¤t_policy_digest); + if (r < 0) + return r; + + previous_policy_digest = *current_policy_digest; + } + + for (uint32_t pcr = 0; pcr < TPM2_PCRS_MAX; pcr++) { + size_t n_branches; + + if (!FLAGS_SET(prediction->pcrs, UINT32_C(1) << pcr)) + continue; + + n_branches = ordered_set_size(prediction->results[pcr]); + if (n_branches < 1 || n_branches > 8) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Number of variants per PCR not in range 1…8"); + + if (n_branches == 1) /* Single choice PCRs are already covered by the loop above */ + continue; + + log_debug("Submitting PCR/OR policy for PCR %" PRIu32, pcr); + + TPML_PCR_SELECTION pcr_selection; + tpm2_tpml_pcr_selection_from_mask(UINT32_C(1) << pcr, algorithm, &pcr_selection); + + _cleanup_free_ TPM2B_DIGEST *current_policy_digest = NULL; + r = tpm2_policy_pcr( + c, + session, + &pcr_selection, + ¤t_policy_digest); + if (r < 0) + return r; + + _cleanup_free_ TPM2B_DIGEST *branches = NULL; + branches = new0(TPM2B_DIGEST, n_branches); + if (!branches) + return log_oom(); + + Tpm2PCRPredictionResult *banks; + size_t i = 0; + ORDERED_SET_FOREACH(banks, prediction->results[pcr]) { + TPM2B_DIGEST pcr_policy_digest = previous_policy_digest; + + TPM2B_DIGEST *hash = tpm2_pcr_prediction_result_get_hash(banks, algorithm); + if (!hash) + continue; + + /* Evolve it based on the expected PCR value for this PCR */ + r = tpm2_calculate_policy_pcr( + &TPM2_PCR_VALUE_MAKE( + pcr, + algorithm, + *hash), + /* n_pcr_values= */ 1, + &pcr_policy_digest); + if (r < 0) + return r; + + branches[i++] = pcr_policy_digest; + } + + assert_se(i == n_branches); + + current_policy_digest = mfree(current_policy_digest); + r = tpm2_policy_or( + c, + session, + branches, + n_branches, + ¤t_policy_digest); + if (r < 0) + return r; + + previous_policy_digest = *current_policy_digest; + } + + return 0; +} + +void tpm2_pcrlock_policy_done(Tpm2PCRLockPolicy *data) { + assert(data); + + data->prediction_json = json_variant_unref(data->prediction_json); + tpm2_pcr_prediction_done(&data->prediction); + iovec_done(&data->nv_handle); + iovec_done(&data->nv_public); + iovec_done(&data->srk_handle); + iovec_done(&data->pin_public); + iovec_done(&data->pin_private); +} + +static int json_dispatch_tpm2_algorithm(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + uint16_t *algorithm = ASSERT_PTR(userdata); + int r; + + r = tpm2_hash_alg_from_string(json_variant_string(variant)); + if (r < 0 || tpm2_hash_algorithm_index(r) == SIZE_MAX) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid hash algorithm: %s", json_variant_string(variant)); + + *algorithm = r; + return 0; +} + +int tpm2_pcrlock_search_file(const char *path, FILE **ret_file, char **ret_path) { + static const char search[] = + "/run/systemd\0" + "/var/lib/systemd\0"; + + int r; + + if (!path) + path = "pcrlock.json"; + + r = search_and_fopen_nulstr(path, ret_file ? "re" : NULL, NULL, search, ret_file, ret_path); + if (r < 0) + return log_debug_errno(r, "Failed to find TPM2 pcrlock policy file '%s': %m", path); + + return 0; +} + +int tpm2_pcrlock_policy_load( + const char *path, + Tpm2PCRLockPolicy *ret_policy) { + + _cleanup_free_ char *discovered_path = NULL; + _cleanup_fclose_ FILE *f = NULL; + int r; + + r = tpm2_pcrlock_search_file(path, &f, &discovered_path); + if (r == -ENOENT) { + *ret_policy = (Tpm2PCRLockPolicy) {}; + return 0; + } + if (r < 0) + return log_error_errno(r, "Failed to load TPM2 pcrlock policy file: %m"); + + _cleanup_(json_variant_unrefp) JsonVariant *configuration_json = NULL; + r = json_parse_file( + f, + discovered_path, + /* flags = */ 0, + &configuration_json, + /* ret_line= */ NULL, + /* ret_column= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to parse existing pcrlock policy file '%s': %m", discovered_path); + + JsonDispatch policy_dispatch[] = { + { "pcrBank", JSON_VARIANT_STRING, json_dispatch_tpm2_algorithm, offsetof(Tpm2PCRLockPolicy, algorithm), JSON_MANDATORY }, + { "pcrValues", JSON_VARIANT_ARRAY, json_dispatch_variant, offsetof(Tpm2PCRLockPolicy, prediction_json), JSON_MANDATORY }, + { "nvIndex", JSON_VARIANT_INTEGER, json_dispatch_uint32, offsetof(Tpm2PCRLockPolicy, nv_index), JSON_MANDATORY }, + { "nvHandle", JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, offsetof(Tpm2PCRLockPolicy, nv_handle), JSON_MANDATORY }, + { "nvPublic", JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, offsetof(Tpm2PCRLockPolicy, nv_public), JSON_MANDATORY }, + { "srkHandle", JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, offsetof(Tpm2PCRLockPolicy, srk_handle), JSON_MANDATORY }, + { "pinPublic", JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, offsetof(Tpm2PCRLockPolicy, pin_public), JSON_MANDATORY }, + { "pinPrivate", JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, offsetof(Tpm2PCRLockPolicy, pin_private), JSON_MANDATORY }, + {} + }; + + _cleanup_(tpm2_pcrlock_policy_done) Tpm2PCRLockPolicy policy = {}; + + r = json_dispatch(configuration_json, policy_dispatch, JSON_LOG, &policy); + if (r < 0) + return r; + + r = tpm2_pcr_prediction_from_json(&policy.prediction, policy.algorithm, policy.prediction_json); + if (r < 0) + return r; + + *ret_policy = TAKE_STRUCT(policy); + return 1; +} #endif char *tpm2_pcr_mask_to_string(uint32_t mask) { @@ -4842,6 +6161,7 @@ int tpm2_make_luks2_json( JSON_BUILD_PAIR_CONDITION(!!tpm2_asym_alg_to_string(primary_alg), "tpm2-primary-alg", JSON_BUILD_STRING(tpm2_asym_alg_to_string(primary_alg))), JSON_BUILD_PAIR("tpm2-policy-hash", JSON_BUILD_HEX(policy_hash, policy_hash_size)), JSON_BUILD_PAIR("tpm2-pin", JSON_BUILD_BOOLEAN(flags & TPM2_FLAGS_USE_PIN)), + JSON_BUILD_PAIR("tpm2-pcrlock", JSON_BUILD_BOOLEAN(flags & TPM2_FLAGS_USE_PCRLOCK)), JSON_BUILD_PAIR_CONDITION(pubkey_pcr_mask != 0, "tpm2_pubkey_pcrs", JSON_BUILD_VARIANT(pkmj)), JSON_BUILD_PAIR_CONDITION(pubkey_pcr_mask != 0, "tpm2_pubkey", JSON_BUILD_BASE64(pubkey, pubkey_size)), JSON_BUILD_PAIR_CONDITION(salt, "tpm2_salt", JSON_BUILD_BASE64(salt, salt_size)), @@ -4960,6 +6280,14 @@ int tpm2_parse_luks2_json( SET_FLAG(flags, TPM2_FLAGS_USE_PIN, json_variant_boolean(w)); } + w = json_variant_by_key(v, "tpm2-pcrlock"); + if (w) { + if (!json_variant_is_boolean(w)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "TPM2 pclock policy is not a boolean."); + + SET_FLAG(flags, TPM2_FLAGS_USE_PCRLOCK, json_variant_boolean(w)); + } + w = json_variant_by_key(v, "tpm2_salt"); if (w) { r = json_variant_unbase64(w, &salt, &salt_size); diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index ce75563e58..35d851a0d0 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -8,11 +8,13 @@ #include "json.h" #include "macro.h" #include "openssl-util.h" +#include "ordered-set.h" #include "sha256.h" #include "tpm2-pcr.h" typedef enum TPM2Flags { - TPM2_FLAGS_USE_PIN = 1 << 0, + TPM2_FLAGS_USE_PIN = 1 << 0, + TPM2_FLAGS_USE_PCRLOCK = 1 << 1, } TPM2Flags; /* As per https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClient_PFP_r1p05_v23_pub.pdf a @@ -37,6 +39,8 @@ static inline bool TPM2_PCR_MASK_VALID(uint32_t pcr_mask) { #define FOREACH_PCR_IN_MASK(pcr, mask) BIT_FOREACH(pcr, mask) +#define TPM2_N_HASH_ALGORITHMS 4U + #if HAVE_TPM2 #include @@ -111,9 +115,14 @@ char *tpm2_pcr_values_to_string(const Tpm2PCRValue *pcr_values, size_t n_pcr_val int tpm2_pcr_values_hash_count(const Tpm2PCRValue *pcr_values, size_t n_pcr_values, size_t *ret_count); int tpm2_tpml_pcr_selection_from_pcr_values(const Tpm2PCRValue *pcr_values, size_t n_pcr_values, TPML_PCR_SELECTION *ret_selection, TPM2B_DIGEST **ret_values, size_t *ret_n_values); +int tpm2_make_encryption_session(Tpm2Context *c, const Tpm2Handle *primary, const Tpm2Handle *bind_key, Tpm2Handle **ret_session); + int tpm2_create_primary(Tpm2Context *c, const Tpm2Handle *session, const TPM2B_PUBLIC *template, const TPM2B_SENSITIVE_CREATE *sensitive, TPM2B_PUBLIC **ret_public, Tpm2Handle **ret_handle); int tpm2_create(Tpm2Context *c, const Tpm2Handle *parent, const Tpm2Handle *session, const TPMT_PUBLIC *template, const TPMS_SENSITIVE_CREATE *sensitive, TPM2B_PUBLIC **ret_public, TPM2B_PRIVATE **ret_private); int tpm2_create_loaded(Tpm2Context *c, const Tpm2Handle *parent, const Tpm2Handle *session, const TPMT_PUBLIC *template, const TPMS_SENSITIVE_CREATE *sensitive, TPM2B_PUBLIC **ret_public, TPM2B_PRIVATE **ret_private, Tpm2Handle **ret_handle); +int tpm2_load(Tpm2Context *c, const Tpm2Handle *parent, const Tpm2Handle *session, const TPM2B_PUBLIC *public, const TPM2B_PRIVATE *private, Tpm2Handle **ret_handle); +int tpm2_marshal_nv_public(const TPM2B_NV_PUBLIC *nv_public, void **ret, size_t *ret_size); +int tpm2_unmarshal_nv_public(const void *data, size_t size, TPM2B_NV_PUBLIC *ret_nv_public); bool tpm2_supports_alg(Tpm2Context *c, TPM2_ALG_ID alg); bool tpm2_supports_command(Tpm2Context *c, TPM2_CC command); @@ -126,6 +135,7 @@ int tpm2_get_good_pcr_banks_strv(Tpm2Context *c, uint32_t pcr_mask, char ***ret) int tpm2_get_best_pcr_bank(Tpm2Context *c, uint32_t pcr_mask, TPMI_ALG_HASH *ret); const char *tpm2_userspace_log_path(void); +const char *tpm2_firmware_log_path(void); typedef enum Tpm2UserspaceEventType { TPM2_EVENT_PHASE, @@ -184,22 +194,85 @@ void tpm2_log_debug_buffer(const void *buffer, size_t size, const char *msg); void tpm2_log_debug_digest(const TPM2B_DIGEST *digest, const char *msg); void tpm2_log_debug_name(const TPM2B_NAME *name, const char *msg); +typedef struct Tpm2PCRPredictionResult { + TPM2B_DIGEST hash[TPM2_N_HASH_ALGORITHMS]; /* a hash for each potential algorithm */ +} Tpm2PCRPredictionResult; + +TPM2B_DIGEST *tpm2_pcr_prediction_result_get_hash(Tpm2PCRPredictionResult *result, uint16_t alg); + +/* A structure encapsulating a full set of PCR predictions with alternatives. This can be converted into a + * series of PolicyOR + PolicyPCR items for the TPM. */ +typedef struct Tpm2PCRPrediction { + uint32_t pcrs; /* A mask of pcrs included */ + OrderedSet* results[TPM2_PCRS_MAX]; /* set of Tpm2PCRPredictionResult objects, one for each PCR */ +} Tpm2PCRPrediction; + +void tpm2_pcr_prediction_done(Tpm2PCRPrediction *p); + +extern const struct hash_ops tpm2_pcr_prediction_result_hash_ops; + +bool tpm2_pcr_prediction_equal(Tpm2PCRPrediction *a, Tpm2PCRPrediction *b, uint16_t algorithm); + +int tpm2_pcr_prediction_to_json(const Tpm2PCRPrediction *prediction, uint16_t algorithm, JsonVariant **ret); +int tpm2_pcr_prediction_from_json(Tpm2PCRPrediction *prediction, uint16_t algorithm, JsonVariant *aj); + +/* As structure encapsulating all metadata stored for a pcrlock policy on disk */ +typedef struct Tpm2PCRLockPolicy { + /* The below is the fixed metadata encoding information about the NV index we store the + * PolicyAuthorizeNV policy in, as well as a pinned SRK, and the encrypted PIN to use for writing to + * the NV Index. */ + uint16_t algorithm; + uint32_t nv_index; + struct iovec nv_handle; + struct iovec nv_public; + struct iovec srk_handle; + struct iovec pin_public; + struct iovec pin_private; + + /* The below contains the current prediction whose resulting policy is stored in the NV + * index. Once in JSON and once in parsed form. When the policy is updated the fields below are + * changed, the fields above remain fixed. */ + JsonVariant *prediction_json; + Tpm2PCRPrediction prediction; +} Tpm2PCRLockPolicy; + +void tpm2_pcrlock_policy_done(Tpm2PCRLockPolicy *data); +int tpm2_pcrlock_search_file(const char *path, FILE **ret_file, char **ret_path); +int tpm2_pcrlock_policy_load(const char *path, Tpm2PCRLockPolicy *ret_policy); + int tpm2_index_to_handle(Tpm2Context *c, TPM2_HANDLE index, const Tpm2Handle *session, TPM2B_PUBLIC **ret_public, TPM2B_NAME **ret_name, TPM2B_NAME **ret_qname, Tpm2Handle **ret_handle); int tpm2_index_from_handle(Tpm2Context *c, const Tpm2Handle *handle, TPM2_HANDLE *ret_index); int tpm2_pcr_read(Tpm2Context *c, const TPML_PCR_SELECTION *pcr_selection, Tpm2PCRValue **ret_pcr_values, size_t *ret_n_pcr_values); int tpm2_pcr_read_missing_values(Tpm2Context *c, Tpm2PCRValue *pcr_values, size_t n_pcr_values); +int tpm2_get_pin_auth(TPMI_ALG_HASH hash, const char *pin, TPM2B_AUTH *ret_auth); +int tpm2_set_auth(Tpm2Context *c, const Tpm2Handle *handle, const char *pin); +int tpm2_set_auth_binary(Tpm2Context *c, const Tpm2Handle *handle, const TPM2B_AUTH *auth); + +int tpm2_make_policy_session(Tpm2Context *c, const Tpm2Handle *primary, const Tpm2Handle *encryption_session, Tpm2Handle **ret_session); + +int tpm2_policy_auth_value(Tpm2Context *c, const Tpm2Handle *session, TPM2B_DIGEST **ret_policy_digest); +int tpm2_policy_authorize_nv(Tpm2Context *c, const Tpm2Handle *session, const Tpm2Handle *nv_handle, TPM2B_DIGEST **ret_policy_digest); +int tpm2_policy_pcr(Tpm2Context *c, const Tpm2Handle *session, const TPML_PCR_SELECTION *pcr_selection, TPM2B_DIGEST **ret_policy_digest); +int tpm2_policy_or(Tpm2Context *c, const Tpm2Handle *session, const TPM2B_DIGEST *branches, size_t n_branches, TPM2B_DIGEST **ret_policy_digest); +int tpm2_policy_super_pcr(Tpm2Context *c, const Tpm2Handle *session, const Tpm2PCRPrediction *prediction, uint16_t algorithm); + int tpm2_calculate_pubkey_name(const TPMT_PUBLIC *public, TPM2B_NAME *ret_name); +int tpm2_calculate_nv_index_name(const TPMS_NV_PUBLIC *nvpublic, TPM2B_NAME *ret_name); + int tpm2_calculate_policy_auth_value(TPM2B_DIGEST *digest); int tpm2_calculate_policy_authorize(const TPM2B_PUBLIC *public, const TPM2B_DIGEST *policy_ref, TPM2B_DIGEST *digest); +int tpm2_calculate_policy_authorize_nv(const TPM2B_NV_PUBLIC *public, TPM2B_DIGEST *digest); int tpm2_calculate_policy_pcr(const Tpm2PCRValue *pcr_values, size_t n_pcr_values, TPM2B_DIGEST *digest); -int tpm2_calculate_sealing_policy(const Tpm2PCRValue *pcr_values, size_t n_pcr_values, const TPM2B_PUBLIC *public, bool use_pin, TPM2B_DIGEST *digest); +int tpm2_calculate_policy_or(const TPM2B_DIGEST *branches, size_t n_branches, TPM2B_DIGEST *digest); +int tpm2_calculate_policy_super_pcr(Tpm2PCRPrediction *prediction, uint16_t algorithm, TPM2B_DIGEST *pcr_policy); +int tpm2_calculate_sealing_policy(const Tpm2PCRValue *pcr_values, size_t n_pcr_values, const TPM2B_PUBLIC *public, bool use_pin, const Tpm2PCRLockPolicy *policy, TPM2B_DIGEST *digest); int tpm2_get_or_create_srk(Tpm2Context *c, const Tpm2Handle *session, TPM2B_PUBLIC **ret_public, TPM2B_NAME **ret_name, TPM2B_NAME **ret_qname, Tpm2Handle **ret_handle); int tpm2_seal(Tpm2Context *c, uint32_t seal_key_handle, const TPM2B_DIGEST *policy, const char *pin, void **ret_secret, size_t *ret_secret_size, void **ret_blob, size_t *ret_blob_size, uint16_t *ret_primary_alg, void **ret_srk_buf, size_t *ret_srk_buf_size); -int tpm2_unseal(Tpm2Context *c, uint32_t hash_pcr_mask, uint16_t pcr_bank, const void *pubkey, size_t pubkey_size, uint32_t pubkey_pcr_mask, JsonVariant *signature, const char *pin, uint16_t primary_alg, const void *blob, size_t blob_size, const void *policy_hash, size_t policy_hash_size, const void *srk_buf, size_t srk_buf_size, void **ret_secret, size_t *ret_secret_size); +int tpm2_unseal(Tpm2Context *c, uint32_t hash_pcr_mask, uint16_t pcr_bank, const void *pubkey, size_t pubkey_size, uint32_t pubkey_pcr_mask, JsonVariant *signature, const char *pin, const Tpm2PCRLockPolicy *pcrlock_policy, uint16_t primary_alg, const void *blob, size_t blob_size, const void *policy_hash, size_t policy_hash_size, const void *srk_buf, size_t srk_buf_size, void **ret_secret, size_t *ret_secret_size); #if HAVE_OPENSSL int tpm2_tpm2b_public_to_openssl_pkey(const TPM2B_PUBLIC *public, EVP_PKEY **ret); @@ -209,6 +282,16 @@ int tpm2_tpm2b_public_from_openssl_pkey(const EVP_PKEY *pkey, TPM2B_PUBLIC *ret) int tpm2_tpm2b_public_from_pem(const void *pem, size_t pem_size, TPM2B_PUBLIC *ret); int tpm2_tpm2b_public_to_fingerprint(const TPM2B_PUBLIC *public, void **ret_fingerprint, size_t *ret_fingerprint_size); +int tpm2_define_policy_nv_index(Tpm2Context *c, const Tpm2Handle *session, TPM2_HANDLE requested_nv_index, const TPM2B_DIGEST *write_policy, const char *pin, const TPM2B_AUTH *auth, TPM2_HANDLE *ret_nv_index, Tpm2Handle **ret_nv_handle, TPM2B_NV_PUBLIC *ret_nv_public); +int tpm2_write_policy_nv_index(Tpm2Context *c, const Tpm2Handle *policy_session, TPM2_HANDLE nv_index, const Tpm2Handle *nv_handle, const TPM2B_DIGEST *policy_digest); +int tpm2_undefine_policy_nv_index(Tpm2Context *c, const Tpm2Handle *session, TPM2_HANDLE nv_index, const Tpm2Handle *nv_handle); + +int tpm2_seal_data(Tpm2Context *c, const struct iovec *data, const Tpm2Handle *primary_handle, const Tpm2Handle *encryption_session, const TPM2B_DIGEST *policy, struct iovec *ret_public, struct iovec *ret_private); +int tpm2_unseal_data(Tpm2Context *c, const struct iovec *public, const struct iovec *private, const Tpm2Handle *primary_handle, const Tpm2Handle *policy_session, const Tpm2Handle *encryption_session, struct iovec *ret_data); + +int tpm2_serialize(Tpm2Context *c, const Tpm2Handle *handle, void **ret_serialized, size_t *ret_serialized_size); +int tpm2_deserialize(Tpm2Context *c, const void *serialized, size_t serialized_size, Tpm2Handle **ret_handle); + /* The tpm2-tss library has many structs that are simply a combination of an array (or object) and * size. These macros allow easily initializing or assigning instances of such structs from an existing * buffer/object and size, while also checking the size for safety with the struct buffer/object size. If the @@ -271,6 +354,11 @@ typedef struct {} Tpm2Handle; typedef struct {} Tpm2PCRValue; #define TPM2_PCR_VALUE_MAKE(i, h, v) (Tpm2PCRValue) {} + +static inline int tpm2_pcrlock_search_file(const char *path, FILE **ret_file, char **ret_path) { + return -ENOENT; +} + #endif /* HAVE_TPM2 */ int tpm2_list_devices(void); @@ -322,10 +410,13 @@ int tpm2_asym_alg_from_string(const char *alg) _pure_; char *tpm2_pcr_mask_to_string(uint32_t mask); +extern const uint16_t tpm2_hash_algorithms[]; + typedef struct { uint32_t search_pcr_mask; const char *device; const char *signature_path; + const char *pcrlock_path; } systemd_tpm2_plugin_params; typedef enum Tpm2Support { diff --git a/src/test/test-tpm2.c b/src/test/test-tpm2.c index 80e20db360..7cefb0c93b 100644 --- a/src/test/test-tpm2.c +++ b/src/test/test-tpm2.c @@ -1043,6 +1043,7 @@ static void check_seal_unseal_for_handle(Tpm2Context *c, TPM2_HANDLE handle) { /* pubkey_pcr_mask= */ 0, /* signature= */ NULL, /* pin= */ NULL, + /* pcrlock_policy= */ NULL, /* primary_alg= */ 0, blob, blob_size, /* policy_hash= */ NULL, /* policy_hash_size= */ 0, diff --git a/test/units/testsuite-70.pcrlock.sh b/test/units/testsuite-70.pcrlock.sh new file mode 100755 index 0000000000..415bfa8ff4 --- /dev/null +++ b/test/units/testsuite-70.pcrlock.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +export SYSTEMD_LOG_LEVEL=debug +export PAGER= +SD_PCREXTEND="/usr/lib/systemd/systemd-pcrextend" +SD_PCRLOCK="/usr/lib/systemd/systemd-pcrlock" + +if [[ ! -x "${SD_PCREXTEND:?}" ]] || [[ ! -x "${SD_PCRLOCK:?}" ]] ; then + echo "$SD_PCREXTEND or $SD_PCRLOCK not found, skipping pcrlock tests" + exit 0 +fi + +at_exit() { + if [[ $? -ne 0 ]]; then + # Dump the event log on fail, to make debugging a bit easier + [[ -e /run/log/systemd/tpm2-measure.log ]] && jq --seq --slurp /tmp/pcrlockpwd +chmod 0600 /tmp/pcrlockpwd +cryptsetup luksFormat -q --pbkdf pbkdf2 --pbkdf-force-iterations 1000 --use-urandom "$img" /tmp/pcrlockpwd + +systemd-cryptenroll --unlock-key-file=/tmp/pcrlockpwd --tpm2-device=auto --tpm2-pcrlock=/var/lib/systemd/pcrlock.json --tpm2-public-key= --wipe-slot=tpm2 "$img" +systemd-cryptsetup attach pcrlock "$img" - tpm2-device=auto,tpm2-pcrlock=/var/lib/systemd/pcrlock.json +systemd-cryptsetup detach pcrlock + +# Measure something into PCR 16 (the "debug" PCR), which should make the activation fail +"$SD_PCREXTEND" --pcr=16 test70 + +"$SD_PCRLOCK" cel --json=pretty + +(! systemd-cryptsetup attach pcrlock "$img" - tpm2-device=auto,tpm2-pcrlock=/var/lib/systemd/pcrlock.json,headless ) + +# Now add a component for it, rebuild policy and it should work (we'll rebuild +# once like that, but don't provide the recovery pin. This should fail, since +# the PCR is hosed after all. But then we'll use recovery pin, and it should +# work. +echo -n test70 | "$SD_PCRLOCK" lock-raw --pcrlock=/var/lib/pcrlock.d/910-test70.pcrlock --pcr=16 +(! "$SD_PCRLOCK" make-policy --pcr="$PCRS") +PIN=huhu "$SD_PCRLOCK" make-policy --pcr="$PCRS" --recovery-pin=yes + +systemd-cryptsetup attach pcrlock "$img" - tpm2-device=auto,tpm2-pcrlock=/var/lib/systemd/pcrlock.json +systemd-cryptsetup detach pcrlock + +# And now let's do it the clean way, and generate the right policy ahead of time. +echo -n test70-take-two | "$SD_PCRLOCK" lock-raw --pcrlock=/var/lib/pcrlock.d/920-test70.pcrlock --pcr=16 +"$SD_PCRLOCK" make-policy --pcr="$PCRS" + +"$SD_PCREXTEND" --pcr=16 test70-take-two + +"$SD_PCRLOCK" cel --json=pretty + +systemd-cryptsetup attach pcrlock "$img" - tpm2-device=auto,tpm2-pcrlock=/var/lib/systemd/pcrlock.json +systemd-cryptsetup detach pcrlock + +"$SD_PCRLOCK" remove-policy + +"$SD_PCRLOCK" unlock-firmware-config +"$SD_PCRLOCK" unlock-gpt +"$SD_PCRLOCK" unlock-machine-id +"$SD_PCRLOCK" unlock-file-system +"$SD_PCRLOCK" unlock-raw --pcrlock=/var/lib/pcrlock.d/910-test70.pcrlock +"$SD_PCRLOCK" unlock-raw --pcrlock=/var/lib/pcrlock.d/920-test70.pcrlock + +(! "$SD_PCRLOCK" "") +(! "$SD_PCRLOCK" predict --pcr=-1) +(! "$SD_PCRLOCK" predict --pcr=foo) +(! "$SD_PCRLOCK" predict --pcr=1+1) +(! "$SD_PCRLOCK" predict --pcr=1+++++1) +(! "$SD_PCRLOCK" make-policy --nv-index=0) +(! "$SD_PCRLOCK" make-policy --nv-index=foo) +(! "$SD_PCRLOCK" list-components --location=:) +(! "$SD_PCRLOCK" lock-gpt "") +(! "$SD_PCRLOCK" lock-gpt /dev/sr0) +(! "$SD_PCRLOCK" lock-pe /dev/full) +(! "$SD_PCRLOCK" lock-pe /bin/true) +(! "$SD_PCRLOCK" lock-uki /dev/full) +(! "$SD_PCRLOCK" lock-uki /bin/true) +(! "$SD_PCRLOCK" lock-file-system "") + +rm "$img" /tmp/pcrlockpwd diff --git a/units/meson.build b/units/meson.build index 51ae9cee5d..e7bfb7f838 100644 --- a/units/meson.build +++ b/units/meson.build @@ -477,6 +477,34 @@ units = [ 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'], 'symlinks' : ['sysinit.target.wants/'], }, + { + 'file' : 'systemd-pcrlock-make-policy.service.in', + 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'], + }, + { + 'file' : 'systemd-pcrlock-secureboot-policy.service.in', + 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'], + }, + { + 'file' : 'systemd-pcrlock-secureboot-authority.service.in', + 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'], + }, + { + 'file' : 'systemd-pcrlock-file-system.service.in', + 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'], + }, + { + 'file' : 'systemd-pcrlock-machine-id.service.in', + 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'], + }, + { + 'file' : 'systemd-pcrlock-firmware-code.service.in', + 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'], + }, + { + 'file' : 'systemd-pcrlock-firmware-config.service.in', + 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'], + }, { 'file' : 'systemd-portabled.service.in', 'conditions' : ['ENABLE_PORTABLED'], diff --git a/units/systemd-pcrlock-file-system.service.in b/units/systemd-pcrlock-file-system.service.in new file mode 100644 index 0000000000..d68a42e09a --- /dev/null +++ b/units/systemd-pcrlock-file-system.service.in @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Lock File Systems to TPM2 PCR Policy +Documentation=man:systemd-pcrlock(8) +DefaultDependencies=no +Conflicts=shutdown.target +Before=sysinit.target shutdown.target systemd-pcrlock-make-policy.service +ConditionPathExists=!/etc/initrd-release +ConditionSecurity=measured-uki + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart={{LIBEXECDIR}}/systemd-pcrlock lock-file-system + +[Install] +WantedBy=sysinit.target diff --git a/units/systemd-pcrlock-firmware-code.service.in b/units/systemd-pcrlock-firmware-code.service.in new file mode 100644 index 0000000000..a24f2ba015 --- /dev/null +++ b/units/systemd-pcrlock-firmware-code.service.in @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Lock Firmware Code to TPM2 PCR Policy +Documentation=man:systemd-pcrlock(8) +DefaultDependencies=no +Conflicts=shutdown.target +After=systemd-tpm2-setup.service +Before=sysinit.target shutdown.target systemd-pcrlock-make-policy.service +ConditionPathExists=!/etc/initrd-release +ConditionSecurity=measured-uki + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart={{LIBEXECDIR}}/systemd-pcrlock lock-firmware-code + +[Install] +WantedBy=sysinit.target diff --git a/units/systemd-pcrlock-firmware-config.service.in b/units/systemd-pcrlock-firmware-config.service.in new file mode 100644 index 0000000000..64e63f86a6 --- /dev/null +++ b/units/systemd-pcrlock-firmware-config.service.in @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Lock Firmware Configuration to TPM2 PCR Policy +Documentation=man:systemd-pcrlock(8) +DefaultDependencies=no +Conflicts=shutdown.target +After=systemd-tpm2-setup.service +Before=sysinit.target shutdown.target systemd-pcrlock-make-policy.service +ConditionPathExists=!/etc/initrd-release +ConditionSecurity=measured-uki + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart={{LIBEXECDIR}}/systemd-pcrlock lock-firmware-config + +[Install] +WantedBy=sysinit.target diff --git a/units/systemd-pcrlock-machine-id.service.in b/units/systemd-pcrlock-machine-id.service.in new file mode 100644 index 0000000000..0ff22c586e --- /dev/null +++ b/units/systemd-pcrlock-machine-id.service.in @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Lock Machine ID to TPM2 PCR Policy +Documentation=man:systemd-pcrlock(8) +DefaultDependencies=no +Conflicts=shutdown.target +Before=sysinit.target shutdown.target systemd-pcrlock-make-policy.service +ConditionPathExists=!/etc/initrd-release +ConditionSecurity=measured-uki + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart={{LIBEXECDIR}}/systemd-pcrlock lock-machine-id + +[Install] +WantedBy=sysinit.target diff --git a/units/systemd-pcrlock-make-policy.service.in b/units/systemd-pcrlock-make-policy.service.in new file mode 100644 index 0000000000..4127cc7c61 --- /dev/null +++ b/units/systemd-pcrlock-make-policy.service.in @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Make TPM2 PCR Policy +Documentation=man:systemd-pcrlock(8) +DefaultDependencies=no +Conflicts=shutdown.target +After=systemd-tpm2-setup.service +Before=sysinit.target shutdown.target +ConditionPathExists=!/etc/initrd-release +ConditionSecurity=measured-uki + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart={{LIBEXECDIR}}/systemd-pcrlock make-policy --location=770 + +[Install] +WantedBy=sysinit.target diff --git a/units/systemd-pcrlock-secureboot-authority.service.in b/units/systemd-pcrlock-secureboot-authority.service.in new file mode 100644 index 0000000000..a8d55bad3c --- /dev/null +++ b/units/systemd-pcrlock-secureboot-authority.service.in @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Lock UEFI SecureBoot Authority to TPM2 PCR Policy +Documentation=man:systemd-pcrlock(8) +DefaultDependencies=no +Conflicts=shutdown.target +After=systemd-tpm2-setup.service +Before=sysinit.target shutdown.target systemd-pcrlock-make-policy.service +ConditionPathExists=!/etc/initrd-release +ConditionSecurity=measured-uki + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart={{LIBEXECDIR}}/systemd-pcrlock lock-secureboot-authority + +[Install] +WantedBy=sysinit.target diff --git a/units/systemd-pcrlock-secureboot-policy.service.in b/units/systemd-pcrlock-secureboot-policy.service.in new file mode 100644 index 0000000000..10e603c1b6 --- /dev/null +++ b/units/systemd-pcrlock-secureboot-policy.service.in @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Lock UEFI SecureBoot Policy to TPM2 PCR Policy +Documentation=man:systemd-pcrlock(8) +DefaultDependencies=no +Conflicts=shutdown.target +After=systemd-tpm2-setup.service +Before=sysinit.target shutdown.target systemd-pcrlock-make-policy.service +ConditionPathExists=!/etc/initrd-release +ConditionSecurity=measured-uki + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart={{LIBEXECDIR}}/systemd-pcrlock lock-secureboot-policy + +[Install] +WantedBy=sysinit.target