From 10ec83273d2f959874bf83c048b3635571e565a4 Mon Sep 17 00:00:00 2001 From: Slawek Koszewski Date: Sun, 27 Jul 2025 20:00:43 +0200 Subject: [PATCH] Refactored code to one IssueCertificate function. --- ca.go | 350 +++++++++++++++++++++++++++++----------------------------- 1 file changed, 175 insertions(+), 175 deletions(-) diff --git a/ca.go b/ca.go index 0e8ba4c..890888f 100644 --- a/ca.go +++ b/ca.go @@ -287,6 +287,7 @@ func IssueCertificate(configPath, subject, certType, validity string, san []stri } successes := 0 errors := 0 + var basename, certFile, keyFile string // Declare variables before the loop for i, def := range certDefs { if defaults != nil { if def.Type == "" { @@ -315,23 +316,161 @@ func IssueCertificate(configPath, subject, certType, validity string, san []stri 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 { successes++ continue } - err := IssueCertificateWithBasename(configPath, basename, finalDef.Subject, finalDef.Type, finalDef.Validity, finalDef.SAN, overwrite, dryRun) - if err != nil { - fmt.Printf("ERROR: %v\n", err) - errors++ - } else { - if !verbose { - fmt.Printf("done\n") - } - successes++ + // Inline certificate issuance logic for batch mode + // Add default dns SAN for server/server-only if none specified + if (finalDef.Type == "server" || finalDef.Type == "server-only") && len(finalDef.SAN) == 0 { + finalDef.SAN = append(finalDef.SAN, "dns:"+finalDef.Subject) } + + 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) // 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(" SAN: %v\n", finalDef.SAN) } - internalIssueCertificate(configPath, finalDef.Name, finalDef.Subject, finalDef.Type, finalDef.Validity, finalDef.SAN, overwrite) - // 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) { + // Inline the logic from internalIssueCertificate here // 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) + if (finalDef.Type == "server" || finalDef.Type == "server-only") && len(finalDef.SAN) == 0 { + finalDef.SAN = append(finalDef.SAN, "dns:"+finalDef.Subject) } - ca, err := LoadCA(configPath) + ca, err = LoadCA(configPath) if err != nil { fmt.Println("Error loading config:", err) return @@ -426,35 +555,35 @@ func internalIssueCertificate(configPath, name string, subject, certType, validi return } - var validity time.Duration - if validityFlag != "" { - validity, err = parseValidity(validityFlag) + var validityDur time.Duration + if finalDef.Validity != "" { + validityDur, err = parseValidity(finalDef.Validity) if err != nil { fmt.Println("Invalid validity value:", err) return } } 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 var subjectPKIX pkix.Name - if isDNFormat(subject) { - subjectPKIX = parseDistinguishedName(subject) + if isDNFormat(finalDef.Subject) { + subjectPKIX = parseDistinguishedName(finalDef.Subject) } else { - subjectPKIX = pkix.Name{CommonName: subject} + subjectPKIX = pkix.Name{CommonName: finalDef.Subject} } certTmpl := x509.Certificate{ SerialNumber: serialNumber, Subject: subjectPKIX, NotBefore: time.Now(), - NotAfter: time.Now().Add(validity), + NotAfter: time.Now().Add(validityDur), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, } // Handle SANs - for _, s := range san { + for _, s := range finalDef.SAN { sLower := strings.ToLower(s) var val string 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": certTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth} case "server": @@ -493,12 +622,12 @@ func internalIssueCertificate(configPath, name string, subject, certType, validi certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}) - basename := name + basename := finalDef.Name if basename == "" { - basename = subject + basename = finalDef.Subject } - certFile := filepath.Join(ca.Paths.Certificates, basename+"."+certType+".crt.pem") - keyFile := filepath.Join(ca.Paths.PrivateKeys, basename+"."+certType+".key.pem") + certFile := filepath.Join(ca.Paths.Certificates, basename+"."+finalDef.Type+".crt.pem") + keyFile := filepath.Join(ca.Paths.PrivateKeys, basename+"."+finalDef.Type+".key.pem") if err := SavePEM(certFile, certPEM, false, overwrite); err != nil { fmt.Println("Error saving certificate:", err) return @@ -507,7 +636,14 @@ func internalIssueCertificate(configPath, name string, subject, certType, validi fmt.Println("Error saving key:", err) 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) @@ -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=...) func isDNFormat(s string) bool { return len(s) > 0 && strings.Contains(s, "CN=")