samples: Rewrite of swtpm_localca in 'C' (addresses issue #437)

This is a 1:1 rewrite of swtpm_localca in 'C' as a final step to get
rid of the python dependency. It addresses issue #437.

Tested on: Cygwin, Fedora 33, Ubuntu Xenial & Bionic, FreeBSD,
           DragonFlyBSD, OpenBSD (i386), Mac, Debian (recent), Centos-8,
           CentOS (recent), Alpine (recent), OpenSUSE (recent),
           Fedora 28 PPC Big Endian

Signed-off-by: Stefan Berger <stefanb@linux.ibm.com>
This commit is contained in:
Stefan Berger 2021-05-24 10:43:49 -04:00 committed by Stefan Berger
parent b1cde69b09
commit e689684c57
21 changed files with 1194 additions and 859 deletions

3
.gitignore vendored
View File

@ -40,11 +40,10 @@ Makefile
/include/swtpm.h
/man/man3/*.3
/man/man8/*.8
/samples/dist
/samples/py_swtpm_localca/swtpm_localca_conf.py
/samples/swtpm-create-user-config-files
/samples/swtpm-localca
/samples/swtpm-localca.conf
/samples/swtpm_localca_conf.h
/src/selinux/*.pp.bz2
/src/selinux/swtpm.pp
/src/selinux/swtpm_svirt.fc

View File

@ -3,6 +3,8 @@ CHANGES - changes for swtpm
version 0.6.0
- swtpm_setup:
- Rewritten in 'C'; needs json-glib
- swtpm-localca:
- Rewritten in 'C'
version 0.5.0:
- swtpm:

View File

@ -27,7 +27,4 @@ selinux-install selinux-uninstall:
@cd src/selinux && $(MAKE) $(AM_MAKEFLAGS) $@
endif
python-install python-uninstall:
@cd samples && $(MAKE) $(AM_MAKEFLAGS) $@
@cd src/swtpm_setup && $(MAKE) $(AM_MAKEFLAGS) $@
.PHONY: python-install python-uninstall selinux-install selinux-uninstall
.PHONY: selinux-install selinux-uninstall

View File

@ -382,16 +382,6 @@ if test "x$PIP3" = "x"; then
fi
fi
AC_MSG_CHECKING([for python setuptools package])
$PYTHON -c "import setuptools"
AS_IF([ test $? = 0 ],
[AC_MSG_RESULT([yes])],
[AC_MSG_ERROR([python setuptools is required])])
AC_ARG_ENABLE([python-installation],
AS_HELP_STRING([--disable-python-installation], [Disable running setup.py install for swtpm_setup]))
AM_CONDITIONAL([PYTHON_INSTALLATION], [test "x$enable_python_installation" != "xno"])
AC_ARG_ENABLE([hardening],
AS_HELP_STRING([--disable-hardening], [Disable hardening flags]))
@ -560,9 +550,9 @@ AC_CONFIG_FILES([Makefile \
etc/Makefile \
etc/swtpm_setup.conf \
samples/Makefile \
samples/py_swtpm_localca/swtpm_localca_conf.py \
samples/swtpm-localca.conf \
samples/swtpm-create-user-config-files \
samples/swtpm_localca_conf.h \
include/Makefile \
include/swtpm/Makefile \
include/swtpm.h \
@ -581,12 +571,6 @@ AC_CONFIG_FILES([Makefile \
tests/Makefile \
tests/test_config \
])
AC_CONFIG_FILES([samples/swtpm-localca],
[chmod 755 samples/swtpm-localca])
dnl for out-of-tree builds:
AC_CONFIG_LINKS([samples/py_swtpm_localca/__init__.py:samples/py_swtpm_localca/__init__.py \
samples/py_swtpm_localca/swtpm_localca.py:samples/py_swtpm_localca/swtpm_localca.py \
samples/py_swtpm_localca/swtpm_utils.py:samples/py_swtpm_localca/swtpm_utils.py])
AC_OUTPUT
echo

1
debian/control vendored
View File

@ -14,7 +14,6 @@ Build-Depends: automake,
expect,
libtasn1-dev,
socat,
python3-setuptools,
python3-twisted,
gnutls-dev,
gnutls-bin,

3
dist/swtpm.spec vendored
View File

@ -28,9 +28,6 @@ BuildRequires: expect
BuildRequires: net-tools
BuildRequires: openssl-devel
BuildRequires: socat
BuildRequires: python3
BuildRequires: python3-devel
BuildRequires: python3-setuptools
BuildRequires: python3-twisted
BuildRequires: softhsm
BuildRequires: trousers >= 0.3.9

3
dist/swtpm.spec.in vendored
View File

@ -28,9 +28,6 @@ BuildRequires: expect
BuildRequires: net-tools
BuildRequires: openssl-devel
BuildRequires: socat
BuildRequires: python3
BuildRequires: python3-devel
BuildRequires: python3-setuptools
BuildRequires: python3-twisted
BuildRequires: softhsm
BuildRequires: trousers >= 0.3.9

View File

@ -6,54 +6,44 @@
samplesconfdir = $(datadir)/@PACKAGE@
samplessysconfdir = $(sysconfdir)
# swtpm-localca was always installed in /usr/samples/swtpm-localca
bindir = $(samplesconfdir)
samplesconf_SCRIPTS = \
swtpm-create-tpmca \
swtpm-create-user-config-files \
swtpm-localca
swtpm-create-user-config-files
samplessysconf_DATA = \
swtpm-localca.conf \
swtpm-localca.options
PY_SWTPM_LOCALCA_FILES = $(wildcard py_swtpm_localca/*.py)
noinst_HEADERS = \
swtpm_localca.h \
swtpm_localca_utils.h
PY_PACKAGE_NAME=swtpm_localca
PY_PACKAGE_NV=$(PY_PACKAGE_NAME)-$(SWTPM_VER_MAJOR).$(SWTPM_VER_MINOR).$(SWTPM_VER_MICRO)
PY_PACKAGE=dist/$(PY_PACKAGE_NV).tar.gz
bin_PROGRAMS = \
swtpm-localca
# for out-of-tree builds we need to copy setup.py
$(PY_PACKAGE): $(PY_SWTPM_LOCALCA_FILES)
@$(CP) $(srcdir)/setup.py ./ &>/dev/null || true
./setup.py sdist
$(top_builddir)/src/utils/libswtpm_utils.la:
$(MAKE) -C$(dir $@)
all-local: $(PY_PACKAGE)
swtpm_localca_DEPENDENCIES = \
$(top_builddir)/src/utils/libswtpm_utils.la
python-install: $(PY_PACKAGE)
echo "Installing python to DESTDIR '/$(DESTDIR)'"
./setup.py install --prefix=$(prefix) --root "/$(DESTDIR)" --install-layout deb 2>/dev/null || \
./setup.py install --prefix=$(prefix) --root "/$(DESTDIR)" 2>/dev/null
swtpm_localca_SOURCES = \
swtpm_localca.c \
swtpm_localca_utils.c
python-uninstall:
echo "Local pip3 uninstall"
$(PIP3) uninstall -y $(PY_PACKAGE_NAME)
swtpm_localca_LDADD = \
$(top_builddir)/src/utils/libswtpm_utils.la
if PYTHON_INSTALLATION
install-exec-local: $(PY_PACKAGE)
@if ! test $(findstring /usr, "$(DESTDIR)$(bindir)"); then \
echo "Warning: Not installing python package to $(DESTDIR)$(bindir)"; \
else \
$(MAKE) python-install; \
fi
uninstall-local:
@if ! test $(findstring /usr, "$(DESTDIR)$(bindir)"); then \
echo "Cleanup for distcheck build not implemented" ; \
else \
$(MAKE) python-uninstall; \
fi
endif
swtpm_localca_LDFLAGS = \
-L$(top_builddir)/src/utils -lswtpm_utils \
$(GLIB_LIBS)
swtpm_localca_CFLAGS = \
-I$(top_srcdir)/src/utils \
$(GLIB_CFLAGS)
install-data-local:
$(MKDIR_P) $(DESTDIR)$(localstatedir)/lib/swtpm-localca
@ -62,19 +52,8 @@ install-data-local:
chmod 0750 $(DESTDIR)$(localstatedir)/lib/swtpm-localca || true; \
fi
# for out-of-tree builds we need to clean up
clean-local:
if test "$(top_srcdir)" != "$(top_builddir)"; then \
rm -f setup.py; \
fi
rm -rf py_swtpm_localca/__pycache__/
rm -rf swtpm_localca.egg-info/
rm -rf dist
EXTRA_DIST= \
setup.py \
swtpm-create-tpmca \
swtpm-create-user-config-files \
swtpm-localca \
swtpm-localca.conf \
swtpm-localca.options

View File

@ -1,701 +0,0 @@
#!/usr/bin/env python3
""" swtpm_localca.py
A tool for creating TPM 1.2 and TPM 2 certificates localy or using pkcs11
"""
# Disable a couple of warnings:
# R0911: Too many return statements (10/6) (too-many-return-statements)
# R0912: Too many branches (15/12) (too-many-branches)
# R0913: Too many arguments (14/5) (too-many-arguments)
# R0914: Too many local variables (21/15) (too-many-locals)
# R0915: Too many statements (57/50) (too-many-statements)
# W0703: Catching too general exception Exception (broad-except)
# pylint: disable=W0703,R0911,R0912,R0913,R0914,R0915
#
# swtpm_localca.py
#
# Authors: Stefan Berger <stefanb@linux.ibm.com>
#
# (c) Copyright IBM Corporation 2020
#
import codecs
import fcntl
import getopt
import getpass
import os
import re
import stat
import subprocess
import sys
import tempfile
from py_swtpm_localca.swtpm_localca_conf import SYSCONFDIR
from py_swtpm_localca.swtpm_utils import logit, logerr
# Some flags
SETUP_TPM2_F = 1
# for TPM 2 EK
ALLOW_SIGNING_F = 2
DECRYPTION_F = 4
XCH = os.getenv("XDG_CONFIG_HOME")
HOME = os.getenv("HOME")
LOCALCA_OPTIONS = "swtpm-localca.options"
if XCH and os.access(os.path.join(XCH, LOCALCA_OPTIONS), os.R_OK):
DEFAULT_LOCALCA_OPTIONS = os.path.join(XCH, LOCALCA_OPTIONS)
elif HOME and os.access(os.path.join(HOME, ".config", LOCALCA_OPTIONS), os.R_OK):
DEFAULT_LOCALCA_OPTIONS = os.path.join(HOME, ".config", LOCALCA_OPTIONS)
else:
DEFAULT_LOCALCA_OPTIONS = os.path.join(os.sep + SYSCONFDIR, LOCALCA_OPTIONS)
LOCALCA_CONFIG = "swtpm-localca.conf"
if XCH and os.access(os.path.join(XCH, LOCALCA_CONFIG), os.R_OK):
DEFAULT_LOCALCA_CONFIG = os.path.join(XCH, LOCALCA_CONFIG)
elif HOME and os.access(os.path.join(HOME, ".config", LOCALCA_CONFIG), os.R_OK):
DEFAULT_LOCALCA_CONFIG = os.path.join(HOME, ".config", LOCALCA_CONFIG)
else:
DEFAULT_LOCALCA_CONFIG = os.path.join(os.sep + SYSCONFDIR, LOCALCA_CONFIG)
# Default logging goes to stderr
LOGFILE = ""
def resolve_string(inp):
""" resolve environment variables in a string """
result = ""
sidx = 0
while True:
idx = inp.find("${", sidx)
if idx < 0:
if sidx == 0:
return inp
result += inp[sidx:]
return result
result += inp[sidx:idx]
eidx = inp.find("}", idx + 2)
if eidx < 0:
result += inp[idx:]
return result
result += os.getenv(inp[idx + 2:eidx], '')
sidx = eidx + 1
def get_config_value(lines, configname, default=None):
""" Get a config value from a list of strings """
regex = r'^' + configname + r"\s*=\s*([^#\n]*).*"
for line in lines:
match = re.match(regex, line)
if match:
return resolve_string(match.groups()[0])
return default
def get_config_envvars(lines):
""" Extract all environment variables from the config file and return a map.
Environment variable lines must start with 'env:' and must not contain
trailing spaces or a comment starting with '#' """
res = {}
regex = r"^env:([a-zA-Z_][a-zA-Z_0-9]*)\s*=\s*([^\n]*).*"
for line in lines:
match = re.match(regex, line)
if match:
try:
encoded = codecs.encode(match.group(2), "latin-1", "backslashreplace")
res[match.group(1)] = codecs.decode(encoded, "unicode_escape")
except Exception as err:
logerr(LOGFILE, "Invalid character in value of %s environment variable: %s\n" %
(match.group(1), str(err)))
return {}, 1
return res, 0
def write_file(filename, text):
""" Write some text to a file """
try:
fileobj = open(filename, "w")
fileobj.write(text)
fileobj.close()
return 0
except Exception as err:
logerr(LOGFILE, "Could not write to file %s: %s\n" % (filename, str(err)))
return 1
def read_file(filename):
""" read contents from a file """
try:
fobj = open(filename, mode='rb')
result = fobj.read()
fobj.close()
return result, 0
except Exception as err:
logerr(LOGFILE, "Could not read from file %s: %s\n" % (filename, str(err)))
return "", 1
def read_file_lines(filename):
""" Read the lines from a file and return a list of the lines """
try:
fobj = open(filename, 'r')
lines = fobj.readlines()
fobj.close()
return lines, 0
except Exception as err:
logerr(LOGFILE, "Could not read from file %s : %s\n" & (filename, str(err)))
return [], 1
def makedir(dirname, purpose):
""" Create a directory if it does not exist """
if not os.path.exists(dirname):
logit(LOGFILE, "Creating swtpm-local dir '%s'.\n" % dirname)
try:
os.makedirs(dirname)
except OSError as err:
logerr(LOGFILE, "Could not create directory for '%s': %s\n" % (purpose, str(err)))
return 1
return 0
def remove_file(filename, verbose=True):
""" remove a file """
if not os.path.exists(filename):
return 0
try:
os.remove(filename)
return 0
except Exception as err:
if verbose:
logerr(LOGFILE, "Could not remove file %s: %s\n" % (filename, str(err)))
return 1
def remove_files(filename_list):
""" remove files in a list of filenames """
for filename in filename_list:
remove_file(filename, verbose=False)
def get_certtool():
""" Get the name of the certtool to use """
if os.uname().sysname == "Darwin":
return "gnutls-certtool"
return "certtool"
def create_localca_cert(lockfile, statedir, signkey, signkey_password, issuercert):
""" Create the local CA's certificate if it doesn't already exist. """
try:
filedes = os.open(lockfile, os.O_RDWR|os.O_CREAT)
except Exception as err:
logerr(LOGFILE, "Could not open lockfile %s: %s\n" % (lockfile, str(err)))
return 1
try:
fcntl.flock(filedes, fcntl.LOCK_EX)
if not os.path.exists(statedir):
if makedir(statedir, "statedir") != 0:
return 1
if not os.access(signkey, os.R_OK):
directory = os.path.dirname(signkey)
cakey = os.path.join(directory, "swtpm-localca-rootca-privkey.pem")
cacert = os.path.join(directory, "swtpm-localca-rootca-cert.pem")
swtpm_rootca_password = os.getenv("SWTPM_ROOTCA_PASSWORD")
certtool = get_certtool()
# First the root CA
cmd = [certtool, "--generate-privkey", "--outfile", cakey]
if swtpm_rootca_password:
# neither env. variable nor template file work...
cmd.extend(["--password", swtpm_rootca_password])
try:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
output = proc.communicate()[0]
if proc.returncode:
logerr(LOGFILE, "Could not create root-CA key %s\n" % cakey)
logerr(LOGFILE, "%s" % output.decode())
return 1
except Exception as err:
logerr(LOGFILE, "Could not create root-CA key %s: %s\n" % (cakey, str(err)))
return 1
os.chmod(cakey, stat.S_IRUSR|stat.S_IWUSR|stat.S_IRGRP)
temp = tempfile.NamedTemporaryFile()
try:
filecontent = \
"cn=swtpm-localca-rootca\n" \
"ca\n" \
"cert_signing_key\n" \
"expiration_days = 3650\n"
temp.write(filecontent.encode())
temp.seek(0)
cmd = [certtool,
"--generate-self-signed",
"--template", temp.name,
"--outfile", cacert,
"--load-privkey", cakey]
certtool_env = {
"PATH": os.getenv("PATH")
}
if swtpm_rootca_password:
certtool_env["GNUTLS_PIN"] = swtpm_rootca_password
try:
proc = subprocess.Popen(cmd, env=certtool_env,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
output = proc.communicate()[0]
if proc.returncode:
logerr(LOGFILE, "Could not create root-CA\n")
logerr(LOGFILE, "%s" % output.decode())
remove_files([cakey, cacert])
return 1
except Exception as err:
logerr(LOGFILE, "Could not create root-CA: %s\n" % str(err))
remove_files([cakey, cacert])
return 1
finally:
temp.close()
# intermediate CA
cmd = [certtool, "--generate-privkey", "--outfile", signkey]
if signkey_password:
cmd.extend(["--password", signkey_password])
try:
proc = subprocess.Popen(cmd,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
output = proc.communicate()[0]
if proc.returncode:
logerr(LOGFILE, "Could not create local-CA key %s\n" % signkey)
logerr(LOGFILE, "certtool failed: %s\n" % output.decode())
remove_files([cakey, cacert, signkey])
return 1
except Exception as err:
logerr(LOGFILE, "Could not create local-CA key %s: %s\n" % (signkey, str(err)))
remove_files([cakey, cacert, signkey])
return 1
os.chmod(signkey, stat.S_IRUSR|stat.S_IWUSR|stat.S_IRGRP)
temp = tempfile.NamedTemporaryFile()
try:
filecontent = \
"cn=swtpm-localca\n" \
"ca\n" \
"cert_signing_key\n" \
"expiration_days = 3650\n"
if swtpm_rootca_password and signkey_password:
filecontent += "password = %s\n" % swtpm_rootca_password
temp.write(filecontent.encode())
temp.seek(0)
cmd = [certtool,
"--generate-certificate",
"--template", temp.name,
"--outfile", issuercert,
"--load-privkey", signkey,
"--load-ca-privkey", cakey,
"--load-ca-certificate", cacert]
certtool_env = {
"PATH": os.getenv("PATH")
}
if signkey_password:
certtool_env["GNUTLS_PIN"] = signkey_password
elif swtpm_rootca_password:
certtool_env["GNUTLS_PIN"] = swtpm_rootca_password
try:
proc = subprocess.Popen(cmd, env=certtool_env,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
output = proc.communicate()[0]
if proc.returncode:
logerr(LOGFILE, "Could not create local CA\n")
logerr(LOGFILE, "%s" % output.decode())
remove_files([cakey, cacert, signkey, issuercert])
return 1
except Exception as err:
logerr(LOGFILE, "Could not create local CA: %s\n" % str(err))
remove_files([cakey, cacert, signkey, issuercert])
return 1
finally:
temp.close()
finally:
os.close(filedes)
return 0
def get_next_cert_serial(certserial, lockfile):
""" Get the next serial number for a certificate """
try:
filedes = os.open(lockfile, os.O_RDWR|os.O_CREAT)
except Exception as err:
logerr(LOGFILE, "Could not open lockfile %s: %s\n" % (lockfile, str(err)))
return 1
try:
fcntl.flock(filedes, fcntl.LOCK_EX)
if not os.access(certserial, os.R_OK):
_ = write_file(certserial, "1")
serial, ret = read_file(certserial)
if ret != 0:
return "", 1
if not serial.decode().isnumeric():
serial_n = 1
else:
serial_n = int(serial) + 1
ret = write_file(certserial, "%d" % serial_n)
if ret != 0:
return "", 1
finally:
os.close(filedes)
return "%d" % serial_n, 0
def create_cert(flags, typ, directory, ekparams, vmid, tpm_spec_params, tpm_attr_params,
signkey, signkey_password, issuercert, parentkey_password, swtpm_cert_env,
certserial, lockfile, optsfile):
""" Create the certificate """
serial, ret = get_next_cert_serial(certserial, lockfile)
if ret != 0:
return 1
options = []
lines, _ = read_file_lines(optsfile)
for line in lines:
if not line.strip():
continue
options.extend([x.strip() for x in line.split(" ", 1)])
if vmid:
subj = "CN=%s" % vmid
else:
subj = "CN=unknown"
if flags & SETUP_TPM2_F:
options.append("--tpm2")
else:
options.append("--add-header")
if typ == "ek":
if flags & ALLOW_SIGNING_F:
options.append("--allow-signing")
if flags & DECRYPTION_F:
options.append("--decryption")
match = re.search(r'x=([0-9A-Fa-f]+),y=([0-9A-Fa-f]+)(,id=([^,]+))?', ekparams)
if match:
keyparams = ["--ecc-x", match.group(1), "--ecc-y", match.group(2)]
if match.group(4):
keyparams.extend(["--ecc-curveid", match.group(4)])
else:
keyparams = ["--modulus", ekparams]
cmd = ["swtpm_cert",
"--subject", subj]
cmd.extend(options)
temp1 = None
temp2 = None
if signkey_password:
temp1 = tempfile.NamedTemporaryFile()
temp1.write(signkey_password.encode())
temp1.seek(0)
cmd.extend(["--signkey-pwd", "file:%s" % temp1.name])
if parentkey_password:
temp2 = tempfile.NamedTemporaryFile()
temp2.write(parentkey_password.encode())
temp2.seek(0)
cmd.extend(["--parentkey-pwd", "file:%s" % temp2.name])
if typ == "ek":
cmd.extend(tpm_spec_params)
cmd.extend(tpm_attr_params)
if typ == "platform":
cmd.extend(["--type", "platform",
"--out-cert", os.path.join(directory, "platform.cert")])
else:
cmd.extend(["--out-cert", os.path.join(directory, "ek.cert")])
cmd.extend(keyparams)
cmd.extend(["--signkey", signkey,
"--issuercert", issuercert,
"--days", "3650",
"--serial", serial])
if typ == "ek":
certtype = "EK"
else:
certtype = "platform"
try:
proc = subprocess.Popen(cmd, env=swtpm_cert_env,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
output = proc.communicate()[0]
if proc.returncode:
logerr(LOGFILE, "Could not create %s certificate locally\n" % certtype)
logerr(LOGFILE, "%s" % output.decode())
return 1
except Exception as err:
logerr(LOGFILE, "Could not run swtpm_cert: %s\n" % str(err))
return 1
finally:
if temp1:
temp1.close()
if temp2:
temp2.close()
logit(LOGFILE, "Successfully created %s certificate locally.\n" % certtype)
return 0
def usage(prgname):
""" Display usage """
print(
"Usage: {prgname} [options]\n"
"\n"
"The following options are supported:\n"
"\n"
"--type type The type of certificate to create: 'ek' or 'platform'\n"
"--ek key-param The modulus of an RSA key or x=...,y=,... for an EC key\n"
"--dir directory The directory to write the resulting certificate into\n"
"--vmid vmid The ID of the virtual machine\n"
"--optsfile file A file containing options to pass to swtpm_cert\n"
"--configfile file A file containing configuration parameters for directory,\n"
" signing key and password and certificate to use\n"
"--logfile file A file to write a log into\n"
"--tpm-spec-family s The implemented spec family, e.g., '2.0'\n"
"--tpm-spec-revision i The spec revision of the TPM as integer; e.g., 146\n"
"--tpm-spec-level i The spec level of the TPM; must be an integer; e.g. 0\n"
"--tpm-manufacturer s The manufacturer of the TPM; e.g., id:00001014\n"
"--tpm-model s The model of the TPM; e.g., 'swtpm'\n"
"--tpm-version i The (firmware) version of the TPM; e.g., id:20160511\n"
"--tpm2 Generate a certificate for a TPM 2\n"
"--allow-signing The TPM 2's EK can be used for signing\n"
"--decryption The TPM 2's EK can be used for decryption\n"
"--help, -h, -? Display this help screen and exit\n"
"\n"
"\n"
"The following environment variables are supported:\n"
"\n"
"SWTPM_ROOTCA_PASSWORD The root CA's private key password\n"
"\n".format_map({
'prgname': prgname,
}))
def main():
""" main function - parses command line parameters and low level dealing with them """
global LOGFILE # pylint: disable=W0603
try:
opts, _ = getopt.getopt(sys.argv[1:], "h?",
["type=",
"ek=",
"dir=",
"vmid=",
"optsfile=",
"configfile=",
"logfile=",
"tpm-spec-family=",
"tpm-spec-revision=",
"tpm-spec-level=",
"tpm-manufacturer=",
"tpm-model=",
"tpm-version=",
"tpm2",
"allow-signing",
"decryption",
"help"])
except getopt.GetoptError as err:
print(err)
usage(sys.argv[0])
sys.exit(1)
flags = 0
typ = ""
ekparams = ""
directory = "." # default to current directory
vmid = ""
optsfile = DEFAULT_LOCALCA_OPTIONS
configfile = DEFAULT_LOCALCA_CONFIG
tpm_spec_params = []
tpm_attr_params = []
for opt, arg in opts:
if opt == '--type':
typ = arg
elif opt == '--ek':
ekparams = arg
elif opt == '--dir':
directory = arg
elif opt == '--vmid':
vmid = arg
elif opt == '--optsfile':
optsfile = arg
elif opt == '--configfile':
configfile = arg
elif opt == '--logfile':
LOGFILE = arg
elif opt in ['--tpm-spec-family', '--tpm-spec-revision', '--tpm-spec-level']:
tpm_spec_params.extend([opt, arg])
elif opt in ['--tpm-manufacturer', '--tpm-model', '--tpm-version']:
tpm_attr_params.extend([opt, arg])
elif opt == '--tpm2':
flags |= SETUP_TPM2_F
elif opt == '--allow-signing':
flags |= ALLOW_SIGNING_F
elif opt == '--decryption':
flags |= DECRYPTION_F
elif opt in ['--help', '-h', '-?']:
usage(sys.argv[0])
sys.exit(0)
if len(LOGFILE) > 0:
if os.path.islink(LOGFILE):
sys.stderr.write("Logfile must not be a symlink.\n")
sys.exit(1)
try:
fobj = open(LOGFILE, "a") # do not truncate
fobj.close()
except PermissionError:
sys.stderr.write("Cannot write to logfile %s.\n", LOGFILE)
sys.exit(1)
if not os.access(optsfile, os.R_OK):
logerr(LOGFILE, "Need read rights on options file %s for user %s.\n" %
(optsfile, getpass.getuser()))
sys.exit(1)
if not os.access(configfile, os.R_OK):
logerr(LOGFILE, "Need read rights on options file %s for user %s.\n" %
(configfile, getpass.getuser()))
sys.exit(1)
lines, ret = read_file_lines(configfile)
if ret != 0:
sys.exit(1)
statedir = get_config_value(lines, "statedir")
if not statedir:
logerr(LOGFILE, "Missing 'statedir' config value in config file %s.\n" % configfile)
sys.exit(1)
if makedir(statedir, "statedir") != 0:
sys.exit(1)
if not os.access(statedir, os.W_OK | os.R_OK):
logerr(LOGFILE, "Need read/write rights on statedir %s for user %s.\n" %
(statedir, getpass.getuser()))
lockfile = os.path.join(statedir, ".lock.swtpm-localca")
if os.path.exists(lockfile) and not os.access(lockfile, os.W_OK | os.R_OK):
logerr(LOGFILE, "Need read/write rights on %s for user %s.\n" %
(lockfile, getpass.getuser()))
sys.exit(1)
signkey = get_config_value(lines, "signingkey")
if not signkey:
logerr(LOGFILE, "Missing 'signingkey' config value in config file %s.\n" % configfile)
sys.exit(1)
# SIGNKEY may be a GNUTLS url like tpmkey:file= or tpmkey:uuid=
if not signkey.startswith("tpmkey:file=") and \
not signkey.startswith("tpmkey:uuid=") and \
not signkey.startswith("pkcs11:"):
if makedir(os.path.dirname(signkey), "signkey") != 0:
sys.exit(1)
signkey_password = get_config_value(lines, "signingkey_password")
parentkey_password = get_config_value(lines, "parentkey_password")
issuercert = get_config_value(lines, 'issuercert')
if not issuercert:
logerr(LOGFILE, "Missing 'issuercert' config value in config file %s.\n" % configfile)
sys.exit(1)
if makedir(os.path.dirname(issuercert), "issuercert") != 0:
sys.exit(1)
# environment needed for calling swtpm_cert
swtpm_cert_env = os.environ
# TPM keys are GNUTLS URIs...
if signkey.startswith("tpmkey:file=") or signkey.startswith("tpmkey:uuid="):
tss_tcsd_hostname = get_config_value(lines, "TSS_TCSD_HOSTNAME", "localhost")
tss_tcsd_port = get_config_value(lines, "TSS_TCSD_PORT", 30003)
swtpm_cert_env["TSS_TCSD_HOSTNAME"] = tss_tcsd_hostname
swtpm_cert_env["TSS_TCSD_PORT"] = tss_tcsd_port
logit(LOGFILE, "CA uses a GnuTLS TPM key; using TSS_TCSD_HOSTNAME=%s " \
"TSS_TCSD_PORT=%s\n" % (tss_tcsd_hostname, tss_tcsd_port))
elif signkey.startswith("pkcs11:"):
signkey = signkey.replace(r"\;", ";")
if signkey_password:
swtpm_cert_env["SWTPM_PKCS11_PIN"] = signkey_password
logit(LOGFILE, "CA uses a PKCS#11 key; using password from 'signingkey_password'\n")
else:
swtpm_pkcs11_pin = get_config_value(lines, "SWTPM_PKCS11_PIN", "swtpm-tpmca")
swtpm_cert_env["SWTPM_PKCS11_PIN"] = swtpm_pkcs11_pin
logit(LOGFILE, "CA uses a PKCS#11 key; using SWTPM_PKCS11_PIN\n")
# Get additional environment variables pkcs11 modules may need
envvars, ret = get_config_envvars(lines)
if ret != 0:
sys.exit(1)
swtpm_cert_env.update(envvars)
else:
# if signkey does not exists it will be created...
if not os.access(signkey, os.R_OK):
if os.path.exists(signkey):
logerr(LOGFILE, "Need read rights on signing key %s for user %s.\n" %
(signkey, getpass.getuser()))
sys.exit(1)
logit(LOGFILE, "Creating root CA and a local CA's signing key and issuer cert.\n")
if create_localca_cert(lockfile, statedir, signkey, signkey_password,
issuercert) != 0:
logerr(LOGFILE, "Error creating local CA's signing key and cert.\n")
sys.exit(1)
if not os.access(signkey, os.R_OK):
logerr(LOGFILE, "Need read rights on signing key %s for user %s.\n" %
(signkey, getpass.getuser()))
sys.exit(1)
if not os.access(issuercert, os.R_OK):
logerr(LOGFILE, "Need read rights on issuer certificate %s for user %s.\n" %
(issuercert, getpass.getuser()))
sys.exit(1)
certserial = get_config_value(lines, "certserial", os.path.join(statedir, "certserial"))
if makedir(os.path.dirname(certserial), "certserial") != 0:
sys.exit(1)
ret = create_cert(flags, typ, directory, ekparams, vmid, tpm_spec_params, tpm_attr_params,
signkey, signkey_password, issuercert, parentkey_password, swtpm_cert_env,
certserial, lockfile, optsfile)
sys.exit(ret)
if __name__ == "__main__":
main()

View File

@ -1,11 +0,0 @@
""" swtpm_localca_conf.py
This module holds compile-time constants
"""
SWTPM_VER_MAJOR = @SWTPM_VER_MAJOR@
SWTPM_VER_MINOR = @SWTPM_VER_MINOR@
SWTPM_VER_MICRO = @SWTPM_VER_MICRO@
SYSCONFDIR = "@SYSCONFDIR@"

View File

@ -1,40 +0,0 @@
""" swtpm_logging.py
"""
# pylint: disable=W0703
import os
import sys
def append_to_file(filename, string):
"""" Append a string to a file """
try:
filedesc = os.open(filename, os.O_WRONLY|os.O_APPEND|os.O_CREAT|os.O_NOFOLLOW, 0o640)
os.write(filedesc, string.encode('utf-8'))
os.close(filedesc)
except Exception as ex:
sys.stdout.write("Error: %s\n" % ex)
sys.stdout.write(string)
try:
if filedesc > 0:
os.close(filedesc)
except Exception:
pass
def logit(logfile, string):
""" Print the given string to stdout or into the logfile """
if len(logfile) == 0:
sys.stdout.write(string)
else:
append_to_file(logfile, string)
def logerr(logfile, string):
""" Print the given string to stderr or into the logfile """
if len(logfile) == 0:
sys.stdout.write(string)
else:
append_to_file(logfile, string)

View File

@ -1,26 +0,0 @@
#!/usr/bin/env python3
""" setup.py
Install swtpm-localca
"""
import setuptools
from py_swtpm_localca.swtpm_localca_conf import SWTPM_VER_MAJOR, SWTPM_VER_MINOR, SWTPM_VER_MICRO
setuptools.setup(
name="swtpm-localca",
version="%d.%d.%d" % (SWTPM_VER_MAJOR, SWTPM_VER_MINOR, SWTPM_VER_MICRO),
author="Stefan Berger et al.",
author_email="stefanb@linux.ibm.com",
url="https::/github.com/stefanberger/swtpm",
#packages=setuptools.find_packages(),
description="A local CA for creating TPM 1.2 and TPM 2 EK and platform certificates",
python_requires=">=3.2",
packages=["py_swtpm_localca"],
package_dir={
"py_swtpm_localca": "py_swtpm_localca"
},
license="BSD3",
install_requires=[
],
)

View File

@ -1,7 +0,0 @@
#!/usr/bin/env python3
""" Launcher for swtpm-localca
"""
from py_swtpm_localca.swtpm_localca import main
main()

864
samples/swtpm_localca.c Normal file
View File

@ -0,0 +1,864 @@
/* SPDX-License-Identifier: BSD-3-Clause */
/*
* swtpm_localca.c: A tool for creating TPM 1.2 and TPM 2 certificates localy or using pkcs11
*
* Author: Stefan Berger, stefanb@linux.ibm.com
*
* Copyright (c) IBM Corporation, 2021
*/
#include "config.h"
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <pwd.h>
#include <regex.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <glib.h>
#include "swtpm_utils.h"
#include "swtpm_localca_conf.h"
#include "swtpm_localca_utils.h"
#define SETUP_TPM2_F 1
/* for TPM 2 EK */
#define ALLOW_SIGNING_F 2
#define DECRYPTION_F 4
/* Default logging goes to stderr */
gchar *gl_LOGFILE = NULL;
#define LOCALCA_OPTIONS "swtpm-localca.options"
#define LOCALCA_CONFIG "swtpm-localca.conf"
#if defined __APPLE__
# define CERTTOOL_NAME "gnutls-certtool"
#else
# define CERTTOOL_NAME "certtool"
#endif
/* initialize the path of the options and config files */
static int init(gchar **options_file, gchar **config_file)
{
const char *xch = getenv("XDG_CONFIG_HOME");
const char *home = getenv("HOME");
char path[PATH_MAX];
const char *p = NULL;
int ret = 0;
if (xch != NULL &&
(p = pathjoin(path, sizeof(path), xch, LOCALCA_OPTIONS, NULL)) != NULL &&
access(p, R_OK) == 0) {
/* p is good */
} else if (home != NULL &&
(p = pathjoin(path, sizeof(path), home, ".config", LOCALCA_OPTIONS)) != NULL &&
access(p, R_OK) == 0) {
/* p is good */
} else {
p = pathjoin(path, sizeof(path), G_DIR_SEPARATOR_S, SYSCONFDIR, LOCALCA_OPTIONS);
}
*options_file = g_strdup(p);
if (xch != NULL &&
(p = pathjoin(path, sizeof(path), xch, LOCALCA_CONFIG, NULL)) != NULL &&
access(p, R_OK) == 0) {
/* p is good */
} else if (home != NULL &&
(p = pathjoin(path, sizeof(path), home, ".config", LOCALCA_CONFIG)) != NULL &&
access(p, R_OK) == 0) {
/* p is good */
} else {
p = pathjoin(path, sizeof(path), G_DIR_SEPARATOR_S, SYSCONFDIR, LOCALCA_CONFIG);
}
*config_file = g_strdup(p);
return ret;
}
/* Run the certtool command line prepared in cmd. Display error message
* in case of failure and also display the keyfile if something goes wrong.
*/
static int run_certtool(gchar **cmd, gchar **env, const char *msg, gchar *keyfile)
{
g_autofree gchar *standard_error = NULL;
gint exit_status;
GError *error = NULL;
gboolean success;
success = g_spawn_sync(NULL, cmd, env, G_SPAWN_STDOUT_TO_DEV_NULL, NULL, NULL,
NULL, &standard_error, &exit_status, &error);
if (!success || exit_status != 0) {
logerr(gl_LOGFILE, "%s" , msg);
if (keyfile)
logerr(gl_LOGFILE, " %s:", keyfile);
if (!success) {
logerr(gl_LOGFILE, "%s\n", error->message);
g_error_free(error);
} else {
logerr(gl_LOGFILE, "%s\n", standard_error);
}
return 1;
}
return 0;
}
/* Create a root CA key and cert and a local CA key and cert. The latter will be
* used for signing the TPM certs.
*/
static int create_localca_cert(const gchar *lockfile, const gchar *statedir,
const gchar *signkey, const gchar *signkey_password,
const gchar *issuercert)
{
int lockfd;
int ret = 1;
struct stat statbuf;
int template1_file_fd = -1;
int template2_file_fd = -1;
g_autofree gchar *template1_file = NULL;
g_autofree gchar *template2_file = NULL;
gchar **certtool_env = NULL;
lockfd = lock_file(lockfile);
if (lockfd < 0)
return 1;
if (stat(statedir, &statbuf) != 0) {
if (makedir(statedir, "statedir") != 0)
goto error;
}
if (access(signkey, R_OK) != 0) {
g_autofree gchar *directory = g_path_get_dirname(signkey);
g_autofree gchar *cakey = g_strjoin(G_DIR_SEPARATOR_S, directory, "swtpm-localca-rootca-privkey.pem", NULL);
g_autofree gchar *cacert = g_strjoin(G_DIR_SEPARATOR_S, directory, "swtpm-localca-rootca-cert.pem", NULL);
const gchar *swtpm_rootca_password = g_getenv("SWTPM_ROOTCA_PASSWORD");
g_autofree gchar *certtool = g_find_program_in_path(CERTTOOL_NAME);
g_autofree gchar **cmd = NULL;
g_autofree gchar *fc = NULL;
const char *filecontent;
if (certtool == NULL) {
logerr(gl_LOGFILE, "Could not find %s in PATH.\n", CERTTOOL_NAME);
goto error;
}
/* generate the root-CA's private key */
cmd = concat_arrays(cmd, (gchar*[]){
(gchar *)certtool, "--generate-privkey", "--outfile", cakey, NULL
}, TRUE);
if (swtpm_rootca_password != NULL)
cmd = concat_arrays(cmd, (gchar*[]){
"--password", (gchar *)swtpm_rootca_password, NULL
}, TRUE);
if (run_certtool(cmd, certtool_env, "Could not create root-CA key", cakey))
goto error;
if (chmod(cakey, S_IRUSR | S_IWUSR | S_IRGRP) != 0) {
logerr(gl_LOGFILE, "Could not chmod %s: %s\n", cakey, strerror(errno));
goto error;
}
certtool_env = g_environ_setenv(NULL, "PATH", g_getenv("PATH"), TRUE);
/* create the root-CA's cert */
filecontent = "cn=swtpm-localca-rootca\n"
"ca\n"
"cert_signing_key\n"
"expiration_days = 7200\n";
template1_file_fd = write_to_tempfile(&template1_file,
(const unsigned char *)filecontent, strlen(filecontent));
if (template1_file_fd < 0)
goto error;
g_free(cmd);
cmd = concat_arrays(NULL,
(gchar *[]) {
certtool,
"--generate-self-signed",
"--template", template1_file,
"--outfile", cacert,
"--load-privkey", cakey,
NULL
}, FALSE);
if (swtpm_rootca_password != NULL)
certtool_env = g_environ_setenv(certtool_env, "GNUTLS_PIN", swtpm_rootca_password, TRUE);
if (run_certtool(cmd, certtool_env, "Could not create root-CA:", NULL))
goto error;
g_free(cmd);
/* create the intermediate CA's key */
cmd = concat_arrays(NULL,
(gchar *[]) {
certtool, "--generate-privkey", "--outfile", (gchar *)signkey, NULL
}, FALSE);
if (signkey_password != NULL)
cmd = concat_arrays(cmd, (gchar *[]){
"--password", (gchar *)signkey_password, NULL},
TRUE);
if (run_certtool(cmd, certtool_env, "Could not create local-CA key", cakey))
goto error;
if (chmod(signkey, S_IRUSR | S_IWUSR | S_IRGRP) != 0) {
logerr(gl_LOGFILE, "Could not chmod %s: %s\n", signkey, strerror(errno));
goto error;
}
filecontent = "cn=swtpm-localca\n"
"ca\n"
"cert_signing_key\n"
"expiration_days = 7200\n";
if (swtpm_rootca_password != NULL && signkey_password != NULL)
fc = g_strdup_printf("%spassword = %s\n", filecontent, swtpm_rootca_password);
else
fc = g_strdup(filecontent);
template2_file_fd = write_to_tempfile(&template2_file,
(const unsigned char *)fc, strlen(fc));
if (template2_file_fd < 0)
goto error;
g_free(cmd);
cmd = concat_arrays(NULL,
(gchar *[]) {
certtool,
"--generate-certificate",
"--template", template2_file,
"--outfile", (gchar *)issuercert,
"--load-privkey", (gchar *)signkey,
"--load-ca-privkey", cakey,
"--load-ca-certificate", cacert,
NULL
}, FALSE);
if (signkey_password != NULL)
certtool_env = g_environ_setenv(certtool_env, "GNUTLS_PIN", signkey_password, TRUE);
else if (swtpm_rootca_password != NULL)
certtool_env = g_environ_setenv(certtool_env, "GNUTLS_PIN", swtpm_rootca_password, TRUE);
if (run_certtool(cmd, certtool_env, "Could not create local-CA:", NULL))
goto error;
}
ret = 0;
error:
if (template1_file_fd >= 0)
close(template1_file_fd);
if (template1_file != NULL)
unlink(template1_file);
if (template2_file_fd >= 0)
close(template2_file_fd);
if (template2_file != NULL)
unlink(template2_file);
g_strfreev(certtool_env);
unlock_file(lockfd);
return ret;
}
/* Extract the ECC parameters from a string like x=12,y=34,id=secp384r1.
* This function returns 1 on error, 2 if the ECC parameters could be extracted
* and 0 if no parameters could be extracted (likely a modulus).
*/
static gboolean extract_ecc_params(const gchar *ekparams, gchar **ecc_x, gchar **ecc_y, gchar **ecc_curveid)
{
regmatch_t pmatch[5];
regex_t preg;
int ret;
if (regcomp(&preg, "x=([0-9A-Fa-f]+),y=([0-9A-Fa-f]+)(,id=([^,]+))?",
REG_EXTENDED) != 0) {
logerr(gl_LOGFILE, "Internal error: Could not compile regex\n");
return 1;
}
ret = 0;
if (regexec(&preg, ekparams, 5, pmatch, 0) == 0) {
*ecc_x = g_strndup(&ekparams[pmatch[1].rm_so],
pmatch[1].rm_eo - pmatch[1].rm_so);
*ecc_y = g_strndup(&ekparams[pmatch[2].rm_so],
pmatch[2].rm_eo - pmatch[2].rm_so);
if (pmatch[4].rm_so > 0 && pmatch[4].rm_eo > 0)
*ecc_curveid = g_strndup(&ekparams[pmatch[4].rm_so],
pmatch[4].rm_eo - pmatch[4].rm_so);
ret = 2;
}
regfree(&preg);
return ret;
}
/* Get the next serial number from the certserial file; if it contains
* a non-numeric content start over with serial number '1'.
*/
static int get_next_serial(const gchar *certserial, const gchar *lockfile,
gchar **serial_str)
{
g_autofree gchar *buffer = NULL;
size_t buffer_len;
unsigned long long serial, serial_n;
char *endptr = NULL;
int lockfd;
int ret = 1;
lockfd = lock_file(lockfile);
if (lockfd < 0)
return 1;
if (access(certserial, R_OK) != 0)
write_file(certserial, (unsigned char *)"1", 1);
if (read_file(certserial, &buffer, &buffer_len) != 0)
goto error;
if (buffer_len > 0) {
serial = strtoull(buffer, &endptr, 10);
if (*endptr == '\0') {
serial_n = serial + 1;
} else {
serial_n = 1;
}
} else {
serial_n = 1;
}
*serial_str = g_strdup_printf("%llu", serial_n);
write_file(certserial, (unsigned char *)*serial_str, strlen(*serial_str));
ret = 0;
error:
unlock_file(lockfd);
return ret;
}
/* Create a TPM 1.2 or TPM 2 EK or platform cert */
static int create_cert(unsigned long flags, const gchar *typ, const gchar *directory,
gchar *ekparams, const gchar *vmid, gchar **tpm_spec_params,
gchar **tpm_attr_params, const gchar *signkey,
const gchar *signkey_password, const gchar *issuercert,
const gchar *parentkey_password, gchar **swtpm_cert_env,
const gchar *certserial, const gchar *lockfile,
const gchar *optsfile)
{
gchar ** optsfile_lines = NULL;
g_autofree gchar **options = NULL;
g_autofree gchar **keyparams = NULL;
g_autofree gchar **cmd = NULL;
g_autofree gchar *subject = NULL;
g_autofree gchar *ecc_x = NULL;
g_autofree gchar *ecc_y = NULL;
g_autofree gchar *ecc_curveid = NULL;
g_autofree gchar *certfile = NULL;
g_autofree gchar *serial_str = NULL;
gchar **to_free = NULL;
gchar **split;
const char *certtype;
int signkey_pwd_fd = -1;
int parentkey_pwd_fd = -1;
g_autofree gchar *signkey_pwd_file = NULL;
g_autofree gchar *signkey_pwd_file_param = NULL;
g_autofree gchar *parentkey_pwd_file = NULL;
g_autofree gchar *parentkey_pwd_file_param = NULL;
gboolean success;
g_autofree gchar *standard_output = NULL;
g_autofree gchar *standard_error = NULL;
g_autofree gchar *swtpm_cert_path = NULL;
GError *error = NULL;
gint exit_status;
int ret = 1;
size_t i, j;
swtpm_cert_path = g_find_program_in_path("swtpm_cert");
if (swtpm_cert_path == NULL) {
logerr(gl_LOGFILE, "Could not find swtpm_cert in PATH.\n");
return 1;
}
if (get_next_serial(certserial, lockfile, &serial_str) != 0)
return 1;
/* try to read the optsfile */
read_file_lines(optsfile, &optsfile_lines);
/* split each line from the optsfile and add the stripped parameters to options */
for (i = 0; optsfile_lines != NULL && optsfile_lines[i] != NULL; i++) {
gchar *chomped = g_strchomp(optsfile_lines[i]);
if (strlen(chomped) == 0)
continue;
split = g_strsplit(chomped, " ", -1);
for (j = 0; split[j] != NULL; j++) {
chomped = g_strchomp(split[j]);
if (strlen(chomped) > 0) {
gchar *to_add = g_strdup(chomped);
options = concat_arrays(options, (gchar *[]){to_add, NULL}, TRUE);
/* need to collect this also to free later on */
to_free = concat_arrays(to_free, (gchar *[]){to_add, NULL}, TRUE);
}
}
g_strfreev(split);
}
if (vmid != NULL)
subject = g_strdup_printf("CN=%s", vmid);
else
subject = g_strdup("CN=unknown");
if (flags & SETUP_TPM2_F)
options = concat_arrays(options, (gchar *[]){"--tpm2", NULL}, TRUE);
else
options = concat_arrays(options, (gchar *[]){"--add-header", NULL}, TRUE);
if (strcmp(typ, "ek") == 0) {
if (flags & ALLOW_SIGNING_F)
options = concat_arrays(options, (gchar *[]){"--allow-signing", NULL}, TRUE);
if (flags & DECRYPTION_F)
options = concat_arrays(options, (gchar *[]){"--decryption", NULL}, TRUE);
}
switch (extract_ecc_params(ekparams, &ecc_x, &ecc_y, &ecc_curveid)) {
case 1:
goto error;
case 2:
keyparams = concat_arrays((gchar *[]){
"--ecc-x", ecc_x,
"--ecc-y", ecc_y,
NULL
},
NULL, FALSE);
if (ecc_curveid != NULL)
keyparams = concat_arrays(keyparams,
(gchar *[]){
"--ecc-curveid", ecc_curveid,
NULL
}, TRUE);
break;
case 0:
keyparams = concat_arrays((gchar *[]){
"--modulus", ekparams,
NULL},
NULL, FALSE);
break;
}
cmd = concat_arrays((gchar *[]){
swtpm_cert_path, "--subject", subject, NULL
}, options, FALSE);
if (signkey_password != NULL) {
signkey_pwd_fd = write_to_tempfile(&signkey_pwd_file,
(unsigned char *)signkey_password, strlen(signkey_password));
if (signkey_pwd_fd < 0)
goto error;
signkey_pwd_file_param = g_strdup_printf("file:%s", signkey_pwd_file);
cmd = concat_arrays(cmd, (gchar*[]){"--signkey-pwd", signkey_pwd_file_param, NULL}, TRUE);
}
if (parentkey_password != NULL) {
parentkey_pwd_fd = write_to_tempfile(&parentkey_pwd_file,
(unsigned char *)parentkey_password, strlen(parentkey_password));
if (parentkey_pwd_fd < 0)
goto error;
parentkey_pwd_file_param = g_strdup_printf("file:%s", parentkey_pwd_file);
cmd = concat_arrays(cmd, (gchar*[]){"--parentkey-pwd", parentkey_pwd_file_param, NULL}, TRUE);
}
if (strcmp(typ, "ek") == 0)
cmd = concat_arrays(cmd, tpm_spec_params, TRUE);
cmd = concat_arrays(cmd, tpm_attr_params, TRUE);
if (strcmp(typ, "platform") == 0) {
certfile = g_strjoin(G_DIR_SEPARATOR_S, directory, "platform.cert", NULL);
cmd = concat_arrays(cmd,
(gchar *[]){
"--type", "platform",
"--out-cert", certfile,
NULL},
TRUE);
} else {
certfile = g_strjoin(G_DIR_SEPARATOR_S, directory, "ek.cert", NULL);
cmd = concat_arrays(cmd,
(gchar *[]){
"--out-cert", certfile,
NULL
}, TRUE);
}
cmd = concat_arrays(cmd, keyparams, TRUE);
cmd = concat_arrays(cmd, (gchar *[]){
"--signkey", (gchar *)signkey,
"--issuercert", (gchar *)issuercert,
"--days", "7300",
"--serial", (gchar *)serial_str,
NULL
}, TRUE);
if (strcmp(typ, "ek") == 0)
certtype = "EK";
else
certtype = "platform";
#if 0
{
g_autofree gchar *join = g_strjoinv(" ", cmd);
fprintf(stderr, "Starting: %s\n", join);
}
#endif
success = g_spawn_sync(NULL, cmd, swtpm_cert_env, G_SPAWN_DEFAULT, NULL, NULL,
&standard_output, &standard_error, &exit_status, &error);
if (!success) {
logerr(gl_LOGFILE, "Could not run swtpm_cert: %s\n", error);
g_error_free(error);
goto error;
}
if (exit_status != 0) {
logerr(gl_LOGFILE, "Could not create %s certificate locally\n", certtype);
logerr(gl_LOGFILE, "%s\n", standard_error);
goto error;
}
logit(gl_LOGFILE, "Successfully created %s certificate locally.\n", certtype);
ret = 0;
error:
g_strfreev(optsfile_lines);
g_strfreev(to_free);
if (signkey_pwd_fd >= 0)
close(signkey_pwd_fd);
if (signkey_pwd_file)
unlink(signkey_pwd_file);
if (parentkey_pwd_fd >= 0)
close(parentkey_pwd_fd);
if (parentkey_pwd_file)
unlink(parentkey_pwd_file);
return ret;
}
static void usage(const char *prgname)
{
printf(
"Usage: %s [options]\n"
"\n"
"The following options are supported:\n"
"\n"
"--type type The type of certificate to create: 'ek' or 'platform'\n"
"--ek key-param The modulus of an RSA key or x=...,y=,... for an EC key\n"
"--dir directory The directory to write the resulting certificate into\n"
"--vmid vmid The ID of the virtual machine\n"
"--optsfile file A file containing options to pass to swtpm_cert\n"
"--configfile file A file containing configuration parameters for directory,\n"
" signing key and password and certificate to use\n"
"--logfile file A file to write a log into\n"
"--tpm-spec-family s The implemented spec family, e.g., '2.0'\n"
"--tpm-spec-revision i The spec revision of the TPM as integer; e.g., 146\n"
"--tpm-spec-level i The spec level of the TPM; must be an integer; e.g. 0\n"
"--tpm-manufacturer s The manufacturer of the TPM; e.g., id:00001014\n"
"--tpm-model s The model of the TPM; e.g., 'swtpm'\n"
"--tpm-version i The (firmware) version of the TPM; e.g., id:20160511\n"
"--tpm2 Generate a certificate for a TPM 2\n"
"--allow-signing The TPM 2's EK can be used for signing\n"
"--decryption The TPM 2's EK can be used for decryption\n"
"--help, -h, -? Display this help screen and exit\n"
"\n"
"\n"
"The following environment variables are supported:\n"
"\n"
"SWTPM_ROOTCA_PASSWORD The root CA's private key password\n"
"\n", prgname);
}
int main(int argc, char *argv[])
{
int opt, option_index = 0;
const static struct option long_options[] = {
{"type", required_argument, NULL, 't'},
{"ek", required_argument, NULL, 'e'},
{"dir", required_argument, NULL, 'd'},
{"vmid", required_argument, NULL, 'v'},
{"optsfile", required_argument, NULL, 'o'},
{"configfile", required_argument, NULL, 'c'},
{"logfile", required_argument, NULL, 'l'},
{"tpm-spec-family", required_argument, NULL, 'f'},
{"tpm-spec-revision", required_argument, NULL, 'r'},
{"tpm-spec-level", required_argument, NULL, '1'},
{"tpm-manufacturer", required_argument, NULL, 'a'},
{"tpm-model", required_argument, NULL, 'm'},
{"tpm-version", required_argument, NULL, 's'},
{"tpm2", no_argument, NULL, '2'},
{"allow-signing", no_argument, NULL, 'i'},
{"decryption", no_argument, NULL, 'y'},
{"help", no_argument, NULL, 'h'},
};
g_autofree gchar *default_options_file = NULL;
g_autofree gchar *default_config_file = NULL;
g_autofree gchar *optsfile = NULL;
g_autofree gchar *configfile = NULL;
unsigned long flags = 0;
g_autofree gchar *typ =g_strdup("");
g_autofree gchar *ekparams = g_strdup("");
g_autofree gchar *directory = g_strdup("."); /* default to current directory */
g_autofree gchar *vmid = NULL;
g_autofree gchar *lockfile = NULL;
g_autofree gchar *statedir = NULL;
g_autofree gchar *signkey = NULL;
g_autofree gchar *signkey_password = NULL;
g_autofree gchar *parentkey_password = NULL;
g_autofree gchar *issuercert = NULL;
g_autofree gchar *certserial = NULL;
gchar **tpm_spec_params = NULL;
gchar **tpm_attr_params = NULL;
gchar **config_file_lines = NULL;
gchar **swtpm_cert_env = NULL;
const struct passwd *curr_user;
struct stat statbuf;
int ret = 1;
if (init(&default_options_file, &default_config_file) < 0)
goto error;
optsfile = g_strdup(default_options_file);
configfile = g_strdup(default_config_file);
while ((opt = getopt_long(argc, argv, "h?",
long_options, &option_index)) != -1) {
switch (opt) {
case 't': /* --type */
g_free(typ);
typ = g_strdup(optarg);
break;
case 'e': /* --ek */
g_free(ekparams);
ekparams = g_strdup(optarg);
break;
case 'd': /* --dir */
g_free(directory);
directory = g_strdup(optarg);
break;
case 'v': /* --vmid */
g_free(vmid);
vmid = g_strdup(optarg);
break;
case 'o': /* --optsfile */
g_free(optsfile);
optsfile = g_strdup(optarg);
break;
case 'c': /* --configfile */
g_free(configfile);
configfile = g_strdup(optarg);
break;
case 'l': /* --logfile */
g_free(gl_LOGFILE);
gl_LOGFILE = g_strdup(optarg);
break;
case 'f': /* --tpm-spec-family */
case 'r': /* --tpm-spec-revision */
case '1': /* --tpm-spec-level */
tpm_spec_params = concat_arrays(tpm_spec_params,
(gchar *[]) {
g_strdup_printf("--%s", long_options[option_index].name), g_strdup(optarg), NULL
}, TRUE);
break;
case 'a': /* --tpm-manufacturer */
case 'm': /* --tpm-model */
case 's': /* --tpm-version */
tpm_attr_params = concat_arrays(tpm_attr_params,
(gchar *[]) {
g_strdup_printf("--%s", long_options[option_index].name), g_strdup(optarg), NULL
}, TRUE);
break;
case '2': /* --tpm2 */
flags |= SETUP_TPM2_F;
break;
case 'i': /* --allow-signing */
flags |= ALLOW_SIGNING_F;
break;
case 'y': /* --decryption */
flags |= DECRYPTION_F;
break;
case '?':
case 'h': /* --help */
usage(argv[0]);
ret = 0;
goto out;
default:
fprintf(stderr, "Unknown option code %d\n", opt);
usage(argv[0]);
goto error;
}
}
curr_user = getpwuid(getuid());
if (gl_LOGFILE != NULL) {
FILE *tmpfile;
if (stat(gl_LOGFILE, &statbuf) == 0 &&
(statbuf.st_mode & S_IFMT) == S_IFLNK) {
fprintf(stderr, "Logfile must not be a symlink.\n");
goto error;
}
tmpfile = fopen(gl_LOGFILE, "a"); // do not truncate
if (tmpfile == NULL) {
fprintf(stderr, "Cannot write to logfile %s.\n", gl_LOGFILE);
goto error;
}
fclose(tmpfile);
}
if (access(optsfile, R_OK) != 0) {
logerr(gl_LOGFILE, "Need read rights on options file %s for user %s.\n",
optsfile, curr_user ? curr_user->pw_name : "<unknown>");
goto error;
}
if (access(configfile, R_OK) != 0) {
logerr(gl_LOGFILE, "Need read rights on config file %s for user %s.\n",
configfile, curr_user ? curr_user->pw_name : "<unknown>");
goto error;
}
if (read_file_lines(configfile, &config_file_lines) != 0)
goto error;
statedir = get_config_value(config_file_lines, "statedir", NULL);
if (statedir == NULL) {
logerr(gl_LOGFILE, "Missing 'statedir' config value in config file %s.\n", configfile);
goto error;
}
if (makedir(statedir, "statedir") != 0)
goto error;
if (access(statedir, W_OK | R_OK) != 0) {
logerr(gl_LOGFILE, "Need read/write rights on statedir %s for user %s.\n",
statedir, curr_user ? curr_user->pw_name : "<unknown>");
goto error;
}
lockfile = g_strjoin(G_DIR_SEPARATOR_S, statedir, ".lock.swtpm-localca", NULL);
if (stat(lockfile, &statbuf) == 0 &&
access(lockfile, W_OK | R_OK) != 0) {
logerr(gl_LOGFILE, "Need read/write rights on %s for user %s.\n",
lockfile, curr_user ? curr_user->pw_name : "<unknown>");
goto error;
}
signkey = get_config_value(config_file_lines, "signingkey", NULL);
if (signkey == NULL) {
logerr(gl_LOGFILE, "Missing 'signingkey' config value in config file %s.\n",
configfile);
goto error;
}
if (!g_str_has_prefix(signkey, "tpmkey:file=") &&
!g_str_has_prefix(signkey, "tpmkey:uuid=") &&
!g_str_has_prefix(signkey, "pkcs11:")) {
g_autofree gchar *d = g_path_get_dirname(signkey);
if (makedir(d, "signkey") != 0)
goto error;
}
signkey_password = get_config_value(config_file_lines, "signingkey_password", NULL);
parentkey_password = get_config_value(config_file_lines, "parentkey_password", NULL);
issuercert = get_config_value(config_file_lines, "issuercert", NULL);
if (issuercert == NULL) {
logerr(gl_LOGFILE, "Missing 'issuercert' config value in config file %s.\n", configfile);
goto error;
}
{
g_autofree gchar *d = g_path_get_dirname(issuercert);
if (makedir(d, "issuercert") != 0)
goto error;
}
swtpm_cert_env = g_get_environ();
// TPM keys are GNUTLS URIs...
if (g_str_has_prefix(signkey, "tpmkey:file=") || g_str_has_prefix(signkey, "tpmkey:uuid=")) {
gchar *tss_tcsd_hostname = get_config_value(config_file_lines,
"TSS_TCSD_HOSTNAME", "localhost");
gchar *tss_tcsd_port = get_config_value(config_file_lines,
"TSS_TCSD_PORT", "30003");
swtpm_cert_env = g_environ_setenv(swtpm_cert_env,
"TSS_TCSD_HOSTNAME", tss_tcsd_hostname, TRUE);
swtpm_cert_env = g_environ_setenv(swtpm_cert_env,
"TSS_TCSD_PORT", tss_tcsd_port, TRUE);
logit(gl_LOGFILE, "CA uses a GnuTLS TPM key; using TSS_TCSD_HOSTNAME=%s " \
"TSS_TCSD_PORT=%s\n", tss_tcsd_hostname, tss_tcsd_port);
} else if (g_str_has_prefix(signkey, "pkcs11:")) {
gchar *tmp = str_replace(signkey, "\\;", ";"); /* historical reasons ... */
g_free(signkey);
signkey = tmp;
if (signkey_password != NULL) {
swtpm_cert_env = g_environ_setenv(swtpm_cert_env,
"SWTPM_PKCS11_PIN", g_strdup(signkey_password), TRUE);
logit(gl_LOGFILE, "CA uses a PKCS#11 key; using SWTPM_PKCSS1_PIN\n");
}
ret = get_config_envvars(config_file_lines, &swtpm_cert_env);
if (ret != 0)
goto error;
} else {
if (access(signkey, R_OK) != 0) {
if (stat(signkey, &statbuf) == 0) {
logerr(gl_LOGFILE, "Need read rights on signing key %s for user %s.\n",
signkey, curr_user ? curr_user->pw_name : "<unknown>");
goto error;
}
logit(gl_LOGFILE, "Creating root CA and a local CA's signing key and issuer cert.\n");
if (create_localca_cert(lockfile, statedir, signkey, signkey_password,
issuercert) != 0) {
logerr(gl_LOGFILE, "Error creating local CA's signing key and cert.\n");
goto error;
}
if (access(signkey, R_OK) != 0) {
logerr(gl_LOGFILE, "Need read rights on signing key %s for user %s.\n",
signkey, curr_user ? curr_user->pw_name : "<unknown>");
goto error;
}
}
}
if (access(issuercert, R_OK) != 0) {
logerr(gl_LOGFILE, "Need read rights on issuer certificate %s for user %s.\n",
issuercert, curr_user ? curr_user->pw_name : "<unknown>");
goto error;
}
{
g_autofree gchar *d = NULL;
g_autofree gchar *p = g_strjoin(G_DIR_SEPARATOR_S, statedir, "certserial", NULL);
certserial = get_config_value(config_file_lines, "certserial", p);
d = g_path_get_dirname(certserial);
if (makedir(d, "certserial") != 0)
goto error;
}
ret = create_cert(flags, typ, directory, ekparams, vmid, tpm_spec_params, tpm_attr_params,
signkey, signkey_password, issuercert, parentkey_password, swtpm_cert_env,
certserial, lockfile, optsfile);
out:
error:
g_strfreev(tpm_attr_params);
g_strfreev(tpm_spec_params);
exit(ret);
}

17
samples/swtpm_localca.h Normal file
View File

@ -0,0 +1,17 @@
/* SPDX-License-Identifier: BSD-3-Clause */
/*
* swtpm_localca.h: Header file
*
* Author: Stefan Berger, stefanb@linux.ibm.com
*
* Copyright (c) IBM Corporation, 2021
*/
#ifndef SWTPM_LOCALCA_H
#define SWTPM_LOCALCA_H
#include <glib.h>
extern gchar *gl_LOGFILE;
#endif /* SWTPM_LOCALCA_H */

View File

@ -0,0 +1,15 @@
/* SPDX-License-Identifier: BSD-3-Clause */
/*
* swtpm_setup_conf.h: Compile-time constants
*
* Author: Stefan Berger, stefanb@linux.ibm.com
*
* Copyright (c) IBM Corporation, 2021
*/
#ifndef SWTPM_LOCALCA_CONF_H
#define SWTPM_LOCALCA_CONF_H
#define SYSCONFDIR "@SYSCONFDIR@"
#endif /* SWTPM_LOCALCA_CONF_H */

View File

@ -0,0 +1,139 @@
/* SPDX-License-Identifier: BSD-3-Clause */
/*
* swtpm_localca_utils.c: Utility functions
*
* Author: Stefan Berger, stefanb@linux.ibm.com
*
* Copyright (c) IBM Corporation, 2021
*/
#include "config.h"
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <regex.h>
#include <errno.h>
#include <sys/file.h>
#include "swtpm_utils.h"
#include "swtpm_localca.h"
#include "swtpm_localca_utils.h"
/* Create a directory pat (and all its predecessors) if it doesn't exist */
int makedir(const char *dirname, const char *purpose)
{
struct stat statbuf;
if (stat(dirname, &statbuf) != 0) {
logit(gl_LOGFILE, "Creating swtpm-localca dir '%s'.\n", dirname);
if (g_mkdir_with_parents(dirname, S_IRWXU | S_IRWXG | S_IXGRP | S_IRGRP) == -1) {
logerr(gl_LOGFILE, "Could not create directory for '%s': %s\n",
purpose, strerror(errno));
return 1;
}
}
return 0;
}
/* Get a configuration value given its name */
gchar *get_config_value(gchar **config_file_lines, const gchar *configname, const gchar *fallback)
{
g_autofree gchar *regex = g_strdup_printf("^%s[[:space:]]*=[[:space:]]*([^#\n]*).*", configname);
gchar *result = NULL;
regex_t preg;
size_t idx;
regmatch_t pmatch[2];
if (regcomp(&preg, regex, REG_EXTENDED) != 0) {
logerr(gl_LOGFILE, "Internal error: Could not compile regex\n");
goto error;
}
for (idx = 0; config_file_lines[idx] != NULL; idx++) {
const gchar *line = config_file_lines[idx];
if (regexec(&preg, line, 2, pmatch, 0) == 0) {
result = g_strndup(&line[pmatch[1].rm_so],
pmatch[1].rm_eo - pmatch[1].rm_so);
/* coverity: g_strchmop modifies in-place */
result = g_strchomp(result);
break;
}
}
regfree(&preg);
error:
if (result == NULL)
result = g_strdup(fallback);
//printf("Found match for %s: |%s|\n", configname, result);
return result;
}
/* Extract all environment variables from the config file and add them to
* the given environent.
* Environment variable lines must start with 'env:' and must not contain
* trailing spaces or a comment starting with '#'
*/
int get_config_envvars(gchar **config_file_lines, gchar ***env)
{
const char *regex = "^env:([a-zA-Z_][a-zA-Z_0-9]*)[[:space:]]*=[[:space:]]*([^\n]*)";
regex_t preg;
size_t idx;
regmatch_t pmatch[3];
if (regcomp(&preg, regex, REG_EXTENDED) != 0) {
logerr(gl_LOGFILE, "Internal error: Could not compile regex\n");
return 1;
}
for (idx = 0; config_file_lines[idx] != NULL; idx++) {
const gchar *line = config_file_lines[idx];
if (regexec(&preg, line, 3, pmatch, 0) == 0) {
g_autofree gchar *key, *value;
key = g_strndup(&line[pmatch[1].rm_so],
pmatch[1].rm_eo - pmatch[1].rm_so);
value = g_strndup(&line[pmatch[2].rm_so],
pmatch[2].rm_eo - pmatch[2].rm_so);
*env = g_environ_setenv(*env, key, value, TRUE);
}
}
regfree(&preg);
return 0;
}
/* flock a file; the file descriptor for the file to unlock later on is returned */
int lock_file(const gchar *lockfile)
{
int lockfd;
mode_t mode = S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH;
lockfd = open(lockfile, O_RDWR | O_CREAT, mode);
if (lockfd < 0) {
logerr(gl_LOGFILE, "Could not open lockfile %s: %s\n", lockfile, strerror(errno));
return -1;
}
if (flock(lockfd, LOCK_EX) < 0) {
logerr(gl_LOGFILE, "Could not lock file %s: %s\n", lockfile, strerror(errno));
close(lockfd);
return -1;
}
return lockfd;
}
/* unlock a file previously locked using lock_file */
void unlock_file(int lockfd) {
if (lockfd >= 0) {
flock(lockfd, LOCK_UN);
close(lockfd);
}
}

View File

@ -0,0 +1,23 @@
/* SPDX-License-Identifier: BSD-3-Clause */
/*
* swtpm_localca_utils.h: Header file for swtpm_localca_utils.c
*
* Author: Stefan Berger, stefanb@linux.ibm.com
*
* Copyright (c) IBM Corporation, 2021
*/
#ifndef SWTPM_LOCALCA_UTILS_H
#define SWTPM_LOCALCA_UTILS_H
#include <glib.h>
gchar *get_config_value(gchar **config_file_lines, const gchar *varname, const gchar *fallback);
int get_config_envvars(gchar **config_file_lines, gchar ***env);
int makedir(const char *dirname, const char *purpose);
int lock_file(const gchar *lockfile);
void unlock_file(int lockfd);
#endif /* SWTPM_LOCALCA_UTILS_H */

View File

@ -321,3 +321,106 @@ int read_file_lines(const char *filename, gchar ***config_file_lines)
return 0;
}
static ssize_t write_to_fd(int fd, const unsigned char *data, size_t data_len)
{
ssize_t n;
n = write(fd, data, data_len);
if (n < 0) {
logerr(gl_LOGFILE, "Could not write to file: %s\n", strerror(errno));
} else if ((size_t)n != data_len) {
logerr(gl_LOGFILE, "Could not write all bytes to the file.\n");
n = -1;
}
return n;
}
/* Write to a file with the given name */
int write_file(const gchar *filename, const unsigned char *data, size_t data_len)
{
ssize_t n;
int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC,
S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH );
if (fd < 0) {
logerr(gl_LOGFILE, "Could not open file %s for writing: %s\n",
filename, strerror(errno));
return 1;
}
n = write_to_fd(fd, data, data_len);
close(fd);
if (n < 0)
return 1;
return 0;
}
int write_to_tempfile(gchar **filename, const unsigned char *data, size_t data_len)
{
GError *error = NULL;
int fd = g_file_open_tmp("XXXXXX", filename, &error);
ssize_t n;
if (error) {
logerr(gl_LOGFILE, "Could not create temporary file: %s\n", error->message);
g_error_free(error);
return 1;
}
n = write_to_fd(fd, data, data_len);
if (n < 0)
goto error;
n = lseek(fd, 0, SEEK_SET);
if (n < 0) {
logerr(gl_LOGFILE, "Could not seek(0) on file '%s': %s\n", filename, strerror(errno));
goto error;
}
return fd;
error:
close(fd);
return -1;
}
/* replace occurences of 'torep' with 'rep' in a string 'in' */
gchar *str_replace(const char *in, const char *torep, const char *rep)
{
char *res;
const char *s, *b;
size_t torep_len;
size_t rep_len;
size_t ctr = 0;
size_t off = 0;
if (in == NULL || torep == NULL || rep == NULL)
return NULL;
torep_len = strlen(torep);
if (torep_len == 0)
return NULL;
rep_len = strlen(rep);
s = in;
while ((s = strstr(s, torep)) != NULL) {
s += torep_len;
ctr++;
}
res = g_malloc(strlen(in) - ctr * torep_len + ctr * rep_len + 1);
b = s = in;
while ((s = strstr(s, torep)) != NULL) {
strncpy(&res[off], b, s - b);
off += (s - b);
s += torep_len;
b = s;
strcpy(&res[off], rep);
off += rep_len;
}
strcpy(&res[off], b);
return res;
}

View File

@ -33,4 +33,9 @@ gchar **split_cmdline(const gchar *cmdline);
int read_file(const gchar *filename, gchar **buffer, gsize *buffer_len);
int read_file_lines(const char *filename, gchar ***config_file_lines);
int write_file(const gchar *filename, const unsigned char *data, size_t data_len);
int write_to_tempfile(gchar **filename, const unsigned char *data, size_t data_len);
gchar *str_replace(const char *in, const char *torep, const char *rep);
#endif /* SWTPM_UTILS_H */