-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Autounlock ZFS Encrypted Root Filesystems using TPM 2.0 #9852
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
dist_sbin_SCRIPTS = tpm2_autounlock.sh |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
#! /bin/bash | ||
|
||
# Default variables | ||
# If password is stored in "defaultpassword" field, ensure the security of the file. | ||
defaultpassword="" | ||
defaulttpmindex="0x1800016" | ||
defaulttpmpcrs="sha256:0,1,2,3,8,9" | ||
|
||
helpmsg() { | ||
echo "tpm2_autounlock.sh -- configure the TPM 2.0 chip for zfs autounlock" | ||
echo "" | ||
echo "Usage: <-c, -v, or -n> -i <val>, -p <val>, -P <val>, -r <val>" | ||
echo "" | ||
echo "Modes: (choose up to one)" | ||
echo "-c or --clearonly Clear TPM and ZFS properties only" | ||
echo "-v or --verifyonly Check current contents of TPM" | ||
echo "-n or --nvlockonly Lock the ability to read password until reboot" | ||
echo "Not selecting any of the above will initialize the TPM for autounlock" | ||
echo "" | ||
echo "Options to override defaults:" | ||
echo "-i or --index TPM NVRAM index (default: 0x1800016)" | ||
echo "-p or --password Drive unlock password (default: <none provided>)" | ||
echo "-P or --pcrs PCRS evaluated at unlock (default: sha256:0,1,2,3)" | ||
echo "-r or --rootfs Rootfs being processed (default: current booted rootfs)" | ||
echo "The password can also be piped to tpm2_autounlock.sh, though '-p' takes precedence" | ||
} | ||
|
||
tpm_session_execute() { | ||
tpm2_startauthsession -Q ${define_policy} --session=s.dat | ||
tpm2_policypassword -Q --session=s.dat | ||
tpm2_policypcr -Q --pcr-list="$tpmpcrs" --session=s.dat --policy=policy.dat | ||
eval "${cmd}" | ||
returnval=$? | ||
tpm2_flushcontext s.dat | ||
rm s.dat | ||
rm policy.dat | ||
unset define_policy | ||
unset cmd | ||
return $returnval | ||
} | ||
|
||
# Check for root permissions | ||
if [ $(id -u) -ne 0 ]; then | ||
echo "This script must be run as root" | ||
helpmsg | ||
exit 1 | ||
fi | ||
|
||
# Check for presence of needed programs | ||
command -v zfs >/dev/null 2>&1 && | ||
[ $(zfs version | grep zfs-0 | awk -F 'zfs-0' '{print $2}' | awk -F '.' '{print $2}') -ge 8 ] \ | ||
|| { echo >&2 "zfs 0.8 or greater required, but not found. Aborting."; exit 1; } | ||
|
||
for i in tpm2_startauthsession tpm2_policypassword tpm2_policypcr tpm2_flushcontext tpm2_nvdefine tpm2_nvwrite tpm2_nvread; do | ||
command -v $i >/dev/null 2>&1 && | ||
[ $($i -v | awk -F 'version=' '{print $2}' | awk -F '"' '{print $2}' | awk -F '.' '{print $1}') -ge 4 ] \ | ||
|| { echo >&2 "'$i' version 4.0 or greater required, but not found. Aborting."; exit 1; } | ||
done | ||
|
||
# Check for presence of TPM 2.0 Hardware | ||
if [ $(cat /sys/module/tpm/version) != "2.0" ]; then | ||
echo "TPM 2.0 not found on system" | ||
exit 1 | ||
fi | ||
|
||
# Process user entered arguments | ||
while [ "$1" != "" ]; do | ||
case $1 in | ||
-i | --index ) shift; tpmindex=$1;; | ||
-P | --pcrs ) shift; tpmpcrs=$1;; | ||
-r | --rootfs ) shift; rootfs=$1;; | ||
-p | --password ) shift; password=$1;; | ||
-v | --verifyonly ) verifyonly=1;; | ||
-c | --clearonly ) clearonly=1;; | ||
-n | --nvlockonly ) nvlockonly=1;; | ||
-h | --help ) helpmsg; exit 0;; | ||
* ) helpmsg; exit 1;; | ||
esac | ||
shift | ||
done | ||
|
||
# Use -r or --rootfs value if specified, otherwise try to detect running rootfs | ||
if [ -z "$rootfs" ]; then | ||
rootfs=$(zfs mount |awk '$2 == "/" { print $1 }') | ||
if [ -z "$rootfs" ]; then | ||
echo "Rootfs not specified (-r / --rootfs) and current rootfs could not be determined" | ||
exit 1 | ||
fi | ||
fi | ||
|
||
# With rootfs determined, find the encryptionroot and its guid | ||
ENCRYPTIONROOT=$(zfs get -H -o value encryptionroot "${rootfs}") | ||
rootguid=$(zfs get guid -o value -H "${ENCRYPTIONROOT}") | ||
|
||
# Use -i or --index value if specified, otherwise look for existing property, and finally fallback to script default | ||
if [ -z "$tpmindex" ]; then | ||
tpmindex=$(zfs get -H -o value org.zfsonlinux.tpm2:index "${ENCRYPTIONROOT}") | ||
if [ "$tpmindex" = "-" ]; then | ||
tpmindex="$defaulttpmindex" | ||
fi | ||
fi | ||
|
||
# Use -P or --pcrs value if specified, otherwise look for existing property, and finally fallback to script default | ||
if [ -z "$tpmpcrs" ]; then | ||
tpmpcrs=$(zfs get -H -o value org.zfsonlinux.tpm2:pcrs "${ENCRYPTIONROOT}") | ||
if [ "$tpmpcrs" = "-" ]; then | ||
tpmpcrs="$defaulttpmpcrs" | ||
fi | ||
fi | ||
|
||
# If password is needed, use argument, piped, "script default" or stdin, in that order | ||
if [ -z "$password" ] && (( verifyonly+clearonly+nvlockonly == 0)); then | ||
[[ -p /dev/stdin ]] && { mapfile -t; set -- "${MAPFILE[@]}"; set -- $@; } | ||
pipepassword="$@" | ||
if [ -n "$pipepassword" ]; then | ||
password="$pipepassword" | ||
elif [ -n "$defaultpassword" ]; then | ||
password="$defaultpassword" | ||
else | ||
while true; do | ||
read -s -p "Drive unlock password: " password | ||
echo | ||
read -s -p "Confirm Password: " password2 | ||
echo | ||
[ "$password" = "$password2" ] && break | ||
echo "Please try again" | ||
done | ||
fi | ||
fi | ||
|
||
# Check for invalid arguments | ||
if (( verifyonly+clearonly+nvlockonly > 1)) ; then | ||
echo " You can only select only one of the following: verifyonly, clearonly, or nvlockonly" | ||
exit 1 | ||
fi | ||
|
||
######################################## | ||
# Start execution | ||
if [ $clearonly ]; then | ||
tpm2_nvundefine "$tpmindex" >/dev/null 2>&1 | ||
zfs inherit org.zfsonlinux.tpm2:index "${ENCRYPTIONROOT}" | ||
zfs inherit org.zfsonlinux.tpm2:pcrs "${ENCRYPTIONROOT}" | ||
|
||
elif [ $verifyonly ]; then | ||
echo "Attempting to read TPM index '$tpmindex' locked with pcrs '$tpmpcrs' and guid '$rootguid'." | ||
define_policy="--policy" | ||
cmd="tpm2_nvread ${tpmindex} --auth=session:s.dat+${rootguid}" | ||
tpm_session_execute | ||
if [ $? = 0 ]; then | ||
echo "is the stored password." | ||
# else | ||
# The tpm2_nvread stdout messages will be visible to user | ||
fi | ||
|
||
elif [ $nvlockonly ]; then | ||
define_policy="--policy" | ||
cmd="tpm2_nvreadlock ${tpmindex} --auth=session:s.dat+${rootguid}" | ||
tpm_session_execute | ||
else | ||
# Preemptively clear the tpm index | ||
tpm2_nvundefine "$tpmindex" >/dev/null 2>&1 | ||
|
||
# Define NVRAM location and its access rules | ||
cmd="tpm2_nvdefine ${tpmindex} -Q --hierarchy=o --index-auth=${rootguid} --size=512 --policy=policy.dat --attributes='policyread|policywrite|read_stclear'" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why are you setting --index-auth without attribute TPMA_NV_AUTHREAD? IIUC, that essentially is saying, hey this is another password but i never want to use it. I would imagine the rootguid is not a secret, and thus you really just want to drop this option. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, seems redundant. "policyread and policywrite" are the only allowed access methods. The password requirement is built within the policy and is not freestanding. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well..local testing didn't like removing
When running There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably because you still have policypassword present, which doesn't make a whole lot sense with a known value. For something like an FS encryption key, you would probably want to tie it to:
Typically folks avoid hard-coding PCR's due to brittleness and use policyauthorize which says, use any policy signed by key X. Not sure what prevents rollback to a bad policy or OS state on that method, may be worth investigating. There might not be a mechanism there.. so maybe owner auth and re-creating the nv index will work for upgrade path. The question is, for automount, where can we get the user password. Not sure where you can source it, from PAM, login session, kernel keyring, etc. I really don't know much about that side of the house and don't want to give you bad advice there. One would want a secret that is tethered to the lifetime of a successful login or from the user typing it in directly. Item 1 - Tie to a PCR value only: tpm2_startauthsession -S session.ctx
tpm2_policypcr -S session.ctx --pcr-list="sha256:0,1,2,3,8,9" --policy=policy.dat
tpm2_flushcontext session.ctx
tpm2_nvdefine -C o --size=512 --policy=policy.dat --attributes='policyread|policywrite|read_stclear' $NV_INDEX
tpm2_startauthsession --policy -S session.ctx
tpm2_policypcr -S session.ctx --pcr-list="sha256:0,1,2,3,8,9"
echo 'secret' | tpm2_nvwrite -P'session:session.ctx' -i- $NV_INDEX
tpm2_flushcontext session.ctx
# should work
tpm2_startauthsession --policy -S session.ctx
tpm2_policypcr -S session.ctx --pcr-list="sha256:0,1,2,3,8,9"
tpm2_nvread -P'session:session.ctx' -s6 $NV_INDEX
tpm2_flushcontext session.ctx Item 2 - tie to PCR and user password: NV_INDEX=0x01800000
set -e -o pipefail
tpm2_startauthsession -S session.ctx
tpm2_policypcr -S session.ctx --pcr-list="sha256:0,1,2,3,8,9"
tpm2_policypassword -S session.ctx --policy=policy.dat
tpm2_flushcontext session.ctx
tpm2_nvdefine -C o -p 'userpass' --size=512 --policy=policy.dat --attributes='policyread|policywrite|read_stclear' $NV_INDEX
tpm2_startauthsession --policy -S session.ctx
tpm2_policypcr -S session.ctx --pcr-list="sha256:0,1,2,3,8,9"
tpm2_policypassword -S session.ctx
echo 'secret' | tpm2_nvwrite -P'session:session.ctx+userpass' -i- $NV_INDEX
tpm2_flushcontext session.ctx
tpm2_startauthsession --policy -S session.ctx
tpm2_policypcr -S session.ctx --pcr-list="sha256:0,1,2,3,8,9"
tpm2_policypassword -S session.ctx
tpm2_nvread -P'session:session.ctx+userpass' -s6 $NV_INDEX
tpm2_flushcontext session.ctx Item 3, add in recovery option. #!/usr/bin/env bash
NV_INDEX=0x01800000
set -e -o pipefail
# this may not work for you, my TPM currently has an empty owner password
tpm2_changeauth -c o "ownerpass"
tpm2_startauthsession -S session.ctx
tpm2_policypcr -S session.ctx --pcr-list="sha256:0,1,2,3,8,9"
tpm2_policypassword -S session.ctx --policy=policy.dat
tpm2_flushcontext session.ctx
tpm2_nvdefine -C o -P 'ownerpass' -p 'userpass' --size=512 --policy=policy.dat --attributes='ownerread|policyread|policywrite|read_stclear' $NV_INDEX
tpm2_startauthsession --policy -S session.ctx
tpm2_policypcr -S session.ctx --pcr-list="sha256:0,1,2,3,8,9"
tpm2_policypassword -S session.ctx
echo 'secret' | tpm2_nvwrite -P'session:session.ctx+userpass' -i- $NV_INDEX
tpm2_flushcontext session.ctx
# recovery with owner password
tpm2_nvread -C o -P'ownerpass' -s6 $NV_INDEX
# normal flow, user auth
tpm2_startauthsession --policy -S session.ctx
tpm2_policypcr -S session.ctx --pcr-list="sha256:0,1,2,3,8,9"
tpm2_policypassword -S session.ctx
tpm2_nvread -P'session:session.ctx+userpass' -s6 $NV_INDEX
tpm2_flushcontext session.ctx There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, in my testing I found that when when you couple auth to a permanent object like owner hiearchy DA protections stop applying unless you authorize to the NV index and specify a bad authvalue. ie
I started looking at using policysecret to couple to the owner hiearchy via a policy OR to see if that would work, but I was unable to get the script to run and ran out of time, sharing here so you can perhaps figure it out. The manpage and test code for tpm2_policyor sucks, so I will bug @idesai to look at this. #!/usr/bin/env bash
NV_INDEX=0x01800000
set -e -o pipefail
# this may not work for you, my TPM currently has an empty owner password
tpm2_changeauth -c o "ownerpass"
do_session() {
# PCR + user auth
tpm2_startauthsession -S session.ctx
tpm2_policypcr -S session.ctx --pcr-list="sha256:0,1,2,3,8,9"
tpm2_policypassword -S session.ctx --policy=policy1.dat
tpm2_flushcontext session.ctx
# Tether to owner hiearchy auth, but not via attr so DA works
tpm2_startauthsession -S session.ctx
tpm2_policysecret -S session.ctx -c o --policy=policy2.dat 'ownerpass'
tpm2_flushcontext session.ctx
# create the OR policy of them both
tpm2_startauthsession -S session.ctx
tpm2_policyor -S session.ctx -lpolicy1.dat,policy2.dat -L policy.dat
}
do_session_user() {
# PCR + user auth
tpm2_startauthsession -S session.ctx --policy
tpm2_policypcr -S session.ctx --pcr-list="sha256:0,1,2,3,8,9"
tpm2_policypassword -S session.ctx --policy=policy1.dat
tpm2_flushcontext session.ctx
# create the OR policy of them both
tpm2_startauthsession -S session.ctx --policy
tpm2_policyor -S session.ctx -lpolicy1.dat,policy2.dat
}
do_session
tpm2_nvdefine -C o -P 'ownerpass' -p 'userpass' --size=512 --policy=policy.dat --attributes='policyread|policywrite|read_stclear' $NV_INDEX
tpm2_flushcontext session.ctx
do_session_user
echo 'secret' | tpm2_nvwrite -P'session.ctx:session.ctx+userpass' -i- $NV_INDEX
tpm2_flushcontext session.ctx
# recovery with owner password
do_session_owner
tpm2_nvread -C o -P'ownerpass' -s6 $NV_INDEX There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
IIUC the requirement is that,
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @williamcroberts @ghfields this is one way to do what you are trying to achieve with enhanced authorization NV Index always read policytpm2_startauthsession -S session.ctx PolicySecret NV Index --> Recovery Password == NV Index Passworddd if=/dev/urandom bs=1 count=34 status=none | tpm2_nvwrite -C o -i- 0x01000000 PolicyPCRtpm2_startauthsession -S session.ctx Policy ( PCR || Recovery-Password)tpm2_startauthsession -S session.ctx Policy written out to NV index with owner auth.New PCR states will replace this value.echo "000b" | xxd -p -r | cat - policy.or | tpm2_nvwrite -C o -i- 0x01000000 Sealing objecttpm2_createprimary -C o -c prim.ctx -Q Unsealing with PolicyPCR - Failing this triggers DA mechanismtpm2_startauthsession -S session_read.ctx --policy-session Unsealing with Recovery-Password - Failing this triggers DA mechanismtpm2_startauthsession -S session_read.ctx --policy-session |
||
tpm_session_execute | ||
|
||
# Write password to NVRAM location | ||
define_policy="--policy" | ||
cmd="echo $password | tpm2_nvwrite ${tpmindex} -Q --auth=session:s.dat+${rootguid} --input=-" | ||
tpm_session_execute | ||
echo "Note: Password storage can be verified with '--verifyonly' until reboot or locked manually with '--nvlockonly'." | ||
|
||
# Store zfs filesystem properties | ||
zfs set org.zfsonlinux.tpm2:index="$tpmindex" "${ENCRYPTIONROOT}" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since you store the index, you might want to search for an open NV slot and use that rather than requiring a hard coded default or user to specify. tpm2_getcap handles-nv-index Will give you the current in use handles... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is true. Additionally, looking at the 4.1 changelog: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Filed tpm2-software/tpm2-tools#1891 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yeah it just starts at index 0, perhaps we should smarten that up. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Maybe add an option --search= which makes the argument the starting index and searches until stop is reached. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is |
||
zfs set org.zfsonlinux.tpm2:pcrs="$tpmpcrs" "${ENCRYPTIONROOT}" | ||
fi |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -106,4 +106,15 @@ if [ ! -x "$DESTDIR/usr/bin/net" ]; then | |
chmod +x "$DESTDIR/usr/bin/net" | ||
fi | ||
|
||
#These are optional files to support tpm2-autounlock | ||
if [ -x /usr/bin/tpm2_startauthsession ]; then | ||
copy_exec /usr/bin/tpm2_nvread | ||
copy_exec /usr/bin/tpm2_nvreadlock | ||
copy_exec /usr/bin/tpm2_startauthsession | ||
copy_exec /usr/bin/tpm2_policypassword | ||
copy_exec /usr/bin/tpm2_policypcr | ||
copy_modules_dir kernel/drivers/char/tpm | ||
copy_exec /usr/lib/x86_64-linux-gnu/libtss2-tcti-device.so.0 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are the tpm2 tools x86_64 only? (I could see how they might be, though they probably also work on x32 and maybe work in i386?) If not, then the architecture here should probably be detected. |
||
fi | ||
|
||
exit 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this be plain
/bin/sh
? It looks like you have a bashism (or few) in here (I see a[[ ... ]]
), but they could probably be fixed.