diff --git a/sbin/attest-enroll b/sbin/attest-enroll index 6c167df7..7feb142b 100755 --- a/sbin/attest-enroll +++ b/sbin/attest-enroll @@ -9,30 +9,347 @@ # - Client Key (CK): a host certificate for this system # - Other data -BASEDIR="$(dirname $0)/.." -OUTPREFIX="$BASEDIR/build/attest" -die() { echo >&2 "$@" ; exit 1 ; } +set -euo pipefail +shopt -s extglob + +PROG=${0##*/} +if [[ $0 = /* ]]; then + BASEDIR=${0%/*} +elif [[ $0 = */* ]]; then + BASEDIR=$PWD/${0%/*} +else + BASEDIR=$PWD +fi +DBDIR="$BASEDIR/build/attest" +CONF=/etc/safeboot-enroll.conf +EKPUB=/dev/stdin +ESCROW_PUBS= +POLICY= +TRANSPORT_METHOD=EK +GENPROGS=(genhostname genfskey) +# FIXME ESCROW_PUBS should be an array of key files, not a directory name... +# FIXME Policies should be handled more intelligently. It'd be nice to have +# names for them! + +. "$BASEDIR/../functions.sh" + +die() { echo >&2 "Error: $PROG" "$@" ; exit 1 ; } warn() { echo >&2 "$@" ; } -tpm2() { "$BASEDIR/bin/tpm2" "$@" ; } +genhostname() { + echo "$hostname" > "${1}/hostname-${hostname:0:80}" + echo "public hostname-${hostname:0:80}" +} + +genfskey() { + info "Creating a secret filesystem key for enrolled system" + openssl rand 64 > "${1}/rootfs.key" \ + || die "$0: unable to create disk encryption key" + echo "sensitive rootfs.key" +} + +gentest0() { + dd if=/dev/urandom of="${1}/test0" bs=512 count=1 2>/dev/null + sha256 < "${1}/test0" > "${1}/test0pub" + echo "sensitive test0 test0pub" +} + +gentest1() { + local suffix=$RANDOM + echo test4$suffix > "${1}/test4-$suffix" + echo test4pub > "${1}/test4pub-$suffix" + echo "public test4-$suffix test4pub-$suffix" +} + +gentest2() { + echo "skip" +} + +usage() { + ((${1:-1} == 0)) || exec 1>&2 + cat <.pub" and contain an + escrow TPM's EKpub, or ".pem" and contain an RSA + key for PKCS encryption. + -G GENPROG A program to generate a secret and/or metadata (details below). + May be given multiple times. Hostname (public) and filesystem + keys (secret) are always generated first. + -O DIR Output directory (default: $DBDIR) + -P POLICY A policyDigest or a script that outputs one. The TPM to which + the enrolled EKpub belongs _will_ enforce the policy when + decrypting secrets for it. + (default: $POLICY) + -T METHOD Secrets transport method. MEDTHOD is one of: + - TK (uses a long-lived RSA transport key whose primary key is + TPM2_Duplicate()ed to the enrolled EKpub) + - EK (uses TPM2_MakeCredential() to encrypt to the enrolled + EKpub) + (default: $TRANSPORT_METHOD) + + CONF is a bash script that should set/clear one or more of: + + DBDIR ESCROW_PUBS EKPUB POLICY ESCROW_POLICY TRANSPORT_METHOD + GENPROGS (bash array) + + GENPROGS will be called in the order they are given, with the following + arguments: + + TMP-OUTPUT-DIR ENROLL-DIR HOSTNAME + + and are expected to a) create a file in OUTPUT-DIR, b) output: + + sensitive NAME1 [NAME2 ..]. + + or + + public NAME1 [NAME2 ..]. + + or + + skip [REASON] + + E.g., + + sensitive SOMEPRIVKEY + + sensitive SOMEPRIVKEY.PEM SOMEPUBKEY.PEM SOMECERT.PEM + + public SOME-META-DATA-HERE + + skip NOT NEEDED + + The NAMEs are of files in the TMP-OUTPUT-DIR. Any sensitive NAME1 file will + be encrypted and escrowed; all other files will be copied to the enrollment + area for the given EKpub. +EOF + exit ${1:-1} +} + +add=false +debug=false +replace=false +VERBOSE=0 +while getopts +:C:E:G:I:O:P:T:adhrvx opt; do +case "$opt" in +C) CONF=$OPTARG;; +E) ESCROW_PUBS=$OPTARG;; +G) GENPROGS+=($OPTARG);; +I) EKPUB=$OPTARG;; +O) DBDIR=$OPTARG;; +P) POLICY=$OPTARG;; +T) TRANSPORT_METHOD=$OPTARG;; +a) add=true;; +d) debug=true;; +h) usage 0;; +r) replace=true;; +v) ((VERBOSE++)) || true;; +x) set -vx;; +*) usage;; +esac +done +shift $((OPTIND - 1)) +(($# == 1)) || usage hostname="$1" +shift +[[ -n ${CONF} && -f ${CONF} ]] && . "${CONF}" +PROG=${0##*/} + +case "$TRANSPORT_METHOD" in +TK|EK) true;; +*) die "TRANSPORT_METHOD must be either 'TK' or 'EK'";; +esac +[[ -z $ESCROW_PUBS || -d $ESCROW_PUBS ]] \ +|| die "-E DIR -- must be a directory or not given" + +tmp= +cleanup() { + set +euo pipefail; + if ((VERBOSE > 1)); then + ( + [[ -n $outdir && -d $outdir ]] && sha256sum "${outdir}/"* + sha256sum "$tmp"/* + ) | sort + fi + + if $debug; then + echo "LEAVING TEMP DIR ALONE: $tmp" 1>&2 + exit 0; + fi + [[ -n $tmp ]] && rm -rf "$tmp"; +} tmp="$(mktemp -d)" -cleanup() { rm -rf "$tmp" ; } trap cleanup EXIT +info() { + ((VERBOSE)) && echo info: "$@" 1>&2 || true +} -cat - > "$tmp/ek.pub" \ -|| die "$0: unable to read ek.pub from stdin" +# escrow SRC-FILE-NAME [DST-FILE-NAME] +escrow() { + local src="$1" + local dst="${2:-$1}" + local aname k + + [[ -n $ESCROW_PUBS && -d $ESCROW_PUBS ]] && + for k in "${ESCROW_PUBS}"/*.pem "${ESCROW_PUBS}"/*.pub; do + aname=${k##*/} + [[ $aname = [*].??? ]] && continue + if [[ $aname = tpm-* ]]; then + info "Escrowing secret ${src} to TPM $k" + send-to-tpm.sh -f -P "${ESCROW_POLICY:-}" \ + "$k" "${tmp}/${src}" \ + "${outdir}/escrow-${aname}-${dst}" \ + || die "$0: unable to escrow secret with EK" + else + info "Escrowing secret $src as $dst to bare RSA pubkey $k" + openssl rsautl \ + -encrypt \ + -pubin \ + -inkey "$k" \ + -in "${tmp}/${src}" \ + -out "${outdir}/escrow-${aname}-${dst}" \ + || die "$0: unable to escrow secret with bare public key" + fi + done +} + +genTK() { + if [[ $TRANSPORT_METHOD != TK ]]; then + echo "skip not needed" + return 0 + fi + if [[ -s $outdir/tk.pem ]]; then + echo "skip already exists" + return 0 + fi + rm -f $outdir/tk.pem + info "Making a TK" + # + # Generate a device specific RSA key and create a TPM2 duplicate structure + # so that only the destination device can use it with their TPM + # + openssl genrsa -out "$tmp/tk-priv.pem" \ + || die "$0: unable to create TK private key" + + openssl rsa \ + -pubout \ + -in "$tmp/tk-priv.pem" \ + -out "$outdir/tk.pem" \ + || die "$0: unable to create TK public key" + + # HACK: hard code the policy that PCR11 == 0 for the TK + echo 'fd32fa22c52cfc8e1a0c29eb38519f87084cab0b04b0d8f020a4d38b2f4e223e' \ + | xxd -p -r > "$tmp/policy.dat" \ + || die "$0: unable to create TK policy" + info "Exporting TK to enrolled client's TPM" + tpm2 duplicate \ + --tcti none \ + -U "$outdir/ek.pub" \ + -G rsa \ + -L "$tmp/policy.dat" \ + -k "$tmp/tk-priv.pem" \ + -u "$outdir/tk.pub" \ + -r "$outdir/tk.dpriv" \ + -s "$outdir/tk.seed" \ + || die "$0: unable to duplicate TK into TPM for EK" + + info "Escrowing TK" + escrow "TK" "${tmp}/tk-priv.pem" + echo "skip" +} + +# Encrypt $1 and place the resulting ciphertext in ${2}.symkeyenc and ${2}.enc. +# +# Creates a symmetric key in ${1}-symkey that the caller may escrow and must +# remove. +# +# ${2}.enc is encrypted in the ${1}-symkey using AES-128 keyed with the +# randomly generated key (${1}-symkey), and MAC'ed with HMAC-SHA256. See +# functions.sh:aead_encrypt() for details. +# +# ${2}.symkeyenc is encrypted using the specified $TRANSPORT_METHOD. If that's +# "TK" then the enrolled TKpub is used to encrypt ${1}-symkey. If the +# $TRANSPORT_METHOD is "EK" then TPM2_MakeCredential() is used to encrypt +# ${1}-symkey to the enrolled EKpub. Either way the requested $POLICY will be +# enforced by the TPM to which the enrolled EKpub belongs. +encrypt_util() { + local bytes=$((${AES_KEY_SIZE:-128} / 8)) + local symkey="${1}-symkey" + + dd if=/dev/urandom of="$symkey" bs=$bytes count=1 2>/dev/null + aead_encrypt "$1" "$symkey" "${2}.enc" + + case "$TRANSPORT_METHOD" in + TK) # encrypt the disk encryption key with the seed key so + # that only the destination machine can decrypt it using + # a TPM duplicate key + info "Encrypting secret $1 to TK" + openssl rsautl \ + -encrypt \ + -pubin \ + -inkey "$outdir/tk.pem" \ + -in "$symkey" \ + -out "${2}.symkeyenc" \ + || die "$0: unable to encrypt disk key with TK" + ;; + EK) # Encrypt to the TPM using TPM2_MakeCredential(EKpub, WKname) + info "Encrypting secret $1 to TPM" + send-to-tpm.sh -f -P "$POLICY" "$EKPUB" "$symkey" "${2}.symkeyenc" + ;; + esac +} + +# Encrypt $1 +encrypt() { + [[ -f $outdir/${1}.symkeyenc && -f $outdir/${1}.enc ]] \ + && return 0 + + # Encrypt and escrow + encrypt_util "${tmp}/$1" "${outdir}/${1}" + escrow "${1}-symkey" "${1}.symkeyenc" + shift + + # Copy any remaining files + while (($# > 0)); do + [[ -f ${outdir}/$1 ]] \ + && mv -f "${outdir}/$1" "${outdir}/${1}-" + cp -f "${tmp}/$1" "${outdir}/${1}" + shift + done +} + +cat "$EKPUB" > "$tmp/ek.pub" \ +|| die "$0: unable to read ek.pub from stdin" +exec "$tmp/ek.txt" \ || die "$0: unable to parse EK" grep -q "value: fixedtpm.*sensitivedataorigin.*restricted" "$tmp/ek.txt" \ @@ -41,71 +358,76 @@ grep -q "value: fixedtpm.*sensitivedataorigin.*restricted" "$tmp/ek.txt" \ grep -q "authorization policy: 837197..." "$tmp/ek.txt" \ || warn "$0: EK has wrong authorization policy, attestation will likely fail" - # # Figure out where to put this enrolled key # ekhash="$(sha256sum $tmp/ek.pub | cut -f1 -d' ' )" -ekprefix="$(echo $ekhash | cut -c1-2)" +ekprefix=${ekhash:0:2} -if [ -z "$hostname" ] ; then +if [[ -z "$hostname" ]] ; then hostname="$(echo $ekhash | cut -c1-8)" - warn "$0: using default hostname $hostname" + warn "$PROG: using default hostname $hostname" fi # Create the output directory and install files into it -outdir="$OUTPREFIX/$ekprefix/$ekhash" -mkdir -p "$outdir" || die "$outdir: unable to create output directory" +outdir="$DBDIR/$ekprefix/$ekhash" +mkdir -p "$DBDIR/$ekprefix/" || die "unable to mkdir $DBDIR/$ekprefix/" +if [[ -d $outdir ]] && $replace; then + [[ -d "${outdir}-" ]] && rm -rf "${outdir}-" + # XXX Cleanup "$DBDIR/hostname2ekpub/*" for the old enrollment's + # hostnames. + mv "$outdir" "${outdir}-" || die "could not rename previous enrollment" +elif [[ -d $outdir ]] && ! $add; then + die "already enrolled: $ekhash" +fi +if $add; then + mkdir -p "$outdir" || die "unable to create output directory $outdir" +else + mkdir "$outdir" || die "unable to create output directory $outdir" +fi cp "$tmp/ek.pub" "$outdir/ek.pub" \ -|| die "$0: unable to copy EK public key to output directory $outdir" - -# -# Generate a device specific RSA key and create a TPM2 duplicate structure -# so that only the destination device can use it with their TPM -# -openssl genrsa -out "$tmp/tk-priv.pem" \ -|| die "$0: unable to create TK private key" - -openssl rsa \ - -pubout \ - -in "$tmp/tk-priv.pem" \ - -out "$outdir/tk.pem" \ -|| die "$0: unable to create TK public key" - -# HACK: hard code the policy that PCR11 == 0 for the TK -echo 'fd32fa22c52cfc8e1a0c29eb38519f87084cab0b04b0d8f020a4d38b2f4e223e' \ -| xxd -p -r > "$tmp/policy.dat" \ -|| die "$0: unable to create TK policy" - -tpm2 duplicate \ - --tcti none \ - -U "$outdir/ek.pub" \ - -G rsa \ - -L "$tmp/policy.dat" \ - -k "$tmp/tk-priv.pem" \ - -u "$outdir/tk.pub" \ - -r "$outdir/tk.dpriv" \ - -s "$outdir/tk.seed" \ -|| die "$0: unable to duplicate TK into TPM for EK" - -# -# TODO: escrow the TK -# - -# encrypt the disk encryption key with the seed key so that only the destination -# machine can decrypt it using a TPM duplicate key -openssl rand 64 > "$tmp/rootfs.key" \ -|| die "$0: unable to create disk encryption key" +|| die "unable to copy EK public key to output directory $outdir" -openssl rsautl \ - -encrypt \ - -pubin \ - -inkey "$outdir/tk.pem" \ - -in "$tmp/rootfs.key" \ - -out "$outdir/rootfs.enc" \ -|| die "$0: unable to encrypt disk key with TK" +info "Generating secrets and metadata" +for i in genTK "${GENPROGS[@]}"; do + info "Running GENPROG $i" + set -- $("$i" "$tmp" "$tmp/ek.pub" "$hostname") + if (($# > 0)) && [[ $1 = skip ]]; then + shift + warn "GENPROG $i skipped${1:+": "}$*" + continue + fi + if (($# < 2)) || + [[ $1 != @(sensitive|public) || + ! -f $tmp/$2 ]]; then + warn "GENPROG $i output is unexpected: $*; skipping" + continue + fi + kind=$1 + shift + if [[ $kind = sensitive ]]; then + # Encrypt file, escrow, and place into output dir. + info "Encrypting secret $*" + encrypt "$@" + else + # Public file(s) + if [[ -f $outdir/$1 ]]; then + warn "Skipping GENPROG $i (already enrolled)" + continue + fi + info "Installing metadata file(s): $*" + while (($# > 0)); do + cp -f "${tmp}/$1" "${outdir}/$1" + shift + done + fi + set -- +done +# Update hostname "index" +mkdir -p "$DBDIR/hostname2ekpub" +echo "$ekhash" > "$DBDIR/hostname2ekpub/$hostname" # # Build the cloud-init data for this host diff --git a/tests/test-enroll.sh b/tests/test-enroll.sh new file mode 100755 index 00000000..72b1c6bf --- /dev/null +++ b/tests/test-enroll.sh @@ -0,0 +1,113 @@ +#!/bin/bash + +set -euo pipefail + +if [[ $0 = /* ]]; then + TOP=${0%/*} +elif [[ $0 = */* ]]; then + TOP=$PWD/${0%/*} +else + TOP=$PWD +fi + +TOP=${TOP%/*} + +. $TOP/functions.sh + +#PATH=$TOP/sbin:$TOP/swtpm/src/swtpm:$PATH + +d= +swtpmpids=() +cleanup() { + for pid in "${swtpmpids[@]}"; do + kill -9 $pid + done + [[ -n $d ]] && rm -rf "$d" +} +trap cleanup EXIT +d=$(mktemp -d) +cd "$d" +mkdir db +mkdir escrowpubs + +declare -A TCTIs +start_port=9880 +start_swtpm() { + local port=$start_port + local cport=$((start_port + 1)) + + mkdir "${d}/tpm$port" + swtpm socket --tpm2 \ + --tpmstate dir=${d}/tpm$port \ + --server type=tcp,bindaddr=0.0.0.0,port=$port \ + --ctrl type=tcp,bindaddr=0.0.0.0,port=$cport \ + --flags startup-clear & + swtpmpids+=($!) + TCTIs[$1]="swtpm:host=localhost,port=$port" + ((start_port += 2)) +} + +make_escrow() { + start_swtpm "$1" + TPM2TOOLS_TCTI="${TCTIs[$1]}" \ + tpm2 createek --ek-context /dev/null \ + --public "${d}/escrowpubs/tpm-${1}.pub" +} + +make_client() { + mkdir "${d}/$1" + start_swtpm "$1" + TPM2TOOLS_TCTI="${TCTIs[$1]}" \ + tpm2 createek --ek-context /dev/null \ + --public "${d}/${1}/ek.pub" + attest-enroll -O "${d}/db" \ + -E "${d}/escrowpubs" \ + -G gentest0 \ + -I "${d}/${1}/ek.pub" \ + "$1" + + local ekpub=$(cat "${d}/db/hostname2ekpub/$1") + local dir="${d}/db/${ekpub:0:2}/${ekpub}" + + # Check that the client can recover the secret + TPM2TOOLS_TCTI="${TCTIs[$1]}" \ + tpm-receive.sh "${dir}/test0.symkeyenc" \ + "${d}/symkey" + aead_decrypt "${dir}/test0.enc" \ + "${d}/symkey" \ + "${d}/pt" + sha256 < "${d}/pt" > "${d}/digest" + cmp "${d}/db/${ekpub:0:2}/${ekpub}/test0pub" "${d}/digest" + rm -f "${d}/symkey" "${d}/pt" "${d}/digest" + + # Check that we can recover the secret + TPM2TOOLS_TCTI="${TCTIs[BreakGlass]}" \ + tpm-receive.sh "${dir}/escrow-tpm-BreakGlass.pub-test0.symkeyenc" \ + "${d}/symkey" + aead_decrypt "${dir}/test0.enc" \ + "${d}/symkey" \ + "${d}/pt" + sha256 < "${d}/pt" > "${d}/digest" + cmp "${d}/db/${ekpub:0:2}/${ekpub}/test0pub" "${d}/digest" + rm -f "${d}/symkey" "${d}/pt" "${d}/digest" + (cd $dir && find . -type f|sort) +} + +make_escrow BreakGlass +for i in foo bar baz; do + make_client $i +done +for i in foo bar baz; do + ekpub=$(cat "${d}/db/hostname2ekpub/$i") + dir="${d}/db/${ekpub:0:2}/${ekpub}" + for k in foo bar baz; do + [[ $i = $k ]] && continue + echo "Checking that $i can't read ${k}'s secrets" + rm -f "${d}/symkey" + TPM2TOOLS_TCTI="${TCTIs[$k]}" \ + tpm-receive.sh "${dir}/test0.symkeyenc" \ + "${d}/symkey" 2>/dev/null \ + && die "Whoops! $i _can_ read ${k}'s secrets!!" + rm -f "${d}/symkey" + done +done