diff --git a/Makefile b/Makefile index 6c16338e..886af57d 100644 --- a/Makefile +++ b/Makefile @@ -307,6 +307,7 @@ shellcheck: sbin/tpm2-recv \ sbin/tpm2-policy \ initramfs/*/* \ + tests/test-enroll.sh \ ; do \ shellcheck $$file functions.sh ; \ done diff --git a/docs/attest-enroll.md b/docs/attest-enroll.md index 177152ac..df899a4b 100644 --- a/docs/attest-enroll.md +++ b/docs/attest-enroll.md @@ -93,14 +93,14 @@ Decryption is implemented by [`sbin/tpm2-recv`](/sbin/tpm2-recv). Two methods are possible for encryption to a target TPM's `EKpub`: - - the "EK" method (our name for it) + - the "WK" method (our name for it) - the "TK" method (our name for it) Both methods support setting a policy on the ciphertext such that any application using the target's TPM to decrypt it must first execute and satisfy that policy. -The "EK" method uses `TPM2_MakeCredential()` via tpm2-tools' `tpm2 +The "WK" method uses `TPM2_MakeCredential()` via tpm2-tools' `tpm2 makecredential` command, using the `none` TCTI (i.e., implemented in software). The target's `EKpub` is used as the `handle` input parameter to `TPM2_MakeCredential()`. A well-known key (`WK`), and the desired policy diff --git a/functions.sh b/functions.sh index c57ea475..719b213c 100755 --- a/functions.sh +++ b/functions.sh @@ -10,7 +10,7 @@ die() { echo "${PROG:+${PROG}: }$die_msg""$*" >&2 ; exit 1 ; } warn() { echo "$@" >&2 ; } error() { echo "$@" >&2 ; return 1 ; } info() { ((${VERBOSE:-0})) && echo "$@" >&2 ; return 0 ; } -debug() { ((${VERBOSE:-0})) && echo "$@" >&2 ; return 0 ; } +debug() { ((${VERBOSE:-0}>1)) && echo "$@" >&2 ; return 0 ; } ######################################## @@ -64,6 +64,7 @@ PCR_DEFAULT=0000000000000000000000000000000000000000000000000000000000000000 TPM2="$(command -v tpm2 || true)" [[ -z $TPM2 ]] && warn "tpm2 program not found! things will probably break" +[[ $TPM2 = tpm2 ]] && TPM2="command tpm2" # if the TPM2 resource manager is running, talk to it. # otherwise use a direct connection to the TPM @@ -75,12 +76,12 @@ if ! pidof tpm2-abrmd > /dev/null ; then fi fi - tpm2() { - if ((${VERBOSE:-0})); then - /usr/bin/time -f '%E %C' "$TPM2" "$@" + if ((${VERBOSE:-0} > 2)); then + # shellcheck disable=SC2086 + /usr/bin/time -f '%E %C' ${TPM2#command } "$@" else - "$TPM2" "$@" + $TPM2 "$@" fi } @@ -495,45 +496,219 @@ aead_decrypt() { fi } -# Execute a policy given as arguments. +indent() { + local depth=$((${depth:-0} + 1)) + + while ((depth > 0)); do printf ' '; ((depth--)); done +} + +# subexp_length POLICY... # -# The first argument may be a command code; if given, then {tpm2} -# {policycommandcode} will be added to the given policy. The rest must be -# {tpm2_policy*} or {tpm2} {policy*} commands w/o any {--session}|{-c} or -# {--policy}|{-L} arguments, and multiple commands may be given separate by +# Returns the number of arguments making up a policy expression. The caller +# will presumably shift those into an array and exec that sub-policy. +# +# See {exec_policy} for details on {POLICY}. +subexp_length() { + local -n nv="$1" + local i depth=0 startswithparen=false + shift + + nv=0 + for i in "$@"; do + ((++nv)) + if [[ $i = '(' ]]; then + ((nv == 1)) && startswithparen=true + ((++depth)) + elif [[ $i = ')' ]]; then + ((depth--)) || true + if ((depth == 0)) && $startswithparen; then + return 0; + fi + fi + ((depth < 0)) && die "Missing open parenthesis in policy expression: $*" + ((depth == 0)) && [[ $i = ';' ]] && return 0 + done + ((depth == 0)) || die "Missing close parenthesis in policy expression: $*" + return 0; +} + +# exec_policy SESSIONCTX POLICYDAT [ALTERNATIVES] POLICY... +# +# Starts a policy session {SESSIONCTX} and executes the given {POLICY} in that +# session, leaving the final policyDigest for the {POLICY} in {POLICYDAT}. +# +# {SESSIONCTX} is the name of a saved session context file. +# +# {POLICYDAT} is the name of a file into which to write the POLICY's digest. +# +# {ALTERNATIVES} is zero, one, or more integers 0 through 9 that are indices of +# policy alternations in the {POLICY} that are to be executed. +# +# {POLICY} starts with an optional command-code and is followed by +# {tpm2 policy*} command-lines as arguments, w/o any {--session}|{-c} or +# {--policy|-L} arguments. Multiple policy commands may be given, separated by # {';'}. # -# E.g., +# [TPM2_CC_*] tpm2 policy* ... ';' tpm2 policy* ... ';' ... +# +# {tpm2 policyor} command-lines are special: the alternatives must be given as +# separate arguments, either as a {POLICY} in parenthesis, or as policy +# digests: # -# exec_policy TPM2_CC_ActivateCredential "$@" -# exec_policy tpm2 policypcr ... ';' tpm2 policysigned ... -function exec_policy { +# exec_policy sess pol tpm2 policyor '(' POLICY0 ')' '(' POLICY1 ')' ... +# exec_policy sess pol tpm2 policyor $digest0 $digest1 ... +# exec_policy sess pol tpm2 policyor '(' POLICY0 ')' $digest1... +# +# Do not specify a command-code if the {POLICY} includes any {tpm2 policyor} +# commands. +# +# The output of this function is the digest of the {POLICYDAT} as it stands at +# the end of {POLICY} execution. +# +# Simple policy example (no alternation): +# +# exec_policy sp p TPM2_CC_ActivateCredential ';' \ +# tpm2 policysecret --object-context endorsement +# +# Complex example (alternation): +# +# # Compute digest of a policy that allows signing and decryption to +# # applications with access to the endorsement hierarchy: +# exec_policy sp p \ +# tpm2 policyor \ +# '(' tpm2 policycommandcode TPM2_CC_Sign ')' \ +# '(' tpm2 policycommandcode TPM2_CC_RSA_Decrypt ')' ';' \ +# tpm2 policysecret --object-context endorsement +# +# # Execute the same policy to sign: +# exec_policy 1 sp p \ +# tpm2 policyor \ +# '(' tpm2 policycommandcode TPM2_CC_Sign ')' \ +# '(' tpm2 policycommandcode TPM2_CC_RSA_Decrypt ')' ';' \ +# tpm2 policysecret --object-context endorsement +# +# # Execute the same policy to decrypt: +# exec_policy 1 sp p \ +# tpm2 policyor \ +# '(' tpm2 policycommandcode TPM2_CC_Sign ')' \ +# '(' tpm2 policycommandcode TPM2_CC_RSA_Decrypt ')' ';' \ +# tpm2 policysecret --object-context endorsement +exec_policy() { + local -a alternatives rm_policies + local depth=0 session policy + local trial=true + + session=$1 + policy=$2 + shift 2 + + # Use a dynamic variable to keep track of alternatives. + rm_policies=() + alternatives=() + while (($# > 0)) && [[ $1 = @([0-9]) ]]; do + alternatives+=("$1") + shift + done + + debug "$(indent)Running (in trial session): exec_policy_helper $*" + tpm2_flushsome + tpm2 startauthsession --session "$session" + exec_policy_helper "$@" + + debug "$(indent)Running (in policy session): exec_policy_helper $*" + tpm2_flushsome + tpm2 startauthsession --session "$session" \ + --policy-session + trial=false + exec_policy_helper "$@" + + rm -f "${rm_policies[@]}" +} + +# make_policyDigest SESSIONCTX POLICYDAT [ALTERNATIVES] POLICY... +# +# Compute the policyDigest of a given {POLICY} by executing it in a trial +# session, {SESSIONCTX}. The policyDigest will be placed in {POLICYDAT} +# (binary) and will be output on stdout (hex-encoded). +# +# See {exec_policy} for details on {POLICY}. +make_policyDigest() { + local -a alternatives rm_policies + local depth=0 session policy + local trial=true + + session=$1 + policy=$2 + shift 2 + + (($# > 0)) && [[ $1 = [0-9]* ]] && \ + warn "Ignoring alternation alternatives to take when computing policyDigest" + while (($# > 0)) && [[ $1 = [0-9]* ]]; do + shift + done + + rm -f "$policy" + rm_policies=() + alternatives=() + debug "$(indent)Running (in trial session): exec_policy_helper $*" + tpm2_flushsome + tpm2 startauthsession --session "$session" + exec_policy_helper "$@" + rm -f "${rm_policies[@]}" +} + +# exec_policy_helper POLICY... +exec_policy_helper() { local command_code='' local add_commandcode=true local has_policy=false + local subexp_length local -a cmd - if (($# > 0)) && [[ -z $1 || $1 = TPM2_CC_* ]]; then + (($# == 0)) && return 0 + + if [[ -z $1 || $1 = TPM2_CC_* ]]; then command_code=$1 shift fi 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") + subexp_length=0 + has_policy=true + subexp_length subexp_length "$@" + + [[ "${*:$subexp_length:1}" = ';' ]] && ((subexp_length--)) + if ((subexp_length > 0)) && [[ $1 = tpm2_* ]]; then + if [[ $1 = tpm2_policyor ]]; then + cmd=(exec_policyOR_helper) + else + cmd=("$1" --session "$session" + --policy "$policy") fi + ((subexp_length--)) || true shift - done - (($# > 0)) && shift + elif ((subexp_length > 1)) && [[ $1 = tpm2 ]]; then + if [[ $2 = policyor ]]; then + cmd=(exec_policyOR_helper) + else + cmd=("$1" "$2" --session "$session" + --policy "$policy") + fi + ((subexp_length -= 2)) || true + shift 2 + fi + + # Build ONE tpm2 policy* command-line (or exec_policyOR_helper) + cmd+=("${@:1:$subexp_length}") + shift "$subexp_length" + if [[ ${cmd[0]} = exec_policyOR_helper ]]; then + add_commandcode=false + fi + (($# > 0)) && [[ $1 = ';' ]] && shift + # Run the policy command in the temp dir. It -or the last command- must # leave a file there named 'policy'. + info "$(indent)Running: (AND) ${cmd[*]}" "${cmd[@]}" 1>&2 \ || die "unable to execute policy command: ${cmd[*]}" [[ ${cmd[0]} = tpm2 ]] && ((${#cmd[@]} == 1)) \ @@ -544,33 +719,129 @@ function exec_policy { && add_commandcode=false done if $has_policy && $add_commandcode && [[ -n $command_code ]]; then + info "$(indent)Running: (AND) tpm2 policycommandcode --session $session --policy $policy $command_code" tpm2 policycommandcode \ - --session "${d}/session.ctx" \ - --policy "${d}/policy" \ + --session "$session" \ + --policy "$policy" \ "$command_code" 1>&2 \ || die "unable to execute policy command: tpm2 policycommandcode $command_code" fi - xxd -p -c 100 "${d}/policy" + xxd -p -c 100 "$policy" } -# Compute the policyDigest of a given policy by executing it in a trial -# session. -function make_policyDigest { - tpm2 flushcontext --transient-object - tpm2 flushcontext --loaded-session - tpm2 startauthsession --session "${d}/session.ctx" - exec_policy "$@" +exec_policyOR_helper() { + local npolicies=0 + local subexp_length alt doit=false + local altsession='' altpolicy='' + local -a policyDigests + local -a policies + local -a cmd + + # The alternative to take, if any + alt=${alternatives[$depth]:-"-1"} + policyDigests=() + policies=() + cmd=() + + $trial || ((alt >= 0)) \ + || die "Missing alternatives at expression depth $depth" + + while (($# > 0)); do + if [[ $1 = ';' ]]; then + break + fi + subexp_length=0 + altsession="${session}-${depth}-${npolicies}" + altpolicy="${policy}-${depth}-${npolicies}" + policies+=("$altpolicy") + ((++depth)) + if [[ $1 = '(' ]]; then + if ! $trial && ((alt == npolicies)); then + # Execute this sub-policy in the main, policy + # session + altsession=$session + debug "$(indent)Using policy session $altsession" + doit=true + elif ! $trial && [[ -f $altpolicy ]]; then + debug "$(indent)Using previously computed policyDigest for sub-expression" + doit=false + else + # Execute this sub-policy in a trial session + debug "$(indent)Starting trial session $altsession" + debug "$(indent)-- trial=$trial altpolicy=$altpolicy" + tpm2 startauthsession --session "$altsession" + doit=true + fi + # Count the number of arguments for this + # sub-expression. (Includes the two parens.) + subexp_length subexp_length "$@" + shift # open paren + ((subexp_length -= 2)) || true # open and close parens + + # Build command-line + cmd=(exec_policy_helper "${@:1:$subexp_length}") + shift "$subexp_length" + shift # close paren + + if $doit; then + # Run it (changing the $session and $policy seen by the + # command-line) + debug "$(indent)Running: (OR) ${cmd[*]}" + session="$altsession" \ + policy="$altpolicy" \ + "${cmd[@]}" + fi + + # Cleanup + if $trial || ((alt != npolicies)); then + # Reduce our burden on the TPM by flushing this + # trial session + [[ -f $altsession ]] && tpm2 flushcontext "$altsession" + rm -f "$altsession" + fi + elif [[ ${#1} == 64 && $1 = +([0-9a-fA-F]) ]]; then + # Alternative given as SHA-256 digest + echo "$1" | xxd -p -r > "${policy}-${depth}-$npolicies" + shift + elif [[ -f $1 ]] && (($(stat -c %s "$1") == 32)); then + # Alternative given as policy SHA-256 digest (binary) file + cp "$1" "${policy}-${depth}-$npolicies" + shift + else + die "Invalid policyor alternative" + fi + policyDigests+=("$(xxd -p -c 100 "$altpolicy")") + ((++npolicies)) + ((depth--)) + done + IFS=",$IFS" + set -- "${policies[*]}" + IFS="${IFS#,}" + info "$(indent)ORing: ${policyDigests[*]}" + info "$(indent)Running: tpm2 policyor --session $session --policy $policy sha256:$1" + tpm2 policyor --session "$session" \ + --policy "$policy" \ + sha256:"$1" + + # We leave these around so that we can first compute the + # policies in trial sessions then use a single policy session to + # evaluate the taken alternatives: + rm_policies+=("${policies[@]}") } # A well-known private key just for the TPM2_MakeCredential()-based encryption # of secrets to TPMs. It was generated with: -# openssl genpkey -genparam \ -# -algorithm EC \ -# -out "${d}/ecp.pem" \ -# -pkeyopt ec_paramgen_curve:secp384r1 \ +# openssl genpkey -genparam \ +# -algorithm EC \ +# -out ecp.pem \ +# -pkeyopt ec_paramgen_curve:secp384r1 \ # -pkeyopt ec_param_enc:named_curve -# openssl genpkey -paramfile "${d}/ecp.pem" -function wkpriv { +# openssl genpkey -paramfile ecp.pem +# +# This key is NEVER used for signing or key exchange (therefore also not for +# encryption). It is only ever used for constructing activation objects whose +# cryptographic names will be bound by TPM2_MakeCredential() into its outputs. +wkpriv() { cat <<"EOF" -----BEGIN PRIVATE KEY----- MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDAlMnCWue7CfXjNLibH diff --git a/sbin/attest-enroll b/sbin/attest-enroll index 813fdfaa..3174d471 100755 --- a/sbin/attest-enroll +++ b/sbin/attest-enroll @@ -36,11 +36,12 @@ DBDIR="$BASEDIR/build/attest" POLICY= ESCROW_POLICY= ESCROW_PUBS_DIR= -TRANSPORT_METHOD=EK +TRANSPORT_METHOD=WK DEFAULT_EK_POLICY= declare -a GENPROGS GENPROGS=(genhostname genrootfskey) declare -A POLICIES +POLICIES=() # For the configure function (see below) declare -A vars @@ -223,7 +224,7 @@ $(configs) names of hard-coded policies, or names of executables (default: POLICIES[rootkey]=pcr11). - TRANSPORT_METHOD should be EK or TK (default: EK). + TRANSPORT_METHOD should be WK or TK (default: WK). NOTE: Until https://github.com/tpm2-software/tpm2-tools/issues/2761 is closed, {$PROG} may require a TPM (simulated will suffice) for @@ -337,14 +338,14 @@ if [[ -n ${DBDIR:-} && -f ${DBDIR:-}/attest-enroll.conf ]]; then configured=true fi fi -[[ ${TRANSPORT_METHOD:-} = @(TK|EK) ]] \ +[[ ${TRANSPORT_METHOD:-} = @(TK|WK) ]] \ || die "TRANSPORT_METHOD must be either 'TK' or 'EK'" [[ -z $ESCROW_PUBS_DIR || -d $ESCROW_PUBS_DIR ]] \ || die "ESCROW_PUBS_DIR -- must be a directory or not given" -# XXX This policy is for the EK method. +# XXX This policy is for the WK method. # -# FIXME We could make policies for EK/TK have the same digest by using +# FIXME We could make policies for WK/TK have the same digest by using # TPM2_PolicyOR: # # tpm2 policy... -L ... diff --git a/sbin/tpm2-policy b/sbin/tpm2-policy index 0c85c2f9..653219e7 100755 --- a/sbin/tpm2-policy +++ b/sbin/tpm2-policy @@ -16,45 +16,86 @@ shopt -s extglob function usage { ((${1:-1} > 0)) && exec 1>&2 cat < 0)) && [[ $1 = [0-9] && -n $command_code ]] \ + && die "-A and -D are not allowed when using alternations" + + # exec_policy will create the policy session + exec_policy "$session" "$policy" $command_code "$@" + exit 0 +fi + +session="${d}/session" +policyDigest=$(make_policyDigest "$session" "$policy" $command_code "$@") [[ -z $out ]] || ! $force >| "$out" [[ -z $out ]] || $force > "$out" echo "$policyDigest" diff --git a/sbin/tpm2-recv b/sbin/tpm2-recv index 20e43ebe..d84857e2 100755 --- a/sbin/tpm2-recv +++ b/sbin/tpm2-recv @@ -19,7 +19,7 @@ Usage: $PROG CIPHERTEXT OUT [POLICY-CMD [ARGS] [;] ...] If {CIPHERTEXT}.tk.pem, {CIPHERTEXT}.tk.dpriv, {CIPHERTEXT}.tk.pub, and {CIPHERTEXT}.tk.seed exist, then the "TK" method of encryption is - assumed. Otherwise the "EK" method of encryption is assumed. + assumed. Otherwise the "WK" method of encryption is assumed. See {tpm2-send} for details of the two encryption-to-TPM methods supported. @@ -80,6 +80,9 @@ for i in dpriv pub seed; do done $use_tk || command_code=TPM2_CC_ActivateCredential +# Don't pass a command-code to exec_policy if the policy refers to one +[[ "$*" = *TPM2_CC_* ]] && command_code='' + # Get the EK handle tpm2 flushcontext --transient-object tpm2 flushcontext --loaded-session @@ -90,8 +93,9 @@ tpm2 createek \ --public "${d}/ek.pub" \ || die "tpm2: unable to create ek object" -# Make policyDigest -(($# > 0)) && make_policyDigest "$command_code" "$@" +# Make policyDigest (needed for WK method, when loading the WK) +(($# > 0)) && ! $use_tk \ +&& make_policyDigest "${d}/session" "${d}/policy" $command_code "$@" # Create empty auth session for EK tpm2 flushcontext --transient-object @@ -103,13 +107,9 @@ tpm2 policysecret --session "${d}/sessionek.ctx" --object-context endorsement function auth { tpm2 flushcontext --transient-object tpm2 flushcontext --loaded-session - tpm2 startauthsession \ - --session "${d}/session.ctx" \ - --policy-session - # exec_policy will {die} if we fail to satisfy the policy - exec_policy "$command_code" "$@" + exec_policy "${d}/session.ctx" "${d}/policy.dat" $command_code "$@" tpm2 flushcontext --transient-object - tpm2 flushcontext --loaded-session + rm -f "${d}/policy.dat" } if $use_tk; then diff --git a/sbin/tpm2-send b/sbin/tpm2-send index 29a52b90..8d649a5f 100755 --- a/sbin/tpm2-send +++ b/sbin/tpm2-send @@ -39,7 +39,7 @@ Usage: $PROG EK-PUB SECRET OUT # Null policy Options: -h This help message. - -M EK|TK Method to use for encryption to TPM (default: EK). + -M WK|TK Method to use for encryption to TPM (default: WK). -P POLICY Use the named policy or policyDigest. -f Overwrite {OUT}. -x Trace this script. @@ -65,14 +65,14 @@ Usage: $PROG EK-PUB SECRET OUT # Null policy The two methods of encryption to a TPM are: - - EK Uses {TPM2_MakeCredential()} to encrypt an AES key to + - WK Uses {TPM2_MakeCredential()} to encrypt an AES key to the target's EKpub. The target uses {TPM2_ActivateCredential()} to decrypt the AES key. - A well-known key is used as the activation object, and - the given policy is associated with it. + A well-known key ("WK") is used as the activation object, + and the given policy is associated with it. This method produces a single file named {OUT}. - TK Uses {TPM2_Duplicate()} to encrypt an RSA private key to @@ -100,7 +100,7 @@ EOF . "$BASEDIR/../functions.sh" force=false -method=EK +method=WK policy= policyDigest= while getopts +:hfxM:P: opt; do @@ -121,9 +121,9 @@ function err { } case "$method" in -EK) command_code=TPM2_CC_ActivateCredential;; +WK) command_code=TPM2_CC_ActivateCredential;; TK) command_code=TPM2_CC_RSA_Decrypt;; -*) err "METHOD must be \"EK\" or \"TK\"";; +*) err "METHOD must be \"WK\" or \"TK\"";; esac if [[ -n $policy ]] && (($# > 3)); then echo "Error: -P and policy commands are mutually exclusive" 1>&2 @@ -179,7 +179,7 @@ function wkname_tpm { attrs='adminwithpolicy|sign' has_policy=true elif (($# > 0)); then - make_policyDigest "$command_code" "$@" 1>&2 + make_policyDigest "${d}/sesswk" "${d}/policy" "$command_code" "$@" 1>&2 attrs='adminwithpolicy|sign' has_policy=true @@ -229,7 +229,7 @@ function wkname { } case "$method" in -EK) info "Computing WKname" +WK) info "Computing WKname" wkname=$(wkname "$@") \ || die "unable to compute the MakeCredential activation object's cryptographic name" info "Encrypting to EKpub using TPM2_MakeCredential" @@ -251,7 +251,7 @@ TK) info "Generating TK" args=() if (($# > 0)); then - make_policyDigest "$command_code" "$@" 1>&2 + make_policyDigest "${d}/sesstk" "${d}/policy" "$command_code" "$@" 1>&2 args=("--policy=${d}/policy") fi diff --git a/tests/test-enroll.sh b/tests/test-enroll.sh index 88b2ddcb..21c2402d 100755 --- a/tests/test-enroll.sh +++ b/tests/test-enroll.sh @@ -12,7 +12,7 @@ fi TOP=${TOP%/*} -# shellcheck source=functions.sh +# shellcheck disable=SC1091 . "$TOP/functions.sh" #PATH=$TOP/sbin:$TOP/swtpm/src/swtpm:$PATH @@ -49,7 +49,7 @@ cat > "${d}/attest-enroll.conf" <