From c79b99e11735ebbc23b0c37eb7f634e8c5005025 Mon Sep 17 00:00:00 2001 From: Nicolas Williams Date: Thu, 10 Jun 2021 17:26:12 -0500 Subject: [PATCH] Add sample encrypt-to-TPM scripts --- Attestation/README.md | 8 +- Enrollment/README.md | 93 +++++++++++++++++- Enrollment/send-to-tpm.sh | 165 ++++++++++++++++++++++++++++++++ Enrollment/tpm-receive.sh | 194 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 456 insertions(+), 4 deletions(-) create mode 100755 Enrollment/send-to-tpm.sh create mode 100755 Enrollment/tpm-receive.sh diff --git a/Attestation/README.md b/Attestation/README.md index 4a4d08b..27fcb8b 100644 --- a/Attestation/README.md +++ b/Attestation/README.md @@ -1,6 +1,7 @@ # What Attestation is -A computer can use a TPM to demonstrate: +An [enrolled device](/Enrollment/README.md) can use a TPM to +demonstrate: - possession of a valid TPM @@ -820,6 +821,11 @@ We'll discuss two ways to do this: the value zero so that extending it can disable use of `TPM2_MakeCredential()` post-boot. + We have two sample bash scripts demonstrating this approach: + + - [`send-to-tpm.sh`](/Enrollment/send-to-tpm.sh) + - [`tpm-receive.sh`](/Enrollment/tpm-receive.sh) + - use an `LTAK` -- a long-term `AK` I.e., an `AK` that lacks the `stClear` attribute, and _preferably_ diff --git a/Enrollment/README.md b/Enrollment/README.md index 76a0027..0367c68 100644 --- a/Enrollment/README.md +++ b/Enrollment/README.md @@ -38,7 +38,7 @@ device using only that information. - encrypted filesystems? - device credentials? (e.g., TLS server certificates, Kerberos keys ["keytabs"], etc.) -# Secrets Transport +# Secrets Long-Term Storage and Transport Every time an enrolled device reboots, or possibly more often, it may have to connect to an attestation server to obtain secrets from it that @@ -46,8 +46,95 @@ the device needs in order to proceed. For example, filesystem decryption keys, general network access, device authentication credentials, etc. -See [attestation](/Attestation/README.md) for details of how to -transport secrets onto an enrolled device post-enrollment. +See [attestation](/Attestation/README.md#Secret-Transport-Sub-Protocols) +for details of how to store and transport secrets onto an enrolled +device post-enrollment. + +## Encrypt-to-TPM Sample Scripts + +A pair of scripts are included here to demonstrate how to make long-term +secrets encrypted to TPMs for use in +[attestation](/Attestation/README.md) protocols. The method used is the +one described in the [attestation +tutorial](/Attestation/README.md#Secret-Transport-Sub-Protocols) using +[`TPM2_MakeCredential()`](/TPM-Commands/TPM2_MakeCredential.md) and +[`TPM2_ActivateCredential()`](/TPM-Commands/TPM2_ActivateCredential.md) +with a hard-coded, _well-known_ activation key (`WK`) to implement +encryption-to-`EKpub` with (optional) sender-asserted authorization +policy: + + - [`send-to-tpm.sh`](send-to-tpm.sh) + - [`tpm-receive.sh`](tpm-receive.sh) + +You can use these scripts like so: + + - without policy: + + ```bash + : ; # Make a secret + : ; dd if=/dev/urandom of=secret.bin bs=16 count=1 + : ; + : ; # Encrypt the secret to some TPM whose EKpub is in a file named + : ; # ek.pub: + : ; /safeboot/sbin/send-to-tpm.sh ek.pub secret.bin cipher.bin + fd32fa22c52cfc8e1a0c29eb38519f87084cab0b04b0d8f020a4d38b2f4e223e + 7fdad037a921f7eec4f97c08722692028e96888f0b970dc7b3bb6a9c97e8f988 + ``` + + ```bash + : ; # Decrypt the secret: + : ; tpm-receive.sh cipher.bin secret.bin + fd32fa22c52cfc8e1a0c29eb38519f87084cab0b04b0d8f020a4d38b2f4e223e + 7fdad037a921f7eec4f97c08722692028e96888f0b970dc7b3bb6a9c97e8f988 + name: 000be1fe1b777ead331f2da896ced2bf7a3949d732a0c6adf6f0a292567d587c4408 + 837197674484b3f81a90cc8d46a5d724fd52d76e06520b64f2a1da1b331469aa + fd32fa22c52cfc8e1a0c29eb38519f87084cab0b04b0d8f020a4d38b2f4e223e + 7fdad037a921f7eec4f97c08722692028e96888f0b970dc7b3bb6a9c97e8f988 + certinfodata:b7bd59980628c33a14377d53e165c229 + : ; + - with policy + + ```bash + : ; # Make up a policy (here that PCR11 must be unextended): + : ; dd if=/dev/zero of=pcr.dat bs=32 count=1 + : ; policy=(tpm2 policypcr -l sha256:11 -f pcr.dat) + : ; + : ; send-to-tpm.sh ek.pub secret.bin cipher.bin "${policy[@]}" + fd32fa22c52cfc8e1a0c29eb38519f87084cab0b04b0d8f020a4d38b2f4e223e + 7fdad037a921f7eec4f97c08722692028e96888f0b970dc7b3bb6a9c97e8f988 + ``` + + ```bash + : ; # We have to satisfy the same policy on the receive side: + : ; policy=(tpm2 policypcr -l sha256:11 -f pcr.dat) + : ; + : ; tpm-receive.sh -f cipher.bin "${policy[@]}" + fd32fa22c52cfc8e1a0c29eb38519f87084cab0b04b0d8f020a4d38b2f4e223e + 7fdad037a921f7eec4f97c08722692028e96888f0b970dc7b3bb6a9c97e8f988 + name: 000be1fe1b777ead331f2da896ced2bf7a3949d732a0c6adf6f0a292567d587c4408 + 837197674484b3f81a90cc8d46a5d724fd52d76e06520b64f2a1da1b331469aa + fd32fa22c52cfc8e1a0c29eb38519f87084cab0b04b0d8f020a4d38b2f4e223e + 7fdad037a921f7eec4f97c08722692028e96888f0b970dc7b3bb6a9c97e8f988 + certinfodata:b7bd59980628c33a14377d53e165c229 + : ; + ``` + + Multiple policy commands can be separated with a quoted semi-colon: + + ```bash + send-to-tpm.sh ... tpm2 policyblah ... \; policyfoo ... + ``` + + Multiple policy commands can be separated with a quoted semi-colon: + + ```bash + send-to-tpm.sh ... tpm2 policyblah ... \; policyfoo ... + ``` + + When a policy is specified, these scripts will automatically set the + `adminWithPolicy` attribute of the activation object, and will add + `tpm2 policycommandcode TPM2_CC_ActivateCredential` to the policy, as + that is required for activation objects with `adminWithPolicy` set. # Enrollment Semantics diff --git a/Enrollment/send-to-tpm.sh b/Enrollment/send-to-tpm.sh new file mode 100755 index 0000000..372e81a --- /dev/null +++ b/Enrollment/send-to-tpm.sh @@ -0,0 +1,165 @@ +#!/bin/bash + +PROG=${0##*/} + +set -euo pipefail + +function usage { + ((${1:-1} > 0)) && exec 1>&2 + cat <= 3)) || usage +ekpub_file=$1 +secret_file=$2 +out_file=$3 +shift 3 + +function err { + echo "ERROR: $*" 1>&2 + exit 1 +} + +[[ -f ${ekpub_file:-} ]] || usage +[[ -f ${secret_file:-} ]] || usage +[[ -f ${out_file:-} ]] && $force && rm -f "${out_file:-}" +[[ -f ${out_file:-} ]] && err "output file ($out_file) exists" + +# Make a temp dir and remove it when we exit: +d= +trap 'rm -rf "$d"' EXIT +d=$(mktemp -d) + +function exec_policy { + local add_commandcode=true + local has_policy=false + + while (($# > 0)); do + has_policy=true + cmd=() + while (($# > 0)) && [[ $1 != ';' ]]; do + cmd+=("$1") + if ((${#cmd[@]} == 1)) && [[ ${cmd[0]} = tpm2_* ]]; then + cmd+=(--session "${d}/session.ctx" --policy "${d}/policy") + elif ((${#cmd[@]} == 2)) && [[ ${cmd[0]} = tpm2 ]]; then + cmd+=(--session "${d}/session.ctx" --policy "${d}/policy") + fi + shift + done + (($# > 0)) && shift + # Run the policy command in the temp dir. It -or the last command- must + # leave a file there named 'policy'. + "${cmd[@]}" + if [[ ${cmd[0]} = tpm2 ]] && ((${#cmd[@]} == 1)); then + echo "Policy is incomplete" 1>&2 + exit 1 + fi + [[ ${cmd[0]} = tpm2 && ${cmd[1]} = policycommandcode ]] && + add_commandcode=false + [[ ${cmd[0]} = tpm2_policycommandcode ]] && add_commandcode=false + done + $has_policy && $add_commandcode && + tpm2 policycommandcode --session "${d}/session.ctx" \ + --policy "${d}/policy" \ + TPM2_CC_ActivateCredential +} + +function make_policyDigest { + # Start a trial session, execute the given policy commands, save the + # policyDigest. + tpm2 startauthsession --session "${d}/session.ctx" + exec_policy "$@" +} + +function wkname { + local attrs='decrypt|sign' + local has_policy + + # This is the WK. It was generated with: + # openssl genpkey -genparam \ + # -algorithm EC \ + # -out "${d}/ecp.pem" \ + # -pkeyopt ec_paramgen_curve:secp384r1 \ + # -pkeyopt ec_param_enc:named_curve + # openssl genpkey -paramfile "${d}/ecp.pem" + cat > "${d}/wkpriv.pem" <&2 + + # Load + attrs='decrypt|sign' + if (($# > 0)); then + make_policyDigest "$@" 1>&2 + attrs='adminwithpolicy|decrypt|sign' + has_policy=true + + # Flush again, but this time not saved sessions + tpm2 flushcontext --transient-object 1>&2 + tpm2 flushcontext --loaded-session 1>&2 + fi + + # Load the WK + tpm2 loadexternal -C n \ + -Gecc \ + -r "${d}/wkpriv.pem" \ + ${has_policy:+-L "${d}/policy"} \ + -a "$attrs" \ + -c "${d}/wk.ctx" | + grep ^name: | cut -d' ' -f2 +} + +[[ -z $wkname ]] && wkname=$(wkname "$@") + +tpm2 makecredential \ + --tcti "none" \ + --encryption-key "${ekpub_file}" \ + --name "$wkname" \ + --secret "${secret_file}" \ + --credential-blob "$out_file" diff --git a/Enrollment/tpm-receive.sh b/Enrollment/tpm-receive.sh new file mode 100755 index 0000000..32593f5 --- /dev/null +++ b/Enrollment/tpm-receive.sh @@ -0,0 +1,194 @@ +#!/bin/bash + +PROG=${0##*/} + +set -euo pipefail + +function usage { + echo "Usage: $PROG [OPTIONS] CIPHERTEXT-FILE OUT-FILE [POLICY-CMD [ARGS] [\; ...]]" + cat <= 2)) || usage +ciphertext_file=$1 +out_file=$2 +shift 2 + +[[ -f ${ciphertext_file:-} ]] || usage +[[ -f ${out_file:-} ]] && $force && rm -f "$out_file" +[[ -f ${out_file:-} ]] && usage + +d= +trap 'rm -rf "$d"' EXIT +d=$(mktemp -d) + +function v { + if $verbose; then + printf 'Running:' + printf ' %q' "$@" + printf '\n' + fi >/dev/tty || true + if "$@"; then + $verbose && printf '(SUCCESS)\n' >/dev/tty || true + else + stat=$? + printf 'ERROR: Command exited with %d\n' $stat >/dev/tty || true + return $stat + fi +} + +function exec_policy { + local add_commandcode=true + local has_policy=false + + while (($# > 0)); do + has_policy=true + cmd=() + while (($# > 0)) && [[ $1 != ';' ]]; do + cmd+=("$1") + if ((${#cmd[@]} == 1)) && [[ ${cmd[0]} = tpm2_* ]]; then + cmd+=(--session "${d}/session.ctx" --policy "${d}/policy") + elif ((${#cmd[@]} == 2)) && [[ ${cmd[0]} = tpm2 ]]; then + cmd+=(--session "${d}/session.ctx" --policy "${d}/policy") + fi + shift + done + (($# > 0)) && shift + # Run the policy command in the temp dir. It -or the last command- must + # leave a file there named 'policy'. + "${cmd[@]}" + if [[ ${cmd[0]} = tpm2 ]] && ((${#cmd[@]} == 1)); then + echo "Policy is incomplete" 1>&2 + exit 1 + fi + [[ ${cmd[0]} = tpm2 && ${cmd[1]} = policycommandcode ]] && + add_commandcode=false + [[ ${cmd[0]} = tpm2_policycommandcode ]] && add_commandcode=false + done + $has_policy && $add_commandcode && + tpm2 policycommandcode --session "${d}/session.ctx" \ + --policy "${d}/policy" \ + TPM2_CC_ActivateCredential +} + +function make_policyDigest { + tpm2 flushcontext --transient-object + tpm2 flushcontext --loaded-session + v tpm2 startauthsession --session "${d}/session.ctx" + exec_policy "$@" +} + +# Get the EK handle: +tpm2 flushcontext --transient-object +tpm2 flushcontext --loaded-session +tpm2 flushcontext --saved-session 1>&2 +tpm2 createek --key-algorithm rsa \ + --ek-context "${d}/ek.ctx" \ + --public "${d}/ek.pub" + +# Make policyDigest and load WK +attrs='decrypt|sign' +adminwithpolicy= +if (($# > 0)); then + make_policyDigest "$@" + attrs='adminwithpolicy|decrypt|sign' + adminwithpolicy=true +fi + +rm -f "${d}/session.ctx" + +# This is the WK. It was generated with: +# openssl genpkey -genparam \ +# -algorithm EC \ +# -out "${d}/ecp.pem" \ +# -pkeyopt ec_paramgen_curve:secp384r1 \ +# -pkeyopt ec_param_enc:named_curve +# openssl genpkey -paramfile "${d}/ecp.pem" +cat > "${d}/wkpriv.pem" <&2 +v tpm2 flushcontext --loaded-session 1>&2 +if v tpm2 loadexternal -C n \ + -Gecc \ + -r "${d}/wkpriv.pem" \ + ${adminwithpolicy:+-L "${d}/policy"} \ + -a "$attrs" \ + -c "${d}/wk.ctx" > "${d}/out" 2> "${d}/err"; then + cat "${d}/out" 1>&2 +else + stat=$? + echo "ERROR: Failed to load WK:" 1>&2 + cat "${d}/out" + cat "${d}/err" 1>&2 + exit $stat +fi + +# Create empty auth session for EK +v tpm2 flushcontext --transient-object +v tpm2 flushcontext --loaded-session +v tpm2 startauthsession --session "${d}/sessionek.ctx" --policy-session +v tpm2 policysecret --session "${d}/sessionek.ctx" --object-context endorsement + +activatecredential_args=() +if (($# > 0)); then + activatecredential_args+=(--credentialedkey-auth session:"${d}/session.ctx") + # Create auth session for the WK, since it has adminWithPolicy + v tpm2 flushcontext --transient-object + v tpm2 flushcontext --loaded-session + v tpm2 startauthsession --session "${d}/session.ctx" --policy-session + exec_policy "$@" + v tpm2 flushcontext --transient-object + v tpm2 flushcontext --loaded-session +fi +# Finally, ActivateCredential +$verbose && tpm2 readpublic -c "${d}/wk.ctx" | grep name: +v tpm2 activatecredential --credentialedkey-context "${d}/wk.ctx" \ + "${activatecredential_args[@]}" \ + --credentialkey-context "${d}/ek.ctx" \ + --credentialkey-auth session:"${d}/sessionek.ctx" \ + --credential-blob "$ciphertext_file" \ + -o "$out_file"