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

")) - } else if stage == "root-ca" { + } else if stage == "root-01" { return template.HTML(fmt.Sprint("

This is the top level certificate that will sign the issuer\n", "certificate(s). You can either generate a fresh Root CA (Certificate Authority) or import an\n", "existing one, e.g. a backup from another LabCA instance.

\n", "

If you want to generate a new certificate, pick a key type and strength (the higher the number the\n", - "more secure, ECDSA is more modern than RSA), provide at least a country and organization name,\n", + "more secure, ECDSA is more modern than RSA), provide a country and organization name,\n", "and the common name. It is recommended that the common name contains the word 'Root' as well\n", "as your organization name so you can recognize it, and that's why that is automatically filled\n", "once you leave the organization field.

\n", @@ -2378,7 +2398,7 @@ func _helptext(stage string) template.HTML { "offline for security reasons according to best practices. If you do include it here, we will be able\n", "to generate an issuing certificate automatically in the next step. If you don't include it, we will\n", "ask for it when needed.

")) - } else if stage == "ca-int" { + } else if stage == "issuer-01" { return template.HTML(fmt.Sprint("

This is what end users will see as the issuing certificate. Again,\n", "you can either generate a fresh certificate or import an existing one, as long as it is signed by\n", "the Root CA from the previous step.

\n", @@ -2458,7 +2478,7 @@ func _setupAdminUser(w http.ResponseWriter, r *http.Request) bool { } defer conn.Close() - fmt.Fprintf(conn, "backup-restore\n"+header.Filename+"\n") + fmt.Fprint(conn, "backup-restore\n"+header.Filename+"\n") reader := bufio.NewReader(conn) message, err := io.ReadAll(reader) if err != nil { @@ -2734,18 +2754,18 @@ func setupHandler(w http.ResponseWriter, r *http.Request) { } // 3. Setup root CA certificate - if !_certCreate(w, r, "root-ca", true) { + if !_certCreate(w, r, "root-01", true) { // Cleanup the cert (if it even exists) so we will retry on the next run - if _, err := os.Stat("data/root-ca.pem"); !errors.Is(err, fs.ErrNotExist) { - exeCmd("mv data/root-ca.pem data/root-ca.pem_TMP") + if _, err := os.Stat(CERT_FILES_PATH + "root-01-cert.pem"); !errors.Is(err, fs.ErrNotExist) { + exeCmd("mv " + CERT_FILES_PATH + "root-01-cert.pem " + CERT_FILES_PATH + "root-01-cert.pem_TMP") } return } // 4. Setup issuer certificate - if !_certCreate(w, r, "ca-int", false) { + if !_certCreate(w, r, "issuer-01", false) { // Cleanup the cert (if it even exists) so we will retry on the next run - os.Remove("data/issuer/ca-int.pem") + os.Remove(CERT_FILES_PATH + "issuer-01-cert.pem") return } @@ -3326,9 +3346,10 @@ func init() { port := flag.Int("port", 0, "Port to listen on (default 3000 when using init)") versionFlag := flag.Bool("version", false, "Show version number and exit") decrypt := flag.String("d", "", "Decrypt a value") + renewcrl := flag.Int("renewcrl", 0, "Check root CRL files and renew if nextUpdate is in less than this number of days") flag.Parse() - if *versionFlag { + if *versionFlag && standaloneVersion != "" { fmt.Println(standaloneVersion) os.Exit(0) } @@ -3355,6 +3376,11 @@ func init() { panic(fmt.Errorf("fatal error config file: '%s'", err)) } + if *versionFlag && standaloneVersion == "" { + fmt.Println(viper.GetString("version")) + os.Exit(0) + } + if *decrypt != "" { plain, err := _decrypt(*decrypt) if err == nil { @@ -3365,6 +3391,57 @@ func init() { } } + if *renewcrl != 0 { + crlFiles, err := filepath.Glob(filepath.Join(CERT_FILES_PATH, "root-*-crl.pem")) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + for _, crlFile := range crlFiles { + read, err := os.ReadFile(crlFile) + if err != nil { + fmt.Printf("could not read '%s': %s\n", crlFile, err.Error()) + os.Exit(1) + } + block, _ := pem.Decode(read) + if block == nil || block.Type != "X509 CRL" { + fmt.Println(block) + fmt.Println("failed to decode PEM block containing revocation list") + os.Exit(1) + } + crl, err := x509.ParseRevocationList(block.Bytes) + if err != nil { + fmt.Printf("could not parse revocation list: %s\n", err.Error()) + os.Exit(1) + } + + now := time.Now() + if crl.NextUpdate.Sub(now) < time.Hour*24*time.Duration(*renewcrl) { + fmt.Printf("renewing crl file '%s'...\n", crlFile) + re := regexp.MustCompile(`-(\d{2})-`) + match := re.FindStringSubmatch(crlFile) + if len(match) > 1 { + seqnr := match[1] + ci := &CertificateInfo{} + ci.Initialize() + err = ci.CeremonyRootCRL(seqnr) + if err == nil { + fmt.Printf("updated %s\n", crlFile) + } else { + fmt.Printf("could not update crl file '%s': %s\n", crlFile, err.Error()) + os.Exit(1) + } + } else { + fmt.Printf("could not extract sequence number from filename '%s'\n", crlFile) + os.Exit(1) + } + } + } + + os.Exit(0) + } + var err error if *init || viper.GetBool("standalone") { tmpls, err = templates.New().ParseEmbed(embeddedTemplates, "templates/") @@ -3452,6 +3529,10 @@ func init() { updateAvailable = false + if !viper.GetBool("standalone") { + CheckUpgrades() + } + /* // TODO: Still needs to be done for this! // Store boulder chains if we don't have them already @@ -3474,6 +3555,47 @@ func init() { */ } +type BackupResult struct { + Existed bool + NewName string + OrigName string +} + +func (br BackupResult) Remove() { + os.Remove(br.NewName) +} + +func (br BackupResult) Restore() { + if br.Existed { + os.Rename(br.NewName, br.OrigName) + } +} + +func renameBackup(filename string) BackupResult { + result := BackupResult{ + Existed: false, + } + + if _, err := os.Stat(filename); !errors.Is(err, os.ErrNotExist) { + os.Remove(filename + "_BAK") // May not exist... + result.Existed = true + } + + if !result.Existed { + return result + } + + err := os.Rename(filename, filename+"_BAK") + if err != nil { + fmt.Printf("warning: failed to backup previous file '%s': %s\n", filename, err.Error()) + } else { + result.OrigName = filename + result.NewName = filename + "_BAK" + } + + return result +} + func main() { tmpls.Parse() diff --git a/gui/static/certs/index.html b/gui/static/certs/index.html index fa89867..0847f02 100644 --- a/gui/static/certs/index.html +++ b/gui/static/certs/index.html @@ -60,8 +60,7 @@ CA Type Distinguished Name - Windows format - Linux format + Certificate File Validity Period CRL CRL Validity @@ -70,17 +69,15 @@ Root CA PKI_ROOT_DN - root-ca.der - root-ca.pem + root-01-cert.pem PKI_ROOT_VALIDITY - root-ca.crl + Issuing CA PKI_INT_DN - ca-int.der - ca-int.pem + issuer-01-cert.pem PKI_INT_VALIDITY @@ -90,9 +87,7 @@

To trust the certificates provided by LabCA, all your client devices - should install the root certificate in their Trusted Root Certification Authorities store. You may choose - to download the format best suited for your Operating System: DER format for Windows machines or PEM format for - Linux/unix machines and Android phones. + should install the root certificate in their Trusted Root Certification Authorities store.

The CRL (Certificate Revocation List) is a type of blocklist that includes certificates that should no longer be diff --git a/gui/templates/cert-ceremonies/issuer-cert.yaml b/gui/templates/cert-ceremonies/issuer-cert.yaml new file mode 100644 index 0000000..ea0d258 --- /dev/null +++ b/gui/templates/cert-ceremonies/issuer-cert.yaml @@ -0,0 +1,26 @@ +ceremony-type: intermediate +pkcs11: + module: {{ .Module }} + pin: {{ .UserPIN }} + signing-key-slot: {{ .RootSlotID }} + signing-key-label: {{ .RootLabel }} +inputs: + public-key-path: {{ .Path }}issuer-{{ .SeqNr }}-pubkey.pem + issuer-certificate-path: {{ .Path }}root-{{ .RootSeqNr }}-cert.pem +outputs: + certificate-path: {{ .Path }}issuer-{{ .SeqNr }}-cert.pem +certificate-profile: + signature-algorithm: {{ .SignAlgorithm }} + common-name: {{ .CommonName }} + organization: {{ .OrgName }} + country: {{ .Country }} + not-before: {{ .NotBefore }} + not-after: {{ .NotAfter }} + crl-url: {{ .CrlUrl }} + issuer-url: {{ .IssuerUrl }} + policies: + - oid: 2.23.140.1.2.1 + key-usages: + - Digital Signature + - Cert Sign + - CRL Sign diff --git a/gui/templates/cert-ceremonies/issuer-key.yaml b/gui/templates/cert-ceremonies/issuer-key.yaml new file mode 100644 index 0000000..afd69e1 --- /dev/null +++ b/gui/templates/cert-ceremonies/issuer-key.yaml @@ -0,0 +1,19 @@ +ceremony-type: key +pkcs11: + module: {{ .Module }} + pin: {{ .UserPIN }} + store-key-in-slot: {{ .SlotID }} + store-key-with-label: {{ .Label }} +key: + type: {{ .KeyType }} +{{ if eq .KeyType "rsa" }} + rsa-mod-length: {{ .KeyParam }} +{{ else }} + ecdsa-curve: {{ .KeyParam }} +{{ end }} +{{ if eq .Extractable "true" }} + extractable: true +{{ end }} +outputs: + public-key-path: {{ .Path }}issuer-{{ .SeqNr }}-pubkey.pem + pkcs11-config-path: {{ .Path }}issuer-{{ .SeqNr }}.pkcs11.json diff --git a/gui/templates/cert-ceremonies/root-crl.yaml b/gui/templates/cert-ceremonies/root-crl.yaml new file mode 100644 index 0000000..4f00b63 --- /dev/null +++ b/gui/templates/cert-ceremonies/root-crl.yaml @@ -0,0 +1,14 @@ +ceremony-type: crl +pkcs11: + module: {{ .Module }} + pin: {{ .UserPIN }} + signing-key-slot: {{ .RootSlotID }} + signing-key-label: {{ .RootLabel }} +inputs: + issuer-certificate-path: {{ .Path }}root-{{ .RootSeqNr }}-cert.pem +outputs: + crl-path: {{ .Path }}root-{{ .RootSeqNr }}-crl.pem +crl-profile: + this-update: {{ .ThisUpdate }} + next-update: {{ .NextUpdate }} + number: {{ .CrlNumber }} diff --git a/gui/templates/cert-ceremonies/root.yaml b/gui/templates/cert-ceremonies/root.yaml new file mode 100644 index 0000000..33a74f8 --- /dev/null +++ b/gui/templates/cert-ceremonies/root.yaml @@ -0,0 +1,34 @@ +ceremony-type: root +pkcs11: + module: {{ .Module }} + pin: {{ .UserPIN }} + store-key-in-slot: {{ .SlotID }} + store-key-with-label: {{ .Label }} +key: + type: {{ .KeyType }} +{{ if eq .KeyType "rsa" }} + rsa-mod-length: {{ .KeyParam }} +{{ else }} + ecdsa-curve: {{ .KeyParam }} +{{ end }} +{{ if eq .Extractable "true" }} + extractable: true +{{ end }} +outputs: + public-key-path: {{ .Path }}root-{{ .SeqNr }}-pubkey.pem + certificate-path: {{ .Path }}root-{{ .SeqNr }}-cert.pem +certificate-profile: + signature-algorithm: {{ .SignAlgorithm }} + common-name: {{ .CommonName }} + organization: {{ .OrgName }} + country: {{ .Country }} + not-before: {{ .NotBefore }} + not-after: {{ .NotAfter }} + key-usages: + - Cert Sign + - CRL Sign +skip-lints: + - n_ca_digital_signature_not_set +{{ if eq .Renewal "true" }} +renewal: true +{{ end }} diff --git a/gui/templates/views/cert.tmpl b/gui/templates/views/cert.tmpl index ec5a770..2eb4b4d 100644 --- a/gui/templates/views/cert.tmpl +++ b/gui/templates/views/cert.tmpl @@ -59,13 +59,6 @@ {{ . }} {{ end }} -

- - - {{ with .Errors.OrgUnit }} - {{ . }} - {{ end }} -
@@ -136,12 +129,12 @@
-