This updates systemd-tpm2-setup to create and persist an endorsement key if there isn't one already. For each supported EK template profile, it will read the EK certificate from its NV index if there is one. When there is an EK certificate present, a primary key is created using the corresponding template. If the resulting EKpub matches the public key in the certificate, the created EK is persisted and the process is complete. The low-range templates and the high-range RSA 2048/3072 and ECC NIST P256/P384 storage templates are supported, as detailed in section 5.3 of the "TCG EK Credential Profile For TPM Family 2.0" spec v2.7. High-range templates are preferred because these permit EK usage without requiring knowledge of the authorization value for the endorsement hierarchy, meaning that, like with the SRK, it is possible to restrict the usage of the endorsement hierarchy whilst still permitting use of the persistent EK. The EK is always persisted at handle 0x81010001. This handle is reserved in Table 2 of the "TCG TPM v2.0 Provisioning Guidance" spec v1.0r1.0, although this is only a recommendation. This handle is within the block of handles reserved for endorsement primary keys in the "Registry of Reserved TPM 2.0 Handles and Localities" spec v1.2r1.00. Section 2.3.2 of this specification also makes a suggestion that there should be a relationship between the EK certificate NV index and a corresponding persistent EK handle by using handles at the same offsets within their respective ranges. However, this contradicts the provisioning guidance spec which reserves 0x81010001 when there isn't a certificate at 0x01c00001. For simplicity, I've chosen to use a single handle for the EK regardless of which profile it is created with. The "TCG EK Credential Profile For TPM Family 2.0" spec also provides a way for endorsement keys to be certified with non-standard templates by storing the template in a NV index. This is also supported. The EK creation is not executed with tpm2-setup --early, as there's no need for it to be created so early, unlike with the SRK. I also haven't stored EKpub in /var/lib/systemd like with the SRKpub, as I'm not sure there will be a use case for this yet. A follow-up PR may be needed to add some internal helpers to make use of the persisted EK, as use of low-range EKs requires a policy session. High-range EKs can be used with a HMAC session because they have the userWithAuth attribute set and we are creating them with an empty authorization value. https://trustedcomputinggroup.org/resource/http-trustedcomputinggroup-org-wp-content-uploads-tcg-ek-credential-profile/ https://trustedcomputinggroup.org/resource/tcg-tpm-v2-0-provisioning-guidance/ https://trustedcomputinggroup.org/resource/registry/
191 lines
7.9 KiB
Bash
Executable File
191 lines
7.9 KiB
Bash
Executable File
#!/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
|
|
SD_PCREXTEND="/usr/lib/systemd/systemd-pcrextend"
|
|
SD_TPM2SETUP="/usr/lib/systemd/systemd-tpm2-setup"
|
|
|
|
if [[ ! -x "${SD_PCREXTEND:?}" ]] || ! tpm_has_pcr sha256 11; then
|
|
echo "$SD_PCREXTEND or PCR sysfs files not found, skipping PCR extension tests"
|
|
exit 0
|
|
fi
|
|
|
|
at_exit() {
|
|
if [[ $? -ne 0 ]]; then
|
|
# Dump the event log on fail, to make debugging a bit easier
|
|
jq --seq --slurp </run/log/systemd/tpm2-measure.log
|
|
fi
|
|
|
|
rm -rf /run/nvpcr /tmp/nvpcr
|
|
rm -f /var/tmp/nvpcr.raw /run/verity.d/test-70-nvpcr.crt
|
|
rm -f /run/systemd/nvpcr/test.anchor /run/systemd/nvpcr/test2.anchor /run/systemd/nvpcr/aaa.anchor /run/systemd/nvpcr/zzz.anchor
|
|
}
|
|
|
|
trap at_exit EXIT
|
|
|
|
# systemd-tpm2-setup returns EX_UNAVAILABLE rather than 0 when it cannot set something up but this
|
|
# is still considered success. This happens at the moment because there is no EK certificate in
|
|
# QEMU guests.
|
|
run_tpm2_setup() {
|
|
local rc=0
|
|
"$SD_TPM2SETUP" || rc=$?
|
|
[[ "$rc" -eq 0 || "$rc" -eq 69 ]]
|
|
}
|
|
|
|
# Temporarily override sd-pcrextend's sanity checks
|
|
export SYSTEMD_FORCE_MEASURE=1
|
|
|
|
mkdir -p /run/nvpcr
|
|
|
|
cat >/run/nvpcr/test.nvpcr <<EOF
|
|
{"name":"test","algorithm":"sha256","nvIndex":30474762}
|
|
EOF
|
|
run_tpm2_setup
|
|
test -f /run/systemd/nvpcr/test.anchor
|
|
/usr/lib/systemd/systemd-pcrextend --nvpcr=test schrumpel
|
|
# To calculate the current value we need the anchor measurement
|
|
DIGEST_BASE="$(cat /run/systemd/nvpcr/test.anchor)"
|
|
DIGEST_MEASURED="$(echo -n "schrumpel" | openssl dgst -sha256 -hex -r | cut -d' ' -f1)"
|
|
DIGEST_EXPECTED="$(echo "$DIGEST_BASE$DIGEST_MEASURED" | tr '[:lower:]' '[:upper:]' | basenc --base16 -d | openssl dgst -sha256 -hex -r | cut -d' ' -f1)"
|
|
DIGEST_ACTUAL="$(systemd-analyze nvpcrs test --json=pretty | jq -r '.[] | select(.name=="test") | .value')"
|
|
test "$DIGEST_ACTUAL" = "$DIGEST_EXPECTED"
|
|
|
|
# Now "destroy" the value via another measurement (this time we use Varlink, to test the API)
|
|
varlinkctl call /usr/lib/systemd/systemd-pcrextend io.systemd.PCRExtend.Extend '{"nvpcr":"test","text":"schnurz"}'
|
|
DIGEST_ACTUAL2="$(systemd-analyze nvpcrs test --json=pretty | jq -r '.[] | select(.name=="test") | .value')"
|
|
test "$DIGEST_ACTUAL2" != "$DIGEST_EXPECTED"
|
|
|
|
# And calculate the new result
|
|
DIGEST_MEASURED2="$(echo -n "schnurz" | openssl dgst -sha256 -hex -r | cut -d' ' -f1)"
|
|
DIGEST_EXPECTED2="$(echo "$DIGEST_EXPECTED$DIGEST_MEASURED2" | tr '[:lower:]' '[:upper:]' | basenc --base16 -d | openssl dgst -sha256 -hex -r | cut -d' ' -f1)"
|
|
test "$DIGEST_ACTUAL2" = "$DIGEST_EXPECTED2"
|
|
|
|
# Verify the 'priority' field round-trips through the JSON definition. The 'test' NvPCR above sets no
|
|
# priority, so it must report the default (1000).
|
|
PRIORITY_DEFAULT="$(systemd-analyze nvpcrs test --json=pretty | jq -r '.[] | select(.name=="test") | .priority')"
|
|
test "$PRIORITY_DEFAULT" = "1000"
|
|
|
|
# A definition with an explicit priority must report exactly that value.
|
|
cat >/run/nvpcr/test2.nvpcr <<EOF
|
|
{"name":"test2","algorithm":"sha256","nvIndex":30474763,"priority":42}
|
|
EOF
|
|
PRIORITY_EXPLICIT="$(systemd-analyze nvpcrs test2 --json=pretty | jq -r '.[] | select(.name=="test2") | .priority')"
|
|
test "$PRIORITY_EXPLICIT" = "42"
|
|
|
|
# Verify NvPCRs are allocated in order of priority (lower value = more important = allocated first),
|
|
# independent of lexical filename order. 'aaa' is lexically first but less important (higher priority
|
|
# value), while 'zzz' is lexically last but more important (lower priority value), so 'zzz' must be set
|
|
# up before 'aaa'.
|
|
cat >/run/nvpcr/aaa.nvpcr <<EOF
|
|
{"name":"aaa","algorithm":"sha256","nvIndex":30474772,"priority":900}
|
|
EOF
|
|
cat >/run/nvpcr/zzz.nvpcr <<EOF
|
|
{"name":"zzz","algorithm":"sha256","nvIndex":30474773,"priority":100}
|
|
EOF
|
|
SETUP_LOG="$(run_tpm2_setup 2>&1)"
|
|
AAA_LINE="$(echo "$SETUP_LOG" | grep -n "Setting up NvPCR 'aaa'" | cut -d: -f1)"
|
|
ZZZ_LINE="$(echo "$SETUP_LOG" | grep -n "Setting up NvPCR 'zzz'" | cut -d: -f1)"
|
|
test "$ZZZ_LINE" -lt "$AAA_LINE"
|
|
|
|
# Test the --login= mode and the 'login' NvPCR, used in production by systemd-pcrlogin@.service.
|
|
if [[ -f /usr/lib/nvpcr/login.nvpcr ]]; then
|
|
login_nvpcr_value() {
|
|
systemd-analyze nvpcrs login --json=pretty | jq -r '.[] | select(.name=="login") | .value'
|
|
}
|
|
|
|
# Extract the most recently measured word for the 'login' NvPCR from the event log.
|
|
login_last_word() {
|
|
jq --seq --slurp -r '[.[] | select(.content.nvIndexName=="login") | .content.string] | last' </run/log/systemd/tpm2-measure.log
|
|
}
|
|
|
|
# Measure root's user record. This lazily initializes the 'login' NvPCR if it isn't already.
|
|
"$SD_PCREXTEND" --login=root
|
|
|
|
# The 'login' NvPCR must now exist and carry a non-empty value.
|
|
LOGIN_DIGEST1="$(login_nvpcr_value)"
|
|
test -n "$LOGIN_DIGEST1"
|
|
test "$LOGIN_DIGEST1" != "null"
|
|
|
|
# A matching event log entry must be present (the word is "login:<name>:<canonical json>").
|
|
grep -F '"nvIndexName":"login","string":"login:root:' /run/log/systemd/tpm2-measure.log >/dev/null
|
|
LOGIN_WORD_BY_NAME="$(login_last_word)"
|
|
|
|
# Looking the same user up by numeric UID must yield the identical measured word
|
|
# (systemd-pcrextend uses USERDB_PARSE_NUMERIC, and systemd-pcrlogin@.service is instanced by UID).
|
|
"$SD_PCREXTEND" --login=0
|
|
LOGIN_WORD_BY_UID="$(login_last_word)"
|
|
test "$LOGIN_WORD_BY_NAME" = "$LOGIN_WORD_BY_UID"
|
|
|
|
# Direct tool invocations always re-extend (the once-per-boot guarantee lives in the unit's
|
|
# RemainAfterExit=yes, not in the tool), so the NvPCR value must have advanced.
|
|
LOGIN_DIGEST2="$(login_nvpcr_value)"
|
|
test "$LOGIN_DIGEST2" != "$LOGIN_DIGEST1"
|
|
fi
|
|
|
|
systemd-analyze identify-tpm2
|
|
udevadm test-builtin 'tpm2_id identify' /dev/tpmrm0
|
|
|
|
# systemd-dissect calls io.systemd.PCRExtend over Varlink to extend the verity NvPCR after activation,
|
|
# but systemd-pcrextend.socket has ConditionSecurity=measured-os which fails when the firmware did not
|
|
# initialize PCRs (e.g. when not booting via a signed UKI). Skip the rest in that case, otherwise the
|
|
# 'diff | grep' below would find no new measurement and fail.
|
|
if ! systemctl is-active --quiet systemd-pcrextend.socket; then
|
|
echo "systemd-pcrextend.socket not active, skipping verity NvPCR measurement check"
|
|
exit 0
|
|
fi
|
|
|
|
mkdir -p /tmp/nvpcr/tree
|
|
touch /tmp/nvpcr/tree/file
|
|
|
|
if machine_supports_verity_keyring; then
|
|
SYSTEMD_REPART_OVERRIDE_FSTYPE=squashfs \
|
|
systemd-repart -P \
|
|
-s /tmp/nvpcr/tree \
|
|
--certificate=/usr/share/mkosi.crt \
|
|
--private-key=/usr/share/mkosi.key \
|
|
/var/tmp/nvpcr.raw
|
|
else
|
|
OPENSSL_CONFIG="/tmp/nvpcr/opensslconfig"
|
|
# Unfortunately OpenSSL insists on reading some config file, hence provide one with mostly placeholder contents
|
|
cat >"${OPENSSL_CONFIG:?}" <<EOF
|
|
[ req ]
|
|
prompt = no
|
|
distinguished_name = req_distinguished_name
|
|
|
|
[ req_distinguished_name ]
|
|
C = DE
|
|
ST = Test State
|
|
L = Test Locality
|
|
O = Org Name
|
|
OU = Org Unit Name
|
|
CN = Common Name
|
|
emailAddress = test@email.com
|
|
EOF
|
|
|
|
openssl req -config "$OPENSSL_CONFIG" -subj="/CN=waldo" \
|
|
-x509 -sha256 -nodes -days 365 -newkey rsa:4096 \
|
|
-keyout /tmp/nvpcr/test-70-nvpcr.key -out /tmp/nvpcr/test-70-nvpcr.crt
|
|
|
|
SYSTEMD_REPART_OVERRIDE_FSTYPE=squashfs \
|
|
systemd-repart -P \
|
|
-s /tmp/nvpcr/tree \
|
|
--certificate=/tmp/nvpcr/test-70-nvpcr.crt \
|
|
--private-key=/tmp/nvpcr/test-70-nvpcr.key \
|
|
/var/tmp/nvpcr.raw
|
|
|
|
mkdir -p /run/verity.d
|
|
cp /tmp/nvpcr/test-70-nvpcr.crt /run/verity.d/
|
|
fi
|
|
|
|
cp /run/log/systemd/tpm2-measure.log /tmp/nvpcr/log-before
|
|
|
|
systemd-dissect --image-policy='root=signed:=absent+unused' --mtree /var/tmp/nvpcr.raw
|
|
|
|
set +o pipefail
|
|
diff /tmp/nvpcr/log-before /run/log/systemd/tpm2-measure.log | grep -F '"content":{"nvIndexName":"verity","string":"verity:'
|