Use ceremony tool for generating keys and certs; store keys on SoftHSM

Replace openssl certificate / CRL generation with the tool as used by
Let's Encrypt, storing the keys on SoftHSMv2, a simulated HSM (Hardware
Security Module).
Include migration of old setups where key files were also stored on
disk.
This commit is contained in:
Arjan H
2025-01-31 20:44:48 +01:00
parent 8852d49425
commit 6d72d32398
38 changed files with 2181 additions and 583 deletions

5
backup
View File

@@ -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

View File

@@ -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

View File

@@ -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/

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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: /")<br/> $(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="<a class=\"public\" href=\"../crl/$ROOT_CRL_NAME\">$ROOT_CRL_NAME</a></td>"
PKI_ROOT_CRL_VALIDITY="$(openssl crl -noout -in $ROOT_CRL_FILE -lastupdate | sed -e "s/.*=/Last Update: /")<br/> $(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="<a class=\"public\" href=\"../crl/$PKI_ISSUER_NAME_ID.crl\">$PKI_ISSUER_NAME_ID.crl</a></td>"
[ -L "crl/$INT_CRL_NAME" ] || ln -sf $PKI_ISSUER_NAME_ID.crl crl/$INT_CRL_NAME
PKI_INT_CRL_LINK="<a class=\"public\" href=\"../crl/$INT_CRL_NAME\">$INT_CRL_NAME</a></td>"
PKI_INT_CRL_VALIDITY="$(openssl crl -noout -inform der -in crl/$PKI_ISSUER_NAME_ID.crl -lastupdate | sed -e "s/.*=/Last Update: /")<br/> $(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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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|<title>.*</title>|<title>$PKI_WEB_TITLE</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="<a class=\"public\" href=\"$ROOT_BASE_NAME.pem\">$ROOT_BASE_NAME.pem</a></td>"
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: /")<br/> $(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: /")<br/> $(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="<a class=\"public\" href=\"../crl/$ROOT_CRL_NAME\">$ROOT_CRL_NAME</a></td>"
PKI_ROOT_CRL_VALIDITY="$(openssl crl -noout -in $ROOT_CRL_FILE -lastupdate | sed -e "s/.*=/Last Update: /")<br/> $(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="<a class=\"public\" href=\"$INT_BASE_NAME.pem\">$INT_BASE_NAME.pem</a></td>"
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: /")<br/> $(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="<a class=\"public\" href=\"../crl/$PKI_ISSUER_NAME_ID.crl\">$PKI_ISSUER_NAME_ID.crl</a></td>"
[ -L "crl/$INT_CRL_NAME" ] || ln -sf $PKI_ISSUER_NAME_ID.crl crl/$INT_CRL_NAME
PKI_INT_CRL_LINK="<a class=\"public\" href=\"../crl/$INT_CRL_NAME\">$INT_CRL_NAME</a></td>"
PKI_INT_CRL_VALIDITY="$(openssl crl -noout -inform der -in crl/$PKI_ISSUER_NAME_ID.crl -lastupdate | sed -e "s/.*=/Last Update: /")<br/> $(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

File diff suppressed because it is too large Load Diff

View File

@@ -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
}

View File

@@ -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

View File

@@ -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=

733
gui/hsm.go Normal file
View File

@@ -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)
}

View File

@@ -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.</p>"))
} else if stage == "root-ca" {
} else if stage == "root-01" {
return template.HTML(fmt.Sprint("<p>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.</p>\n",
"<p>If you want to <b>generate</b> 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.</p>\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.</p>"))
} else if stage == "ca-int" {
} else if stage == "issuer-01" {
return template.HTML(fmt.Sprint("<p>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.</p>\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()

View File

@@ -60,8 +60,7 @@
<thead><tr>
<th>CA Type</th>
<th>Distinguished Name</th>
<th>Windows format</th>
<th>Linux format</th>
<th>Certificate File</th>
<th>Validity Period</th>
<th>CRL</th>
<th>CRL Validity</th>
@@ -70,17 +69,15 @@
<tr>
<td>Root CA</td>
<td><!-- BEGIN PKI_ROOT_DN -->PKI_ROOT_DN<!-- END PKI_ROOT_DN --></td>
<td><a class="public" href="root-ca.der">root-ca.der</a></td>
<td><a class="public" href="root-ca.pem">root-ca.pem</a></td>
<td><!-- BEGIN PKI_ROOT_LINK --><a class="public" href="root-01-cert.pem">root-01-cert.pem</a><!-- END PKI_ROOT_LINK --></td>
<td><!-- BEGIN PKI_ROOT_VALIDITY -->PKI_ROOT_VALIDITY<!-- END PKI_ROOT_VALIDITY --></td>
<td><a class="public" href="../crl/root-ca.crl">root-ca.crl</a></td>
<td><!-- BEGIN PKI_ROOT_CRL_LINK --><!-- END PKI_ROOT_CRL_LINK --></td>
<td><!-- BEGIN PKI_ROOT_CRL_VALIDITY --><!-- END PKI_ROOT_CRL_VALIDITY --></td>
</tr>
<tr>
<td>Issuing CA</td>
<td><!-- BEGIN PKI_INT_DN -->PKI_INT_DN<!-- END PKI_INT_DN --></td>
<td><a class="public" href="ca-int.der">ca-int.der</a></td>
<td><a class="public" href="ca-int.pem">ca-int.pem</a></td>
<td><!-- BEGIN PKI_INT_LINK --><a class="public" href="issuer-01-cert.pem">issuer-01-cert.pem</a><!-- END PKI_INT_LINK --></td>
<td><!-- BEGIN PKI_INT_VALIDITY -->PKI_INT_VALIDITY<!-- END PKI_INT_VALIDITY --></td>
<td><!-- BEGIN PKI_INT_CRL_LINK --><!-- END PKI_INT_CRL_LINK --></td>
<td><!-- BEGIN PKI_INT_CRL_VALIDITY --><!-- END PKI_INT_CRL_VALIDITY --></td>
@@ -90,9 +87,7 @@
<p>
To trust the certificates provided by <!-- BEGIN WEBTITLE -->LabCA<!-- END WEBTITLE -->, all your client devices
should install the root certificate in their <b>Trusted Root Certification Authorities</b> 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 <b>Trusted Root Certification Authorities</b> store.
</p>
<p>
The CRL (Certificate Revocation List) is a type of blocklist that includes certificates that should no longer be

View File

@@ -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

View File

@@ -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

View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -59,13 +59,6 @@
<span class="error">{{ . }}</span>
{{ end }}
</div>
<div class="form-group">
<label for="ou">Org. Unit (optional):</label>
<input class="form-control" type="text" id="ou" name="ou" value="{{ .OrgUnit }}" {{ if and (ne .Organization "") (.IsRootGenerated) }}readonly{{ end }}>
{{ with .Errors.OrgUnit }}
<span class="error">{{ . }}</span>
{{ end }}
</div>
<div class="form-group">
<label for="cn">Common Name:</label>
<input class="form-control" type="text" id="cn" name="cn" value="{{ .CommonName }}" required>
@@ -136,12 +129,12 @@
<textarea class="form-control" id="certificate" name="certificate" rows="10" cols="80" required>{{ .Certificate }}</textarea>
</div>
<div class="form-group">
<label for="key">Key (in PEM format{{ if .IsRoot }}; optional{{ end }}):
<label for="key">Key (in PEM format):
{{ with .Errors.Key }}
<span class="error"><br/>{{ . }}</span>
{{ end }}
</label>
<textarea class="form-control" id="key" name="key" rows="10" cols="80">{{ .Key }}</textarea>
<textarea class="form-control" id="key" name="key" rows="10" cols="80" required>{{ .Key }}</textarea>
</div>
<div class="form-group">
<label for="passphrase">Passphrase (optional):
@@ -353,4 +346,4 @@
updateEndDate();
});
</script>
{{end}}
{{end}}

View File

@@ -165,6 +165,7 @@
<tr>
<th>Subject</th>
<th></th>
<th>Type</th>
<th>Active</th>
<th></th>
</tr>
@@ -177,10 +178,11 @@
<td class="vmiddle">
<a href="/certs/{{ $item.RootCert.BaseName }}.pem" title="Download this certificate">download</a>
</td>
<td class="vmiddle">{{ $item.RootCert.KeyType }}</td>
<td class="vmiddle"></td>
<td class="vmiddle">
<button class="btn btn-outline btn-reg btn-success export-cert-key" type="button" title="Export this certificate and key" data-name="{{ $item.RootCert.BaseName }}" data-subject="{{ $item.RootCert.Subject }}">Export</button>
<button class="btn btn-outline btn-reg btn-warning renew-cert" type="button" title="Generate new version with extended lifetime" data-name="{{ $item.RootCert.BaseName }}" data-rootname="{{ $item.RootCert.BaseName }}" data-subject="{{ $item.RootCert.Subject }}" data-rootsubject="{{ $item.RootCert.Subject }}" data-notbefore="{{ $item.RootCert.NotAfter }}" data-isroot="true">Renew</button>
<button class="btn btn-outline btn-reg btn-warning renew-cert" type="button" title="Generate new version with extended lifetime" data-name="{{ $item.RootCert.BaseName }}" data-rootname="{{ $item.RootCert.BaseName }}" data-subject="{{ $item.RootCert.Subject }}" data-keytype="{{ $item.RootCert.KeyType }}" data-rootsubject="{{ $item.RootCert.Subject }}" data-notbefore="{{ $item.RootCert.NotAfter }}" data-isroot="true">Renew</button>
<!--
<button class="btn btn-outline btn-new-issuer btn-warning new-issuer-cert" type="button" title="Generate new Issuer Certificate under this Root" data-root="{{ $item.RootCert.BaseName }}" data-rootsubject="{{ $item.RootCert.Subject }}">New Issuer</button>
<button class="btn btn-outline btn-reg btn-danger delete-cert hidden" type="button" title="Delete this certificate" data-name="{{ $item.RootCert.BaseName }}">Delete</button>
@@ -196,10 +198,11 @@
<td class="vmiddle">
<a href="/certs/{{ $subitem.BaseName }}.pem" title="Download this certificate">download</a>
</td>
<td class="vmiddle">{{ $subitem.KeyType }}</td>
<td class="vmiddle center"><input type="radio" id="active-{{ $subitem.BaseName }}" name="issue-active" value="{{ $subitem.BaseName }}" title="Use this certificate for issueing leave certificates" {{ if $subitem.ActiveIssuer }}data-orig="true" checked{{ end }}/></td>
<td class="vmiddle">
<button class="btn btn-outline btn-reg btn-success export-cert-key" type="button" title="Export this certificate and key" data-name="{{ $subitem.BaseName }}" data-subject="{{ $subitem.Subject }}">Export</button>
<button class="btn btn-outline btn-reg btn-warning renew-cert" type="button" title="Generate new version with extended lifetime" data-name="{{ $subitem.BaseName }}" data-rootname="{{ $item.RootCert.BaseName }}" data-subject="{{ $subitem.Subject }}" data-rootsubject="{{ $item.RootCert.Subject }}" data-notbefore="{{ $subitem.NotAfter }}" data-notafter="{{ $item.RootCert.NotAfter }}" data-isroot="false">Renew</button>
<button class="btn btn-outline btn-reg btn-warning renew-cert" type="button" title="Generate new version with extended lifetime" data-name="{{ $subitem.BaseName }}" data-rootname="{{ $item.RootCert.BaseName }}" data-subject="{{ $subitem.Subject }}" data-keytype="{{ $subitem.KeyType }}" data-rootsubject="{{ $item.RootCert.Subject }}" data-notbefore="{{ $subitem.NotAfter }}" data-notafter="{{ $item.RootCert.NotAfter }}" data-isroot="false">Renew</button>
<!--
<button class="btn btn-outline btn-reg btn-danger delete-cert" type="button" title="Delete this certificate" data-name="{{ $subitem.BaseName }}">Delete</button>
-->
@@ -226,13 +229,12 @@
<td class="vmiddle">Root CRL</td>
<td class="vmiddle">
<button class="btn btn-outline btn-success mt5" type="button" id="gen-root-crl" title="Generate root CRL now (requires Root CA key)">Generate</button>
<button class="btn btn-outline btn-success mt5" type="button" id="upload-root-crl" title="Upload a root CRL">Upload</button>
</td>
</tr>
<tr>
<td class="vmiddle">Issuer CRL</td>
<td class="vmiddle">
<button class="btn btn-outline btn-success mt5" type="button" id="gen-issuer-crl" title="Generate root CRL now (requires Root CA key)">Generate</button>
<button class="btn btn-outline btn-success mt5" type="button" id="gen-issuer-crl" title="Generate issuer CRL now">Generate</button>
</td>
</tr>
</tbody>
@@ -549,7 +551,8 @@
<input type="hidden" id="modal-renew-cert" name="modal-renew-cert">
<input type="hidden" id="renew-rootcert" name="renew-rootcert">
<input type="hidden" id="renew-rootsubject" name="renew-rootsubject">
Subject: <span id="renew-subject"></span><br><br>
Subject: <span id="renew-subject"></span><br>
Key Type: <span id="renew-keytype"></span><br><br>
Current end date: <span id="renew-current-enddate"></span><br>
<span id="renew-show-root-enddate" class="hidden">Root end date: <span id="renew-root-enddate"></span><br></span>
<br>
@@ -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);

191
gui/upgrades.go Normal file
View File

@@ -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
}

11
install
View File

@@ -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"

View File

@@ -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 <<EOL
{
"module": "/usr/lib/softhsm/libsofthsm2.so",
"tokenLabel": "intermediate signing key (rsa)",
"pin": "1234"
}
EOL
fi
rm -f test-ca2.pem

View File

@@ -23,6 +23,10 @@ $SUDO patch -p1 < $cloneDir/patches/boulder-va_main.patch
$SUDO patch -p1 < $cloneDir/patches/ca_ca.patch
$SUDO patch -p1 < $cloneDir/patches/ca_ca_keytype_hack.patch
$SUDO patch -p1 < $cloneDir/patches/ca_crl.patch
$SUDO patch -p1 < $cloneDir/patches/ceremony_ecdsa.patch
$SUDO patch -p1 < $cloneDir/patches/ceremony_key.patch
$SUDO patch -p1 < $cloneDir/patches/ceremony_main.patch
$SUDO patch -p1 < $cloneDir/patches/ceremony_rsa.patch
$SUDO patch -p1 < $cloneDir/patches/cert-checker_main.patch
$SUDO patch -p1 < $cloneDir/patches/cmd_config.patch
$SUDO patch -p1 < $cloneDir/patches/config_duration.patch
@@ -50,6 +54,7 @@ $SUDO patch -p1 < $cloneDir/patches/ra_ra.patch
$SUDO patch -p1 < $cloneDir/patches/ratelimit_rate-limits.patch
$SUDO patch -p1 < $cloneDir/patches/ratelimits_names.patch
$SUDO patch -p1 < $cloneDir/patches/remoteva_main.patch
$SUDO patch -p1 < $cloneDir/patches/start.patch
if [ "$SUDO" == "" ]; then
# TODO: should include this into startservers.patch
$SUDO patch -p1 < $cloneDir/build/tmp2.patch

View File

@@ -0,0 +1,40 @@
diff --git a/cmd/ceremony/ecdsa.go b/cmd/ceremony/ecdsa.go
index 65f5c6f99..24102ad8e 100644
--- a/cmd/ceremony/ecdsa.go
+++ b/cmd/ceremony/ecdsa.go
@@ -29,7 +29,7 @@ var curveToOIDDER = map[string][]byte{
// ecArgs constructs the private and public key template attributes sent to the
// device and specifies which mechanism should be used. curve determines which
// type of key should be generated.
-func ecArgs(label string, curve elliptic.Curve, keyID []byte) generateArgs {
+func ecArgs(label string, curve elliptic.Curve, keyID []byte, extractable bool) generateArgs {
encodedCurve := curveToOIDDER[curve.Params().Name]
log.Printf("\tEncoded curve parameters for %s: %X\n", curve.Params().Name, encodedCurve)
return generateArgs{
@@ -50,7 +50,7 @@ func ecArgs(label string, curve elliptic.Curve, keyID []byte) generateArgs {
// Prevent attributes being retrieved
pkcs11.NewAttribute(pkcs11.CKA_SENSITIVE, true),
// Prevent the key being extracted from the device
- pkcs11.NewAttribute(pkcs11.CKA_EXTRACTABLE, false),
+ pkcs11.NewAttribute(pkcs11.CKA_EXTRACTABLE, extractable),
// Allow the key to sign data
pkcs11.NewAttribute(pkcs11.CKA_SIGN, true),
},
@@ -81,7 +81,7 @@ func ecPub(
// specified by curveStr and with the provided label. It returns the public
// part of the generated key pair as a ecdsa.PublicKey and the random key ID
// that the HSM uses to identify the key pair.
-func ecGenerate(session *pkcs11helpers.Session, label, curveStr string) (*ecdsa.PublicKey, []byte, error) {
+func ecGenerate(session *pkcs11helpers.Session, label, curveStr string, extractable bool) (*ecdsa.PublicKey, []byte, error) {
curve, present := stringToCurve[curveStr]
if !present {
return nil, nil, fmt.Errorf("curve %q not supported", curveStr)
@@ -92,7 +92,7 @@ func ecGenerate(session *pkcs11helpers.Session, label, curveStr string) (*ecdsa.
return nil, nil, err
}
log.Printf("Generating ECDSA key with curve %s and ID %x\n", curveStr, keyID)
- args := ecArgs(label, curve, keyID)
+ args := ecArgs(label, curve, keyID, extractable)
pub, _, err := session.GenerateKeyPair(args.mechanism, args.publicAttrs, args.privateAttrs)
if err != nil {
return nil, nil, err

View File

@@ -0,0 +1,19 @@
diff --git a/cmd/ceremony/key.go b/cmd/ceremony/key.go
index e0ed20594..4c4656b4d 100644
--- a/cmd/ceremony/key.go
+++ b/cmd/ceremony/key.go
@@ -56,12 +56,12 @@ func generateKey(session *pkcs11helpers.Session, label string, outputPath string
var keyID []byte
switch config.Type {
case "rsa":
- pubKey, keyID, err = rsaGenerate(session, label, config.RSAModLength)
+ pubKey, keyID, err = rsaGenerate(session, label, config.RSAModLength, config.Extractable)
if err != nil {
return nil, fmt.Errorf("failed to generate RSA key pair: %s", err)
}
case "ecdsa":
- pubKey, keyID, err = ecGenerate(session, label, config.ECDSACurve)
+ pubKey, keyID, err = ecGenerate(session, label, config.ECDSACurve, config.Extractable)
if err != nil {
return nil, fmt.Errorf("failed to generate ECDSA key pair: %s", err)
}

View File

@@ -0,0 +1,85 @@
diff --git a/cmd/ceremony/main.go b/cmd/ceremony/main.go
index f18979fef..0aa8eb0c7 100644
--- a/cmd/ceremony/main.go
+++ b/cmd/ceremony/main.go
@@ -98,6 +98,7 @@ type keyGenConfig struct {
Type string `yaml:"type"`
RSAModLength uint `yaml:"rsa-mod-length"`
ECDSACurve string `yaml:"ecdsa-curve"`
+ Extractable bool `yaml:"extractable"`
}
var allowedCurves = map[string]bool{
@@ -174,6 +175,7 @@ type rootConfig struct {
} `yaml:"outputs"`
CertProfile certProfile `yaml:"certificate-profile"`
SkipLints []string `yaml:"skip-lints"`
+ Renewal bool `yaml:"renewal"`
}
func (rc rootConfig) validate() error {
@@ -189,9 +191,11 @@ func (rc rootConfig) validate() error {
}
// Output fields
- err = checkOutputFile(rc.Outputs.PublicKeyPath, "public-key-path")
- if err != nil {
- return err
+ if !rc.Renewal {
+ err = checkOutputFile(rc.Outputs.PublicKeyPath, "public-key-path")
+ if err != nil {
+ return err
+ }
}
err = checkOutputFile(rc.Outputs.CertificatePath, "certificate-path")
if err != nil {
@@ -629,23 +633,42 @@ func rootCeremony(configBytes []byte) error {
return fmt.Errorf("failed to setup session and PKCS#11 context for slot %d: %s", config.PKCS11.StoreSlot, err)
}
log.Printf("Opened PKCS#11 session for slot %d\n", config.PKCS11.StoreSlot)
- keyInfo, err := generateKey(session, config.PKCS11.StoreLabel, config.Outputs.PublicKeyPath, config.Key)
- if err != nil {
- return err
+ var rKeyInfo *keyInfo
+ if config.Renewal {
+ // Reuse existing root key for a renewal
+ pub, _, err := loadPubKey(config.Outputs.PublicKeyPath)
+ if err != nil {
+ return err
+ }
+
+ der, err := x509.MarshalPKIXPublicKey(pub)
+ if err != nil {
+ return fmt.Errorf("Failed to marshal public key: %s", err)
+ }
+
+ rKeyInfo = &keyInfo{
+ key: pub,
+ der: der,
+ }
+ } else {
+ rKeyInfo, err = generateKey(session, config.PKCS11.StoreLabel, config.Outputs.PublicKeyPath, config.Key)
+ if err != nil {
+ return err
+ }
}
- signer, err := session.NewSigner(config.PKCS11.StoreLabel, keyInfo.key)
+ signer, err := session.NewSigner(config.PKCS11.StoreLabel, rKeyInfo.key)
if err != nil {
return fmt.Errorf("failed to retrieve signer: %s", err)
}
- template, err := makeTemplate(newRandReader(session), &config.CertProfile, keyInfo.der, nil, rootCert)
+ template, err := makeTemplate(newRandReader(session), &config.CertProfile, rKeyInfo.der, nil, rootCert)
if err != nil {
return fmt.Errorf("failed to create certificate profile: %s", err)
}
- lintCert, err := issueLintCertAndPerformLinting(template, template, keyInfo.key, signer, config.SkipLints)
+ lintCert, err := issueLintCertAndPerformLinting(template, template, rKeyInfo.key, signer, config.SkipLints)
if err != nil {
return err
}
- finalCert, err := signAndWriteCert(template, template, lintCert, keyInfo.key, signer, config.Outputs.CertificatePath)
+ finalCert, err := signAndWriteCert(template, template, lintCert, rKeyInfo.key, signer, config.Outputs.CertificatePath)
if err != nil {
return err
}

View File

@@ -0,0 +1,39 @@
diff --git a/cmd/ceremony/rsa.go b/cmd/ceremony/rsa.go
index 69e326b39..1bd60f750 100644
--- a/cmd/ceremony/rsa.go
+++ b/cmd/ceremony/rsa.go
@@ -18,7 +18,7 @@ const (
// device and specifies which mechanism should be used. modulusLen specifies the
// length of the modulus to be generated on the device in bits and exponent
// specifies the public exponent that should be used.
-func rsaArgs(label string, modulusLen, exponent uint, keyID []byte) generateArgs {
+func rsaArgs(label string, modulusLen, exponent uint, keyID []byte, extractable bool) generateArgs {
// Encode as unpadded big endian encoded byte slice
expSlice := big.NewInt(int64(exponent)).Bytes()
log.Printf("\tEncoded public exponent (%d) as: %0X\n", exponent, expSlice)
@@ -44,7 +44,7 @@ func rsaArgs(label string, modulusLen, exponent uint, keyID []byte) generateArgs
// Prevent attributes being retrieved
pkcs11.NewAttribute(pkcs11.CKA_SENSITIVE, true),
// Prevent the key being extracted from the device
- pkcs11.NewAttribute(pkcs11.CKA_EXTRACTABLE, false),
+ pkcs11.NewAttribute(pkcs11.CKA_EXTRACTABLE, extractable),
// Allow the key to create signatures
pkcs11.NewAttribute(pkcs11.CKA_SIGN, true),
},
@@ -75,14 +75,14 @@ func rsaPub(session *pkcs11helpers.Session, object pkcs11.ObjectHandle, modulusL
// specified by modulusLen and with the exponent 65537.
// It returns the public part of the generated key pair as a rsa.PublicKey
// and the random key ID that the HSM uses to identify the key pair.
-func rsaGenerate(session *pkcs11helpers.Session, label string, modulusLen uint) (*rsa.PublicKey, []byte, error) {
+func rsaGenerate(session *pkcs11helpers.Session, label string, modulusLen uint, extractable bool) (*rsa.PublicKey, []byte, error) {
keyID := make([]byte, 4)
_, err := newRandReader(session).Read(keyID)
if err != nil {
return nil, nil, err
}
log.Printf("Generating RSA key with %d bit modulus and public exponent %d and ID %x\n", modulusLen, rsaExp, keyID)
- args := rsaArgs(label, modulusLen, rsaExp, keyID)
+ args := rsaArgs(label, modulusLen, rsaExp, keyID, extractable)
pub, _, err := session.GenerateKeyPair(args.mechanism, args.publicAttrs, args.privateAttrs)
if err != nil {
return nil, nil, err

View File

@@ -1,5 +1,5 @@
diff --git a/docker-compose.yml b/docker-compose.yml
index 6caf66306..c7939ece4 100644
index d0a439f0f..71203004d 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,3 +1,4 @@
@@ -22,7 +22,7 @@ index 6caf66306..c7939ece4 100644
+ - /home/labca/nginx_data/static:/opt/wwwstatic
- ./.gocache:/root/.cache/go-build:cached
- - ./test/certs/.softhsm-tokens/:/var/lib/softhsm/tokens/:cached
+ - /home/labca/boulder_labca/certs/.softhsm-tokens/:/var/lib/softhsm/tokens/:cached
+ - /home/labca/boulder_labca/certs/.softhsm-tokens/:/var/lib/softhsm/tokens/
networks:
bouldernet:
ipv4_address: 10.77.77.77
@@ -66,7 +66,7 @@ index 6caf66306..c7939ece4 100644
networks:
bouldernet:
aliases:
@@ -84,46 +79,96 @@ services:
@@ -84,46 +79,98 @@ services:
# small.
command: mysqld --bind-address=0.0.0.0 --slow-query-log --log-output=TABLE --log-queries-not-using-indexes=ON
logging:
@@ -122,6 +122,7 @@ index 6caf66306..c7939ece4 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/
+ expose:
+ - 3000
+ depends_on:
@@ -162,6 +163,7 @@ index 6caf66306..c7939ece4 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

View File

@@ -1,8 +1,8 @@
diff --git a/test/entrypoint.sh b/test/entrypoint.sh
index a47fd2c9a..90148c0d5 100755
index a47fd2c9a..626d57155 100755
--- a/test/entrypoint.sh
+++ b/test/entrypoint.sh
@@ -13,15 +13,27 @@ service rsyslog start
@@ -13,15 +13,15 @@ service rsyslog start
# make sure we can reach the mysqldb.
./test/wait-for-it.sh boulder-mysql 3306
@@ -17,18 +17,6 @@ index a47fd2c9a..90148c0d5 100755
+# Generate the internal keys and certs
+./test/certs/generate.sh
+
+fl=$(pwd)/labca/setup_complete
+while [ ! -f $fl ]; do
+ echo "Waiting for $fl to appear..."
+ sleep 30
+done
+
+#softhsm2-util --show-slots
+softhsm2-util --init-token --slot 0 --label "intermediate signing key (rsa)" --pin 1234 --so-pin 5678 | /bin/true
+[ -e labca/test-ca.p8 ] && softhsm2-util --import labca/test-ca.p8 --id 333333 --force --token "intermediate signing key (rsa)" --pin 1234 --so-pin 5678 --label 'intermediate_key'
+softhsm2-util --init-token --slot 1 --label "root signing key (rsa)" --pin 1234 --so-pin 5678 | /bin/true
+[ -e labca/test-root.p8 ] && softhsm2-util --import labca/test-root.p8 --id 777777 --force --token "root signing key (rsa)" --pin 1234 --so-pin 5678 --label 'root_key'
+
if [[ $# -eq 0 ]]; then
exec python3 ./start.py

16
patches/start.patch Normal file
View File

@@ -0,0 +1,16 @@
diff --git a/start.py b/start.py
index f224b9e6c..017fe5cd5 100755
--- a/start.py
+++ b/start.py
@@ -20,6 +20,11 @@ import startservers
if not startservers.install(race_detection=False):
raise(Exception("failed to build"))
+fl = "./labca/setup_complete"
+while not os.path.exists(fl):
+ print(f"Waiting for '{fl}' to appear...")
+ time.sleep(30)
+
if not startservers.start(fakeclock=None):
sys.exit(1)
try:

View File

@@ -25,4 +25,10 @@ vrs=$(grep version /opt/labca/data/config.json | sed -e 's/.*:[ ]*//' | sed -e '
rm -rf /opt/labca/data/* && mv $TMPDIR/data/* /opt/labca/data/
sed -i -e "s/\"version\": \".*\"/\"version\": \"$vrs\"/" /opt/labca/data/config.json
[ -d $TMPDIR/webpki ] || (echo "Public CA files backup not found"; exit 1)
rm -rf /opt/boulder/labca/certs/webpki/* && mv $TMPDIR/webpki/* /opt/boulder/labca/certs/webpki/
[ -d $TMPDIR/tokens ] || (echo "SoftHSMv2 tokens folder backup not found"; exit 1)
rm -rf /var/lib/softhsm/tokens/* && mv $TMPDIR/tokens/* /var/lib/softhsm/tokens/
rm -rf $TMPDIR