From 33208bf3473846105ea95f462ecb36cf73f30f00 Mon Sep 17 00:00:00 2001
From: Arjan H
Date: Tue, 26 Dec 2023 11:56:45 +0100
Subject: [PATCH] Add way to renew (extend lifetime of) CA certificates (#74)
---
build/Dockerfile-gui | 1 +
control_do.sh | 2 +
gui/apply | 11 +-
gui/apply-boulder | 2 +-
gui/apply-nginx | 2 +-
gui/certificate.go | 357 ++++++++++++++----
gui/chains.go | 339 +++++++++++++++++
gui/main.go | 282 +++++++++++---
gui/setup.sh | 2 +-
gui/static/css/labca.css | 29 +-
gui/templates/views/cert.tmpl | 92 ++++-
gui/templates/views/manage.tmpl | 641 ++++++++++++++++++++++++++------
install | 4 +-
patches/storer_storer.patch | 18 +-
14 files changed, 1535 insertions(+), 247 deletions(-)
create mode 100644 gui/chains.go
diff --git a/build/Dockerfile-gui b/build/Dockerfile-gui
index 6c53493..7f1a2ee 100644
--- a/build/Dockerfile-gui
+++ b/build/Dockerfile-gui
@@ -32,6 +32,7 @@ RUN apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates \
tzdata \
+ unzip \
zip \
&& rm -rf /var/lib/apt/lists/*
diff --git a/control_do.sh b/control_do.sh
index 76f8246..143ed06 100755
--- a/control_do.sh
+++ b/control_do.sh
@@ -58,8 +58,10 @@ setup_nginx_data() {
[ -e /opt/labca/data/root-ca.crl ] && cp /opt/labca/data/root-ca.crl crl/ || true
[ -e /opt/labca/data/root-ca.pem ] && cp /opt/labca/data/root-ca.pem certs/ || true
+ [ -e /opt/labca/data/root-ca.pem ] && ln -sf root-ca.pem certs/test-root.pem || true
[ -e /opt/labca/data/root-ca.der ] && cp /opt/labca/data/root-ca.der certs/ || true
[ -e /opt/labca/data/issuer/ca-int.pem ] && cp /opt/labca/data/issuer/ca-int.pem certs/ || true
+ [ -e /opt/labca/data/issuer/ca-int.pem ] && ln -sf ca-int.pem certs/test-ca.pem || true
[ -e /opt/labca/data/issuer/ca-int.pem ] && cp /opt/labca/data/issuer/ca-int.der certs/ || true
if [ ! -e /etc/nginx/ssl/labca_cert.pem ]; then
diff --git a/gui/apply b/gui/apply
index 2984ab3..21c62b9 100755
--- a/gui/apply
+++ b/gui/apply
@@ -8,9 +8,10 @@ dataDir="$baseDir/data"
export PKI_ROOT_CERT_BASE="$dataDir/root-ca"
export PKI_INT_CERT_BASE="$dataDir/issuer/ca-int"
-cd /opt/wwwstatic
+cd /opt/boulder/labca
+$baseDir/apply-boulder
-$baseDir/apply-nginx
+cd /opt/wwwstatic
if [ -e "$PKI_ROOT_CERT_BASE.crl" ]; then
cp $PKI_ROOT_CERT_BASE.crl crl/
@@ -18,10 +19,10 @@ 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/
-
-cd /opt/boulder/labca
-$baseDir/apply-boulder
+$baseDir/apply-nginx
diff --git a/gui/apply-boulder b/gui/apply-boulder
index 9d912ad..9b33f8d 100755
--- a/gui/apply-boulder
+++ b/gui/apply-boulder
@@ -2,7 +2,7 @@
set -e
-baseDir=$(dirname $0)
+baseDir=$(cd $(dirname $0) && pwd)
dataDir="$baseDir/data"
PKI_DNS=$(grep dns $dataDir/config.json | perl -p0e 's/.*?:\s+(.*)/\1/' | sed -e 's/\",//g' | sed -e 's/\"//g')
diff --git a/gui/apply-nginx b/gui/apply-nginx
index fdc4c56..35c9489 100755
--- a/gui/apply-nginx
+++ b/gui/apply-nginx
@@ -2,7 +2,7 @@
set -e
-baseDir=$(dirname $0)
+baseDir=$(cd $(dirname $0) && pwd)
dataDir="$baseDir/data"
PKI_WEB_TITLE=$(grep web_title $dataDir/config.json | sed -e 's/.*:[ ]*//' | sed -e 's/\",//g' | sed -e 's/\"//g')
diff --git a/gui/certificate.go b/gui/certificate.go
index 00ee587..1322a97 100644
--- a/gui/certificate.go
+++ b/gui/certificate.go
@@ -2,25 +2,36 @@ package main
import (
"crypto/rand"
+ "crypto/x509"
+ "encoding/pem"
"errors"
"fmt"
"io"
+ "io/fs"
+ "math"
"math/big"
"mime/multipart"
"os"
"os/exec"
"path/filepath"
+ "regexp"
"runtime/debug"
+ "strconv"
"strings"
+ "time"
)
// CertificateInfo contains all data related to a certificate (file)
type CertificateInfo struct {
IsRoot bool
+ IsFirst bool
KeyTypes map[string]string
KeyType string
CreateType string
IsRootGenerated bool
+ RootSubject string
+ RootEnddate string
+ NumDays int
Country string
Organization string
@@ -115,7 +126,7 @@ func reportError(param interface{}) error {
fmt.Println(strings.Join(lines, "\n"))
- res := errors.New("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")
@@ -151,7 +162,7 @@ func preCreateTasks(path string) error {
return reportError(err)
}
- if _, err := os.Stat(path + "serial"); os.IsNotExist(err) {
+ if _, err := os.Stat(path + "serial"); errors.Is(err, fs.ErrNotExist) {
s, err := getRandomSerial()
if err != nil {
return err
@@ -160,7 +171,7 @@ func preCreateTasks(path string) error {
return err
}
}
- if _, err := os.Stat(path + "crlnumber"); os.IsNotExist(err) {
+ if _, err := os.Stat(path + "crlnumber"); errors.Is(err, fs.ErrNotExist) {
if err = os.WriteFile(path+"crlnumber", []byte("1000\n"), 0644); err != nil {
return err
}
@@ -173,6 +184,23 @@ func preCreateTasks(path string) error {
return nil
}
+func updateRootCRLDays(filename string, numDays int) error {
+ read, err := os.ReadFile(filename)
+ if err != nil {
+ fmt.Println(err)
+ return errors.New("could not read '" + filename + "': " + err.Error())
+ }
+ re := regexp.MustCompile(`(default_crl_days\s*=).*`)
+ res := re.ReplaceAll(read, []byte("$1 "+strconv.Itoa(numDays)))
+
+ if err = os.WriteFile(filename, res, 0640); err != nil {
+ fmt.Println(err)
+ return errors.New("could not write '" + filename + "': " + err.Error())
+ }
+
+ return nil
+}
+
// Generate a key and certificate file for the data from this CertificateInfo
func (ci *CertificateInfo) Generate(path string, certBase string) error {
// 1. Generate key
@@ -217,14 +245,18 @@ func (ci *CertificateInfo) Generate(path string, certBase string) error {
subject = strings.Replace(subject, " ", "\\\\", -1)
if ci.IsRoot {
- if _, err := exeCmd("openssl req -config " + path + "openssl.cnf -days 3650 -new -utf8 -x509 -extensions v3_ca -subj " + subject + " -key " + path + certBase + ".key -out " + path + certBase + ".pem"); err != nil {
+ if _, err := exeCmd("openssl req -config " + path + "openssl.cnf -days " + strconv.Itoa(ci.NumDays) + " -new -utf8 -x509 -extensions v3_ca -subj " + subject + " -key " + path + certBase + ".key -out " + path + certBase + ".pem"); err != nil {
+ return reportError(err)
+ }
+
+ if err := updateRootCRLDays(path+"openssl.cnf", ci.NumDays); err != nil {
return reportError(err)
}
} else {
if _, err := exeCmd("openssl req -config " + path + "openssl.cnf -new -utf8 -subj " + subject + " -key " + path + certBase + ".key -out " + path + certBase + ".csr"); err != nil {
return reportError(err)
}
- if out, err := exeCmd("openssl ca -config " + path + "../openssl.cnf -extensions v3_intermediate_ca -days 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 " + strconv.Itoa(ci.NumDays) + " -md sha384 -notext -batch -in " + path + certBase + ".csr -out " + path + certBase + ".pem"); err != nil {
if strings.Contains(string(out), "root-ca.key for reading, No such file or directory") {
return errors.New("NO_ROOT_KEY")
}
@@ -321,7 +353,7 @@ func (ci *CertificateInfo) Import(tmpDir string, tmpKey string, tmpCert string)
return err
}
- } else if contentType == "application/zip" {
+ } else if contentType == "application/zip" || contentType == "application/x-zip-compressed" {
err := ci.ImportZip(tmpFile, tmpDir)
if err != nil {
return err
@@ -377,7 +409,7 @@ func (ci *CertificateInfo) Upload(tmpKey string, tmpCert string) error {
func parseSubjectDn(subject string) map[string]string {
trackerResultMap := map[string]string{"C=": "", "C =": "", "O=": "", "O =": "", "CN=": "", "CN =": "", "OU=": "", "OU =": ""}
- for tracker, _ := range trackerResultMap {
+ for tracker := range trackerResultMap {
index := strings.Index(subject, tracker)
if index < 0 {
@@ -449,7 +481,7 @@ func (ci *CertificateInfo) ImportCerts(path string, rootCert string, rootKey str
}
keyFileExists := true
- if _, err := os.Stat(rootKey); os.IsNotExist(err) {
+ if _, err := os.Stat(rootKey); errors.Is(err, fs.ErrNotExist) {
keyFileExists = false
}
if keyFileExists {
@@ -499,7 +531,7 @@ 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)
+ _, 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")
}
@@ -524,7 +556,7 @@ func (ci *CertificateInfo) MoveFiles(path string, rootCert string, rootKey strin
}
if rootKey != "" {
keyFileExists := true
- if _, err := os.Stat(rootKey); os.IsNotExist(err) {
+ if _, err := os.Stat(rootKey); errors.Is(err, fs.ErrNotExist) {
keyFileExists = false
}
if keyFileExists {
@@ -564,26 +596,60 @@ func (ci *CertificateInfo) Extract(path string, certBase string, tmpDir string,
rootCert = filepath.Join(tmpDir, "root-ca.pem")
rootKey = filepath.Join(tmpDir, "root-ca.key")
- if _, err := os.Stat(rootCert); os.IsNotExist(err) {
- return errors.New("file does not contain root-ca.pem")
+ if _, err := os.Stat(rootCert); errors.Is(err, fs.ErrNotExist) {
+ altCert := filepath.Join(tmpDir, "test-root.pem")
+ if _, err = os.Stat(altCert); err == nil {
+ if _, err := exeCmd("mv " + altCert + " " + rootCert); err != nil {
+ return err
+ }
+ }
+
+ altKey := filepath.Join(tmpDir, "test-root.key")
+ if _, err = os.Stat(altKey); err == nil {
+ if _, err := exeCmd("mv " + altKey + " " + rootKey); err != nil {
+ return err
+ }
+ }
+
+ if _, err := os.Stat(rootCert); errors.Is(err, fs.ErrNotExist) {
+ return errors.New("file does not contain root-ca.pem")
+ }
}
}
issuerCert = filepath.Join(tmpDir, "ca-int.pem")
issuerKey = filepath.Join(tmpDir, "ca-int.key")
- if _, err := os.Stat(issuerCert); os.IsNotExist(err) {
+ if _, err := os.Stat(issuerCert); errors.Is(err, fs.ErrNotExist) {
if ci.IsRoot {
issuerCert = ""
} else {
- return errors.New("file does not contain ca-int.pem")
+ altCert := filepath.Join(tmpDir, "test-ca.pem")
+ if _, err = os.Stat(altCert); err == nil {
+ if _, err := exeCmd("mv " + altCert + " " + issuerCert); err != nil {
+ return err
+ }
+ }
+
+ if _, err := os.Stat(issuerCert); errors.Is(err, fs.ErrNotExist) {
+ return errors.New("file does not contain ca-int.pem")
+ }
}
}
- if _, err := os.Stat(issuerKey); os.IsNotExist(err) {
+ if _, err := os.Stat(issuerKey); errors.Is(err, fs.ErrNotExist) {
if ci.IsRoot || wasCSR {
issuerKey = ""
} else {
- return errors.New("file does not contain ca-int.key")
+ altKey := filepath.Join(tmpDir, "test-ca.key")
+ if _, err = os.Stat(altKey); err == nil {
+ if _, err := exeCmd("mv " + altKey + " " + issuerKey); err != nil {
+ return err
+ }
+ }
+
+ if _, err := os.Stat(issuerKey); errors.Is(err, fs.ErrNotExist) {
+ return errors.New("file does not contain ca-int.key")
+ }
}
}
@@ -598,6 +664,29 @@ func (ci *CertificateInfo) Extract(path string, certBase string, tmpDir string,
return err
}
+ // Extract enddate to determine what the default CRL validity should be
+ if ci.IsRoot {
+ certFile := path + filepath.Base(rootCert)
+ read, err := os.ReadFile(certFile)
+ if err != nil {
+ fmt.Println(err)
+ return errors.New("could not read '" + certFile + "': " + err.Error())
+ }
+ block, _ := pem.Decode(read)
+ if block == nil || block.Type != "CERTIFICATE" {
+ fmt.Println(block)
+ return errors.New("failed to decode PEM block containing certificate")
+ }
+ crt, err := x509.ParseCertificate(block.Bytes)
+ if err != nil {
+ return err
+ }
+ numDays := time.Until(crt.NotAfter).Hours() / 24
+ if err := updateRootCRLDays("data/openssl.cnf", int(math.Ceil(numDays))); err != nil {
+ return err
+ }
+ }
+
return nil
}
@@ -646,7 +735,7 @@ func (ci *CertificateInfo) Create(path string, certBase string, wasCSR bool) err
return fmt.Errorf("unknown CreateType")
}
- // This is shared between pfx/zip upload and pem text upload
+ // This is shared between pfx/zip import and pem text upload
if ci.CreateType != "generate" {
err := ci.Extract(path, certBase, tmpDir, wasCSR)
if err != nil {
@@ -660,7 +749,7 @@ func (ci *CertificateInfo) Create(path string, certBase string, wasCSR bool) err
if ci.IsRoot {
keyFileExists := true
- if _, err := os.Stat(path + certBase + ".key"); os.IsNotExist(err) {
+ if _, err := os.Stat(path + certBase + ".key"); errors.Is(err, fs.ErrNotExist) {
keyFileExists = false
}
if keyFileExists {
@@ -687,6 +776,48 @@ func postCreateTasks(path string, certBase string, isRoot bool) error {
return nil
}
+func storeRootKey(path, certName, tmpDir, keyData, passphrase string) (bool, string) {
+ tmpKey := filepath.Join(tmpDir, certName+".key")
+
+ if err := os.WriteFile(tmpKey, []byte(keyData), 0600); err != nil {
+ return false, err.Error()
+ }
+
+ if passphrase != "" {
+ pwd := "pass:" + strings.Replace(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 false, "Incorrect password"
+ }
+
+ return false, "Unable to load Root CA key"
+ }
+
+ if _, err := exeCmd("mv " + tmpKey + "-out " + tmpKey); err != nil {
+ return false, err.Error()
+ }
+ }
+
+ modKey, err := exeCmd("openssl rsa -noout -modulus -in " + tmpKey)
+ if err != nil {
+ return false, "Not a private key"
+ }
+ modCert, err := exeCmd("openssl x509 -noout -modulus -in " + path + certName + ".pem")
+ if err != nil {
+ return false, "Unable to load Root CA certificate"
+ }
+ if string(modKey) != string(modCert) {
+ return false, "Key does not match the Root CA certificate"
+ }
+
+ if _, err := exeCmd("mv " + tmpKey + " " + path); err != nil {
+ return false, err.Error()
+ }
+
+ return true, ""
+}
+
func (ci *CertificateInfo) StoreRootKey(path string) bool {
if ci.Errors == nil {
ci.Errors = make(map[string]string)
@@ -704,54 +835,13 @@ func (ci *CertificateInfo) StoreRootKey(path string) bool {
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()
+ certBase := "root-ca"
+ if res, newError := storeRootKey(path, certBase, tmpDir, ci.Key, ci.Passphrase); !res {
+ ci.Errors["Modal"] = newError
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
@@ -808,6 +898,147 @@ func (ci *CertificateInfo) StoreCRL(path string) bool {
return true
}
+func renewCertificate(certname string, days int, rootname string, rootkeyfile string, passphrase string) error {
+ certFile := locateFile(certname + ".pem")
+ path := filepath.Dir(certFile) + "/"
+ certBase := path + certname
+ keyFile := certBase + ".key"
+ rootCert := ""
+ rootKey := keyFile
+
+ if strings.HasPrefix(certname, "ca-int") || strings.HasPrefix(certname, "test-ca") {
+ rootCert = locateFile(rootname + ".pem")
+ rootKey = locateFile(rootname + ".key")
+
+ // Make sure openssl allows us to add certificates with the same subject
+ attrFile := "data/index.txt.attr"
+ read, err := os.ReadFile(attrFile)
+ if err != nil {
+ fmt.Println(err)
+ return errors.New("could not read index.txt.attr file: " + err.Error())
+ }
+ re := regexp.MustCompile(`unique_subject = yes`)
+ res := re.ReplaceAll(read, []byte("unique_subject = no"))
+
+ if string(res) != string(read) {
+ if err = os.WriteFile(attrFile, res, 0640); err != nil {
+ fmt.Println(err)
+ return errors.New("could not write index.txt.attr file: " + err.Error())
+ }
+ }
+ }
+
+ tmpDir, err := os.MkdirTemp("", "labca")
+ if err != nil {
+ return err
+ }
+
+ defer os.RemoveAll(tmpDir)
+
+ if _, err := os.Stat(rootKey); errors.Is(err, fs.ErrNotExist) {
+ if rootkeyfile == "" {
+ return errors.New("NO_ROOT_KEY")
+ } else {
+ if res, newError := storeRootKey(path, certname, tmpDir, rootkeyfile, passphrase); !res {
+ return errors.New("NO_ROOT_KEY:" + newError)
+ }
+ defer exeCmd("rm " + rootKey)
+ }
+ }
+
+ r, err := exeCmd("openssl x509 -noout -subject -nameopt utf8 -in " + certFile)
+ if err != nil {
+ return err
+ }
+ subject := string(r[8 : len(r)-1])
+ subject = "/" + strings.ReplaceAll(subject, ", ", "/")
+ subject = strings.Replace(subject, " ", "\\\\", -1)
+
+ if rootKey == keyFile {
+ if _, err := exeCmd("openssl req -config data/openssl.cnf -days " + strconv.Itoa(days) + " -new -utf8 -x509 -extensions v3_ca -subj " + subject +
+ " -key " + keyFile + " -out " + certFile + ".tmp"); err != nil {
+ return reportError(err)
+ }
+
+ if err := updateRootCRLDays("data/openssl.cnf", days); err != nil {
+ return reportError(err)
+ }
+
+ if _, err := exeCmd("openssl ca -config data/openssl.cnf -gencrl -keyfile " + keyFile + " -cert " + certFile + ".tmp -out " + certBase + ".crl"); err != nil {
+ return reportError(err)
+ }
+
+ } else {
+ if _, err := exeCmd("openssl req -config data/issuer/openssl.cnf -new -utf8 -subj " + subject + " -key " + keyFile + " -out " + certBase + ".csr"); err != nil {
+ return reportError(err)
+ }
+ if out, err := exeCmd("openssl ca -config data/openssl.cnf -cert " + rootCert + " -keyfile " + rootKey + " -extensions v3_intermediate_ca -days " +
+ strconv.Itoa(days) + " -md sha384 -notext -batch -in " + certBase + ".csr -out " + certFile + ".tmp"); err != nil {
+ if strings.Contains(string(out), ".key for reading, No such file or directory") {
+ fmt.Println(out)
+ return errors.New("NO_ROOT_KEY")
+ }
+ return reportError(err)
+ }
+
+ }
+ if _, err := exeCmd("mv " + certFile + ".tmp " + certFile); err != nil {
+ return reportError(err)
+ }
+
+ // TODO: need to get rid of this!
+ if rootKey == keyFile {
+ if strings.HasPrefix(certname, "test-root") {
+ dataFile := locateFile("root-ca.pem")
+ if _, err := exeCmd("cp " + certFile + " " + dataFile); err != nil {
+ fmt.Println(err)
+ }
+ dataKeyFile := strings.TrimSuffix(dataFile, filepath.Ext(dataFile)) + ".key"
+ if _, err := exeCmd("cp " + keyFile + " " + dataKeyFile); err != nil {
+ fmt.Println(err)
+ }
+ crlFile := strings.TrimSuffix(dataFile, filepath.Ext(dataFile)) + ".crl"
+ if _, err := exeCmd("cp " + certBase + ".crl " + crlFile); err != nil {
+ fmt.Println(err)
+ }
+ }
+ } else {
+ if strings.HasPrefix(certname, "test-ca") {
+ dataFile := locateFile("ca-int.pem")
+ if _, err := exeCmd("cp " + certFile + " " + dataFile); err != nil {
+ fmt.Println(err)
+ }
+ dataKeyFile := strings.TrimSuffix(dataFile, filepath.Ext(dataFile)) + ".key"
+ if _, err := exeCmd("cp " + keyFile + " " + dataKeyFile); err != nil {
+ fmt.Println(err)
+ }
+ }
+ }
+
+ return nil
+}
+
+func locateFileIn(path, name string) string {
+ if _, err := os.Stat(path + name); err == nil {
+ return path + name
+ }
+ return ""
+}
+
+// TODO: sort out the file naming/locations properly to not need this and be future proof!!
+// Most is found in /opt/boulder/ and some in /opt/boulder/labca/
+func locateFile(name string) string {
+
+ for _, path := range []string{"", "data/", "data/issuer", "/go/src/labca/data/", "/go/src/labca/data/issuer/", "labca/", "/opt/boulder/", "/opt/boulder/labca/"} {
+ if res := locateFileIn(path, name); res != "" {
+ return res
+ }
+ }
+
+ fmt.Printf("WARNING: could not find '%s'!\n", name)
+ return ""
+}
+
func exeCmd(cmd string) ([]byte, error) {
parts := strings.Fields(cmd)
for i := 0; i < len(parts); i++ {
@@ -821,4 +1052,4 @@ func exeCmd(cmd string) ([]byte, error) {
fmt.Print(fmt.Sprint(err) + ": " + string(out))
}
return out, err
-}
+}
\ No newline at end of file
diff --git a/gui/chains.go b/gui/chains.go
new file mode 100644
index 0000000..3e00bc8
--- /dev/null
+++ b/gui/chains.go
@@ -0,0 +1,339 @@
+package main
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ "github.com/spf13/viper"
+)
+
+const caaConfFile = "/opt/boulder/labca/config/ca-a.json"
+const cabConfFile = "/opt/boulder/labca/config/ca-b.json"
+const wfeConfFile = "/opt/boulder/labca/config/wfe2.json"
+
+// From boulder: cmd/boulder-wfe2/main.go
+type WFEConfig struct {
+ WFE struct {
+ Chains [][]string `validate:"required,min=1,dive,min=2,dive,required"`
+ }
+}
+
+// From boulder: issuance/issuer.go
+type IssuerLoc struct {
+ ConfigFile string `validate:"required_without_all=PKCS11 File" json:"configFile"`
+ CertFile string `validate:"required" json:"certFile,omitempty"`
+ NumSessions int `json:"numSessions"`
+}
+
+// From boulder: issuance/issuer.go
+type IssuerConfig struct {
+ UseForRSALeaves bool `json:"useForRSALeaves"`
+ UseForECDSALeaves bool `json:"useForECDSALeaves"`
+
+ IssuerURL string `validate:"required,url" json:"issuerURL,omitempty"`
+ OCSPURL string `validate:"required,url" json:"ocspURL,omitempty"`
+ CRLURL string `validate:"omitempty,url" json:"crlURL,omitempty"`
+
+ Location IssuerLoc `json:"location,omitempty"`
+}
+
+// From boulder: cmd/boulder-ca/main.go but deconstructed
+type Issuance struct {
+ Issuers []IssuerConfig `validate:"min=1,dive" json:"issuers"`
+}
+type CA struct {
+ Issuance Issuance `json:"issuance"`
+}
+type CAConfig struct {
+ CA CA `json:"ca"`
+}
+
+// CertDetails contains info about each certificate for use in the GUI
+type CertDetails struct {
+ CertFile string
+ BaseName string
+ Subject string
+ IsRoot bool
+ UseForRSA bool
+ UseForECDSA bool
+ NotAfter string
+ Details string
+}
+
+type CertChain struct {
+ RootCert CertDetails
+ IssuerCerts []CertDetails
+}
+
+func getCertFileDetails(certFile string) (string, error) {
+ var details string
+
+ res, err := exeCmd("openssl x509 -noout -text -nameopt utf8 -in " + certFile)
+ if err != nil {
+ fmt.Println("cannot get details from '" + certFile + "': " + fmt.Sprint(err))
+ return "", err
+ }
+ details = string(res)
+
+ return details, nil
+}
+
+func getCertFileNotAFter(certFile string) (string, error) {
+ var notafter string
+
+ res, err := exeCmd("openssl x509 -noout -enddate -nameopt utf8 -in " + certFile)
+ if err != nil {
+ fmt.Println("cannot get enddate from '" + certFile + "': " + fmt.Sprint(err))
+ return "", err
+ }
+ if len(res) <= 9 {
+ fmt.Println("enddate of '" + certFile + "'does not start with 'notAfter='")
+ return "", errors.New("enddate of '" + certFile + "'does not start with 'notAfter='")
+ }
+ notafter = string(res[9 : len(res)-1])
+ return notafter, nil
+}
+
+func getCertFileSubject(certFile string) (string, error) {
+ var subject string
+
+ res, err := exeCmd("openssl x509 -noout -subject -nameopt utf8 -in " + certFile)
+ if err != nil {
+ fmt.Println("cannot get subject from '" + certFile + "': " + fmt.Sprint(err))
+ return "", err
+ }
+ if len(res) <= 8 {
+ fmt.Println("subject of '" + certFile + "'does not start with 'subject='")
+ return "", errors.New("subject of '" + certFile + "'does not start with 'subject='")
+ }
+ subject = string(res[8 : len(res)-1])
+ return subject, nil
+}
+
+func getRawCAChains() []IssuerConfig {
+ caConf, err := os.Open(caaConfFile)
+ if err != nil {
+ fmt.Println(err)
+ return nil
+ }
+ defer caConf.Close()
+
+ byteValue, _ := io.ReadAll(caConf)
+
+ var result CAConfig
+ json.Unmarshal([]byte(byteValue), &result)
+
+ return result.CA.Issuance.Issuers
+}
+
+func enhanceChains(chains []CertChain) []CertChain {
+ rawChains := getRawCAChains()
+
+ for i := 0; i < len(rawChains); i++ {
+ for k := 0; k < len(chains); k++ {
+ for n := 0; n < len(chains[k].IssuerCerts); n++ {
+ if chains[k].IssuerCerts[n].CertFile == rawChains[i].Location.CertFile {
+ chains[k].IssuerCerts[n].UseForRSA = rawChains[i].UseForRSALeaves
+ chains[k].IssuerCerts[n].UseForECDSA = rawChains[i].UseForECDSALeaves
+ certFile := locateFile(rawChains[i].Location.CertFile)
+ if d, err := getCertFileDetails(certFile); err == nil {
+ chains[k].IssuerCerts[n].Details = d
+ }
+ if na, err := getCertFileNotAFter(certFile); err == nil {
+ chains[k].IssuerCerts[n].NotAfter = na
+ }
+ if s, err := getCertFileSubject(certFile); err == nil {
+ chains[k].IssuerCerts[n].Subject = s
+ }
+ }
+ }
+
+ if chains[k].RootCert.Subject == "" {
+ certFile := locateFile(chains[k].RootCert.CertFile)
+ if d, err := getCertFileDetails(certFile); err == nil {
+ chains[k].RootCert.Details = d
+ }
+ if na, err := getCertFileNotAFter(certFile); err == nil {
+ chains[k].RootCert.NotAfter = na
+ }
+ if s, err := getCertFileSubject(certFile); err == nil {
+ chains[k].RootCert.Subject = s
+ }
+ }
+ }
+ }
+
+ return chains
+}
+
+func getRawWFEChains() [][]string {
+ wfeConf, err := os.Open(wfeConfFile)
+ if err != nil {
+ fmt.Println(err)
+ return nil
+ }
+ defer wfeConf.Close()
+
+ byteValue, _ := io.ReadAll(wfeConf)
+
+ var result WFEConfig
+ json.Unmarshal([]byte(byteValue), &result)
+
+ return result.WFE.Chains
+}
+
+func getChains() []CertChain {
+ var chains []CertChain
+
+ rawChains := getRawWFEChains()
+
+ for i := 0; i < len(rawChains); i++ {
+ chain := rawChains[i]
+ issuer := chain[0]
+ root := chain[1]
+
+ var certChain CertChain
+ cIdx := -1
+ for k := 0; k < len(chains); k++ {
+ if chains[k].RootCert.CertFile == root {
+ certChain = chains[k]
+ cIdx = k
+ }
+ }
+
+ if cIdx < 0 {
+ base := filepath.Base(root)
+ base = strings.TrimSuffix(base, filepath.Ext(base))
+ certChain = CertChain{RootCert: CertDetails{
+ CertFile: root,
+ BaseName: base,
+ IsRoot: true,
+ }}
+ chains = append(chains, certChain)
+ cIdx = len(chains) - 1
+ }
+
+ base := filepath.Base(issuer)
+ base = strings.TrimSuffix(base, filepath.Ext(base))
+ certChain.IssuerCerts = append(certChain.IssuerCerts, CertDetails{
+ CertFile: issuer,
+ BaseName: base,
+ IsRoot: false,
+ })
+
+ chains[cIdx] = certChain
+ }
+
+ chains = enhanceChains(chains)
+
+ return chains
+}
+
+func setUseForLeavesFile(filename, forRSA, forECDSA string) error {
+ caConf, err := os.Open(filename)
+ if err != nil {
+ fmt.Println(err)
+ return errors.New("could not open config file: " + err.Error())
+ }
+ defer caConf.Close()
+
+ byteValue, _ := io.ReadAll(caConf)
+
+ var result CAConfig
+ if err = json.Unmarshal([]byte(byteValue), &result); err != nil {
+ return errors.New("could not parse config file: " + err.Error())
+ }
+
+ // Make sure that the named certificate(s) exist
+ foundRSA := false
+ foundECDSA := false
+ for i := 0; i < len(result.CA.Issuance.Issuers); i++ {
+ if strings.Contains(result.CA.Issuance.Issuers[i].Location.CertFile, forRSA) {
+ foundRSA = true
+ }
+ if strings.Contains(result.CA.Issuance.Issuers[i].Location.CertFile, forECDSA) {
+ foundECDSA = true
+ }
+ }
+ if !foundRSA {
+ return errors.New("certificate '" + forRSA + "' not found in ca file")
+ }
+ if !foundECDSA {
+ return errors.New("certificate '" + forECDSA + "' not found in ca file")
+ }
+
+ // Now set the flags for the named certificate(s)
+ for i := 0; i < len(result.CA.Issuance.Issuers); i++ {
+ if forRSA != "" {
+ result.CA.Issuance.Issuers[i].UseForRSALeaves = strings.Contains(result.CA.Issuance.Issuers[i].Location.CertFile, forRSA)
+ }
+ if forECDSA != "" {
+ result.CA.Issuance.Issuers[i].UseForECDSALeaves = strings.Contains(result.CA.Issuance.Issuers[i].Location.CertFile, forECDSA)
+ }
+ }
+
+ // Write the modified data back to file, using regex magic to replace only the issuers list...
+ if jsonString, err := json.MarshalIndent(result, "", "\t"); err == nil {
+ re := regexp.MustCompile(`(?s).*"issuers": \[(.*?)\s*\].*`)
+ iss := re.ReplaceAll(jsonString, []byte("$1"))
+
+ read, err := os.ReadFile(filename)
+ if err != nil {
+ fmt.Println(err)
+ return errors.New("could not read config file: " + err.Error())
+ }
+ re = regexp.MustCompile(`(?s)(\s*"issuers": \[).*?(\s*\])`)
+ res := re.ReplaceAll(read, []byte("$1"+string(iss)+"$2"))
+
+ if err = os.WriteFile(filename, res, 0640); err != nil {
+ fmt.Println(err)
+ return errors.New("could not write config file: " + err.Error())
+ }
+ } else {
+ return errors.New("could not convert json data: " + err.Error())
+ }
+
+ return nil
+}
+
+func setUseForLeaves(forRSA, forECDSA string) error {
+ if err := exec.Command("cp", "-f", caaConfFile, caaConfFile+"_BAK").Run(); err != nil {
+ return errors.New("could not create ca-a backup file: " + err.Error())
+ }
+ if err := exec.Command("cp", "-f", cabConfFile, cabConfFile+"_BAK").Run(); err != nil {
+ return errors.New("could not create ca-b backup file: " + err.Error())
+ }
+
+ if err := setUseForLeavesFile(caaConfFile, forRSA, forECDSA); err != nil {
+ exec.Command("mv", caaConfFile+"_BAK", caaConfFile).Run()
+ exec.Command("mv", cabConfFile+"_BAK", cabConfFile).Run()
+ return err
+ }
+ if err := setUseForLeavesFile(cabConfFile, forRSA, forECDSA); err != nil {
+ exec.Command("mv", caaConfFile+"_BAK", caaConfFile).Run()
+ exec.Command("mv", cabConfFile+"_BAK", cabConfFile).Run()
+ return err
+ }
+
+ exec.Command("rm", caaConfFile+"_BAK").Run()
+ exec.Command("rm", cabConfFile+"_BAK").Run()
+
+ if forRSA != "" {
+ viper.Set("certs.issuerRSA", forRSA)
+ }
+ if forECDSA != "" {
+ viper.Set("certs.issuerECDSA", forECDSA)
+ }
+ if forRSA != "" || forECDSA != "" {
+ viper.WriteConfig()
+ }
+
+ return nil
+}
\ No newline at end of file
diff --git a/gui/main.go b/gui/main.go
index 9268973..aa6c2b3 100644
--- a/gui/main.go
+++ b/gui/main.go
@@ -18,7 +18,7 @@ import (
"fmt"
"html/template"
"io"
- "io/ioutil"
+ "io/fs"
"log"
"math"
"math/big"
@@ -497,6 +497,8 @@ func _sendCmdOutput(w http.ResponseWriter, r *http.Request, cmd string) {
out, err := exec.Command(head, parts...).Output()
if err != nil {
+ fmt.Println(err)
+ fmt.Println(out)
errorHandler(w, r, err, http.StatusInternalServerError)
return
}
@@ -963,49 +965,31 @@ func _emailSendHandler(w http.ResponseWriter, r *http.Request) {
}
func _exportHandler(w http.ResponseWriter, r *http.Request) {
- basename := "certificates"
- if r.Form.Get("root") != "true" {
- basename = "issuer"
- }
- if r.Form.Get("issuer") != "true" {
- basename = "root"
- }
+ certname := r.Form.Get("certname")
+
+ certFile := locateFile(certname + ".pem")
+ keyFile := strings.TrimSuffix(certFile, filepath.Ext(certFile)) + ".key"
if r.Form.Get("type") == "pfx" {
w.Header().Set("Content-Type", "application/x-pkcs12")
- w.Header().Set("Content-Disposition", "attachment; filename=labca_"+basename+".pfx")
+ w.Header().Set("Content-Disposition", "attachment; filename=labca_"+certname+".pfx")
- var certBase string
- if basename == "root" {
- certBase = "data/root-ca"
- } else {
- certBase = "data/issuer/ca-int"
- }
-
- cmd := "openssl pkcs12 -export -inkey " + certBase + ".key -in " + certBase + ".pem -passout pass:" + r.Form.Get("export-pwd")
+ cmd := "openssl pkcs12 -export -inkey " + keyFile + " -in " + certFile + " -passout pass:" + r.Form.Get("export-pwd")
_sendCmdOutput(w, r, cmd)
}
if r.Form.Get("type") == "zip" {
w.Header().Set("Content-Type", "application/zip")
- w.Header().Set("Content-Disposition", "attachment; filename=labca_"+basename+".zip")
+ w.Header().Set("Content-Disposition", "attachment; filename=labca_"+certname+".zip")
- cmd := "zip -j -P " + r.Form.Get("export-pwd") + " - "
- var certBase string
- if r.Form.Get("root") == "true" {
- certBase = "data/root-ca"
- cmd = cmd + certBase + ".key " + certBase + ".pem "
- }
- if r.Form.Get("issuer") == "true" {
- certBase = "data/issuer/ca-int"
- cmd = cmd + certBase + ".key " + certBase + ".pem "
- }
+ cmd := "zip -j -P " + r.Form.Get("export-pwd") + " - " + keyFile + " " + certFile
_sendCmdOutput(w, r, cmd)
}
}
+/*
func _doCmdOutput(w http.ResponseWriter, r *http.Request, cmd string) string {
parts := strings.Fields(cmd)
for i := 0; i < len(parts); i++ {
@@ -1022,6 +1006,7 @@ func _doCmdOutput(w http.ResponseWriter, r *http.Request, cmd string) string {
return string(out)
}
+*/
func _encrypt(plaintext []byte) (string, error) {
key := []byte(viper.GetString("keys.enc"))
@@ -1126,7 +1111,7 @@ func generateCRLHandler(w http.ResponseWriter, r *http.Request, isRoot bool) {
path := "data/"
certBase := "root-ca"
keyFileExists := true
- if _, err := os.Stat(path + certBase + ".key"); os.IsNotExist(err) {
+ if _, err := os.Stat(path + certBase + ".key"); errors.Is(err, fs.ErrNotExist) {
keyFileExists = false
}
if keyFileExists {
@@ -1155,13 +1140,13 @@ func generateCRLHandler(w http.ResponseWriter, r *http.Request, isRoot bool) {
}
// Remove the Root Key if we want to keep it offline
if viper.GetBool("keep_root_offline") {
- if _, err := os.Stat(path + certBase + ".key"); !os.IsNotExist(err) {
+ 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"); !os.IsNotExist(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)
}
@@ -1205,6 +1190,63 @@ func uploadCRLHandler(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(res)
}
+func updateLeaveIssuersHandler(w http.ResponseWriter, r *http.Request) {
+ res := struct {
+ Success bool
+ Error string
+ }{Success: true}
+
+ if err := setUseForLeaves(r.Form.Get("rsa"), r.Form.Get("ecdsa")); err != nil {
+ res.Success = false
+ res.Error = err.Error()
+ } else {
+ defer func() {
+ if !_hostCommand(w, r, "boulder-restart") {
+ log.Printf("updateLeaveIssuersHandler: error restarting boulder: %v", err)
+ }
+ }()
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(res)
+}
+
+func renewCertHandler(w http.ResponseWriter, r *http.Request) {
+ res := struct {
+ Success bool
+ Error string
+ }{Success: true}
+
+ days, err := strconv.Atoi(r.Form.Get("days"))
+ if err != nil {
+ fmt.Printf("'%v' is not a number", r.Form.Get("days"))
+ errorHandler(w, r, err, http.StatusBadRequest)
+ return
+ }
+
+ if err := renewCertificate(r.Form.Get("certname"), days, r.Form.Get("rootname"), r.Form.Get("root_key"), r.Form.Get("passphrase")); err != nil {
+ res.Success = false
+ res.Error = err.Error()
+ }
+
+ ex, _ := os.Executable()
+ exePath := filepath.Dir(ex)
+ path, _ := filepath.Abs(exePath + "/..")
+ if _, err := exeCmd(path + "/apply"); err != nil {
+ fmt.Println(err)
+ res.Success = false
+ res.Error = "Could not apply: " + err.Error()
+ }
+
+ if !_hostCommand(w, r, "boulder-restart") {
+ res.Success = false
+ res.Error = "Error restarting Boulder (ACME)"
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(res)
+}
+
func _managePostDispatch(w http.ResponseWriter, r *http.Request, action string) bool {
if action == "backup-restore" || action == "backup-delete" || action == "backup-now" || action == "backup-upload" {
_backupHandler(w, r)
@@ -1266,6 +1308,16 @@ func _managePostDispatch(w http.ResponseWriter, r *http.Request, action string)
return true
}
+ if action == "update-leave-issuers" {
+ updateLeaveIssuersHandler(w, r)
+ return true
+ }
+
+ if action == "renew-cert" {
+ renewCertHandler(w, r)
+ return true
+ }
+
if action == "svc-restart" {
if _, err := exeCmd("./restart_control"); err != nil {
log.Printf("_managePostDispatch: error restarting control container: %v", err)
@@ -1319,6 +1371,8 @@ func _managePost(w http.ResponseWriter, r *http.Request) {
"upload-root-crl",
"gen-root-crl",
"gen-issuer-crl",
+ "update-leave-issuers",
+ "renew-cert",
} {
if a == action {
actionKnown = true
@@ -1509,8 +1563,8 @@ func _manageGet(w http.ResponseWriter, r *http.Request) {
backupFiles = backupFiles[:len(backupFiles)-1]
manageData["BackupFiles"] = backupFiles
- manageData["RootDetails"] = _doCmdOutput(w, r, "openssl x509 -noout -text -nameopt utf8 -in data/root-ca.pem")
- manageData["IssuerDetails"] = _doCmdOutput(w, r, "openssl x509 -noout -text -nameopt utf8 -in data/issuer/ca-int.pem")
+ chains := getChains()
+ manageData["CertificateChains"] = chains
if viper.Get("crl_interval") == nil || viper.GetString("crl_interval") == "" {
manageData["CRLInterval"] = "24h"
@@ -1575,6 +1629,54 @@ func manageHandler(w http.ResponseWriter, r *http.Request) {
}
}
+/*
+func manageNewRootHandler(w http.ResponseWriter, r *http.Request) {
+ if !viper.GetBool("config.complete") {
+ http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/setup", http.StatusFound)
+ return
+ }
+
+ // TODO: dynamically determine next filename (root-ca-2, root-ca-3, etc.)
+
+ if !_certCreate(w, r, "root-ca-3", true) {
+ // Cleanup the cert (if it even exists) so we will retry on the next run
+ if _, err := os.Stat("data/root-ca-3.pem"); !errors.Is(err, fs.ErrNotExist) {
+ exeCmd("mv data/root-ca-3.pem data/root-ca-3.pem_TMP")
+ }
+ return
+ }
+
+ // TODO: actually add the newly created key to the relevant config files (ca-a, ca-b, wfe2, possibly others)
+
+ // TODO: reload boulder!
+
+ http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/manage#certs", http.StatusSeeOther)
+}
+
+func manageNewIssuerHandler(w http.ResponseWriter, r *http.Request) {
+ if !viper.GetBool("config.complete") {
+ http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/setup", http.StatusFound)
+ return
+ }
+
+ // TODO: dynamically determine next filename (ca-int-2, ca-int-3, etc.)
+
+ // Is revertroot at all relevant in this scenario?
+
+ if !_certCreate(w, r, "ca-int-3", false) {
+ // Cleanup the cert (if it even exists) so we will retry on the next run
+ os.Remove("data/issuer/ca-int-3.pem")
+ return
+ }
+
+ // TODO: actually add the newly created key to the relevant config files (ca-a, ca-b, wfe2, possibly others)
+
+ // TODO: reload boulder!
+
+ http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/manage#certs", http.StatusSeeOther)
+}
+*/
+
func logsHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
logType := vars["type"]
@@ -1749,9 +1851,11 @@ func _buildCI(r *http.Request, session *sessions.Session, isRoot bool) *Certific
CreateType: "generate",
CommonName: "Root CA",
RequestBase: r.Header.Get("X-Request-Base"),
+ NumDays: 3652, // 10 years
}
if !isRoot {
ci.CommonName = "CA"
+ ci.NumDays = 1826 // 5 years
}
ci.Initialize()
@@ -1834,7 +1938,7 @@ func _certCreate(w http.ResponseWriter, r *http.Request, certBase string, isRoot
// 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) {
+ if errors.Is(errPem, fs.ErrNotExist) && !errors.Is(errTmp, fs.ErrNotExist) {
exeCmd("mv data/root-ca.pem_TMP data/root-ca.pem")
}
@@ -1848,11 +1952,54 @@ func _certCreate(w http.ResponseWriter, r *http.Request, certBase string, isRoot
path = path + "issuer/"
}
- if _, err := os.Stat(path + certBase + ".pem"); os.IsNotExist(err) {
+ if _, err := os.Stat(path + certBase + ".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") {
+ ci.IsFirst = true
+ } else if !isRoot && (certBase == "ca-int" || certBase == "test-ca") {
+ 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())
+ errorHandler(w, r, err, http.StatusInternalServerError)
+ return false
+ }
+
+ ci.RootSubject, err = getCertFileSubject(certFile)
+ if err != nil {
+ fmt.Println(err.Error())
+ errorHandler(w, r, err, http.StatusInternalServerError)
+ return false
+ }
+ subjectMap := parseSubjectDn(ci.RootSubject)
+ if val, ok := subjectMap["C"]; ok {
+ ci.Country = val
+ }
+ if val, ok := subjectMap["O"]; ok {
+ ci.Organization = val
+ }
+ } else if !isRoot {
+ certFile := locateFile("root-ca.pem")
+ ci.RootEnddate, err = getCertFileNotAFter(certFile)
+ if err != nil {
+ fmt.Println(err.Error())
+ errorHandler(w, r, err, http.StatusInternalServerError)
+ return false
+ }
+ ci.RootSubject, err = getCertFileSubject(certFile)
+ if err != nil {
+ fmt.Println(err.Error())
+ errorHandler(w, r, err, http.StatusInternalServerError)
+ return false
+ }
+ }
render(w, r, "cert:manage", map[string]interface{}{"CertificateInfo": ci, "Progress": _progress(certBase), "HelpText": _helptext(certBase)})
return false
@@ -1877,6 +2024,19 @@ func _certCreate(w http.ResponseWriter, r *http.Request, certBase string, isRoot
ci.OrgUnit = r.Form.Get("ou")
ci.CommonName = r.Form.Get("cn")
+ ci.RootEnddate = r.Form.Get("root-enddate")
+ ci.RootSubject = r.Form.Get("root-subject")
+ if r.Form.Get("numdays") != "" {
+ ci.NumDays, err = strconv.Atoi(r.Form.Get("numdays"))
+ if err != nil {
+ if ci.IsRoot {
+ ci.NumDays = 3652
+ } else {
+ ci.NumDays = 1826
+ }
+ }
+ }
+
if ci.CreateType == "import" {
file, handler, err := r.FormFile("import")
if err != nil {
@@ -1934,7 +2094,7 @@ func _certCreate(w http.ResponseWriter, r *http.Request, certBase string, isRoot
return false
}
defer csr.Close()
- b, err := ioutil.ReadAll(csr)
+ b, _ := io.ReadAll(csr)
render(w, r, "cert:manage", map[string]interface{}{"CertificateInfo": ci, "CSR": string(b), "Progress": _progress(certBase), "HelpText": _helptext(certBase)})
return false
@@ -1973,7 +2133,7 @@ func _certCreate(w http.ResponseWriter, r *http.Request, certBase string, isRoot
return false
}
defer csr.Close()
- b, err := ioutil.ReadAll(csr)
+ b, _ := io.ReadAll(csr)
session.Values["csr"] = true
if err = session.Save(r, w); err != nil {
@@ -2002,13 +2162,13 @@ func _certCreate(w http.ResponseWriter, r *http.Request, certBase string, isRoot
}
if viper.GetBool("keep_root_offline") {
- if _, err := os.Stat(path + "../root-ca.key"); !os.IsNotExist(err) {
+ 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"); !os.IsNotExist(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)
}
@@ -2040,7 +2200,7 @@ func _certCreate(w http.ResponseWriter, r *http.Request, certBase string, isRoot
return false
}
defer key.Close()
- b, err := ioutil.ReadAll(key)
+ 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
@@ -2160,9 +2320,9 @@ func _helptext(stage string) template.HTML {
if stage == "register" {
return template.HTML(fmt.Sprint("You need to create an admin account for\n",
"managing this instance of LabCA. There can only be one admin account, but you can configure all\n",
- "its attributes once the initial setup has completed.
Instead, you can also\n",
+ "its attributes once the initial setup has completed.
Instead, you can also\n",
"restore from a backup file of a\n",
- "previous LabCA installation.
\n",
+ "previous LabCA installation.
\n",
"If you have a backup file from a previous LabCA installation and want to\n",
"restore this instance with the exact same configuration, use that backup file here.\n",
"
Otherwise you should follow the textarea {
- width: 600px;
+ width: 660px;
height: 550px;
}
@@ -197,3 +209,16 @@ pre.json {
.hide-overflow {
overflow: hidden;
}
+
+#modal-spinner {
+ z-index: 10000;
+}
+
+button.close {
+ margin-top: -40px;
+}
+
+.form-small-inline {
+ display: inline-block;
+ width: 5em !important;
+}
\ No newline at end of file
diff --git a/gui/templates/views/cert.tmpl b/gui/templates/views/cert.tmpl
index 6ab86c9..ec5a770 100644
--- a/gui/templates/views/cert.tmpl
+++ b/gui/templates/views/cert.tmpl
@@ -3,6 +3,9 @@
{{with .CertificateInfo}}
{{ if .IsRoot }}Root{{ else }}Issuer (2nd level){{ end }} Certificate
+ {{ if not .IsRoot }}
+ Root Subject:
{{ .RootSubject }}
+ {{ end }}