diff --git a/sbin/attest-enroll b/sbin/attest-enroll index 6c167df7..34ca9df4 100755 --- a/sbin/attest-enroll +++ b/sbin/attest-enroll @@ -9,30 +9,375 @@ # - Client Key (CK): a host certificate for this system # - Other data -BASEDIR="$(dirname $0)/.." +set -euo pipefail +shopt -s extglob + +PROG=${0##*/} +if [[ $0 = /* ]]; then + BASEDIR=${0%/*} +elif [[ $0 = */* ]]; then + BASEDIR=$PWD/${0%/*} +else + BASEDIR=$PWD +fi OUTPREFIX="$BASEDIR/build/attest" -die() { echo >&2 "$@" ; exit 1 ; } +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... + +. "$BASEDIR/../functions.sh" + +die() { echo >&2 "Error: $PROG" "$@" ; exit 1 ; } warn() { echo >&2 "$@" ; } -tpm2() { "$BASEDIR/bin/tpm2" "$@" ; } +genhostname() { + echo "$hostname" > "${1}/hostname" + echo "public idempotent hostname" +} + +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 unique rootfs.key" +} + +gentest0() { + dd if=/dev/urandom of=${1}/test0 bs=16 count=1 2>/dev/null + echo test0pub > "${1}/test0pub" + echo "sensitive unique test0 test0pub" +} + +gentest1() { + echo "${USER:-test1}" > "${1}/test1" + echo test1pub > "${1}/test1pub" + echo "sensitive idempotent test1 test1pub" +} + +gentest2() { + echo test2 > "${1}/test2" + echo test2pub > "${1}/test2pub" + echo "public unique test2 test2pub" +} + +gentest3() { + echo test3 > "${1}/test3" + echo test3pub > "${1}/test3pub" + echo "public idempotent test3 test3pub" +} + +gentest4() { + echo test4$((RANDOM)) > "${1}/test4" + echo test4pub > "${1}/test4pub" + echo "public idempotent test4 test4pub" +} + +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 (idempotent metadata) + and filesystem keys (secret, unique) are always generated + first. + -O DIR Output directory (default: $OUTPREFIX) + -P POLICY A policyDigest or a script that outputs one + (default: $POLICY) + -T METHOD Secrets transport method. MEDTHOD is one of: + - TK + - EK + (default: TK) + + CONF is a bash script that should set/clear one or more of: + + OUTPREFIX ESCROW_PUBS EKPUB POLICY ESCROW_POLICY TRANSPORT_METHOD + GENPROGS (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|public unique|idempotent NAME1 [NAME2 ..]. + + or + + skip + + E.g., + + sensitive idempotent SOMEPRIVKEY + + or + + sensitive idempotent SOMEPRIVKEY.PEM SOMEPUBKEY.PEM SOMECERT.PEM + + 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. + + Unique means there may be no more than one instance of the given NAME. + Idempotent means that there may be more than one of the given NAME, and that + the copied names will be suffixed with a date and hash of content of NAME1 + (ciphertext, if sensitive). +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) OUTPREFIX=$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 +} -cat - > "$tmp/ek.pub" \ -|| die "$0: unable to read ek.pub from stdin" +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 -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 +} + +make_tk() { + if [[ $TRANSPORT_METHOD != TK || -s $outdir/tk.pem ]]; then + echo "skip" + 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" "unique" "${tmp}/tk-priv.pem" + echo "skip" +} + +# Encrypt $1 and place ciphertext in $2 +encrypt_util() { + # XXX Use AEAD for large inputs + # XXX This will require we make two output files: one for the symmetric + # key, one for the large ciphertext + # + # Or maybe do this in secrets gen progs. + 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 "$1" \ + -out "$2" \ + || 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 -P "$POLICY" "$EKPUB" "$1" "$2" + ;; + esac +} + +# Encrypt $1, replacing or not $outdir/$1.enc; copy $2 .. $n +# +# E.g., `encrypt_unique /tmp/x/foo /tmp/x/foopub` encrypts /tmp/x/foo and +# installs it as ${outdir}/foo.enc, then it copies /tmp/x/foopub to +# ${outdir}/foopub. +encrypt_unique() { + local i + + if ! $replace && [[ -f $outdir/${1}.enc ]]; then + warn "Ignoring secret $1" + return 0 + fi + [[ -f $outdir/${1}.enc ]] \ + && mv "$outdir/${1}.enc" "$outdir/${1}.enc-" + + # Encrypt + encrypt_util "${tmp}/$1" "${outdir}/${1}.enc" + + # Escrow + escrow "$1" "$1" + + # Copy remaining files + shift + for i in "$@"; do + [[ -f ${outdir}/${i} ]] \ + && mv -f "${outdir}/${i}" "${outdir}/${i}-" + cp -f "${tmp}/${i}" "${outdir}/${i}" + done +} + +# Encrypt $1, install as $outdir/$1-$suffix.enc; copy $2 .. $n, suffixed +# +# E.g., `encrypt_idempotent /tmp/x/foo /tmp/x/foopub` encrypts /tmp/x/foo, +# hashes it, and installs it as ${outdir}/foo-DATE-HASH.enc, and then it copies +# /tmp/x/foopub to ${outdir}/foopub-DATE-HASH. +encrypt_idempotent() { + local i + # Encrypt + rm -f "${tmp}/enc" + encrypt_util "${tmp}/$1" "${tmp}/enc" + + # Compute suffix + suffix="$(date +%Y-%m-%dT%H:%M:%S)-$(sha256 "${tmp}/enc")" + + # Escrow (suffixed) + escrow "$1" "${1}-${suffix}" + + # Install ciphertext (suffixed) + mv -f "${tmp}/enc" "${outdir}/${1}-${suffix}.enc" + + # Copy remaining files (suffixed) + shift + for i in "$@"; do + cp -f "${tmp}/${i}" "${outdir}/${i}-${suffix}" + 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 +386,89 @@ 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" +mkdir -p "$OUTPREFIX/$ekprefix/" || die "unable to mkdir $OUTPREFIX/$ekprefix/" +if [[ -d $outdir ]] && $replace; then + [[ -d "${outdir}-" ]] && rm -rf "${outdir}-" + 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 "${GENPROGS[@]}"; do + info "Running GENPROG $i" + set -- $("$i" "$tmp" "$tmp/ek.pub" "$hostname") + if (($# < 3)) || + [[ $1 != @(sensitive|public) || + $2 != @(unique|idempotent) || + ! -f $tmp/$3 ]]; then + warn "GENPROG $i output is unexpected: $*; skipping" + continue + fi + kind=$1 + semantics=$2 + shift 2 + if [[ $kind = sensitive ]]; then + # Encrypt file, escrow, and place into output dir. + # + # These util functions will handle unique vs idempotent + # semantics. + info "Encrypting secret $*" + encrypt_$semantics "$@" + elif [[ $semantics = unique ]]; then + # Public file(s) + if ! $replace && [[ -f $outdir/$1 ]]; then + warn "Skipping GENPROG $i (already enrolled)" + continue + fi + # Install public, unique metadata + info "Installing metadata file(s): $*" + for k in "$@"; do + cp -f "${tmp}/${k}" "${outdir}/${k}" + done + else + # Install public, non-unique metadata + suffix=$(date +%Y-%m-%dT%H:%M:%S)-$(sha256 "${tmp}/$1") + info "Installing suffixed ($suffix) metadata file(s): $*" + found=false + for m in "${outdir}/${1}-"*; do + if cmp "${tmp}/${1}" "$m" 2>/dev/null; then + found=true + break + fi + done + if ! $found; then + cp -f "${tmp}/${1}" "${outdir}/${1}-${suffix}" + shift + for k in "$@"; do + $found || cp -f "${tmp}/${k}" "${outdir}/${k}-${suffix}" + done + fi + fi + set -- +done # # Build the cloud-init data for this host