From 9f77d1a308d83bcff01f2f8168fc701886a9fbee Mon Sep 17 00:00:00 2001
From: Arjan H
Date: Thu, 8 Jun 2023 20:24:41 +0200
Subject: [PATCH] Add ability to keep private Root CA key offline (#53)
When generating a new Root CA certificate, show the key in the GUI and ask the user to
store it offline. When importing an existing CA make the root key optional.
When the private key is needed but we don't have it, ask the user to provide it. You
can now also create a CSR for the Issuer CA that can be signed by the offline Root CA.
---
gui/apply | 6 +-
gui/apply-boulder | 6 +-
gui/certificate.go | 323 ++++++++++++++++++++++++++++------
gui/main.go | 188 ++++++++++++++++++--
gui/static/css/labca.css | 14 ++
gui/templates/views/cert.tmpl | 136 ++++++++++++--
6 files changed, 598 insertions(+), 75 deletions(-)
diff --git a/gui/apply b/gui/apply
index cc1b8a9..2984ab3 100755
--- a/gui/apply
+++ b/gui/apply
@@ -12,7 +12,11 @@ cd /opt/wwwstatic
$baseDir/apply-nginx
-cp $PKI_ROOT_CERT_BASE.crl crl/
+if [ -e "$PKI_ROOT_CERT_BASE.crl" ]; then
+ cp $PKI_ROOT_CERT_BASE.crl crl/
+else
+ echo "WARNING: no Root CRL file present - please upload one from the manage page"
+fi
cp $PKI_ROOT_CERT_BASE.pem certs/
cp $PKI_ROOT_CERT_BASE.der certs/
cp $PKI_INT_CERT_BASE.pem certs/
diff --git a/gui/apply-boulder b/gui/apply-boulder
index 68c0206..d5dc047 100755
--- a/gui/apply-boulder
+++ b/gui/apply-boulder
@@ -191,13 +191,15 @@ fi
if [ -e $PKI_ROOT_CERT_BASE.key ]; then
cp -p $PKI_ROOT_CERT_BASE.key test-root.key
cp -p $PKI_ROOT_CERT_BASE.key.der test-root.key.der
- cp -p $PKI_ROOT_CERT_BASE.pem test-root.pem
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 PKI.md | cut -d" " -f 3,4 | sed 's/ /:/g'` .
-if [ -e $PKI_INT_CERT_BASE.key ] && [ -e $PKI_ROOT_CERT_BASE.key ]; then
+if [ -e $PKI_INT_CERT_BASE.key ] && [ -e $PKI_ROOT_CERT_BASE.pem ]; then
[ -f setup_complete ] || touch setup_complete
fi
diff --git a/gui/certificate.go b/gui/certificate.go
index 4351735..5111244 100644
--- a/gui/certificate.go
+++ b/gui/certificate.go
@@ -16,10 +16,11 @@ import (
// CertificateInfo contains all data related to a certificate (file)
type CertificateInfo struct {
- IsRoot bool
- KeyTypes map[string]string
- KeyType string
- CreateType string
+ IsRoot bool
+ KeyTypes map[string]string
+ KeyType string
+ CreateType string
+ IsRootGenerated bool
Country string
Organization string
@@ -33,6 +34,7 @@ type CertificateInfo struct {
Key string
Passphrase string
Certificate string
+ CRL string
RequestBase string
Errors map[string]string
@@ -84,7 +86,7 @@ func (ci *CertificateInfo) Validate() bool {
}
if ci.CreateType == "upload" {
- if strings.TrimSpace(ci.Key) == "" {
+ if !ci.IsRoot && strings.TrimSpace(ci.Key) == "" {
ci.Errors["Key"] = "Please provide a PEM-encoded key"
}
if strings.TrimSpace(ci.Certificate) == "" {
@@ -95,7 +97,7 @@ func (ci *CertificateInfo) Validate() bool {
return len(ci.Errors) == 0
}
-func reportError(err error) error {
+func reportError(param interface{}) error {
lines := strings.Split(string(debug.Stack()), "\n")
if len(lines) >= 5 {
lines = append(lines[:0], lines[5:]...)
@@ -113,7 +115,17 @@ func reportError(err error) error {
fmt.Println(strings.Join(lines, "\n"))
- return errors.New("Error (" + err.Error() + ")! See LabCA logs for details")
+ res := errors.New("Error! See LabCA logs for details")
+ switch v := param.(type) {
+ case error:
+ res = errors.New("Error (" + v.Error() + ")! See LabCA logs for details")
+ case []byte:
+ res = errors.New("Error (" + string(v) + ")! See LabCA logs for details")
+ default:
+ fmt.Printf("unexpected type %T", v)
+ }
+
+ return res
}
func getRandomSerial() (string, error) {
@@ -212,7 +224,10 @@ func (ci *CertificateInfo) Generate(path string, certBase string) error {
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 _, err := exeCmd("openssl ca -config " + path + "../openssl.cnf -extensions v3_intermediate_ca -days 3600 -md sha384 -notext -batch -in " + path + certBase + ".csr -out " + path + certBase + ".pem"); err != nil {
+ if out, err := exeCmd("openssl ca -config " + path + "../openssl.cnf -extensions v3_intermediate_ca -days 3600 -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)
}
}
@@ -287,7 +302,7 @@ func (ci *CertificateInfo) ImportZip(tmpFile string, tmpDir string) error {
}
// Import a certificate and key file
-func (ci *CertificateInfo) Import(path string, certBase string, tmpDir string, tmpKey string, tmpCert string) error {
+func (ci *CertificateInfo) Import(tmpDir string, tmpKey string, tmpCert string) error {
tmpFile := filepath.Join(tmpDir, ci.ImportHandler.Filename)
f, err := os.OpenFile(tmpFile, os.O_WRONLY|os.O_CREATE, 0666)
@@ -320,34 +335,36 @@ func (ci *CertificateInfo) Import(path string, certBase string, tmpDir string, t
}
// Upload a certificate and key file
-func (ci *CertificateInfo) Upload(path string, certBase string, tmpKey string, tmpCert string) error {
- if err := os.WriteFile(tmpKey, []byte(ci.Key), 0644); err != nil {
- return err
- }
-
- pwd := "pass:dummy"
- if ci.Passphrase != "" {
- pwd = "pass:" + strings.Replace(ci.Passphrase, " ", "\\\\", -1)
- }
-
- if out, err := exeCmd("openssl pkey -passin " + pwd + " -in " + tmpKey + " -out " + tmpKey + "-out"); err != nil {
- if strings.Contains(string(out), ":bad decrypt:") {
- return errors.New("incorrect password")
+func (ci *CertificateInfo) Upload(tmpKey string, tmpCert string) error {
+ if ci.Key != "" {
+ if err := os.WriteFile(tmpKey, []byte(ci.Key), 0644); err != nil {
+ return err
}
- return reportError(err)
- }
+ pwd := "pass:dummy"
+ if ci.Passphrase != "" {
+ pwd = "pass:" + strings.Replace(ci.Passphrase, " ", "\\\\", -1)
+ }
- if _, err := exeCmd("mv " + tmpKey + "-out " + tmpKey); err != nil {
- return reportError(err)
+ if out, err := exeCmd("openssl pkey -passin " + pwd + " -in " + tmpKey + " -out " + tmpKey + "-out"); err != nil {
+ if strings.Contains(string(out), ":bad decrypt:") {
+ return errors.New("incorrect password")
+ }
+
+ return reportError(err)
+ }
+
+ if _, err := exeCmd("mv " + tmpKey + "-out " + tmpKey); err != nil {
+ return reportError(err)
+ }
}
if err := os.WriteFile(tmpCert, []byte(ci.Certificate), 0644); err != nil {
return err
}
- if _, err := exeCmd("openssl x509 -in " + tmpCert + " -out " + tmpCert + "-out"); err != nil {
- return reportError(err)
+ if out, err := exeCmd("openssl x509 -in " + tmpCert + " -out " + tmpCert + "-out"); err != nil {
+ return reportError(out)
}
if _, err := exeCmd("mv " + tmpCert + "-out " + tmpCert); err != nil {
@@ -357,6 +374,54 @@ func (ci *CertificateInfo) Upload(path string, certBase string, tmpKey string, t
return nil
}
+func parseSubjectDn(subject string) map[string]string {
+ trackerResultMap := map[string]string{"C=": "", "C =": "", "O=": "", "O =": "", "CN=": "", "CN =": "", "OU=": "", "OU =": ""}
+
+ for tracker, _ := range trackerResultMap {
+ index := strings.Index(subject, tracker)
+
+ if index < 0 {
+ continue
+ }
+
+ var res string
+ // track quotes for delimited fields so we know not to split on the comma
+ quoteCount := 0
+
+ for i := index + len(tracker); i < len(subject); i++ {
+ char := subject[i]
+
+ // if ", we need to count and delimit
+ if char == 34 {
+ quoteCount++
+ if quoteCount == 2 {
+ break
+ } else {
+ continue
+ }
+ }
+
+ // comma, lets stop here but only if we don't have quotes
+ if char == 44 && quoteCount == 0 {
+ break
+ }
+
+ // add this individual char
+ res += string(rune(char))
+ }
+
+ trackerResultMap[strings.TrimSpace(strings.TrimSuffix(tracker, "="))] = strings.TrimSpace(strings.TrimPrefix(res, "="))
+ }
+
+ for k, v := range trackerResultMap {
+ if len(v) == 0 {
+ delete(trackerResultMap, k)
+ }
+ }
+
+ 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 {
var rootSubject string
@@ -369,12 +434,32 @@ func (ci *CertificateInfo) ImportCerts(path string, rootCert string, rootKey str
rootSubject = string(r[0 : len(r)-1])
fmt.Printf("Import root with subject '%s'\n", rootSubject)
- _, err = exeCmd("openssl pkey -noout -in " + rootKey)
- if err != nil {
- return reportError(err)
+ subjectMap := parseSubjectDn(rootSubject)
+ if val, ok := subjectMap["C"]; ok {
+ ci.Country = val
+ }
+ 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
}
- fmt.Println("Import root key")
+ keyFileExists := true
+ if _, err := os.Stat(rootKey); os.IsNotExist(err) {
+ keyFileExists = false
+ }
+ if keyFileExists {
+ _, err = exeCmd("openssl pkey -noout -in " + rootKey)
+ if err != nil {
+ return reportError(err)
+ }
+
+ fmt.Println("Import root key")
+ }
}
if (issuerCert != "") && (issuerKey != "") {
@@ -414,6 +499,11 @@ func (ci *CertificateInfo) ImportCerts(path string, rootCert string, rootKey str
return errors.New("issuer not issued by our Root CA")
}
+ r, err = exeCmd("openssl verify -CAfile data/root-ca.pem " + issuerCert)
+ if err != nil {
+ return errors.New("could not verify that issuer was issued by our Root CA")
+ }
+
_, err = exeCmd("openssl pkey -noout -in " + issuerKey)
if err != nil {
return reportError(err)
@@ -433,8 +523,14 @@ func (ci *CertificateInfo) MoveFiles(path string, rootCert string, rootKey strin
}
}
if rootKey != "" {
- if _, err := exeCmd("mv " + rootKey + " " + path); err != nil {
- return reportError(err)
+ keyFileExists := true
+ if _, err := os.Stat(rootKey); os.IsNotExist(err) {
+ keyFileExists = false
+ }
+ if keyFileExists {
+ if _, err := exeCmd("mv " + rootKey + " " + path); err != nil {
+ return reportError(err)
+ }
}
}
if issuerCert != "" {
@@ -449,7 +545,7 @@ func (ci *CertificateInfo) MoveFiles(path string, rootCert string, rootKey strin
}
if (issuerCert != "") && (issuerKey != "") && ci.IsRoot {
- if err := postCreateTasks(path+"issuer/", "ca-int"); err != nil {
+ if err := postCreateTasks(path+"issuer/", "ca-int", false); err != nil {
return err
}
}
@@ -458,7 +554,7 @@ 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) error {
+func (ci *CertificateInfo) Extract(path string, certBase string, tmpDir string, wasCSR bool) error {
var rootCert string
var rootKey string
var issuerCert string
@@ -471,9 +567,6 @@ func (ci *CertificateInfo) Extract(path string, certBase string, tmpDir string)
if _, err := os.Stat(rootCert); os.IsNotExist(err) {
return errors.New("file does not contain root-ca.pem")
}
- if _, err := os.Stat(rootKey); os.IsNotExist(err) {
- return errors.New("file does not contain root-ca.key")
- }
}
issuerCert = filepath.Join(tmpDir, "ca-int.pem")
@@ -487,7 +580,7 @@ func (ci *CertificateInfo) Extract(path string, certBase string, tmpDir string)
}
}
if _, err := os.Stat(issuerKey); os.IsNotExist(err) {
- if ci.IsRoot {
+ if ci.IsRoot || wasCSR {
issuerKey = ""
} else {
return errors.New("file does not contain ca-int.key")
@@ -509,7 +602,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) error {
+func (ci *CertificateInfo) Create(path string, certBase string, wasCSR bool) error {
if err := preCreateTasks(path); err != nil {
return err
}
@@ -538,13 +631,13 @@ func (ci *CertificateInfo) Create(path string, certBase string) error {
}
} else if ci.CreateType == "import" {
- err := ci.Import(path, certBase, tmpDir, tmpKey, tmpCert)
+ err := ci.Import(tmpDir, tmpKey, tmpCert)
if err != nil {
return err
}
} else if ci.CreateType == "upload" {
- err := ci.Upload(path, certBase, tmpKey, tmpCert)
+ err := ci.Upload(tmpKey, tmpCert)
if err != nil {
return err
}
@@ -555,28 +648,37 @@ func (ci *CertificateInfo) Create(path string, certBase string) error {
// This is shared between pfx/zip upload and pem text upload
if ci.CreateType != "generate" {
- err := ci.Extract(path, certBase, tmpDir)
+ err := ci.Extract(path, certBase, tmpDir, wasCSR)
if err != nil {
return err
}
}
- if err := postCreateTasks(path, certBase); err != nil {
+ if err := postCreateTasks(path, certBase, ci.IsRoot); err != nil {
return err
}
if ci.IsRoot {
- 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)
+ keyFileExists := true
+ if _, err := os.Stat(path + certBase + ".key"); os.IsNotExist(err) {
+ keyFileExists = false
+ }
+ if keyFileExists {
+ // TODO: make option in GUI to trigger crl creation!
+ 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) error {
- if _, err := exeCmd("openssl pkey -in " + path + certBase + ".key -out " + path + certBase + ".key.der -outform der"); err != nil {
- return reportError(err)
+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 {
@@ -586,6 +688,127 @@ func postCreateTasks(path string, certBase string) error {
return nil
}
+func (ci *CertificateInfo) StoreRootKey(path string) bool {
+ if ci.Errors == nil {
+ ci.Errors = make(map[string]string)
+ }
+ if strings.TrimSpace(ci.Key) == "" {
+ ci.Errors["Modal"] = "Please provide a PEM-encoded key"
+ return false
+ }
+
+ tmpDir, err := os.MkdirTemp("", "labca")
+ if err != nil {
+ ci.Errors["Modal"] = err.Error()
+ return false
+ }
+
+ defer os.RemoveAll(tmpDir)
+
+ tmpKey := filepath.Join(tmpDir, "root-ca.key")
+
+ if err := os.WriteFile(tmpKey, []byte(ci.Key), 0644); err != nil {
+ ci.Errors["Modal"] = err.Error()
+ return false
+ }
+
+ if ci.Passphrase != "" {
+ pwd := "pass:" + strings.Replace(ci.Passphrase, " ", "\\\\", -1)
+
+ if out, err := exeCmd("openssl pkey -passin " + pwd + " -in " + tmpKey + " -out " + tmpKey + "-out"); err != nil {
+ if strings.Contains(string(out), ":bad decrypt:") {
+ ci.Errors["Modal"] = "Incorrect password"
+ return false
+ }
+
+ ci.Errors["Modal"] = "Unable to load Root CA key"
+ return false
+ }
+
+ if _, err := exeCmd("mv " + tmpKey + "-out " + tmpKey); err != nil {
+ ci.Errors["Modal"] = err.Error()
+ return false
+ }
+ }
+
+ modKey, err := exeCmd("openssl rsa -noout -modulus -in " + tmpKey)
+ if err != nil {
+ ci.Errors["Modal"] = "Not a private key"
+ return false
+ }
+ modCert, err := exeCmd("openssl x509 -noout -modulus -in " + path + "root-ca.pem")
+ if err != nil {
+ ci.Errors["Modal"] = "Unable to load Root CA certificate"
+ return false
+ }
+ if string(modKey) != string(modCert) {
+ ci.Errors["Modal"] = "Key does not match the Root CA certificate"
+ return false
+ }
+
+ if _, err := exeCmd("mv " + tmpKey + " " + path); err != nil {
+ ci.Errors["Modal"] = err.Error()
+ return false
+ }
+
+ // Create root CRL file now that we have the key
+ certBase := "root-ca"
+ if _, err := exeCmd("openssl ca -config " + path + "openssl.cnf -gencrl -keyfile " + path + certBase + ".key -cert " + path + certBase + ".pem -out " + path + certBase + ".crl"); err != nil {
+ fmt.Printf("StoreRootKey: %s\n", err.Error())
+ return false
+ }
+
+ return true
+}
+
+func (ci *CertificateInfo) StoreCRL(path string) bool {
+ if ci.Errors == nil {
+ ci.Errors = make(map[string]string)
+ }
+ if strings.TrimSpace(ci.CRL) == "" {
+ fmt.Println("WARNING: no Root CRL file provided - please upload one from the manage page")
+ return true
+ }
+
+ tmpDir, err := os.MkdirTemp("", "labca")
+ if err != nil {
+ ci.Errors["Modal"] = err.Error()
+ return false
+ }
+
+ defer os.RemoveAll(tmpDir)
+
+ tmpCRL := filepath.Join(tmpDir, "root-ca.crl")
+
+ if err := os.WriteFile(tmpCRL, []byte(ci.CRL), 0644); err != nil {
+ ci.Errors["Modal"] = err.Error()
+ return false
+ }
+
+ crlIssuer, err := exeCmd("openssl crl -noout -issuer -in " + tmpCRL)
+ if err != nil {
+ ci.Errors["Modal"] = "Not a CRL file"
+ return false
+ }
+ rootSubj, err := exeCmd("openssl x509 -noout -subject -in " + path + "root-ca.pem")
+ if err != nil {
+ ci.Errors["Modal"] = "Cannot get Root CA subject"
+ return false
+ }
+
+ if strings.TrimPrefix(string(crlIssuer), "issuer=") != strings.TrimPrefix(string(rootSubj), "subject=") {
+ ci.Errors["Modal"] = "CRL file does not match the Root CA certificate"
+ return false
+ }
+
+ if _, err := exeCmd("mv " + tmpCRL + " " + path); err != nil {
+ ci.Errors["Modal"] = err.Error()
+ return false
+ }
+
+ return true
+}
+
func exeCmd(cmd string) ([]byte, error) {
parts := strings.Fields(cmd)
for i := 0; i < len(parts); i++ {
diff --git a/gui/main.go b/gui/main.go
index 7e2c735..2e7493f 100644
--- a/gui/main.go
+++ b/gui/main.go
@@ -18,6 +18,7 @@ import (
"fmt"
"html/template"
"io"
+ "io/ioutil"
"log"
"math"
"math/big"
@@ -1542,6 +1543,9 @@ func _buildCI(r *http.Request, session *sessions.Session, isRoot bool) *Certific
ci.Initialize()
if session.Values["ct"] != nil {
+ if !isRoot && session.Values["ct"].(string) == "generate" {
+ ci.IsRootGenerated = true
+ }
ci.CreateType = session.Values["ct"].(string)
}
if session.Values["kt"] != nil {
@@ -1587,6 +1591,49 @@ func issuerNameID(certfile string) (int64, error) {
}
func _certCreate(w http.ResponseWriter, r *http.Request, certBase string, isRoot bool) bool {
+ if r.Method == "POST" {
+ if err := r.ParseMultipartForm(2 * 1024 * 1024); err != nil {
+ errorHandler(w, r, err, http.StatusInternalServerError)
+ return false
+ }
+
+ 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
+ if _, err := exeCmd("rm data/root-ca.pem"); err != nil {
+ errorHandler(w, r, err, http.StatusInternalServerError)
+ return false
+ }
+ certBase = "root-ca"
+ isRoot = true
+ r.Method = "GET"
+ sess, _ := sessionStore.Get(r, "labca")
+ sess.Values["ct"] = "generate"
+ 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?
+ if r.Form.Get("keep-root-online") == "true" {
+ sess, _ := sessionStore.Get(r, "labca")
+ sess.Values["root-online"] = true
+ if err := sess.Save(r, w); err != nil {
+ log.Printf("cannot save session: %s\n", err)
+ }
+ }
+
+ // Undo what setupHandler did when showing the public key...
+ _, errPem := os.Stat("data/root-ca.pem")
+ _, errTmp := os.Stat("data/root-ca.pem_TMP")
+ if os.IsNotExist(errPem) && !os.IsNotExist(errTmp) {
+ exeCmd("mv data/root-ca.pem_TMP data/root-ca.pem")
+ }
+
+ r.Method = "GET"
+ return true
+ }
+ }
+
path := "data/"
if !isRoot {
path = path + "issuer/"
@@ -1641,15 +1688,99 @@ func _certCreate(w http.ResponseWriter, r *http.Request, certBase string, isRoot
ci.RequestBase = r.Header.Get("X-Request-Base")
if !ci.Validate() {
- render(w, r, "cert:manage", map[string]interface{}{"CertificateInfo": ci, "Progress": _progress(certBase), "HelpText": _helptext(certBase)})
- return false
+ if session.Values["csr"] == true {
+ delete(ci.Errors, "Key")
+ } else {
+ render(w, r, "cert:manage", map[string]interface{}{"CertificateInfo": ci, "Progress": _progress(certBase), "HelpText": _helptext(certBase)})
+ return false
+ }
}
- if err := ci.Create(path, certBase); err != nil {
- ci.Errors[cases.Title(language.Und).String(ci.CreateType)] = err.Error()
- log.Printf("_certCreate: create failed: %v", err)
- render(w, r, "cert:manage", map[string]interface{}{"CertificateInfo": ci, "Progress": _progress(certBase), "HelpText": _helptext(certBase)})
- return false
+ wasCSR := session.Values["csr"] == true
+ if r.Form.Get("ack-rootkey") != "yes" {
+ if r.Form.Get("rootkey") != "" {
+ rootci := &CertificateInfo{
+ IsRoot: true,
+ Key: r.Form.Get("rootkey"),
+ Passphrase: r.Form.Get("rootpassphrase"),
+ }
+ if !rootci.StoreRootKey("data/") {
+ ci.Errors["Modal"] = rootci.Errors["Modal"]
+ render(w, r, "cert:manage", map[string]interface{}{"CertificateInfo": ci, "GetRootKey": true, "Progress": _progress(certBase), "HelpText": _helptext(certBase)})
+ return false
+ }
+ }
+ if r.Form.Get("crl") != "" {
+ rootci := &CertificateInfo{
+ IsRoot: true,
+ CRL: r.Form.Get("crl"),
+ }
+ if !rootci.StoreCRL("data/") {
+ ci.Errors["Modal"] = rootci.Errors["Modal"]
+ csr, err := os.Open(path + certBase + ".csr")
+ 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)
+ render(w, r, "cert:manage", map[string]interface{}{"CertificateInfo": ci, "Progress": _progress(certBase), "HelpText": _helptext(certBase)})
+ return false
+ }
+ defer csr.Close()
+ b, err := ioutil.ReadAll(csr)
+
+ render(w, r, "cert:manage", map[string]interface{}{"CertificateInfo": ci, "CSR": string(b), "Progress": _progress(certBase), "HelpText": _helptext(certBase)})
+ return false
+ }
+ }
+
+ if err := ci.Create(path, certBase, wasCSR); err != nil {
+ if err.Error() == "NO_ROOT_KEY" {
+ if r.Form.Get("generate") != "" {
+ if r.Form.Get("rootkey") == "" {
+ render(w, r, "cert:manage", map[string]interface{}{"CertificateInfo": ci, "GetRootKey": true, "Progress": _progress(certBase), "HelpText": _helptext(certBase)})
+ return false
+ } else {
+ rootci := &CertificateInfo{
+ IsRoot: true,
+ Key: r.Form.Get("rootkey"),
+ Passphrase: r.Form.Get("rootpassphrase"),
+ }
+ if !rootci.StoreRootKey("data/") {
+ ci.Errors["Modal"] = rootci.Errors["Modal"]
+ render(w, r, "cert:manage", map[string]interface{}{"CertificateInfo": ci, "GetRootKey": true, "Progress": _progress(certBase), "HelpText": _helptext(certBase)})
+ return false
+ }
+
+ render(w, r, "cert:manage", map[string]interface{}{"CertificateInfo": ci, "Progress": _progress(certBase), "HelpText": _helptext(certBase)})
+ return false
+ }
+ }
+
+ if r.Form.Get("getcsr") != "" {
+ csr, err := os.Open(path + certBase + ".csr")
+ 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)
+ render(w, r, "cert:manage", map[string]interface{}{"CertificateInfo": ci, "Progress": _progress(certBase), "HelpText": _helptext(certBase)})
+ return false
+ }
+ defer csr.Close()
+ b, err := ioutil.ReadAll(csr)
+
+ session.Values["csr"] = true
+ if err = session.Save(r, w); err != nil {
+ log.Printf("cannot save session: %s\n", err)
+ }
+
+ render(w, r, "cert:manage", map[string]interface{}{"CertificateInfo": ci, "CSR": string(b), "Progress": _progress(certBase), "HelpText": _helptext(certBase)})
+ return false
+ }
+ } else {
+ ci.Errors[cases.Title(language.Und).String(ci.CreateType)] = err.Error()
+ log.Printf("_certCreate: create failed: %v", err)
+ render(w, r, "cert:manage", map[string]interface{}{"CertificateInfo": ci, "Progress": _progress(certBase), "HelpText": _helptext(certBase)})
+ return false
+ }
+ }
}
if !ci.IsRoot {
@@ -1660,6 +1791,15 @@ func _certCreate(w http.ResponseWriter, r *http.Request, certBase string, isRoot
} else {
log.Printf("_certCreate: could not calculate IssuerNameID: %v", err)
}
+
+ if session.Values["root-online"] != true {
+ if _, err := os.Stat(path + "../root-ca.key"); !os.IsNotExist(err) {
+ 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 viper.Get("labca.organization") == nil {
@@ -1677,6 +1817,21 @@ func _certCreate(w http.ResponseWriter, r *http.Request, certBase string, isRoot
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, err := ioutil.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 {
@@ -1811,18 +1966,25 @@ func _helptext(stage string) template.HTML {
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 certificate, pick a key type and strength (the higher the number the\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",
"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.
"))
+ "once you leave the organization field.
\n",
+ "
If you want to upload an existing root certificate, you may choose to keep the private key\n",
+ "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" {
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",
- "
If you want to generate a certificate, by default the same key type and strength is selected as\n",
+ "
If you want to generate a certificate, by default the same key type and strength is selected as\n",
"was chosen in the previous step when generating the root, but you may choose a different\n",
- "one. By default the common name is the same as the CN for the Root CA, minus the word 'Root'.
"))
+ "one. By default the common name is the same as the CN for the Root CA, minus the word 'Root'.\n",
+ "
If you are using an offline Root CA certificate then you can download the Certificate Signing\n",
+ "Request (CSR) here and have it signed by the Root CA. Alternatively we (temporarily) need the\n",
+ "secret key of the Root CA for generating the issuing certificate.
"))
} else if stage == "standalone" {
return template.HTML(fmt.Sprint("
Currently only step-ca is supported, using the MySQL database backend.\n",
"Please provide the necessary connectiuon details here."))
@@ -2168,7 +2330,9 @@ func setupHandler(w http.ResponseWriter, r *http.Request) {
// 3. Setup root CA certificate
if !_certCreate(w, r, "root-ca", true) {
// Cleanup the cert (if it even exists) so we will retry on the next run
- os.Remove("data/root-ca.pem")
+ if _, err := os.Stat("data/root-ca.pem"); !os.IsNotExist(err) {
+ exeCmd("mv data/root-ca.pem data/root-ca.pem_TMP")
+ }
return
}
diff --git a/gui/static/css/labca.css b/gui/static/css/labca.css
index e555a38..4c11d77 100644
--- a/gui/static/css/labca.css
+++ b/gui/static/css/labca.css
@@ -124,6 +124,10 @@ td.pad-low-top {
width: 10em;
}
+.btn-right {
+ float: right;
+}
+
.vmiddle {
vertical-align: middle !important;
}
@@ -140,6 +144,16 @@ td.pad-low-top {
border: none;
}
+.modal-content {
+ padding: 6px;
+ width: 614px;
+}
+
+.modal-content > textarea {
+ width: 600px;
+ height: 550px;
+}
+
.non-fluid {
width: auto !important;
}
diff --git a/gui/templates/views/cert.tmpl b/gui/templates/views/cert.tmpl
index e5ccf9f..8261595 100644
--- a/gui/templates/views/cert.tmpl
+++ b/gui/templates/views/cert.tmpl
@@ -22,6 +22,10 @@
@@ -105,12 +115,20 @@
-
+
+ Key (in PEM format{{ if .IsRoot }}; optional{{ end }}):
{{ with .Errors.Key }}
{{ . }}
{{ end }}
-
+
Passphrase (optional):
@@ -120,13 +138,14 @@
-
- Certificate (in PEM format):
- {{ with .Errors.Certificate }}
+
+ You should provide the Root CRL here as well, but this can also be done later
+ Root CRL (optional):
+ {{ with .Errors.CRL }}
{{ . }}
{{ end }}
-
+
{{ with .Errors.Upload }}
@@ -140,6 +159,63 @@
Setting up LabCA and applying configuration. This will take a minute...
{{end}}
+
+{{ if ne .RootKey nil }}
+
+
+
+
Root Key
+
This is the generated private key for the Root CA certificate. Copy it now and store
+ it in a secure, private and offline location! After setting up the Issuer CA certificate in the next
+ step, we will delete the key from the system.
+ When we need the key for certain operations in the future, we will ask for it.
+
+ Keep the root key on the LabCA server (UNSAFE)
+ {{ with .CertificateInfo.Errors.Modal }}
+ {{ . }}
+ {{ end }}
+
+
+
+
+{{ end }}
+
+{{ if and (not .CertificateInfo.IsRoot) (ne .CSR nil) }}
+
+
+
+
CSR
+
+
Please have this CSR signed by the offline Root CA. Then you can close this dialog and upload the resulting certificate.
+ {{ with .CertificateInfo.Errors.Modal }}
+ {{ . }}
+ {{ end }}
+
+
+
+
+{{ end }}
+
+{{ if .GetRootKey }}
+
+
+
+
Root Key Required
+
Please provide the Root CA key file in PEM format. As soon as we are done with it, it will be removed from the system again.
+
+
+ Passphrase (optional):
+
+
+ {{ with .CertificateInfo.Errors.Modal }}
+ {{ . }}
+ {{ end }}
+
+
+
+
+{{ end }}
+
{{ template "partials/progress.tmpl" . }}
{{end}}
@@ -151,6 +227,46 @@
$(window).resize();
}, 250);
});
+
+{{ if ne .CSR nil }}
+ $("#modal-csr").modal("show");
+{{ end }}
+ $("#modal-csr-done").click(function() {
+ $('.nav-tabs a[href="#upload"]').tab('show');
+ $("#key").val("").parent().hide();
+ $("#passphrase").val("").parent().hide();
+ $("#crl").val("").parent().show();
+ $("#modal-csr").modal("hide");
+ });
+
+{{ if ne .RootKey nil }}
+ $("#modal-rootkey").modal("show");
+{{ end }}
+ $("#modal-rootkey-done").click(function() {
+ $("#ack-rootkey").val("yes");
+ $("#keep-root-online").val($("#modal-keep-root-online").prop("checked"));
+ $("#modal-rootkey").modal("hide");
+ $("#generatebtn").click();
+ });
+
+{{ if .GetRootKey }}
+ $("#modal-root-key").modal("show");
+{{ end }}
+ $("#modal-root-key-upload").click(function() {
+ $("#rootkey").val($("#modal-rootkey").val());
+ $("#rootpassphrase").val($("#modal-rootpassphrase").val());
+ $("#modal-root-key").modal("hide");
+ $("#generatebtn").click();
+ });
+
+ $("#revertroot").click(function() {
+ if (!window.confirm("Do you really want to remove the Root CA certificate?")) {
+ return false;
+ }
+ $("#c").removeAttr("required");
+ $("#o").removeAttr("required");
+ });
+
{{ if not .CertificateInfo.IsRoot }}
$(".form-cert").submit(function() {