diff --git a/backup b/backup
index 7a4061c..45f8c2a 100755
--- a/backup
+++ b/backup
@@ -21,6 +21,11 @@ docker compose exec bmysql mysqldump boulder_sa_integration >$TMPDIR/boulder_sa_
cp -p /etc/nginx/ssl/*key* /etc/nginx/ssl/*cert.pem /etc/nginx/ssl/*.csr $TMPDIR/
cp -rp /opt/labca/data $TMPDIR/
+#cp -p /opt/labca/data/config.json $TMPDIR/
+
+cp -rp /opt/boulder/labca/certs/webpki $TMPDIR/
+
+cp -rp /var/lib/softhsm/tokens $TMPDIR/
cd /tmp
diff --git a/build/Dockerfile-control b/build/Dockerfile-control
index 937539c..c8428d5 100644
--- a/build/Dockerfile-control
+++ b/build/Dockerfile-control
@@ -37,6 +37,7 @@ RUN export DEBIAN_FRONTEND=noninteractive \
cron \
curl \
python3 \
+ softhsm2 \
tzdata \
ucspi-tcp \
&& rm -rf /var/lib/apt/lists/*
@@ -59,6 +60,7 @@ COPY tmp/restore /opt/labca/
COPY tmp/utils.sh /opt/labca/
COPY tmp/src/labca /opt/staging/boulder_labca
COPY tmp/admin/apply-boulder /opt/labca/
+COPY tmp/admin/apply /opt/labca/
COPY tmp/admin/static /opt/staging/static
COPY tmp/admin/data /opt/staging/data
@@ -68,7 +70,4 @@ COPY tmp/admin/apply-nginx /opt/labca/
COPY tmp/bin/boulder /opt/boulder/bin/
-RUN cd /opt/boulder/bin/ \
- && ln -s boulder admin-revoker \
- && ln -s boulder mail-tester \
- && mkdir /opt/logs
+RUN mkdir /opt/logs
diff --git a/build/Dockerfile-gui b/build/Dockerfile-gui
index dc072c3..8a293e1 100644
--- a/build/Dockerfile-gui
+++ b/build/Dockerfile-gui
@@ -1,4 +1,4 @@
-FROM ubuntu:focal as builder
+FROM ubuntu:focal AS builder
RUN export DEBIAN_FRONTEND=noninteractive \
&& apt-get update \
@@ -31,6 +31,7 @@ FROM ubuntu:focal
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates \
+ softhsm2 \
tzdata \
unzip \
zip \
@@ -46,3 +47,6 @@ COPY tmp/admin/apply-boulder /opt/labca/
COPY tmp/admin/apply-nginx /opt/labca/
COPY tmp/admin/restart_control /opt/labca/
COPY tmp/admin/templates /opt/labca/templates/
+
+COPY tmp/bin/ceremony /opt/boulder/bin/
+COPY tmp/bin/nameid /opt/boulder/bin/
diff --git a/build/docker-compose.yml b/build/docker-compose.yml
index 4dd0d51..b17125e 100644
--- a/build/docker-compose.yml
+++ b/build/docker-compose.yml
@@ -22,7 +22,7 @@ services:
- boulder_data:/opt/boulder/labca
- certificates:/opt/boulder/labca/certs
- nginx_html:/opt/wwwstatic
- - softhsm:/var/lib/softhsm/tokens:cached
+ - softhsm:/var/lib/softhsm/tokens
networks:
bouldernet:
ipv4_address: 10.77.77.77
@@ -113,6 +113,7 @@ services:
- backup:/opt/backup
- boulder_data:/opt/boulder/labca
- certificates:/opt/boulder/labca/certs
+ - softhsm:/var/lib/softhsm/tokens
expose:
- 3000
depends_on:
@@ -154,6 +155,7 @@ services:
- logs:/opt/logs
- boulder_data:/opt/boulder/labca
- certificates:/opt/boulder/labca/certs
+ - softhsm:/var/lib/softhsm/tokens
- nginx_conf:/etc/nginx/conf.d
- nginx_ssl:/etc/nginx/ssl
- nginx_html:/var/www/html
diff --git a/build/tag_and_upload.sh b/build/tag_and_upload.sh
index 1e1e425..7e7140c 100755
--- a/build/tag_and_upload.sh
+++ b/build/tag_and_upload.sh
@@ -35,7 +35,7 @@ if [ "$BRANCH" == "master" ] || [ "$BRANCH" == "main" ]; then
fi
cnt=$(ls -1 tmp/bin | wc -l)
-[ $cnt -gt 20 ] || die "Only found $cnt boulder binaries!" # ?? still correct??
+[ $cnt -gt 16 ] || die "Only found $cnt boulder binaries!" # ?? still correct??
docker build -f Dockerfile-boulder -t $LABCA_BOULDER_TAG .
if [ "$BRANCH" == "master" ] || [ "$BRANCH" == "main" ]; then
diff --git a/build/tmp.patch b/build/tmp.patch
index d14ee80..59f0cea 100644
--- a/build/tmp.patch
+++ b/build/tmp.patch
@@ -1,5 +1,5 @@
diff --git a/docker-compose.yml b/docker-compose.yml
-index c7939ece4..0a2854919 100644
+index 71203004d..b17125e54 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -4,7 +4,7 @@ services:
@@ -19,11 +19,11 @@ index c7939ece4..0a2854919 100644
- - /home/labca/boulder_labca:/opt/boulder/labca
- - /home/labca/nginx_data/static:/opt/wwwstatic
- - ./.gocache:/root/.cache/go-build:cached
-- - /home/labca/boulder_labca/certs/.softhsm-tokens/:/var/lib/softhsm/tokens/:cached
+- - /home/labca/boulder_labca/certs/.softhsm-tokens/:/var/lib/softhsm/tokens/
+ - boulder_data:/opt/boulder/labca
+ - certificates:/opt/boulder/labca/certs
+ - nginx_html:/opt/wwwstatic
-+ - softhsm:/var/lib/softhsm/tokens:cached
++ - softhsm:/var/lib/softhsm/tokens
networks:
bouldernet:
ipv4_address: 10.77.77.77
@@ -35,7 +35,7 @@ index c7939ece4..0a2854919 100644
entrypoint: labca/entrypoint.sh
working_dir: &boulder_working_dir /opt/boulder
logging:
-@@ -87,34 +87,39 @@ services:
+@@ -87,35 +87,40 @@ services:
bconsul:
image: hashicorp/consul:1.15.4
@@ -67,12 +67,14 @@ index c7939ece4..0a2854919 100644
- - /home/labca/backup:/opt/backup
- - .:/opt/boulder
- - /home/labca/boulder_labca:/opt/boulder/labca
+- - /home/labca/boulder_labca/certs/.softhsm-tokens/:/var/lib/softhsm/tokens/
+ - ./docker-compose.yml:/opt/boulder/docker-compose.yml
+ - ldata:/opt/labca/data
+ - nginx_html:/opt/wwwstatic
+ - backup:/opt/backup
+ - boulder_data:/opt/boulder/labca
+ - certificates:/opt/boulder/labca/certs
++ - softhsm:/var/lib/softhsm/tokens
expose:
- 3000
depends_on:
@@ -85,7 +87,7 @@ index c7939ece4..0a2854919 100644
logging:
driver: "json-file"
options:
-@@ -131,27 +136,27 @@ services:
+@@ -132,28 +137,28 @@ services:
- 80:80
- 443:443
volumes:
@@ -113,6 +115,7 @@ index c7939ece4..0a2854919 100644
- - /home/labca/control_logs:/opt/logs
- - .:/opt/boulder
- - /home/labca/boulder_labca:/opt/boulder/labca
+- - /home/labca/boulder_labca/certs/.softhsm-tokens/:/var/lib/softhsm/tokens/
- - /home/labca/nginx_data/conf.d:/etc/nginx/conf.d
- - /home/labca/nginx_data/ssl:/etc/nginx/ssl
- - /home/labca/nginx_data/static:/var/www/html
@@ -122,13 +125,14 @@ index c7939ece4..0a2854919 100644
+ - logs:/opt/logs
+ - boulder_data:/opt/boulder/labca
+ - certificates:/opt/boulder/labca/certs
++ - softhsm:/var/lib/softhsm/tokens
+ - nginx_conf:/etc/nginx/conf.d
+ - nginx_ssl:/etc/nginx/ssl
+ - nginx_html:/var/www/html
expose:
- 3030
environment:
-@@ -169,6 +174,15 @@ services:
+@@ -171,6 +176,15 @@ services:
volumes:
dbdata:
diff --git a/checkcrl b/checkcrl
index 8cf9c5f..77af5d3 100755
--- a/checkcrl
+++ b/checkcrl
@@ -2,32 +2,34 @@
set -e
-if [ -e data/root-ca.crl ] && [ ! -e /var/www/html/crl/root-ca.crl ]; then
- cp -p data/root-ca.crl /var/www/html/crl/root-ca.crl
- touch /var/www/html/crl
-fi
-if [ -e data/root-ca.crl ] && [ data/root-ca.crl -nt /var/www/html/crl/root-ca.crl ]; then
- cp -p data/root-ca.crl /var/www/html/crl/root-ca.crl
- touch /var/www/html/crl
-fi
-
cd /var/www/html
+
+ROOT_CRL_FILE=/opt/boulder/labca/certs/webpki/root-01-crl.pem
+ROOT_CRL_NAME=$(basename $ROOT_CRL_FILE)
+[ -e "crl/$ROOT_CRL_NAME" ] || (cp $ROOT_CRL_FILE crl/ && touch crl/)
+[ $ROOT_CRL_FILE -ot "crl/$ROOT_CRL_NAME" ] || (cp $ROOT_CRL_FILE crl/ && touch crl/)
+
if [ crl/ -nt certs/index.html ]; then
echo "Updating certs/index.html with latest CRL info..."
- PKI_ROOT_CERT_BASE="crl/root-ca"
- PKI_ISSUER_NAME_ID=$(grep issuer_name_id /opt/labca/data/config.json | sed -e 's/.*:[ ]*//' | sed -e 's/,//g' | sed -e 's/\"//g')
-
+ PKI_ROOT_CRL_LINK=""
PKI_ROOT_CRL_VALIDITY=""
- if [ -e "$PKI_ROOT_CERT_BASE.crl" ]; then
- PKI_ROOT_CRL_VALIDITY="$(openssl crl -noout -in $PKI_ROOT_CERT_BASE.crl -lastupdate | sed -e "s/.*=/Last Update: /") $(openssl crl -noout -in $PKI_ROOT_CERT_BASE.crl -nextupdate | sed -e "s/.*=/Next Update: /")"
+ if [ -e $ROOT_CRL_FILE ]; then
+ PKI_ROOT_CRL_LINK="$ROOT_CRL_NAME "
+ PKI_ROOT_CRL_VALIDITY="$(openssl crl -noout -in $ROOT_CRL_FILE -lastupdate | sed -e "s/.*=/Last Update: /") $(openssl crl -noout -in $ROOT_CRL_FILE -nextupdate | sed -e "s/.*=/Next Update: /")"
fi
+ sed -i -e "s|<\!-- BEGIN PKI_ROOT_CRL_LINK -->.*<\!-- END PKI_ROOT_CRL_LINK -->|<\!-- BEGIN PKI_ROOT_CRL_LINK -->$PKI_ROOT_CRL_LINK<\!-- END PKI_ROOT_CRL_LINK -->|g" certs/index.html
sed -i -e "s|<\!-- BEGIN PKI_ROOT_CRL_VALIDITY -->.*<\!-- END PKI_ROOT_CRL_VALIDITY -->|<\!-- BEGIN PKI_ROOT_CRL_VALIDITY -->$PKI_ROOT_CRL_VALIDITY<\!-- END PKI_ROOT_CRL_VALIDITY -->|g" certs/index.html
+ PKI_INT_CERT_BASE="/opt/boulder/labca/certs/webpki/issuer-01-cert"
+ INT_BASE_NAME=$(basename $PKI_INT_CERT_BASE)
+ INT_CRL_NAME=${INT_BASE_NAME//-cert/-crl}.pem
+ PKI_ISSUER_NAME_ID=$(grep issuer_name_id /opt/labca/data/config.json | sed -e 's/.*:[ ]*//' | sed -e 's/,//g' | sed -e 's/\"//g')
PKI_INT_CRL_LINK=""
PKI_INT_CRL_VALIDITY=""
if [ -e "crl/$PKI_ISSUER_NAME_ID.crl" ]; then
- PKI_INT_CRL_LINK="$PKI_ISSUER_NAME_ID.crl "
+ [ -L "crl/$INT_CRL_NAME" ] || ln -sf $PKI_ISSUER_NAME_ID.crl crl/$INT_CRL_NAME
+ PKI_INT_CRL_LINK="$INT_CRL_NAME "
PKI_INT_CRL_VALIDITY="$(openssl crl -noout -inform der -in crl/$PKI_ISSUER_NAME_ID.crl -lastupdate | sed -e "s/.*=/Last Update: /") $(openssl crl -noout -inform der -in crl/$PKI_ISSUER_NAME_ID.crl -nextupdate | sed -e "s/.*=/Next Update: /")"
fi
sed -i -e "s|<\!-- BEGIN PKI_INT_CRL_LINK -->.*<\!-- END PKI_INT_CRL_LINK -->|<\!-- BEGIN PKI_INT_CRL_LINK -->$PKI_INT_CRL_LINK<\!-- END PKI_INT_CRL_LINK -->|g" certs/index.html
diff --git a/checkrenew b/checkrenew
index 120685a..babee7a 100755
--- a/checkrenew
+++ b/checkrenew
@@ -12,3 +12,6 @@ if ! expires=`openssl x509 -checkend $[ 86400 * $RENEW ] -noout -in /etc/nginx/s
cp -p /etc/nginx/ssl/labca_cert.pem /etc/nginx/ssl/labca_cert_$TODAY.pem
/opt/labca/renew
fi
+
+cd /opt/labca/gui
+/opt/labca/bin/labca-gui -config /opt/labca/data/config.json -renewcrl $RENEW
diff --git a/commander b/commander
index 84f87b1..36edc76 100755
--- a/commander
+++ b/commander
@@ -51,12 +51,12 @@ case $txt in
wait_up $PS_BOULDER $PS_BOULDER_COUNT &>>$LOGFILE
cd /etc/nginx/ssl
[ -e account.key ] || openssl genrsa 4096 > account.key
- [ ! -f labca_key.pem ] || mv labca_key.pem labca_key_rsa.pem
+ [ -L labca_key.pem ] || mv labca_key.pem labca_key_rsa.pem
[ -e labca_key_rsa.pem ] || openssl genrsa 4096 > labca_key_rsa.pem
[ -e labca_key_ecdsa.pem ] || openssl ecparam -name secp384r1 -genkey -out labca_key_ecdsa.pem
set +e
- curve_count=$(openssl pkey -pubin -in /opt/boulder/labca/test-ca.pubkey.pem -text | grep -i curve | wc -l)
+ curve_count=$(openssl pkey -pubin -in /opt/boulder/labca/certs/webpki/issuer-01-pubkey.pem -text | grep -i curve | wc -l)
set -e
[ "$curve_count" == "0" ] && ln -sf labca_key_rsa.pem labca_key.pem || /bin/true
[ "$curve_count" != "0" ] && ln -sf labca_key_ecdsa.pem labca_key.pem || /bin/true
@@ -75,6 +75,11 @@ case $txt in
wait_server $url
sleep 10
/opt/labca/renew
+
+ sleep 5
+ cd /opt/boulder
+ docker compose exec -i boulder ./bin/boulder crl-updater --config labca/config/crl-updater.json -runOnce -debug-addr :18021
+ /opt/labca/checkcrl
fi
ln -sf /opt/labca/cron_d /etc/cron.d/labca
@@ -83,12 +88,12 @@ case $txt in
"acme-change")
read fqdn
cd /etc/nginx/ssl
- [ ! -f labca_key.pem ] || mv labca_key.pem labca_key_rsa.pem
+ [ -L labca_key.pem ] || mv labca_key.pem labca_key_rsa.pem
[ -e labca_key_rsa.pem ] || openssl genrsa 4096 > labca_key_rsa.pem
[ -e labca_key_ecdsa.pem ] || openssl ecparam -name secp384r1 -genkey -out labca_key_ecdsa.pem
set +e
- curve_count=$(openssl pkey -pubin -in /opt/boulder/labca/test-ca.pubkey.pem -text | grep -i curve | wc -l)
+ curve_count=$(openssl pkey -pubin -in /opt/boulder/labca/certs/webpki/issuer-01-pubkey.pem -text | grep -i curve | wc -l)
set -e
[ "$curve_count" == "0" ] && ln -sf labca_key_rsa.pem labca_key.pem || /bin/true
[ "$curve_count" != "0" ] && ln -sf labca_key_ecdsa.pem labca_key.pem || /bin/true
@@ -270,14 +275,34 @@ case $txt in
nohup /labca/install -b $branch &>>$LOGFILE
fi
;;
+"gen-root-crl")
+ cd /opt/labca/gui
+ /opt/labca/bin/labca-gui -config /opt/labca/data/config.json -renewcrl 999 &>>$LOGFILE
+ /opt/labca/checkcrl &>>$LOGFILE
+ ;;
"gen-issuer-crl")
cd /opt/boulder
- docker compose exec -i boulder ./bin/boulder crl-updater --config labca/config/crl-updater.json -runOnce -debug-addr :18021 &>>$LOGFILE
+ docker compose exec -i boulder ./bin/boulder crl-updater -config labca/config/crl-updater.json -runOnce -debug-addr :18021 &>>$LOGFILE
/opt/labca/checkcrl &>>$LOGFILE
;;
"check-crl")
/opt/labca/checkcrl &>>$LOGFILE
;;
+"apply")
+ [ ! -e /opt/labca/apply ] || /opt/labca/apply &>>$LOGFILE
+ [ ! -e /opt/labca/gui/apply ] || /opt/labca/gui/apply &>>$LOGFILE
+ [ -e /opt/labca/apply ] || [ -e /opt/labca/gui/apply ] || echo "Could not find apply script!"
+ ;;
+"git-version")
+ if [ -x /usr/bin/git ]; then
+ git config --global --add safe.directory /opt/labca &>>$LOGFILE
+ gd=$(git describe --always --tags HEAD)
+ echo "$gd"
+ else
+ echo "unknown"
+ fi
+ exit 0
+ ;;
*)
echo "Unknown command '$txt'. ERROR!"
exit 1
diff --git a/control_do.sh b/control_do.sh
index 05e2ca6..1f240ff 100755
--- a/control_do.sh
+++ b/control_do.sh
@@ -53,13 +53,8 @@ setup_nginx_data() {
[ -e cert ] || ln -s certs cert
cp -rp /opt/staging/static/* .
- [ -e /opt/labca/data/root-ca.crl ] && cp /opt/labca/data/root-ca.crl crl/ || true
[ -e /opt/labca/data/root-ca.pem ] && cp /opt/labca/data/root-ca.pem certs/ || true
- [ -e /opt/labca/data/root-ca.pem ] && ln -sf root-ca.pem certs/test-root.pem || true
- [ -e /opt/labca/data/root-ca.der ] && cp /opt/labca/data/root-ca.der certs/ || true
[ -e /opt/labca/data/issuer/ca-int.pem ] && cp /opt/labca/data/issuer/ca-int.pem certs/ || true
- [ -e /opt/labca/data/issuer/ca-int.pem ] && ln -sf ca-int.pem certs/test-ca.pem || true
- [ -e /opt/labca/data/issuer/ca-int.pem ] && cp /opt/labca/data/issuer/ca-int.der certs/ || true
if [ ! -e /etc/nginx/ssl/labca_cert.pem ]; then
pushd /etc/nginx/ssl >/dev/null
diff --git a/gui/apply b/gui/apply
index 21c62b9..c70c67d 100755
--- a/gui/apply
+++ b/gui/apply
@@ -3,26 +3,23 @@
set -e
baseDir=$(cd $(dirname $0) && pwd)
-dataDir="$baseDir/data"
+dataDir="/opt/boulder/labca/certs/webpki"
-export PKI_ROOT_CERT_BASE="$dataDir/root-ca"
-export PKI_INT_CERT_BASE="$dataDir/issuer/ca-int"
+export PKI_ROOT_CERT_BASE="$dataDir/root-01-cert"
+export PKI_INT_CERT_BASE="$dataDir/issuer-01-cert"
cd /opt/boulder/labca
$baseDir/apply-boulder
cd /opt/wwwstatic
-if [ -e "$PKI_ROOT_CERT_BASE.crl" ]; then
- cp $PKI_ROOT_CERT_BASE.crl crl/
+PKI_ROOT_CRL_FILE=${PKI_ROOT_CERT_BASE/-cert/-crl}.pem
+if [ -e "$PKI_ROOT_CRL_FILE" ]; then
+ cp $PKI_ROOT_CRL_FILE crl/
else
echo "WARNING: no Root CRL file present - please upload one from the manage page"
fi
cp $PKI_ROOT_CERT_BASE.pem certs/
-ln -sf root-ca.pem certs/test-root.pem
-cp $PKI_ROOT_CERT_BASE.der certs/
cp $PKI_INT_CERT_BASE.pem certs/
-ln -sf ca-int.pem certs/test-ca.pem
-cp $PKI_INT_CERT_BASE.der certs/
$baseDir/apply-nginx
diff --git a/gui/apply-boulder b/gui/apply-boulder
index 53a6ac2..6d39c5b 100755
--- a/gui/apply-boulder
+++ b/gui/apply-boulder
@@ -14,8 +14,8 @@ PKI_DOMAIN=$(echo $PKI_FQDN | perl -p0e 's/.*?\.//')
PKI_DOMAIN_MODE=$(grep domain_mode $dataDir/config.json | sed -e 's/.*:[ ]*//' | sed -e 's/\",//g' | sed -e 's/\"//g')
PKI_LOCKDOWN_DOMAINS=$(grep lockdown $dataDir/config.json | grep -v domain_mode | sed -e 's/.*:[ ]*//' | sed -e 's/\",//g' | sed -e 's/\"//g')
PKI_WHITELIST_DOMAINS=$(grep whitelist $dataDir/config.json | grep -v domain_mode | sed -e 's/.*:[ ]*//' | sed -e 's/\",//g' | sed -e 's/\"//g')
-PKI_ROOT_CERT_BASE="$dataDir/root-ca"
-PKI_INT_CERT_BASE="$dataDir/issuer/ca-int"
+PKI_ROOT_CERT_BASE="/opt/boulder/labca/certs/webpki/root-01-cert"
+PKI_INT_CERT_BASE="/opt/boulder/labca/certs/webpki/issuer-01-cert"
PKI_ISSUER_NAME_ID=$(grep issuer_name_id $dataDir/config.json | sed -e 's/.*:[ ]*//' | sed -e 's/,//g' | sed -e 's/\"//g')
if [ -z "$PKI_ISSUER_NAME_ID" ] && [ -e "$PKI_INT_CERT_BASE.pem" ]; then
nmid=$(/opt/boulder/bin/nameid -s $PKI_INT_CERT_BASE.pem)
@@ -38,15 +38,15 @@ if [ "$enabled" == "true," ]; then
PKI_EMAIL_PASS=$(grep pass $dataDir/config.json | grep -v password | head -1 | perl -p0e 's/.*?:\s+(.*)/\1/' | sed -e 's/\",//g' | sed -e 's/\"//g')
pwd=""
if [ -e $baseDir/bin/labca-gui ]; then
- pwd=$([ -e ] && $baseDir/bin/labca-gui -d $PKI_EMAIL_PASS || echo "")
+ pwd=$([ -e ] && $baseDir/bin/labca-gui -config $dataDir/config.json -d $PKI_EMAIL_PASS || echo "")
elif [ -e $baseDir/bin/labca-gui_prev ]; then
- pwd=$([ -e ] && $baseDir/bin/labca-gui_prev -d $PKI_EMAIL_PASS || echo "")
+ pwd=$([ -e ] && $baseDir/bin/labca-gui_prev -config $dataDir/config.json -d $PKI_EMAIL_PASS || echo "")
fi
PKI_EMAIL_PASS=$pwd
PKI_EMAIL_FROM=$(grep from $dataDir/config.json | perl -p0e 's/.*?:\s+(.*)/\1/' | sed -e 's/\",//g' | sed -e 's/\"//g')
PKI_EMAIL_TRUST=$(grep trust_root $dataDir/config.json | perl -p0e 's/.*?:\s+(.*)/\1/' | sed -e 's/\",//g' | sed -e 's/\"//g')
if [ "$PKI_EMAIL_TRUST" == "private" ]; then
- PKI_EMAIL_TRUST="labca/test-root.pem"
+ PKI_EMAIL_TRUST="labca/certs/webpki/root-01-cert.pem"
elif [ "$PKI_EMAIL_TRUST" == "skip" ]; then
PKI_EMAIL_TRUST="InsecureSkipVerify"
else
@@ -166,7 +166,8 @@ rm -f config/orphan-finder.json
rm -f config/ca-a.json
rm -f config/ca-b.json
-sed -i -e "s|\"issuerURL\": \".*\"|\"issuerURL\": \"http://$PKI_FQDN/certs/ca-int.pem\"|" config/ca.json
+INT_BASE_NAME=$(basename $PKI_INT_CERT_BASE.pem)
+sed -i -e "s|\"issuerURL\": \".*\"|\"issuerURL\": \"http://$PKI_FQDN/certs/$INT_BASE_NAME\"|" config/ca.json
sed -i -e "s|\"ocspURL\": \".*\"|\"ocspURL\": \"http://$PKI_FQDN/ocsp/\"|" config/ca.json
sed -i -e "s|\"crlURLBase\": \".*\"|\"crlURLBase\": \"http://$PKI_FQDN/crl/\"|" config/ca.json
@@ -229,31 +230,8 @@ rm -f test-root.pem
rm -f test-root.der
rm -f test-root.p8
-if [ -e $PKI_INT_CERT_BASE.key ]; then
- cp -p $PKI_INT_CERT_BASE.key test-ca.key
- if [ ! -e $PKI_INT_CERT_BASE.key.der ]; then
- openssl pkey -in $PKI_INT_CERT_BASE.key -out $PKI_INT_CERT_BASE.key.der -outform der
- fi
- cp -p $PKI_INT_CERT_BASE.key.der test-ca.key.der
- cp -p $PKI_INT_CERT_BASE.pem test-ca.pem
- openssl rsa -in $PKI_INT_CERT_BASE.key -pubout > test-ca.pubkey.pem 2>/dev/null || openssl ec -in $PKI_INT_CERT_BASE.key -pubout > test-ca.pubkey.pem
- openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in test-ca.key -out test-ca.p8
-fi
-if [ -e $PKI_ROOT_CERT_BASE.key ]; then
- cp -p $PKI_ROOT_CERT_BASE.key test-root.key
- if [ ! -e $PKI_ROOT_CERT_BASE.key.der ]; then
- openssl pkey -in $PKI_ROOT_CERT_BASE.key -out $PKI_ROOT_CERT_BASE.key.der -outform der
- fi
- cp -p $PKI_ROOT_CERT_BASE.key.der test-root.key.der
- openssl rsa -in $PKI_ROOT_CERT_BASE.key -pubout > test-root.pubkey.pem 2>/dev/null || openssl ec -in $PKI_ROOT_CERT_BASE.key -pubout > test-root.pubkey.pem
- openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in test-root.key -out test-root.p8
-fi
-if [ -e $PKI_ROOT_CERT_BASE.pem ]; then
- cp -p $PKI_ROOT_CERT_BASE.pem test-root.pem
-fi
-
chown -R `ls -l helpers.py | cut -d" " -f 3,4 | sed 's/ /:/g'` .
-if [ -e $PKI_INT_CERT_BASE.key ] && [ -e $PKI_ROOT_CERT_BASE.pem ]; then
+if [ -e $PKI_INT_CERT_BASE.pem ] && [ -e $PKI_ROOT_CERT_BASE.pem ]; then
[ -f setup_complete ] || touch setup_complete
fi
diff --git a/gui/apply-nginx b/gui/apply-nginx
index 3bb4305..14c24b9 100755
--- a/gui/apply-nginx
+++ b/gui/apply-nginx
@@ -9,8 +9,10 @@ PKI_WEB_TITLE=$(grep web_title $dataDir/config.json | sed -e 's/.*:[ ]*//' | sed
if [ "$PKI_WEB_TITLE" == "" ]; then
export PKI_WEB_TITLE="LabCA"
fi
-PKI_ROOT_CERT_BASE="$dataDir/root-ca"
-PKI_INT_CERT_BASE="$dataDir/issuer/ca-int"
+PKI_ROOT_CERT_BASE="/opt/boulder/labca/certs/webpki/root-01-cert"
+[ ! -d "/home/labca/boulder_labca/certs/webpki" ] || PKI_ROOT_CERT_BASE="/home/labca/boulder_labca/certs/webpki/root-01-cert"
+PKI_INT_CERT_BASE="/opt/boulder/labca/certs/webpki/issuer-01-cert"
+[ ! -d "/home/labca/boulder_labca/certs/webpki" ] || PKI_INT_CERT_BASE="/home/labca/boulder_labca/certs/webpki/issuer-01-cert"
PKI_ISSUER_NAME_ID=$(grep issuer_name_id $dataDir/config.json | sed -e 's/.*:[ ]*//' | sed -e 's/,//g' | sed -e 's/\"//g')
if [ -z "$PKI_ISSUER_NAME_ID" ] && [ -e "$PKI_INT_CERT_BASE.pem" ]; then
nmid=$(/opt/boulder/bin/nameid -s $PKI_INT_CERT_BASE.pem)
@@ -27,26 +29,42 @@ sed -i -e "s|
.* |$PKI_WEB_TITLE |g" 502.html
sed -i -e "s|<\!-- BEGIN WEBTITLE -->.*<\!-- END WEBTITLE -->|<\!-- BEGIN WEBTITLE -->$PKI_WEB_TITLE<\!-- END WEBTITLE -->|g" 502.html
if [ -e $PKI_ROOT_CERT_BASE.pem ]; then
- PKI_ROOT_DN=$(openssl x509 -noout -in $PKI_ROOT_CERT_BASE.pem -subject | sed -e "s/subject= //")
+ PKI_ROOT_DN=$(openssl x509 -noout -in $PKI_ROOT_CERT_BASE.pem -subject | sed -e "s/subject=//")
sed -i -e "s|<\!-- BEGIN PKI_ROOT_DN -->.*<\!-- END PKI_ROOT_DN -->|<\!-- BEGIN PKI_ROOT_DN -->$PKI_ROOT_DN<\!-- END PKI_ROOT_DN -->|g" certs/index.html
+ ROOT_BASE_NAME=$(basename $PKI_ROOT_CERT_BASE)
+ PKI_ROOT_LINK="$ROOT_BASE_NAME.pem "
+ sed -i -e "s|<\!-- BEGIN PKI_ROOT_LINK -->.*<\!-- END PKI_ROOT_LINK -->|<\!-- BEGIN PKI_ROOT_LINK -->$PKI_ROOT_LINK<\!-- END PKI_ROOT_LINK -->|g" certs/index.html
PKI_ROOT_VALIDITY="$(openssl x509 -noout -in $PKI_ROOT_CERT_BASE.pem -startdate | sed -e "s/.*=/Not Before: /") $(openssl x509 -noout -in $PKI_ROOT_CERT_BASE.pem -enddate | sed -e "s/.*=/Not After: /")"
sed -i -e "s|<\!-- BEGIN PKI_ROOT_VALIDITY -->.*<\!-- END PKI_ROOT_VALIDITY -->|<\!-- BEGIN PKI_ROOT_VALIDITY -->$PKI_ROOT_VALIDITY<\!-- END PKI_ROOT_VALIDITY -->|g" certs/index.html
+
+ ROOT_CRL_FILE=${PKI_ROOT_CERT_BASE/-cert/-crl}.pem
+ PKI_ROOT_CRL_LINK=""
PKI_ROOT_CRL_VALIDITY=""
- if [ -e "$PKI_ROOT_CERT_BASE.crl" ]; then
- PKI_ROOT_CRL_VALIDITY="$(openssl crl -noout -in $PKI_ROOT_CERT_BASE.crl -lastupdate | sed -e "s/.*=/Last Update: /") $(openssl crl -noout -in $PKI_ROOT_CERT_BASE.crl -nextupdate | sed -e "s/.*=/Next Update: /")"
+ if [ -e $ROOT_CRL_FILE ]; then
+ ROOT_CRL_NAME=$(basename $ROOT_CRL_FILE)
+ [ -e "crl/$ROOT_CRL_NAME" ] || cp $ROOT_CRL_FILE crl/$ROOT_CRL_NAME
+ PKI_ROOT_CRL_LINK="$ROOT_CRL_NAME "
+ PKI_ROOT_CRL_VALIDITY="$(openssl crl -noout -in $ROOT_CRL_FILE -lastupdate | sed -e "s/.*=/Last Update: /") $(openssl crl -noout -in $ROOT_CRL_FILE -nextupdate | sed -e "s/.*=/Next Update: /")"
fi
+ sed -i -e "s|<\!-- BEGIN PKI_ROOT_CRL_LINK -->.*<\!-- END PKI_ROOT_CRL_LINK -->|<\!-- BEGIN PKI_ROOT_CRL_LINK -->$PKI_ROOT_CRL_LINK<\!-- END PKI_ROOT_CRL_LINK -->|g" certs/index.html
sed -i -e "s|<\!-- BEGIN PKI_ROOT_CRL_VALIDITY -->.*<\!-- END PKI_ROOT_CRL_VALIDITY -->|<\!-- BEGIN PKI_ROOT_CRL_VALIDITY -->$PKI_ROOT_CRL_VALIDITY<\!-- END PKI_ROOT_CRL_VALIDITY -->|g" certs/index.html
fi
if [ -e $PKI_INT_CERT_BASE.pem ]; then
- PKI_INT_DN=$(openssl x509 -noout -in $PKI_INT_CERT_BASE.pem -subject | sed -e "s/subject= //")
+ PKI_INT_DN=$(openssl x509 -noout -in $PKI_INT_CERT_BASE.pem -subject | sed -e "s/subject=//")
sed -i -e "s|<\!-- BEGIN PKI_INT_DN -->.*<\!-- END PKI_INT_DN -->|<\!-- BEGIN PKI_INT_DN -->$PKI_INT_DN<\!-- END PKI_INT_DN -->|g" certs/index.html
+ INT_BASE_NAME=$(basename $PKI_INT_CERT_BASE)
+ PKI_INT_LINK="$INT_BASE_NAME.pem "
+ sed -i -e "s|<\!-- BEGIN PKI_INT_LINK -->.*<\!-- END PKI_INT_LINK -->|<\!-- BEGIN PKI_INT_LINK -->$PKI_INT_LINK<\!-- END PKI_INT_LINK -->|g" certs/index.html
PKI_INT_VALIDITY="$(openssl x509 -noout -in $PKI_INT_CERT_BASE.pem -startdate | sed -e "s/.*=/Not Before: /") $(openssl x509 -noout -in $PKI_INT_CERT_BASE.pem -enddate | sed -e "s/.*=/Not After: /")"
sed -i -e "s|<\!-- BEGIN PKI_INT_VALIDITY -->.*<\!-- END PKI_INT_VALIDITY -->|<\!-- BEGIN PKI_INT_VALIDITY -->$PKI_INT_VALIDITY<\!-- END PKI_INT_VALIDITY -->|g" certs/index.html
+
+ INT_CRL_NAME=${INT_BASE_NAME/-cert/-crl}.pem
PKI_INT_CRL_LINK=""
PKI_INT_CRL_VALIDITY=""
if [ -e "crl/$PKI_ISSUER_NAME_ID.crl" ]; then
- PKI_INT_CRL_LINK="$PKI_ISSUER_NAME_ID.crl "
+ [ -L "crl/$INT_CRL_NAME" ] || ln -sf $PKI_ISSUER_NAME_ID.crl crl/$INT_CRL_NAME
+ PKI_INT_CRL_LINK="$INT_CRL_NAME "
PKI_INT_CRL_VALIDITY="$(openssl crl -noout -inform der -in crl/$PKI_ISSUER_NAME_ID.crl -lastupdate | sed -e "s/.*=/Last Update: /") $(openssl crl -noout -inform der -in crl/$PKI_ISSUER_NAME_ID.crl -nextupdate | sed -e "s/.*=/Next Update: /")"
fi
sed -i -e "s|<\!-- BEGIN PKI_INT_CRL_LINK -->.*<\!-- END PKI_INT_CRL_LINK -->|<\!-- BEGIN PKI_INT_CRL_LINK -->$PKI_INT_CRL_LINK<\!-- END PKI_INT_CRL_LINK -->|g" certs/index.html
diff --git a/gui/certificate.go b/gui/certificate.go
index a5342b5..8a373b7 100644
--- a/gui/certificate.go
+++ b/gui/certificate.go
@@ -1,24 +1,29 @@
package main
import (
- "crypto/rand"
+ "crypto/ecdsa"
+ "crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
+ "html/template"
"io"
"io/fs"
+ "log"
"math"
- "math/big"
"mime/multipart"
"os"
"os/exec"
"path/filepath"
+ "reflect"
"regexp"
"runtime/debug"
"strconv"
"strings"
"time"
+
+ "github.com/spf13/viper"
)
// CertificateInfo contains all data related to a certificate (file)
@@ -35,7 +40,6 @@ type CertificateInfo struct {
Country string
Organization string
- OrgUnit string
CommonName string
ImportFile multipart.File
@@ -47,6 +51,15 @@ type CertificateInfo struct {
Certificate string
CRL string
+ /*
+ KeyFromHSM bool
+ HSMInfo HSMInfo
+ HSMKeys map[string]string
+ HSMKey string
+ HSMLabel string
+ StoreCertOnHSM bool
+ */
+
RequestBase string
Errors map[string]string
}
@@ -57,12 +70,14 @@ func (ci *CertificateInfo) Initialize() {
ci.KeyTypes = make(map[string]string)
ci.KeyTypes["rsa4096"] = "RSA-4096"
- ci.KeyTypes["rsa3072"] = "RSA-3072"
ci.KeyTypes["rsa2048"] = "RSA-2048"
ci.KeyTypes["ecdsa384"] = "ECDSA-384"
ci.KeyTypes["ecdsa256"] = "ECDSA-256"
ci.KeyType = "rsa4096"
+
+ // ci.HSMKeys = make(map[string]string)
+ // ci.StoreCertOnHSM = true
}
// ValidateGenerate that the CertificateInfo contains valid and all required data for generating a cert
@@ -97,7 +112,7 @@ func (ci *CertificateInfo) Validate() bool {
}
if ci.CreateType == "upload" {
- if !ci.IsRoot && strings.TrimSpace(ci.Key) == "" {
+ if strings.TrimSpace(ci.Key) == "" {
ci.Errors["Key"] = "Please provide a PEM-encoded key"
}
if strings.TrimSpace(ci.Certificate) == "" {
@@ -139,128 +154,309 @@ func reportError(param interface{}) error {
return res
}
-func getRandomSerial() (string, error) {
- // from ca.generateSerialNumberAndValidity()
- const randBits = 136
- serialBytes := make([]byte, randBits/8+1)
- serialBytes[0] = 0xee
- if _, err := rand.Read(serialBytes[1:]); err != nil {
- return "", reportError(err)
+func ceremonyConfig(path string, rewrites map[string]string) (string, error) {
+ tmplBytes, err := os.ReadFile(path)
+ if err != nil {
+ return "", err
}
-
- serialBigInt := big.NewInt(0)
- serialBigInt.SetBytes(serialBytes)
-
- return fmt.Sprintf("%x", serialBigInt), nil
+ tmp, err := os.CreateTemp(os.TempDir(), "ceremony-config")
+ if err != nil {
+ return "", err
+ }
+ defer tmp.Close()
+ tmpl, err := template.New("config").Parse(string(tmplBytes))
+ if err != nil {
+ return "", err
+ }
+ err = tmpl.Execute(tmp, rewrites)
+ if err != nil {
+ return "", err
+ }
+ return tmp.Name(), nil
}
-func preCreateTasks(path string) error {
- if _, err := exeCmd("touch " + path + "index.txt"); err != nil {
- return reportError(err)
- }
- if _, err := exeCmd("touch " + path + "index.txt.attr"); err != nil {
- return reportError(err)
+func (ci *CertificateInfo) CeremonyRoot(seqnr string, use_existing_key bool) (string, error) {
+ keytype := "rsa"
+ keyparam := strings.Replace(ci.KeyType, "rsa", "", -1)
+ algo := "SHA256WithRSA"
+ if strings.HasPrefix(ci.KeyType, "ecdsa") {
+ keytype = "ecdsa"
+ len := strings.Replace(ci.KeyType, "ecdsa", "", -1)
+ keyparam = "P-" + len
+ algo = "ECDSAWithSHA" + len
}
- if _, err := os.Stat(path + "serial"); errors.Is(err, fs.ErrNotExist) {
- s, err := getRandomSerial()
+ notbefore := time.Now().Add(-1 * time.Second)
+ notafter := notbefore.AddDate(0, 0, ci.NumDays).Add(-1 * time.Second)
+
+ cfg := &HSMConfig{}
+ cfg.Initialize("root", seqnr)
+ if err := cfg.CreateSlot(); err != nil {
+ return "", fmt.Errorf("failed to create root slot: %s", err.Error())
+ }
+
+ certFileName := fmt.Sprintf("%sroot-%s-cert.pem", CERT_FILES_PATH, seqnr)
+ cb := renameBackup(certFileName)
+ var pb BackupResult
+ if !use_existing_key {
+ pb = renameBackup(fmt.Sprintf("%sroot-%s-pubkey.pem", CERT_FILES_PATH, seqnr))
+ }
+
+ ceremonyCfg, err := ceremonyConfig("templates/cert-ceremonies/root.yaml", map[string]string{
+ "Module": cfg.Module,
+ "UserPIN": cfg.UserPIN,
+ "SlotID": cfg.SlotID,
+ "Label": cfg.Label,
+ "Path": CERT_FILES_PATH,
+ "KeyType": keytype,
+ "KeyParam": keyparam,
+ "Extractable": strconv.FormatBool(true), // For now, with SoftHSM, this is fine. In future we need to ask for informed consent!
+ "SeqNr": seqnr,
+ "SignAlgorithm": algo,
+ "CommonName": ci.CommonName,
+ "OrgName": ci.Organization,
+ "Country": ci.Country,
+ "NotBefore": notbefore.UTC().Format("2006-01-02 15:04:05"),
+ "NotAfter": notafter.UTC().Format("2006-01-02 15:04:05"),
+ "Renewal": strconv.FormatBool(use_existing_key),
+ })
+ if err != nil {
+ ci.Errors["Generate"] = "error preparing for root ceremony, see logs for details"
+ cb.Restore()
+ if !use_existing_key {
+ pb.Restore()
+ }
+ return "", fmt.Errorf("could not fill root ceremony template: %s", err.Error())
+ }
+ defer os.Remove(ceremonyCfg)
+
+ if _, err = exeCmd("/opt/boulder/bin/ceremony -config " + ceremonyCfg); err != nil {
+ ci.Errors["Generate"] = "failed to execute root ceremony, see logs for details"
+ cb.Restore()
+ if !use_existing_key {
+ pb.Restore()
+ }
+ return "", err
+ }
+
+ cb.Remove()
+ if !use_existing_key {
+ pb.Remove()
+ }
+ return certFileName, nil
+}
+
+func (ci *CertificateInfo) CeremonyIssuer(seqnr, rootseqnr string, use_existing_key bool) (string, error) {
+ fqdn := viper.GetString("labca.fqdn")
+
+ keytype := "rsa"
+ keyparam := strings.Replace(ci.KeyType, "rsa", "", -1)
+ algo := "SHA256WithRSA"
+ if strings.HasPrefix(ci.KeyType, "ecdsa") {
+ keytype = "ecdsa"
+ len := strings.Replace(ci.KeyType, "ecdsa", "", -1)
+ keyparam = "P-" + len
+ algo = "ECDSAWithSHA" + len
+ }
+
+ notbefore := time.Now().Add(-1 * time.Second)
+ notafter := notbefore.AddDate(0, 0, ci.NumDays).Add(-1 * time.Second)
+
+ cfg := &HSMConfig{}
+ cfg.Initialize("issuer", seqnr)
+ if err := cfg.CreateSlot(); err != nil {
+ return "", fmt.Errorf("failed to create issuer slot: %s", err.Error())
+ }
+
+ if !use_existing_key {
+ pb := renameBackup(fmt.Sprintf("%sissuer-%s-pubkey.pem", CERT_FILES_PATH, seqnr))
+ jb := renameBackup(fmt.Sprintf("%sissuer-%s.pkcs11.json", CERT_FILES_PATH, seqnr))
+
+ keyCfg, err := ceremonyConfig("templates/cert-ceremonies/issuer-key.yaml", map[string]string{
+ "Module": cfg.Module,
+ "UserPIN": cfg.UserPIN,
+ "SlotID": cfg.SlotID,
+ "Label": cfg.Label,
+ "Path": CERT_FILES_PATH,
+ "KeyType": keytype,
+ "KeyParam": keyparam,
+ "Extractable": strconv.FormatBool(true), // For now, with SoftHSM, this is fine. In future we need to ask for informed consent!
+ "SeqNr": seqnr,
+ })
if err != nil {
- return err
+ ci.Errors["Generate"] = "error preparing for issuer key ceremony, see logs for details"
+ pb.Restore()
+ jb.Restore()
+ return "", fmt.Errorf("could not fill issuer key ceremony template: %s", err.Error())
}
- if err := os.WriteFile(path+"serial", []byte(s+"\n"), 0644); err != nil {
- return err
- }
- }
- if _, err := os.Stat(path + "crlnumber"); errors.Is(err, fs.ErrNotExist) {
- if err = os.WriteFile(path+"crlnumber", []byte("1000\n"), 0644); err != nil {
- return err
+ defer os.Remove(keyCfg)
+
+ if _, err = exeCmd("/opt/boulder/bin/ceremony -config " + keyCfg); err != nil {
+ ci.Errors["Generate"] = "failed to execute issuer key ceremony, see logs for details"
+ pb.Restore()
+ jb.Restore()
+ return "", err
}
+
+ pb.Remove()
+ jb.Remove()
}
- if _, err := exeCmd("mkdir -p " + path + "certs"); err != nil {
- return reportError(err)
+ cfg = &HSMConfig{}
+ cfg.Initialize("root", rootseqnr)
+ if err := cfg.CreateSlot(); err != nil {
+ return "", fmt.Errorf("failed to get root slot: %s", err.Error())
}
- return nil
+ certFileName := fmt.Sprintf("%sissuer-%s-cert.pem", CERT_FILES_PATH, seqnr)
+ cb := renameBackup(certFileName)
+
+ ceremonyCfg, err := ceremonyConfig("templates/cert-ceremonies/issuer-cert.yaml", map[string]string{
+ "Module": cfg.Module,
+ "UserPIN": cfg.UserPIN,
+ "RootSlotID": cfg.SlotID,
+ "RootLabel": cfg.Label,
+ "Path": CERT_FILES_PATH,
+ "SeqNr": seqnr,
+ "RootSeqNr": rootseqnr,
+ "SignAlgorithm": algo,
+ "CommonName": ci.CommonName,
+ "OrgName": ci.Organization,
+ "Country": ci.Country,
+ "NotBefore": notbefore.UTC().Format("2006-01-02 15:04:05"),
+ "NotAfter": notafter.UTC().Format("2006-01-02 15:04:05"),
+ "CrlUrl": fmt.Sprintf("http://%s/crl", fqdn),
+ "IssuerUrl": fmt.Sprintf("http://%s/aia/issuer", fqdn), // TODO: fix this
+ })
+ if err != nil {
+ ci.Errors["Generate"] = "error preparing for issuer cert ceremony, see logs for details"
+ cb.Restore()
+ return "", fmt.Errorf("could not fill issuer cert ceremony template: %s", err.Error())
+ }
+ defer os.Remove(ceremonyCfg)
+
+ if _, err = exeCmd("/opt/boulder/bin/ceremony -config " + ceremonyCfg); err != nil {
+ ci.Errors["Generate"] = "failed to execute issuer cert ceremony, see logs for details"
+ cb.Restore()
+ return "", err
+ }
+
+ cb.Remove()
+ return certFileName, nil
}
-func updateRootCRLDays(filename string, numDays int) error {
+func readCertificate(filename string) (*x509.Certificate, error) {
read, err := os.ReadFile(filename)
if err != nil {
fmt.Println(err)
- return errors.New("could not read '" + filename + "': " + err.Error())
+ return nil, errors.New("could not read '" + filename + "': " + err.Error())
}
- re := regexp.MustCompile(`(default_crl_days\s*=).*`)
- res := re.ReplaceAll(read, []byte("$1 "+strconv.Itoa(numDays)))
-
- if err = os.WriteFile(filename, res, 0640); err != nil {
- fmt.Println(err)
- return errors.New("could not write '" + filename + "': " + err.Error())
+ block, _ := pem.Decode(read)
+ if block == nil || block.Type != "CERTIFICATE" {
+ fmt.Println(block)
+ return nil, errors.New("failed to decode PEM block containing certificate")
+ }
+ crt, err := x509.ParseCertificate(block.Bytes)
+ if err != nil {
+ return nil, err
}
+ return crt, nil
+}
+
+func (ci *CertificateInfo) CeremonyRootCRL(seqnr string) error {
+ now := time.Now()
+
+ if viper.Get("crl_root_days") == nil || viper.Get("crl_root_days") == "" {
+ viper.Set("crl_root_days", 365)
+ viper.WriteConfig()
+ }
+ crlint, err := time.ParseDuration(fmt.Sprintf("%dh", viper.GetInt("crl_root_days")*24-1))
+ if err != nil {
+ crlint, _ = time.ParseDuration("8759h") // 365 days - 1 hour
+ }
+
+ cert, err := readCertificate(fmt.Sprintf("%sroot-%s-cert.pem", CERT_FILES_PATH, seqnr))
+ if err != nil {
+ return err
+ }
+
+ thisupdate := now
+ if thisupdate.Before(cert.NotBefore) {
+ thisupdate = cert.NotBefore.Add(1 * time.Second)
+ }
+
+ nextupdate := now.Add(crlint)
+ maxNext := cert.NotAfter.Add(-1 * time.Second)
+ if nextupdate.After(maxNext) {
+ nextupdate = maxNext
+ }
+ if nextupdate.Sub(thisupdate) > time.Hour*24*365 {
+ nextupdate = thisupdate.Add(time.Hour * 24 * 365).Add(-1 * time.Second)
+ }
+
+ crlnumber := fmt.Sprintf("%02d%03d%s", now.Year()-2000, now.YearDay(), seqnr)
+
+ cb := renameBackup(fmt.Sprintf("%sroot-%s-crl.pem", CERT_FILES_PATH, seqnr))
+
+ cfg := &HSMConfig{}
+ cfg.Initialize("root", seqnr)
+ if err := cfg.CreateSlot(); err != nil {
+ return fmt.Errorf("failed to get root slot: %s", err.Error())
+ }
+
+ keyCfg, err := ceremonyConfig("templates/cert-ceremonies/root-crl.yaml", map[string]string{
+ "Module": cfg.Module,
+ "UserPIN": cfg.UserPIN,
+ "RootSlotID": cfg.SlotID,
+ "RootLabel": cfg.Label,
+ "Path": CERT_FILES_PATH,
+ "RootSeqNr": seqnr,
+ "ThisUpdate": thisupdate.UTC().Format("2006-01-02 15:04:05"),
+ "NextUpdate": nextupdate.UTC().Format("2006-01-02 15:04:05"),
+ "CrlNumber": crlnumber,
+ })
+ if err != nil {
+ ci.Errors["CRL"] = "error preparing for root crl ceremony, see logs for details"
+ cb.Restore()
+ return fmt.Errorf("could not fill root crl ceremony template: %s", err.Error())
+ }
+ defer os.Remove(keyCfg)
+
+ if _, err = exeCmd("/opt/boulder/bin/ceremony -config " + keyCfg); err != nil {
+ ci.Errors["CRL"] = "failed to execute root crl ceremony, see logs for details"
+ cb.Restore()
+ return err
+ }
+
+ cb.Remove()
return nil
}
// Generate a key and certificate file for the data from this CertificateInfo
-func (ci *CertificateInfo) Generate(path string, certBase string) error {
- // 1. Generate key
- createCmd := "genrsa -aes256 -passout pass:foobar"
- keySize := " 4096"
- if strings.HasPrefix(ci.KeyType, "ecdsa") {
- keySize = ""
- createCmd = "ecparam -genkey -name "
- if ci.KeyType == "ecdsa256" {
- createCmd = createCmd + "prime256v1"
- }
- if ci.KeyType == "ecdsa384" {
- createCmd = createCmd + "secp384r1"
- }
- } else {
- if strings.HasSuffix(ci.KeyType, "3072") {
- keySize = " 3072"
- }
- if strings.HasSuffix(ci.KeyType, "2048") {
- keySize = " 2048"
- }
- }
-
- if _, err := exeCmd("openssl " + createCmd + " -out " + path + certBase + ".key" + keySize); err != nil {
- return reportError(err)
- }
- if _, err := exeCmd("openssl pkey -in " + path + certBase + ".key -passin pass:foobar -out " + path + certBase + ".tmp"); err != nil {
- return reportError(err)
- }
- if _, err := exeCmd("mv " + path + certBase + ".tmp " + path + certBase + ".key"); err != nil {
- return reportError(err)
- }
-
- _, _ = exeCmd("sleep 1")
-
- // 2. Generate certificate
- subject := "/C=" + ci.Country + "/O=" + ci.Organization
- if ci.OrgUnit != "" {
- subject = subject + "/OU=" + ci.OrgUnit
- }
- subject = subject + "/CN=" + ci.CommonName
- subject = strings.Replace(subject, " ", "\\\\", -1)
-
+func (ci *CertificateInfo) Generate(certBase string) error {
+ var err error
if ci.IsRoot {
- if _, err := exeCmd("openssl req -config " + path + "openssl.cnf -days " + strconv.Itoa(ci.NumDays) + " -new -utf8 -x509 -extensions v3_ca -subj " + subject + " -key " + path + certBase + ".key -out " + path + certBase + ".pem"); err != nil {
- return reportError(err)
- }
+ _, err = ci.CeremonyRoot("01", false)
- if err := updateRootCRLDays(path+"openssl.cnf", ci.NumDays); err != nil {
- return reportError(err)
- }
+ viper.Set("crl_root_days", ci.NumDays)
+ viper.WriteConfig()
} else {
- if _, err := exeCmd("openssl req -config " + path + "openssl.cnf -new -utf8 -subj " + subject + " -key " + path + certBase + ".key -out " + path + certBase + ".csr"); err != nil {
- return reportError(err)
- }
- if out, err := exeCmd("openssl ca -config " + path + "../openssl.cnf -extensions v3_intermediate_ca -days " + strconv.Itoa(ci.NumDays) + " -md sha384 -notext -batch -in " + path + certBase + ".csr -out " + path + certBase + ".pem"); err != nil {
- if strings.Contains(string(out), "root-ca.key for reading, No such file or directory") {
- return errors.New("NO_ROOT_KEY")
- }
- return reportError(err)
+ _, err = ci.CeremonyIssuer("01", "01", false)
+ }
+
+ if err != nil {
+ log.Printf("failed to create certificate: %s", err.Error())
+ return errors.New("failed to create certificate, see logs for details")
+ }
+
+ if !ci.IsRoot {
+ // Create CRLs stating that the intermediates are not revoked.
+ err = ci.CeremonyRootCRL("01")
+
+ if err != nil {
+ log.Printf("failed to create crl: %s", err.Error())
+ return errors.New("failed to create crl, see logs for details")
}
}
@@ -270,11 +466,11 @@ func (ci *CertificateInfo) Generate(path string, certBase string) error {
// ImportPkcs12 imports an uploaded PKCS#12 / PFX file
func (ci *CertificateInfo) ImportPkcs12(tmpFile string, tmpKey string, tmpCert string) error {
if ci.IsRoot {
- if strings.Index(ci.ImportHandler.Filename, "labca_root") != 0 {
+ if (strings.Index(ci.ImportHandler.Filename, "labca-root-01-cert") != 0) && (strings.Index(ci.ImportHandler.Filename, "labca_root") != 0) {
fmt.Printf("WARNING: importing root from .pfx file but name is %s\n", ci.ImportHandler.Filename)
}
} else {
- if strings.Index(ci.ImportHandler.Filename, "labca_issuer") != 0 {
+ if (strings.Index(ci.ImportHandler.Filename, "labca-issuer-01-cert") != 0) && (strings.Index(ci.ImportHandler.Filename, "labca_issuer") != 0) {
fmt.Printf("WARNING: importing issuer from .pfx file but name is %s\n", ci.ImportHandler.Filename)
}
}
@@ -305,11 +501,11 @@ func (ci *CertificateInfo) ImportPkcs12(tmpFile string, tmpKey string, tmpCert s
// ImportZip imports an uploaded ZIP file
func (ci *CertificateInfo) ImportZip(tmpFile string, tmpDir string) error {
if ci.IsRoot {
- if (strings.Index(ci.ImportHandler.Filename, "labca_root") != 0) && (strings.Index(ci.ImportHandler.Filename, "labca_certificates") != 0) {
+ if (strings.Index(ci.ImportHandler.Filename, "labca-root-01-cert") != 0) && (strings.Index(ci.ImportHandler.Filename, "labca_root") != 0) && (strings.Index(ci.ImportHandler.Filename, "labca_certificates") != 0) {
fmt.Printf("WARNING: importing root from .zip file but name is %s\n", ci.ImportHandler.Filename)
}
} else {
- if strings.Index(ci.ImportHandler.Filename, "labca_issuer") != 0 {
+ if (strings.Index(ci.ImportHandler.Filename, "labca-issuer-01-cert") != 0) && (strings.Index(ci.ImportHandler.Filename, "labca_issuer") != 0) {
fmt.Printf("WARNING: importing issuer from .zip file but name is %s\n", ci.ImportHandler.Filename)
}
}
@@ -341,7 +537,6 @@ func (ci *CertificateInfo) Import(tmpDir string, tmpKey string, tmpCert string)
if err != nil {
return err
}
-
defer f.Close()
io.Copy(f, ci.ImportFile)
@@ -454,8 +649,8 @@ func parseSubjectDn(subject string) map[string]string {
return trackerResultMap
}
-// ImportCerts imports both the root and the issuer certificates
-func (ci *CertificateInfo) ImportCerts(path string, rootCert string, rootKey string, issuerCert string, issuerKey string) error {
+// VerifyCerts verifies the root and the issuer certificates
+func (ci *CertificateInfo) VerifyCerts(path string, rootCert string, rootKey string, issuerCert string, issuerKey string) error {
var rootSubject string
if (rootCert != "") && (rootKey != "") {
r, err := exeCmd("openssl x509 -noout -subject -in " + rootCert)
@@ -473,9 +668,6 @@ func (ci *CertificateInfo) ImportCerts(path string, rootCert string, rootKey str
if val, ok := subjectMap["O"]; ok {
ci.Organization = val
}
- if val, ok := subjectMap["OU"]; ok {
- ci.OrgUnit = val
- }
if val, ok := subjectMap["CN"]; ok {
ci.CommonName = val
}
@@ -495,12 +687,6 @@ func (ci *CertificateInfo) ImportCerts(path string, rootCert string, rootKey str
}
if (issuerCert != "") && (issuerKey != "") {
- if ci.IsRoot {
- if err := preCreateTasks(path + "issuer/"); err != nil {
- return err
- }
- }
-
r, err := exeCmd("openssl x509 -noout -subject -in " + issuerCert)
if err != nil {
return reportError(err)
@@ -517,7 +703,7 @@ func (ci *CertificateInfo) ImportCerts(path string, rootCert string, rootKey str
fmt.Printf("Issuer certificate issued by CA '%s'\n", issuerIssuer)
if rootSubject == "" {
- r, err := exeCmd("openssl x509 -noout -subject -in data/root-ca.pem")
+ r, err := exeCmd("openssl x509 -noout -subject -in " + CERT_FILES_PATH + "root-01-cert.pem")
if err != nil {
return reportError(err)
}
@@ -531,7 +717,7 @@ func (ci *CertificateInfo) ImportCerts(path string, rootCert string, rootKey str
return errors.New("issuer not issued by our Root CA")
}
- _, err = exeCmd("openssl verify -CAfile data/root-ca.pem " + issuerCert)
+ _, err = exeCmd("openssl verify -CAfile " + CERT_FILES_PATH + "root-01-cert.pem " + issuerCert)
if err != nil {
return errors.New("could not verify that issuer was issued by our Root CA")
}
@@ -547,38 +733,93 @@ func (ci *CertificateInfo) ImportCerts(path string, rootCert string, rootKey str
return nil
}
-// MoveFiles moves certificate / key files to their final location
-func (ci *CertificateInfo) MoveFiles(path string, rootCert string, rootKey string, issuerCert string, issuerKey string) error {
- if rootCert != "" {
- if _, err := exeCmd("mv " + rootCert + " " + path); err != nil {
- return reportError(err)
- }
- }
+// ImportFiles moves certificate files to their final location and imports the keys into the HSM
+func (ci *CertificateInfo) ImportFiles(path string, rootCert string, rootKey string, issuerCert string, issuerKey string) error {
if rootKey != "" {
keyFileExists := true
if _, err := os.Stat(rootKey); errors.Is(err, fs.ErrNotExist) {
keyFileExists = false
}
if keyFileExists {
- if _, err := exeCmd("mv " + rootKey + " " + path); err != nil {
- return reportError(err)
+ rootseqnr := "01"
+ cfg := &HSMConfig{}
+ cfg.Initialize("root", rootseqnr)
+ if err := cfg.CreateSlot(); err != nil {
+ return fmt.Errorf("failed to create root slot: %s", err.Error())
+ }
+
+ pubKey, err := cfg.ImportKeyCert(rootKey, rootCert)
+ if err != nil {
+ return fmt.Errorf("failed to import root key: %s", err.Error())
+ }
+
+ var pubKeyBytes []byte
+ if reflect.TypeOf(pubKey).String() == "rsa.PublicKey" {
+ pk := pubKey.(rsa.PublicKey)
+ pubKeyBytes, err = x509.MarshalPKIXPublicKey(&pk)
+ } else if reflect.TypeOf(pubKey).String() == "ecdsa.PublicKey" {
+ pk := pubKey.(ecdsa.PublicKey)
+ pubKeyBytes, err = x509.MarshalPKIXPublicKey(&pk)
+ } else {
+ return fmt.Errorf("unknown private key type: %s", reflect.TypeOf(pubKey).String())
+ }
+ if err != nil {
+ return fmt.Errorf("failed to marshal root pubkey: %s", err.Error())
+ }
+ file, err := os.Create(fmt.Sprintf("%sroot-%s-pubkey.pem", CERT_FILES_PATH, rootseqnr))
+ if err != nil {
+ return fmt.Errorf("failed to create root pubkey file: %s", err.Error())
+ }
+ defer file.Close()
+ if err := pem.Encode(file, &pem.Block{Type: "PUBLIC KEY", Bytes: pubKeyBytes}); err != nil {
+ return fmt.Errorf("failed to write root pubkey: %s", err.Error())
}
}
}
- if issuerCert != "" {
- if _, err := exeCmd("mv " + issuerCert + " data/issuer/"); err != nil {
- return reportError(err)
- }
- }
- if issuerKey != "" {
- if _, err := exeCmd("mv " + issuerKey + " data/issuer/"); err != nil {
+ if rootCert != "" {
+ if _, err := exeCmd("mv " + rootCert + " " + path); err != nil {
return reportError(err)
}
}
- if (issuerCert != "") && (issuerKey != "") && ci.IsRoot {
- if err := postCreateTasks(path+"issuer/", "ca-int", false); err != nil {
- return err
+ if issuerKey != "" {
+ seqnr := "01"
+ cfg := &HSMConfig{}
+ cfg.Initialize("issuer", seqnr)
+ if err := cfg.CreateSlot(); err != nil {
+ return fmt.Errorf("failed to create issuer slot: %s", err.Error())
+ }
+
+ pubKey, err := cfg.ImportKeyCert(issuerKey, issuerCert)
+ if err != nil {
+ return reportError(err)
+ }
+
+ var pubKeyBytes []byte
+ if reflect.TypeOf(pubKey).String() == "rsa.PublicKey" {
+ pk := pubKey.(rsa.PublicKey)
+ pubKeyBytes, err = x509.MarshalPKIXPublicKey(&pk)
+ } else if reflect.TypeOf(pubKey).String() == "ecdsa.PublicKey" {
+ pk := pubKey.(ecdsa.PublicKey)
+ pubKeyBytes, err = x509.MarshalPKIXPublicKey(&pk)
+ } else {
+ return fmt.Errorf("unknown private key type: %s", reflect.TypeOf(pubKey).String())
+ }
+ if err != nil {
+ return fmt.Errorf("failed to marshal issuer pubkey: %s", err.Error())
+ }
+ file, err := os.Create(fmt.Sprintf("%sissuer-%s-pubkey.pem", CERT_FILES_PATH, seqnr))
+ if err != nil {
+ return fmt.Errorf("failed to create issuer pubkey file: %s", err.Error())
+ }
+ defer file.Close()
+ if err := pem.Encode(file, &pem.Block{Type: "PUBLIC KEY", Bytes: pubKeyBytes}); err != nil {
+ return fmt.Errorf("failed to write issuer pubkey: %s", err.Error())
+ }
+ }
+ if issuerCert != "" {
+ if _, err := exeCmd("mv " + issuerCert + " " + path); err != nil {
+ return reportError(err)
}
}
@@ -586,15 +827,33 @@ func (ci *CertificateInfo) MoveFiles(path string, rootCert string, rootKey strin
}
// Extract key and certificate files from a container file
-func (ci *CertificateInfo) Extract(path string, certBase string, tmpDir string, wasCSR bool) error {
+func (ci *CertificateInfo) Extract(certBase string, tmpDir string, wasCSR bool) error {
var rootCert string
var rootKey string
var issuerCert string
var issuerKey string
+ path := CERT_FILES_PATH // TODO !!
+
if ci.IsRoot {
- rootCert = filepath.Join(tmpDir, "root-ca.pem")
- rootKey = filepath.Join(tmpDir, "root-ca.key")
+ rootCert = filepath.Join(tmpDir, "root-01-cert.pem")
+ rootKey = filepath.Join(tmpDir, "root-01-key.pem")
+
+ if _, err := os.Stat(rootCert); errors.Is(err, fs.ErrNotExist) {
+ altCert := filepath.Join(tmpDir, "root-ca.pem")
+ if _, err = os.Stat(altCert); err == nil {
+ if _, err := exeCmd("mv " + altCert + " " + rootCert); err != nil {
+ return err
+ }
+ }
+
+ altKey := filepath.Join(tmpDir, "root-ca.key")
+ if _, err = os.Stat(altKey); err == nil {
+ if _, err := exeCmd("mv " + altKey + " " + rootKey); err != nil {
+ return err
+ }
+ }
+ }
if _, err := os.Stat(rootCert); errors.Is(err, fs.ErrNotExist) {
altCert := filepath.Join(tmpDir, "test-root.pem")
@@ -612,19 +871,19 @@ func (ci *CertificateInfo) Extract(path string, certBase string, tmpDir string,
}
if _, err := os.Stat(rootCert); errors.Is(err, fs.ErrNotExist) {
- return errors.New("file does not contain root-ca.pem")
+ return errors.New("file does not contain root certificate")
}
}
}
- issuerCert = filepath.Join(tmpDir, "ca-int.pem")
- issuerKey = filepath.Join(tmpDir, "ca-int.key")
+ issuerCert = filepath.Join(tmpDir, "issuer-01-cert.pem")
+ issuerKey = filepath.Join(tmpDir, "issuer-01-key.pem")
if _, err := os.Stat(issuerCert); errors.Is(err, fs.ErrNotExist) {
if ci.IsRoot {
issuerCert = ""
} else {
- altCert := filepath.Join(tmpDir, "test-ca.pem")
+ altCert := filepath.Join(tmpDir, "ca-int.pem")
if _, err = os.Stat(altCert); err == nil {
if _, err := exeCmd("mv " + altCert + " " + issuerCert); err != nil {
return err
@@ -632,7 +891,16 @@ func (ci *CertificateInfo) Extract(path string, certBase string, tmpDir string,
}
if _, err := os.Stat(issuerCert); errors.Is(err, fs.ErrNotExist) {
- return errors.New("file does not contain ca-int.pem")
+ altCert := filepath.Join(tmpDir, "test-ca.pem")
+ if _, err = os.Stat(altCert); err == nil {
+ if _, err := exeCmd("mv " + altCert + " " + issuerCert); err != nil {
+ return err
+ }
+ }
+
+ if _, err := os.Stat(issuerCert); errors.Is(err, fs.ErrNotExist) {
+ return errors.New("file does not contain issuer certificate")
+ }
}
}
}
@@ -640,7 +908,7 @@ func (ci *CertificateInfo) Extract(path string, certBase string, tmpDir string,
if ci.IsRoot || wasCSR {
issuerKey = ""
} else {
- altKey := filepath.Join(tmpDir, "test-ca.key")
+ altKey := filepath.Join(tmpDir, "ca-int.key")
if _, err = os.Stat(altKey); err == nil {
if _, err := exeCmd("mv " + altKey + " " + issuerKey); err != nil {
return err
@@ -648,18 +916,27 @@ func (ci *CertificateInfo) Extract(path string, certBase string, tmpDir string,
}
if _, err := os.Stat(issuerKey); errors.Is(err, fs.ErrNotExist) {
- return errors.New("file does not contain ca-int.key")
+ altKey := filepath.Join(tmpDir, "test-ca.key")
+ if _, err = os.Stat(altKey); err == nil {
+ if _, err := exeCmd("mv " + altKey + " " + issuerKey); err != nil {
+ return err
+ }
+ }
+
+ if _, err := os.Stat(issuerKey); errors.Is(err, fs.ErrNotExist) {
+ return errors.New("file does not contain issuer key")
+ }
}
}
}
- err := ci.ImportCerts(path, rootCert, rootKey, issuerCert, issuerKey)
+ err := ci.VerifyCerts(path, rootCert, rootKey, issuerCert, issuerKey)
if err != nil {
return err
}
// All is good now, move files to their permanent location...
- err = ci.MoveFiles(path, rootCert, rootKey, issuerCert, issuerKey)
+ err = ci.ImportFiles(path, rootCert, rootKey, issuerCert, issuerKey)
if err != nil {
return err
}
@@ -682,8 +959,17 @@ func (ci *CertificateInfo) Extract(path string, certBase string, tmpDir string,
return err
}
numDays := time.Until(crt.NotAfter).Hours() / 24
- if err := updateRootCRLDays("data/openssl.cnf", int(math.Ceil(numDays))); err != nil {
- return err
+ // TODO: adjust for max root ceremony value...
+ viper.Set("crl_root_days", int(math.Ceil(numDays)))
+ viper.WriteConfig()
+
+ } else {
+ // Create CRLs stating that the intermediates are not revoked.
+ err = ci.CeremonyRootCRL("01")
+
+ if err != nil {
+ log.Printf("failed to create crl: %s", err.Error())
+ return errors.New("failed to create crl, see logs for details")
}
}
@@ -691,11 +977,7 @@ func (ci *CertificateInfo) Extract(path string, certBase string, tmpDir string,
}
// Create a new pair of key + certificate files based on the info in CertificateInfo
-func (ci *CertificateInfo) Create(path string, certBase string, wasCSR bool) error {
- if err := preCreateTasks(path); err != nil {
- return err
- }
-
+func (ci *CertificateInfo) Create(certBase string, wasCSR bool) error {
tmpDir, err := os.MkdirTemp("", "labca")
if err != nil {
return err
@@ -706,15 +988,15 @@ func (ci *CertificateInfo) Create(path string, certBase string, wasCSR bool) err
var tmpKey string
var tmpCert string
if ci.IsRoot {
- tmpKey = filepath.Join(tmpDir, "root-ca.key")
- tmpCert = filepath.Join(tmpDir, "root-ca.pem")
+ tmpKey = filepath.Join(tmpDir, "root-01-key.pem")
+ tmpCert = filepath.Join(tmpDir, "root-01-cert.pem")
} else {
- tmpKey = filepath.Join(tmpDir, "ca-int.key")
- tmpCert = filepath.Join(tmpDir, "ca-int.pem")
+ tmpKey = filepath.Join(tmpDir, "issuer-01-key.pem")
+ tmpCert = filepath.Join(tmpDir, "issuer-01-cert.pem")
}
if ci.CreateType == "generate" {
- err := ci.Generate(path, certBase)
+ err := ci.Generate(certBase)
if err != nil {
return err
}
@@ -737,42 +1019,12 @@ func (ci *CertificateInfo) Create(path string, certBase string, wasCSR bool) err
// This is shared between pfx/zip import and pem text upload
if ci.CreateType != "generate" {
- err := ci.Extract(path, certBase, tmpDir, wasCSR)
+ err := ci.Extract(certBase, tmpDir, wasCSR)
if err != nil {
return err
}
}
- if err := postCreateTasks(path, certBase, ci.IsRoot); err != nil {
- return err
- }
-
- if ci.IsRoot {
- keyFileExists := true
- if _, err := os.Stat(path + certBase + ".key"); errors.Is(err, fs.ErrNotExist) {
- keyFileExists = false
- }
- if keyFileExists {
- if _, err := exeCmd("openssl ca -config " + path + "openssl.cnf -gencrl -keyfile " + path + certBase + ".key -cert " + path + certBase + ".pem -out " + path + certBase + ".crl"); err != nil {
- return reportError(err)
- }
- }
- }
-
- return nil
-}
-
-func postCreateTasks(path string, certBase string, isRoot bool) error {
- if !isRoot {
- if _, err := exeCmd("openssl pkey -in " + path + certBase + ".key -out " + path + certBase + ".key.der -outform der"); err != nil {
- return reportError(err)
- }
- }
-
- if _, err := exeCmd("openssl x509 -in " + path + certBase + ".pem -out " + path + certBase + ".der -outform DER"); err != nil {
- return reportError(err)
- }
-
return nil
}
@@ -835,7 +1087,7 @@ func (ci *CertificateInfo) StoreRootKey(path string) bool {
defer os.RemoveAll(tmpDir)
- certBase := "root-ca"
+ certBase := "root-01"
if res, newError := storeRootKey(path, certBase, tmpDir, ci.Key, ci.Passphrase); !res {
ci.Errors["Modal"] = newError
return false
@@ -898,125 +1150,94 @@ func (ci *CertificateInfo) StoreCRL(path string) bool {
return true
}
-func renewCertificate(certname string, days int, rootname string, rootkeyfile string, passphrase string) error {
- certFile := locateFile(certname + ".pem")
- path := filepath.Dir(certFile) + "/"
- certBase := path + certname
- keyFile := certBase + ".key"
- rootCert := ""
- rootKey := keyFile
+func renewCertificate(certname string, days int, rootname string, _ string, _ string) error {
+ ci := &CertificateInfo{
+ IsRoot: strings.HasPrefix(certname, "root-"),
+ NumDays: days,
+ }
+ ci.Initialize()
- if strings.HasPrefix(certname, "ca-int") || strings.HasPrefix(certname, "test-ca") {
- rootCert = locateFile(rootname + ".pem")
- rootKey = locateFile(rootname + ".key")
+ certFile := fmt.Sprintf("%s%s.pem", CERT_FILES_PATH, certname)
+ rootCertFile := ""
- // Make sure openssl allows us to add certificates with the same subject
- attrFile := "data/index.txt.attr"
- read, err := os.ReadFile(attrFile)
- if err != nil {
- fmt.Println(err)
- return errors.New("could not read index.txt.attr file: " + err.Error())
- }
- re := regexp.MustCompile(`unique_subject = yes`)
- res := re.ReplaceAll(read, []byte("unique_subject = no"))
-
- if string(res) != string(read) {
- if err = os.WriteFile(attrFile, res, 0640); err != nil {
- fmt.Println(err)
- return errors.New("could not write index.txt.attr file: " + err.Error())
- }
- }
+ if !ci.IsRoot {
+ rootCertFile = fmt.Sprintf("%s%s.pem", CERT_FILES_PATH, rootname)
}
- tmpDir, err := os.MkdirTemp("", "labca")
- if err != nil {
- return err
+ seqnr := ""
+ re := regexp.MustCompile(`-(\d{2})-`)
+ match := re.FindStringSubmatch(certFile)
+ if len(match) > 1 {
+ seqnr = match[1]
+ } else {
+ return fmt.Errorf("failed to extract sequence number from filename '%s'", certFile)
}
- defer os.RemoveAll(tmpDir)
-
- if rootKey != "" {
- _, err = os.Stat(rootKey)
- }
-
- if rootKey == "" || errors.Is(err, fs.ErrNotExist) {
- if rootkeyfile == "" {
- return errors.New("NO_ROOT_KEY")
+ rootseqnr := ""
+ if !ci.IsRoot {
+ match := re.FindStringSubmatch(rootCertFile)
+ if len(match) > 1 {
+ rootseqnr = match[1]
} else {
- if res, newError := storeRootKey(path, rootname, tmpDir, rootkeyfile, passphrase); !res {
- return errors.New("NO_ROOT_KEY:" + newError)
- }
- rootKey = path + rootname + ".key"
- defer exeCmd("rm " + rootKey)
+ return fmt.Errorf("failed to extract sequence number from filename '%s'", rootCertFile)
}
}
- r, err := exeCmd("openssl x509 -noout -subject -nameopt utf8 -in " + certFile)
+ crt, err := readCertificate(certFile)
if err != nil {
- return err
+ return fmt.Errorf("failed to read current certificate: %w", err)
}
- subject := string(r[8 : len(r)-1])
- subject = "/" + strings.ReplaceAll(subject, ", ", "/")
- subject = strings.Replace(subject, " ", "\\\\", -1)
- if rootKey == keyFile {
- if _, err := exeCmd("openssl req -config data/openssl.cnf -days " + strconv.Itoa(days) + " -new -utf8 -x509 -extensions v3_ca -subj " + subject +
- " -key " + keyFile + " -out " + certFile + ".tmp"); err != nil {
- return reportError(err)
+ if crt.PublicKeyAlgorithm == x509.RSA {
+ pub := crt.PublicKey.(*rsa.PublicKey)
+ if pub.N.BitLen() == 2048 {
+ ci.KeyType = "rsa2048"
}
-
- if err := updateRootCRLDays("data/openssl.cnf", days); err != nil {
- return reportError(err)
+ if pub.N.BitLen() == 4096 {
+ ci.KeyType = "rsa4096"
}
-
- if _, err := exeCmd("openssl ca -config data/openssl.cnf -gencrl -keyfile " + keyFile + " -cert " + certFile + ".tmp -out " + certBase + ".crl"); err != nil {
- return reportError(err)
+ }
+ if crt.PublicKeyAlgorithm == x509.ECDSA {
+ if crt.SignatureAlgorithm == x509.ECDSAWithSHA256 {
+ ci.KeyType = "ecdsa256"
}
+ if crt.SignatureAlgorithm == x509.ECDSAWithSHA384 {
+ ci.KeyType = "ecdsa384"
+ }
+ }
+ subjectMap := parseSubjectDn(crt.Subject.String())
+ if val, ok := subjectMap["C"]; ok {
+ ci.Country = val
+ }
+ if val, ok := subjectMap["O"]; ok {
+ ci.Organization = val
+ }
+ if val, ok := subjectMap["CN"]; ok {
+ ci.CommonName = val
+ }
+
+ if ci.IsRoot {
+ _, err = ci.CeremonyRoot(seqnr, true)
+
+ viper.Set("crl_root_days", ci.NumDays)
+ viper.WriteConfig()
} else {
- if _, err := exeCmd("openssl req -config data/issuer/openssl.cnf -new -utf8 -subj " + subject + " -key " + keyFile + " -out " + certBase + ".csr"); err != nil {
- return reportError(err)
- }
- if out, err := exeCmd("openssl ca -config data/openssl.cnf -cert " + rootCert + " -keyfile " + rootKey + " -extensions v3_intermediate_ca -days " +
- strconv.Itoa(days) + " -md sha384 -notext -batch -in " + certBase + ".csr -out " + certFile + ".tmp"); err != nil {
- if strings.Contains(string(out), ".key for reading, No such file or directory") {
- fmt.Println(out)
- return errors.New("NO_ROOT_KEY")
- }
- return reportError(err)
- }
-
- }
- if _, err := exeCmd("mv " + certFile + ".tmp " + certFile); err != nil {
- return reportError(err)
+ _, err = ci.CeremonyIssuer(seqnr, rootseqnr, true)
}
- // TODO: need to get rid of this!
- if rootKey == keyFile {
- if strings.HasPrefix(certname, "test-root") {
- dataFile := locateFile("root-ca.pem")
- if _, err := exeCmd("cp " + certFile + " " + dataFile); err != nil {
- fmt.Println(err)
- }
- dataKeyFile := strings.TrimSuffix(dataFile, filepath.Ext(dataFile)) + ".key"
- if _, err := exeCmd("cp " + keyFile + " " + dataKeyFile); err != nil {
- fmt.Println(err)
- }
- crlFile := strings.TrimSuffix(dataFile, filepath.Ext(dataFile)) + ".crl"
- if _, err := exeCmd("cp " + certBase + ".crl " + crlFile); err != nil {
- fmt.Println(err)
- }
- }
- } else {
- if strings.HasPrefix(certname, "test-ca") {
- dataFile := locateFile("ca-int.pem")
- if _, err := exeCmd("cp " + certFile + " " + dataFile); err != nil {
- fmt.Println(err)
- }
- dataKeyFile := strings.TrimSuffix(dataFile, filepath.Ext(dataFile)) + ".key"
- if _, err := exeCmd("cp " + keyFile + " " + dataKeyFile); err != nil {
- fmt.Println(err)
- }
+ if err != nil {
+ log.Printf("failed to create certificate: %s", err.Error())
+ return errors.New("failed to create certificate, see logs for details")
+ }
+
+ if !ci.IsRoot {
+ // Create CRLs stating that the intermediates are not revoked.
+ err = ci.CeremonyRootCRL(rootseqnr)
+
+ if err != nil {
+ log.Printf("failed to create crl: %s", err.Error())
+ return errors.New("failed to create crl, see logs for details")
}
}
@@ -1057,4 +1278,4 @@ func exeCmd(cmd string) ([]byte, error) {
fmt.Print(fmt.Sprint(err) + ": " + string(out))
}
return out, err
-}
\ No newline at end of file
+}
diff --git a/gui/chains.go b/gui/chains.go
index 2230a4d..2823a90 100644
--- a/gui/chains.go
+++ b/gui/chains.go
@@ -1,6 +1,7 @@
package main
import (
+ "crypto/x509"
"encoding/json"
"errors"
"fmt"
@@ -58,6 +59,7 @@ type CertDetails struct {
CertFile string
BaseName string
Subject string
+ KeyType string
IsRoot bool
ActiveIssuer bool
NotAfter string
@@ -69,6 +71,22 @@ type CertChain struct {
IssuerCerts []CertDetails
}
+func getCertFileKeyType(certFile string) (string, error) {
+ crt, err := readCertificate(certFile)
+ if err != nil {
+ fmt.Println("cannot read certificate file '" + certFile + "': " + fmt.Sprint(err))
+ return "", err
+ }
+
+ if crt.PublicKeyAlgorithm == x509.RSA {
+ return "RSA", nil
+ } else if crt.PublicKeyAlgorithm == x509.ECDSA {
+ return "ECDSA", nil
+ } else {
+ return "", fmt.Errorf("unknown public key algorithm: %s", crt.PublicKeyAlgorithm)
+ }
+}
+
func getCertFileDetails(certFile string) (string, error) {
var details string
@@ -139,6 +157,9 @@ func enhanceChains(chains []CertChain) []CertChain {
if chains[k].IssuerCerts[n].CertFile == rawChains[i].Location.CertFile {
chains[k].IssuerCerts[n].ActiveIssuer = rawChains[i].Active
certFile := locateFile(rawChains[i].Location.CertFile)
+ if kt, err := getCertFileKeyType(certFile); err == nil {
+ chains[k].IssuerCerts[n].KeyType = kt
+ }
if d, err := getCertFileDetails(certFile); err == nil {
chains[k].IssuerCerts[n].Details = d
}
@@ -153,6 +174,9 @@ func enhanceChains(chains []CertChain) []CertChain {
if chains[k].RootCert.Subject == "" {
certFile := locateFile(chains[k].RootCert.CertFile)
+ if kt, err := getCertFileKeyType(certFile); err == nil {
+ chains[k].RootCert.KeyType = kt
+ }
if d, err := getCertFileDetails(certFile); err == nil {
chains[k].RootCert.Details = d
}
diff --git a/gui/go.mod b/gui/go.mod
index c47579b..f72bc82 100644
--- a/gui/go.mod
+++ b/gui/go.mod
@@ -12,6 +12,7 @@ require (
github.com/gorilla/sessions v1.2.1
github.com/gorilla/websocket v1.5.0
github.com/jmoiron/sqlx v1.3.5
+ github.com/miekg/pkcs11 v1.1.1
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354
github.com/pkg/errors v0.9.1
github.com/smallstep/certificates v0.24.2
diff --git a/gui/go.sum b/gui/go.sum
index 774e0a6..2385049 100644
--- a/gui/go.sum
+++ b/gui/go.sum
@@ -694,6 +694,8 @@ github.com/micromdm/scep/v2 v2.1.0/go.mod h1:BkF7TkPPhmgJAMtHfP+sFTKXmgzNJgLQlvv
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
+github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
+github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
diff --git a/gui/hsm.go b/gui/hsm.go
new file mode 100644
index 0000000..4e3e799
--- /dev/null
+++ b/gui/hsm.go
@@ -0,0 +1,733 @@
+package main
+
+import (
+ "crypto"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/subtle"
+ "crypto/x509"
+ "encoding/asn1"
+ "encoding/binary"
+ "encoding/hex"
+ "encoding/pem"
+ "errors"
+ "fmt"
+ "math/big"
+ "os"
+ "path"
+ "path/filepath"
+ "reflect"
+ "strconv"
+ "strings"
+
+ "github.com/miekg/pkcs11"
+)
+
+const CERT_FILES_PATH = "/opt/boulder/labca/certs/webpki/"
+
+type HSMConfig struct {
+ Module string
+ UserPIN string
+ SOPIN string
+ SlotID string
+ Label string
+}
+
+// HSMSession represents a session with a given PKCS#11 module. It is NOT safe for concurrent access.
+type HSMSession struct {
+ Context PKCSCtx
+ Handle pkcs11.SessionHandle
+}
+
+type PKCSCtx interface {
+ CloseSession(pkcs11.SessionHandle) error
+ CreateObject(pkcs11.SessionHandle, []*pkcs11.Attribute) (pkcs11.ObjectHandle, error)
+ DestroyObject(pkcs11.SessionHandle, pkcs11.ObjectHandle) error
+ FindObjects(pkcs11.SessionHandle, int) ([]pkcs11.ObjectHandle, bool, error)
+ FindObjectsInit(pkcs11.SessionHandle, []*pkcs11.Attribute) error
+ FindObjectsFinal(pkcs11.SessionHandle) error
+ GenerateKey(pkcs11.SessionHandle, []*pkcs11.Mechanism, []*pkcs11.Attribute) (pkcs11.ObjectHandle, error)
+ GetAttributeValue(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error)
+ Logout(pkcs11.SessionHandle) error
+ WrapKey(pkcs11.SessionHandle, []*pkcs11.Mechanism, pkcs11.ObjectHandle, pkcs11.ObjectHandle) ([]byte, error)
+}
+
+func (cfg *HSMConfig) Initialize(ca_type string, seqnr string) {
+ cfg.Module = "/usr/lib/softhsm/libsofthsm2.so"
+ cfg.UserPIN = "1234"
+ cfg.SOPIN = "5678"
+ cfg.SlotID = "0"
+ if ca_type != "root" {
+ cfg.SlotID = "1"
+ }
+ cfg.Label = fmt.Sprintf("%s %s", ca_type, seqnr)
+}
+
+func (cfg *HSMConfig) CreateSlot() error {
+ s, err := strconv.ParseUint(cfg.SlotID, 10, 32)
+ if err != nil {
+ return fmt.Errorf("failed to convert slot id '%s' to uint: %s", cfg.SlotID, err.Error())
+ }
+ id, err := cfg.createSlot(uint(s), cfg.Label)
+ if err != nil {
+ return fmt.Errorf("failed to create slot: %s", err.Error())
+ }
+ cfg.SlotID = id
+
+ return nil
+}
+
+func findSlotWithLabel(ctx *pkcs11.Ctx, label string, missing_ok bool) (string, error) {
+ slots, err := ctx.GetSlotList(true)
+ if err != nil {
+ return "", fmt.Errorf("failed to get slots list: %s", err)
+ }
+
+ for _, slot := range slots {
+ info, err := ctx.GetSlotInfo(slot)
+ if err != nil {
+ return "", fmt.Errorf("failed to get slot info: %s", err)
+ }
+
+ if info.Flags&pkcs11.CKF_TOKEN_PRESENT == pkcs11.CKF_TOKEN_PRESENT {
+ token, err := ctx.GetTokenInfo(slot)
+ if err != nil {
+ return "", fmt.Errorf("failed to get token info: %s", err)
+ }
+
+ if token.Label == label {
+ return fmt.Sprint(slot), nil
+ }
+ }
+ }
+
+ if missing_ok {
+ return "", nil
+ }
+
+ return "", errors.New("no slot found matching this label")
+}
+
+func (cfg *HSMConfig) createSlot(slotId uint, label string) (string, error) {
+ ctx := pkcs11.New(cfg.Module)
+ if ctx == nil {
+ return "", errors.New("failed to load pkcs11 module")
+ }
+ err := ctx.Initialize()
+ if err != nil && err.Error() != "pkcs11: 0x191: CKR_CRYPTOKI_ALREADY_INITIALIZED" {
+ return "", fmt.Errorf("failed to initialize pkcs11 context: %s", err)
+ }
+
+ slot, err := findSlotWithLabel(ctx, label, true)
+ if err != nil {
+ return "", err
+ }
+ if slot != "" {
+ return slot, nil
+ }
+
+ // No slot found with this token label, so create a new one
+ err = ctx.InitToken(slotId, cfg.SOPIN, label)
+ if err != nil {
+ if strings.Contains(err.Error(), "0x3: CKR_SLOT_ID_INVALID") {
+ slots, err := ctx.GetSlotList(true)
+ if err != nil {
+ return "", fmt.Errorf("failed to initialize token, failed to get slot list: %s", err)
+ }
+ slotId = uint(len(slots) - 1)
+ cfg.SlotID = fmt.Sprint(slotId)
+ err = ctx.InitToken(slotId, cfg.SOPIN, label)
+ if err != nil {
+ return "", fmt.Errorf("failed to initialize token with id %d: %s", slotId, err)
+ }
+ } else {
+ return "", fmt.Errorf("failed to initialize token: %s", err)
+ }
+ }
+
+ session, err := ctx.OpenSession(slotId, pkcs11.CKF_SERIAL_SESSION|pkcs11.CKF_RW_SESSION)
+ if err != nil {
+ return "", fmt.Errorf("failed to open session: %s", err)
+ }
+ defer ctx.CloseSession(session)
+
+ err = ctx.Login(session, pkcs11.CKU_SO, cfg.SOPIN)
+ if err != nil {
+ if err.Error() == "pkcs11: 0xA0: CKR_PIN_INCORRECT" {
+ return "", errors.New("incorrect SO PIN")
+ } else {
+ return "", fmt.Errorf("failed to login: %s", err)
+ }
+ }
+ defer ctx.Logout(session)
+
+ err = ctx.InitPIN(session, cfg.UserPIN)
+ if err != nil {
+ return "", fmt.Errorf("failed to initialize pin: %s", err)
+ }
+
+ // Forced reconnect to get the renumbered slots from SoftHSM2
+ ctx.Finalize()
+ ctx.Destroy()
+ ctx = pkcs11.New(cfg.Module)
+ if ctx == nil {
+ return "", errors.New("failed to reload pkcs11 module")
+ }
+ err = ctx.Initialize()
+ if err != nil && err.Error() != "pkcs11: 0x191: CKR_CRYPTOKI_ALREADY_INITIALIZED" {
+ return "", fmt.Errorf("failed to reinitialize pkcs11 context: %s", err)
+ }
+
+ slot, err = findSlotWithLabel(ctx, label, false)
+ if err != nil {
+ return "", err
+ }
+ if slot != "" {
+ return slot, nil
+ }
+
+ return "", errors.New("failed to create slot")
+}
+
+// getSession establishes a logged in session on a pkcs11 slot.
+//
+// Don't forget to call .Close() on the resulting session when done!
+func (cfg *HSMConfig) getSession() (*HSMSession, error) {
+ ctx := pkcs11.New(cfg.Module)
+ if ctx == nil {
+ return nil, errors.New("failed to load pkcs11 module")
+ }
+ err := ctx.Initialize()
+ if err != nil && err.Error() != "pkcs11: 0x191: CKR_CRYPTOKI_ALREADY_INITIALIZED" {
+ return nil, fmt.Errorf("failed to initialize pkcs11 context: %s", err)
+ }
+
+ slot, err := findSlotWithLabel(ctx, cfg.Label, true)
+ if err != nil {
+ return nil, err
+ }
+ if slot == "" {
+ return nil, nil
+ }
+
+ s, err := strconv.ParseUint(slot, 10, 32)
+ if err != nil {
+ return nil, fmt.Errorf("failed to convert slot id '%s' to uint: %s", cfg.SlotID, err.Error())
+ }
+
+ session, err := ctx.OpenSession(uint(s), pkcs11.CKF_SERIAL_SESSION|pkcs11.CKF_RW_SESSION)
+ if err != nil {
+ return nil, fmt.Errorf("failed to open session: %s", err)
+ }
+
+ err = ctx.Login(session, pkcs11.CKU_USER, cfg.UserPIN)
+ if err != nil {
+ if err.Error() == "pkcs11: 0xA0: CKR_PIN_INCORRECT" {
+ return nil, errors.New("incorrect user PIN")
+ } else {
+ return nil, fmt.Errorf("failed to login: %s", err)
+ }
+ }
+
+ return &HSMSession{ctx, session}, nil
+}
+
+func (cfg *HSMConfig) ClearAll() error {
+ hs, err := cfg.getSession()
+ if err != nil {
+ return fmt.Errorf("failed to get session: %s", err)
+ }
+ defer hs.Close()
+
+ err = hs.DestroyAllObjects(cfg.Label)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func arrConcat(arrays ...[]byte) []byte {
+ out := make([]byte, len(arrays[0]))
+ copy(out, arrays[0])
+ for _, array := range arrays[1:] {
+ out = append(out, array...)
+ }
+
+ return out
+}
+
+func arrXor(arrL []byte, arrR []byte) []byte {
+ out := make([]byte, len(arrL))
+ for x := range arrL {
+ out[x] = arrL[x] ^ arrR[x]
+ }
+ return out
+}
+
+// AES Key Wrap algorithm is specified in RFC 3394
+func UnwrapKey(block cipher.Block, cipherText []byte) ([]byte, error) {
+ //Initialize variables
+ a := make([]byte, 8)
+ n := (len(cipherText) / 8) - 1
+
+ r := make([][]byte, n)
+ for i := range r {
+ r[i] = make([]byte, 8)
+ copy(r[i], cipherText[(i+1)*8:])
+ }
+ copy(a, cipherText[:8])
+
+ //Compute intermediate values
+ for j := 5; j >= 0; j-- {
+ for i := n; i >= 1; i-- {
+ t := (n * j) + i
+ tBytes := make([]byte, 8)
+ binary.BigEndian.PutUint64(tBytes, uint64(t))
+
+ b := arrConcat(arrXor(a, tBytes), r[i-1])
+ block.Decrypt(b, b)
+
+ copy(a, b[:len(b)/2])
+ copy(r[i-1], b[len(b)/2:])
+ }
+ }
+
+ var defaultIV = []byte{0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6}
+ if subtle.ConstantTimeCompare(a, defaultIV) != 1 {
+ return nil, errors.New("integrity check failed - unexpected IV")
+ }
+
+ //Output
+ c := arrConcat(r...)
+ return c, nil
+}
+
+func (cfg *HSMConfig) GetPrivateKey() ([]byte, error) {
+ hs, err := cfg.getSession()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get session: %s", err)
+ }
+ defer hs.Close()
+
+ tmpl := []*pkcs11.Attribute{
+ pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PRIVATE_KEY),
+ pkcs11.NewAttribute(pkcs11.CKA_LABEL, []byte(cfg.Label)),
+ }
+
+ keyHandle, err := hs.FindObject(tmpl)
+ if err != nil {
+ return nil, fmt.Errorf("failed to find private key with label='%s': %w", cfg.Label, err)
+ }
+
+ // Generate a temporary wrapping key in memory
+ mechs := []*pkcs11.Mechanism{
+ // pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so -M | grep -v generate_key_pair | grep generate
+ pkcs11.NewMechanism(pkcs11.CKM_AES_KEY_GEN, nil),
+ }
+ tmpl = []*pkcs11.Attribute{
+ pkcs11.NewAttribute(pkcs11.CKA_VALUE_LEN, 16),
+ pkcs11.NewAttribute(pkcs11.CKA_ENCRYPT, true),
+ pkcs11.NewAttribute(pkcs11.CKA_DECRYPT, true),
+ pkcs11.NewAttribute(pkcs11.CKA_WRAP, true),
+ pkcs11.NewAttribute(pkcs11.CKA_UNWRAP, true),
+ pkcs11.NewAttribute(pkcs11.CKA_EXTRACTABLE, true),
+ pkcs11.NewAttribute(pkcs11.CKA_TOKEN, false),
+ }
+ wrapKeyHandle, err := hs.GenerateKey(mechs, tmpl)
+ if err != nil {
+ return nil, fmt.Errorf("failed to generate wrapping key: %w", err)
+ }
+
+ // Extract the key value
+ tmpl = []*pkcs11.Attribute{
+ pkcs11.NewAttribute(pkcs11.CKA_VALUE, nil),
+ }
+ wrapKeyAttrs, err := hs.GetAttributeValue(wrapKeyHandle, tmpl)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get attribute values from object: %w", err)
+ }
+ var wrapKey []byte
+ for _, wrapKeyAttr := range wrapKeyAttrs {
+ switch wrapKeyAttr.Type {
+ case pkcs11.CKA_VALUE:
+ wrapKey = wrapKeyAttr.Value
+ default:
+ if wrapKeyAttr.Value == nil {
+ fmt.Printf("unexpected attribute #%d: nil\n", wrapKeyAttr.Type)
+ } else {
+ fmt.Printf("unexpected attribute #%d: %s / %s\n", wrapKeyAttr.Type, hex.EncodeToString(wrapKeyAttr.Value), wrapKeyAttr.Value)
+ }
+ }
+ }
+
+ // Wrap the private key on the HSM
+ mechs = []*pkcs11.Mechanism{
+ // pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so -M | grep wrap
+ pkcs11.NewMechanism(pkcs11.CKM_AES_KEY_WRAP, nil),
+ }
+ wrappedKey, err := hs.WrapKey(mechs, wrapKeyHandle, keyHandle)
+ if err != nil {
+ return nil, fmt.Errorf("failed to wrap private key: %w", err)
+ }
+
+ // Unwrap the key locally
+ c, err := aes.NewCipher(wrapKey)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create new aes cipher: %w", err)
+ }
+ key, err := UnwrapKey(c, wrappedKey)
+ if err != nil {
+ return nil, fmt.Errorf("failed to unwrap key: %w", err)
+ }
+
+ return key, nil
+}
+
+func loadKey(filename string) (crypto.PrivateKey, crypto.PublicKey, error) {
+ var priv crypto.PrivateKey
+ var pub crypto.PublicKey
+
+ keyPEM, err := os.ReadFile(filename)
+ if err != nil {
+ return priv, pub, err
+ }
+
+ block, _ := pem.Decode(keyPEM)
+ if block == nil {
+ return priv, pub, fmt.Errorf("no data in key PEM file %s", filename)
+ }
+
+ parseResult, _ := x509.ParsePKCS8PrivateKey(block.Bytes)
+ if reflect.TypeOf(parseResult).String() == "*rsa.PrivateKey" {
+ k := parseResult.(*rsa.PrivateKey)
+ priv = k
+ pub = k.PublicKey
+ } else if reflect.TypeOf(parseResult).String() == "*ecdsa.PrivateKey" {
+ k := parseResult.(*ecdsa.PrivateKey)
+ priv = k
+ pub = k.PublicKey
+ } else {
+ return priv, pub, fmt.Errorf("unknown private key type '%s'", reflect.TypeOf(parseResult).String())
+ }
+
+ if priv == nil {
+ fmt.Printf("WARNING: unknown private key type for %+v\n", parseResult)
+ return priv, pub, errors.New("unknown private key type")
+ }
+
+ return priv, pub, nil
+}
+
+func loadCert(filename string) (*x509.Certificate, error) {
+ certPEM, err := os.ReadFile(filename)
+ if err != nil {
+ return nil, err
+ }
+
+ block, _ := pem.Decode(certPEM)
+ if block == nil {
+ return nil, fmt.Errorf("no data in certificate PEM file %s", filename)
+ }
+
+ parseResult, err := x509.ParseCertificate(block.Bytes)
+ if err != nil {
+ return nil, fmt.Errorf("could not parse certificate: %s", err.Error())
+ }
+
+ return parseResult, nil
+}
+
+var curveToOIDDER = map[string][]byte{
+ elliptic.P224().Params().Name: {6, 5, 43, 129, 4, 0, 33},
+ elliptic.P256().Params().Name: {6, 8, 42, 134, 72, 206, 61, 3, 1, 7},
+ elliptic.P384().Params().Name: {6, 5, 43, 129, 4, 0, 34},
+ elliptic.P521().Params().Name: {6, 5, 43, 129, 4, 0, 35},
+}
+
+func storePubKey(hs *HSMSession, pubKey crypto.PublicKey, keyID []byte, label string) error {
+ tmpl := []*pkcs11.Attribute{
+ pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PUBLIC_KEY),
+ pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true),
+ pkcs11.NewAttribute(pkcs11.CKA_PRIVATE, false),
+ pkcs11.NewAttribute(pkcs11.CKA_VERIFY, true),
+ pkcs11.NewAttribute(pkcs11.CKA_ENCRYPT, true),
+ pkcs11.NewAttribute(pkcs11.CKA_WRAP, true),
+ pkcs11.NewAttribute(pkcs11.CKA_ID, keyID),
+ pkcs11.NewAttribute(pkcs11.CKA_LABEL, []byte(label)),
+ }
+
+ if reflect.TypeOf(pubKey).String() == "rsa.PublicKey" {
+ p := pubKey.(rsa.PublicKey)
+
+ tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_RSA))
+ tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_MODULUS, p.N.Bytes()))
+ tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_PUBLIC_EXPONENT, big.NewInt(int64(p.E)).Bytes()))
+ } else if reflect.TypeOf(pubKey).String() == "ecdsa.PublicKey" {
+ p := pubKey.(ecdsa.PublicKey)
+ eh, err := p.ECDH()
+ if err != nil {
+ return fmt.Errorf("failed to convert ecdsa pubkey to ecdh: %s", err.Error())
+ }
+
+ tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_EC))
+
+ encodedCurve := curveToOIDDER[p.Curve.Params().Name]
+ tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, encodedCurve))
+
+ rawValue := asn1.RawValue{
+ Tag: asn1.TagOctetString,
+ Bytes: eh.Bytes(),
+ }
+ marshalledPoint, err := asn1.Marshal(rawValue)
+ if err != nil {
+ return fmt.Errorf("failed to marshall ecdsa point: %s", err.Error())
+ }
+ tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_EC_POINT, marshalledPoint))
+
+ } else {
+ return fmt.Errorf("unknown public key type '%s'", reflect.TypeOf(pubKey).String())
+ }
+
+ _, err := hs.CreateObject(tmpl)
+ if err != nil {
+ fmt.Printf("failed to create public key on HSM: %s\n", err.Error())
+ return err
+ }
+
+ return nil
+}
+
+func storePrivKey(hs *HSMSession, privKey crypto.PrivateKey, keyID []byte, label string, extractable bool) error {
+ tmpl := []*pkcs11.Attribute{
+ pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PRIVATE_KEY),
+ pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true),
+ pkcs11.NewAttribute(pkcs11.CKA_PRIVATE, true),
+ pkcs11.NewAttribute(pkcs11.CKA_SENSITIVE, true),
+ pkcs11.NewAttribute(pkcs11.CKA_EXTRACTABLE, extractable),
+ pkcs11.NewAttribute(pkcs11.CKA_SIGN, true),
+ pkcs11.NewAttribute(pkcs11.CKA_DECRYPT, true),
+ pkcs11.NewAttribute(pkcs11.CKA_DERIVE, true),
+ pkcs11.NewAttribute(pkcs11.CKA_WRAP_WITH_TRUSTED, false),
+ pkcs11.NewAttribute(pkcs11.CKA_UNWRAP, true),
+ pkcs11.NewAttribute(pkcs11.CKA_ID, keyID),
+ pkcs11.NewAttribute(pkcs11.CKA_LABEL, []byte(label)),
+ }
+
+ if reflect.TypeOf(privKey).String() == "*rsa.PrivateKey" {
+ k := privKey.(*rsa.PrivateKey)
+
+ tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_RSA))
+ tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_MODULUS, k.PublicKey.N.Bytes()))
+ tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_PUBLIC_EXPONENT, big.NewInt(int64(k.PublicKey.E)).Bytes()))
+ tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_PRIVATE_EXPONENT, big.NewInt(int64(k.E)).Bytes()))
+ tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_PRIME_1, new(big.Int).Set(k.Primes[0]).Bytes()))
+ tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_PRIME_2, new(big.Int).Set(k.Primes[1]).Bytes()))
+ tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_EXPONENT_1, new(big.Int).Set(k.Precomputed.Dp).Bytes()))
+ tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_EXPONENT_2, new(big.Int).Set(k.Precomputed.Dq).Bytes()))
+ tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_COEFFICIENT, new(big.Int).Set(k.Precomputed.Qinv).Bytes()))
+
+ } else if reflect.TypeOf(privKey).String() == "*ecdsa.PrivateKey" {
+ k := privKey.(*ecdsa.PrivateKey)
+
+ tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_EC))
+ encodedCurve := curveToOIDDER[k.Params().Name]
+ tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, encodedCurve))
+ tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_VALUE, new(big.Int).Set(k.D).Bytes()))
+
+ } else {
+ return fmt.Errorf("unknown private key type '%s'", reflect.TypeOf(privKey).String())
+ }
+
+ _, err := hs.CreateObject(tmpl)
+ if err != nil {
+ fmt.Printf("failed to create private key on HSM: %s\n", err.Error())
+ return err
+ }
+
+ return nil
+}
+
+func storeCertificate(hs *HSMSession, certificate *x509.Certificate, keyID []byte, label string) error {
+ serial, err := asn1.Marshal(certificate.SerialNumber)
+ if err != nil {
+ return err
+ }
+
+ tmpl := []*pkcs11.Attribute{
+ pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_CERTIFICATE),
+ pkcs11.NewAttribute(pkcs11.CKA_CERTIFICATE_TYPE, pkcs11.CKC_X_509),
+ pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true),
+ pkcs11.NewAttribute(pkcs11.CKA_PRIVATE, false),
+ pkcs11.NewAttribute(pkcs11.CKA_SUBJECT, certificate.RawSubject),
+ pkcs11.NewAttribute(pkcs11.CKA_ISSUER, certificate.RawIssuer),
+ pkcs11.NewAttribute(pkcs11.CKA_SERIAL_NUMBER, serial),
+ pkcs11.NewAttribute(pkcs11.CKA_ID, keyID),
+ pkcs11.NewAttribute(pkcs11.CKA_LABEL, []byte(label)),
+ pkcs11.NewAttribute(pkcs11.CKA_VALUE, certificate.Raw),
+ }
+
+ _, err = hs.CreateObject(tmpl)
+ if err != nil {
+ fmt.Printf("failed to create certificate on HSM: %s\n", err.Error())
+ return err
+ }
+
+ return nil
+}
+
+func (cfg *HSMConfig) ImportKeyCert(keyFile, certFile string) (crypto.PublicKey, error) {
+ hs, err := cfg.getSession()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get session: %s", err)
+ }
+ defer hs.Close()
+
+ privKey, pubKey, err := loadKey(keyFile)
+ if err != nil {
+ return pubKey, err
+ }
+
+ keyID := make([]byte, 4)
+ _, err = rand.Read(keyID)
+ if err != nil {
+ return pubKey, err
+ }
+
+ err = storePubKey(hs, pubKey, keyID, cfg.Label)
+ if err != nil {
+ fmt.Printf("failed to store public key on HSM: %s\n", err.Error())
+ return pubKey, err
+ }
+
+ extractable := true // For now, with SoftHSM, this is fine. In future we need to ask for informed consent!
+
+ err = storePrivKey(hs, privKey, keyID, cfg.Label, extractable)
+ if err != nil {
+ fmt.Printf("failed to store private key on HSM: %s\n", err.Error())
+ return pubKey, err
+ }
+
+ if strings.Index(filepath.Base(keyFile), "root-") != 0 {
+ jsonFile := path.Join(CERT_FILES_PATH, filepath.Base(keyFile))
+ jsonFile = strings.Replace(jsonFile, "-key.pem", ".pkcs11.json", -1)
+ contents := fmt.Sprintf(`{"module": %q, "tokenLabel": %q, "pin": %q}`, cfg.Module, cfg.Label, cfg.UserPIN)
+ err = os.WriteFile(jsonFile, []byte(contents), 0644)
+ if err != nil {
+ return pubKey, fmt.Errorf("failed to write '%s' file: %s", jsonFile, err.Error())
+ }
+ }
+
+ if certFile != "" {
+ cert, err := loadCert(certFile)
+ if err != nil {
+ return pubKey, err
+ }
+
+ err = storeCertificate(hs, cert, keyID, cfg.Label)
+ if err != nil {
+ fmt.Printf("failed to store certificate on HSM: %s\n", err.Error())
+ return pubKey, err
+ }
+ }
+
+ return pubKey, nil
+}
+
+func (hs *HSMSession) Close() {
+ hs.Context.CloseSession(hs.Handle)
+ hs.Context.Logout(hs.Handle)
+}
+
+func (hs *HSMSession) CreateObject(tmpl []*pkcs11.Attribute) (pkcs11.ObjectHandle, error) {
+ return hs.Context.CreateObject(hs.Handle, tmpl)
+}
+
+func (hs *HSMSession) DestroyObject(object pkcs11.ObjectHandle) error {
+ return hs.Context.DestroyObject(hs.Handle, object)
+}
+
+func (hs *HSMSession) DestroyAllObjects(label string) error {
+ tmpl := []*pkcs11.Attribute{
+ pkcs11.NewAttribute(pkcs11.CKA_LABEL, []byte(label)),
+ }
+
+ keys, err := hs.FindObjects(tmpl)
+ if err != nil {
+ return fmt.Errorf("failed to find objects with label='%s': %w", label, err)
+ }
+
+ for _, key := range keys {
+ err = hs.DestroyObject(key)
+ if err != nil {
+ return fmt.Errorf("failed to destroy object '%+v': %w", key, err)
+ }
+ }
+
+ return nil
+}
+
+func (hs *HSMSession) FindObject(tmpl []*pkcs11.Attribute) (pkcs11.ObjectHandle, error) {
+ err := hs.Context.FindObjectsInit(hs.Handle, tmpl)
+ if err != nil {
+ return 0, err
+ }
+ handles, _, err := hs.Context.FindObjects(hs.Handle, 2)
+ if err != nil {
+ return 0, err
+ }
+ err = hs.Context.FindObjectsFinal(hs.Handle)
+ if err != nil {
+ return 0, err
+ }
+ if len(handles) == 0 {
+ return 0, errors.New("no objects found matching provided template")
+ }
+ if len(handles) > 1 {
+ return 0, fmt.Errorf("too many objects (%d) that match the provided template", len(handles))
+ }
+ return handles[0], nil
+}
+
+func (hs *HSMSession) FindObjects(tmpl []*pkcs11.Attribute) ([]pkcs11.ObjectHandle, error) {
+ result := []pkcs11.ObjectHandle{}
+
+ err := hs.Context.FindObjectsInit(hs.Handle, tmpl)
+ if err != nil {
+ return result, err
+ }
+
+ for {
+ handles, _, err := hs.Context.FindObjects(hs.Handle, 10)
+ if err != nil {
+ return result, err
+ }
+ if len(handles) == 0 {
+ break
+ }
+ result = append(result, handles...)
+ }
+
+ err = hs.Context.FindObjectsFinal(hs.Handle)
+ if err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
+
+func (hs *HSMSession) GenerateKey(mechs []*pkcs11.Mechanism, tmpl []*pkcs11.Attribute) (pkcs11.ObjectHandle, error) {
+ return hs.Context.GenerateKey(hs.Handle, mechs, tmpl)
+}
+
+func (hs *HSMSession) GetAttributeValue(handle pkcs11.ObjectHandle, tmpl []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) {
+ return hs.Context.GetAttributeValue(hs.Handle, handle, tmpl)
+}
+
+func (hs *HSMSession) WrapKey(mechs []*pkcs11.Mechanism, wkh pkcs11.ObjectHandle, kh pkcs11.ObjectHandle) ([]byte, error) {
+ return hs.Context.WrapKey(hs.Handle, mechs, wkh, kh)
+}
diff --git a/gui/main.go b/gui/main.go
index b8cdf03..5767ff4 100644
--- a/gui/main.go
+++ b/gui/main.go
@@ -26,6 +26,7 @@ import (
"net/http"
"os"
"os/exec"
+ "path"
"path/filepath"
"reflect"
"regexp"
@@ -973,13 +974,58 @@ func _emailSendHandler(w http.ResponseWriter, r *http.Request) {
func _exportHandler(w http.ResponseWriter, r *http.Request) {
certname := r.Form.Get("certname")
+ certFile := fmt.Sprintf("%s%s.pem", CERT_FILES_PATH, certname)
- certFile := locateFile(certname + ".pem")
- keyFile := strings.TrimSuffix(certFile, filepath.Ext(certFile)) + ".key"
+ seqnr := ""
+ re := regexp.MustCompile(`-(\d{2})-`)
+ match := re.FindStringSubmatch(certname)
+ if len(match) > 1 {
+ seqnr = match[1]
+ } else {
+ errorHandler(w, r, fmt.Errorf("failed to extract sequence number from filename '%s'", certFile), http.StatusInternalServerError)
+ return
+ }
+
+ cfg := &HSMConfig{}
+ if strings.HasPrefix(certname, "root-") {
+ cfg.Initialize("root", seqnr)
+ }
+ if strings.HasPrefix(certname, "issuer-") {
+ cfg.Initialize("issuer", seqnr)
+ }
+
+ key, err := cfg.GetPrivateKey()
+ if err != nil {
+ fmt.Println(err)
+ if strings.Contains(err.Error(), "CKR_KEY_UNEXTRACTABLE") {
+ errorHandler(w, r, err, http.StatusBadRequest)
+ } else {
+ errorHandler(w, r, err, http.StatusInternalServerError)
+ }
+ return
+ }
+
+ tmpDir, err := os.MkdirTemp("", "labca")
+ if err != nil {
+ fmt.Println(err)
+ errorHandler(w, r, err, http.StatusInternalServerError)
+ return
+ }
+ defer os.RemoveAll(tmpDir)
+
+ keyFile := path.Join(tmpDir, fmt.Sprintf("%s.pem", strings.Replace(certname, "-cert", "-key", -1)))
+
+ keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: key})
+ err = os.WriteFile(keyFile, keyPEM, os.ModeAppend)
+ if err != nil {
+ fmt.Println(err)
+ errorHandler(w, r, err, http.StatusInternalServerError)
+ return
+ }
if r.Form.Get("type") == "pfx" {
w.Header().Set("Content-Type", "application/x-pkcs12")
- w.Header().Set("Content-Disposition", "attachment; filename=labca_"+certname+".pfx")
+ w.Header().Set("Content-Disposition", "attachment; filename=labca-"+certname+".pfx")
cmd := "openssl pkcs12 -export -inkey " + keyFile + " -in " + certFile + " -passout pass:" + r.Form.Get("export-pwd")
@@ -988,7 +1034,7 @@ func _exportHandler(w http.ResponseWriter, r *http.Request) {
if r.Form.Get("type") == "zip" {
w.Header().Set("Content-Type", "application/zip")
- w.Header().Set("Content-Disposition", "attachment; filename=labca_"+certname+".zip")
+ w.Header().Set("Content-Disposition", "attachment; filename=labca-"+certname+".zip")
cmd := "zip -j -P " + r.Form.Get("export-pwd") + " - " + keyFile + " " + certFile
@@ -1088,7 +1134,7 @@ func (res *Result) ManageComponents(w http.ResponseWriter, r *http.Request, acti
}
}
-func _checkUpdatesHandler(w http.ResponseWriter, r *http.Request) {
+func _checkUpdatesHandler(w http.ResponseWriter, _ *http.Request) {
res := struct {
Success bool
UpdateAvailable bool
@@ -1112,62 +1158,14 @@ func _checkUpdatesHandler(w http.ResponseWriter, r *http.Request) {
func generateCRLHandler(w http.ResponseWriter, r *http.Request, isRoot bool) {
res := makeErrorsResponse(true)
+ command := "gen-issuer-crl"
if isRoot {
- path := "data/"
- certBase := "root-ca"
- keyFileExists := true
- if _, err := os.Stat(path + certBase + ".key"); errors.Is(err, fs.ErrNotExist) {
- keyFileExists = false
- }
- if keyFileExists {
- if _, err := exeCmd("openssl ca -config " + path + "openssl.cnf -gencrl -keyfile " + path + certBase + ".key -cert " + path + certBase + ".pem -out " + path + certBase + ".crl"); err != nil {
- res.Success = false
- res.Errors["CRL"] = "Could not generate Root CRL - see logs"
- }
- } else {
- if r.Form.Get("rootkey") == "" {
- res.Success = false
- res.Errors["CRL"] = "NO_ROOT_KEY"
- } else {
- rootci := &CertificateInfo{
- IsRoot: true,
- Key: r.Form.Get("rootkey"),
- Passphrase: r.Form.Get("rootpassphrase"),
- }
- if !rootci.StoreRootKey(path) {
- res.Success = false
- res.Errors["CRL"] = rootci.Errors["Modal"]
- } else {
- // Generate CRL now that we have the key
- if _, err := exeCmd("openssl ca -config " + path + "openssl.cnf -gencrl -keyfile " + path + certBase + ".key -cert " + path + certBase + ".pem -out " + path + certBase + ".crl"); err != nil {
- res.Success = false
- res.Errors["CRL"] = "Could not generate Root CRL - see logs"
- }
- // Remove the Root Key if we want to keep it offline
- if viper.GetBool("keep_root_offline") {
- if _, err := os.Stat(path + certBase + ".key"); !errors.Is(err, fs.ErrNotExist) {
- fmt.Println("Removing private Root key from the system...")
- if _, err := exeCmd("rm " + path + certBase + ".key"); err != nil {
- log.Printf("_certCreate: error deleting root key: %v", err)
- }
- }
- if _, err := os.Stat(path + certBase + ".key.der"); !errors.Is(err, fs.ErrNotExist) {
- if _, err := exeCmd("rm " + path + certBase + ".key.der"); err != nil {
- log.Printf("_certCreate: error deleting root key (DER format): %v", err)
- }
- }
- }
- }
- }
- }
+ command = "gen-root-crl"
+ }
- _hostCommand(w, r, "check-crl")
-
- } else { // !isRoot
- if !_hostCommand(w, r, "gen-issuer-crl") {
- res.Success = false
- res.Errors["CRL"] = "Failed to generate CRL - see logs"
- }
+ if !_hostCommand(w, r, command) {
+ res.Success = false
+ res.Errors["CRL"] = "Failed to generate CRL - see logs"
}
w.Header().Set("Content-Type", "application/json")
@@ -1754,7 +1752,7 @@ func getLog(w http.ResponseWriter, r *http.Request, logType string) string {
defer conn.Close()
- fmt.Fprintf(conn, "log-"+logType+"\n")
+ fmt.Fprintf(conn, "log-%s\n", logType)
reader := bufio.NewReader(conn)
contents, err := io.ReadAll(reader)
if err != nil {
@@ -1787,7 +1785,7 @@ func showLog(ws *websocket.Conn, logType string) {
defer conn.Close()
- fmt.Fprintf(conn, "log-"+logType+"\n")
+ fmt.Fprintf(conn, "log-%s\n", logType)
scanner := bufio.NewScanner(conn)
for scanner.Scan() {
msg := scanner.Text()
@@ -1891,9 +1889,6 @@ func _buildCI(r *http.Request, session *sessions.Session, isRoot bool) *Certific
if session.Values["o"] != nil {
ci.Organization = session.Values["o"].(string)
}
- if session.Values["ou"] != nil {
- ci.OrgUnit = session.Values["ou"].(string)
- }
if session.Values["cn"] != nil {
ci.CommonName = session.Values["cn"].(string)
ci.CommonName = strings.Replace(ci.CommonName, "Root", "", -1)
@@ -1933,13 +1928,24 @@ func _certCreate(w http.ResponseWriter, r *http.Request, certBase string, isRoot
if r.Form.Get("revertroot") != "" {
// From issuer certificate creation page it is possible to remove the root again and start over
- exeCmd("rm data/root-ca.key") // Does not necessarily exist
- exeCmd("rm data/root-ca.key.der") // Does not necessarily exist
- if _, err := exeCmd("rm data/root-ca.pem"); err != nil {
- errorHandler(w, r, err, http.StatusInternalServerError)
- return false
+ rootseqnr := "01"
+ seqnr := "01"
+ err := deleteFiles(fmt.Sprintf("%sroot-%s*", CERT_FILES_PATH, rootseqnr))
+ if err != nil {
+ fmt.Printf("failed to delete root %s files: %+v\n", rootseqnr, err.Error())
}
- certBase = "root-ca"
+ err = deleteFiles(fmt.Sprintf("%sissuer-%s*", CERT_FILES_PATH, seqnr))
+ if err != nil {
+ fmt.Printf("failed to delete issuer %s files: %+v\n", seqnr, err.Error())
+ }
+
+ cfg := &HSMConfig{}
+ cfg.Initialize("issuer", seqnr)
+ cfg.ClearAll()
+ cfg.Initialize("root", rootseqnr)
+ cfg.ClearAll()
+
+ certBase = "root-01"
isRoot = true
r.Method = "GET"
sess, _ := sessionStore.Get(r, "labca")
@@ -1947,6 +1953,7 @@ func _certCreate(w http.ResponseWriter, r *http.Request, certBase string, isRoot
if err := sess.Save(r, w); err != nil {
log.Printf("cannot save session: %s\n", err)
}
+
} else if r.Form.Get("ack-rootkey") == "yes" {
// Root Key was shown, do we need to keep it online?
viper.Set("keep_root_offline", r.Form.Get("keep-root-online") != "true")
@@ -1964,24 +1971,20 @@ func _certCreate(w http.ResponseWriter, r *http.Request, certBase string, isRoot
}
}
- path := "data/"
- if !isRoot {
- path = path + "issuer/"
- }
-
- if _, err := os.Stat(path + certBase + ".pem"); errors.Is(err, fs.ErrNotExist) {
+ if _, err := os.Stat(CERT_FILES_PATH + certBase + "-cert.pem"); errors.Is(err, fs.ErrNotExist) {
session, _ := sessionStore.Get(r, "labca")
if r.Method == "GET" {
ci := _buildCI(r, session, isRoot)
- if isRoot && (certBase == "root-ca" || certBase == "test-root") {
+ if isRoot && (certBase == "root-ca" || certBase == "test-root" || certBase == "root-01") {
ci.IsFirst = true
- } else if !isRoot && (certBase == "ca-int" || certBase == "test-ca") {
+ } else if !isRoot && (certBase == "ca-int" || certBase == "test-ca" || certBase == "issuer-01") {
ci.IsFirst = true
}
if len(r.URL.Query()["root"]) > 0 {
certFile := locateFile(r.URL.Query()["root"][0] + ".pem")
+
ci.RootEnddate, err = getCertFileNotAFter(certFile)
if err != nil {
fmt.Println(err.Error())
@@ -2003,7 +2006,33 @@ func _certCreate(w http.ResponseWriter, r *http.Request, certBase string, isRoot
ci.Organization = val
}
} else if !isRoot {
- certFile := locateFile("root-ca.pem")
+ certFile := CERT_FILES_PATH + "root-01-cert.pem"
+
+ // The rules are quite strict on what type is allowed for issuer certs!
+ crt, err := readCertificate(certFile)
+ if err == nil {
+ validKeyTypes := make(map[string]string)
+
+ if crt.PublicKeyAlgorithm == x509.RSA {
+ for k, v := range ci.KeyTypes {
+ if strings.HasPrefix(k, "rsa") {
+ validKeyTypes[k] = v
+ }
+ }
+ }
+
+ if crt.PublicKeyAlgorithm == x509.ECDSA {
+ if crt.SignatureAlgorithm == x509.ECDSAWithSHA256 {
+ validKeyTypes["ecdsa256"] = "ECDSA-256"
+ }
+ if crt.SignatureAlgorithm == x509.ECDSAWithSHA384 {
+ validKeyTypes["ecdsa384"] = "ECDSA-384"
+ }
+ }
+
+ ci.KeyTypes = validKeyTypes
+ }
+
ci.RootEnddate, err = getCertFileNotAFter(certFile)
if err != nil {
fmt.Println(err.Error())
@@ -2038,7 +2067,6 @@ func _certCreate(w http.ResponseWriter, r *http.Request, certBase string, isRoot
}
ci.Country = r.Form.Get("c")
ci.Organization = r.Form.Get("o")
- ci.OrgUnit = r.Form.Get("ou")
ci.CommonName = r.Form.Get("cn")
ci.RootEnddate = r.Form.Get("root-enddate")
@@ -2103,7 +2131,7 @@ func _certCreate(w http.ResponseWriter, r *http.Request, certBase string, isRoot
}
if !rootci.StoreCRL("data/") {
ci.Errors["Modal"] = rootci.Errors["Modal"]
- csr, err := os.Open(path + certBase + ".csr")
+ csr, err := os.Open(CERT_FILES_PATH + certBase + ".csr") // TODO !!
if err != nil {
ci.Errors[cases.Title(language.Und).String(ci.CreateType)] = "Error reading .csr file! See LabCA logs for details"
log.Printf("_certCreate: read csr: %v", err)
@@ -2118,7 +2146,7 @@ func _certCreate(w http.ResponseWriter, r *http.Request, certBase string, isRoot
}
}
- if err := ci.Create(path, certBase, wasCSR); err != nil {
+ if err := ci.Create(certBase, wasCSR); err != nil {
if err.Error() == "NO_ROOT_KEY" {
if r.Form.Get("generate") != "" {
if r.Form.Get("rootkey") == "" {
@@ -2142,7 +2170,7 @@ func _certCreate(w http.ResponseWriter, r *http.Request, certBase string, isRoot
}
if r.Form.Get("getcsr") != "" {
- csr, err := os.Open(path + certBase + ".csr")
+ csr, err := os.Open(CERT_FILES_PATH + certBase + ".csr") // TODO !
if err != nil {
ci.Errors[cases.Title(language.Und).String(ci.CreateType)] = "Error reading .csr file! See LabCA logs for details"
log.Printf("_certCreate: read csr: %v", err)
@@ -2170,27 +2198,13 @@ func _certCreate(w http.ResponseWriter, r *http.Request, certBase string, isRoot
}
if !ci.IsRoot {
- nameID, err := issuerNameID(path + certBase + ".pem")
+ nameID, err := issuerNameID(CERT_FILES_PATH + "issuer-01-cert.pem")
if err == nil {
viper.Set("issuer_name_id", nameID)
viper.WriteConfig()
} else {
log.Printf("_certCreate: could not calculate IssuerNameID: %v", err)
}
-
- if viper.GetBool("keep_root_offline") {
- if _, err := os.Stat(path + "../root-ca.key"); !errors.Is(err, fs.ErrNotExist) {
- fmt.Println("Removing private Root key from the system...")
- if _, err := exeCmd("rm " + path + "../root-ca.key"); err != nil {
- log.Printf("_certCreate: error deleting root key: %v", err)
- }
- }
- if _, err := os.Stat(path + "../root-ca.key.der"); !errors.Is(err, fs.ErrNotExist) {
- if _, err := exeCmd("rm " + path + "../root-ca.key.der"); err != nil {
- log.Printf("_certCreate: error deleting root key (DER format): %v", err)
- }
- }
- }
}
if viper.Get("labca.organization") == nil {
@@ -2202,27 +2216,11 @@ func _certCreate(w http.ResponseWriter, r *http.Request, certBase string, isRoot
session.Values["kt"] = ci.KeyType
session.Values["c"] = ci.Country
session.Values["o"] = ci.Organization
- session.Values["ou"] = ci.OrgUnit
session.Values["cn"] = ci.CommonName
if err = session.Save(r, w); err != nil {
log.Printf("cannot save session: %s\n", err)
}
- if ci.IsRoot && ci.CreateType == "generate" && r.Form.Get("ack-rootkey") != "yes" {
- key, err := os.Open(path + certBase + ".key")
- if err != nil {
- ci.Errors[cases.Title(language.Und).String(ci.CreateType)] = "Error reading .key file! See LabCA logs for details"
- log.Printf("_certCreate: read key: %v", err)
- render(w, r, "cert:manage", map[string]interface{}{"CertificateInfo": ci, "Progress": _progress(certBase), "HelpText": _helptext(certBase)})
- return false
- }
- defer key.Close()
- b, _ := io.ReadAll(key)
-
- render(w, r, "cert:manage", map[string]interface{}{"CertificateInfo": ci, "RootKey": string(b), "Progress": _progress(certBase), "HelpText": _helptext(certBase)})
- return false
- }
-
// Fake the method to GET as we need to continue in the setupHandler() function
r.Method = "GET"
} else {
@@ -2234,6 +2232,28 @@ func _certCreate(w http.ResponseWriter, r *http.Request, certBase string, isRoot
return true
}
+func deleteFiles(pattern string) error {
+ files, err := filepath.Glob(pattern)
+ if err != nil {
+ return fmt.Errorf("failed to find files: %w", err)
+ }
+
+ ok := true
+ for _, file := range files {
+ err := os.Remove(file)
+ if err != nil {
+ ok = false
+ fmt.Printf("failed to remove %s: %v\n", file, err)
+ }
+ }
+
+ if !ok {
+ return fmt.Errorf("failed to remove at least one file, see logs for details")
+ }
+
+ return nil
+}
+
func _hostCommand(w http.ResponseWriter, r *http.Request, command string, params ...string) bool {
conn, err := net.Dial("tcp", "control:3030")
if err != nil {
@@ -2244,9 +2264,9 @@ func _hostCommand(w http.ResponseWriter, r *http.Request, command string, params
defer conn.Close()
- fmt.Fprintf(conn, command+"\n")
+ fmt.Fprint(conn, command+"\n")
for _, param := range params {
- fmt.Fprintf(conn, param+"\n")
+ fmt.Fprint(conn, param+"\n")
}
reader := bufio.NewReader(conn)
@@ -2314,12 +2334,12 @@ func _progress(stage string) int {
}
curr += 3.0
- if stage == "root-ca" {
+ if stage == "root-01" {
return int(math.Round(curr / max))
}
curr += 4.0
- if stage == "ca-int" {
+ if stage == "issuer-01" {
return int(math.Round(curr / max))
}
curr += 3.0
@@ -2365,12 +2385,12 @@ func _helptext(stage string) template.HTML {
"domain, e.g. '.localdomain'. In lockdown mode only those domains are allowed. In whitelist mode\n",
"those domains are allowed next to all official, internet accessible domains and in standard\n",
"mode only the official domains are allowed."))
- } else if stage == "root-ca" {
+ } else if stage == "root-01" {
return template.HTML(fmt.Sprint("This is the top level certificate that will sign the issuer\n",
"certificate(s). You can either generate a fresh Root CA (Certificate Authority) or import an\n",
"existing one, e.g. a backup from another LabCA instance.
\n",
"If you want to generate a new certificate, pick a key type and strength (the higher the number the\n",
- "more secure, ECDSA is more modern than RSA), provide at least a country and organization name,\n",
+ "more secure, ECDSA is more modern than RSA), provide a country and organization name,\n",
"and the common name. It is recommended that the common name contains the word 'Root' as well\n",
"as your organization name so you can recognize it, and that's why that is automatically filled\n",
"once you leave the organization field.
\n",
@@ -2378,7 +2398,7 @@ func _helptext(stage string) template.HTML {
"offline for security reasons according to best practices. If you do include it here, we will be able\n",
"to generate an issuing certificate automatically in the next step. If you don't include it, we will\n",
"ask for it when needed."))
- } else if stage == "ca-int" {
+ } else if stage == "issuer-01" {
return template.HTML(fmt.Sprint("This is what end users will see as the issuing certificate. Again,\n",
"you can either generate a fresh certificate or import an existing one, as long as it is signed by\n",
"the Root CA from the previous step.
\n",
@@ -2458,7 +2478,7 @@ func _setupAdminUser(w http.ResponseWriter, r *http.Request) bool {
}
defer conn.Close()
- fmt.Fprintf(conn, "backup-restore\n"+header.Filename+"\n")
+ fmt.Fprint(conn, "backup-restore\n"+header.Filename+"\n")
reader := bufio.NewReader(conn)
message, err := io.ReadAll(reader)
if err != nil {
@@ -2734,18 +2754,18 @@ func setupHandler(w http.ResponseWriter, r *http.Request) {
}
// 3. Setup root CA certificate
- if !_certCreate(w, r, "root-ca", true) {
+ if !_certCreate(w, r, "root-01", true) {
// Cleanup the cert (if it even exists) so we will retry on the next run
- if _, err := os.Stat("data/root-ca.pem"); !errors.Is(err, fs.ErrNotExist) {
- exeCmd("mv data/root-ca.pem data/root-ca.pem_TMP")
+ if _, err := os.Stat(CERT_FILES_PATH + "root-01-cert.pem"); !errors.Is(err, fs.ErrNotExist) {
+ exeCmd("mv " + CERT_FILES_PATH + "root-01-cert.pem " + CERT_FILES_PATH + "root-01-cert.pem_TMP")
}
return
}
// 4. Setup issuer certificate
- if !_certCreate(w, r, "ca-int", false) {
+ if !_certCreate(w, r, "issuer-01", false) {
// Cleanup the cert (if it even exists) so we will retry on the next run
- os.Remove("data/issuer/ca-int.pem")
+ os.Remove(CERT_FILES_PATH + "issuer-01-cert.pem")
return
}
@@ -3326,9 +3346,10 @@ func init() {
port := flag.Int("port", 0, "Port to listen on (default 3000 when using init)")
versionFlag := flag.Bool("version", false, "Show version number and exit")
decrypt := flag.String("d", "", "Decrypt a value")
+ renewcrl := flag.Int("renewcrl", 0, "Check root CRL files and renew if nextUpdate is in less than this number of days")
flag.Parse()
- if *versionFlag {
+ if *versionFlag && standaloneVersion != "" {
fmt.Println(standaloneVersion)
os.Exit(0)
}
@@ -3355,6 +3376,11 @@ func init() {
panic(fmt.Errorf("fatal error config file: '%s'", err))
}
+ if *versionFlag && standaloneVersion == "" {
+ fmt.Println(viper.GetString("version"))
+ os.Exit(0)
+ }
+
if *decrypt != "" {
plain, err := _decrypt(*decrypt)
if err == nil {
@@ -3365,6 +3391,57 @@ func init() {
}
}
+ if *renewcrl != 0 {
+ crlFiles, err := filepath.Glob(filepath.Join(CERT_FILES_PATH, "root-*-crl.pem"))
+ if err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ }
+
+ for _, crlFile := range crlFiles {
+ read, err := os.ReadFile(crlFile)
+ if err != nil {
+ fmt.Printf("could not read '%s': %s\n", crlFile, err.Error())
+ os.Exit(1)
+ }
+ block, _ := pem.Decode(read)
+ if block == nil || block.Type != "X509 CRL" {
+ fmt.Println(block)
+ fmt.Println("failed to decode PEM block containing revocation list")
+ os.Exit(1)
+ }
+ crl, err := x509.ParseRevocationList(block.Bytes)
+ if err != nil {
+ fmt.Printf("could not parse revocation list: %s\n", err.Error())
+ os.Exit(1)
+ }
+
+ now := time.Now()
+ if crl.NextUpdate.Sub(now) < time.Hour*24*time.Duration(*renewcrl) {
+ fmt.Printf("renewing crl file '%s'...\n", crlFile)
+ re := regexp.MustCompile(`-(\d{2})-`)
+ match := re.FindStringSubmatch(crlFile)
+ if len(match) > 1 {
+ seqnr := match[1]
+ ci := &CertificateInfo{}
+ ci.Initialize()
+ err = ci.CeremonyRootCRL(seqnr)
+ if err == nil {
+ fmt.Printf("updated %s\n", crlFile)
+ } else {
+ fmt.Printf("could not update crl file '%s': %s\n", crlFile, err.Error())
+ os.Exit(1)
+ }
+ } else {
+ fmt.Printf("could not extract sequence number from filename '%s'\n", crlFile)
+ os.Exit(1)
+ }
+ }
+ }
+
+ os.Exit(0)
+ }
+
var err error
if *init || viper.GetBool("standalone") {
tmpls, err = templates.New().ParseEmbed(embeddedTemplates, "templates/")
@@ -3452,6 +3529,10 @@ func init() {
updateAvailable = false
+ if !viper.GetBool("standalone") {
+ CheckUpgrades()
+ }
+
/*
// TODO: Still needs to be done for this!
// Store boulder chains if we don't have them already
@@ -3474,6 +3555,47 @@ func init() {
*/
}
+type BackupResult struct {
+ Existed bool
+ NewName string
+ OrigName string
+}
+
+func (br BackupResult) Remove() {
+ os.Remove(br.NewName)
+}
+
+func (br BackupResult) Restore() {
+ if br.Existed {
+ os.Rename(br.NewName, br.OrigName)
+ }
+}
+
+func renameBackup(filename string) BackupResult {
+ result := BackupResult{
+ Existed: false,
+ }
+
+ if _, err := os.Stat(filename); !errors.Is(err, os.ErrNotExist) {
+ os.Remove(filename + "_BAK") // May not exist...
+ result.Existed = true
+ }
+
+ if !result.Existed {
+ return result
+ }
+
+ err := os.Rename(filename, filename+"_BAK")
+ if err != nil {
+ fmt.Printf("warning: failed to backup previous file '%s': %s\n", filename, err.Error())
+ } else {
+ result.OrigName = filename
+ result.NewName = filename + "_BAK"
+ }
+
+ return result
+}
+
func main() {
tmpls.Parse()
diff --git a/gui/static/certs/index.html b/gui/static/certs/index.html
index fa89867..0847f02 100644
--- a/gui/static/certs/index.html
+++ b/gui/static/certs/index.html
@@ -60,8 +60,7 @@
CA Type
Distinguished Name
- Windows format
- Linux format
+ Certificate File
Validity Period
CRL
CRL Validity
@@ -70,17 +69,15 @@
Root CA
PKI_ROOT_DN
- root-ca.der
- root-ca.pem
+ root-01-cert.pem
PKI_ROOT_VALIDITY
- root-ca.crl
+
Issuing CA
PKI_INT_DN
- ca-int.der
- ca-int.pem
+ issuer-01-cert.pem
PKI_INT_VALIDITY
@@ -90,9 +87,7 @@
To trust the certificates provided by LabCA, all your client devices
- should install the root certificate in their Trusted Root Certification Authorities store. You may choose
- to download the format best suited for your Operating System: DER format for Windows machines or PEM format for
- Linux/unix machines and Android phones.
+ should install the root certificate in their Trusted Root Certification Authorities store.
The CRL (Certificate Revocation List) is a type of blocklist that includes certificates that should no longer be
diff --git a/gui/templates/cert-ceremonies/issuer-cert.yaml b/gui/templates/cert-ceremonies/issuer-cert.yaml
new file mode 100644
index 0000000..ea0d258
--- /dev/null
+++ b/gui/templates/cert-ceremonies/issuer-cert.yaml
@@ -0,0 +1,26 @@
+ceremony-type: intermediate
+pkcs11:
+ module: {{ .Module }}
+ pin: {{ .UserPIN }}
+ signing-key-slot: {{ .RootSlotID }}
+ signing-key-label: {{ .RootLabel }}
+inputs:
+ public-key-path: {{ .Path }}issuer-{{ .SeqNr }}-pubkey.pem
+ issuer-certificate-path: {{ .Path }}root-{{ .RootSeqNr }}-cert.pem
+outputs:
+ certificate-path: {{ .Path }}issuer-{{ .SeqNr }}-cert.pem
+certificate-profile:
+ signature-algorithm: {{ .SignAlgorithm }}
+ common-name: {{ .CommonName }}
+ organization: {{ .OrgName }}
+ country: {{ .Country }}
+ not-before: {{ .NotBefore }}
+ not-after: {{ .NotAfter }}
+ crl-url: {{ .CrlUrl }}
+ issuer-url: {{ .IssuerUrl }}
+ policies:
+ - oid: 2.23.140.1.2.1
+ key-usages:
+ - Digital Signature
+ - Cert Sign
+ - CRL Sign
diff --git a/gui/templates/cert-ceremonies/issuer-key.yaml b/gui/templates/cert-ceremonies/issuer-key.yaml
new file mode 100644
index 0000000..afd69e1
--- /dev/null
+++ b/gui/templates/cert-ceremonies/issuer-key.yaml
@@ -0,0 +1,19 @@
+ceremony-type: key
+pkcs11:
+ module: {{ .Module }}
+ pin: {{ .UserPIN }}
+ store-key-in-slot: {{ .SlotID }}
+ store-key-with-label: {{ .Label }}
+key:
+ type: {{ .KeyType }}
+{{ if eq .KeyType "rsa" }}
+ rsa-mod-length: {{ .KeyParam }}
+{{ else }}
+ ecdsa-curve: {{ .KeyParam }}
+{{ end }}
+{{ if eq .Extractable "true" }}
+ extractable: true
+{{ end }}
+outputs:
+ public-key-path: {{ .Path }}issuer-{{ .SeqNr }}-pubkey.pem
+ pkcs11-config-path: {{ .Path }}issuer-{{ .SeqNr }}.pkcs11.json
diff --git a/gui/templates/cert-ceremonies/root-crl.yaml b/gui/templates/cert-ceremonies/root-crl.yaml
new file mode 100644
index 0000000..4f00b63
--- /dev/null
+++ b/gui/templates/cert-ceremonies/root-crl.yaml
@@ -0,0 +1,14 @@
+ceremony-type: crl
+pkcs11:
+ module: {{ .Module }}
+ pin: {{ .UserPIN }}
+ signing-key-slot: {{ .RootSlotID }}
+ signing-key-label: {{ .RootLabel }}
+inputs:
+ issuer-certificate-path: {{ .Path }}root-{{ .RootSeqNr }}-cert.pem
+outputs:
+ crl-path: {{ .Path }}root-{{ .RootSeqNr }}-crl.pem
+crl-profile:
+ this-update: {{ .ThisUpdate }}
+ next-update: {{ .NextUpdate }}
+ number: {{ .CrlNumber }}
diff --git a/gui/templates/cert-ceremonies/root.yaml b/gui/templates/cert-ceremonies/root.yaml
new file mode 100644
index 0000000..33a74f8
--- /dev/null
+++ b/gui/templates/cert-ceremonies/root.yaml
@@ -0,0 +1,34 @@
+ceremony-type: root
+pkcs11:
+ module: {{ .Module }}
+ pin: {{ .UserPIN }}
+ store-key-in-slot: {{ .SlotID }}
+ store-key-with-label: {{ .Label }}
+key:
+ type: {{ .KeyType }}
+{{ if eq .KeyType "rsa" }}
+ rsa-mod-length: {{ .KeyParam }}
+{{ else }}
+ ecdsa-curve: {{ .KeyParam }}
+{{ end }}
+{{ if eq .Extractable "true" }}
+ extractable: true
+{{ end }}
+outputs:
+ public-key-path: {{ .Path }}root-{{ .SeqNr }}-pubkey.pem
+ certificate-path: {{ .Path }}root-{{ .SeqNr }}-cert.pem
+certificate-profile:
+ signature-algorithm: {{ .SignAlgorithm }}
+ common-name: {{ .CommonName }}
+ organization: {{ .OrgName }}
+ country: {{ .Country }}
+ not-before: {{ .NotBefore }}
+ not-after: {{ .NotAfter }}
+ key-usages:
+ - Cert Sign
+ - CRL Sign
+skip-lints:
+ - n_ca_digital_signature_not_set
+{{ if eq .Renewal "true" }}
+renewal: true
+{{ end }}
diff --git a/gui/templates/views/cert.tmpl b/gui/templates/views/cert.tmpl
index ec5a770..2eb4b4d 100644
--- a/gui/templates/views/cert.tmpl
+++ b/gui/templates/views/cert.tmpl
@@ -59,13 +59,6 @@
{{ . }}
{{ end }}
-
- Org. Unit (optional):
-
- {{ with .Errors.OrgUnit }}
- {{ . }}
- {{ end }}
-
Common Name:
@@ -136,12 +129,12 @@
- Key (in PEM format{{ if .IsRoot }}; optional{{ end }}):
+ Key (in PEM format):
{{ with .Errors.Key }}
{{ . }}
{{ end }}
-
+
Issuer CRL
- Generate
+ Generate
@@ -549,7 +551,8 @@
- Subject:
+ Subject:
+ Key Type:
Current end date:
Root end date:
@@ -844,6 +847,9 @@
}
$('#modal-export').modal('hide');
+
+ } else if (event.currentTarget.status == 400 || event.currentTarget.statusText == "Bad Request") {
+ $("#modal-export-error").removeClass("hidden").show().text("Key is not extractable from the HSM!");
} else {
$("#modal-export-error").removeClass("hidden").show().text("Backend returned: " + event.currentTarget.statusText);
}
@@ -1363,6 +1369,7 @@
$('#modal-renew-cert').val($(evt.target).data('name'));
$('#renew-rootcert').val($(evt.target).data('rootname'));
$('#renew-subject').text($(evt.target).data('subject'));
+ $('#renew-keytype').text($(evt.target).data('keytype'));
$('#renew-rootsubject').val($(evt.target).data('rootsubject'));
d = new Date($(evt.target).data('notbefore')).toUTCString()
$('#renew-current-enddate').text(d);
diff --git a/gui/upgrades.go b/gui/upgrades.go
new file mode 100644
index 0000000..ee3fb21
--- /dev/null
+++ b/gui/upgrades.go
@@ -0,0 +1,191 @@
+package main
+
+import (
+ "bufio"
+ "errors"
+ "fmt"
+ "io"
+ "io/fs"
+ "log"
+ "net"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/spf13/viper"
+)
+
+func CheckUpgrades() {
+ v := viper.GetString("version")
+ if standaloneVersion == "" {
+ gitVersion := controlCommand("git-version")
+ if gitVersion != "" {
+ viper.Set("version", gitVersion)
+ viper.WriteConfig()
+ }
+ } else if v != standaloneVersion {
+ viper.Set("version", standaloneVersion)
+ viper.WriteConfig()
+ }
+
+ changed := CheckUpgrade_01_CeremonyHSM()
+
+ if changed {
+ time.Sleep(2 * time.Second)
+ log.Println("Applying updated configuration...")
+ controlCommand("apply")
+ time.Sleep(2 * time.Second)
+ log.Println("Updating CRL links if needed...")
+ controlCommand("check-crl")
+ time.Sleep(2 * time.Second)
+ log.Println("Restarting boulder containers...")
+ controlCommand("boulder-restart")
+ }
+}
+
+func readFileAsString(filename string) string {
+ read, err := os.ReadFile(filename)
+ if err != nil {
+ log.Printf("**** Could not read '%s': %s\n", filename, err.Error())
+ log.Println("**** ABORT MIGRATION ****")
+ time.Sleep(1 * time.Minute)
+ os.Exit(1)
+ }
+
+ return string(read)
+}
+
+func controlCommand(command string) string {
+ conn, err := net.Dial("tcp", "control:3030")
+ if err != nil {
+ log.Println("**** Failed to connect to control container!")
+ time.Sleep(1 * time.Minute)
+ os.Exit(1)
+ }
+ defer conn.Close()
+
+ fmt.Fprint(conn, command+"\n")
+
+ reader := bufio.NewReader(conn)
+ message, err := io.ReadAll(reader)
+ if err != nil {
+ log.Printf("**** Failed to read response from control container: %s\n", err.Error())
+ time.Sleep(1 * time.Minute)
+ os.Exit(1)
+ }
+
+ if len(message) >= 4 {
+ tail := message[len(message)-4:]
+ if strings.Compare(string(tail), "\nok\n") == 0 {
+ msg := message[0 : len(message)-4]
+ log.Printf("**** Message from control server: '%s'", msg)
+ }
+ }
+
+ return string(message)
+}
+
+func copyFile(src, dst string) error {
+ sourceFile, err := os.Open(src)
+ if err != nil {
+ return err
+ }
+ defer sourceFile.Close()
+
+ destinationFile, err := os.Create(dst)
+ if err != nil {
+ return err
+ }
+ defer destinationFile.Close()
+
+ _, err = io.Copy(destinationFile, sourceFile)
+ if err != nil {
+ return err
+ }
+
+ err = destinationFile.Sync()
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// Check if we should upgrade to using the Ceremony tool and store keys on SoftHSM (January 2025).
+func CheckUpgrade_01_CeremonyHSM() bool {
+ baseDir := "/opt/labca/data/"
+ prevRootCert := baseDir + "root-ca.pem"
+ if _, err := os.Stat(prevRootCert); errors.Is(err, fs.ErrNotExist) {
+ baseDir = "/go/src/labca/data/"
+ prevRootCert = baseDir + "root-ca.pem"
+ if _, err := os.Stat(prevRootCert); errors.Is(err, fs.ErrNotExist) {
+ return false
+ }
+ }
+
+ log.Println("**** BEGIN MIGRATION: upgrade01 ****")
+
+ rootCertFile := fmt.Sprintf("%sroot-01-cert.pem", CERT_FILES_PATH)
+ if _, err := os.Stat(rootCertFile); !errors.Is(err, fs.ErrNotExist) {
+ log.Printf("**** File %s already exists!\n", rootCertFile)
+ log.Println("**** ABORT MIGRATION ****")
+ time.Sleep(1 * time.Minute)
+ os.Exit(1)
+ }
+
+ prevRootKey := baseDir + "root-ca.key"
+ if _, err := os.Stat(prevRootKey); errors.Is(err, fs.ErrNotExist) {
+ log.Println("**** Root key file not present on the system: cannot upgrade automatically!")
+ log.Println("**** Please do a fresh install of LabCA and import / upload the root certificate and key.")
+ log.Println("**** ABORT MIGRATION ****")
+ time.Sleep(1 * time.Minute)
+ os.Exit(1)
+ }
+
+ // Migrate root certificate and key
+ ci := &CertificateInfo{IsRoot: true}
+ ci.Initialize()
+ ci.IsRoot = true
+ ci.CreateType = "upload"
+ ci.Certificate = readFileAsString(prevRootCert)
+ ci.Key = readFileAsString(prevRootKey)
+ prevRootCRL := baseDir + "root-ca.crl"
+ if _, err := os.Stat(prevRootCRL); !errors.Is(err, fs.ErrNotExist) {
+ ci.CRL = readFileAsString(prevRootCRL)
+ copyFile(prevRootCRL, strings.Replace(rootCertFile, "-cert.", "-crl.", -1))
+ }
+
+ if err := ci.Create("root-01", false); err != nil {
+ log.Printf("**** Could not convert previous root certificate and key: %s\n", err.Error())
+ log.Println("**** ABORT MIGRATION ****")
+ time.Sleep(1 * time.Minute)
+ os.Exit(1)
+ }
+
+ // Migrate issuer certificate and key
+ ci = &CertificateInfo{IsRoot: false}
+ ci.Initialize()
+ ci.IsRoot = false
+ ci.CreateType = "upload"
+ prevIssuerCert := baseDir + "issuer/ca-int.pem"
+ ci.Certificate = readFileAsString(prevIssuerCert)
+ prevIssuerKey := baseDir + "issuer/ca-int.key"
+ ci.Key = readFileAsString(prevIssuerKey)
+ ci.CRL = ""
+
+ if err := ci.Create("issuer-01", false); err != nil {
+ log.Printf("**** Could not convert previous issuer certificate and key: %s\n", err.Error())
+ log.Println("**** ABORT MIGRATION ****")
+ time.Sleep(1 * time.Minute)
+ os.Exit(1)
+ }
+
+ os.Rename(prevRootCert, prevRootCert+"_backup")
+ os.Rename(prevRootKey, prevRootKey+"_backup")
+ os.Rename(prevRootCRL, prevRootCRL+"_backup")
+ os.Rename(prevIssuerCert, prevIssuerCert+"_backup")
+ os.Rename(prevIssuerKey, prevIssuerKey+"_backup")
+
+ log.Println("**** END MIGRATION ****")
+ return true
+}
diff --git a/install b/install
index 91bfd19..344448a 100755
--- a/install
+++ b/install
@@ -517,13 +517,8 @@ static_web() {
mkdir -p crl
[ -e cert ] || ln -s certs cert
cp -rp $cloneDir/gui/static/* .
- [ -e $adminDir/data/root-ca.crl ] && cp $adminDir/data/root-ca.crl crl/ || true
[ -e $adminDir/data/root-ca.pem ] && cp $adminDir/data/root-ca.pem certs/ || true
- [ -e $adminDir/data/root-ca.pem ] && ln -sf root-ca.pem certs/test-root.pem || true
- [ -e $adminDir/data/root-ca.der ] && cp $adminDir/data/root-ca.der certs/ || true
[ -e $adminDir/data/issuer/ca-int.pem ] && cp $adminDir/data/issuer/ca-int.pem certs/ || true
- [ -e $adminDir/data/issuer/ca-int.pem ] && ln -sf ca-int.pem certs/test-ca.pem || true
- [ -e $adminDir/data/issuer/ca-int.der ] && cp $adminDir/data/issuer/ca-int.der certs/ || true
local have_config=$(grep restarted $adminDir/data/config.json | grep true)
if [ "$have_config" != "" ]; then
@@ -868,6 +863,12 @@ main() {
curChecksum=$(md5sum $this 2>/dev/null | cut -d' ' -f1)
[ ! -e "$cloneDir/cron_d" ] || chown labca:labca "$cloneDir/cron_d"
+ # Stop any running containers to prevent data migration issues...
+ if [ -d "$boulderDir" ]; then
+ cd "$boulderDir"
+ docker compose stop &>/dev/null || true
+ fi
+
parse_cmdline "$@"
if [ $keepLocal -eq 0 ]; then
clone_or_pull "$cloneDir" "$labcaUrl" "$cmdlineBranch"
diff --git a/patch-cfg.sh b/patch-cfg.sh
index 1080eba..a2e8b76 100755
--- a/patch-cfg.sh
+++ b/patch-cfg.sh
@@ -44,27 +44,20 @@ fi
for f in $(grep -l boulder-proxysql $boulderLabCADir/secrets/*); do sed -i -e "s/proxysql:6033/mysql:3306/" $f; done
cd "$boulderLabCADir"
-sed -i -e "s/test-ca2.pem/test-ca.pem/" config/ocsp-responder.json
-sed -i -e "s/test-ca2.pem/test-ca.pem/" config/publisher.json
-sed -i -e "s/test-ca2.pem/test-ca.pem/" config/ra.json
-sed -i -e "s/test-ca2.pem/test-ca.pem/" config/wfe2.json
-sed -i -e "s|test/certs/webpki/int-rsa-a.cert.pem|labca/test-ca.pem|" config/akamai-purger.json
-sed -i -e "s|test/certs/webpki/int-rsa-a.cert.pem|labca/test-ca.pem|" config/ocsp-responder.json
-sed -i -e "s|test/certs/webpki/int-rsa-a.cert.pem|labca/test-ca.pem|" config/publisher.json
-sed -i -e "s|test/certs/webpki/int-rsa-a.cert.pem|labca/test-ca.pem|" config/ca.json
-sed -i -e "s|test/certs/webpki/int-rsa-a.cert.pem|labca/test-ca.pem|" config/wfe2.json
-sed -i -e "s|test/certs/webpki/int-rsa-a.cert.pem|labca/test-ca.pem|" config/crl-storer.json
-sed -i -e "s|test/certs/webpki/int-rsa-a.cert.pem|labca/test-ca.pem|" config/crl-updater.json
-sed -i -e "s|test/certs/webpki/int-rsa-a.cert.pem|labca/test-ca.pem|" config/ra.json
-sed -i -e "s|test/certs/webpki/int-rsa-a.cert.pem|labca/test-ca.pem|" v2_integration.py
-sed -i -e "s|test/certs/webpki/int-rsa-a.pkcs11.json|labca/test-ca.key-pkcs11.json|" config/ca.json
-sed -i -e "s|test/certs/webpki/root-rsa.cert.pem|labca/test-root.pem|" certs/root-ceremony-rsa.yaml
-sed -i -e "s|test/certs/webpki/root-rsa.cert.pem|labca/test-root.pem|" certs/root-crl-rsa.yaml
-sed -i -e "s|test/certs/webpki/root-rsa.cert.pem|labca/test-root.pem|" certs/intermediate-cert-ceremony-rsa.yaml
-sed -i -e "s|test/certs/webpki/root-rsa.cert.pem|labca/test-root.pem|" config/publisher.json
-sed -i -e "s|test/certs/webpki/root-rsa.cert.pem|labca/test-root.pem|" config/wfe2.json
-sed -i -e "s|test/certs/webpki/root-rsa.cert.pem|labca/test-root.pem|" integration-test.py
-sed -i -e "s|test/certs/webpki/root-rsa.cert.pem|labca/test-root.pem|" helpers.py
+sed -i -e "s|test/certs/webpki/int-rsa-a.cert.pem|labca/certs/webpki/issuer-01-cert.pem|" config/ocsp-responder.json
+sed -i -e "s|test/certs/webpki/int-rsa-a.cert.pem|labca/certs/webpki/issuer-01-cert.pem|" config/publisher.json
+sed -i -e "s|test/certs/webpki/int-rsa-a.cert.pem|labca/certs/webpki/issuer-01-cert.pem|" config/ca.json
+sed -i -e "s|test/certs/webpki/int-rsa-a.cert.pem|labca/certs/webpki/issuer-01-cert.pem|" config/wfe2.json
+sed -i -e "s|test/certs/webpki/int-rsa-a.cert.pem|labca/certs/webpki/issuer-01-cert.pem|" config/crl-storer.json
+sed -i -e "s|test/certs/webpki/int-rsa-a.cert.pem|labca/certs/webpki/issuer-01-cert.pem|" config/crl-updater.json
+sed -i -e "s|test/certs/webpki/int-rsa-a.cert.pem|labca/certs/webpki/issuer-01-cert.pem|" config/ra.json
+sed -i -e "s|test/certs/webpki/int-rsa-a.pkcs11.json|labca/certs/webpki/issuer-01.pkcs11.json|" config/ca.json
+sed -i -e "s|test/certs/webpki/root-rsa.cert.pem|labca/certs/webpki/root-01-cert.pem|" certs/root-ceremony-rsa.yaml
+sed -i -e "s|test/certs/webpki/root-rsa.cert.pem|labca/certs/webpki/root-01-cert.pem|" certs/root-crl-rsa.yaml
+sed -i -e "s|test/certs/webpki/root-rsa.cert.pem|labca/certs/webpki/root-01-cert.pem|" certs/intermediate-cert-ceremony-rsa.yaml
+sed -i -e "s|test/certs/webpki/root-rsa.cert.pem|labca/certs/webpki/root-01-cert.pem|" config/publisher.json
+sed -i -e "s|test/certs/webpki/root-rsa.cert.pem|labca/certs/webpki/root-01-cert.pem|" config/wfe2.json
+sed -i -e "s|test/certs/webpki/root-rsa.cert.pem|labca/certs/webpki/root-01-cert.pem|" helpers.py
sed -i -e "s|letsencrypt/boulder|hakwerk/labca|" config/wfe2.json
sed -i -e "s|1.2.3.4|1.3.6.1.4.1.44947.1.1.1|g" config/ca.json
sed -i -e "s/ocspURL.Path = encodedReq/ocspURL.Path += encodedReq/" ocsp/helper/helper.go
@@ -90,14 +83,4 @@ done
sed -i -e "s/names/name\(s\)/" config/expiration-mailer.gotmpl
-if [ ! -e "test-ca.key-pkcs11.json" ]; then
- cat > test-ca.key-pkcs11.json <