mirror of
https://github.com/linuxboot/heads.git
synced 2025-01-31 00:24:17 +00:00
0a38717e20
Provide tpmr unseal to unseal a file with TPM1 or TPM2. For TPM1, it wraps tpm nv_readvalue and tpm unsealfile. For TPM2, it wraps tpm2 unseal. kexec-unseal-key, seal-hotpkey, unseal-hotp, and unseal-totp no longer need to differentiate TPM1/TPM2. Fixes spurious shred errors on TPM2 that only apply to TPM1 (temporary sealed secret file and shred are now internal to tpmr). Fixes TPM1 disk unlock key unsealing due to logic errors relating to exit status of tpmr unseal or tpm unsealfile (now always uses status of tpmr unseal). Signed-off-by: Jonathon Hall <jonathon.hall@puri.sm>
382 lines
11 KiB
Bash
Executable File
382 lines
11 KiB
Bash
Executable File
#!/bin/bash
|
|
# TPM Wrapper - to unify tpm and tpm2 subcommands
|
|
|
|
. /etc/functions
|
|
|
|
SECRET_DIR="/tmp/secret"
|
|
PRIMARY_HANDLE="0x81000000"
|
|
ENC_SESSION_FILE="enc.ctx"
|
|
DEC_SESSION_FILE="dec.ctx"
|
|
PRIMARY_HANDLE_FILE="primary.handle"
|
|
|
|
set -e -o pipefail
|
|
if [ -r "/tmp/config" ]; then
|
|
. /tmp/config
|
|
else
|
|
. /etc/config
|
|
fi
|
|
|
|
TRACE "Under /bin/tpmr"
|
|
|
|
tpm2_extend() {
|
|
TRACE "Under /bin/tpmr:tpm2_extend"
|
|
DEBUG "value of passed arguments: $1 $2 $3 $4 $5 $6"
|
|
while true; do
|
|
case "$1" in
|
|
-ix)
|
|
DEBUG "case: -ix $2"
|
|
index="$2"
|
|
shift 2;;
|
|
-ic)
|
|
DEBUG "case: -ic $2"
|
|
hash="`echo $2|sha256sum|cut -d' ' -f1`"
|
|
shift 2;;
|
|
-if)
|
|
DEBUG "case: -if $2"
|
|
hash="`sha256sum $2|cut -d' ' -f1`"
|
|
shift 2;;
|
|
*)
|
|
break;;
|
|
esac
|
|
done
|
|
DEBUG "tpm2 pcrextend $index:sha256=$hash"
|
|
tpm2 pcrextend "$index:sha256=$hash"
|
|
DEBUG "tpm2 pcread sha256:$index"
|
|
tpm2 pcrread "sha256:$index"
|
|
DEBUG "$(pcrs)"
|
|
}
|
|
|
|
tpm2_counter_read() {
|
|
TRACE "Under /bin/tpmr:tpm2_counter_read"
|
|
while true; do
|
|
case "$1" in
|
|
-ix)
|
|
index="$2"
|
|
shift 2;;
|
|
*)
|
|
break;;
|
|
esac
|
|
done
|
|
echo "$index: `tpm2 nvread 0x$index | xxd -pc8`"
|
|
}
|
|
|
|
tpm2_counter_inc() {
|
|
TRACE "Under /bin/tpmr:tpm2_counter_inc"
|
|
while true; do
|
|
case "$1" in
|
|
-ix)
|
|
index="$2"
|
|
shift 2;;
|
|
-pwdc)
|
|
pwd="$2"
|
|
shift 2;;
|
|
*)
|
|
break;;
|
|
esac
|
|
done
|
|
tpm2 nvincrement "0x$index" > /dev/console
|
|
echo "$index: `tpm2 nvread 0x$index | xxd -pc8`"
|
|
}
|
|
|
|
tpm2_counter_cre() {
|
|
TRACE "Under /bin/tpmr:tpm2_counter_cre"
|
|
while true; do
|
|
case "$1" in
|
|
-pwdo)
|
|
pwdo="$2"
|
|
shift 2;;
|
|
-pwdof)
|
|
pwdo="file:$2"
|
|
shift 2;;
|
|
-pwdc)
|
|
pwd="$2"
|
|
shift 2;;
|
|
-la)
|
|
label="$2"
|
|
shift 2;;
|
|
*)
|
|
break;;
|
|
esac
|
|
done
|
|
rand_index="1`dd if=/dev/urandom bs=1 count=3 | xxd -pc3`"
|
|
tpm2 nvdefine -C o -s 8 -a "ownerread|authread|authwrite|nt=1" -P "$pwdo" "0x$rand_index" > /dev/console
|
|
echo "$rand_index: (valid after an increment)"
|
|
}
|
|
|
|
tpm2_startsession() {
|
|
TRACE "Under /bin/tpmr:tpm2_startsession"
|
|
mkdir -p "$SECRET_DIR"
|
|
tpm2 flushcontext -Q \
|
|
--transient-object \
|
|
|| die "tpm2_flushcontext: unable to flush transient handles"
|
|
|
|
tpm2 flushcontext -Q \
|
|
--loaded-session \
|
|
|| die "tpm2_flushcontext: unable to flush sessions"
|
|
|
|
tpm2 flushcontext -Q \
|
|
--saved-session \
|
|
|| die "tpm2_flushcontext: unable to flush saved session"
|
|
tpm2 readpublic -Q -c "$PRIMARY_HANDLE" -t "/tmp/$PRIMARY_HANDLE_FILE"
|
|
tpm2 startauthsession -Q -c "/tmp/$PRIMARY_HANDLE_FILE" --hmac-session -S "/tmp/$ENC_SESSION_FILE"
|
|
tpm2 startauthsession -Q -c "/tmp/$PRIMARY_HANDLE_FILE" --hmac-session -S "/tmp/$DEC_SESSION_FILE"
|
|
tpm2 sessionconfig -Q --disable-encrypt "/tmp/$DEC_SESSION_FILE"
|
|
}
|
|
|
|
# Trap EXIT with cleanup_session() to release a TPM2 session and delete the
|
|
# session file. E.g.:
|
|
# trap "cleanup_session '$SESSION_FILE'" EXIT
|
|
cleanup_session() {
|
|
session_file="$1"
|
|
if [ -f "$session_file" ]; then
|
|
DEBUG "Clean up session: $session_file"
|
|
# Nothing else we can do if this fails, still remove the file
|
|
tpm2 flushcontext -Q "$session_file" || DEBUG "Flush failed for session $session_file"
|
|
rm -f "$session_file"
|
|
else
|
|
DEBUG "No need to clean up session: $session_file"
|
|
fi
|
|
}
|
|
|
|
# Clean up a file by shredding it. No-op if the file wasn't created. Use with
|
|
# trap EXIT, e.g.:
|
|
# trap "cleanup_shred '$FILE'" EXIT
|
|
cleanup_shred() {
|
|
shred -n 10 -z -u "$1" 2>/dev/null || true
|
|
}
|
|
|
|
# tpm2_seal: Seal a file against PCR values and, optionally, a password.
|
|
# If a password is given, both the PCRs and password are required to unseal the
|
|
# file. PCRs are provided as a PCR list and data file. PCR data must be
|
|
# provided - TPM2 allows the TPM to fall back to current PCR values, but it is
|
|
# not required to support this.
|
|
tpm2_seal() {
|
|
TRACE "Under /bin/tpmr:tpm2_seal"
|
|
file="$1" #$KEY_FILE
|
|
handle="$2" # 0x8100000$TPM_INDEX
|
|
pcrl="$3" #sha256:0,1,2,3,4,5,6,7
|
|
pcrf="$4"
|
|
pass="$5"
|
|
mkdir -p "$SECRET_DIR"
|
|
bname="`basename $file`"
|
|
|
|
DEBUG "tpm2_seal: file=$file handle=$handle pcrl=$pcrl pcrf=$pcrf pass=$([ "$pass" ] && echo "<yes>" || echo "<no>")"
|
|
|
|
# Create a policy requiring both PCRs and the object's authentication
|
|
# value using a trial session.
|
|
TRIAL_SESSION=/tmp/sealfile_trial.session
|
|
AUTH_POLICY=/tmp/sealfile_auth.policy
|
|
rm -f "$TRIAL_SESSION" "$AUTH_POLICY"
|
|
tpm2 startauthsession -g sha256 -S "$TRIAL_SESSION"
|
|
# We have to clean up the session
|
|
trap "cleanup_session '$TRIAL_SESSION'" EXIT
|
|
# Save the policy hash in case the password policy is not used (we have
|
|
# to get this from the last step, whichever it is).
|
|
tpm2 policypcr -Q -l "$pcrl" -f "$pcrf" -S "$TRIAL_SESSION" -L "$AUTH_POLICY"
|
|
CREATE_PASS_ARGS=()
|
|
if [ "$pass" ]; then
|
|
# Add an object authorization policy (the object authorization
|
|
# will be the password). Save the digest, this is the resulting
|
|
# policy.
|
|
tpm2 policypassword -Q -S "$TRIAL_SESSION" -L "$AUTH_POLICY"
|
|
# Pass the password to create later. Pass the sha256sum of the
|
|
# password to the TPM so the password is not limited to 32 chars
|
|
# in length.
|
|
CREATE_PASS_ARGS=(-p "hex:$(echo -n "$pass" | sha256sum | cut -d ' ' -f 1)")
|
|
fi
|
|
|
|
# Create the object with this policy and the auth value.
|
|
# NOTE: We disable USERWITHAUTH and enable ADMINWITHPOLICY so the
|
|
# password cannot be used on its own, the PCRs are also required.
|
|
# (The default is to allow either policy auth _or_ password auth. In
|
|
# this case the policy includes the password, and we don't want to allow
|
|
# the password on its own.)
|
|
# TODO: Providing the password directly limits it to the size of the
|
|
# largest hash supported by the TPM (at least 32 chars for sha256)
|
|
tpm2 create -Q -C "/tmp/$PRIMARY_HANDLE_FILE" \
|
|
-i "$file" \
|
|
-u "$SECRET_DIR/$bname.priv" \
|
|
-r "$SECRET_DIR/$bname.pub" \
|
|
-L "$AUTH_POLICY" \
|
|
-S "/tmp/$DEC_SESSION_FILE" \
|
|
-a "fixedtpm|fixedparent|adminwithpolicy" \
|
|
"${CREATE_PASS_ARGS[@]}"
|
|
|
|
tpm2 load -Q -C "/tmp/$PRIMARY_HANDLE_FILE" -u "$SECRET_DIR/$bname.priv" -r "$SECRET_DIR/$bname.pub" -c "$SECRET_DIR/$bname.seal.ctx"
|
|
read -s -p "TPM owner password: " key_password
|
|
echo # new line after password prompt
|
|
# remove possible data occupying this handle
|
|
tpm2 evictcontrol -Q -C o -P "$key_password" -c "$handle" 2>/dev/null || true
|
|
DO_WITH_DEBUG tpm2 evictcontrol -Q -C o -P "$key_password" -c "$SECRET_DIR/$bname.seal.ctx" "$handle"
|
|
}
|
|
|
|
# Unseal a file sealed by tpm2_seal. The PCR list must be provided, the
|
|
# password must be provided if one was used to seal (and cannot be provided if
|
|
# no password was used to seal).
|
|
tpm2_unseal() {
|
|
TRACE "Under /bin/tpmr:tpm2_unseal"
|
|
index="$1"
|
|
pcrl="$2"
|
|
size="$3"
|
|
file="$4"
|
|
pass="$5"
|
|
|
|
# Pad with up to 6 zeros, i.e. '0x81000001', '0x81001234', etc.
|
|
handle="$(printf "0x81%6s" "$index" | tr ' ' 0)"
|
|
|
|
DEBUG "tpm2_unseal: handle=$handle pcrl=$pcrl size=$size file=$file pass=$([ "$pass" ] && echo "<yes>" || echo "<no>")"
|
|
|
|
POLICY_SESSION=/tmp/unsealfile_policy.session
|
|
rm -f "$POLICY_SESSION"
|
|
tpm2 startauthsession -Q -g sha256 -c "/tmp/$PRIMARY_HANDLE_FILE" -S "$POLICY_SESSION" --policy-session
|
|
trap "cleanup_session '$POLICY_SESSION'" EXIT
|
|
# Check the PCR policy
|
|
tpm2 policypcr -Q -l "$pcrl" -S "$POLICY_SESSION"
|
|
UNSEAL_PASS_SUFFIX=""
|
|
if [ "$pass" ]; then
|
|
# Add the object authorization policy (the actual password is
|
|
# provided later, but we must include this so the policy we
|
|
# attempt to use is correct).
|
|
tpm2 policypassword -Q -S "$POLICY_SESSION"
|
|
# When unsealing, include the password with the auth session
|
|
UNSEAL_PASS_SUFFIX="+hex:$(echo -n "$pass" | sha256sum | cut -d ' ' -f 1)"
|
|
# Disable encryption in the policy session - there seems to be a
|
|
# bug in tpm2's decryption. If we leave encryption enabled, the
|
|
# unseal succeeds but we receive garbage, probably because it
|
|
# was decrypted incorrectly. However, this causes the unsealed
|
|
# data to be sent in the clear from the TPM.
|
|
tpm2 sessionconfig -Q --disable-encrypt "$POLICY_SESSION"
|
|
fi
|
|
|
|
tpm2 unseal -Q -c "$handle" -p "session:$POLICY_SESSION$UNSEAL_PASS_SUFFIX" > "$file"
|
|
|
|
# We don't need to know the size to unseal in TPM2, but TPM1 does.
|
|
# Ensure the correct size is provided so both will work.
|
|
# TODO - This isn't actually what -sz means to tpm nv_readvalue
|
|
#actual_size="$(stat -c "%s" "$file")"
|
|
#if [ "$actual_size" -ne "$size" ]; then
|
|
# die "Expected size $size for $file but got $actual_size"
|
|
#fi
|
|
}
|
|
|
|
tpm2_reset() {
|
|
TRACE "Under /bin/tpmr:tpm2_reset"
|
|
key_password="$1"
|
|
mkdir -p "$SECRET_DIR"
|
|
tpm2 clear -c platform || warn "Unable to clear TPM on platform hierarchy!"
|
|
tpm2 changeauth -c owner "$key_password"
|
|
tpm2 createprimary -C owner -g sha256 -G "${CONFIG_PRIMARY_KEY_TYPE:-rsa}" -c "$SECRET_DIR/primary.ctx" -P "$key_password"
|
|
tpm2 evictcontrol -C owner -c "$SECRET_DIR/primary.ctx" "$PRIMARY_HANDLE" -P "$key_password"
|
|
shred -u "$SECRET_DIR/primary.ctx"
|
|
tpm2_startsession
|
|
}
|
|
|
|
# Perform final cleanup before boot and lock the platform heirarchy.
|
|
tpm2_kexec_finalize() {
|
|
# Flush sessions and transient objects
|
|
tpm2 flushcontext -Q --transient-object \
|
|
|| warn "tpm2_flushcontext: unable to flush transient handles"
|
|
tpm2 flushcontext -Q --loaded-session \
|
|
|| warn "tpm2_flushcontext: unable to flush sessions"
|
|
tpm2 flushcontext -Q --saved-session \
|
|
|| warn "tpm2_flushcontext: unable to flush saved session"
|
|
|
|
# Add a random passphrase to platform hierarchy to prevent TPM2 from
|
|
# being cleared in the OS.
|
|
# This passphrase is only effective before the next boot.
|
|
echo "Locking platform hierarchy..."
|
|
randpass=$(dd if=/dev/urandom bs=4 count=1 | xxd -p)
|
|
tpm2 changeauth -c platform "$randpass" \
|
|
|| warn "Failed to lock platform hierarchy of TPM2!"
|
|
}
|
|
|
|
tpm1_unseal() {
|
|
TRACE "Under /bin/tpmr:tpm1_unseal"
|
|
index="$1"
|
|
pcrl="$2"
|
|
size="$3"
|
|
file="$4"
|
|
pass="$5"
|
|
|
|
# pcrl (the PCR list) is unused in TPM1. The TPM itself knows which
|
|
# PCRs were used to seal and checks them. We can't verify that it's
|
|
# correct either, so just ignore it in TPM1.
|
|
|
|
sealed_file="$SECRET_DIR/tpm1_unseal_sealed.bin"
|
|
trap "cleanup_shred '$sealed_file'" EXIT
|
|
|
|
rm -f "$sealed_file"
|
|
|
|
tpm nv_readvalue \
|
|
-in "$index" \
|
|
-sz "$size" \
|
|
-of "$sealed_file" \
|
|
|| die "Unable to read sealed file from TPM NVRAM"
|
|
|
|
PASS_ARGS=()
|
|
if [ "$pass" ]; then
|
|
PASS_ARGS=(-pwdd "$pass")
|
|
fi
|
|
|
|
tpm unsealfile \
|
|
-if "$sealed_file" \
|
|
-of "$file" \
|
|
"${PASS_ARGS[@]}" \
|
|
-hk 40000000
|
|
}
|
|
|
|
if [ "$CONFIG_TPM" != "y" ]; then
|
|
echo >&2 "No TPM!"
|
|
exit 1
|
|
fi
|
|
|
|
# TPM1 - most commands forward directly to tpm, but some are still wrapped for
|
|
# consistency with tpm2.
|
|
if [ "$CONFIG_TPM2_TOOLS" != "y" ]; then
|
|
subcmd="$1"
|
|
# Don't shift yet, for most commands we will just forward to tpm.
|
|
case "$subcmd" in
|
|
kexec_finalize)
|
|
;; # Nothing on TPM1.
|
|
unseal)
|
|
shift; tpm1_unseal "$@";;
|
|
*)
|
|
exec tpm "$@"
|
|
;;
|
|
esac
|
|
exit 0
|
|
fi
|
|
|
|
# TPM2 - all commands implemented as wrappers around tpm2
|
|
subcmd="$1"
|
|
shift 1
|
|
case "$subcmd" in
|
|
extend)
|
|
tpm2_extend "$@";;
|
|
counter_read)
|
|
tpm2_counter_read "$@";;
|
|
counter_increment)
|
|
tpm2_counter_inc "$@";;
|
|
counter_create)
|
|
tpm2_counter_cre "$@";;
|
|
nv_definespace)
|
|
tpm2_nvdef "$@";;
|
|
nv_writevalue)
|
|
tpm2_nvw "$@";;
|
|
nv_readvalue)
|
|
tpm2_nvr "$@";;
|
|
seal)
|
|
tpm2_seal "$@";;
|
|
startsession)
|
|
tpm2_startsession "$@";;
|
|
unseal)
|
|
tpm2_unseal "$@";;
|
|
reset)
|
|
tpm2_reset "$@";;
|
|
kexec_finalize)
|
|
tpm2_kexec_finalize "$@";;
|
|
*)
|
|
echo "Command $subcmd not wrapped!"
|
|
exit 1
|
|
esac
|