#!/usr/bin/env bash # # sample/swtpm-localca # # Authors: Stefan Berger # # (c) Copyright IBM Corporation 2014, 2015. # # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # Neither the names of the IBM Corporation nor the names of its contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # some flags SETUP_TPM2_F=1 # for TPM 2 EK ALLOW_SIGNING_F=2 DECRYPTION_F=4 LOCALCA_OPTIONS="swtpm-localca.options" if [ -n "$XDG_CONFIG_HOME" ] && [ -r "$XDG_CONFIG_HOME/$LOCALCA_OPTIONS" ]; then LOCALCA_OPTIONS="$XDG_CONFIG_HOME/$LOCALCA_OPTIONS" elif [ -n "$HOME" ] && [ -r "$HOME/.config/$LOCALCA_OPTIONS" ]; then LOCALCA_OPTIONS="$HOME/.config/$LOCALCA_OPTIONS" else LOCALCA_OPTIONS="@SYSCONFDIR@/$LOCALCA_OPTIONS" fi LOCALCA_CONFIG="swtpm-localca.conf" if [ -n "$XDG_CONFIG_HOME" ] && [ -r "$XDG_CONFIG_HOME/$LOCALCA_CONFIG" ]; then LOCALCA_CONFIG="$XDG_CONFIG_HOME/$LOCALCA_CONFIG" elif [ -n "$HOME" ] && [ -r "$HOME/.config/$LOCALCA_CONFIG" ]; then LOCALCA_CONFIG="$HOME/.config/$LOCALCA_CONFIG" else LOCALCA_CONFIG="@SYSCONFDIR@/$LOCALCA_CONFIG" fi # Default logging goes to stderr LOGFILE="" UNAME_S=$(uname -s) logit() { if [ -z "$LOGFILE" ]; then echo "$@" >&1 else echo "$@" >> "$LOGFILE" fi } logerr() { if [ -z "$LOGFILE" ]; then echo "Error: $*" >&2 else echo "Error: $*" >> "$LOGFILE" fi } flock_fd() { local fd=$1 case "${UNAME_S}" in Darwin) flock "$fd";; *) flock -x "$fd";; esac } # Escape a string for call with eval. Replace any occurrences of '$(' with # '(' to avoid subshells and escape '][;|&()'`'<>!\#*' since those are shell # special characters. Unescape any already-escaped characters and then # escape again. Do NOT escape '${' since we use it for variable substitution # but escape all other $-sequences. Squeeze all sequences of multiple '\'. escape_eval() { echo "$1" | \ sed -e 's/\$(/(/g' \ -e 's/\\\([][;|&()`<>!#*]\)/\1/g' -e 's/\([][;|&()`<>!#*]\)/\\\1/g' \ -e "s/\\\'/'/g" -e "s/'/\\\'/g" \ -e 's/$\([^{]\)/\\$\\\1/g' | \ tr -s '\\' } # Get a configuration value from a configuration file # @param1: The file with the options # @param2: The name of the option # @param3: The default value get_config_value() { local configfile="$1" local configname="$(echo "$2" | sed 's/-/\\-/g')" local defaultvalue="$3" local tmp if [ ! -r "$configfile" ]; then logerr "Cannot read config file $configfile" return 1 fi tmp=$(sed -n "s/^${configname}[[:space:]]*=[[:space:]]*//p" \ "$configfile") tmp=${tmp%% } if [ -z "$tmp" ]; then if [ -n "$defaultvalue" ]; then echo "$defaultvalue" else return 1 fi else # only use eval if there are any ${ that are not \${ # or the command starts with '${' if [ "$tmp" != "${tmp%[^\\]\${*}" ] || \ [ "${tmp:0:2}" = '${' ]; then tmp="$(escape_eval "$tmp")" echo "$(eval echo "$tmp")" else # unescape any previously required '\;' echo "$tmp" | sed -e 's/\\;/;/g' fi fi return 0 } make_dir() { local dir="$1" if [ ! -d "$dir" ]; then logit "Creating swtpm-local dir '${dir}'." mkdir -p "$dir" if [ $? -ne 0 ]; then logerr "Could not create directory '${dir}." exit 1 fi fi } # Get the next serial number for the certificate # # If an error occurs nothing is echo'ed and the return code 1 is returned, # otherwise the next serial number is echo'ed with a return code of 0. get_next_cert_serial() { local serial touch "${LOCK}" ( # Avoid concurrent creation of next serial flock_fd 100 if [ $? -ne 0 ]; then logerr "Could not get lock ${LOCK}" return 1 fi if [ ! -r "${CERTSERIAL}" ]; then echo -n "0" > "${CERTSERIAL}" fi serial=$(cat "${CERTSERIAL}") if ! [[ "$serial" =~ ^[0-9]+$ ]]; then serial=1 else serial=$((serial+1)) fi echo -n $serial > "${CERTSERIAL}" if [ $? -ne 0 ]; then logerr "Could not write cert serial number file" return 1 fi echo $serial ) 100>"${LOCK}" return 0 } create_cert() { local flags="$1" local typ="$2" local dir="$3" local ek="$4" local vmid="$5" local tpm_spec_params="$6" local tpm_attr_params="$7" local options="" rc=0 keyparms="" serial tmp subj serial=$(get_next_cert_serial) if [ -z "$serial" ]; then return 1 fi if [ -r "${LOCALCA_OPTIONS}" ]; then options=$(cat "${LOCALCA_OPTIONS}") fi if [ -n "$vmid" ]; then subj="CN=$vmid" else subj="CN=unknown" fi if [ $((flags & SETUP_TPM2_F)) -ne 0 ]; then options="$options --tpm2" else # TPM 1.2 cert needs a header options="$options --add-header" fi if [ "$typ" == "ek" ]; then if [ $((flags & ALLOW_SIGNING_F)) -ne 0 ]; then options="$options --allow-signing" fi if [ $((flags & DECRYPTION_F)) -ne 0 ]; then options="$options --decryption" fi fi # if ek contains x=..,y=... it's an ECC key if [[ "$ek" =~ x=.*,y=.* ]]; then keyparms="--ecc-x $(echo "$ek" | \ sed -n 's/x=\([[:xdigit:]]*\),.*/\1/p') " keyparms+="--ecc-y $(echo "$ek" | \ sed -n 's/.*y=\([[:xdigit:]]*\).*/\1/p')" else keyparms="--modulus ${ek}" fi case "$typ" in ek) if [ -z "$(type -p swtpm_cert)" ]; then logerr "Missing swtpm_cert tool" rc=1 else swtpm_cert \ --subject "$subj" \ $options \ ${SIGNKEY_PASSWORD:+--signkey-pwd file:<(echo -en "$SIGNKEY_PASSWORD")} \ ${PARENTKEY_PASSWORD:+--parentkey-pwd file:<(echo -en "$PARENTKEY_PASSWORD")} \ $tpm_spec_params \ $tpm_attr_params \ --signkey "${SIGNKEY}" \ --issuercert "${ISSUERCERT}" \ --out-cert "${dir}/ek.cert" \ $keyparms \ --days 3650 \ --serial "$serial" if [ $? -eq 0 ]; then logit "Successfully created EK certificate locally." else logerr "Could not create EK certificate locally." rc=1 fi fi ;; platform) if [ -z "$(type -p swtpm_cert)" ]; then logerr "Missing swtpm_cert tool" rc=1 else swtpm_cert \ --subject "$subj" \ $options \ ${SIGNKEY_PASSWORD:+--signkey-pwd file:<(echo -en "$SIGNKEY_PASSWORD")} \ ${PARENTKEY_PASSWORD:+--parentkey-pwd file:<(echo -en "$PARENTKEY_PASSWORD")} \ $tpm_attr_params \ --type platform \ --signkey "${SIGNKEY}" \ --issuercert "${ISSUERCERT}" \ --out-cert "${dir}/platform.cert" \ $keyparms \ --days 3650 \ --serial "$serial" if [ $? -eq 0 ]; then logit "Successfully created platform certificate locally." else logerr "Could not create platform certificate locally." rc=1 fi fi ;; esac return $rc } # Create the local CA's certificate if it doesn't already exist. # The local CA will be an intermediate CA with a root CA we create # here as well so that we get an Authority Key Id in our EK cert. # create_localca_cert() { touch "${LOCK}" ( # Avoid concurrent creation of keys and certs flock_fd 100 if [ $? -ne 0 ]; then logerr "Could not get lock ${LOCK}" return 1 fi if [ ! -d "${STATEDIR}" ]; then # RPM installation must have created this already ... # so user tss can use it (user tss cannot create it) mkdir -p "${STATEDIR}" fi if [ ! -r "${SIGNKEY}" ]; then local dir=$(dirname "${SIGNKEY}") local cakey=${dir}/swtpm-localca-rootca-privkey.pem local cacert=${dir}/swtpm-localca-rootca-cert.pem local msg password # create a CA first msg=$("${CERTTOOL}" \ --generate-privkey \ ${SWTPM_ROOTCA_PASSWORD:+--password "${SWTPM_ROOTCA_PASSWORD}"} \ --outfile "${cakey}" \ 2>&1) [ $? -ne 0 ] && { logerr "Could not create root-CA key ${cakey}." logerr "${msg}" return 1 } chmod 640 "${cakey}" local tmp=$(mktemp) echo "cn=swtpm-localca-rootca" > "${tmp}" echo "ca" >> "${tmp}" echo "cert_signing_key" >> "${tmp}" echo "expiration_days = 3650" >> "${tmp}" msg=$(GNUTLS_PIN="${SWTPM_ROOTCA_PASSWORD}" "${CERTTOOL}" \ --generate-self-signed \ --template "${tmp}" \ --outfile "${cacert}" \ --load-privkey "${cakey}" \ 2>&1) [ $? -ne 0 ] && { logerr "Could not create root CA." logerr "${msg}" rm -f "${cakey}" return 1 } # now our signing CA msg=$("${CERTTOOL}" \ --generate-privkey \ ${SIGNKEY_PASSWORD:+--password "${SIGNKEY_PASSWORD}"} \ --outfile "${SIGNKEY}" \ 2>&1) [ $? -ne 0 ] && { rm -f "${cakey}" "${cacert}" logerr "Could not create local-CA key ${SIGNKEY}." logerr "${msg}" return 1 } chmod 640 "${SIGNKEY}" echo "cn=swtpm-localca" > "${tmp}" echo "ca" >> "${tmp}" echo "cert_signing_key" >> "${tmp}" echo "expiration_days = 3650" >> "${tmp}" if [ -n "${SIGNKEY_PASSWORD}" ] && [ -n "${SWTPM_ROOTCA_PASSWORD}" ]; then GNUTLS_PIN="${SIGNKEY_PASSWORD}" password="${SWTPM_ROOTCA_PASSWORD}" elif [ -n "${SIGNKEY_PASSWORD}" ]; then GNUTLS_PIN="${SIGNKEY_PASSWORD}" else GNUTLS_PIN="${SWTPM_ROOTCA_PASSWORD}" fi msg=$(GNUTLS_PIN="${GNUTLS_PIN}" "${CERTTOOL}" \ --generate-certificate \ --template "${tmp}" \ --outfile "${ISSUERCERT}" \ --load-privkey "${SIGNKEY}" \ ${password:+--password "${password}"} \ --load-ca-privkey "${cakey}" \ --load-ca-certificate "${cacert}" \ 2>&1) [ $? -ne 0 ] && { rm -f "${cakey}" "${cacert}" "${SIGNKEY}" logerr "Could not create local CA." logerr "${msg}" return 1 } rm -f "${tmp}" fi ) 100>"${LOCK}" return 0 } usage() { cat <<_EOF_ Usage: $(basename "$1") [options] The following options are supported: --type type The type of certificate to create: 'ek' or 'platform' --ek key-param The modulus of an RSA key or x=...,y=,... for an EC key --dir directory The directory to write the resulting certificate into --vmid vmid The ID of the virtual machine --optsfile file A file containing options to pass to swtpm_cert --configfile file A file containing configuration parameters for directory, signing key and password and certificate to use --logfile file A file to write a log into --tpm-spec-family s The implemented spec family, e.g., '2.0' --tpm-spec-revision i The spec revision of the TPM as integer; e.g., 146 --tpm-spec-level i The spec level of the TPM; must be an integer; e.g. 00 --tpm-manufacturer s The manufacturer of the TPM; e.g., id:00001014 --tpm-model s The model of the TPM; e.g., 'swtpm' --tpm-version i The (firmware) version of the TPM; e.g., id:20160511 --tpm2 Generate a certificate for a TPM 2 --allow-signing The TPM 2's EK can be used for signing --decryption The TPM 2's EK can be used for decryption --help, -h, -? Display this help screen and exit The following environment variables are supported: SWTPM_ROOTCA_PASSWORD The root CA's private key password _EOF_ } main() { local typ ek dir vmid tmp local tpm_spec_params="" tpm_attr_params="" local flags=0 while [ $# -ne 0 ]; do case "$1" in --type) shift typ="$1" ;; --ek) shift ek="$1" ;; --dir) shift dir="$1" ;; --vmid) shift vmid="$1" ;; --optsfile) shift LOCALCA_OPTIONS="$1" ;; --configfile) shift LOCALCA_CONFIG="$1" ;; --logfile) shift LOGFILE="$1" ;; --tpm-spec-family) shift tpm_spec_params+="--tpm-spec-family $1 " ;; --tpm-spec-revision) shift tpm_spec_params+="--tpm-spec-revision $1 " ;; --tpm-spec-level) shift tpm_spec_params+="--tpm-spec-level $1 " ;; --tpm-manufacturer) shift tpm_attr_params+="--tpm-manufacturer $1 " ;; --tpm-model) shift tpm_attr_params+="--tpm-model $1 " ;; --tpm-version) # this is the firmware version! shift tpm_attr_params+="--tpm-version $1 " ;; --tpm2) flags=$((flags | SETUP_TPM2_F)) ;; --allow-signing) flags=$((flags | ALLOW_SIGNING_F)) ;; --decryption) flags=$((flags | DECRYPTION_F)) ;; --help|-h|-?) usage "$0" exit 0 ;; *) logerr "Unsupported option $1" exit 1 ;; esac shift done if [ -n "$LOGFILE" ]; then touch "$LOGFILE" &>/dev/null if [ ! -w "$LOGFILE" ]; then logerr "Cannot write to logfile ${LOGFILE}." exit 1 fi fi if [ ! -r "$LOCALCA_OPTIONS" ]; then logerr "Cannot access options file ${LOCALCA_OPTIONS}." exit 1 fi if [ ! -r "$LOCALCA_CONFIG" ]; then logerr "Cannot access config file ${LOCALCA_CONFIG}." exit 1 fi tmp=$(get_config_value "$LOCALCA_CONFIG" "statedir") if [ -z "$tmp" ]; then logerr "Missing 'statedir' config value in config file ${LOCALCA_CONFIG}" exit 1 fi STATEDIR="$tmp" make_dir "$STATEDIR" LOCK="${STATEDIR}/.lock.swtpm-localca" if [ ! -w "${LOCK}" ]; then touch "$LOCK" if [ ! -w "${LOCK}" ]; then logerr "Could not create lock file ${LOCK}." exit 1 fi fi SIGNKEY=$(get_config_value "$LOCALCA_CONFIG" "signingkey") if [ -z "$SIGNKEY" ]; then logerr "Missing signingkey variable in config file $LOCALCA_CONFIG." exit 1 fi # SIGNKEY may be a GNUTLS url like tpmkey:file= or tpmkey:uuid= if ! [[ "${SIGNKEY}" =~ ^tpmkey:(file|uuid)= ]]; then make_dir "$(dirname "$SIGNKEY")" fi SIGNKEY_PASSWORD=$(get_config_value "$LOCALCA_CONFIG" "signingkey_password") PARENTKEY_PASSWORD=$(get_config_value "$LOCALCA_CONFIG" "parentkey_password") ISSUERCERT=$(get_config_value "$LOCALCA_CONFIG" "issuercert") if [ -z "$ISSUERCERT" ]; then logerr "Missing issuercert variable in config file $LOCALCA_CONFIG." exit 1 fi make_dir "$(dirname "$ISSUERCERT")" # set global CERTTOOL to gnutls's certtool case "${UNAME_S}" in Darwin) CERTTOOL="gnutls-certtool";; *) CERTTOOL="certtool";; esac # TPM keys are GNUTLS URLs... if [[ "$SIGNKEY" =~ ^tpmkey:(uuid|file)= ]]; then export TSS_TCSD_HOSTNAME=$(get_config_value "$LOCALCA_CONFIG" \ "TSS_TCSD_HOSTNAME" "localhost") export TSS_TCSD_PORT=$(get_config_value "$LOCALCA_CONFIG" \ "TSS_TCSD_PORT" "30003") logit "CA uses a GnuTLS TPM key; using TSS_TCSD_HOSTNAME=${TSS_TCSD_HOSTNAME}" \ "TSS_TCSD_PORT=${TSS_TCSD_PORT}" elif [[ "$SIGNKEY" =~ ^pkcs11: ]]; then export SWTPM_PKCS11_PIN=$(get_config_value "$LOCALCA_CONFIG" \ "SWTPM_PKCS11_PIN" "swtpm-tpmca") logit "CA uses a PKCS#11 key; using SWTPM_PKCS11_PIN" else if [ ! -r "$SIGNKEY" ]; then if [ -f "$SIGNKEY" ]; then logerr "Signing key $SIGNKEY exists but cannot access" \ "it as $(id -un):$(id -gn)." exit 1 fi # Create the signing key and issuer cert since it will be missing logit "Creating root CA and a local CA's signing key and issuer cert." create_localca_cert if [ $? -ne 0 ]; then logerr "Error creating local CA's signing key and cert" exit 1 fi fi if [ ! -r "$SIGNKEY" ]; then logerr "Cannot access signing key ${SIGNKEY}." exit 1 fi fi if [ ! -r "$ISSUERCERT" ]; then logerr "Cannot access issuer certificate ${ISSUERCERT}." exit 1 fi CERTSERIAL=$(get_config_value "$LOCALCA_CONFIG" "certserial" \ "${STATEDIR}/certserial") make_dir "$(dirname "$CERTSERIAL")" create_cert "$flags" "$typ" "$dir" "$ek" "$vmid" "$tpm_spec_params" \ "$tpm_attr_params" exit $? } main "$@" # 2>&1 | tee -a /tmp/localca.log