mirror of
https://github.com/outbackdingo/labca.git
synced 2026-01-27 18:19:33 +00:00
1318 lines
37 KiB
Go
1318 lines
37 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
"io/fs"
|
|
"log"
|
|
"math"
|
|
"mime/multipart"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"reflect"
|
|
"regexp"
|
|
"runtime/debug"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
// CertificateInfo contains all data related to a certificate (file)
|
|
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
|
|
CommonName string
|
|
|
|
ImportFile multipart.File
|
|
ImportHandler *multipart.FileHeader
|
|
ImportPwd string
|
|
|
|
Key string
|
|
Passphrase string
|
|
Certificate string
|
|
CRL string
|
|
|
|
/*
|
|
KeyFromHSM bool
|
|
HSMInfo HSMInfo
|
|
HSMKeys map[string]string
|
|
HSMKey string
|
|
HSMLabel string
|
|
StoreCertOnHSM bool
|
|
*/
|
|
|
|
RequestBase string
|
|
Errors map[string]string
|
|
}
|
|
|
|
// Initialize the CertificateInfo and set the list of available key types
|
|
func (ci *CertificateInfo) Initialize() {
|
|
ci.Errors = make(map[string]string)
|
|
|
|
ci.KeyTypes = make(map[string]string)
|
|
ci.KeyTypes["rsa4096"] = "RSA-4096"
|
|
ci.KeyTypes["rsa2048"] = "RSA-2048"
|
|
ci.KeyTypes["ecdsa384"] = "ECDSA-384"
|
|
ci.KeyTypes["ecdsa256"] = "ECDSA-256"
|
|
|
|
ci.KeyType = "rsa4096"
|
|
|
|
// ci.HSMKeys = make(map[string]string)
|
|
// ci.StoreCertOnHSM = true
|
|
}
|
|
|
|
// ValidateGenerate that the CertificateInfo contains valid and all required data for generating a cert
|
|
func (ci *CertificateInfo) ValidateGenerate() {
|
|
if strings.TrimSpace(ci.KeyType) == "" || strings.TrimSpace(ci.KeyTypes[ci.KeyType]) == "" {
|
|
ci.Errors["KeyType"] = "Please select a key type/size"
|
|
}
|
|
if strings.TrimSpace(ci.Country) == "" || len(ci.Country) < 2 {
|
|
ci.Errors["Country"] = "Please enter a valid 2-character country code"
|
|
}
|
|
if strings.TrimSpace(ci.Organization) == "" {
|
|
ci.Errors["Organization"] = "Please enter an organization name"
|
|
}
|
|
if strings.TrimSpace(ci.CommonName) == "" {
|
|
ci.Errors["CommonName"] = "Please enter a common name"
|
|
}
|
|
}
|
|
|
|
// Validate that the CertificateInfo contains valid and all required data
|
|
func (ci *CertificateInfo) Validate() bool {
|
|
ci.Errors = make(map[string]string)
|
|
|
|
if ci.CreateType == "generate" {
|
|
ci.ValidateGenerate()
|
|
}
|
|
|
|
if (ci.CreateType == "import") && (ci.ImportHandler != nil) {
|
|
ext := ci.ImportHandler.Filename[len(ci.ImportHandler.Filename)-4:]
|
|
if (ci.ImportHandler.Size == 0) || (ext != ".zip" && ext != ".pfx") {
|
|
ci.Errors["Import"] = "Please provide a bundle (.pfx or .zip) with a key and certificate"
|
|
}
|
|
}
|
|
|
|
if ci.CreateType == "upload" {
|
|
if strings.TrimSpace(ci.Key) == "" {
|
|
ci.Errors["Key"] = "Please provide a PEM-encoded key"
|
|
}
|
|
if strings.TrimSpace(ci.Certificate) == "" {
|
|
ci.Errors["Certificate"] = "Please provide a PEM-encoded certificate"
|
|
}
|
|
}
|
|
|
|
return len(ci.Errors) == 0
|
|
}
|
|
|
|
func reportError(param interface{}) error {
|
|
lines := strings.Split(string(debug.Stack()), "\n")
|
|
if len(lines) >= 5 {
|
|
lines = append(lines[:0], lines[5:]...)
|
|
}
|
|
|
|
stop := len(lines)
|
|
for i := 0; i < len(lines); i++ {
|
|
if strings.Contains(lines[i], ".ServeHTTP(") {
|
|
stop = i
|
|
break
|
|
}
|
|
}
|
|
lines = lines[:stop]
|
|
lines = append(lines, "...")
|
|
|
|
fmt.Println(strings.Join(lines, "\n"))
|
|
|
|
res := errors.New("error: see LabCA logs for details")
|
|
switch v := param.(type) {
|
|
case error:
|
|
res = errors.New("Error (" + v.Error() + ")! See LabCA logs for details")
|
|
case []byte:
|
|
res = errors.New("Error (" + string(v) + ")! See LabCA logs for details")
|
|
default:
|
|
fmt.Printf("unexpected type %T", v)
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
func ceremonyConfig(path string, rewrites map[string]string) (string, error) {
|
|
tmplBytes, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
tmp, err := os.CreateTemp(os.TempDir(), "ceremony-config")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer func() { _ = tmp.Close() }()
|
|
tmpl, err := template.New("config").Parse(string(tmplBytes))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
err = tmpl.Execute(tmp, rewrites)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return tmp.Name(), nil
|
|
}
|
|
|
|
func waitForFile(filePath string) error {
|
|
start := time.Now()
|
|
for {
|
|
if _, err := os.Stat(filePath); err == nil {
|
|
return nil // File found
|
|
} else if !os.IsNotExist(err) {
|
|
return fmt.Errorf("error checking file: %v", err) // Unexpected error
|
|
}
|
|
|
|
// Check if the timeout has been reached
|
|
if time.Since(start) > 2*time.Minute {
|
|
return fmt.Errorf("timeout reached while waiting for file")
|
|
}
|
|
|
|
// Sleep for a short interval before checking again
|
|
time.Sleep(5 * time.Second)
|
|
}
|
|
}
|
|
|
|
func (ci *CertificateInfo) CeremonyRoot(seqnr string, use_existing_key bool) (string, error) {
|
|
keytype := "rsa"
|
|
keyparam := strings.ReplaceAll(ci.KeyType, "rsa", "")
|
|
algo := "SHA256WithRSA"
|
|
if strings.HasPrefix(ci.KeyType, "ecdsa") {
|
|
keytype = "ecdsa"
|
|
len := strings.ReplaceAll(ci.KeyType, "ecdsa", "")
|
|
keyparam = "P-" + len
|
|
algo = "ECDSAWithSHA" + len
|
|
}
|
|
|
|
notbefore := time.Now().Add(-1 * time.Second)
|
|
notafter := notbefore.AddDate(0, 0, ci.NumDays).Add(-1 * time.Second)
|
|
|
|
cfg := &HSMConfig{}
|
|
cfg.Initialize("root", seqnr)
|
|
if err := cfg.CreateSlot(); err != nil {
|
|
return "", fmt.Errorf("failed to create root slot: %s", err.Error())
|
|
}
|
|
|
|
certFileName := fmt.Sprintf("%sroot-%s-cert.pem", CERT_FILES_PATH, seqnr)
|
|
cb := renameBackup(certFileName)
|
|
var pb BackupResult
|
|
if !use_existing_key {
|
|
pb = renameBackup(fmt.Sprintf("%sroot-%s-pubkey.pem", CERT_FILES_PATH, seqnr))
|
|
}
|
|
|
|
ceremonyCfg, err := ceremonyConfig("templates/cert-ceremonies/root.yaml", map[string]string{
|
|
"Module": cfg.Module,
|
|
"UserPIN": cfg.UserPIN,
|
|
"SlotID": cfg.SlotID,
|
|
"Label": cfg.Label,
|
|
"Path": CERT_FILES_PATH,
|
|
"KeyType": keytype,
|
|
"KeyParam": keyparam,
|
|
"Extractable": strconv.FormatBool(true), // For now, with SoftHSM, this is fine. In future we need to ask for informed consent!
|
|
"SeqNr": seqnr,
|
|
"SignAlgorithm": algo,
|
|
"CommonName": ci.CommonName,
|
|
"OrgName": ci.Organization,
|
|
"Country": ci.Country,
|
|
"NotBefore": notbefore.UTC().Format("2006-01-02 15:04:05"),
|
|
"NotAfter": notafter.UTC().Format("2006-01-02 15:04:05"),
|
|
"Renewal": strconv.FormatBool(use_existing_key),
|
|
})
|
|
if err != nil {
|
|
ci.Errors["Generate"] = "error preparing for root ceremony, see logs for details"
|
|
cb.Restore()
|
|
if !use_existing_key {
|
|
pb.Restore()
|
|
}
|
|
return "", fmt.Errorf("could not fill root ceremony template: %s", err.Error())
|
|
}
|
|
defer func() { _ = os.Remove(ceremonyCfg) }()
|
|
|
|
err = waitForFile("/opt/boulder/bin/ceremony")
|
|
if err != nil {
|
|
return "", fmt.Errorf("could not wait for /opt/boulder/bin/ceremony to exist: %s", err.Error())
|
|
}
|
|
|
|
if _, err = exeCmd("/opt/boulder/bin/ceremony -config " + ceremonyCfg); err != nil {
|
|
ci.Errors["Generate"] = "failed to execute root ceremony, see logs for details"
|
|
cb.Restore()
|
|
if !use_existing_key {
|
|
pb.Restore()
|
|
}
|
|
return "", err
|
|
}
|
|
|
|
cb.Remove()
|
|
if !use_existing_key {
|
|
pb.Remove()
|
|
}
|
|
return certFileName, nil
|
|
}
|
|
|
|
func (ci *CertificateInfo) CeremonyIssuer(seqnr, rootseqnr string, use_existing_key bool) (string, error) {
|
|
fqdn := viper.GetString("labca.fqdn")
|
|
|
|
keytype := "rsa"
|
|
keyparam := strings.ReplaceAll(ci.KeyType, "rsa", "")
|
|
algo := "SHA256WithRSA"
|
|
if strings.HasPrefix(ci.KeyType, "ecdsa") {
|
|
keytype = "ecdsa"
|
|
len := strings.ReplaceAll(ci.KeyType, "ecdsa", "")
|
|
keyparam = "P-" + len
|
|
algo = "ECDSAWithSHA" + len
|
|
}
|
|
|
|
notbefore := time.Now().Add(-1 * time.Second)
|
|
notafter := notbefore.AddDate(0, 0, ci.NumDays).Add(-1 * time.Second)
|
|
|
|
cfg := &HSMConfig{}
|
|
cfg.Initialize("issuer", seqnr)
|
|
if err := cfg.CreateSlot(); err != nil {
|
|
return "", fmt.Errorf("failed to create issuer slot: %s", err.Error())
|
|
}
|
|
|
|
if !use_existing_key {
|
|
pb := renameBackup(fmt.Sprintf("%sissuer-%s-pubkey.pem", CERT_FILES_PATH, seqnr))
|
|
jb := renameBackup(fmt.Sprintf("%sissuer-%s.pkcs11.json", CERT_FILES_PATH, seqnr))
|
|
|
|
keyCfg, err := ceremonyConfig("templates/cert-ceremonies/issuer-key.yaml", map[string]string{
|
|
"Module": cfg.Module,
|
|
"UserPIN": cfg.UserPIN,
|
|
"SlotID": cfg.SlotID,
|
|
"Label": cfg.Label,
|
|
"Path": CERT_FILES_PATH,
|
|
"KeyType": keytype,
|
|
"KeyParam": keyparam,
|
|
"Extractable": strconv.FormatBool(true), // For now, with SoftHSM, this is fine. In future we need to ask for informed consent!
|
|
"SeqNr": seqnr,
|
|
})
|
|
if err != nil {
|
|
ci.Errors["Generate"] = "error preparing for issuer key ceremony, see logs for details"
|
|
pb.Restore()
|
|
jb.Restore()
|
|
return "", fmt.Errorf("could not fill issuer key ceremony template: %s", err.Error())
|
|
}
|
|
defer func() { _ = os.Remove(keyCfg) }()
|
|
|
|
err = waitForFile("/opt/boulder/bin/ceremony")
|
|
if err != nil {
|
|
return "", fmt.Errorf("could not wait for /opt/boulder/bin/ceremony to exist: %s", err.Error())
|
|
}
|
|
|
|
if _, err = exeCmd("/opt/boulder/bin/ceremony -config " + keyCfg); err != nil {
|
|
ci.Errors["Generate"] = "failed to execute issuer key ceremony, see logs for details"
|
|
pb.Restore()
|
|
jb.Restore()
|
|
return "", err
|
|
}
|
|
|
|
pb.Remove()
|
|
jb.Remove()
|
|
}
|
|
|
|
cfg = &HSMConfig{}
|
|
cfg.Initialize("root", rootseqnr)
|
|
if err := cfg.CreateSlot(); err != nil {
|
|
return "", fmt.Errorf("failed to get root slot: %s", err.Error())
|
|
}
|
|
|
|
certFileName := fmt.Sprintf("%sissuer-%s-cert.pem", CERT_FILES_PATH, seqnr)
|
|
cb := renameBackup(certFileName)
|
|
|
|
ceremonyCfg, err := ceremonyConfig("templates/cert-ceremonies/issuer-cert.yaml", map[string]string{
|
|
"Module": cfg.Module,
|
|
"UserPIN": cfg.UserPIN,
|
|
"RootSlotID": cfg.SlotID,
|
|
"RootLabel": cfg.Label,
|
|
"Path": CERT_FILES_PATH,
|
|
"SeqNr": seqnr,
|
|
"RootSeqNr": rootseqnr,
|
|
"SignAlgorithm": algo,
|
|
"CommonName": ci.CommonName,
|
|
"OrgName": ci.Organization,
|
|
"Country": ci.Country,
|
|
"NotBefore": notbefore.UTC().Format("2006-01-02 15:04:05"),
|
|
"NotAfter": notafter.UTC().Format("2006-01-02 15:04:05"),
|
|
"CrlUrl": fmt.Sprintf("http://%s/crl/root-%s-crl.pem", fqdn, rootseqnr),
|
|
"IssuerUrl": fmt.Sprintf("http://%s/certs/root-%s-cert.pem", fqdn, rootseqnr),
|
|
})
|
|
if err != nil {
|
|
ci.Errors["Generate"] = "error preparing for issuer cert ceremony, see logs for details"
|
|
cb.Restore()
|
|
return "", fmt.Errorf("could not fill issuer cert ceremony template: %s", err.Error())
|
|
}
|
|
defer func() { _ = os.Remove(ceremonyCfg) }()
|
|
|
|
err = waitForFile("/opt/boulder/bin/ceremony")
|
|
if err != nil {
|
|
return "", fmt.Errorf("could not wait for /opt/boulder/bin/ceremony to exist: %s", err.Error())
|
|
}
|
|
|
|
if _, err = exeCmd("/opt/boulder/bin/ceremony -config " + ceremonyCfg); err != nil {
|
|
ci.Errors["Generate"] = "failed to execute issuer cert ceremony, see logs for details"
|
|
cb.Restore()
|
|
return "", err
|
|
}
|
|
|
|
cb.Remove()
|
|
return certFileName, nil
|
|
}
|
|
|
|
func readCertificate(filename string) (*x509.Certificate, error) {
|
|
read, err := os.ReadFile(filename)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return nil, errors.New("could not read '" + filename + "': " + err.Error())
|
|
}
|
|
block, _ := pem.Decode(read)
|
|
if block == nil || block.Type != "CERTIFICATE" {
|
|
fmt.Println(block)
|
|
return nil, errors.New("failed to decode PEM block containing certificate")
|
|
}
|
|
crt, err := x509.ParseCertificate(block.Bytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return crt, nil
|
|
}
|
|
|
|
func (ci *CertificateInfo) CeremonyRootCRL(seqnr string) error {
|
|
now := time.Now()
|
|
|
|
if viper.Get("crl_root_days") == nil || viper.Get("crl_root_days") == "" {
|
|
viper.Set("crl_root_days", 365)
|
|
_ = viper.WriteConfig()
|
|
}
|
|
crlint, err := time.ParseDuration(fmt.Sprintf("%dh", viper.GetInt("crl_root_days")*24-1))
|
|
if err != nil {
|
|
crlint, _ = time.ParseDuration("8759h") // 365 days - 1 hour
|
|
}
|
|
|
|
cert, err := readCertificate(fmt.Sprintf("%sroot-%s-cert.pem", CERT_FILES_PATH, seqnr))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
thisupdate := now
|
|
if thisupdate.Before(cert.NotBefore) {
|
|
thisupdate = cert.NotBefore.Add(1 * time.Second)
|
|
}
|
|
|
|
nextupdate := now.Add(crlint)
|
|
maxNext := cert.NotAfter.Add(-1 * time.Second)
|
|
if nextupdate.After(maxNext) {
|
|
nextupdate = maxNext
|
|
}
|
|
if nextupdate.Sub(thisupdate) > time.Hour*24*365 {
|
|
nextupdate = thisupdate.Add(time.Hour * 24 * 365).Add(-1 * time.Second)
|
|
}
|
|
|
|
crlnumber := fmt.Sprintf("%02d%03d%s", now.Year()-2000, now.YearDay(), seqnr)
|
|
|
|
cb := renameBackup(fmt.Sprintf("%sroot-%s-crl.pem", CERT_FILES_PATH, seqnr))
|
|
|
|
cfg := &HSMConfig{}
|
|
cfg.Initialize("root", seqnr)
|
|
if err := cfg.CreateSlot(); err != nil {
|
|
return fmt.Errorf("failed to get root slot: %s", err.Error())
|
|
}
|
|
|
|
keyCfg, err := ceremonyConfig("templates/cert-ceremonies/root-crl.yaml", map[string]string{
|
|
"Module": cfg.Module,
|
|
"UserPIN": cfg.UserPIN,
|
|
"RootSlotID": cfg.SlotID,
|
|
"RootLabel": cfg.Label,
|
|
"Path": CERT_FILES_PATH,
|
|
"RootSeqNr": seqnr,
|
|
"ThisUpdate": thisupdate.UTC().Format("2006-01-02 15:04:05"),
|
|
"NextUpdate": nextupdate.UTC().Format("2006-01-02 15:04:05"),
|
|
"CrlNumber": crlnumber,
|
|
})
|
|
if err != nil {
|
|
ci.Errors["CRL"] = "error preparing for root crl ceremony, see logs for details"
|
|
cb.Restore()
|
|
return fmt.Errorf("could not fill root crl ceremony template: %s", err.Error())
|
|
}
|
|
defer func() { _ = os.Remove(keyCfg) }()
|
|
|
|
err = waitForFile("/opt/boulder/bin/ceremony")
|
|
if err != nil {
|
|
return fmt.Errorf("could not wait for /opt/boulder/bin/ceremony to exist: %s", err.Error())
|
|
}
|
|
|
|
if _, err = exeCmd("/opt/boulder/bin/ceremony -config " + keyCfg); err != nil {
|
|
ci.Errors["CRL"] = "failed to execute root crl ceremony, see logs for details"
|
|
cb.Restore()
|
|
return err
|
|
}
|
|
|
|
cb.Remove()
|
|
return nil
|
|
}
|
|
|
|
// Generate a key and certificate file for the data from this CertificateInfo
|
|
func (ci *CertificateInfo) Generate(certBase string) error {
|
|
var err error
|
|
if ci.IsRoot {
|
|
_, err = ci.CeremonyRoot("01", false)
|
|
|
|
viper.Set("crl_root_days", ci.NumDays)
|
|
_ = viper.WriteConfig()
|
|
} else {
|
|
_, err = ci.CeremonyIssuer("01", "01", false)
|
|
}
|
|
|
|
if err != nil {
|
|
log.Printf("failed to create certificate: %s", err.Error())
|
|
return errors.New("failed to create certificate, see logs for details")
|
|
}
|
|
|
|
if !ci.IsRoot {
|
|
// Create CRLs stating that the intermediates are not revoked.
|
|
err = ci.CeremonyRootCRL("01")
|
|
|
|
if err != nil {
|
|
log.Printf("failed to create crl: %s", err.Error())
|
|
return errors.New("failed to create crl, see logs for details")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ImportPkcs12 imports an uploaded PKCS#12 / PFX file
|
|
func (ci *CertificateInfo) ImportPkcs12(tmpFile string, tmpKey string, tmpCert string) error {
|
|
if ci.IsRoot {
|
|
if (strings.Index(ci.ImportHandler.Filename, "labca-root-01-cert") != 0) && (strings.Index(ci.ImportHandler.Filename, "labca_root") != 0) {
|
|
fmt.Printf("WARNING: importing root from .pfx file but name is %s\n", ci.ImportHandler.Filename)
|
|
}
|
|
} else {
|
|
if (strings.Index(ci.ImportHandler.Filename, "labca-issuer-01-cert") != 0) && (strings.Index(ci.ImportHandler.Filename, "labca_issuer") != 0) {
|
|
fmt.Printf("WARNING: importing issuer from .pfx file but name is %s\n", ci.ImportHandler.Filename)
|
|
}
|
|
}
|
|
|
|
pwd := "pass:dummy"
|
|
if ci.ImportPwd != "" {
|
|
pwd = "pass:" + strings.ReplaceAll(ci.ImportPwd, " ", "\\\\")
|
|
}
|
|
|
|
if out, err := exeCmd("openssl pkcs12 -in " + strings.ReplaceAll(tmpFile, " ", "\\\\") + " -password " + pwd + " -nocerts -nodes -out " + tmpKey); err != nil {
|
|
if strings.Contains(string(out), "invalid password") {
|
|
return errors.New("incorrect password")
|
|
}
|
|
|
|
return reportError(err)
|
|
}
|
|
if out, err := exeCmd("openssl pkcs12 -in " + strings.ReplaceAll(tmpFile, " ", "\\\\") + " -password " + pwd + " -nokeys -out " + tmpCert); err != nil {
|
|
if strings.Contains(string(out), "invalid password") {
|
|
return errors.New("incorrect password")
|
|
}
|
|
|
|
return reportError(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ImportZip imports an uploaded ZIP file
|
|
func (ci *CertificateInfo) ImportZip(tmpFile string, tmpDir string) error {
|
|
if ci.IsRoot {
|
|
if (strings.Index(ci.ImportHandler.Filename, "labca-root-01-cert") != 0) && (strings.Index(ci.ImportHandler.Filename, "labca_root") != 0) && (strings.Index(ci.ImportHandler.Filename, "labca_certificates") != 0) {
|
|
fmt.Printf("WARNING: importing root from .zip file but name is %s\n", ci.ImportHandler.Filename)
|
|
}
|
|
} else {
|
|
if (strings.Index(ci.ImportHandler.Filename, "labca-issuer-01-cert") != 0) && (strings.Index(ci.ImportHandler.Filename, "labca_issuer") != 0) {
|
|
fmt.Printf("WARNING: importing issuer from .zip file but name is %s\n", ci.ImportHandler.Filename)
|
|
}
|
|
}
|
|
|
|
cmd := "unzip -j"
|
|
if ci.ImportPwd != "" {
|
|
cmd = cmd + " -P " + strings.ReplaceAll(ci.ImportPwd, " ", "\\\\")
|
|
} else {
|
|
cmd = cmd + " -P dummy"
|
|
}
|
|
cmd = cmd + " " + strings.ReplaceAll(tmpFile, " ", "\\\\") + " -d " + tmpDir
|
|
|
|
if _, err := exeCmd(cmd); err != nil {
|
|
if err.Error() == "exit status 82" {
|
|
return errors.New("incorrect password")
|
|
}
|
|
|
|
return reportError(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Import a certificate and key file
|
|
func (ci *CertificateInfo) Import(tmpDir string, tmpKey string, tmpCert string) error {
|
|
tmpFile := filepath.Join(tmpDir, ci.ImportHandler.Filename)
|
|
|
|
f, err := os.OpenFile(tmpFile, os.O_WRONLY|os.O_CREATE, 0666)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
|
|
_, _ = io.Copy(f, ci.ImportFile)
|
|
|
|
contentType := ci.ImportHandler.Header.Get("Content-Type")
|
|
switch contentType {
|
|
case "application/x-pkcs12":
|
|
err := ci.ImportPkcs12(tmpFile, tmpKey, tmpCert)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case "application/zip", "application/x-zip-compressed":
|
|
err := ci.ImportZip(tmpFile, tmpDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return errors.New("Content Type '" + contentType + "' not supported!")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Upload a certificate and key file
|
|
func (ci *CertificateInfo) Upload(tmpKey string, tmpCert string) error {
|
|
if ci.Key != "" {
|
|
if err := os.WriteFile(tmpKey, []byte(ci.Key), 0644); err != nil {
|
|
return err
|
|
}
|
|
|
|
pwd := "pass:dummy"
|
|
if ci.Passphrase != "" {
|
|
pwd = "pass:" + strings.ReplaceAll(ci.Passphrase, " ", "\\\\")
|
|
}
|
|
|
|
if out, err := exeCmd("openssl pkey -passin " + pwd + " -in " + tmpKey + " -out " + tmpKey + "-out"); err != nil {
|
|
if strings.Contains(string(out), ":bad decrypt:") {
|
|
return errors.New("incorrect password")
|
|
}
|
|
|
|
return reportError(err)
|
|
}
|
|
|
|
if _, err := exeCmd("mv " + tmpKey + "-out " + tmpKey); err != nil {
|
|
return reportError(err)
|
|
}
|
|
}
|
|
|
|
if err := os.WriteFile(tmpCert, []byte(ci.Certificate), 0644); err != nil {
|
|
return err
|
|
}
|
|
|
|
if out, err := exeCmd("openssl x509 -in " + tmpCert + " -out " + tmpCert + "-out"); err != nil {
|
|
return reportError(out)
|
|
}
|
|
|
|
if _, err := exeCmd("mv " + tmpCert + "-out " + tmpCert); err != nil {
|
|
return reportError(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseSubjectDn(subject string) map[string]string {
|
|
trackerResultMap := map[string]string{"C=": "", "C =": "", "O=": "", "O =": "", "CN=": "", "CN =": "", "OU=": "", "OU =": ""}
|
|
|
|
for tracker := range trackerResultMap {
|
|
index := strings.Index(subject, tracker)
|
|
|
|
if index < 0 {
|
|
continue
|
|
}
|
|
|
|
var res string
|
|
// track quotes for delimited fields so we know not to split on the comma
|
|
quoteCount := 0
|
|
|
|
for i := index + len(tracker); i < len(subject); i++ {
|
|
char := subject[i]
|
|
|
|
// if ", we need to count and delimit
|
|
if char == 34 {
|
|
quoteCount++
|
|
if quoteCount == 2 {
|
|
break
|
|
} else {
|
|
continue
|
|
}
|
|
}
|
|
|
|
// comma, lets stop here but only if we don't have quotes
|
|
if char == 44 && quoteCount == 0 {
|
|
break
|
|
}
|
|
|
|
// add this individual char
|
|
res += string(rune(char))
|
|
}
|
|
|
|
trackerResultMap[strings.TrimSpace(strings.TrimSuffix(tracker, "="))] = strings.TrimSpace(strings.TrimPrefix(res, "="))
|
|
}
|
|
|
|
for k, v := range trackerResultMap {
|
|
if len(v) == 0 {
|
|
delete(trackerResultMap, k)
|
|
}
|
|
}
|
|
|
|
return trackerResultMap
|
|
}
|
|
|
|
// VerifyCerts verifies the root and the issuer certificates
|
|
func (ci *CertificateInfo) VerifyCerts(path string, rootCert string, rootKey string, issuerCert string, issuerKey string) error {
|
|
var rootSubject string
|
|
if (rootCert != "") && (rootKey != "") {
|
|
r, err := exeCmd("openssl x509 -noout -subject -in " + rootCert)
|
|
if err != nil {
|
|
return reportError(err)
|
|
}
|
|
|
|
rootSubject = string(r[0 : len(r)-1])
|
|
fmt.Printf("Import root with subject '%s'\n", rootSubject)
|
|
|
|
subjectMap := parseSubjectDn(rootSubject)
|
|
if val, ok := subjectMap["C"]; ok {
|
|
ci.Country = val
|
|
}
|
|
if val, ok := subjectMap["O"]; ok {
|
|
ci.Organization = val
|
|
}
|
|
if val, ok := subjectMap["CN"]; ok {
|
|
ci.CommonName = val
|
|
}
|
|
|
|
keyFileExists := true
|
|
if _, err := os.Stat(rootKey); errors.Is(err, fs.ErrNotExist) {
|
|
keyFileExists = false
|
|
}
|
|
if keyFileExists {
|
|
_, err = exeCmd("openssl pkey -noout -in " + rootKey)
|
|
if err != nil {
|
|
return reportError(err)
|
|
}
|
|
|
|
fmt.Println("Import root key")
|
|
}
|
|
}
|
|
|
|
if (issuerCert != "") && (issuerKey != "") {
|
|
r, err := exeCmd("openssl x509 -noout -subject -in " + issuerCert)
|
|
if err != nil {
|
|
return reportError(err)
|
|
}
|
|
|
|
fmt.Printf("Import issuer with subject '%s'\n", string(r[0:len(r)-1]))
|
|
|
|
r, err = exeCmd("openssl x509 -noout -issuer -in " + issuerCert)
|
|
if err != nil {
|
|
return reportError(err)
|
|
}
|
|
|
|
issuerIssuer := string(r[0 : len(r)-1])
|
|
fmt.Printf("Issuer certificate issued by CA '%s'\n", issuerIssuer)
|
|
|
|
if rootSubject == "" {
|
|
r, err := exeCmd("openssl x509 -noout -subject -in " + CERT_FILES_PATH + "root-01-cert.pem")
|
|
if err != nil {
|
|
return reportError(err)
|
|
}
|
|
|
|
rootSubject = string(r[0 : len(r)-1])
|
|
}
|
|
|
|
issuerIssuer = strings.ReplaceAll(issuerIssuer, "issuer=", "")
|
|
rootSubject = strings.ReplaceAll(rootSubject, "subject=", "")
|
|
if issuerIssuer != rootSubject {
|
|
return errors.New("issuer not issued by our Root CA")
|
|
}
|
|
|
|
_, err = exeCmd("openssl verify -CAfile " + CERT_FILES_PATH + "root-01-cert.pem " + issuerCert)
|
|
if err != nil {
|
|
return errors.New("could not verify that issuer was issued by our Root CA")
|
|
}
|
|
|
|
_, err = exeCmd("openssl pkey -noout -in " + issuerKey)
|
|
if err != nil {
|
|
return reportError(err)
|
|
}
|
|
|
|
fmt.Println("Import issuer key")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ImportFiles moves certificate files to their final location and imports the keys into the HSM
|
|
func (ci *CertificateInfo) ImportFiles(path string, rootCert string, rootKey string, issuerCert string, issuerKey string) error {
|
|
if rootKey != "" {
|
|
keyFileExists := true
|
|
if _, err := os.Stat(rootKey); errors.Is(err, fs.ErrNotExist) {
|
|
keyFileExists = false
|
|
}
|
|
if keyFileExists {
|
|
rootseqnr := "01"
|
|
cfg := &HSMConfig{}
|
|
cfg.Initialize("root", rootseqnr)
|
|
if err := cfg.CreateSlot(); err != nil {
|
|
return fmt.Errorf("failed to create root slot: %s", err.Error())
|
|
}
|
|
|
|
pubKey, err := cfg.ImportKeyCert(rootKey, rootCert)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to import root key: %s", err.Error())
|
|
}
|
|
|
|
var pubKeyBytes []byte
|
|
if reflect.TypeOf(pubKey).String() == "rsa.PublicKey" {
|
|
pk := pubKey.(rsa.PublicKey)
|
|
pubKeyBytes, err = x509.MarshalPKIXPublicKey(&pk)
|
|
} else if reflect.TypeOf(pubKey).String() == "ecdsa.PublicKey" {
|
|
pk := pubKey.(ecdsa.PublicKey)
|
|
pubKeyBytes, err = x509.MarshalPKIXPublicKey(&pk)
|
|
} else {
|
|
return fmt.Errorf("unknown private key type: %s", reflect.TypeOf(pubKey).String())
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal root pubkey: %s", err.Error())
|
|
}
|
|
file, err := os.Create(fmt.Sprintf("%sroot-%s-pubkey.pem", CERT_FILES_PATH, rootseqnr))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create root pubkey file: %s", err.Error())
|
|
}
|
|
defer func() { _ = file.Close() }()
|
|
if err := pem.Encode(file, &pem.Block{Type: "PUBLIC KEY", Bytes: pubKeyBytes}); err != nil {
|
|
return fmt.Errorf("failed to write root pubkey: %s", err.Error())
|
|
}
|
|
}
|
|
}
|
|
if rootCert != "" {
|
|
if _, err := exeCmd("mv " + rootCert + " " + path); err != nil {
|
|
return reportError(err)
|
|
}
|
|
}
|
|
|
|
if issuerKey != "" {
|
|
seqnr := "01"
|
|
cfg := &HSMConfig{}
|
|
cfg.Initialize("issuer", seqnr)
|
|
if err := cfg.CreateSlot(); err != nil {
|
|
return fmt.Errorf("failed to create issuer slot: %s", err.Error())
|
|
}
|
|
|
|
pubKey, err := cfg.ImportKeyCert(issuerKey, issuerCert)
|
|
if err != nil {
|
|
return reportError(err)
|
|
}
|
|
|
|
var pubKeyBytes []byte
|
|
if reflect.TypeOf(pubKey).String() == "rsa.PublicKey" {
|
|
pk := pubKey.(rsa.PublicKey)
|
|
pubKeyBytes, err = x509.MarshalPKIXPublicKey(&pk)
|
|
} else if reflect.TypeOf(pubKey).String() == "ecdsa.PublicKey" {
|
|
pk := pubKey.(ecdsa.PublicKey)
|
|
pubKeyBytes, err = x509.MarshalPKIXPublicKey(&pk)
|
|
} else {
|
|
return fmt.Errorf("unknown private key type: %s", reflect.TypeOf(pubKey).String())
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal issuer pubkey: %s", err.Error())
|
|
}
|
|
file, err := os.Create(fmt.Sprintf("%sissuer-%s-pubkey.pem", CERT_FILES_PATH, seqnr))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create issuer pubkey file: %s", err.Error())
|
|
}
|
|
defer func() { _ = file.Close() }()
|
|
if err := pem.Encode(file, &pem.Block{Type: "PUBLIC KEY", Bytes: pubKeyBytes}); err != nil {
|
|
return fmt.Errorf("failed to write issuer pubkey: %s", err.Error())
|
|
}
|
|
}
|
|
if issuerCert != "" {
|
|
if _, err := exeCmd("mv " + issuerCert + " " + path); err != nil {
|
|
return reportError(err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Extract key and certificate files from a container file
|
|
func (ci *CertificateInfo) Extract(certBase string, tmpDir string, wasCSR bool) error {
|
|
var rootCert string
|
|
var rootKey string
|
|
var issuerCert string
|
|
var issuerKey string
|
|
|
|
path := CERT_FILES_PATH // TODO !!
|
|
|
|
if ci.IsRoot {
|
|
rootCert = filepath.Join(tmpDir, "root-01-cert.pem")
|
|
rootKey = filepath.Join(tmpDir, "root-01-key.pem")
|
|
|
|
if _, err := os.Stat(rootCert); errors.Is(err, fs.ErrNotExist) {
|
|
altCert := filepath.Join(tmpDir, "root-ca.pem")
|
|
if _, err = os.Stat(altCert); err == nil {
|
|
if _, err := exeCmd("mv " + altCert + " " + rootCert); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
altKey := filepath.Join(tmpDir, "root-ca.key")
|
|
if _, err = os.Stat(altKey); err == nil {
|
|
if _, err := exeCmd("mv " + altKey + " " + rootKey); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
if _, err := os.Stat(rootCert); errors.Is(err, fs.ErrNotExist) {
|
|
altCert := filepath.Join(tmpDir, "test-root.pem")
|
|
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 certificate")
|
|
}
|
|
}
|
|
}
|
|
|
|
issuerCert = filepath.Join(tmpDir, "issuer-01-cert.pem")
|
|
issuerKey = filepath.Join(tmpDir, "issuer-01-key.pem")
|
|
|
|
if _, err := os.Stat(issuerCert); errors.Is(err, fs.ErrNotExist) {
|
|
if ci.IsRoot {
|
|
issuerCert = ""
|
|
} else {
|
|
altCert := filepath.Join(tmpDir, "ca-int.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) {
|
|
altCert := filepath.Join(tmpDir, "test-ca.pem")
|
|
if _, err = os.Stat(altCert); err == nil {
|
|
if _, err := exeCmd("mv " + altCert + " " + issuerCert); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if _, err := os.Stat(issuerCert); errors.Is(err, fs.ErrNotExist) {
|
|
return errors.New("file does not contain issuer certificate")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if _, err := os.Stat(issuerKey); errors.Is(err, fs.ErrNotExist) {
|
|
if ci.IsRoot || wasCSR {
|
|
issuerKey = ""
|
|
} else {
|
|
altKey := filepath.Join(tmpDir, "ca-int.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) {
|
|
altKey := filepath.Join(tmpDir, "test-ca.key")
|
|
if _, err = os.Stat(altKey); err == nil {
|
|
if _, err := exeCmd("mv " + altKey + " " + issuerKey); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if _, err := os.Stat(issuerKey); errors.Is(err, fs.ErrNotExist) {
|
|
return errors.New("file does not contain issuer key")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
err := ci.VerifyCerts(path, rootCert, rootKey, issuerCert, issuerKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// All is good now, move files to their permanent location...
|
|
err = ci.ImportFiles(path, rootCert, rootKey, issuerCert, issuerKey)
|
|
if err != nil {
|
|
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
|
|
// TODO: adjust for max root ceremony value...
|
|
viper.Set("crl_root_days", int(math.Ceil(numDays)))
|
|
_ = viper.WriteConfig()
|
|
|
|
} else {
|
|
// Create CRLs stating that the intermediates are not revoked.
|
|
err = ci.CeremonyRootCRL("01")
|
|
|
|
if err != nil {
|
|
log.Printf("failed to create crl: %s", err.Error())
|
|
return errors.New("failed to create crl, see logs for details")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Create a new pair of key + certificate files based on the info in CertificateInfo
|
|
func (ci *CertificateInfo) Create(certBase string, wasCSR bool) error {
|
|
tmpDir, err := os.MkdirTemp("", "labca")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() { _ = os.RemoveAll(tmpDir) }()
|
|
|
|
var tmpKey string
|
|
var tmpCert string
|
|
if ci.IsRoot {
|
|
tmpKey = filepath.Join(tmpDir, "root-01-key.pem")
|
|
tmpCert = filepath.Join(tmpDir, "root-01-cert.pem")
|
|
} else {
|
|
tmpKey = filepath.Join(tmpDir, "issuer-01-key.pem")
|
|
tmpCert = filepath.Join(tmpDir, "issuer-01-cert.pem")
|
|
}
|
|
|
|
switch ci.CreateType {
|
|
case "generate":
|
|
err := ci.Generate(certBase)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case "import":
|
|
err := ci.Import(tmpDir, tmpKey, tmpCert)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case "upload":
|
|
err := ci.Upload(tmpKey, tmpCert)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return fmt.Errorf("unknown CreateType")
|
|
}
|
|
|
|
// This is shared between pfx/zip import and pem text upload
|
|
if ci.CreateType != "generate" {
|
|
err := ci.Extract(certBase, tmpDir, wasCSR)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
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.ReplaceAll(passphrase, " ", "\\\\")
|
|
|
|
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)
|
|
}
|
|
if strings.TrimSpace(ci.Key) == "" {
|
|
ci.Errors["Modal"] = "Please provide a PEM-encoded key"
|
|
return false
|
|
}
|
|
|
|
tmpDir, err := os.MkdirTemp("", "labca")
|
|
if err != nil {
|
|
ci.Errors["Modal"] = err.Error()
|
|
return false
|
|
}
|
|
|
|
defer func() { _ = os.RemoveAll(tmpDir) }()
|
|
|
|
certBase := "root-01"
|
|
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
|
|
if _, err := exeCmd("openssl ca -config " + path + "openssl.cnf -gencrl -keyfile " + path + certBase + ".key -cert " + path + certBase + ".pem -out " + path + certBase + ".crl"); err != nil {
|
|
fmt.Printf("StoreRootKey: %s\n", err.Error())
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (ci *CertificateInfo) StoreCRL(path string) bool {
|
|
if ci.Errors == nil {
|
|
ci.Errors = make(map[string]string)
|
|
}
|
|
if strings.TrimSpace(ci.CRL) == "" {
|
|
fmt.Println("WARNING: no Root CRL file provided - please upload one from the manage page")
|
|
return true
|
|
}
|
|
|
|
tmpDir, err := os.MkdirTemp("", "labca")
|
|
if err != nil {
|
|
ci.Errors["Modal"] = err.Error()
|
|
return false
|
|
}
|
|
|
|
defer func() { _ = os.RemoveAll(tmpDir) }()
|
|
|
|
tmpCRL := filepath.Join(tmpDir, "root-ca.crl")
|
|
|
|
if err := os.WriteFile(tmpCRL, []byte(ci.CRL), 0644); err != nil {
|
|
ci.Errors["Modal"] = err.Error()
|
|
return false
|
|
}
|
|
|
|
crlIssuer, err := exeCmd("openssl crl -noout -issuer -in " + tmpCRL)
|
|
if err != nil {
|
|
ci.Errors["Modal"] = "Not a CRL file"
|
|
return false
|
|
}
|
|
rootSubj, err := exeCmd("openssl x509 -noout -subject -in " + path + "root-ca.pem")
|
|
if err != nil {
|
|
ci.Errors["Modal"] = "Cannot get Root CA subject"
|
|
return false
|
|
}
|
|
|
|
if strings.TrimPrefix(string(crlIssuer), "issuer=") != strings.TrimPrefix(string(rootSubj), "subject=") {
|
|
ci.Errors["Modal"] = "CRL file does not match the Root CA certificate"
|
|
return false
|
|
}
|
|
|
|
if _, err := exeCmd("mv " + tmpCRL + " " + path); err != nil {
|
|
ci.Errors["Modal"] = err.Error()
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func renewCertificate(certname string, days int, rootname string, _ string, _ string) error {
|
|
ci := &CertificateInfo{
|
|
IsRoot: strings.HasPrefix(certname, "root-"),
|
|
NumDays: days,
|
|
}
|
|
ci.Initialize()
|
|
|
|
certFile := fmt.Sprintf("%s%s.pem", CERT_FILES_PATH, certname)
|
|
rootCertFile := ""
|
|
|
|
if !ci.IsRoot {
|
|
rootCertFile = fmt.Sprintf("%s%s.pem", CERT_FILES_PATH, rootname)
|
|
}
|
|
|
|
seqnr := ""
|
|
re := regexp.MustCompile(`-(\d{2})-`)
|
|
match := re.FindStringSubmatch(certFile)
|
|
if len(match) > 1 {
|
|
seqnr = match[1]
|
|
} else {
|
|
return fmt.Errorf("failed to extract sequence number from filename '%s'", certFile)
|
|
}
|
|
|
|
rootseqnr := ""
|
|
if !ci.IsRoot {
|
|
match := re.FindStringSubmatch(rootCertFile)
|
|
if len(match) > 1 {
|
|
rootseqnr = match[1]
|
|
} else {
|
|
return fmt.Errorf("failed to extract sequence number from filename '%s'", rootCertFile)
|
|
}
|
|
}
|
|
|
|
crt, err := readCertificate(certFile)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read current certificate: %w", err)
|
|
}
|
|
|
|
if crt.PublicKeyAlgorithm == x509.RSA {
|
|
pub := crt.PublicKey.(*rsa.PublicKey)
|
|
if pub.N.BitLen() == 2048 {
|
|
ci.KeyType = "rsa2048"
|
|
}
|
|
if pub.N.BitLen() == 4096 {
|
|
ci.KeyType = "rsa4096"
|
|
}
|
|
}
|
|
if crt.PublicKeyAlgorithm == x509.ECDSA {
|
|
if crt.SignatureAlgorithm == x509.ECDSAWithSHA256 {
|
|
ci.KeyType = "ecdsa256"
|
|
}
|
|
if crt.SignatureAlgorithm == x509.ECDSAWithSHA384 {
|
|
ci.KeyType = "ecdsa384"
|
|
}
|
|
}
|
|
|
|
subjectMap := parseSubjectDn(crt.Subject.String())
|
|
if val, ok := subjectMap["C"]; ok {
|
|
ci.Country = val
|
|
}
|
|
if val, ok := subjectMap["O"]; ok {
|
|
ci.Organization = val
|
|
}
|
|
if val, ok := subjectMap["CN"]; ok {
|
|
ci.CommonName = val
|
|
}
|
|
|
|
if ci.IsRoot {
|
|
_, err = ci.CeremonyRoot(seqnr, true)
|
|
|
|
viper.Set("crl_root_days", ci.NumDays)
|
|
_ = viper.WriteConfig()
|
|
} else {
|
|
_, err = ci.CeremonyIssuer(seqnr, rootseqnr, true)
|
|
}
|
|
|
|
if err != nil {
|
|
log.Printf("failed to create certificate: %s", err.Error())
|
|
return errors.New("failed to create certificate, see logs for details")
|
|
}
|
|
|
|
if !ci.IsRoot {
|
|
// Create CRLs stating that the intermediates are not revoked.
|
|
err = ci.CeremonyRootCRL(rootseqnr)
|
|
|
|
if err != nil {
|
|
log.Printf("failed to create crl: %s", err.Error())
|
|
return errors.New("failed to create crl, see logs for details")
|
|
}
|
|
}
|
|
|
|
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++ {
|
|
parts[i] = strings.ReplaceAll(parts[i], "\\\\", " ")
|
|
}
|
|
head := parts[0]
|
|
parts = parts[1:]
|
|
|
|
out, err := exec.Command(head, parts...).CombinedOutput()
|
|
if err != nil {
|
|
fmt.Print(fmt.Sprint(err) + ": " + string(out))
|
|
}
|
|
return out, err
|
|
}
|