Add way to renew (extend lifetime of) CA certificates (#74)

This commit is contained in:
Arjan H
2023-12-26 11:56:45 +01:00
parent 23bcde19f8
commit 33208bf347
14 changed files with 1535 additions and 247 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

339
gui/chains.go Normal file
View File

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

View File

@@ -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("<p class=\"form-register\">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.<br><br>Instead, you can also\n",
"its attributes once the initial setup has completed.<br><br><b>Instead, you can also\n",
"<a href=\"#\" onclick=\"false\" class=\"toggle-restore\">restore from a backup file</a> of a\n",
"previous LabCA installation.</p>\n",
"previous LabCA installation.</b></p>\n",
"<p class=\"form-restore\">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",
"<br><br>Otherwise you should follow the <a href=\"#\" onclick=\"false\"\n",
@@ -2544,7 +2704,7 @@ 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
if _, err := os.Stat("data/root-ca.pem"); !os.IsNotExist(err) {
if _, err := os.Stat("data/root-ca.pem"); !errors.Is(err, fs.ErrNotExist) {
exeCmd("mv data/root-ca.pem data/root-ca.pem_TMP")
}
return
@@ -3147,11 +3307,14 @@ func init() {
if *configFile == "" {
viper.SetConfigName("config")
viper.AddConfigPath("data")
configPath = "./data"
ex, _ := os.Executable()
exePath := filepath.Dir(ex)
path, _ := filepath.Abs(exePath + "/..")
configPath = path + "/data"
viper.AddConfigPath(configPath)
} else {
_, err := os.Stat(*configFile)
if os.IsNotExist(err) {
if errors.Is(err, fs.ErrNotExist) {
viper.WriteConfigAs(*configFile)
}
@@ -3260,6 +3423,27 @@ func init() {
listenAddress = fmt.Sprintf("%s:%d", a, p)
updateAvailable = false
/*
// TODO: Still needs to be done for this!
// Store boulder chains if we don't have them already
doWrite := false
if viper.GetString("certs.ca") == "" {
caChains := getRawCAChains()
viper.Set("certs.ca", caChains)
doWrite = true
}
if viper.GetString("certs.wfe") == "" {
chains := getRawWFEChains()
viper.Set("certs.wfe", chains)
doWrite = true
}
if doWrite {
viper.WriteConfig()
}
// TODO: also apply from here if different?? How exaclty is a code upgrade delaing with this??
*/
}
func main() {
@@ -3286,6 +3470,8 @@ func main() {
r.HandleFunc("/stats", statsHandler).Methods("GET")
r.HandleFunc("/about", aboutHandler).Methods("GET")
r.HandleFunc("/manage", manageHandler).Methods("GET", "POST")
// r.HandleFunc("/manage/newissuer", manageNewIssuerHandler).Methods("GET", "POST")
// r.HandleFunc("/manage/newroot", manageNewRootHandler).Methods("GET", "POST")
r.HandleFunc("/final", finalHandler).Methods("GET")
r.HandleFunc("/error", showErrorHandler).Methods("GET")
r.HandleFunc("/login", loginHandler).Methods("GET", "POST")
@@ -3329,9 +3515,11 @@ func main() {
r.PathPrefix("/accounts/static/").Handler(http.StripPrefix("/accounts/static/", sfs))
r.PathPrefix("/authz/static/").Handler(http.StripPrefix("/authz/static/", sfs))
r.PathPrefix("/challenges/static/").Handler(http.StripPrefix("/challenges/static/", sfs))
r.PathPrefix("/certs/static/").Handler(http.StripPrefix("/certs/static/", sfs))
r.PathPrefix("/certificates/static/").Handler(http.StripPrefix("/certificates/static/", sfs))
r.PathPrefix("/orders/static/").Handler(http.StripPrefix("/orders/static/", sfs))
r.PathPrefix("/logs/static/").Handler(http.StripPrefix("/logs/static/", sfs))
r.PathPrefix("/manage/static/").Handler(http.StripPrefix("/manage/static/", sfs))
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", sfs))
}
}
@@ -3350,4 +3538,4 @@ func main() {
} else {
log.Fatal(srv.ListenAndServe())
}
}
}

View File

@@ -13,7 +13,7 @@ fi
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y iproute2 zip
apt-get install -y iproute2 zip unzip
apt-get install -y apt-transport-https ca-certificates curl software-properties-common gnupg
install -m 0755 -d /etc/apt/keyrings
[ ! -e /etc/apt/keyrings/docker.gpg ] || mv /etc/apt/keyrings/docker.gpg /etc/apt/keyrings/docker.gpg_PREV

View File

@@ -124,6 +124,14 @@ td.pad-low-top {
width: 10em;
}
.btn-new-issuer {
width: 7em;
}
.btn-6em {
width: 6em;
}
.btn-right {
float: right;
}
@@ -132,6 +140,10 @@ td.pad-low-top {
vertical-align: middle !important;
}
.center {
text-align: center !important;
}
.bd-modal-lg .modal-dialog {
display: table;
position: relative;
@@ -146,11 +158,11 @@ td.pad-low-top {
.modal-content {
padding: 6px;
width: 614px;
width: 674px;
}
.modal-content > 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;
}

View File

@@ -3,6 +3,9 @@
<div class="col-md-6 col-sm-12">
{{with .CertificateInfo}}
<h3>{{ if .IsRoot }}<b>Root</b>{{ else }}<b>Issuer</b> (2nd level){{ end }} Certificate</h3>
{{ if not .IsRoot }}
Root Subject: <span id="root-subject-display">{{ .RootSubject }}</span><br><br>
{{ end }}
<ul class="nav nav-tabs">
<li class="{{ if eq .CreateType "generate" }}active{{ end }}">
@@ -19,13 +22,17 @@
<div class="tab-content">
<div class="tab-pane fade {{ if eq .CreateType "generate" }}active in{{ end }}" id="generate">
<br/>
<form role="form" class="form-cert" action="{{ .RequestBase }}/setup" enctype="multipart/form-data" method="POST">
<form role="form" class="form-cert" enctype="multipart/form-data" method="POST">
<input type="hidden" name="cert" value="{{ if .IsRoot }}root{{ else }}issuer{{ end }}">
<input type="hidden" name="createtype" value="generate">
<input type="hidden" name="keep-root-online" id="keep-root-online">
<input type="hidden" name="ack-rootkey" id="ack-rootkey">
<input type="hidden" name="rootkey" id="rootkey">
<input type="hidden" name="rootpassphrase" id="rootpassphrase">
{{ if not .IsRoot }}
<input type="hidden" name="root-subject" id="root-subject" value="{{ .RootSubject }}">
<input type="hidden" name="root-enddate" id="root-enddate" value="{{ .RootEnddate }}">
{{ end }}
<div class="form-group">
<label for="keytype">Key type and size:</label>
<select class="form-control" id="keytype" name="keytype" required autocomplete="off">
@@ -66,6 +73,12 @@
<span class="error">{{ . }}</span>
{{ end }}
</div>
<div class="form-group">
<label for="numdays">Validity (days):</label>
<input class="form-control" type="text" id="numdays" name="numdays" value="{{ .NumDays }}" required>
End date: <span id="validity-enddate"></span><br>
<span class="error hidden" id="numdays-error">{{ .Errors.numdays }}</span>
</div>
<div>
{{ with .Errors.Generate }}
<span class="error">{{ . }}</span><br/>
@@ -74,7 +87,7 @@
{{ if and (not .IsRoot) (not .IsRootGenerated) }}
<input class="btn btn-default" type="submit" value="Get CSR" name="getcsr">
{{ end }}
{{ if not .IsRoot }}
{{ if and (not .IsRoot) .IsFirst }}
<input class="btn btn-danger btn-right" type="submit" value="Revert Root" name="revertroot" id="revertroot">
{{ end }}
</div>
@@ -83,7 +96,7 @@
<div class="tab-pane fade {{ if eq .CreateType "import" }}active in{{ end }}" id="import">
<br/>
<form role="form" class="form-cert" action="{{ .RequestBase }}/setup" enctype="multipart/form-data" method="POST">
<form role="form" class="form-cert" enctype="multipart/form-data" method="POST">
<input type="hidden" name="cert" value="{{ if .IsRoot }}root{{ else }}issuer{{ end }}">
<input type="hidden" name="createtype" value="import">
<p>
@@ -111,7 +124,7 @@
<div class="tab-pane fade {{ if eq .CreateType "upload" }}active in{{ end }}" id="upload">
<br/>
<form role="form" class="form-cert" action="{{ .RequestBase }}/setup" enctype="multipart/form-data" method="POST">
<form role="form" class="form-cert" enctype="multipart/form-data" method="POST">
<input type="hidden" name="cert" value="{{ if .IsRoot }}root{{ else }}issuer{{ end }}">
<input type="hidden" name="createtype" value="upload">
<div class="form-group">
@@ -151,7 +164,7 @@
{{ with .Errors.Upload }}
<span class="error">{{ . }}</span><br/>
{{ end }}
<input class="btn btn-default" type="submit" value="Upload">
<input class="btn btn-default" type="submit" id="cert-submit" value="Upload">
</div>
</form>
</div>
@@ -268,11 +281,76 @@
});
{{ if not .CertificateInfo.IsRoot }}
$(".form-cert").submit(function() {
$("#processing").removeClass("hidden");
});
{{end}}
function updateEndDate() {
// Determine new end date
x = new Date();
x.setDate(x.getDate() + parseInt($('#numdays').val()))
$('#validity-enddate').text(x.toUTCString());
try {
if (($("#root-enddate").val() != undefined) && ($("#root-enddate").val().length > 0)) {
r = new Date($("#root-enddate").val());
if (x > r) {
$("#numdays-error").text("Validity cannot exceed root certificate end date");
$("#numdays-error").removeClass('hidden').show();
} else {
$("#numdays-error").text("");
if (!$("#numdays-error").hasClass('hidden')) {
$("#numdays-error").hide().addClass('hidden');
}
}
} else {
$("#numdays-error").text("");
if (!$("#numdays-error").hasClass('hidden')) {
$("#numdays-error").hide().addClass('hidden');
}
}
} catch(e) {
console.log(e);
}
$("#cert-submit").prop('disabled', !$("#numdays-error").hasClass('hidden'));
}
$.fn.inputFilter = function(callback, errMsg) {
return this.on("input keydown keyup mousedown mouseup select contextmenu drop focusout", function(e) {
if (callback(this.value)) {
// Accepted value
if (["keydown","mousedown","focusout"].indexOf(e.type) >= 0){
$("#numdays-error").text("");
if (!$("#numdays-error").hasClass('hidden')) {
$("#numdays-error").hide().addClass('hidden');
}
}
this.oldValue = this.value;
this.oldSelectionStart = this.selectionStart;
this.oldSelectionEnd = this.selectionEnd;
updateEndDate();
} else if (this.hasOwnProperty("oldValue")) {
// Rejected value - restore the previous one
$("#numdays-error").text(errMsg);
if ($("#numdays-error").hasClass('hidden')) {
$("#numdays-error").removeClass('hidden').show();
}
this.value = this.oldValue;
this.setSelectionRange(this.oldSelectionStart, this.oldSelectionEnd);
} else {
// Rejected value - nothing to restore
this.value = "";
}
});
};
$("#numdays").inputFilter(function(value) {
return /^\d*$/.test(value); // Allow digits only, using a RegExp
},"Only digits are allowed");
updateEndDate();
});
</script>
{{end}}
{{end}}

View File

@@ -158,43 +158,65 @@
<div class="tab-pane fade" id="certs">
<br/>
<form role="form">
<div class="form-group">
<p><b>Download certificates:</b><br/>
<a href="/certs/root-ca.der">root-ca.der</a> | <a href="/certs/root-ca.pem">root-ca.pem</a><br/>
<a href="/certs/ca-int.der">ca-int.der</a> | <a href="/certs/ca-int.pem">ca-int.pem</a>
<hr></p>
<label for="root-cb">Export certificates + keys:</label><br/>
<input type="checkbox" class="export-cb" id="root-cb" value="root"></input>
Root Certificate (<a href="#" data-toggle="collapse" data-target="#root-details" title="View Root Certificate details"><small>Details </small><i class="fa fa-fw fa-eye" id="root-show"></i><i class="fa fa-fw fa-eye-slash hidden" id="root-hide"></i></a>)
<pre id="root-details" class="collapse">{{ .RootDetails }}</pre>
<br id="root-br"/>
<input type="checkbox" class="export-cb" id="issuer-cb" value="issuer"></input>
Issuer Certificate (<a href="#" data-toggle="collapse" data-target="#issuer-details" title="View Issuer Certificate details"><small>Details </small><i class="fa fa-fw fa-eye" id="issuer-show"></i><i class="fa fa-fw fa-eye-slash hidden" id="issuer-hide"></i></a>)
<pre id="issuer-details" class="collapse">{{ .IssuerDetails }}</pre>
<div class="row">
<div class="col-lg-9 col-md-12">
<table class="table table-bordered mb15 p48">
<tbody>
<tr>
<th>Subject</th>
<th></th>
<th>Issue RSA</th>
<th>Issue ECDSA</th>
<th></th>
</tr>
{{ range $item := .CertificateChains }}
<tr>
<td class="vmiddle">
{{ $item.RootCert.Subject }}
<a href="#" title="View {{ $item.RootCert.BaseName }} details" id="show-{{ $item.RootCert.BaseName }}"><i class="fa fa-fw fa-eye"></i></a>
</td>
<td class="vmiddle">
<a href="/certs/{{ $item.RootCert.BaseName }}.pem" title="Download this certificate">download</a>
</td>
<td class="vmiddle"></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-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>
-->
</td>
</tr>
{{ range $subitem := $item.IssuerCerts }}
<tr>
<td class="vmiddle">
&nbsp;&#9494;&nbsp; {{ $subitem.Subject }}
<a href="#" title="View {{ $subitem.BaseName }} details"><i class="fa fa-fw fa-eye" id="show-{{ $subitem.BaseName }}"></i></a>
</td>
<td class="vmiddle">
<a href="/certs/{{ $subitem.BaseName }}.pem" title="Download this certificate">download</a>
</td>
<td class="vmiddle center"><input type="radio" id="rsa-{{ $subitem.BaseName }}" name="issue-rsa" value="{{ $subitem.BaseName }}" title="Use this certificate for issueing RSA leave certificates" {{ if $subitem.UseForRSA }}data-orig="true" checked{{ end }}/></td>
<td class="vmiddle center"><input type="radio" id="ecdsa-{{ $subitem.BaseName }}" name="issue-ecdsa" value="{{ $subitem.BaseName }}" title="Use this certificate for issueing ECDSA leave certificates" {{ if $subitem.UseForECDSA }}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-danger delete-cert" type="button" title="Delete this certificate" data-name="{{ $subitem.BaseName }}">Delete</button>
-->
</td>
</tr>
{{ end }}
{{ end }}
</tbody>
</table>
<!--
<button class="btn btn-outline btn-6em btn-warning new-root-cert" type="button" title="Generate new Root Certificate">New Root</button>
-->
</div>
<div class="form-group">
<label for="export-pfx">Export format:</label><br/>
<input type="radio" class="export-rd" id="export-pfx" name="export-type" value="pfx" checked/> .pfx (PKCS#12) file<br/>
<input type="radio" class="export-rd" id="export-zip" name="export-type" value="zip"/> .zip archive (less secure)<br/>
</div>
<div class="form-group">
<label for="export-pwd">Export file password (twice):</label>
<input class="form-control non-fluid" type="password" id="export-pwd" name="export-pwd" required/>
<span class="fa fa-eye vizpwd"></span>
<span class="error hidden" id="export-pwd-err"></span>
<input class="form-control non-fluid" type="password" id="export-pwd2" name="export-pwd2" required/>
<span class="fa fa-eye vizpwd"></span>
<span class="error hidden" id="export-pwd2-err"></span>
</div>
<button class="btn btn-default" type="button" id="cert-export" title="Download selected certificate(s) and their keys" disabled>Export Certificate</button>
<span class="error hidden" id="cert-export-err"></span>
<a id="export-target" style="display: none"></a>
</form>
</div>
</div>
<div class="tab-pane fade" id="crls">
@@ -387,7 +409,9 @@
<div class="modal-dialog">
<div class="modal-content modal-root-key-content">
<h4>Root Key Required</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span>&times;</span></button>
<p>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.</p>
<span id="modal-show-root-key-subject" class="hidden">Root subject: <span id="modal-root-key-subject"></span><br></span>
<textarea class="form-control" id="modal-rootkey" rows="10" cols="80" required></textarea>
<div class="form-group">
<label for="modal-rootpassphrase">Passphrase (optional):</label>
@@ -395,6 +419,7 @@
</div>
<span class="error" id="modal-root-key-error" style="display: none;"></span><br/>
<input class="btn btn-default btn-reg" value="Upload" id="modal-root-key-upload"/>
<input class="btn btn-default btn-reg hidden" value="Upload" id="modal-root-key-renew"/>
<button type="button" class="btn btn-default" data-dismiss="modal" id="cancel-rootkey">Cancel</button>
</div>
</div>
@@ -404,6 +429,7 @@
<div class="modal-dialog">
<div class="modal-content modal-crl-content">
<h4>CRL</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span>&times;</span></button>
<p>Please provide the CRL for the Root CA.</p>
<textarea class="form-control" id="modal-crl-val" rows="10" cols="80" required>{{ .CRL }}</textarea>
<span class="error" id="modal-crl-error" style="display: none;"></span><br/>
@@ -417,6 +443,7 @@
<div class="modal-dialog">
<div class="modal-content modal-backup-content">
<h4>Upload Backup File</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span>&times;</span></button>
<p>Please select a backup (.tgz) file that was downloaded from LabCA earlier.</p>
<form enctype="multipart/form-data" id="modal-backup-form" method="POST">
<input type="hidden" name="action" value="backup-upload">
@@ -428,6 +455,107 @@
</div>
</div>
</div>
{{ range $item := .CertificateChains }}
<div id="modal-{{ $item.RootCert.BaseName }}" class="modal fade" data-backdrop="static" data-keyboard="false" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content modal-{{ $item.RootCert.BaseName }}-content">
<h4>{{ $item.RootCert.Subject }}</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span>&times;</span></button>
<pre>{{ $item.RootCert.Details }}</pre>
<button type="button" class="btn btn-default btn-modal-dismiss" data-dismiss="modal">Close</button>
</div>
</div>
</div>
{{ range $subitem := $item.IssuerCerts }}
<div id="modal-{{ $subitem.BaseName }}" class="modal fade" data-backdrop="static" data-keyboard="false" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content modal-{{ $subitem.BaseName }}-content">
<h4>{{ $subitem.Subject }} Details</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span>&times;</span></button>
<pre>{{ $subitem.Details }}</pre>
<button type="button" class="btn btn-default btn-modal-dismiss" data-dismiss="modal">Close</button>
</div>
</div>
</div>
{{ end }}
{{ end }}
<div id="modal-export" class="modal fade" data-backdrop="static" data-keyboard="false" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content modal-export-content">
<h4>Export certificate + key</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span>&times;</span></button>
<h5><span id="modal-export-subject"></span></h5>
<form enctype="multipart/form-data" id="modal-export-form" method="POST">
<input type="hidden" name="export-certname" id="export-certname">
<div class="form-group">
<label for="export-pfx">Export format:</label><br/>
<input type="radio" class="export-rd" id="export-pfx" name="export-type" value="pfx" checked/> .pfx (PKCS#12) file<br/>
<input type="radio" class="export-rd" id="export-zip" name="export-type" value="zip"/> .zip archive (less secure)<br/>
</div>
<div class="form-group">
<label for="export-pwd">Export file password (twice):</label>
<input class="form-control non-fluid" type="password" id="export-pwd" name="export-pwd" required/>
<span class="fa fa-eye vizpwd"></span>
<span class="error hidden" id="export-pwd-err"></span>
<input class="form-control non-fluid" type="password" id="export-pwd2" name="export-pwd2" required/>
<span class="fa fa-eye vizpwd"></span>
<span class="error hidden" id="export-pwd2-err"></span>
</div>
<span class="error" id="modal-export-error" style="display: none;"></span><br/>
<a id="export-target" style="display: none"></a>
<input class="btn btn-default btn-reg" value="Export" id="modal-export-done"/>
<button type="button" class="btn btn-default" data-dismiss="modal" id="cancel-export">Cancel</button>
</form>
</div>
</div>
</div>
<div id="modal-leaves" class="modal fade" data-backdrop="static" data-keyboard="false" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content modal-leaves-content">
<h4>Are you sure?</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span>&times;</span></button>
<form enctype="multipart/form-data" id="modal-leaves-form" method="POST">
<input type="hidden" id="new-rsa-issuer" name="new-rsa-issuer">
<input type="hidden" id="new-ecdsa-issuer" name="new-ecdsa-issuer">
<span id="want-new-rsa-issuer" class="hidden">&#x2022; Change the RSA issuer certificate<br/></span>
<span id="want-new-ecdsa-issuer" class="hidden">&#x2022; Change the ECDSA issuer certificate<br/></span>
<span class="error" id="modal-leaves-error" style="display: none;"></span><br/>
<input class="btn btn-default btn-reg btn-warning btn-6em" value="Change" id="modal-leaves-done"/>
<button type="button" class="btn btn-default" data-dismiss="modal" id="cancel-leaves">Cancel</button>
</form>
</div>
</div>
</div>
<div id="modal-renew" class="modal fade" data-backdrop="static" data-keyboard="false" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content modal-renew-content">
<h4>Renew CA</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span>&times;</span></button>
<form enctype="multipart/form-data" id="modal-renew-form" method="POST">
<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>
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>
<label>New validity:</label>
<input class="form-control non-fluid form-small-inline" type="text" name="renew-numdays" id="renew-numdays" required> days<br>
<span class="error backend-error hidden" id="renew-numdays-error"></span><br>
New end date: <span id="renew-new-enddate"></span><br>
<span class="error" id="modal-renew-error" style="display: none;"></span><br/>
<input class="btn btn-default btn-reg btn-warning" value="Renew" id="modal-renew-done"/>
<button type="button" class="btn btn-default" data-dismiss="modal" id="cancel-renew">Cancel</button>
</form>
</div>
</div>
</div>
{{end}}
{{ define "tail" }}
@@ -441,6 +569,9 @@
$('.nav-tabs-sticky').stickyTabs();
$('#modal-backup-form').ajaxForm();
$('#modal-export-form').ajaxForm();
$('#modal-leaves-form').ajaxForm();
$('#modal-renew-form').ajaxForm();
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
positionFooter();
@@ -460,13 +591,75 @@
}
});
{{ range $item := .CertificateChains }}
$("#show-{{ $item.RootCert.BaseName }}").click(function(evt) {
$('#modal-{{ $item.RootCert.BaseName }}').modal('show');
return false;
});
{{ range $subitem := $item.IssuerCerts }}
$("#show-{{ $subitem.BaseName }}").click(function(evt) {
$('#modal-{{ $subitem.BaseName }}').modal('show');
return false;
});
{{ end }}
{{ end }}
function leave_issuer_changed() {
iss_rsa = "";
iss_ecdsa = "";
iss_rsa_changed = false;
iss_ecdsa_changed = false;
$('input[type=radio][name=issue-rsa]').each(function() {
if ($(this).prop('checked') && !$(this).data('orig')) {
iss_rsa = $(this).val();
iss_rsa_changed = true;
}
});
$('input[type=radio][name=issue-ecdsa]').each(function() {
if ($(this).prop('checked') && !$(this).data('orig')) {
iss_ecdsa = $(this).val();
iss_ecdsa_changed = true;
}
});
if (iss_rsa_changed || iss_ecdsa_changed) {
$("#new-rsa-issuer").val(iss_rsa);
if (iss_rsa_changed) {
$("#want-new-rsa-issuer").removeClass("hidden").show();
} else {
$("#want-new-rsa-issuer").hide();
}
$("#new-ecdsa-issuer").val(iss_ecdsa);
if (iss_ecdsa_changed) {
$("#want-new-ecdsa-issuer").removeClass("hidden").show();
} else {
$("#want-new-ecdsa-issuer").hide();
}
$('#modal-leaves').modal('show');
}
return false;
}
$('input[type=radio][name=issue-rsa]').change(function() {
return leave_issuer_changed();
});
$('input[type=radio][name=issue-ecdsa]').change(function() {
return leave_issuer_changed();
});
$(".btn").click(function(evt) {
$('#modal-spinner').modal('show');
if ($(evt.target).hasClass('btn-warning') || $(evt.target).hasClass('btn-danger')) {
if (!window.confirm("Are you sure?")) {
$('#modal-spinner').modal('hide');
return false;
if ($(evt.target).attr("id") != "modal-leaves-done" && $(evt.target).attr("id") != "modal-renew-done" && $(evt.target).attr("id") != "modal-newcert-done" &&
!$(evt.target).hasClass('renew-cert') && !$(evt.target).hasClass('new-issuer-cert') && !$(evt.target).hasClass('new-root-cert')) {
if (!window.confirm("Are you sure?")) {
$('#modal-spinner').modal('hide');
return false;
}
}
}
$(evt.target).blur();
@@ -486,7 +679,9 @@
$("#issuer-crl-result").hide();
$("#crl-interval-result").hide();
$("#modal-backup-error").hide();
$("#modal-export-error").hide();
$("#modal-leaves-error").hide();
$("#modal-renew-error").hide();
if ( $(evt.target).attr("id") == "backup-now") {
$.ajax(window.location.href, {
@@ -617,7 +812,7 @@
$("#backup-result").removeClass("hidden").removeClass("success").show().text(err).addClass("error");
});
} else if ( $(evt.target).attr("id") == "cert-export") {
} else if ($(evt.target).attr("id") == "modal-export-done") {
type = ($("#export-zip").prop('checked') ? "zip" : ($("#export-pfx").prop('checked') ? "pfx" : "none"));
if ($("#export-pwd").val().length < 4) {
@@ -635,36 +830,41 @@
req.onload = function (event) {
$('#modal-spinner').modal('hide');
var blob = req.response;
var fileName = null;
var contentType = req.getResponseHeader("content-type");
if (event.currentTarget.status == 200) {
var blob = req.response;
var fileName = null;
var contentType = req.getResponseHeader("content-type");
// IE/EDGE seems not returning some response header
if (req.getResponseHeader("content-disposition")) {
var contentDisposition = req.getResponseHeader("content-disposition");
fileName = contentDisposition.substring(contentDisposition.indexOf("=")+1);
} else {
fileName = "unnamed." + contentType.substring(contentType.indexOf("/")+1);
}
// IE/EDGE seems not returning some response header
if (req.getResponseHeader("content-disposition")) {
var contentDisposition = req.getResponseHeader("content-disposition");
fileName = contentDisposition.substring(contentDisposition.indexOf("=") + 1);
} else {
fileName = "unnamed." + contentType.substring(contentType.indexOf("/") + 1);
}
if (window.navigator.msSaveOrOpenBlob) {
// Internet Explorer
window.navigator.msSaveOrOpenBlob(new Blob([blob], {type: contentType}), fileName);
if (window.navigator.msSaveOrOpenBlob) {
// Internet Explorer
window.navigator.msSaveOrOpenBlob(new Blob([blob], { type: contentType }), fileName);
} else {
var el = document.getElementById("export-target");
el.href = window.URL.createObjectURL(blob);
el.download = fileName;
el.click();
}
$('#modal-export').modal('hide');
} else {
var el = document.getElementById("export-target");
el.href = window.URL.createObjectURL(blob);
el.download = fileName;
el.click();
$("#modal-export-error").removeClass("hidden").show().text("Backend returned: " + event.currentTarget.statusText);
}
};
req.onerror = function (event) {
$('#modal-spinner').modal('hide');
$("#cert-export-err").removeClass("hidden").show().text("Oops, something went wrong...");
$("#modal-export-error").removeClass("hidden").show().text("Error communicating with backend server");
};
req.send("action=" + $(evt.target).attr("id") + "&root=" + $("#root-cb").prop('checked') +
"&issuer=" + $("#issuer-cb").prop('checked') + "&type=" + type + "&export-pwd=" + $("#export-pwd").val());
req.send("action=cert-export&certname=" + $("#export-certname").val() + "&type=" + type + "&export-pwd=" + $("#export-pwd").val());
}
} else if ( $(evt.target).attr("id") == "update-account") {
@@ -1085,13 +1285,200 @@
});
}
} else if ( $(evt.target).attr("id") == "cancel-rootkey" || $(evt.target).attr("id") == "cancel-crl" || $(evt.target).attr("id") == "cancel-backup") {
} else if ($(evt.target).attr("id") == "cancel-rootkey" || $(evt.target).attr("id") == "cancel-crl" || $(evt.target).attr("id") == "cancel-backup" ||
$(evt.target).attr("id") == "cancel-export" || $(evt.target).attr("id") == "cancel-leaves" || $(evt.target).attr("id") == "cancel-renew" ||
$(evt.target).attr("id") == "cancel-newcert") {
$('#modal-spinner').modal('hide');
$('#modal-backup').modal('hide');
$('#modal-crl').modal('hide');
$('#modal-root-key').modal('hide');
$('#modal-export').modal('hide');
$('#modal-leaves').modal('hide');
$('#modal-renew').modal('hide');
$('#modal-newcert').modal('hide');
return false;
} else if ($(evt.target).hasClass("btn-modal-dismiss")) {
$('#modal-spinner').modal('hide');
return true;
} else if ($(evt.target).attr("id") == "modal-leaves-done") {
if ($("#want-new-rsa-issuer").hasClass("hidden") && $("#want-new-ecdsa-issuer").hasClass("hidden")) {
$('#modal-spinner').modal('hide');
$('#modal-leaves').modal('hide');
} else {
var req = new XMLHttpRequest();
req.open("POST", window.location.href, true);
req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
req.onload = function(event) {
$('#modal-spinner').modal('hide');
if (event.currentTarget.status == 200) {
try {
res = JSON.parse(req.response);
if (res.Success) {
$('input[type=radio][name=issue-rsa]').each(function() {
$(this).data('orig', $(this).prop('checked'))
});
$('input[type=radio][name=issue-ecdsa]').each(function() {
$(this).data('orig', $(this).prop('checked'))
});
$('#modal-leaves').modal('hide');
var msg = "Successfully updated issuers, restarting 'Boulder (ACME)' now...";
setTimeout(function() {
window.location.reload();
}, 10000);
$("#modal-leaves-error").removeClass("hidden").removeClass("error").show().text(msg).addClass("success");
} else {
$("#modal-leaves-error").removeClass("hidden").show().text(res.Error);
}
} catch (e) {
console.log(e);
$("#modal-leaves-error").removeClass("hidden").show().text("Received an unexpected response from the server");
}
} else if (event.currentTarget.status == 502) {
// Is a result of the backend restart...
var msg = "Successfully updated issuers and restarted 'Boulder (ACME)'";
$("#modal-leaves-error").removeClass("hidden").removeClass("error").show().text(msg).addClass("success");
setTimeout(function() {
window.location.reload();
}, 2000);
} else {
$("#modal-leaves-error").removeClass("hidden").show().text("Backend returned: " + event.currentTarget.statusText);
}
};
req.onerror = function(event) {
$('#modal-spinner').modal('hide');
$("#modal-leaves-error").removeClass("hidden").show().text("Error communicating with backend server");
};
req.send("action=update-leave-issuers" + "&rsa=" + $("#new-rsa-issuer").val() + "&ecdsa=" + $("#new-ecdsa-issuer").val());
}
return false;
} else if ($(evt.target).hasClass('export-cert-key')) {
$('#modal-spinner').modal('hide');
$('#modal-export-subject').text($(evt.target).data('subject'));
$('#export-certname').val($(evt.target).data('name'));
$('#modal-export').modal('show');
return false;
} else if ($(evt.target).hasClass('renew-cert')) {
$('#modal-spinner').modal('hide');
$('#modal-renew-cert').val($(evt.target).data('name'));
$('#renew-rootcert').val($(evt.target).data('rootname'));
$('#renew-subject').text($(evt.target).data('subject'));
$('#renew-rootsubject').val($(evt.target).data('rootsubject'));
d = new Date($(evt.target).data('notbefore')).toUTCString()
$('#renew-current-enddate').text(d);
if ($(evt.target).data('isroot')) {
$("#renew-show-root-enddate").hide();
$("#renew-root-enddate").text("");
$('#renew-numdays').val(4 * 365);
} else {
$("#renew-show-root-enddate").removeClass("hidden").show();
d = new Date($(evt.target).data('notafter')).toUTCString()
$("#renew-root-enddate").text(d);
$('#renew-numdays').val(365);
}
updateEndDate();
$('#modal-renew').modal('show');
return false;
} else if ($(evt.target).attr("id") == "modal-renew-done") {
var req = new XMLHttpRequest();
req.open("POST", window.location.href, true);
req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
req.onload = function(event) {
$('#modal-spinner').modal('hide');
if (event.currentTarget.status == 200) {
try {
res = JSON.parse(req.response);
if (res.Success) {
var msg = "Successfully updated issuers and restarted 'Boulder (ACME)'";
setTimeout(function () {
$('#modal-renew').modal('hide');
window.location.reload();
}, 2000);
$("#modal-renew-error").removeClass("hidden").removeClass("error").show().text(msg).addClass("success");
} else if (res.Error.startsWith("NO_ROOT_KEY")) {
$("#modal-show-root-key-subject").removeClass("hidden").show();
$("#modal-root-key-subject").text($('#renew-rootsubject').val());
$("#modal-root-key-upload").hide();
$("#modal-root-key-renew").removeClass("hidden").show();
if (res.Error.split(":").length > 1) {
$("#modal-root-key-error").show().text(res.Error.split(":")[1]);
}
$('#modal-root-key').modal('show');
$('#modal-renew').modal('hide');
return false;
} else {
$("#modal-renew-error").removeClass("hidden").show().text(res.Error);
}
} catch (e) {
console.log(e);
$("#modal-renew-error").removeClass("hidden").show().text("Received an unexpected response from the server");
}
} else if (event.currentTarget.status == 502) {
// Is a result of the backend restart...
var msg = "Successfully updated issuers and restarted 'Boulder (ACME)'";
$("#modal-renew-error").removeClass("hidden").removeClass("error").show().text(msg).addClass("success");
setTimeout(function() {
window.location.reload();
}, 2000);
} else {
$("#modal-renew-error").removeClass("hidden").show().text("Backend returned: " + event.currentTarget.statusText);
}
};
req.onerror = function(event) {
$('#modal-spinner').modal('hide');
$("#modal-renew-error").removeClass("hidden").show().text("Error communicating with backend server");
};
req.send("action=renew-cert&certname=" + $('#modal-renew-cert').val() + "&days=" + $("#renew-numdays").val() +
"&rootname=" + $('#renew-rootcert').val() + "&root_key=" + $("#modal-rootkey").val() + "&passphrase=" + $("#modal-rootpassphrase").val());
return false;
} else if ($(evt.target).attr("id") == "modal-root-key-renew") {
if ($("#modal-rootkey").val() == "") {
$('#modal-spinner').modal('hide');
$("#modal-root-key-error").show().text("Please provide the root key");
return false;
} else {
$("#modal-show-root-key-subject").hide();
$("#modal-root-key-upload").removeClass("hidden").show();
$("#modal-root-key-renew").hide();
$('#modal-root-key').modal('hide');
$('#modal-renew').modal('show');
$("#modal-renew-done").click();
return false;
}
} else if ($(evt.target).hasClass('new-issuer-cert')) {
$('#modal-spinner').modal('hide');
window.location.href = "/manage/newissuer?root=" + $(evt.target).data('root');
} else if ($(evt.target).hasClass('new-root-cert')) {
$('#modal-spinner').modal('hide');
window.location.href = "/manage/newroot";
} else if ($(evt.target).hasClass("delete-cert")) {
console.log("TODO: implement delete...");
} else {
$.ajax(window.location.href, {
method: "POST",
@@ -1145,54 +1532,6 @@
}
});
$("#root-details").on('show.bs.collapse', function() {
$("#root-show").hide();
$("#root-hide").removeClass("hidden").show();
$("#root-br").hide();
});
$("#root-details").on('hidden.bs.collapse', function() {
$("#root-show").show();
$("#root-hide").hide();
$("#root-br").show();
});
$("#issuer-details").on('show.bs.collapse', function() {
$("#issuer-show").hide();
$("#issuer-hide").removeClass("hidden").show();
});
$("#issuer-details").on('hidden.bs.collapse', function() {
$("#issuer-show").show();
$("#issuer-hide").hide();
});
function change_export_settings() {
checkbox_count = 0;
if ($("#root-cb").prop('checked')) {
checkbox_count += 1;
}
if ($("#issuer-cb").prop('checked')) {
checkbox_count += 1;
}
if ($("input[type=radio]#export-pfx").prop('checked')) {
$("#cert-export").prop('disabled', checkbox_count != 1);
} else {
$("#cert-export").prop('disabled', checkbox_count == 0);
}
if ($("input[type=radio]#export-zip").prop('checked')) {
$("#cert-export").text("Export Certificate(s)");
} else {
$("#cert-export").text("Export Certificate");
}
}
$(".export-cb").change(change_export_settings);
$(".export-rd").change(change_export_settings);
function check_fqdn() {
if ($("#fqdn").val() == window.location.host) {
$("#fqdn_warning").hide();
@@ -1236,7 +1575,87 @@
$("#update-account").prop('disabled', !ok);
});
function updateEndDate() {
// Determine new end date
x = new Date();
x.setDate(x.getDate() + parseInt($('#renew-numdays').val()))
$('#renew-new-enddate').text(x.toUTCString());
try {
c = new Date($('#renew-current-enddate').text());
if (x <= c) {
if ($("#renew-numdays-error").hasClass('hidden')) {
$("#renew-numdays-error").text("Validity cannot end before current end date");
$("#renew-numdays-error").removeClass('hidden').show();
}
} else if ($('#renew-root-enddate').text().length > 0) {
r = new Date($('#renew-root-enddate').text());
if (x > r) {
if ($("#renew-numdays-error").hasClass('hidden')) {
$("#renew-numdays-error").text("Validity cannot exceed root certificate end date");
$("#renew-numdays-error").removeClass('hidden').show();
}
} else {
$("#renew-numdays-error").text("");
if (!$("#renew-numdays-error").hasClass('hidden')) {
$("#renew-numdays-error").hide().addClass('hidden');
}
}
} else {
$("#renew-numdays-error").text("");
if (!$("#renew-numdays-error").hasClass('hidden')) {
$("#renew-numdays-error").hide().addClass('hidden');
}
}
} catch (e) {
console.log(e);
}
$("#modal-renew-done").prop('disabled', !$("#renew-numdays-error").hasClass('hidden'));
}
$.fn.inputFilter = function(callback, errMsg) {
return this.on("input keydown keyup mousedown mouseup select contextmenu drop focusout", function (e) {
if (callback(this.value)) {
// Accepted value
if (["keydown", "mousedown", "focusout"].indexOf(e.type) >= 0) {
$("#renew-numdays-error").text("");
if (!$("#renew-numdays-error").hasClass('hidden')) {
$("#renew-numdays-error").hide().addClass('hidden');
}
}
this.oldValue = this.value;
this.oldSelectionStart = this.selectionStart;
this.oldSelectionEnd = this.selectionEnd;
updateEndDate();
} else if (this.hasOwnProperty("oldValue")) {
// Rejected value - restore the previous one
$("#renew-numdays-error").text(errMsg);
if ($("#renew-numdays-error").hasClass('hidden')) {
$("#renew-numdays-error").removeClass('hidden').show();
}
this.value = this.oldValue;
this.setSelectionRange(this.oldSelectionStart, this.oldSelectionEnd);
} else {
// Rejected value - nothing to restore
this.value = "";
}
});
};
$("#renew-numdays").inputFilter(function (value) {
return /^\d*$/.test(value); // Allow digits only, using a RegExp
}, "Only digits are allowed");
$(".delete-cert.hidden").each(function () {
try {
if ($($(this).parent().parent().next().children()[0]).text().indexOf("┖") < 0) {
$(this).removeClass('hidden');
}
} catch (e) { }
});
pwduxHandlers('#password-strength', '#new-password', ['#username', '#accemail']);
});
</script>
{{ end }}
{{ end }}

View File

@@ -448,7 +448,7 @@ install_pkg() {
}
install_extra() {
local packages=(apt-transport-https ca-certificates curl gnupg net-tools tzdata ucspi-tcp zip python lsb-release)
local packages=(apt-transport-https ca-certificates curl gnupg net-tools tzdata ucspi-tcp zip unzip python lsb-release)
for package in "${packages[@]}"; do
install_pkg "$package"
done
@@ -519,8 +519,10 @@ static_web() {
cp -rp $cloneDir/gui/static/* .
[ -e $adminDir/data/root-ca.crl ] && cp $adminDir/data/root-ca.crl crl/ || true
[ -e $adminDir/data/root-ca.pem ] && cp $adminDir/data/root-ca.pem certs/ || true
[ -e $adminDir/data/root-ca.pem ] && ln -sf root-ca.pem certs/test-root.pem || true
[ -e $adminDir/data/root-ca.der ] && cp $adminDir/data/root-ca.der certs/ || true
[ -e $adminDir/data/issuer/ca-int.pem ] && cp $adminDir/data/issuer/ca-int.pem certs/ || true
[ -e $adminDir/data/issuer/ca-int.pem ] && ln -sf ca-int.pem certs/test-ca.pem || true
[ -e $adminDir/data/issuer/ca-int.der ] && cp $adminDir/data/issuer/ca-int.der certs/ || true
local have_config=$(grep restarted $adminDir/data/config.json | grep true)

View File

@@ -1,10 +1,12 @@
diff --git a/crl/storer/storer.go b/crl/storer/storer.go
index cd0bf86c..dd492aec 100644
index cd0bf86c0..26e52f789 100644
--- a/crl/storer/storer.go
+++ b/crl/storer/storer.go
@@ -12,6 +12,9 @@ import (
@@ -11,7 +11,11 @@ import (
"errors"
"fmt"
"io"
+ "io/fs"
"math/big"
+ "os"
+ "path/filepath"
@@ -12,7 +14,7 @@ index cd0bf86c..dd492aec 100644
"time"
"github.com/aws/aws-sdk-go-v2/service/s3"
@@ -38,6 +41,7 @@ type crlStorer struct {
@@ -38,6 +42,7 @@ type crlStorer struct {
cspb.UnimplementedCRLStorerServer
s3Client simpleS3
s3Bucket string
@@ -20,7 +22,7 @@ index cd0bf86c..dd492aec 100644
issuers map[issuance.IssuerNameID]*issuance.Certificate
uploadCount *prometheus.CounterVec
sizeHistogram *prometheus.HistogramVec
@@ -50,6 +54,7 @@ func New(
@@ -50,6 +55,7 @@ func New(
issuers []*issuance.Certificate,
s3Client simpleS3,
s3Bucket string,
@@ -28,7 +30,7 @@ index cd0bf86c..dd492aec 100644
stats prometheus.Registerer,
log blog.Logger,
clk clock.Clock,
@@ -83,6 +88,7 @@ func New(
@@ -83,6 +89,7 @@ func New(
issuers: issuersByNameID,
s3Client: s3Client,
s3Bucket: s3Bucket,
@@ -36,7 +38,7 @@ index cd0bf86c..dd492aec 100644
uploadCount: uploadCount,
sizeHistogram: sizeHistogram,
latencyHistogram: latencyHistogram,
@@ -203,15 +209,19 @@ func (cs *crlStorer) UploadCRL(stream cspb.CRLStorer_UploadCRLServer) error {
@@ -203,15 +210,19 @@ func (cs *crlStorer) UploadCRL(stream cspb.CRLStorer_UploadCRLServer) error {
checksum := sha256.Sum256(crlBytes)
checksumb64 := base64.StdEncoding.EncodeToString(checksum[:])
crlContentType := "application/pkix-crl"
@@ -65,7 +67,7 @@ index cd0bf86c..dd492aec 100644
latency := cs.clk.Now().Sub(start)
cs.latencyHistogram.WithLabelValues(issuer.Subject.CommonName).Observe(latency.Seconds())
@@ -240,3 +250,46 @@ func getIDPExt(exts []pkix.Extension) []byte {
@@ -240,3 +251,46 @@ func getIDPExt(exts []pkix.Extension) []byte {
}
return nil
}
@@ -89,7 +91,7 @@ index cd0bf86c..dd492aec 100644
+ lntmp := ln + ".new"
+ fn = fmt.Sprintf("%d-%d-%d.crl", nameID, crlNumber, shardIdx)
+
+ if err = os.Remove(lntmp); err != nil && !os.IsNotExist(err) {
+ if err = os.Remove(lntmp); err != nil && !errors.Is(err, fs.ErrNotExist) {
+ return fmt.Errorf("removing symlink: %w", err)
+ }
+ if err = os.Symlink(fn, lntmp); err != nil {