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 }