package main import ( "crypto/x509" "encoding/json" "errors" "fmt" "io" "os" "os/exec" "path/filepath" "regexp" "strings" "github.com/spf13/viper" ) const caConfFile = "/opt/boulder/labca/config/ca.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 { Active bool `json:"active"` 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 KeyType string IsRoot bool ActiveIssuer bool NotAfter string Details string } type CertChain struct { RootCert CertDetails IssuerCerts []CertDetails } func getCertFileKeyType(certFile string) (string, error) { crt, err := readCertificate(certFile) if err != nil { fmt.Println("cannot read certificate file '" + certFile + "': " + fmt.Sprint(err)) return "", err } switch crt.PublicKeyAlgorithm { case x509.RSA: return "RSA", nil case x509.ECDSA: return "ECDSA", nil default: return "", fmt.Errorf("unknown public key algorithm: %s", crt.PublicKeyAlgorithm) } } 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(caConfFile) if err != nil { fmt.Println(err) return nil } defer func() { _ = 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].ActiveIssuer = rawChains[i].Active certFile := locateFile(rawChains[i].Location.CertFile) if kt, err := getCertFileKeyType(certFile); err == nil { chains[k].IssuerCerts[n].KeyType = kt } 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 kt, err := getCertFileKeyType(certFile); err == nil { chains[k].RootCert.KeyType = kt } 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 func() { _ = 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, active string) error { caConf, err := os.Open(filename) if err != nil { fmt.Println(err) return errors.New("could not open config file: " + err.Error()) } defer func() { _ = 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 foundActive := false for i := 0; i < len(result.CA.Issuance.Issuers); i++ { if strings.Contains(result.CA.Issuance.Issuers[i].Location.CertFile, active) { foundActive = true } } if !foundActive { return errors.New("certificate '" + active + "' 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 active != "" { result.CA.Issuance.Issuers[i].Active = strings.Contains(result.CA.Issuance.Issuers[i].Location.CertFile, active) } } // 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(active string) error { if err := exec.Command("cp", "-f", caConfFile, caConfFile+"_BAK").Run(); err != nil { return errors.New("could not create ca backup file: " + err.Error()) } if err := setUseForLeavesFile(caConfFile, active); err != nil { _ = exec.Command("mv", caConfFile+"_BAK", caConfFile).Run() return err } _ = exec.Command("rm", caConfFile+"_BAK").Run() if active != "" { viper.Set("certs.activeIssuer", active) } if active != "" { _ = viper.WriteConfig() } return nil }