| #!/usr/bin/env bash |
| |
| # |
| # sample/swtpm-localca |
| # |
| # Authors: Stefan Berger <stefanb@us.ibm.com> |
| # |
| # (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 |
| } |
| |
| # Get an configuration value from an configurations file |
| # @param1: The file with the options |
| # @param2: The name of the option |
| 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 |
| # don't let eval execute subshells: removed '`' and |
| # convert '$(' to '(' |
| tmp=$(echo "$tmp" | sed -e 's/\$(/(/g' -e 's/`\(.*\)`/\1/g') |
| echo $(eval echo "$tmp") |
| fi |
| |
| return 0 |
| } |
| |
| # Escape the GnuTLS PKCS11 URL |
| # @param1: The string with the GnuTLS PKCS11 URL |
| escape_pkcs11_url() { |
| echo "$1" | sed 's/;/\\;/g' |
| } |
| |
| 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 serial=$(get_next_cert_serial) |
| local options="" rc=0 keyparms="" |
| |
| if [ -z "$serial" ]; then |
| return 1 |
| fi |
| |
| if [ -r "${LOCALCA_OPTIONS}" ]; then |
| options=$(cat ${LOCALCA_OPTIONS}) |
| fi |
| |
| if [ -n "${SIGNKEY_PASSWORD}" ]; then |
| options="$options --signkey-password ${SIGNKEY_PASSWORD}" |
| fi |
| |
| if [ -n "${PARENTKEY_PASSWORD}" ]; then |
| options="$options --parentkey-password ${PARENTKEY_PASSWORD}" |
| fi |
| |
| if [ -n "$vmid" ]; then |
| options="$options --subject \"CN=$vmid\"" |
| else |
| options="$options --subject \"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 |
| eval swtpm_cert \ |
| $options \ |
| $tpm_spec_params \ |
| $tpm_attr_params \ |
| --signkey "$(escape_pkcs11_url ${SIGNKEY})" \ |
| --issuercert ${ISSUERCERT} \ |
| --out-cert ${dir}/ek.cert \ |
| $keyparms \ |
| --days $((10*365)) \ |
| --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 |
| eval swtpm_cert \ |
| $options \ |
| $tpm_attr_params \ |
| --type platform \ |
| --signkey ${SIGNKEY} \ |
| --issuercert ${ISSUERCERT} \ |
| --out-cert ${dir}/platform.cert \ |
| $keyparms \ |
| --days $((10*365)) \ |
| --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 passparam |
| |
| if [ -n "${SWTPM_ROOTCA_PASSWORD}" ]; then |
| passparam="--password ${SWTPM_ROOTCA_PASSWORD}" |
| fi |
| |
| # create a CA first |
| msg=$(${CERTTOOL} \ |
| --generate-privkey \ |
| --outfile ${cakey} \ |
| ${passparam} \ |
| 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 |
| if [ -n "${SIGNKEY_PASSWORD}" ]; then |
| export GNUTLS_PIN=${SIGNKEY_PASSWORD} |
| fi |
| |
| msg=$(${CERTTOOL} \ |
| --generate-privkey \ |
| --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} |
| |
| msg=$(GNUTLS_PIN=${SWTPM_ROOTCA_PASSWORD} ${CERTTOOL} \ |
| --generate-certificate \ |
| --template ${tmp} \ |
| --outfile ${ISSUERCERT} \ |
| --load-privkey ${SIGNKEY} \ |
| --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 |