Refactored code to one IssueCertificate function.

This commit is contained in:
2025-07-27 20:00:43 +02:00
parent 0c32da1e84
commit 10ec83273d

350
ca.go
View File

@@ -287,6 +287,7 @@ func IssueCertificate(configPath, subject, certType, validity string, san []stri
} }
successes := 0 successes := 0
errors := 0 errors := 0
var basename, certFile, keyFile string // Declare variables before the loop
for i, def := range certDefs { for i, def := range certDefs {
if defaults != nil { if defaults != nil {
if def.Type == "" { if def.Type == "" {
@@ -315,23 +316,161 @@ func IssueCertificate(configPath, subject, certType, validity string, san []stri
fmt.Printf(" SAN: %v\n\n", finalDef.SAN) fmt.Printf(" SAN: %v\n\n", finalDef.SAN)
} }
basename := finalDef.Name + "." + finalDef.Type basename = finalDef.Name
if basename == "" {
basename = finalDef.Subject
}
certFile = filepath.Join(ca.Paths.Certificates, basename+"."+finalDef.Type+".crt.pem")
keyFile = filepath.Join(ca.Paths.PrivateKeys, basename+"."+finalDef.Type+".key.pem")
if dryRun { if dryRun {
successes++ successes++
continue continue
} }
err := IssueCertificateWithBasename(configPath, basename, finalDef.Subject, finalDef.Type, finalDef.Validity, finalDef.SAN, overwrite, dryRun) // Inline certificate issuance logic for batch mode
if err != nil { // Add default dns SAN for server/server-only if none specified
fmt.Printf("ERROR: %v\n", err) if (finalDef.Type == "server" || finalDef.Type == "server-only") && len(finalDef.SAN) == 0 {
errors++ finalDef.SAN = append(finalDef.SAN, "dns:"+finalDef.Subject)
} else {
if !verbose {
fmt.Printf("done\n")
}
successes++
} }
caCertPath := filepath.Join(ca.Paths.Certificates, "ca_cert.pem")
caKeyPath := filepath.Join(ca.Paths.PrivateKeys, "ca_key.pem")
caCertPEM, err := os.ReadFile(caCertPath)
if err != nil {
fmt.Println("Error reading CA certificate file:", err)
errors++
continue
}
caKeyPEM, err := os.ReadFile(caKeyPath)
if err != nil {
fmt.Println("Error reading CA key file:", err)
errors++
continue
}
caCertBlock, _ := pem.Decode(caCertPEM)
if caCertBlock == nil {
fmt.Println("Failed to parse CA certificate PEM")
errors++
continue
}
caCert, err := x509.ParseCertificate(caCertBlock.Bytes)
if err != nil {
fmt.Println("Failed to parse CA certificate:", err)
errors++
continue
}
caKeyBlock, _ := pem.Decode(caKeyPEM)
if caKeyBlock == nil {
fmt.Println("Failed to parse CA key PEM")
errors++
continue
}
caKey, err := x509.ParsePKCS1PrivateKey(caKeyBlock.Bytes)
if err != nil {
fmt.Println("Failed to parse CA private key:", err)
errors++
continue
}
priv, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
fmt.Println("Failed to generate private key:", err)
errors++
continue
}
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
fmt.Println("Failed to generate serial number:", err)
errors++
continue
}
var validityDur time.Duration
if finalDef.Validity != "" {
validityDur, err = parseValidity(finalDef.Validity)
if err != nil {
fmt.Println("Invalid validity value:", err)
errors++
continue
}
} else {
validityDur = 365 * 24 * time.Hour // default 1 year
}
var subjectPKIX pkix.Name
if isDNFormat(finalDef.Subject) {
subjectPKIX = parseDistinguishedName(finalDef.Subject)
} else {
subjectPKIX = pkix.Name{CommonName: finalDef.Subject}
}
certTmpl := x509.Certificate{
SerialNumber: serialNumber,
Subject: subjectPKIX,
NotBefore: time.Now(),
NotAfter: time.Now().Add(validityDur),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
}
for _, s := range finalDef.SAN {
sLower := strings.ToLower(s)
var val string
if n, _ := fmt.Sscanf(sLower, "dns:%s", &val); n == 1 {
certTmpl.DNSNames = append(certTmpl.DNSNames, val)
} else if n, _ := fmt.Sscanf(sLower, "ip:%s", &val); n == 1 {
certTmpl.IPAddresses = append(certTmpl.IPAddresses, net.ParseIP(val))
} else if n, _ := fmt.Sscanf(sLower, "email:%s", &val); n == 1 {
certTmpl.EmailAddresses = append(certTmpl.EmailAddresses, val)
} else {
fmt.Printf("Invalid SAN format: %s\n", s)
errors++
continue
}
}
switch finalDef.Type {
case "client":
certTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}
case "server":
certTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}
case "server-only":
certTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
case "code-signing":
certTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}
case "email":
certTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageEmailProtection}
default:
fmt.Println("Unknown certificate type. Use one of: client, server, server-only, code-signing, email.")
errors++
continue
}
certDER, err := x509.CreateCertificate(rand.Reader, &certTmpl, caCert, &priv.PublicKey, caKey)
if err != nil {
fmt.Println("Failed to create certificate:", err)
errors++
continue
}
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
if err := SavePEM(certFile, certPEM, false, overwrite); err != nil {
fmt.Println("Error saving certificate:", err)
errors++
continue
}
if err := SavePEM(keyFile, keyPEM, true, overwrite); err != nil {
fmt.Println("Error saving key:", err)
errors++
continue
}
if !verbose {
fmt.Printf("done\n")
}
successes++
} }
fmt.Printf("Batch complete: %d succeeded, %d failed.\n", successes, errors) fmt.Printf("Batch complete: %d succeeded, %d failed.\n", successes, errors)
// Save CA state after batch issuance // Save CA state after batch issuance
@@ -357,23 +496,13 @@ func IssueCertificate(configPath, subject, certType, validity string, san []stri
fmt.Printf(" Validity: %s\n", finalDef.Validity) fmt.Printf(" Validity: %s\n", finalDef.Validity)
fmt.Printf(" SAN: %v\n", finalDef.SAN) fmt.Printf(" SAN: %v\n", finalDef.SAN)
} }
internalIssueCertificate(configPath, finalDef.Name, finalDef.Subject, finalDef.Type, finalDef.Validity, finalDef.SAN, overwrite) // Inline the logic from internalIssueCertificate here
// Save CA state after single issuance
caDir := filepath.Dir(configPath)
caLabel := ca.Label
caStatePath := filepath.Join(caDir, caLabel+"_state.json")
if err := SaveCAState(caStatePath, GlobalCAState); err != nil {
fmt.Printf("Error saving CA state: %v\n", err)
}
}
func internalIssueCertificate(configPath, name string, subject, certType, validityFlag string, san []string, overwrite bool) {
// Add default dns SAN for server/server-only if none specified // Add default dns SAN for server/server-only if none specified
if (certType == "server" || certType == "server-only") && len(san) == 0 { if (finalDef.Type == "server" || finalDef.Type == "server-only") && len(finalDef.SAN) == 0 {
san = append(san, "dns:"+subject) finalDef.SAN = append(finalDef.SAN, "dns:"+finalDef.Subject)
} }
ca, err := LoadCA(configPath) ca, err = LoadCA(configPath)
if err != nil { if err != nil {
fmt.Println("Error loading config:", err) fmt.Println("Error loading config:", err)
return return
@@ -426,35 +555,35 @@ func internalIssueCertificate(configPath, name string, subject, certType, validi
return return
} }
var validity time.Duration var validityDur time.Duration
if validityFlag != "" { if finalDef.Validity != "" {
validity, err = parseValidity(validityFlag) validityDur, err = parseValidity(finalDef.Validity)
if err != nil { if err != nil {
fmt.Println("Invalid validity value:", err) fmt.Println("Invalid validity value:", err)
return return
} }
} else { } else {
validity = 365 * 24 * time.Hour // default 1 year validityDur = 365 * 24 * time.Hour // default 1 year
} }
// Parse subject as DN if it looks like a DN, otherwise use as CommonName only // Parse subject as DN if it looks like a DN, otherwise use as CommonName only
var subjectPKIX pkix.Name var subjectPKIX pkix.Name
if isDNFormat(subject) { if isDNFormat(finalDef.Subject) {
subjectPKIX = parseDistinguishedName(subject) subjectPKIX = parseDistinguishedName(finalDef.Subject)
} else { } else {
subjectPKIX = pkix.Name{CommonName: subject} subjectPKIX = pkix.Name{CommonName: finalDef.Subject}
} }
certTmpl := x509.Certificate{ certTmpl := x509.Certificate{
SerialNumber: serialNumber, SerialNumber: serialNumber,
Subject: subjectPKIX, Subject: subjectPKIX,
NotBefore: time.Now(), NotBefore: time.Now(),
NotAfter: time.Now().Add(validity), NotAfter: time.Now().Add(validityDur),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
} }
// Handle SANs // Handle SANs
for _, s := range san { for _, s := range finalDef.SAN {
sLower := strings.ToLower(s) sLower := strings.ToLower(s)
var val string var val string
if n, _ := fmt.Sscanf(sLower, "dns:%s", &val); n == 1 { if n, _ := fmt.Sscanf(sLower, "dns:%s", &val); n == 1 {
@@ -469,7 +598,7 @@ func internalIssueCertificate(configPath, name string, subject, certType, validi
} }
} }
switch certType { switch finalDef.Type {
case "client": case "client":
certTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth} certTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}
case "server": case "server":
@@ -493,12 +622,12 @@ func internalIssueCertificate(configPath, name string, subject, certType, validi
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}) keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
basename := name basename := finalDef.Name
if basename == "" { if basename == "" {
basename = subject basename = finalDef.Subject
} }
certFile := filepath.Join(ca.Paths.Certificates, basename+"."+certType+".crt.pem") certFile := filepath.Join(ca.Paths.Certificates, basename+"."+finalDef.Type+".crt.pem")
keyFile := filepath.Join(ca.Paths.PrivateKeys, basename+"."+certType+".key.pem") keyFile := filepath.Join(ca.Paths.PrivateKeys, basename+"."+finalDef.Type+".key.pem")
if err := SavePEM(certFile, certPEM, false, overwrite); err != nil { if err := SavePEM(certFile, certPEM, false, overwrite); err != nil {
fmt.Println("Error saving certificate:", err) fmt.Println("Error saving certificate:", err)
return return
@@ -507,7 +636,14 @@ func internalIssueCertificate(configPath, name string, subject, certType, validi
fmt.Println("Error saving key:", err) fmt.Println("Error saving key:", err)
return return
} }
fmt.Printf("%s certificate and key for '%s' generated.\n", certType, subject) fmt.Printf("%s certificate and key for '%s' generated.\n", finalDef.Type, finalDef.Subject)
// Save CA state after single issuance
caDir := filepath.Dir(configPath)
caLabel := ca.Label
caStatePath := filepath.Join(caDir, caLabel+"_state.json")
if err := SaveCAState(caStatePath, GlobalCAState); err != nil {
fmt.Printf("Error saving CA state: %v\n", err)
}
} }
// Extract defaults from certificates.hcl (now using new LoadCertificatesFile signature) // Extract defaults from certificates.hcl (now using new LoadCertificatesFile signature)
@@ -523,142 +659,6 @@ func GetCertificateDefaults(path string) CertificateDefinition {
} }
} }
// Issue certificate with custom basename and dry-run support
func IssueCertificateWithBasename(configPath, basename, subject, certType, validityFlag string, san []string, overwrite, dryRun bool) error {
if dryRun {
fmt.Printf("Would issue certificate: name=%s, subject=%s, type=%s, validity=%s, SAN=%v\n", basename, subject, certType, validityFlag, san)
return nil
}
// Call IssueCertificate but override basename logic
return issueCertificateInternal(configPath, basename, subject, certType, validityFlag, san, overwrite)
}
// Internal: like IssueCertificate but with explicit basename
func issueCertificateInternal(configPath, basename, subject, certType, validityFlag string, san []string, overwrite bool) error {
// Add default dns SAN for server/server-only if none specified
if (certType == "server" || certType == "server-only") && len(san) == 0 {
san = append(san, "dns:"+subject)
}
ca, err := LoadCA(configPath)
if err != nil {
return fmt.Errorf("Error loading config: %v", err)
}
caCertPath := filepath.Join(ca.Paths.Certificates, "ca_cert.pem")
caKeyPath := filepath.Join(ca.Paths.PrivateKeys, "ca_key.pem")
caCertPEM, err := os.ReadFile(caCertPath)
if err != nil {
return fmt.Errorf("Error reading CA certificate file: %v", err)
}
caKeyPEM, err := os.ReadFile(caKeyPath)
if err != nil {
return fmt.Errorf("Error reading CA key file: %v", err)
}
caCertBlock, _ := pem.Decode(caCertPEM)
if caCertBlock == nil {
return fmt.Errorf("Failed to parse CA certificate PEM")
}
caCert, err := x509.ParseCertificate(caCertBlock.Bytes)
if err != nil {
return fmt.Errorf("Failed to parse CA certificate: %v", err)
}
caKeyBlock, _ := pem.Decode(caKeyPEM)
if caKeyBlock == nil {
return fmt.Errorf("Failed to parse CA key PEM")
}
caKey, err := x509.ParsePKCS1PrivateKey(caKeyBlock.Bytes)
if err != nil {
return fmt.Errorf("Failed to parse CA private key: %v", err)
}
priv, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return fmt.Errorf("Failed to generate private key: %v", err)
}
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return fmt.Errorf("Failed to generate serial number: %v", err)
}
var validity time.Duration
if validityFlag != "" {
validity, err = parseValidity(validityFlag)
if err != nil {
return fmt.Errorf("Invalid validity value: %v", err)
}
} else {
validity = 365 * 24 * time.Hour // default 1 year
}
// Parse subject as DN if it looks like a DN, otherwise use as CommonName only
var subjectPKIX pkix.Name
if isDNFormat(subject) {
subjectPKIX = parseDistinguishedName(subject)
} else {
subjectPKIX = pkix.Name{CommonName: subject}
}
certTmpl := x509.Certificate{
SerialNumber: serialNumber,
Subject: subjectPKIX,
NotBefore: time.Now(),
NotAfter: time.Now().Add(validity),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
}
// Handle SANs
for _, s := range san {
sLower := strings.ToLower(s)
var val string
if n, _ := fmt.Sscanf(sLower, "dns:%s", &val); n == 1 {
certTmpl.DNSNames = append(certTmpl.DNSNames, val)
} else if n, _ := fmt.Sscanf(sLower, "ip:%s", &val); n == 1 {
certTmpl.IPAddresses = append(certTmpl.IPAddresses, net.ParseIP(val))
} else if n, _ := fmt.Sscanf(sLower, "email:%s", &val); n == 1 {
certTmpl.EmailAddresses = append(certTmpl.EmailAddresses, val)
} else {
return fmt.Errorf("Invalid SAN format: %s", s)
}
}
switch certType {
case "client":
certTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}
case "server":
certTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}
case "server-only":
certTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
case "code-signing":
certTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}
case "email":
certTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageEmailProtection}
default:
return fmt.Errorf("Unknown certificate type. Use one of: client, server, server-only, code-signing, email.")
}
certDER, err := x509.CreateCertificate(rand.Reader, &certTmpl, caCert, &priv.PublicKey, caKey)
if err != nil {
return fmt.Errorf("Failed to create certificate: %v", err)
}
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
certFile := filepath.Join(ca.Paths.Certificates, basename+".crt.pem")
keyFile := filepath.Join(ca.Paths.PrivateKeys, basename+".key.pem")
if err := SavePEM(certFile, certPEM, false, overwrite); err != nil {
return fmt.Errorf("Error saving certificate: %v", err)
}
if err := SavePEM(keyFile, keyPEM, true, overwrite); err != nil {
return fmt.Errorf("Error saving key: %v", err)
}
return nil
}
// Helper: check if string looks like a DN (contains at least CN=...) // Helper: check if string looks like a DN (contains at least CN=...)
func isDNFormat(s string) bool { func isDNFormat(s string) bool {
return len(s) > 0 && strings.Contains(s, "CN=") return len(s) > 0 && strings.Contains(s, "CN=")