diff --git a/Attestation/README.md b/Attestation/README.md index 27fcb8b..0271737 100644 --- a/Attestation/README.md +++ b/Attestation/README.md @@ -97,19 +97,21 @@ For more about enrollment see the tutorial specifically for - `SCn` == server-to-client message number `n` - `{stuff, more_stuff}` == a sequence of data, a "struct" - `{"key":,...}` == JSON text - - `TPM2_MakeCredential()` == outputs of calling `TPM2_MakeCredential()` with `args` arguments - - `TPM2_Certify()` == outputs of calling `TPM2_Certify()` with `args` arguments + - `TPM2_Foo()` == outputs of calling some TPM 2.0 command with `args` arguments - `XK` == `` key, for some `` purpose (the TPM-resident object and its private key) - `EK` == endorsement key (the TPM-resident object and its private key) - `AK` == attestation key (the TPM-resident object and its private key) - `TK` == transport key (the TPM-resident object and its private key) + - `WK` == well-known key used only as a `TPM2_ActivateCredential()` activation object, not used for any actual encryption or signing - `XKpub` == ``'s public key, for some `` purpose - - `EKpub` == endorsement public key - - `AKpub` == attestation public key - - `TKpub` == transport public key + - `EKpub` == `EK` public key + - `AKpub` == `AK` public key + - `TKpub` == `TK` public key + - `WKpub` == `WK` public key - `XKname` == ``'s cryptographic name, for some `` purpose - - `EKname` == endorsement key's cryptographic name - - `AKname` == attestation key's cryptographic name + - `EKname` == `EK`'s cryptographic name + - `AKname` == `AK`'s cryptographic name + - `WKname` == `WK`'s cryptographic name ## Threat Models @@ -118,6 +120,7 @@ address: - attestation client impersonation - attestation server impersonation + - replay attacks - unauthorized firmware and/or OS updates - theft or compromise of of attestation servers - theft of client devices or their local storage (e.g., disks, JBODs) @@ -264,9 +267,9 @@ endorsement keys. Let's start with few observations and security considerations: - - Clients need to know which PCRs to quote. E.g., the [Safe Boot](https://safeboot.dev/) - project and the [IBM sample attestation client and server](https://sourceforge.net/projects/ibmtpm20acs/) - have the client ask for a list of PCRs and then the client quotes + - Clients need to know which PCRs to quote. E.g., + the [IBM sample attestation client and server](https://sourceforge.net/projects/ibmtpm20acs/) + has the client ask for a list of PCRs and then the client quotes just those. But clients could just quote all PCRs. It's more data to send, but @@ -279,25 +282,53 @@ Let's start with few observations and security considerations: stateless method is to use a timestamp and reject requests with old timestamps. - - Replay protection of server to client responses is mostly either not - needed or implicitly provided by [`TPM2_MakeCredential()`](TPM2_MakeCredential.md) - because `TPM2_MakeCredential()` generates a secret seed that - randomizes its outputs even when all the inputs are the same across - multiple calls to it. + As well, one can use the `resetCount` from the quote to check if an + attestation is the first after a reboot or not. Though this does + require that the attestation server maintain some writable state + (namely, the reset count. + + - Replay protection of server to client responses is provided by using + a different `AK` each time the client attests. This works because + [`TPM2_MakeCredential()`](/TPM-Commands/TPM2_MakeCredential.md) binds + the `AK` such that + [`TPM2_ActivateCredential()`](/TPM-Commands/TPM2_ActivateCredential.md) + will not succeed unless the server used the same `AKname` as the name + of the `AK` used by the client. - Ultimately the protocol *must* make use of [`TPM2_MakeCredential()`](/TPM-Commands/TPM2_MakeCredential.md) and [`TPM2_ActivateCredential()`](/TPM-Commands/TPM2_ActivateCredential.md) in order to authenticate a TPM-running host via its TPM's EKpub. + > The same is not true of [`TPM2_Quote()`](/TPM-Commands/TPM2_Quote.md) + > because one can build an attestation protocol that does not depend + > on signing quotes. Essentially one can simply send an unsigned + > reading of the client's TPM's PCRs and clock information and use an + > activation object with `adminWithPolicy` set and a `policyDigest` + > of a policy that uses the `TPM2_PolicyCounterTimer() and + > `TPM2_PolicyPCR()` commands to enforce that the `resetCount` and + > the PCRs are as asserted in the protocol. The server can then + > construct the same policy to compute the name of the activation + > object for + > [`TPM2_MakeCredential()`](/TPM-Commands/TPM2_MakeCredential.md), + > knowing that + > [`TPM2_ActivateCredential()`](/TPM-Commands/TPM2_ActivateCredential.md) + > will enforce that policy. + - Privacy protection of client identifiers may be needed, in which case - TLS may be desired. + TLS may be desired. Alternatively, the client could encrypt a + session key to a public of the attestation server using + [`TPM2_MakeCredential()`](/TPM-Commands/TPM2_MakeCredential.md), and + then use the session key to encrypt confidential parameters, thus + building something of a TLS-like protocol. - Even if a single round trip attestation protocol is adequate, a return routability check may be needed to avoid denial of service attacks. I.e., do not run a single round trip attestation protocol over UDP without first requiring the client to echo a nonce/cookie. + Using TCP effectively provides a return routability check. + - Statelessness on the server side is highly desirable, as that should permit having multiple servers and each of a client's messages can go to different servers. Conversely, keeping state on the server across @@ -308,14 +339,20 @@ Let's start with few observations and security considerations: protocol messages could all be idempotent and therefore map well onto HTTP `GET` requests but for the fact that all the things that may be have to be sent may not fit on a URI local part or URI query - parameters, therefore HTTP `POST` is the better option. + parameters (and `GET` has no request body), therefore HTTP `POST` is + needed for its ability to send a request body. ### Error Cases Not Shown Note that error cases are not shown in the protocols described below. Naturally, in case of error the attestation server will send a suitable -error message back to the client. +error message back to the client. Providing integrity protection for +error messages is tricky, as there will always be some kinds of errors +for which integrity protection cannot be provided, but also, there is no +natural key with which to sign errors. An actual attestation protocol +specification may require that clients know a public key that the server +can use to sign its errors with. ### Databases, Log Sinks, and Dashboarding / Alerting Systems Not Shown @@ -386,12 +423,12 @@ The client obtains those items IFF (if and only if) the AK is resident in the same TPM as the EK, courtesy of `TPM2_ActivateCredential()`'s semantics. -NOTE well that in single round trip attestation protocols using only -decrypt-only EKs it is *essential* that the AKcert not be logged in any -public place since otherwise an attacker can make and send `CS0` using a -non-TPM-resident AK and any TPM's EKpub/EKcert known to the attacker, -and then it may recover the AK certificate from the log in spite of -being unable to recover the AK certificate from `SC1`! +> NOTE well that in single round trip attestation protocols using only +> decrypt-only EKs it is *essential* that the AKcert not be logged in +> any public place since otherwise an attacker can make and send `CS0` +> using a non-TPM-resident AK and any TPM's EKpub/EKcert known to the +> attacker, and then it may recover the AK certificate from the log in +> spite of being unable to recover the AK certificate from `SC1`! Alternatively, a single round trip attestation protocol can be implemented as an optimization to a two round trip protocol when the AK @@ -407,6 +444,13 @@ database: Encrypt_session_key({AKcert, filesystem_keys, etc.})} ``` +> NOTE: persisting the `AK` means that the `AK` must not have `stClear` +> set, which in turn means that it can be used and reused across +> reboots, so detecting reboots requires more mutable, synchronized +> state on the server to keep track of clients' `resetCount`s. Mutable, +> synchronized state complicates distributed databases, so it may not be +> desirable. + ### Three-Message Attestation Protocol Patterns A single round trip protocol using encrypt-only EKpub will not @@ -427,13 +471,13 @@ desirable anyways for monitoring and alerting purposes. (In this diagram we show the use of a TPM simulator on the server side for implementing [`TPM2_MakeCredential()`](/TPM-Commands/TPM2_MakeCredential.md).) -NOTE well that in this protocol, like single round trip attestation -protocols using only decrypt-only EKs, it is *essential* that the AKcert -not be logged in any public place since otherwise an attacker can make -and send `CS0` using a non-TPM-resident AK and any TPM's EKpub/EKcert -known to the attacker, and then it may recover the AK certificate from -the log in spite of being unable to recover the AK certificate from -`SC1`! +> NOTE well that in this protocol, like single round trip attestation +> protocols using only decrypt-only EKs, it is *essential* that the +> AKcert not be logged in any public place since otherwise an attacker +> can make and send `CS0` using a non-TPM-resident AK and any TPM's +> EKpub/EKcert known to the attacker, and then it may recover the AK +> certificate from the log in spite of being unable to recover the AK +> certificate from `SC1`! If such a protocol is instantiated over HTTP or TCP, it will really be more like a two round trip protocol: @@ -575,20 +619,39 @@ to two round trips. ### Actual Protocols: safeboot.dev +[Safeboot.dev](https://safeboot.dev) uses a single round trip stateless +attestation protocol, with a separate, one-time enrollment protocol. + ``` - CS0: - SC0: nonce, PCR_list - CS1: [ID], EKpub, [EKcert], AKpub, PCRs, eventlog, nonce, + CS0: [ID], EKpub, [EKcert], AKpub, PCRs, eventlog, timestamp, TPM2_Quote(AK, PCRs, extra_data)=Signed_AK({hash-of-PCRs, misc, extra_data}) - SC1: {TPM2_MakeCredential(EKpub, AKpub, session_key), - Encrypt_session_key({filesystem_keys})} + SC0: {TPM2_MakeCredential(EKpub, AKpub, session_key), + Encrypt_session_key({long-term-secrets-encrypted-to-EKpub})} + + with + + long-term-secrets-encrypted-to-EKpub = + [{TPM2_MakeCredential(EKpub, WKname(policy), aes_key0), + Encrypt_aes_key0(secret0)}, + {TPM2_MakeCredential(EKpub, WKname(policy), aes_key1), + Encrypt_aes_key1(secret1)}, + .., + {TPM2_MakeCredential(EKpub, WKname(policy), aes_keyN), + Encrypt_aes_key1(secretN)}] ``` -Nonce validation is currently not well-developed in Safeboot. -If a timestamp is used instead of a nonce, and if the client assumes all -PCRs are desired, then this becomes a one round trip protocol. +During an initial enrollment step, the enrollment server can create any +number of secrets to deliver to the client later: -An AKcert will be added to the Safeboot protocol soon. + - local storage / filesystem keys + + - private keys and certificates (PKIX, OpenSSH) + + - OpenSSH host keys + + - Kerberos keys + + - etc. ## Attestation Protocol Patterns and Actual Protocols (signing-only EKs) @@ -762,7 +825,7 @@ would not be performant if, for example, those secrets are being used to encrypt filesystems. We must inherently trust the client to keep those secrets safe when running. -## Break-Glass Recovery +## Break-Glass Recovery and Escrow For break-glass recovery, the simplest thing to do is to store `Encrypt_backupKey({EKpub, hostname, secrets})`, where `backupKey` is an diff --git a/Enrollment/README.md b/Enrollment/README.md index 0367c68..c1212fd 100644 --- a/Enrollment/README.md +++ b/Enrollment/README.md @@ -23,6 +23,19 @@ For example, one might scan an endorsement key (EK) public key or certificate from a QR code on a shipment manifest and then enroll the device using only that information. +## Safeboot.dev Enrollment Protocol + +[Safeboot.dev](https://safeboot.dev) has an enrollment script, +`attest-enroll` which can have a trivial HTTP API binding where an +authenticated and authorized client principal `POST`s an `EKpub` and a +device name to the server. The server then creates enrollment state for +the device that will be used during subsequence attestation. + +The [safeboot.dev enrollment process](https://github.com/osresearch/safeboot/blob/master/docs/enrollment.md) +does not require any interaction with the enrollee device except to +extract its `EKpub`. When the `EKpub` can be determined in an off-line +manner, then the safeboot.dev enrollment process can be fully off-line. + # Server-Side State to Create during Enrollment - device name <-> EKpub binding @@ -52,19 +65,34 @@ 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 +A pair of scripts used in [safeboot.dev](https://safeboot.dev) 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: +either of two methods to encrypt to a TPM: - - [`send-to-tpm.sh`](send-to-tpm.sh) - - [`tpm-receive.sh`](tpm-receive.sh) + - The "EK" method of encryption to a TPM uses + [`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. + + - The "TK" method of encryption to a TPM uses a software implementation + of [`TPM2_Duplicate()`](/TPM-Commands/TPM2_Duplicate.md) to wrap the + private part of a "transport key" (`TK`) to the target TPM, then + normal RSA encryption to the public part of the `TK`. The ciphertext + consists of the outputs of `TPM2_Duplicate()` and the ciphertext + produced by RSA encryption to the TK. + +The "EK" method is the default. Both methods support sender-asserted +policies. + +The scripts: + + - [`send-to-tpm`](send-to-tpm) + - [`tpm-receive`](tpm-receive) You can use these scripts like so: @@ -76,65 +104,73 @@ You can use these scripts like so: : ; : ; # 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 + : ; /safeboot/sbin/send-to-tpm ek.pub secret.bin cipher.bin + : ; ``` ```bash : ; # Decrypt the secret: - : ; tpm-receive.sh cipher.bin secret.bin - fd32fa22c52cfc8e1a0c29eb38519f87084cab0b04b0d8f020a4d38b2f4e223e - 7fdad037a921f7eec4f97c08722692028e96888f0b970dc7b3bb6a9c97e8f988 - name: 000be1fe1b777ead331f2da896ced2bf7a3949d732a0c6adf6f0a292567d587c4408 + : ; tpm-receive cipher.bin plaintext.bin 837197674484b3f81a90cc8d46a5d724fd52d76e06520b64f2a1da1b331469aa - fd32fa22c52cfc8e1a0c29eb38519f87084cab0b04b0d8f020a4d38b2f4e223e - 7fdad037a921f7eec4f97c08722692028e96888f0b970dc7b3bb6a9c97e8f988 - certinfodata:b7bd59980628c33a14377d53e165c229 + name: 000bc76d1462d32d5e6051d0aa121edfa5ed66b8e7f3632ce3c5a172b1ebd8aabc40 : ; + ``` + - with policy + > Here we use a policy that `PCR #11` has not been extended. The + > idea is to extend it immediately after decrypting the ciphertext, + > which means that the ciphertext cannot again be decrypted later + > (by, say, some other application with access to the same TPM) + > without rebooting. + ```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[@]}" + : ; send-to-tpm ek.pub secret.bin cipher.bin "${policy[@]}" fd32fa22c52cfc8e1a0c29eb38519f87084cab0b04b0d8f020a4d38b2f4e223e 7fdad037a921f7eec4f97c08722692028e96888f0b970dc7b3bb6a9c97e8f988 + 7fdad037a921f7eec4f97c08722692028e96888f0b970dc7b3bb6a9c97e8f988 + policyDigest: + 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[@]}" + : ; tpm-receive cipher.bin plaintext.bin "${policy[@]}" fd32fa22c52cfc8e1a0c29eb38519f87084cab0b04b0d8f020a4d38b2f4e223e 7fdad037a921f7eec4f97c08722692028e96888f0b970dc7b3bb6a9c97e8f988 - name: 000be1fe1b777ead331f2da896ced2bf7a3949d732a0c6adf6f0a292567d587c4408 + 7fdad037a921f7eec4f97c08722692028e96888f0b970dc7b3bb6a9c97e8f988 837197674484b3f81a90cc8d46a5d724fd52d76e06520b64f2a1da1b331469aa + name: 000b20a6cc44c93ad206196c65028f9a8bf2590de0b8f89bca9e968f09f4e616dba6 fd32fa22c52cfc8e1a0c29eb38519f87084cab0b04b0d8f020a4d38b2f4e223e 7fdad037a921f7eec4f97c08722692028e96888f0b970dc7b3bb6a9c97e8f988 - certinfodata:b7bd59980628c33a14377d53e165c229 + 7fdad037a921f7eec4f97c08722692028e96888f0b970dc7b3bb6a9c97e8f988 : ; ``` Multiple policy commands can be separated with a quoted semi-colon: ```bash - send-to-tpm.sh ... tpm2 policyblah ... \; policyfoo ... + send-to-tpm ... tpm2 policyblah ... \; policyfoo ... ``` Multiple policy commands can be separated with a quoted semi-colon: ```bash - send-to-tpm.sh ... tpm2 policyblah ... \; policyfoo ... + send-to-tpm ... 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. + `tpm2 policycommandcode TPM2_CC_ActivateCredential` ("EK" method) or + `tpm2 policycommandcode TPM2_CC_RSA_Decrypt` ("TK" method) to the + policy. # Enrollment Semantics @@ -224,3 +260,20 @@ TPMs then perhaps there is a role for the TPM to play in enrollment. # Security Considerations TBD + +The enrollment database, though it contains ciphertexts of secrets +encrypted to enrolled devices' TPMs, is nonetheless to be kept +confidential. This is necessary to avoid attacks where an attacker +compromises an enrolled device then attempts to decrypt those +ciphertexts with the enrolled device's TPM. These ciphertexts should +only be furnished to the device as part of an attestation protocol. + +For the same reason, these ciphertexts must be super-encrypted when +delivering them to enrolled devices during attestation. + +Ciphertexts in enrolled state should be made with suitable sender- +asserted policies. For example, asserting that `PCR #11` has not been +extended so that immediately after decrypting such a ciphertext the +client can extend `PCR #11` to make decrypting that ciphertext again +impossible without an intervening reboot. + diff --git a/Enrollment/send-to-tpm b/Enrollment/send-to-tpm new file mode 100755 index 0000000..cb0dbc8 --- /dev/null +++ b/Enrollment/send-to-tpm @@ -0,0 +1,331 @@ +#!/bin/bash + +PROG=${0##*/} + +set -euo pipefail +shopt -s extglob + +die() { echo "${PROG:+${PROG}: }$die_msg""$*" >&2 ; exit 1 ; } +info() { ((${VERBOSE:-0})) && echo "$@" >&2 ; return 0 ; } + +function usage { + ((${1:-1} > 0)) && exec 1>&2 + pager=cat + if [[ -t 0 && -t 1 && -t 2 ]]; then + if [[ -z ${PAGER:-} ]] && type less >/dev/null 2>&1; then + pager=less + elif [[ -z ${PAGER:-} ]] && type more >/dev/null 2>&1; then + pager=more + elif [[ -n ${PAGER:-} ]]; then + pager=$PAGER + fi + fi + $pager <&2 + exit 1 +} + +case "$method" in +EK) command_code=TPM2_CC_ActivateCredential;; +TK) command_code=TPM2_CC_RSA_Decrypt;; +*) err "METHOD must be \"EK\" or \"TK\"";; +esac +if [[ -n $policy ]] && (($# > 3)); then + echo "Error: -P and policy commands are mutually exclusive" 1>&2 + exit 1 +fi +if [[ -n $policy ]]; then + (($# == 3)) || usage + if ((${#policy} == 64)) && + [[ ! -f $policy && $policy = +([0-9a-fA-F]) ]]; then + # $policy is a policyDigest + policyDigest=$policy + elif [[ -x $policy ]]; then + # Run the script in $policy to get a policyDigest + policyDigest=$("$policy") + else + err "Given policy is neither a SHA-256 policyDigest nor a policy script" + fi +fi +(($# >= 3)) || usage + +ekpub_file=$1 +secret_file=$2 +out_file=$3 +shift 3 + +[[ -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) + +# Execute a policy given as arguments. +# +# 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 +# {';'}. +# +# E.g., +# +# exec_policy TPM2_CC_ActivateCredential "$@" +# exec_policy tpm2 policypcr ... ';' tpm2 policysigned ... +function exec_policy { + local command_code='' + local add_commandcode=true + local has_policy=false + local -a cmd + + if (($# > 0)) && [[ -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") + 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[@]}" 1>&2 \ + || die "unable to execute policy command: ${cmd[*]}" + [[ ${cmd[0]} = tpm2 ]] && ((${#cmd[@]} == 1)) \ + && die "Policy is incomplete" + [[ ${cmd[0]} = tpm2 && ${cmd[1]} = policycommandcode ]] \ + && add_commandcode=false + [[ ${cmd[0]} = tpm2_policycommandcode ]] \ + && add_commandcode=false + done + if $has_policy && $add_commandcode && [[ -n $command_code ]]; then + tpm2 policycommandcode \ + --session "${d}/session.ctx" \ + --policy "${d}/policy" \ + "$command_code" 1>&2 \ + || die "unable to execute policy command: tpm2 policycommandcode $command_code" + fi + xxd -p -c 100 "${d}/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 "$@" +} + +# 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 \ +# -pkeyopt ec_param_enc:named_curve +# openssl genpkey -paramfile "${d}/ecp.pem" +function wkpriv { + cat <<"EOF" +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDAlMnCWue7CfXjNLibH +PTJrsOLUcoxqU3FLWYEWMI+HuPnzcwwl7SkKN6cpf4H3oQihZANiAAQ1pw6D5QVw +vymljYVDyrUriOet8zPB/9tq9XJ7A54qsVkaVufAuEJ6GIvD4xUZ27manMosJADS +aW2TLJkwxecRh2eTwPtSx2U32M2/yHeuWRV/0juiIozefPsTAlHAi3E= +-----END PRIVATE KEY----- +EOF +} + +# Compute a well-known activation object's name for use in +# TPM2_MakeCredential(), binding a given policy into it. +# +# This version uses a TPM via {tpm2 loadexternal}. +function wkname_tpm { + local attrs='sign' + local has_policy= + + wkpriv > "${d}/wkpriv.pem" + + tpm2 flushcontext --transient-object + tpm2 flushcontext --loaded-session + tpm2 flushcontext --saved-session 1>&2 + + # Load + if [[ -n $policyDigest ]]; then + tpm2 startauthsession --session "${d}/session.ctx" + printf '%s' "$policyDigest" | xxd -p -r > "${d}/policy" + echo "policyDigest: $(xxd -p -c 100 < "${d}/policy")" 1>&2 + attrs='adminwithpolicy|sign' + has_policy=true + elif (($# > 0)); then + make_policyDigest "$command_code" "$@" 1>&2 + attrs='adminwithpolicy|sign' + has_policy=true + + # Flush again, but this time not saved sessions + tpm2 flushcontext --transient-object 1>&2 + tpm2 flushcontext --loaded-session 1>&2 + echo "policyDigest: $(xxd -p -c 100 < "${d}/policy")" 1>&2 + fi + + # Load the WK + tpm2 loadexternal \ + --hierarchy n \ + --key-algorithm ecc \ + --private "${d}/wkpriv.pem" \ + ${has_policy:+ --policy "${d}/policy"} \ + --attributes "$attrs" \ + --key-context "${d}/wk.ctx" \ + | grep ^name: | cut -d' ' -f2 \ + || die "unable to load the WK into a TPM for computing its name" +} + +case "$method" in +EK) info "Computing WKname" + wkname=$(wkname_tpm "$@") \ + || die "unable to compute the MakeCredential activation object's cryptographic name" + info "Encrypting to EKpub using TPM2_MakeCredential" + tpm2 makecredential \ + --tcti "none" \ + --encryption-key "${ekpub_file}" \ + --name "$wkname" \ + --secret "${secret_file}" \ + --credential-blob "$out_file" \ + || die "unable to MakeCredential";; +TK) info "Generating TK" + openssl genrsa -out "${d}/tk-priv.pem" \ + || die "unable to create TK private key" + openssl rsa \ + -pubout \ + -in "${d}/tk-priv.pem" \ + -out "${d}/tk.pem" \ + || die "unable to create TK public key" + + args=() + if (($# > 0)); then + make_policyDigest "$command_code" "$@" 1>&2 + args=("--policy=${d}/policy") + fi + + info "Exporting TK to EKpub" + tpm2 duplicate \ + --tcti none \ + --parent-public="$ekpub_file" \ + --wrapper-algorithm=rsa \ + "${args[@]}" \ + --private-key="${d}/tk-priv.pem" \ + --public="${out_file}.tk.pub" \ + --private="${out_file}.tk.dpriv" \ + --encrypted-seed="${out_file}.tk.seed" \ + || die "$0: unable to duplicate TK into TPM for EK" + + info "Encrypting to TK" + openssl rsautl \ + -encrypt \ + -pubin \ + -inkey "${d}/tk.pem" \ + -in "$secret_file" \ + -out "${out_file}" \ + || die "$0: unable to encrypt to TK" ;; +esac diff --git a/Enrollment/send-to-tpm.sh b/Enrollment/send-to-tpm.sh deleted file mode 100755 index 372e81a..0000000 --- a/Enrollment/send-to-tpm.sh +++ /dev/null @@ -1,165 +0,0 @@ -#!/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 b/Enrollment/tpm-receive new file mode 100755 index 0000000..db118f0 --- /dev/null +++ b/Enrollment/tpm-receive @@ -0,0 +1,284 @@ +#!/bin/bash + +PROG=${0##*/} + +set -euo pipefail +shopt -s extglob + +die() { echo "${PROG:+${PROG}: }$die_msg""$*" >&2 ; exit 1 ; } +info() { ((${VERBOSE:-0})) && echo "$@" >&2 ; return 0 ; } + +function usage { + cat <= 2)) || usage +ciphertext_file=$1 +out_file=$2 +shift 2 + +[[ ! -f ${ciphertext_file:-} ]] && die "No ciphertext file given" +[[ -f ${out_file:-} ]] && $force && rm -f "$out_file" +[[ ! -f ${out_file:-} ]] || die "Plaintext file exists; use -f?" + +d= +trap 'rm -rf "$d"' EXIT +d=$(mktemp -d) + +use_tk=true +command_code=TPM2_CC_RSA_Decrypt +for i in dpriv pub seed; do + ($use_tk && [[ -s ${ciphertext_file}.tk.$i ]]) || use_tk=false +done +$use_tk || command_code=TPM2_CC_ActivateCredential + +# Execute a policy given as arguments. +# +# 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 +# {';'}. +# +# E.g., +# +# exec_policy TPM2_CC_ActivateCredential "$@" +# exec_policy tpm2 policypcr ... ';' tpm2 policysigned ... +function exec_policy { + local command_code='' + local add_commandcode=true + local has_policy=false + local -a cmd + + if (($# > 0)) && [[ -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") + 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[@]}" 1>&2 \ + || die "unable to execute policy command: ${cmd[*]}" + [[ ${cmd[0]} = tpm2 ]] && ((${#cmd[@]} == 1)) \ + && die "Policy is incomplete" + [[ ${cmd[0]} = tpm2 && ${cmd[1]} = policycommandcode ]] \ + && add_commandcode=false + [[ ${cmd[0]} = tpm2_policycommandcode ]] \ + && add_commandcode=false + done + if $has_policy && $add_commandcode && [[ -n $command_code ]]; then + tpm2 policycommandcode \ + --session "${d}/session.ctx" \ + --policy "${d}/policy" \ + "$command_code" 1>&2 \ + || die "unable to execute policy command: tpm2 policycommandcode $command_code" + fi + xxd -p -c 100 "${d}/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 "$@" +} + +# 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 \ +# -pkeyopt ec_param_enc:named_curve +# openssl genpkey -paramfile "${d}/ecp.pem" +function wkpriv { + cat <<"EOF" +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDAlMnCWue7CfXjNLibH +PTJrsOLUcoxqU3FLWYEWMI+HuPnzcwwl7SkKN6cpf4H3oQihZANiAAQ1pw6D5QVw +vymljYVDyrUriOet8zPB/9tq9XJ7A54qsVkaVufAuEJ6GIvD4xUZ27manMosJADS +aW2TLJkwxecRh2eTwPtSx2U32M2/yHeuWRV/0juiIozefPsTAlHAi3E= +-----END PRIVATE KEY----- +EOF +} + +# 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" \ +|| die "tpm2: unable to create ek object" + +# Make policyDigest +(($# > 0)) && make_policyDigest "$command_code" "$@" + +# Create empty auth session for EK +tpm2 flushcontext --transient-object +tpm2 flushcontext --loaded-session +tpm2 startauthsession --session "${d}/sessionek.ctx" --policy-session +tpm2 policysecret --session "${d}/sessionek.ctx" --object-context endorsement + +# Execute and satisfy the policy for the TK or WK +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" "$@" + tpm2 flushcontext --transient-object + tpm2 flushcontext --loaded-session +} + +if $use_tk; then + # attempt to load the secret wrapping key into our TPM + # as a transient object + tpm2 flushcontext --transient-object + tpm2 flushcontext --loaded-session + + info "tpm2: Importing duplicate transport key" + tpm2 import \ + --parent-context "${d}/ek.ctx" \ + --parent-auth "session:${d}/sessionek.ctx" \ + --key-algorithm rsa \ + --input "${ciphertext_file}.tk.dpriv" \ + --seed "${ciphertext_file}.tk.seed" \ + --public "${ciphertext_file}.tk.pub" \ + --private "${d}/tk.priv" \ + || die "tpm2: unable to import duplicate transport key object" + + warn "tpm2: Loading duplicate transport key" + tpm2 flushcontext --transient-object + tpm2 flushcontext --loaded-session + tpm2 startauthsession --session "${d}/sessionek.ctx" --policy-session + tpm2 policysecret --session "${d}/sessionek.ctx" --object-context endorsement + tpm2 load \ + --parent-context "${d}/ek.ctx" \ + --auth "session:${d}/sessionek.ctx" \ + --key-context "${d}/tk.ctx" \ + --public "${ciphertext_file}.tk.pub" \ + --private "${d}/tk.priv" \ + || die "tpm2: unable to load duplicate transport key object" + + warn "tpm2: Decrypting with TK" + if (($# > 0)); then + auth "$@" + tpm2 rsadecrypt \ + --auth "session:${d}/session.ctx" \ + --key-context "${d}/tk.ctx" \ + --output "${out_file}" \ + "${ciphertext_file}" \ + || die "tpm2: unable to decrypt with transport key" + else + tpm2 rsadecrypt \ + --key-context "${d}/tk.ctx" \ + --output "${out_file}" \ + "${ciphertext_file}" \ + || die "tpm2: unable to decrypt with transport key" + fi +else + # Load the WK for use as the activation object for + # TPM2_ActivateCredential(): + tpm2 flushcontext --transient-object 1>&2 + tpm2 flushcontext --loaded-session 1>&2 + wkpriv > "${d}/wkpriv.pem" + attrs='sign' + adminwithpolicy= + if (($# > 0)); then + attrs='adminwithpolicy|sign' + adminwithpolicy=true + fi + if tpm2 loadexternal \ + --hierarchy n \ + --key-algorithm ecc \ + --private "${d}/wkpriv.pem" \ + ${adminwithpolicy:+--policy "${d}/policy"} \ + --attributes "$attrs" \ + --key-context "${d}/wk.ctx" 1>&2; then + true + else + stat=$? + echo "ERROR: Failed to load WK: $?" 1>&2 + exit $stat + fi + + # If a policy was given to execute, create a policy session and execute + # and satisfy the policy: + activatecredential_args=() + if (($# > 0)); then + activatecredential_args+=(--credentialedkey-auth session:"${d}/session.ctx") + auth "$@" + fi + + # Finally, ActivateCredential + tpm2 activatecredential \ + --credentialedkey-context "${d}/wk.ctx" \ + "${activatecredential_args[@]}" \ + --credentialkey-context "${d}/ek.ctx" \ + --credentialkey-auth session:"${d}/sessionek.ctx" \ + --credential-blob "$ciphertext_file" \ + --certinfo-data "$out_file" > /dev/null \ + || die "could not decrypt using TPM2_ActivateCredential()" +fi diff --git a/Enrollment/tpm-receive.sh b/Enrollment/tpm-receive.sh deleted file mode 100755 index 32593f5..0000000 --- a/Enrollment/tpm-receive.sh +++ /dev/null @@ -1,194 +0,0 @@ -#!/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"