blob: 2bb382b85919731f1e29d75abbcf9b7895c98580 [file] [log] [blame]
#!/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