mirror of
https://github.com/outbackdingo/labca.git
synced 2026-01-27 10:19:34 +00:00
Use ceremony tool for generating keys and certs; store keys on SoftHSM
Replace openssl certificate / CRL generation with the tool as used by Let's Encrypt, storing the keys on SoftHSMv2, a simulated HSM (Hardware Security Module). Include migration of old setups where key files were also stored on disk.
This commit is contained in:
15
gui/apply
15
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,8 +9,10 @@ PKI_WEB_TITLE=$(grep web_title $dataDir/config.json | sed -e 's/.*:[ ]*//' | sed
|
||||
if [ "$PKI_WEB_TITLE" == "" ]; then
|
||||
export PKI_WEB_TITLE="LabCA"
|
||||
fi
|
||||
PKI_ROOT_CERT_BASE="$dataDir/root-ca"
|
||||
PKI_INT_CERT_BASE="$dataDir/issuer/ca-int"
|
||||
PKI_ROOT_CERT_BASE="/opt/boulder/labca/certs/webpki/root-01-cert"
|
||||
[ ! -d "/home/labca/boulder_labca/certs/webpki" ] || PKI_ROOT_CERT_BASE="/home/labca/boulder_labca/certs/webpki/root-01-cert"
|
||||
PKI_INT_CERT_BASE="/opt/boulder/labca/certs/webpki/issuer-01-cert"
|
||||
[ ! -d "/home/labca/boulder_labca/certs/webpki" ] || PKI_INT_CERT_BASE="/home/labca/boulder_labca/certs/webpki/issuer-01-cert"
|
||||
PKI_ISSUER_NAME_ID=$(grep issuer_name_id $dataDir/config.json | sed -e 's/.*:[ ]*//' | sed -e 's/,//g' | sed -e 's/\"//g')
|
||||
if [ -z "$PKI_ISSUER_NAME_ID" ] && [ -e "$PKI_INT_CERT_BASE.pem" ]; then
|
||||
nmid=$(/opt/boulder/bin/nameid -s $PKI_INT_CERT_BASE.pem)
|
||||
@@ -27,26 +29,42 @@ sed -i -e "s|<title>.*</title>|<title>$PKI_WEB_TITLE</title>|g" 502.html
|
||||
sed -i -e "s|<\!-- BEGIN WEBTITLE -->.*<\!-- END WEBTITLE -->|<\!-- BEGIN WEBTITLE -->$PKI_WEB_TITLE<\!-- END WEBTITLE -->|g" 502.html
|
||||
|
||||
if [ -e $PKI_ROOT_CERT_BASE.pem ]; then
|
||||
PKI_ROOT_DN=$(openssl x509 -noout -in $PKI_ROOT_CERT_BASE.pem -subject | sed -e "s/subject= //")
|
||||
PKI_ROOT_DN=$(openssl x509 -noout -in $PKI_ROOT_CERT_BASE.pem -subject | sed -e "s/subject=//")
|
||||
sed -i -e "s|<\!-- BEGIN PKI_ROOT_DN -->.*<\!-- END PKI_ROOT_DN -->|<\!-- BEGIN PKI_ROOT_DN -->$PKI_ROOT_DN<\!-- END PKI_ROOT_DN -->|g" certs/index.html
|
||||
ROOT_BASE_NAME=$(basename $PKI_ROOT_CERT_BASE)
|
||||
PKI_ROOT_LINK="<a class=\"public\" href=\"$ROOT_BASE_NAME.pem\">$ROOT_BASE_NAME.pem</a></td>"
|
||||
sed -i -e "s|<\!-- BEGIN PKI_ROOT_LINK -->.*<\!-- END PKI_ROOT_LINK -->|<\!-- BEGIN PKI_ROOT_LINK -->$PKI_ROOT_LINK<\!-- END PKI_ROOT_LINK -->|g" certs/index.html
|
||||
PKI_ROOT_VALIDITY="$(openssl x509 -noout -in $PKI_ROOT_CERT_BASE.pem -startdate | sed -e "s/.*=/Not Before: /")<br/> $(openssl x509 -noout -in $PKI_ROOT_CERT_BASE.pem -enddate | sed -e "s/.*=/Not After: /")"
|
||||
sed -i -e "s|<\!-- BEGIN PKI_ROOT_VALIDITY -->.*<\!-- END PKI_ROOT_VALIDITY -->|<\!-- BEGIN PKI_ROOT_VALIDITY -->$PKI_ROOT_VALIDITY<\!-- END PKI_ROOT_VALIDITY -->|g" certs/index.html
|
||||
|
||||
ROOT_CRL_FILE=${PKI_ROOT_CERT_BASE/-cert/-crl}.pem
|
||||
PKI_ROOT_CRL_LINK=""
|
||||
PKI_ROOT_CRL_VALIDITY=""
|
||||
if [ -e "$PKI_ROOT_CERT_BASE.crl" ]; then
|
||||
PKI_ROOT_CRL_VALIDITY="$(openssl crl -noout -in $PKI_ROOT_CERT_BASE.crl -lastupdate | sed -e "s/.*=/Last Update: /")<br/> $(openssl crl -noout -in $PKI_ROOT_CERT_BASE.crl -nextupdate | sed -e "s/.*=/Next Update: /")"
|
||||
if [ -e $ROOT_CRL_FILE ]; then
|
||||
ROOT_CRL_NAME=$(basename $ROOT_CRL_FILE)
|
||||
[ -e "crl/$ROOT_CRL_NAME" ] || cp $ROOT_CRL_FILE crl/$ROOT_CRL_NAME
|
||||
PKI_ROOT_CRL_LINK="<a class=\"public\" href=\"../crl/$ROOT_CRL_NAME\">$ROOT_CRL_NAME</a></td>"
|
||||
PKI_ROOT_CRL_VALIDITY="$(openssl crl -noout -in $ROOT_CRL_FILE -lastupdate | sed -e "s/.*=/Last Update: /")<br/> $(openssl crl -noout -in $ROOT_CRL_FILE -nextupdate | sed -e "s/.*=/Next Update: /")"
|
||||
fi
|
||||
sed -i -e "s|<\!-- BEGIN PKI_ROOT_CRL_LINK -->.*<\!-- END PKI_ROOT_CRL_LINK -->|<\!-- BEGIN PKI_ROOT_CRL_LINK -->$PKI_ROOT_CRL_LINK<\!-- END PKI_ROOT_CRL_LINK -->|g" certs/index.html
|
||||
sed -i -e "s|<\!-- BEGIN PKI_ROOT_CRL_VALIDITY -->.*<\!-- END PKI_ROOT_CRL_VALIDITY -->|<\!-- BEGIN PKI_ROOT_CRL_VALIDITY -->$PKI_ROOT_CRL_VALIDITY<\!-- END PKI_ROOT_CRL_VALIDITY -->|g" certs/index.html
|
||||
fi
|
||||
|
||||
if [ -e $PKI_INT_CERT_BASE.pem ]; then
|
||||
PKI_INT_DN=$(openssl x509 -noout -in $PKI_INT_CERT_BASE.pem -subject | sed -e "s/subject= //")
|
||||
PKI_INT_DN=$(openssl x509 -noout -in $PKI_INT_CERT_BASE.pem -subject | sed -e "s/subject=//")
|
||||
sed -i -e "s|<\!-- BEGIN PKI_INT_DN -->.*<\!-- END PKI_INT_DN -->|<\!-- BEGIN PKI_INT_DN -->$PKI_INT_DN<\!-- END PKI_INT_DN -->|g" certs/index.html
|
||||
INT_BASE_NAME=$(basename $PKI_INT_CERT_BASE)
|
||||
PKI_INT_LINK="<a class=\"public\" href=\"$INT_BASE_NAME.pem\">$INT_BASE_NAME.pem</a></td>"
|
||||
sed -i -e "s|<\!-- BEGIN PKI_INT_LINK -->.*<\!-- END PKI_INT_LINK -->|<\!-- BEGIN PKI_INT_LINK -->$PKI_INT_LINK<\!-- END PKI_INT_LINK -->|g" certs/index.html
|
||||
PKI_INT_VALIDITY="$(openssl x509 -noout -in $PKI_INT_CERT_BASE.pem -startdate | sed -e "s/.*=/Not Before: /")<br/> $(openssl x509 -noout -in $PKI_INT_CERT_BASE.pem -enddate | sed -e "s/.*=/Not After: /")"
|
||||
sed -i -e "s|<\!-- BEGIN PKI_INT_VALIDITY -->.*<\!-- END PKI_INT_VALIDITY -->|<\!-- BEGIN PKI_INT_VALIDITY -->$PKI_INT_VALIDITY<\!-- END PKI_INT_VALIDITY -->|g" certs/index.html
|
||||
|
||||
INT_CRL_NAME=${INT_BASE_NAME/-cert/-crl}.pem
|
||||
PKI_INT_CRL_LINK=""
|
||||
PKI_INT_CRL_VALIDITY=""
|
||||
if [ -e "crl/$PKI_ISSUER_NAME_ID.crl" ]; then
|
||||
PKI_INT_CRL_LINK="<a class=\"public\" href=\"../crl/$PKI_ISSUER_NAME_ID.crl\">$PKI_ISSUER_NAME_ID.crl</a></td>"
|
||||
[ -L "crl/$INT_CRL_NAME" ] || ln -sf $PKI_ISSUER_NAME_ID.crl crl/$INT_CRL_NAME
|
||||
PKI_INT_CRL_LINK="<a class=\"public\" href=\"../crl/$INT_CRL_NAME\">$INT_CRL_NAME</a></td>"
|
||||
PKI_INT_CRL_VALIDITY="$(openssl crl -noout -inform der -in crl/$PKI_ISSUER_NAME_ID.crl -lastupdate | sed -e "s/.*=/Last Update: /")<br/> $(openssl crl -noout -inform der -in crl/$PKI_ISSUER_NAME_ID.crl -nextupdate | sed -e "s/.*=/Next Update: /")"
|
||||
fi
|
||||
sed -i -e "s|<\!-- BEGIN PKI_INT_CRL_LINK -->.*<\!-- END PKI_INT_CRL_LINK -->|<\!-- BEGIN PKI_INT_CRL_LINK -->$PKI_INT_CRL_LINK<\!-- END PKI_INT_CRL_LINK -->|g" certs/index.html
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -694,6 +694,8 @@ github.com/micromdm/scep/v2 v2.1.0/go.mod h1:BkF7TkPPhmgJAMtHfP+sFTKXmgzNJgLQlvv
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||
github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||
github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
|
||||
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
|
||||
733
gui/hsm.go
Normal file
733
gui/hsm.go
Normal file
@@ -0,0 +1,733 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/subtle"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/pkcs11"
|
||||
)
|
||||
|
||||
const CERT_FILES_PATH = "/opt/boulder/labca/certs/webpki/"
|
||||
|
||||
type HSMConfig struct {
|
||||
Module string
|
||||
UserPIN string
|
||||
SOPIN string
|
||||
SlotID string
|
||||
Label string
|
||||
}
|
||||
|
||||
// HSMSession represents a session with a given PKCS#11 module. It is NOT safe for concurrent access.
|
||||
type HSMSession struct {
|
||||
Context PKCSCtx
|
||||
Handle pkcs11.SessionHandle
|
||||
}
|
||||
|
||||
type PKCSCtx interface {
|
||||
CloseSession(pkcs11.SessionHandle) error
|
||||
CreateObject(pkcs11.SessionHandle, []*pkcs11.Attribute) (pkcs11.ObjectHandle, error)
|
||||
DestroyObject(pkcs11.SessionHandle, pkcs11.ObjectHandle) error
|
||||
FindObjects(pkcs11.SessionHandle, int) ([]pkcs11.ObjectHandle, bool, error)
|
||||
FindObjectsInit(pkcs11.SessionHandle, []*pkcs11.Attribute) error
|
||||
FindObjectsFinal(pkcs11.SessionHandle) error
|
||||
GenerateKey(pkcs11.SessionHandle, []*pkcs11.Mechanism, []*pkcs11.Attribute) (pkcs11.ObjectHandle, error)
|
||||
GetAttributeValue(pkcs11.SessionHandle, pkcs11.ObjectHandle, []*pkcs11.Attribute) ([]*pkcs11.Attribute, error)
|
||||
Logout(pkcs11.SessionHandle) error
|
||||
WrapKey(pkcs11.SessionHandle, []*pkcs11.Mechanism, pkcs11.ObjectHandle, pkcs11.ObjectHandle) ([]byte, error)
|
||||
}
|
||||
|
||||
func (cfg *HSMConfig) Initialize(ca_type string, seqnr string) {
|
||||
cfg.Module = "/usr/lib/softhsm/libsofthsm2.so"
|
||||
cfg.UserPIN = "1234"
|
||||
cfg.SOPIN = "5678"
|
||||
cfg.SlotID = "0"
|
||||
if ca_type != "root" {
|
||||
cfg.SlotID = "1"
|
||||
}
|
||||
cfg.Label = fmt.Sprintf("%s %s", ca_type, seqnr)
|
||||
}
|
||||
|
||||
func (cfg *HSMConfig) CreateSlot() error {
|
||||
s, err := strconv.ParseUint(cfg.SlotID, 10, 32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert slot id '%s' to uint: %s", cfg.SlotID, err.Error())
|
||||
}
|
||||
id, err := cfg.createSlot(uint(s), cfg.Label)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create slot: %s", err.Error())
|
||||
}
|
||||
cfg.SlotID = id
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findSlotWithLabel(ctx *pkcs11.Ctx, label string, missing_ok bool) (string, error) {
|
||||
slots, err := ctx.GetSlotList(true)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get slots list: %s", err)
|
||||
}
|
||||
|
||||
for _, slot := range slots {
|
||||
info, err := ctx.GetSlotInfo(slot)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get slot info: %s", err)
|
||||
}
|
||||
|
||||
if info.Flags&pkcs11.CKF_TOKEN_PRESENT == pkcs11.CKF_TOKEN_PRESENT {
|
||||
token, err := ctx.GetTokenInfo(slot)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get token info: %s", err)
|
||||
}
|
||||
|
||||
if token.Label == label {
|
||||
return fmt.Sprint(slot), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if missing_ok {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return "", errors.New("no slot found matching this label")
|
||||
}
|
||||
|
||||
func (cfg *HSMConfig) createSlot(slotId uint, label string) (string, error) {
|
||||
ctx := pkcs11.New(cfg.Module)
|
||||
if ctx == nil {
|
||||
return "", errors.New("failed to load pkcs11 module")
|
||||
}
|
||||
err := ctx.Initialize()
|
||||
if err != nil && err.Error() != "pkcs11: 0x191: CKR_CRYPTOKI_ALREADY_INITIALIZED" {
|
||||
return "", fmt.Errorf("failed to initialize pkcs11 context: %s", err)
|
||||
}
|
||||
|
||||
slot, err := findSlotWithLabel(ctx, label, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if slot != "" {
|
||||
return slot, nil
|
||||
}
|
||||
|
||||
// No slot found with this token label, so create a new one
|
||||
err = ctx.InitToken(slotId, cfg.SOPIN, label)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "0x3: CKR_SLOT_ID_INVALID") {
|
||||
slots, err := ctx.GetSlotList(true)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to initialize token, failed to get slot list: %s", err)
|
||||
}
|
||||
slotId = uint(len(slots) - 1)
|
||||
cfg.SlotID = fmt.Sprint(slotId)
|
||||
err = ctx.InitToken(slotId, cfg.SOPIN, label)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to initialize token with id %d: %s", slotId, err)
|
||||
}
|
||||
} else {
|
||||
return "", fmt.Errorf("failed to initialize token: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
session, err := ctx.OpenSession(slotId, pkcs11.CKF_SERIAL_SESSION|pkcs11.CKF_RW_SESSION)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open session: %s", err)
|
||||
}
|
||||
defer ctx.CloseSession(session)
|
||||
|
||||
err = ctx.Login(session, pkcs11.CKU_SO, cfg.SOPIN)
|
||||
if err != nil {
|
||||
if err.Error() == "pkcs11: 0xA0: CKR_PIN_INCORRECT" {
|
||||
return "", errors.New("incorrect SO PIN")
|
||||
} else {
|
||||
return "", fmt.Errorf("failed to login: %s", err)
|
||||
}
|
||||
}
|
||||
defer ctx.Logout(session)
|
||||
|
||||
err = ctx.InitPIN(session, cfg.UserPIN)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to initialize pin: %s", err)
|
||||
}
|
||||
|
||||
// Forced reconnect to get the renumbered slots from SoftHSM2
|
||||
ctx.Finalize()
|
||||
ctx.Destroy()
|
||||
ctx = pkcs11.New(cfg.Module)
|
||||
if ctx == nil {
|
||||
return "", errors.New("failed to reload pkcs11 module")
|
||||
}
|
||||
err = ctx.Initialize()
|
||||
if err != nil && err.Error() != "pkcs11: 0x191: CKR_CRYPTOKI_ALREADY_INITIALIZED" {
|
||||
return "", fmt.Errorf("failed to reinitialize pkcs11 context: %s", err)
|
||||
}
|
||||
|
||||
slot, err = findSlotWithLabel(ctx, label, false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if slot != "" {
|
||||
return slot, nil
|
||||
}
|
||||
|
||||
return "", errors.New("failed to create slot")
|
||||
}
|
||||
|
||||
// getSession establishes a logged in session on a pkcs11 slot.
|
||||
//
|
||||
// Don't forget to call .Close() on the resulting session when done!
|
||||
func (cfg *HSMConfig) getSession() (*HSMSession, error) {
|
||||
ctx := pkcs11.New(cfg.Module)
|
||||
if ctx == nil {
|
||||
return nil, errors.New("failed to load pkcs11 module")
|
||||
}
|
||||
err := ctx.Initialize()
|
||||
if err != nil && err.Error() != "pkcs11: 0x191: CKR_CRYPTOKI_ALREADY_INITIALIZED" {
|
||||
return nil, fmt.Errorf("failed to initialize pkcs11 context: %s", err)
|
||||
}
|
||||
|
||||
slot, err := findSlotWithLabel(ctx, cfg.Label, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if slot == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
s, err := strconv.ParseUint(slot, 10, 32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert slot id '%s' to uint: %s", cfg.SlotID, err.Error())
|
||||
}
|
||||
|
||||
session, err := ctx.OpenSession(uint(s), pkcs11.CKF_SERIAL_SESSION|pkcs11.CKF_RW_SESSION)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open session: %s", err)
|
||||
}
|
||||
|
||||
err = ctx.Login(session, pkcs11.CKU_USER, cfg.UserPIN)
|
||||
if err != nil {
|
||||
if err.Error() == "pkcs11: 0xA0: CKR_PIN_INCORRECT" {
|
||||
return nil, errors.New("incorrect user PIN")
|
||||
} else {
|
||||
return nil, fmt.Errorf("failed to login: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &HSMSession{ctx, session}, nil
|
||||
}
|
||||
|
||||
func (cfg *HSMConfig) ClearAll() error {
|
||||
hs, err := cfg.getSession()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get session: %s", err)
|
||||
}
|
||||
defer hs.Close()
|
||||
|
||||
err = hs.DestroyAllObjects(cfg.Label)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func arrConcat(arrays ...[]byte) []byte {
|
||||
out := make([]byte, len(arrays[0]))
|
||||
copy(out, arrays[0])
|
||||
for _, array := range arrays[1:] {
|
||||
out = append(out, array...)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func arrXor(arrL []byte, arrR []byte) []byte {
|
||||
out := make([]byte, len(arrL))
|
||||
for x := range arrL {
|
||||
out[x] = arrL[x] ^ arrR[x]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// AES Key Wrap algorithm is specified in RFC 3394
|
||||
func UnwrapKey(block cipher.Block, cipherText []byte) ([]byte, error) {
|
||||
//Initialize variables
|
||||
a := make([]byte, 8)
|
||||
n := (len(cipherText) / 8) - 1
|
||||
|
||||
r := make([][]byte, n)
|
||||
for i := range r {
|
||||
r[i] = make([]byte, 8)
|
||||
copy(r[i], cipherText[(i+1)*8:])
|
||||
}
|
||||
copy(a, cipherText[:8])
|
||||
|
||||
//Compute intermediate values
|
||||
for j := 5; j >= 0; j-- {
|
||||
for i := n; i >= 1; i-- {
|
||||
t := (n * j) + i
|
||||
tBytes := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(tBytes, uint64(t))
|
||||
|
||||
b := arrConcat(arrXor(a, tBytes), r[i-1])
|
||||
block.Decrypt(b, b)
|
||||
|
||||
copy(a, b[:len(b)/2])
|
||||
copy(r[i-1], b[len(b)/2:])
|
||||
}
|
||||
}
|
||||
|
||||
var defaultIV = []byte{0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6}
|
||||
if subtle.ConstantTimeCompare(a, defaultIV) != 1 {
|
||||
return nil, errors.New("integrity check failed - unexpected IV")
|
||||
}
|
||||
|
||||
//Output
|
||||
c := arrConcat(r...)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (cfg *HSMConfig) GetPrivateKey() ([]byte, error) {
|
||||
hs, err := cfg.getSession()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get session: %s", err)
|
||||
}
|
||||
defer hs.Close()
|
||||
|
||||
tmpl := []*pkcs11.Attribute{
|
||||
pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PRIVATE_KEY),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_LABEL, []byte(cfg.Label)),
|
||||
}
|
||||
|
||||
keyHandle, err := hs.FindObject(tmpl)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find private key with label='%s': %w", cfg.Label, err)
|
||||
}
|
||||
|
||||
// Generate a temporary wrapping key in memory
|
||||
mechs := []*pkcs11.Mechanism{
|
||||
// pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so -M | grep -v generate_key_pair | grep generate
|
||||
pkcs11.NewMechanism(pkcs11.CKM_AES_KEY_GEN, nil),
|
||||
}
|
||||
tmpl = []*pkcs11.Attribute{
|
||||
pkcs11.NewAttribute(pkcs11.CKA_VALUE_LEN, 16),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_ENCRYPT, true),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_DECRYPT, true),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_WRAP, true),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_UNWRAP, true),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_EXTRACTABLE, true),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_TOKEN, false),
|
||||
}
|
||||
wrapKeyHandle, err := hs.GenerateKey(mechs, tmpl)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate wrapping key: %w", err)
|
||||
}
|
||||
|
||||
// Extract the key value
|
||||
tmpl = []*pkcs11.Attribute{
|
||||
pkcs11.NewAttribute(pkcs11.CKA_VALUE, nil),
|
||||
}
|
||||
wrapKeyAttrs, err := hs.GetAttributeValue(wrapKeyHandle, tmpl)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get attribute values from object: %w", err)
|
||||
}
|
||||
var wrapKey []byte
|
||||
for _, wrapKeyAttr := range wrapKeyAttrs {
|
||||
switch wrapKeyAttr.Type {
|
||||
case pkcs11.CKA_VALUE:
|
||||
wrapKey = wrapKeyAttr.Value
|
||||
default:
|
||||
if wrapKeyAttr.Value == nil {
|
||||
fmt.Printf("unexpected attribute #%d: nil\n", wrapKeyAttr.Type)
|
||||
} else {
|
||||
fmt.Printf("unexpected attribute #%d: %s / %s\n", wrapKeyAttr.Type, hex.EncodeToString(wrapKeyAttr.Value), wrapKeyAttr.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap the private key on the HSM
|
||||
mechs = []*pkcs11.Mechanism{
|
||||
// pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so -M | grep wrap
|
||||
pkcs11.NewMechanism(pkcs11.CKM_AES_KEY_WRAP, nil),
|
||||
}
|
||||
wrappedKey, err := hs.WrapKey(mechs, wrapKeyHandle, keyHandle)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to wrap private key: %w", err)
|
||||
}
|
||||
|
||||
// Unwrap the key locally
|
||||
c, err := aes.NewCipher(wrapKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create new aes cipher: %w", err)
|
||||
}
|
||||
key, err := UnwrapKey(c, wrappedKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unwrap key: %w", err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func loadKey(filename string) (crypto.PrivateKey, crypto.PublicKey, error) {
|
||||
var priv crypto.PrivateKey
|
||||
var pub crypto.PublicKey
|
||||
|
||||
keyPEM, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return priv, pub, err
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(keyPEM)
|
||||
if block == nil {
|
||||
return priv, pub, fmt.Errorf("no data in key PEM file %s", filename)
|
||||
}
|
||||
|
||||
parseResult, _ := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if reflect.TypeOf(parseResult).String() == "*rsa.PrivateKey" {
|
||||
k := parseResult.(*rsa.PrivateKey)
|
||||
priv = k
|
||||
pub = k.PublicKey
|
||||
} else if reflect.TypeOf(parseResult).String() == "*ecdsa.PrivateKey" {
|
||||
k := parseResult.(*ecdsa.PrivateKey)
|
||||
priv = k
|
||||
pub = k.PublicKey
|
||||
} else {
|
||||
return priv, pub, fmt.Errorf("unknown private key type '%s'", reflect.TypeOf(parseResult).String())
|
||||
}
|
||||
|
||||
if priv == nil {
|
||||
fmt.Printf("WARNING: unknown private key type for %+v\n", parseResult)
|
||||
return priv, pub, errors.New("unknown private key type")
|
||||
}
|
||||
|
||||
return priv, pub, nil
|
||||
}
|
||||
|
||||
func loadCert(filename string) (*x509.Certificate, error) {
|
||||
certPEM, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(certPEM)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("no data in certificate PEM file %s", filename)
|
||||
}
|
||||
|
||||
parseResult, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse certificate: %s", err.Error())
|
||||
}
|
||||
|
||||
return parseResult, nil
|
||||
}
|
||||
|
||||
var curveToOIDDER = map[string][]byte{
|
||||
elliptic.P224().Params().Name: {6, 5, 43, 129, 4, 0, 33},
|
||||
elliptic.P256().Params().Name: {6, 8, 42, 134, 72, 206, 61, 3, 1, 7},
|
||||
elliptic.P384().Params().Name: {6, 5, 43, 129, 4, 0, 34},
|
||||
elliptic.P521().Params().Name: {6, 5, 43, 129, 4, 0, 35},
|
||||
}
|
||||
|
||||
func storePubKey(hs *HSMSession, pubKey crypto.PublicKey, keyID []byte, label string) error {
|
||||
tmpl := []*pkcs11.Attribute{
|
||||
pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PUBLIC_KEY),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_PRIVATE, false),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_VERIFY, true),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_ENCRYPT, true),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_WRAP, true),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_ID, keyID),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_LABEL, []byte(label)),
|
||||
}
|
||||
|
||||
if reflect.TypeOf(pubKey).String() == "rsa.PublicKey" {
|
||||
p := pubKey.(rsa.PublicKey)
|
||||
|
||||
tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_RSA))
|
||||
tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_MODULUS, p.N.Bytes()))
|
||||
tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_PUBLIC_EXPONENT, big.NewInt(int64(p.E)).Bytes()))
|
||||
} else if reflect.TypeOf(pubKey).String() == "ecdsa.PublicKey" {
|
||||
p := pubKey.(ecdsa.PublicKey)
|
||||
eh, err := p.ECDH()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert ecdsa pubkey to ecdh: %s", err.Error())
|
||||
}
|
||||
|
||||
tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_EC))
|
||||
|
||||
encodedCurve := curveToOIDDER[p.Curve.Params().Name]
|
||||
tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, encodedCurve))
|
||||
|
||||
rawValue := asn1.RawValue{
|
||||
Tag: asn1.TagOctetString,
|
||||
Bytes: eh.Bytes(),
|
||||
}
|
||||
marshalledPoint, err := asn1.Marshal(rawValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshall ecdsa point: %s", err.Error())
|
||||
}
|
||||
tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_EC_POINT, marshalledPoint))
|
||||
|
||||
} else {
|
||||
return fmt.Errorf("unknown public key type '%s'", reflect.TypeOf(pubKey).String())
|
||||
}
|
||||
|
||||
_, err := hs.CreateObject(tmpl)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to create public key on HSM: %s\n", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func storePrivKey(hs *HSMSession, privKey crypto.PrivateKey, keyID []byte, label string, extractable bool) error {
|
||||
tmpl := []*pkcs11.Attribute{
|
||||
pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PRIVATE_KEY),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_PRIVATE, true),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_SENSITIVE, true),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_EXTRACTABLE, extractable),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_SIGN, true),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_DECRYPT, true),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_DERIVE, true),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_WRAP_WITH_TRUSTED, false),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_UNWRAP, true),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_ID, keyID),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_LABEL, []byte(label)),
|
||||
}
|
||||
|
||||
if reflect.TypeOf(privKey).String() == "*rsa.PrivateKey" {
|
||||
k := privKey.(*rsa.PrivateKey)
|
||||
|
||||
tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_RSA))
|
||||
tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_MODULUS, k.PublicKey.N.Bytes()))
|
||||
tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_PUBLIC_EXPONENT, big.NewInt(int64(k.PublicKey.E)).Bytes()))
|
||||
tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_PRIVATE_EXPONENT, big.NewInt(int64(k.E)).Bytes()))
|
||||
tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_PRIME_1, new(big.Int).Set(k.Primes[0]).Bytes()))
|
||||
tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_PRIME_2, new(big.Int).Set(k.Primes[1]).Bytes()))
|
||||
tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_EXPONENT_1, new(big.Int).Set(k.Precomputed.Dp).Bytes()))
|
||||
tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_EXPONENT_2, new(big.Int).Set(k.Precomputed.Dq).Bytes()))
|
||||
tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_COEFFICIENT, new(big.Int).Set(k.Precomputed.Qinv).Bytes()))
|
||||
|
||||
} else if reflect.TypeOf(privKey).String() == "*ecdsa.PrivateKey" {
|
||||
k := privKey.(*ecdsa.PrivateKey)
|
||||
|
||||
tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, pkcs11.CKK_EC))
|
||||
encodedCurve := curveToOIDDER[k.Params().Name]
|
||||
tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, encodedCurve))
|
||||
tmpl = append(tmpl, pkcs11.NewAttribute(pkcs11.CKA_VALUE, new(big.Int).Set(k.D).Bytes()))
|
||||
|
||||
} else {
|
||||
return fmt.Errorf("unknown private key type '%s'", reflect.TypeOf(privKey).String())
|
||||
}
|
||||
|
||||
_, err := hs.CreateObject(tmpl)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to create private key on HSM: %s\n", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func storeCertificate(hs *HSMSession, certificate *x509.Certificate, keyID []byte, label string) error {
|
||||
serial, err := asn1.Marshal(certificate.SerialNumber)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmpl := []*pkcs11.Attribute{
|
||||
pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_CERTIFICATE),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_CERTIFICATE_TYPE, pkcs11.CKC_X_509),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_PRIVATE, false),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_SUBJECT, certificate.RawSubject),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_ISSUER, certificate.RawIssuer),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_SERIAL_NUMBER, serial),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_ID, keyID),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_LABEL, []byte(label)),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_VALUE, certificate.Raw),
|
||||
}
|
||||
|
||||
_, err = hs.CreateObject(tmpl)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to create certificate on HSM: %s\n", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *HSMConfig) ImportKeyCert(keyFile, certFile string) (crypto.PublicKey, error) {
|
||||
hs, err := cfg.getSession()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get session: %s", err)
|
||||
}
|
||||
defer hs.Close()
|
||||
|
||||
privKey, pubKey, err := loadKey(keyFile)
|
||||
if err != nil {
|
||||
return pubKey, err
|
||||
}
|
||||
|
||||
keyID := make([]byte, 4)
|
||||
_, err = rand.Read(keyID)
|
||||
if err != nil {
|
||||
return pubKey, err
|
||||
}
|
||||
|
||||
err = storePubKey(hs, pubKey, keyID, cfg.Label)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to store public key on HSM: %s\n", err.Error())
|
||||
return pubKey, err
|
||||
}
|
||||
|
||||
extractable := true // For now, with SoftHSM, this is fine. In future we need to ask for informed consent!
|
||||
|
||||
err = storePrivKey(hs, privKey, keyID, cfg.Label, extractable)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to store private key on HSM: %s\n", err.Error())
|
||||
return pubKey, err
|
||||
}
|
||||
|
||||
if strings.Index(filepath.Base(keyFile), "root-") != 0 {
|
||||
jsonFile := path.Join(CERT_FILES_PATH, filepath.Base(keyFile))
|
||||
jsonFile = strings.Replace(jsonFile, "-key.pem", ".pkcs11.json", -1)
|
||||
contents := fmt.Sprintf(`{"module": %q, "tokenLabel": %q, "pin": %q}`, cfg.Module, cfg.Label, cfg.UserPIN)
|
||||
err = os.WriteFile(jsonFile, []byte(contents), 0644)
|
||||
if err != nil {
|
||||
return pubKey, fmt.Errorf("failed to write '%s' file: %s", jsonFile, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if certFile != "" {
|
||||
cert, err := loadCert(certFile)
|
||||
if err != nil {
|
||||
return pubKey, err
|
||||
}
|
||||
|
||||
err = storeCertificate(hs, cert, keyID, cfg.Label)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to store certificate on HSM: %s\n", err.Error())
|
||||
return pubKey, err
|
||||
}
|
||||
}
|
||||
|
||||
return pubKey, nil
|
||||
}
|
||||
|
||||
func (hs *HSMSession) Close() {
|
||||
hs.Context.CloseSession(hs.Handle)
|
||||
hs.Context.Logout(hs.Handle)
|
||||
}
|
||||
|
||||
func (hs *HSMSession) CreateObject(tmpl []*pkcs11.Attribute) (pkcs11.ObjectHandle, error) {
|
||||
return hs.Context.CreateObject(hs.Handle, tmpl)
|
||||
}
|
||||
|
||||
func (hs *HSMSession) DestroyObject(object pkcs11.ObjectHandle) error {
|
||||
return hs.Context.DestroyObject(hs.Handle, object)
|
||||
}
|
||||
|
||||
func (hs *HSMSession) DestroyAllObjects(label string) error {
|
||||
tmpl := []*pkcs11.Attribute{
|
||||
pkcs11.NewAttribute(pkcs11.CKA_LABEL, []byte(label)),
|
||||
}
|
||||
|
||||
keys, err := hs.FindObjects(tmpl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find objects with label='%s': %w", label, err)
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
err = hs.DestroyObject(key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to destroy object '%+v': %w", key, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (hs *HSMSession) FindObject(tmpl []*pkcs11.Attribute) (pkcs11.ObjectHandle, error) {
|
||||
err := hs.Context.FindObjectsInit(hs.Handle, tmpl)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
handles, _, err := hs.Context.FindObjects(hs.Handle, 2)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
err = hs.Context.FindObjectsFinal(hs.Handle)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(handles) == 0 {
|
||||
return 0, errors.New("no objects found matching provided template")
|
||||
}
|
||||
if len(handles) > 1 {
|
||||
return 0, fmt.Errorf("too many objects (%d) that match the provided template", len(handles))
|
||||
}
|
||||
return handles[0], nil
|
||||
}
|
||||
|
||||
func (hs *HSMSession) FindObjects(tmpl []*pkcs11.Attribute) ([]pkcs11.ObjectHandle, error) {
|
||||
result := []pkcs11.ObjectHandle{}
|
||||
|
||||
err := hs.Context.FindObjectsInit(hs.Handle, tmpl)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
for {
|
||||
handles, _, err := hs.Context.FindObjects(hs.Handle, 10)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
if len(handles) == 0 {
|
||||
break
|
||||
}
|
||||
result = append(result, handles...)
|
||||
}
|
||||
|
||||
err = hs.Context.FindObjectsFinal(hs.Handle)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (hs *HSMSession) GenerateKey(mechs []*pkcs11.Mechanism, tmpl []*pkcs11.Attribute) (pkcs11.ObjectHandle, error) {
|
||||
return hs.Context.GenerateKey(hs.Handle, mechs, tmpl)
|
||||
}
|
||||
|
||||
func (hs *HSMSession) GetAttributeValue(handle pkcs11.ObjectHandle, tmpl []*pkcs11.Attribute) ([]*pkcs11.Attribute, error) {
|
||||
return hs.Context.GetAttributeValue(hs.Handle, handle, tmpl)
|
||||
}
|
||||
|
||||
func (hs *HSMSession) WrapKey(mechs []*pkcs11.Mechanism, wkh pkcs11.ObjectHandle, kh pkcs11.ObjectHandle) ([]byte, error) {
|
||||
return hs.Context.WrapKey(hs.Handle, mechs, wkh, kh)
|
||||
}
|
||||
378
gui/main.go
378
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.</p>"))
|
||||
} else if stage == "root-ca" {
|
||||
} else if stage == "root-01" {
|
||||
return template.HTML(fmt.Sprint("<p>This is the top level certificate that will sign the issuer\n",
|
||||
"certificate(s). You can either generate a fresh Root CA (Certificate Authority) or import an\n",
|
||||
"existing one, e.g. a backup from another LabCA instance.</p>\n",
|
||||
"<p>If you want to <b>generate</b> a new certificate, pick a key type and strength (the higher the number the\n",
|
||||
"more secure, ECDSA is more modern than RSA), provide at least a country and organization name,\n",
|
||||
"more secure, ECDSA is more modern than RSA), provide a country and organization name,\n",
|
||||
"and the common name. It is recommended that the common name contains the word 'Root' as well\n",
|
||||
"as your organization name so you can recognize it, and that's why that is automatically filled\n",
|
||||
"once you leave the organization field.</p>\n",
|
||||
@@ -2378,7 +2398,7 @@ func _helptext(stage string) template.HTML {
|
||||
"offline for security reasons according to best practices. If you do include it here, we will be able\n",
|
||||
"to generate an issuing certificate automatically in the next step. If you don't include it, we will\n",
|
||||
"ask for it when needed.</p>"))
|
||||
} else if stage == "ca-int" {
|
||||
} else if stage == "issuer-01" {
|
||||
return template.HTML(fmt.Sprint("<p>This is what end users will see as the issuing certificate. Again,\n",
|
||||
"you can either generate a fresh certificate or import an existing one, as long as it is signed by\n",
|
||||
"the Root CA from the previous step.</p>\n",
|
||||
@@ -2458,7 +2478,7 @@ func _setupAdminUser(w http.ResponseWriter, r *http.Request) bool {
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
fmt.Fprintf(conn, "backup-restore\n"+header.Filename+"\n")
|
||||
fmt.Fprint(conn, "backup-restore\n"+header.Filename+"\n")
|
||||
reader := bufio.NewReader(conn)
|
||||
message, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
@@ -2734,18 +2754,18 @@ func setupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// 3. Setup root CA certificate
|
||||
if !_certCreate(w, r, "root-ca", true) {
|
||||
if !_certCreate(w, r, "root-01", true) {
|
||||
// Cleanup the cert (if it even exists) so we will retry on the next run
|
||||
if _, err := os.Stat("data/root-ca.pem"); !errors.Is(err, fs.ErrNotExist) {
|
||||
exeCmd("mv data/root-ca.pem data/root-ca.pem_TMP")
|
||||
if _, err := os.Stat(CERT_FILES_PATH + "root-01-cert.pem"); !errors.Is(err, fs.ErrNotExist) {
|
||||
exeCmd("mv " + CERT_FILES_PATH + "root-01-cert.pem " + CERT_FILES_PATH + "root-01-cert.pem_TMP")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 4. Setup issuer certificate
|
||||
if !_certCreate(w, r, "ca-int", false) {
|
||||
if !_certCreate(w, r, "issuer-01", false) {
|
||||
// Cleanup the cert (if it even exists) so we will retry on the next run
|
||||
os.Remove("data/issuer/ca-int.pem")
|
||||
os.Remove(CERT_FILES_PATH + "issuer-01-cert.pem")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3326,9 +3346,10 @@ func init() {
|
||||
port := flag.Int("port", 0, "Port to listen on (default 3000 when using init)")
|
||||
versionFlag := flag.Bool("version", false, "Show version number and exit")
|
||||
decrypt := flag.String("d", "", "Decrypt a value")
|
||||
renewcrl := flag.Int("renewcrl", 0, "Check root CRL files and renew if nextUpdate is in less than this number of days")
|
||||
flag.Parse()
|
||||
|
||||
if *versionFlag {
|
||||
if *versionFlag && standaloneVersion != "" {
|
||||
fmt.Println(standaloneVersion)
|
||||
os.Exit(0)
|
||||
}
|
||||
@@ -3355,6 +3376,11 @@ func init() {
|
||||
panic(fmt.Errorf("fatal error config file: '%s'", err))
|
||||
}
|
||||
|
||||
if *versionFlag && standaloneVersion == "" {
|
||||
fmt.Println(viper.GetString("version"))
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *decrypt != "" {
|
||||
plain, err := _decrypt(*decrypt)
|
||||
if err == nil {
|
||||
@@ -3365,6 +3391,57 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
if *renewcrl != 0 {
|
||||
crlFiles, err := filepath.Glob(filepath.Join(CERT_FILES_PATH, "root-*-crl.pem"))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
for _, crlFile := range crlFiles {
|
||||
read, err := os.ReadFile(crlFile)
|
||||
if err != nil {
|
||||
fmt.Printf("could not read '%s': %s\n", crlFile, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
block, _ := pem.Decode(read)
|
||||
if block == nil || block.Type != "X509 CRL" {
|
||||
fmt.Println(block)
|
||||
fmt.Println("failed to decode PEM block containing revocation list")
|
||||
os.Exit(1)
|
||||
}
|
||||
crl, err := x509.ParseRevocationList(block.Bytes)
|
||||
if err != nil {
|
||||
fmt.Printf("could not parse revocation list: %s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if crl.NextUpdate.Sub(now) < time.Hour*24*time.Duration(*renewcrl) {
|
||||
fmt.Printf("renewing crl file '%s'...\n", crlFile)
|
||||
re := regexp.MustCompile(`-(\d{2})-`)
|
||||
match := re.FindStringSubmatch(crlFile)
|
||||
if len(match) > 1 {
|
||||
seqnr := match[1]
|
||||
ci := &CertificateInfo{}
|
||||
ci.Initialize()
|
||||
err = ci.CeremonyRootCRL(seqnr)
|
||||
if err == nil {
|
||||
fmt.Printf("updated %s\n", crlFile)
|
||||
} else {
|
||||
fmt.Printf("could not update crl file '%s': %s\n", crlFile, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("could not extract sequence number from filename '%s'\n", crlFile)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
var err error
|
||||
if *init || viper.GetBool("standalone") {
|
||||
tmpls, err = templates.New().ParseEmbed(embeddedTemplates, "templates/")
|
||||
@@ -3452,6 +3529,10 @@ func init() {
|
||||
|
||||
updateAvailable = false
|
||||
|
||||
if !viper.GetBool("standalone") {
|
||||
CheckUpgrades()
|
||||
}
|
||||
|
||||
/*
|
||||
// TODO: Still needs to be done for this!
|
||||
// Store boulder chains if we don't have them already
|
||||
@@ -3474,6 +3555,47 @@ func init() {
|
||||
*/
|
||||
}
|
||||
|
||||
type BackupResult struct {
|
||||
Existed bool
|
||||
NewName string
|
||||
OrigName string
|
||||
}
|
||||
|
||||
func (br BackupResult) Remove() {
|
||||
os.Remove(br.NewName)
|
||||
}
|
||||
|
||||
func (br BackupResult) Restore() {
|
||||
if br.Existed {
|
||||
os.Rename(br.NewName, br.OrigName)
|
||||
}
|
||||
}
|
||||
|
||||
func renameBackup(filename string) BackupResult {
|
||||
result := BackupResult{
|
||||
Existed: false,
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filename); !errors.Is(err, os.ErrNotExist) {
|
||||
os.Remove(filename + "_BAK") // May not exist...
|
||||
result.Existed = true
|
||||
}
|
||||
|
||||
if !result.Existed {
|
||||
return result
|
||||
}
|
||||
|
||||
err := os.Rename(filename, filename+"_BAK")
|
||||
if err != nil {
|
||||
fmt.Printf("warning: failed to backup previous file '%s': %s\n", filename, err.Error())
|
||||
} else {
|
||||
result.OrigName = filename
|
||||
result.NewName = filename + "_BAK"
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func main() {
|
||||
tmpls.Parse()
|
||||
|
||||
|
||||
@@ -60,8 +60,7 @@
|
||||
<thead><tr>
|
||||
<th>CA Type</th>
|
||||
<th>Distinguished Name</th>
|
||||
<th>Windows format</th>
|
||||
<th>Linux format</th>
|
||||
<th>Certificate File</th>
|
||||
<th>Validity Period</th>
|
||||
<th>CRL</th>
|
||||
<th>CRL Validity</th>
|
||||
@@ -70,17 +69,15 @@
|
||||
<tr>
|
||||
<td>Root CA</td>
|
||||
<td><!-- BEGIN PKI_ROOT_DN -->PKI_ROOT_DN<!-- END PKI_ROOT_DN --></td>
|
||||
<td><a class="public" href="root-ca.der">root-ca.der</a></td>
|
||||
<td><a class="public" href="root-ca.pem">root-ca.pem</a></td>
|
||||
<td><!-- BEGIN PKI_ROOT_LINK --><a class="public" href="root-01-cert.pem">root-01-cert.pem</a><!-- END PKI_ROOT_LINK --></td>
|
||||
<td><!-- BEGIN PKI_ROOT_VALIDITY -->PKI_ROOT_VALIDITY<!-- END PKI_ROOT_VALIDITY --></td>
|
||||
<td><a class="public" href="../crl/root-ca.crl">root-ca.crl</a></td>
|
||||
<td><!-- BEGIN PKI_ROOT_CRL_LINK --><!-- END PKI_ROOT_CRL_LINK --></td>
|
||||
<td><!-- BEGIN PKI_ROOT_CRL_VALIDITY --><!-- END PKI_ROOT_CRL_VALIDITY --></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Issuing CA</td>
|
||||
<td><!-- BEGIN PKI_INT_DN -->PKI_INT_DN<!-- END PKI_INT_DN --></td>
|
||||
<td><a class="public" href="ca-int.der">ca-int.der</a></td>
|
||||
<td><a class="public" href="ca-int.pem">ca-int.pem</a></td>
|
||||
<td><!-- BEGIN PKI_INT_LINK --><a class="public" href="issuer-01-cert.pem">issuer-01-cert.pem</a><!-- END PKI_INT_LINK --></td>
|
||||
<td><!-- BEGIN PKI_INT_VALIDITY -->PKI_INT_VALIDITY<!-- END PKI_INT_VALIDITY --></td>
|
||||
<td><!-- BEGIN PKI_INT_CRL_LINK --><!-- END PKI_INT_CRL_LINK --></td>
|
||||
<td><!-- BEGIN PKI_INT_CRL_VALIDITY --><!-- END PKI_INT_CRL_VALIDITY --></td>
|
||||
@@ -90,9 +87,7 @@
|
||||
|
||||
<p>
|
||||
To trust the certificates provided by <!-- BEGIN WEBTITLE -->LabCA<!-- END WEBTITLE -->, all your client devices
|
||||
should install the root certificate in their <b>Trusted Root Certification Authorities</b> store. You may choose
|
||||
to download the format best suited for your Operating System: DER format for Windows machines or PEM format for
|
||||
Linux/unix machines and Android phones.
|
||||
should install the root certificate in their <b>Trusted Root Certification Authorities</b> store.
|
||||
</p>
|
||||
<p>
|
||||
The CRL (Certificate Revocation List) is a type of blocklist that includes certificates that should no longer be
|
||||
|
||||
26
gui/templates/cert-ceremonies/issuer-cert.yaml
Normal file
26
gui/templates/cert-ceremonies/issuer-cert.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
ceremony-type: intermediate
|
||||
pkcs11:
|
||||
module: {{ .Module }}
|
||||
pin: {{ .UserPIN }}
|
||||
signing-key-slot: {{ .RootSlotID }}
|
||||
signing-key-label: {{ .RootLabel }}
|
||||
inputs:
|
||||
public-key-path: {{ .Path }}issuer-{{ .SeqNr }}-pubkey.pem
|
||||
issuer-certificate-path: {{ .Path }}root-{{ .RootSeqNr }}-cert.pem
|
||||
outputs:
|
||||
certificate-path: {{ .Path }}issuer-{{ .SeqNr }}-cert.pem
|
||||
certificate-profile:
|
||||
signature-algorithm: {{ .SignAlgorithm }}
|
||||
common-name: {{ .CommonName }}
|
||||
organization: {{ .OrgName }}
|
||||
country: {{ .Country }}
|
||||
not-before: {{ .NotBefore }}
|
||||
not-after: {{ .NotAfter }}
|
||||
crl-url: {{ .CrlUrl }}
|
||||
issuer-url: {{ .IssuerUrl }}
|
||||
policies:
|
||||
- oid: 2.23.140.1.2.1
|
||||
key-usages:
|
||||
- Digital Signature
|
||||
- Cert Sign
|
||||
- CRL Sign
|
||||
19
gui/templates/cert-ceremonies/issuer-key.yaml
Normal file
19
gui/templates/cert-ceremonies/issuer-key.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
ceremony-type: key
|
||||
pkcs11:
|
||||
module: {{ .Module }}
|
||||
pin: {{ .UserPIN }}
|
||||
store-key-in-slot: {{ .SlotID }}
|
||||
store-key-with-label: {{ .Label }}
|
||||
key:
|
||||
type: {{ .KeyType }}
|
||||
{{ if eq .KeyType "rsa" }}
|
||||
rsa-mod-length: {{ .KeyParam }}
|
||||
{{ else }}
|
||||
ecdsa-curve: {{ .KeyParam }}
|
||||
{{ end }}
|
||||
{{ if eq .Extractable "true" }}
|
||||
extractable: true
|
||||
{{ end }}
|
||||
outputs:
|
||||
public-key-path: {{ .Path }}issuer-{{ .SeqNr }}-pubkey.pem
|
||||
pkcs11-config-path: {{ .Path }}issuer-{{ .SeqNr }}.pkcs11.json
|
||||
14
gui/templates/cert-ceremonies/root-crl.yaml
Normal file
14
gui/templates/cert-ceremonies/root-crl.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
ceremony-type: crl
|
||||
pkcs11:
|
||||
module: {{ .Module }}
|
||||
pin: {{ .UserPIN }}
|
||||
signing-key-slot: {{ .RootSlotID }}
|
||||
signing-key-label: {{ .RootLabel }}
|
||||
inputs:
|
||||
issuer-certificate-path: {{ .Path }}root-{{ .RootSeqNr }}-cert.pem
|
||||
outputs:
|
||||
crl-path: {{ .Path }}root-{{ .RootSeqNr }}-crl.pem
|
||||
crl-profile:
|
||||
this-update: {{ .ThisUpdate }}
|
||||
next-update: {{ .NextUpdate }}
|
||||
number: {{ .CrlNumber }}
|
||||
34
gui/templates/cert-ceremonies/root.yaml
Normal file
34
gui/templates/cert-ceremonies/root.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
ceremony-type: root
|
||||
pkcs11:
|
||||
module: {{ .Module }}
|
||||
pin: {{ .UserPIN }}
|
||||
store-key-in-slot: {{ .SlotID }}
|
||||
store-key-with-label: {{ .Label }}
|
||||
key:
|
||||
type: {{ .KeyType }}
|
||||
{{ if eq .KeyType "rsa" }}
|
||||
rsa-mod-length: {{ .KeyParam }}
|
||||
{{ else }}
|
||||
ecdsa-curve: {{ .KeyParam }}
|
||||
{{ end }}
|
||||
{{ if eq .Extractable "true" }}
|
||||
extractable: true
|
||||
{{ end }}
|
||||
outputs:
|
||||
public-key-path: {{ .Path }}root-{{ .SeqNr }}-pubkey.pem
|
||||
certificate-path: {{ .Path }}root-{{ .SeqNr }}-cert.pem
|
||||
certificate-profile:
|
||||
signature-algorithm: {{ .SignAlgorithm }}
|
||||
common-name: {{ .CommonName }}
|
||||
organization: {{ .OrgName }}
|
||||
country: {{ .Country }}
|
||||
not-before: {{ .NotBefore }}
|
||||
not-after: {{ .NotAfter }}
|
||||
key-usages:
|
||||
- Cert Sign
|
||||
- CRL Sign
|
||||
skip-lints:
|
||||
- n_ca_digital_signature_not_set
|
||||
{{ if eq .Renewal "true" }}
|
||||
renewal: true
|
||||
{{ end }}
|
||||
@@ -59,13 +59,6 @@
|
||||
<span class="error">{{ . }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="ou">Org. Unit (optional):</label>
|
||||
<input class="form-control" type="text" id="ou" name="ou" value="{{ .OrgUnit }}" {{ if and (ne .Organization "") (.IsRootGenerated) }}readonly{{ end }}>
|
||||
{{ with .Errors.OrgUnit }}
|
||||
<span class="error">{{ . }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cn">Common Name:</label>
|
||||
<input class="form-control" type="text" id="cn" name="cn" value="{{ .CommonName }}" required>
|
||||
@@ -136,12 +129,12 @@
|
||||
<textarea class="form-control" id="certificate" name="certificate" rows="10" cols="80" required>{{ .Certificate }}</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="key">Key (in PEM format{{ if .IsRoot }}; optional{{ end }}):
|
||||
<label for="key">Key (in PEM format):
|
||||
{{ with .Errors.Key }}
|
||||
<span class="error"><br/>{{ . }}</span>
|
||||
{{ end }}
|
||||
</label>
|
||||
<textarea class="form-control" id="key" name="key" rows="10" cols="80">{{ .Key }}</textarea>
|
||||
<textarea class="form-control" id="key" name="key" rows="10" cols="80" required>{{ .Key }}</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="passphrase">Passphrase (optional):
|
||||
@@ -353,4 +346,4 @@
|
||||
updateEndDate();
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -165,6 +165,7 @@
|
||||
<tr>
|
||||
<th>Subject</th>
|
||||
<th></th>
|
||||
<th>Type</th>
|
||||
<th>Active</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
@@ -177,10 +178,11 @@
|
||||
<td class="vmiddle">
|
||||
<a href="/certs/{{ $item.RootCert.BaseName }}.pem" title="Download this certificate">download</a>
|
||||
</td>
|
||||
<td class="vmiddle">{{ $item.RootCert.KeyType }}</td>
|
||||
<td class="vmiddle"></td>
|
||||
<td class="vmiddle">
|
||||
<button class="btn btn-outline btn-reg btn-success export-cert-key" type="button" title="Export this certificate and key" data-name="{{ $item.RootCert.BaseName }}" data-subject="{{ $item.RootCert.Subject }}">Export</button>
|
||||
<button class="btn btn-outline btn-reg btn-warning renew-cert" type="button" title="Generate new version with extended lifetime" data-name="{{ $item.RootCert.BaseName }}" data-rootname="{{ $item.RootCert.BaseName }}" data-subject="{{ $item.RootCert.Subject }}" data-rootsubject="{{ $item.RootCert.Subject }}" data-notbefore="{{ $item.RootCert.NotAfter }}" data-isroot="true">Renew</button>
|
||||
<button class="btn btn-outline btn-reg btn-warning renew-cert" type="button" title="Generate new version with extended lifetime" data-name="{{ $item.RootCert.BaseName }}" data-rootname="{{ $item.RootCert.BaseName }}" data-subject="{{ $item.RootCert.Subject }}" data-keytype="{{ $item.RootCert.KeyType }}" data-rootsubject="{{ $item.RootCert.Subject }}" data-notbefore="{{ $item.RootCert.NotAfter }}" data-isroot="true">Renew</button>
|
||||
<!--
|
||||
<button class="btn btn-outline btn-new-issuer btn-warning new-issuer-cert" type="button" title="Generate new Issuer Certificate under this Root" data-root="{{ $item.RootCert.BaseName }}" data-rootsubject="{{ $item.RootCert.Subject }}">New Issuer</button>
|
||||
<button class="btn btn-outline btn-reg btn-danger delete-cert hidden" type="button" title="Delete this certificate" data-name="{{ $item.RootCert.BaseName }}">Delete</button>
|
||||
@@ -196,10 +198,11 @@
|
||||
<td class="vmiddle">
|
||||
<a href="/certs/{{ $subitem.BaseName }}.pem" title="Download this certificate">download</a>
|
||||
</td>
|
||||
<td class="vmiddle">{{ $subitem.KeyType }}</td>
|
||||
<td class="vmiddle center"><input type="radio" id="active-{{ $subitem.BaseName }}" name="issue-active" value="{{ $subitem.BaseName }}" title="Use this certificate for issueing leave certificates" {{ if $subitem.ActiveIssuer }}data-orig="true" checked{{ end }}/></td>
|
||||
<td class="vmiddle">
|
||||
<button class="btn btn-outline btn-reg btn-success export-cert-key" type="button" title="Export this certificate and key" data-name="{{ $subitem.BaseName }}" data-subject="{{ $subitem.Subject }}">Export</button>
|
||||
<button class="btn btn-outline btn-reg btn-warning renew-cert" type="button" title="Generate new version with extended lifetime" data-name="{{ $subitem.BaseName }}" data-rootname="{{ $item.RootCert.BaseName }}" data-subject="{{ $subitem.Subject }}" data-rootsubject="{{ $item.RootCert.Subject }}" data-notbefore="{{ $subitem.NotAfter }}" data-notafter="{{ $item.RootCert.NotAfter }}" data-isroot="false">Renew</button>
|
||||
<button class="btn btn-outline btn-reg btn-warning renew-cert" type="button" title="Generate new version with extended lifetime" data-name="{{ $subitem.BaseName }}" data-rootname="{{ $item.RootCert.BaseName }}" data-subject="{{ $subitem.Subject }}" data-keytype="{{ $subitem.KeyType }}" data-rootsubject="{{ $item.RootCert.Subject }}" data-notbefore="{{ $subitem.NotAfter }}" data-notafter="{{ $item.RootCert.NotAfter }}" data-isroot="false">Renew</button>
|
||||
<!--
|
||||
<button class="btn btn-outline btn-reg btn-danger delete-cert" type="button" title="Delete this certificate" data-name="{{ $subitem.BaseName }}">Delete</button>
|
||||
-->
|
||||
@@ -226,13 +229,12 @@
|
||||
<td class="vmiddle">Root CRL</td>
|
||||
<td class="vmiddle">
|
||||
<button class="btn btn-outline btn-success mt5" type="button" id="gen-root-crl" title="Generate root CRL now (requires Root CA key)">Generate</button>
|
||||
<button class="btn btn-outline btn-success mt5" type="button" id="upload-root-crl" title="Upload a root CRL">Upload</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="vmiddle">Issuer CRL</td>
|
||||
<td class="vmiddle">
|
||||
<button class="btn btn-outline btn-success mt5" type="button" id="gen-issuer-crl" title="Generate root CRL now (requires Root CA key)">Generate</button>
|
||||
<button class="btn btn-outline btn-success mt5" type="button" id="gen-issuer-crl" title="Generate issuer CRL now">Generate</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -549,7 +551,8 @@
|
||||
<input type="hidden" id="modal-renew-cert" name="modal-renew-cert">
|
||||
<input type="hidden" id="renew-rootcert" name="renew-rootcert">
|
||||
<input type="hidden" id="renew-rootsubject" name="renew-rootsubject">
|
||||
Subject: <span id="renew-subject"></span><br><br>
|
||||
Subject: <span id="renew-subject"></span><br>
|
||||
Key Type: <span id="renew-keytype"></span><br><br>
|
||||
Current end date: <span id="renew-current-enddate"></span><br>
|
||||
<span id="renew-show-root-enddate" class="hidden">Root end date: <span id="renew-root-enddate"></span><br></span>
|
||||
<br>
|
||||
@@ -844,6 +847,9 @@
|
||||
}
|
||||
|
||||
$('#modal-export').modal('hide');
|
||||
|
||||
} else if (event.currentTarget.status == 400 || event.currentTarget.statusText == "Bad Request") {
|
||||
$("#modal-export-error").removeClass("hidden").show().text("Key is not extractable from the HSM!");
|
||||
} else {
|
||||
$("#modal-export-error").removeClass("hidden").show().text("Backend returned: " + event.currentTarget.statusText);
|
||||
}
|
||||
@@ -1363,6 +1369,7 @@
|
||||
$('#modal-renew-cert').val($(evt.target).data('name'));
|
||||
$('#renew-rootcert').val($(evt.target).data('rootname'));
|
||||
$('#renew-subject').text($(evt.target).data('subject'));
|
||||
$('#renew-keytype').text($(evt.target).data('keytype'));
|
||||
$('#renew-rootsubject').val($(evt.target).data('rootsubject'));
|
||||
d = new Date($(evt.target).data('notbefore')).toUTCString()
|
||||
$('#renew-current-enddate').text(d);
|
||||
|
||||
191
gui/upgrades.go
Normal file
191
gui/upgrades.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func CheckUpgrades() {
|
||||
v := viper.GetString("version")
|
||||
if standaloneVersion == "" {
|
||||
gitVersion := controlCommand("git-version")
|
||||
if gitVersion != "" {
|
||||
viper.Set("version", gitVersion)
|
||||
viper.WriteConfig()
|
||||
}
|
||||
} else if v != standaloneVersion {
|
||||
viper.Set("version", standaloneVersion)
|
||||
viper.WriteConfig()
|
||||
}
|
||||
|
||||
changed := CheckUpgrade_01_CeremonyHSM()
|
||||
|
||||
if changed {
|
||||
time.Sleep(2 * time.Second)
|
||||
log.Println("Applying updated configuration...")
|
||||
controlCommand("apply")
|
||||
time.Sleep(2 * time.Second)
|
||||
log.Println("Updating CRL links if needed...")
|
||||
controlCommand("check-crl")
|
||||
time.Sleep(2 * time.Second)
|
||||
log.Println("Restarting boulder containers...")
|
||||
controlCommand("boulder-restart")
|
||||
}
|
||||
}
|
||||
|
||||
func readFileAsString(filename string) string {
|
||||
read, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
log.Printf("**** Could not read '%s': %s\n", filename, err.Error())
|
||||
log.Println("**** ABORT MIGRATION ****")
|
||||
time.Sleep(1 * time.Minute)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return string(read)
|
||||
}
|
||||
|
||||
func controlCommand(command string) string {
|
||||
conn, err := net.Dial("tcp", "control:3030")
|
||||
if err != nil {
|
||||
log.Println("**** Failed to connect to control container!")
|
||||
time.Sleep(1 * time.Minute)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
fmt.Fprint(conn, command+"\n")
|
||||
|
||||
reader := bufio.NewReader(conn)
|
||||
message, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
log.Printf("**** Failed to read response from control container: %s\n", err.Error())
|
||||
time.Sleep(1 * time.Minute)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(message) >= 4 {
|
||||
tail := message[len(message)-4:]
|
||||
if strings.Compare(string(tail), "\nok\n") == 0 {
|
||||
msg := message[0 : len(message)-4]
|
||||
log.Printf("**** Message from control server: '%s'", msg)
|
||||
}
|
||||
}
|
||||
|
||||
return string(message)
|
||||
}
|
||||
|
||||
func copyFile(src, dst string) error {
|
||||
sourceFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sourceFile.Close()
|
||||
|
||||
destinationFile, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer destinationFile.Close()
|
||||
|
||||
_, err = io.Copy(destinationFile, sourceFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = destinationFile.Sync()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if we should upgrade to using the Ceremony tool and store keys on SoftHSM (January 2025).
|
||||
func CheckUpgrade_01_CeremonyHSM() bool {
|
||||
baseDir := "/opt/labca/data/"
|
||||
prevRootCert := baseDir + "root-ca.pem"
|
||||
if _, err := os.Stat(prevRootCert); errors.Is(err, fs.ErrNotExist) {
|
||||
baseDir = "/go/src/labca/data/"
|
||||
prevRootCert = baseDir + "root-ca.pem"
|
||||
if _, err := os.Stat(prevRootCert); errors.Is(err, fs.ErrNotExist) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("**** BEGIN MIGRATION: upgrade01 ****")
|
||||
|
||||
rootCertFile := fmt.Sprintf("%sroot-01-cert.pem", CERT_FILES_PATH)
|
||||
if _, err := os.Stat(rootCertFile); !errors.Is(err, fs.ErrNotExist) {
|
||||
log.Printf("**** File %s already exists!\n", rootCertFile)
|
||||
log.Println("**** ABORT MIGRATION ****")
|
||||
time.Sleep(1 * time.Minute)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
prevRootKey := baseDir + "root-ca.key"
|
||||
if _, err := os.Stat(prevRootKey); errors.Is(err, fs.ErrNotExist) {
|
||||
log.Println("**** Root key file not present on the system: cannot upgrade automatically!")
|
||||
log.Println("**** Please do a fresh install of LabCA and import / upload the root certificate and key.")
|
||||
log.Println("**** ABORT MIGRATION ****")
|
||||
time.Sleep(1 * time.Minute)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Migrate root certificate and key
|
||||
ci := &CertificateInfo{IsRoot: true}
|
||||
ci.Initialize()
|
||||
ci.IsRoot = true
|
||||
ci.CreateType = "upload"
|
||||
ci.Certificate = readFileAsString(prevRootCert)
|
||||
ci.Key = readFileAsString(prevRootKey)
|
||||
prevRootCRL := baseDir + "root-ca.crl"
|
||||
if _, err := os.Stat(prevRootCRL); !errors.Is(err, fs.ErrNotExist) {
|
||||
ci.CRL = readFileAsString(prevRootCRL)
|
||||
copyFile(prevRootCRL, strings.Replace(rootCertFile, "-cert.", "-crl.", -1))
|
||||
}
|
||||
|
||||
if err := ci.Create("root-01", false); err != nil {
|
||||
log.Printf("**** Could not convert previous root certificate and key: %s\n", err.Error())
|
||||
log.Println("**** ABORT MIGRATION ****")
|
||||
time.Sleep(1 * time.Minute)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Migrate issuer certificate and key
|
||||
ci = &CertificateInfo{IsRoot: false}
|
||||
ci.Initialize()
|
||||
ci.IsRoot = false
|
||||
ci.CreateType = "upload"
|
||||
prevIssuerCert := baseDir + "issuer/ca-int.pem"
|
||||
ci.Certificate = readFileAsString(prevIssuerCert)
|
||||
prevIssuerKey := baseDir + "issuer/ca-int.key"
|
||||
ci.Key = readFileAsString(prevIssuerKey)
|
||||
ci.CRL = ""
|
||||
|
||||
if err := ci.Create("issuer-01", false); err != nil {
|
||||
log.Printf("**** Could not convert previous issuer certificate and key: %s\n", err.Error())
|
||||
log.Println("**** ABORT MIGRATION ****")
|
||||
time.Sleep(1 * time.Minute)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
os.Rename(prevRootCert, prevRootCert+"_backup")
|
||||
os.Rename(prevRootKey, prevRootKey+"_backup")
|
||||
os.Rename(prevRootCRL, prevRootCRL+"_backup")
|
||||
os.Rename(prevIssuerCert, prevIssuerCert+"_backup")
|
||||
os.Rename(prevIssuerKey, prevIssuerKey+"_backup")
|
||||
|
||||
log.Println("**** END MIGRATION ****")
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user