From 30ddadd954addb51792f83d7745a61294a7e0beb Mon Sep 17 00:00:00 2001
From: Jan Dittberner <jandd@cacert.org>
Date: Tue, 22 Dec 2020 08:49:18 +0100
Subject: [PATCH] Add test mgr setup

---
 README.md                          |   7 ++
 application.Dockerfile             |   7 +-
 docker-compose.yml                 |  19 +++
 docker/apache-foreground           |  10 --
 docker/apache-mgr-foreground       |  17 +++
 docker/apache-mgr-virtualhost.conf |  21 ++++
 docker/initdb.sh                   |  19 ++-
 docker/mgr-application.ini         |  57 +++++++++
 docker/run-dovecot                 |   4 +
 docker/run-postfix                 |   2 +-
 mail.Dockerfile                    |  23 ++++
 mariadb.Dockerfile                 |   1 +
 mgr.Dockerfile                     |  44 +++++++
 setup_test_ca.sh                   | 178 +++++++++++++++++++++--------
 14 files changed, 345 insertions(+), 64 deletions(-)
 create mode 100755 docker/apache-mgr-foreground
 create mode 100644 docker/apache-mgr-virtualhost.conf
 create mode 100644 docker/mgr-application.ini
 create mode 100755 docker/run-dovecot
 create mode 100644 mail.Dockerfile
 create mode 100644 mgr.Dockerfile

diff --git a/README.md b/README.md
index 10a7a11..026d2f2 100644
--- a/README.md
+++ b/README.md
@@ -35,6 +35,9 @@ Variable | Usage
 `MYSQL_ROOT_PASSWORD` | Database root password
 `MYSQL_APP_USER` | Database application user
 `MYSQL_APP_PASSWORD` | Database application password
+`CLIENT_CERT_EMAIL` | email address for client certificate generated by `setup_test_ca.sh`
+`CLIENT_CERT_USERNAME` | user name for client certificate generated by `setup_test_ca.sh`
+`CLIENT_CERT_PASSWORD` | PKCS#12 keystore password for client certificate generated by `setup_test_ca.sh`
 
 ```shell
 echo -e "MYSQL_ROOT_PASSWORD=$(openssl rand -base64 18)\nMYSQL_APP_USER=cacert_dev\nMYSQL_APP_PASSWORD=$(openssl rand -base64 18)" > .env
@@ -43,3 +46,7 @@ docker-compose up
 ```
 
 After these steps you should be able to reach the CAcert application at https://test.cacert.localhost:8443/.
+The test manager application is reachable at https://mgr.cacert.localhost:9443/.
+
+A client certificate is created by `setup_test_ca.sh` and is placed in `testca/certs/clientcert.p12`
+which can be imported in a browser to support client certificate authentication.
diff --git a/application.Dockerfile b/application.Dockerfile
index f870bb7..89583f3 100644
--- a/application.Dockerfile
+++ b/application.Dockerfile
@@ -40,7 +40,12 @@ RUN apt-get update \
 STOPSIGNAL SIGWINCH
 
 COPY docker/apache-foreground /usr/local/bin/
-COPY testca/ /usr/local/etc/testca/
+COPY testca/root/ca.crt.pem /usr/local/share/ca-certificates/testca_root.crt
+COPY testca/class3/ca.crt.pem /usr/local/share/ca-certificates/testca_class3.crt
+COPY testca/certs/test.cacert.localhost.crt.pem testca/certs/secure.test.cacert.localhost.crt.pem /etc/ssl/certs/
+COPY testca/certs/test.cacert.localhost.key.pem testca/certs/secure.test.cacert.localhost.key.pem /etc/ssl/private/
+COPY testca/certs/cachain.crt.pem /etc/ssl/certs/combined.crt
+
 COPY docker/apache-virtualhost.conf /etc/apache2/sites-available/
 COPY docker/cacert.conf /etc/apache2/conf-available/
 COPY docker/php5-cacert.ini /etc/php5/mods-available/cacert.ini
diff --git a/docker-compose.yml b/docker-compose.yml
index 5b0cf8b..6151567 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -19,6 +19,12 @@ services:
       dockerfile: smtp.Dockerfile
     volumes:
       - maildir:/home/catchall/Maildir
+  mail:
+    build:
+      context: .
+      dockerfile: mail.Dockerfile
+    volumes:
+      - maildir:/home/catchall/Maildir
   application:
     build:
       context: .
@@ -46,6 +52,19 @@ services:
       - smtp
     volumes:
       - ./cacert-software:/www
+  mgr:
+    build:
+      context: .
+      dockerfile: mgr.Dockerfile
+    env_file:
+      - ./.env
+    ports:
+      - "9443:443"
+    depends_on:
+      - db
+      - mail
+    volumes:
+      - ./cacert-mgr:/var/www
 
 volumes:
   db: { }
diff --git a/docker/apache-foreground b/docker/apache-foreground
index e0c24d9..f83ae5a 100755
--- a/docker/apache-foreground
+++ b/docker/apache-foreground
@@ -4,16 +4,6 @@ set -eux
 # Apache gets grumpy about PID files pre-existing
 rm -f /run/apache2/apache2.pid
 
-cp /usr/local/etc/testca/certs/test.cacert.localhost.crt.pem /etc/ssl/certs/
-cp /usr/local/etc/testca/certs/test.cacert.localhost.key.pem /etc/ssl/private/
-(
-  openssl x509 -in /usr/local/etc/testca/class3/ca.crt.pem
-  openssl x509 -in /usr/local/etc/testca/root/ca.crt.pem
-) >/etc/ssl/certs/combined.crt
-
-cp /usr/local/etc/testca/certs/secure.test.cacert.localhost.crt.pem /etc/ssl/certs/
-cp /usr/local/etc/testca/certs/secure.test.cacert.localhost.key.pem /etc/ssl/private/
-
 cp /usr/local/etc/application/feed.rss /www/pages/index/feed.rss
 
 make -C /www/locale
diff --git a/docker/apache-mgr-foreground b/docker/apache-mgr-foreground
new file mode 100755
index 0000000..e7dc445
--- /dev/null
+++ b/docker/apache-mgr-foreground
@@ -0,0 +1,17 @@
+#!/bin/sh
+set -eux
+
+# Apache gets grumpy about PID files pre-existing
+rm -f /run/apache2/apache2.pid
+
+sed "s/@MYSQL_MGR_USER@/${MYSQL_MGR_USER}/g; s/@MYSQL_MGR_PASSWORD@/${MYSQL_MGR_PASSWORD}/g" \
+  /usr/local/etc/mgr-application.ini > /var/www/manager/application/configs/application.ini
+
+mysql -u "${MYSQL_MGR_USER}" -h db "-p${MYSQL_MGR_PASSWORD}" mgr <<-EOF
+REPLACE INTO system_user (id, system_role_id, login, user_client_crt_s_dn_i_dn)
+VALUES (2, 2,'${CLIENT_CERT_EMAIL}','/CN=${CLIENT_CERT_USERNAME}///C=AU/O=CAcert Inc./CN=Class 3 Test CA');
+EOF
+
+apache2ctl start "$@"
+
+exec tail -F --follow=name --retry /var/log/apache2/error.log
diff --git a/docker/apache-mgr-virtualhost.conf b/docker/apache-mgr-virtualhost.conf
new file mode 100644
index 0000000..707958b
--- /dev/null
+++ b/docker/apache-mgr-virtualhost.conf
@@ -0,0 +1,21 @@
+<VirtualHost *:443>
+  ServerName mgr.cacert.localhost
+  ServerAlias www.mgr.cacert.localhost
+  DocumentRoot /var/www/manager/public
+
+  SSLEngine on
+  SSLStrictSNIVHostCheck on
+  SSLProtocol all -SSLv2 -SSLv3 -TLSv1
+  SSLHonorCipherOrder on
+  SSLCipherSuite kEECDH:kEDH:AESGCM:ALL:!3DES!RC4:!LOW:!EXP:!MD5:!aNULL:!eNULL
+  SSLCertificateFile /etc/ssl/certs/mgr.cacert.localhost.crt.pem
+  SSLCertificateKeyFile /etc/ssl/private/mgr.cacert.localhost.key.pem
+  SSLCertificateChainFile /etc/ssl/certs/combined.crt
+
+  SSLCACertificateFile /etc/ssl/certs/combined.crt
+  SSLVerifyClient require
+  SSLVerifyDepth 2
+  SSLOptions +StdEnvVars
+
+  Header always set Strict-Transport-Security "max-age=31536000"
+</VirtualHost>
diff --git a/docker/initdb.sh b/docker/initdb.sh
index 4a1cc21..83b34dd 100755
--- a/docker/initdb.sh
+++ b/docker/initdb.sh
@@ -4,15 +4,15 @@ set -eux
 
 mysql -h localhost -u root "-p$MYSQL_ROOT_PASSWORD" <<-EOF
 CREATE database cacert CHARSET latin1 COLLATE latin1_swedish_ci;
-CREATE USER $MYSQL_APP_USER@'%' IDENTIFIED BY '$MYSQL_APP_PASSWORD';
-GRANT CREATE TEMPORARY TABLES ON cacert.* TO $MYSQL_APP_USER@'%';
-GRANT SELECT, INSERT, UPDATE, DELETE ON cacert.* TO $MYSQL_APP_USER@'%';
+CREATE database mgr CHARSET utf8 COLLATE utf8_unicode_ci;
 EOF
 
 for script in /db_migrations/*.sh; do
   sh "$script" -h localhost -u root "-p$MYSQL_ROOT_PASSWORD" cacert
 done
 
+mysql -h localhost -u root "-p$MYSQL_ROOT_PASSWORD" mgr </mgr_dbadm/ca_mgr.mysql
+
 mysql -h localhost -u root "-p$MYSQL_ROOT_PASSWORD" cacert <<-'EOF'
 INSERT INTO languages (locale, en_co, en_lang, country, lang)
 VALUES  ('sq_AL', 'Albania', 'Albanian', 'Shqip&euml;ria', 'shqipe'),
@@ -114,4 +114,15 @@ VALUES  ('sq_AL', 'Albania', 'Albanian', 'Shqip&euml;ria', 'shqipe'),
         ('es_VE', 'Venezuela', 'Spanish', 'Venezuela', 'Espa&ntilde;ol'),
         ('vi_VN', 'Vietnam', 'Vietnamese', 'Vi&#7879;t Nam', 'Ti&#7875;ng Vi&#7879;t'),
         ('ar_YE', 'Yemen', 'Arabic', '&#65254;&#65252;&#65268;&#65248;&#65165;', '&#65172;&#65268;&#65168;&#65198;&#65228;&#65248;&#65165;');
-EOF
\ No newline at end of file
+EOF
+
+mysql -h localhost -u root "-p$MYSQL_ROOT_PASSWORD" <<-EOF
+CREATE USER $MYSQL_APP_USER@'%' IDENTIFIED BY '$MYSQL_APP_PASSWORD';
+GRANT CREATE TEMPORARY TABLES ON cacert.* TO $MYSQL_APP_USER@'%';
+GRANT SELECT, INSERT, UPDATE, DELETE ON cacert.* TO $MYSQL_APP_USER@'%';
+
+CREATE USER $MYSQL_MGR_USER@'%' IDENTIFIED BY '$MYSQL_MGR_PASSWORD';
+GRANT CREATE TEMPORARY TABLES ON mgr.* TO $MYSQL_MGR_USER@'%';
+GRANT SELECT, INSERT, UPDATE, DELETE ON mgr.* TO $MYSQL_MGR_USER@'%';
+GRANT SELECT, INSERT, UPDATE, DELETE ON cacert.users TO $MYSQL_MGR_USER@'%';
+EOF
diff --git a/docker/mgr-application.ini b/docker/mgr-application.ini
new file mode 100644
index 0000000..bb2133a
--- /dev/null
+++ b/docker/mgr-application.ini
@@ -0,0 +1,57 @@
+[production]
+phpSettings.display_startup_errors = 1
+phpSettings.display_errors = 1
+includePaths.library = LIBRARY_PATH
+bootstrap.path = APPLICATION_PATH "/Bootstrap.php"
+bootstrap.class = "Bootstrap"
+resources.frontController.controllerDirectory = APPLICATION_PATH "/controllers"
+resources.frontController.noViewRenderer = 0
+resources.frontController.noErrorHandler = 0
+resources.frontController.useDefaultControllerAlways = 0
+resources.layout.layoutPath = APPLICATION_PATH "/layouts/scripts"
+resources.view[] =
+autoloadernamespaces.0 = "CAcert_"
+
+; Database settings for Session DB
+ca_mgr.db.session.pdo               = "Pdo_Mysql"
+ca_mgr.db.session.autocleanup       = 1
+ca_mgr.db.session.host              = "db"
+ca_mgr.db.session.username          = "@MYSQL_MGR_USER@"
+ca_mgr.db.session.password          = "@MYSQL_MGR_PASSWORD@"
+ca_mgr.db.session.dbname            = "mgr"
+
+; Database settings for Auth DB (CACert User Table)
+ca_mgr.db.auth.pdo                  = "Pdo_Mysql"
+ca_mgr.db.auth.host                 = "db"
+ca_mgr.db.auth.username             = "@MYSQL_MGR_USER@"
+ca_mgr.db.auth.password             = "@MYSQL_MGR_PASSWORD@"
+ca_mgr.db.auth.dbname               = "cacert"
+ca_mgr.db.auth.tablename            = "users"
+
+; Database settings for Auth DB (Manager User Table)
+ca_mgr.db.auth2.pdo                 = "Pdo_Mysql"
+ca_mgr.db.auth2.host                = "db"
+ca_mgr.db.auth2.username            = "@MYSQL_MGR_USER@"
+ca_mgr.db.auth2.password            = "@MYSQL_MGR_PASSWORD@"
+ca_mgr.db.auth2.dbname              = "mgr"
+ca_mgr.db.auth2.tablename           = "system_user"
+
+; Database settings for Config DB (access to system_config and dnssecme data tables)
+ca_mgr.db.config.pdo                = "Pdo_Mysql"
+ca_mgr.db.config.host               = "db"
+ca_mgr.db.config.username           = "@MYSQL_MGR_USER@"
+ca_mgr.db.config.password           = "@MYSQL_MGR_PASSWORD@"
+ca_mgr.db.config.dbname             = "mgr"
+
+; Application name for logger
+log.application                     = "web"
+
+[staging : production]
+
+[testing : production]
+phpSettings.display_startup_errors = 1
+phpSettings.display_errors = 1
+
+[development : production]
+phpSettings.display_startup_errors = 1
+phpSettings.display_errors = 1
diff --git a/docker/run-dovecot b/docker/run-dovecot
new file mode 100755
index 0000000..29fd5b5
--- /dev/null
+++ b/docker/run-dovecot
@@ -0,0 +1,4 @@
+#!/bin/sh
+set -eu
+
+dovecot -F
diff --git a/docker/run-postfix b/docker/run-postfix
index 4d80df2..b9836f8 100755
--- a/docker/run-postfix
+++ b/docker/run-postfix
@@ -4,4 +4,4 @@ set -eu
 mkdir -p /home/catchall/Maildir/tmp /home/catchall/Maildir/new /home/catchall/Maildir/cur
 chown -Rc catchall.catchall /home/catchall/Maildir
 
-postfix start-fg
\ No newline at end of file
+postfix start-fg
diff --git a/mail.Dockerfile b/mail.Dockerfile
new file mode 100644
index 0000000..3ff9393
--- /dev/null
+++ b/mail.Dockerfile
@@ -0,0 +1,23 @@
+FROM debian:buster
+
+RUN apt-get update \
+    && DEBIAN_FRONTEND=noninteractive \
+    apt-get install -y --no-install-recommends \
+    ca-certificates \
+    curl \
+    dovecot-imapd \
+    dumb-init \
+    mutt \
+    psmisc \
+    && apt-get clean \
+    && rm -rf /var/lib/apt/lists/*
+
+EXPOSE 143
+
+RUN adduser --uid 1000 --gecos "catchall mailbox" --disabled-password catchall
+
+VOLUME /home/catchall/Maildir
+
+COPY docker/run-dovecot /usr/local/bin/run-dovecot
+
+CMD ["dumb-init", "/usr/local/bin/run-dovecot"]
\ No newline at end of file
diff --git a/mariadb.Dockerfile b/mariadb.Dockerfile
index 6537a50..695c5ab 100644
--- a/mariadb.Dockerfile
+++ b/mariadb.Dockerfile
@@ -2,3 +2,4 @@ FROM mariadb:focal
 
 COPY docker/initdb.sh /docker-entrypoint-initdb.d/initdb.sh
 COPY cacert-software/scripts/db_migrations/*.sh /db_migrations/
+COPY cacert-mgr/dbadm/ca_mgr.mysql /mgr_dbadm/
\ No newline at end of file
diff --git a/mgr.Dockerfile b/mgr.Dockerfile
new file mode 100644
index 0000000..a583678
--- /dev/null
+++ b/mgr.Dockerfile
@@ -0,0 +1,44 @@
+FROM debian:jessie
+
+RUN apt-get update \
+    && DEBIAN_FRONTEND=noninteractive \
+    apt-get install -y --no-install-recommends \
+    ca-certificates \
+    curl \
+    gettext \
+    libapache2-mod-php5 \
+    locales-all \
+    mariadb-client \
+    nullmailer \
+    php5-mysql \
+    zendframework \
+    && apt-get clean \
+    && rm -rf /var/lib/apt/lists/*
+
+STOPSIGNAL SIGWINCH
+
+COPY docker/apache-mgr-foreground /usr/local/bin/
+COPY testca/root/ca.crt.pem /usr/local/share/ca-certificates/testca_root.crt
+COPY testca/class3/ca.crt.pem /usr/local/share/ca-certificates/testca_class3.crt
+COPY testca/certs/mgr.cacert.localhost.crt.pem /etc/ssl/certs/
+COPY testca/certs/mgr.cacert.localhost.key.pem /etc/ssl/private/
+COPY testca/certs/cachain.crt.pem /etc/ssl/certs/combined.crt
+COPY testca/class3/ca.crt.pem /etc/ssl/certs/clientca.crt
+
+COPY docker/apache-mgr-virtualhost.conf /etc/apache2/sites-available/mgr.cacert.localhost.conf
+COPY docker/mgr-application.ini /usr/local/etc/
+
+VOLUME /var/www
+
+RUN a2ensite mgr.cacert.localhost ; \
+    a2dissite 000-default ; \
+    a2enmod headers ; \
+    a2enmod ssl ; \
+    cd /usr/local/share/ca-certificates ; \
+    curl -O http://www.cacert.org/certs/root_X0F.crt ; \
+    curl -O http://www.cacert.org/certs/class3_X0E.crt ; \
+    update-ca-certificates
+
+EXPOSE 443
+
+CMD ["/usr/local/bin/apache-mgr-foreground"]
diff --git a/setup_test_ca.sh b/setup_test_ca.sh
index b6c3578..40ea03d 100755
--- a/setup_test_ca.sh
+++ b/setup_test_ca.sh
@@ -1,19 +1,20 @@
 #!/bin/sh
 
-set -eux
-
-if [ -d testca/ ]; then
-  echo "testca/ exists, remove it if you want to start from scratch"
-  exit 1
-fi
+set -eu
 
 ORGANIZATION="CAcert Inc."
 COUNTRY_CODE="AU"
+. ./.env
+
+if [ ! -d testca/ ]; then
+  mkdir -p testca/
+  cd testca
+  mkdir -p root/newcerts class3/newcerts root/private class3/private certs
+  touch root/index.txt class3/index.txt
+else
+  cd testca
+fi
 
-mkdir -p testca/
-cd testca
-mkdir -p root/newcerts class3/newcerts root/private class3/private certs
-touch root/index.txt class3/index.txt
 cat >ca.cnf <<EOF
 [ca]
 default_ca             = class3_ca
@@ -100,6 +101,16 @@ authorityInfoAccess    = 1.3.6.1.5.5.7.48.2;URI:http://test.cacert.localhost/ca/
 crlDistributionPoints  = URI:http://crl.test.cacert.localhost/class3.crl
 certificatePolicies    = @policy_class3_ca
 
+[client_ext]
+basicConstraints       = critical,CA:false
+keyUsage               = digitalSignature,keyEncipherment
+extendedKeyUsage       = clientAuth
+subjectKeyIdentifier   = hash
+authorityKeyIdentifier = keyid:always
+authorityInfoAccess    = 1.3.6.1.5.5.7.48.2;URI:http://test.cacert.localhost/ca/class3/ca.crt,OCSP;URI:http://ocsp.test.cacert.localhost/
+crlDistributionPoints  = URI:http://crl.test.cacert.localhost/class3.crl
+certificatePolicies    = @policy_class3_ca
+
 [server_ext]
 basicConstraints       = critical,CA:false
 keyUsage               = digitalSignature,keyEncipherment
@@ -115,41 +126,112 @@ policyIdentifier       = 1.3.6.1.5.5.7.2.1
 CPS                    = http://test.cacert.localhost/ca/class3/cps.html
 EOF
 
-openssl req -new -x509 -config ca.cnf \
-  -keyout root/private/ca.key.pem \
-  -nodes \
-  -subj "/CN=Test Root/C=${COUNTRY_CODE}/O=${ORGANIZATION}" \
-  -days 3650 \
-  -extensions root_extensions \
-  -out root/ca.crt.pem
-openssl req -new -config ca.cnf \
-  -keyout class3/private/ca.key.pem \
-  -nodes \
-  -subj "/CN=Class 3 Test CA/C=${COUNTRY_CODE}/O=${ORGANIZATION}" \
-  -out class3/ca.csr.pem
-openssl ca -config ca.cnf \
-  -name root_ca \
-  -in class3/ca.csr.pem -out class3/ca.crt.pem \
-  -rand_serial \
-  -extensions class3_extensions \
-  -batch
-openssl req -new -keyout certs/test.cacert.localhost.key.pem -nodes \
-  -out certs/test.cacert.localhost.csr.pem -subj "/CN=test.cacert.localhost" \
-  -addext "subjectAltName=DNS:test.cacert.localhost,DNS:www.test.cacert.localhost"
-openssl req -new -keyout certs/secure.test.cacert.localhost.key.pem -nodes \
-  -out certs/secure.test.cacert.localhost.csr.pem -subj "/CN=secure.test.cacert.localhost" \
-  -addext "subjectAltName=DNS:secure.test.cacert.localhost"
-openssl ca -config ca.cnf \
-  -name class3_ca \
-  -in certs/test.cacert.localhost.csr.pem \
-  -out certs/test.cacert.localhost.crt.pem \
-  -rand_serial \
-  -extensions server_ext \
-  -batch
-openssl ca -config ca.cnf \
-  -name class3_ca \
-  -in certs/secure.test.cacert.localhost.csr.pem \
-  -out certs/secure.test.cacert.localhost.crt.pem \
-  -rand_serial \
-  -extensions server_ext \
-  -batch
+if [ ! -f root/ca.crt.pem ]; then
+  openssl req -new -x509 -config ca.cnf \
+    -keyout root/private/ca.key.pem \
+    -nodes \
+    -subj "/CN=Test Root/C=${COUNTRY_CODE}/O=${ORGANIZATION}" \
+    -days 3650 \
+    -extensions root_extensions \
+    -out root/ca.crt.pem
+fi
+if [ ! -f class3/ca.crt.pem ]; then
+  openssl req -new -config ca.cnf \
+    -keyout class3/private/ca.key.pem \
+    -nodes \
+    -subj "/CN=Class 3 Test CA/C=${COUNTRY_CODE}/O=${ORGANIZATION}" \
+    -out class3/ca.csr.pem
+  openssl ca -config ca.cnf \
+    -name root_ca \
+    -in class3/ca.csr.pem -out class3/ca.crt.pem \
+    -rand_serial \
+    -extensions class3_extensions \
+    -batch
+fi
+
+if [ ! -f certs/cachain.crt.pem ]; then
+  (
+    openssl x509 -in class3/ca.crt.pem
+    openssl x509 -in root/ca.crt.pem
+  ) >certs/cachain.crt.pem
+fi
+
+if [ ! -f certs/cats.cacert.localhost.crt.pem ]; then
+  openssl req -new -keyout certs/cats.cacert.localhost.key.pem -nodes \
+    -out certs/cats.cacert.localhost.csr.pem -subj "/CN=cats.cacert.localhost" \
+    -addext "subjectAltName=DNS:cats.cacert.localhost,DNS:www.cats.cacert.localhost"
+  openssl ca -config ca.cnf \
+    -name class3_ca \
+    -in certs/cats.cacert.localhost.csr.pem \
+    -out certs/cats.cacert.localhost.crt.pem \
+    -rand_serial \
+    -extensions server_ext \
+    -batch
+fi
+if [ ! -f certs/cats-client.cacert.localhost.crt.pem ]; then
+  openssl req -new -keyout certs/cats-client.cacert.localhost.key.pem -nodes \
+    -out certs/cats-client.cacert.localhost.csr.pem -subj "/CN=cats.cacert.localhost" \
+    -addext "subjectAltName=DNS:cats.cacert.localhost"
+  openssl ca -config ca.cnf \
+    -name class3_ca \
+    -in certs/cats-client.cacert.localhost.csr.pem \
+    -out certs/cats-client.cacert.localhost.crt.pem \
+    -rand_serial \
+    -extensions client_ext \
+    -batch
+fi
+if [ ! -f certs/mgr.cacert.localhost.crt.pem ]; then
+  openssl req -new -keyout certs/mgr.cacert.localhost.key.pem -nodes \
+    -out certs/mgr.cacert.localhost.csr.pem -subj "/CN=mgr.cacert.localhost" \
+    -addext "subjectAltName=DNS:mgr.cacert.localhost,DNS:www.mgr.cacert.localhost"
+  openssl ca -config ca.cnf \
+    -name class3_ca \
+    -in certs/mgr.cacert.localhost.csr.pem \
+    -out certs/mgr.cacert.localhost.crt.pem \
+    -rand_serial \
+    -extensions server_ext \
+    -batch
+fi
+if [ ! -f certs/secure.test.cacert.localhost.crt.pem ]; then
+  openssl req -new -keyout certs/secure.test.cacert.localhost.key.pem -nodes \
+    -out certs/secure.test.cacert.localhost.csr.pem -subj "/CN=secure.test.cacert.localhost" \
+    -addext "subjectAltName=DNS:secure.test.cacert.localhost"
+  openssl ca -config ca.cnf \
+    -name class3_ca \
+    -in certs/secure.test.cacert.localhost.csr.pem \
+    -out certs/secure.test.cacert.localhost.crt.pem \
+    -rand_serial \
+    -extensions server_ext \
+    -batch
+fi
+if [ ! -f certs/test.cacert.localhost.crt.pem ]; then
+  openssl req -new -keyout certs/test.cacert.localhost.key.pem -nodes \
+    -out certs/test.cacert.localhost.csr.pem -subj "/CN=test.cacert.localhost" \
+    -addext "subjectAltName=DNS:test.cacert.localhost,DNS:www.test.cacert.localhost"
+  openssl ca -config ca.cnf \
+    -name class3_ca \
+    -in certs/test.cacert.localhost.csr.pem \
+    -out certs/test.cacert.localhost.crt.pem \
+    -rand_serial \
+    -extensions server_ext \
+    -batch
+fi
+
+if [ ! -f certs/testclient.p12 ]; then
+  openssl req -new -keyout certs/testclient.key.pem -nodes \
+    -out certs/testclient.csr.pem -subj "/CN=${CLIENT_CERT_USERNAME}" \
+    -addext "subjectAltName=email:${CLIENT_CERT_EMAIL}"
+  openssl ca -config ca.cnf \
+    -name class3_ca \
+    -in certs/testclient.csr.pem \
+    -out certs/testclient.crt.pem \
+    -rand_serial \
+    -extensions client_ext \
+    -batch
+  openssl pkcs12 -export -out certs/testclient.p12 \
+    -passout "pass:${CLIENT_CERT_PASSWORD}" \
+    -chain -CAfile certs/cachain.crt.pem \
+    -inkey certs/testclient.key.pem \
+    -in certs/testclient.crt.pem \
+    -name "${CLIENT_CERT_USERNAME}"
+fi