From 9b0d1eceaea77d910fca02ba481ffef0f3b6bc3e Mon Sep 17 00:00:00 2001 From: Slawek Koszewski Date: Mon, 4 Aug 2025 22:09:11 +0200 Subject: [PATCH] Re-enginnered information output during issuance phase. --- ca.go | 148 ++++++++++++++++++++++++++++++++-------------------------- 1 file changed, 81 insertions(+), 67 deletions(-) diff --git a/ca.go b/ca.go index 7de4333..24877e2 100644 --- a/ca.go +++ b/ca.go @@ -448,22 +448,21 @@ func InitCA() error { return nil } -// Helper: issue a single certificate and key, save to files, return error if any -func issueSingleCertificate(def CertificateDefinition) error { +func issueSingleCertificate(def CertificateDefinition, i int, n int) (bool, error) { // Validate Name isValidName, err := regexp.MatchString(`^[A-Za-z0-9_-]+$`, def.Name) if err != nil { - return fmt.Errorf("error validating certificate name: %v", err) + return false, fmt.Errorf("error validating certificate name: %v", err) } if !isValidName { - return fmt.Errorf("certificate name must be specified and contain only letters, numbers, dash, or underscore") + return false, fmt.Errorf("certificate name must be specified and contain only letters, numbers, dash, or underscore") } // Check if the certificate is in database, fail if it is. if caState.FindByName(def.Name, false) != nil { - return fmt.Errorf("certificate %s already exists and is valid.", def.Name) + return false, fmt.Errorf("certificate %s already exists and is valid.", def.Name) } // Initialize Subject if not specified @@ -484,14 +483,32 @@ func issueSingleCertificate(def CertificateDefinition) error { def.SAN = append(def.SAN, "dns:"+cn) } + // Check if the certificate is being issued in dry run mode + if dryRun { + msg := fmt.Sprintf("Would issue certificate for '%s' (dry run).", def.Subject) + if n > 1 { + fmt.Printf("[%d/%d] %s\n", i+1, n, msg) + } else { + fmt.Printf("%s\n", msg) + } + return false, nil + } else { + msg := fmt.Sprintf("Issuing certificate for '%s'... ", def.Subject) + if n > 1 { + fmt.Printf("[%d/%d] %s", i+1, n, msg) + } else { + fmt.Printf("%s", msg) + } + } + priv, err := rsa.GenerateKey(rand.Reader, 4096) if err != nil { - return fmt.Errorf("failed to generate private key: %v", err) + return false, 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) + return false, fmt.Errorf("failed to generate serial number: %v", err) } var validityDur time.Duration @@ -502,7 +519,7 @@ func issueSingleCertificate(def CertificateDefinition) error { validityDur, err = parseValidity(validity) if err != nil { - return fmt.Errorf("invalid validity value: %v", err) + return false, fmt.Errorf("invalid validity value: %v", err) } var subjectPKIX pkix.Name @@ -535,7 +552,7 @@ func issueSingleCertificate(def CertificateDefinition) error { } 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) + return false, fmt.Errorf("invalid SAN format: %s", s) } } @@ -555,13 +572,13 @@ func issueSingleCertificate(def CertificateDefinition) error { case "email": certTmpl.ExtKeyUsage = append(certTmpl.ExtKeyUsage, x509.ExtKeyUsageEmailProtection) default: - return fmt.Errorf("unknown certificate type. Use one of: client, server, code-signing, email") + return false, fmt.Errorf("unknown certificate type. Use one of: client, server, 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) + return false, 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)}) @@ -569,29 +586,12 @@ func issueSingleCertificate(def CertificateDefinition) error { certFile := filepath.Join(caConfig.Paths.Certificates, def.Name+".crt.pem") keyFile := filepath.Join(caConfig.Paths.PrivateKeys, def.Name+".key.pem") if err := SavePEM(certFile, certPEM, false); err != nil { - return fmt.Errorf("error saving certificate: %v", err) + return false, fmt.Errorf("error saving certificate: %v", err) } if err := SavePEM(keyFile, keyPEM, true); err != nil { - return fmt.Errorf("error saving key: %v", err) + return false, fmt.Errorf("error saving key: %v", err) } - if verbose { - fmt.Printf(` -Certificate: - Name: %s - Subject: %s - Type: %s - Validity: %s - SAN: %v -`, - def.Name, - def.Subject, - def.Type, - def.Validity, - def.SAN, - ) - } - - caState.UpdateCAStateAfterIssue( + err = caState.UpdateCAStateAfterIssue( caConfig.SerialType, def.Name, def.Subject, @@ -599,7 +599,45 @@ Certificate: serialNumber, validityDur, ) - return nil + + if err != nil { + return false, fmt.Errorf("error updating CA state: %v", err) + } + + if !verbose { + fmt.Printf("done.\n") + } else { + fmt.Printf(`done. +Certificate generated: + Name: %s + Subject: %s + Type: %s + Validity: %s + SANs: +`, + def.Name, + def.Subject, + def.Type, + def.Validity, + ) + for _, san := range def.SAN { + parts := strings.SplitN(san, ":", 2) + if len(parts) == 2 { + fmt.Printf(" %s (%s)\n", parts[1], parts[0]) + } else { + fmt.Printf(" %s\n", san) + } + } + } + + if err := SaveCAState(); err != nil { + // If saving CA state fails, we still return success for the certificate issuance + fmt.Printf("WARNING: %v\n", err) + fmt.Println("CA state not saved, but certificate issued and saved successfully.") + return true, fmt.Errorf("Error saving CA state: %v", err) + } + + return true, nil } // A prototype of certificate provisioning function @@ -650,27 +688,18 @@ func ProvisionCertificates(filePath string) error { n := len(certDefs.Certificates) // No errors so far, now we can issue certificates for i := range certDefs.Certificates { - fmt.Printf("[%d/%d] Issuing %s... ", i+1, n, certDefs.Certificates[i].Name) - - if dryRun { - fmt.Printf("(dry run)\n") - successes++ - continue - } - - err = issueSingleCertificate(certDefs.Certificates[i]) + issued, err := issueSingleCertificate(certDefs.Certificates[i], i, n) if err != nil { fmt.Printf("error: %v\n", err) errors++ } else { - if !verbose { - fmt.Printf("done\n") + if issued { + successes++ } - successes++ } } - fmt.Printf("Provisioning complete: %d succeeded, %d failed.\n", successes, errors) + fmt.Printf("Provisioning complete: %d succeeded, %d failed, %d skipped.\n", successes, errors, n-successes-errors) err = SaveCAState() if err != nil { @@ -680,38 +709,23 @@ func ProvisionCertificates(filePath string) error { return nil } -func IssueCertificate(certDef CertificateDefinition) error { +func IssueCertificate(def CertificateDefinition) error { err := LoadCA() if err != nil { fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) os.Exit(1) } - if certDef.Subject == "" { - certDef.Subject = certDef.Name + if def.Subject == "" { + def.Subject = def.Name } // Render templates in the certificae subject and SAN fields - variables := map[string]string{"Name": certDef.Name} - certDef.RenderTemplates(variables) + variables := map[string]string{"Name": def.Name} + def.RenderTemplates(variables) - if dryRun { - fmt.Printf("Would issue %s certificate for '%s' (dry run)\n", certDef.Type, certDef.Subject) - return nil - } - - err = issueSingleCertificate(certDef) - - if err != nil { - return err - } - - fmt.Printf("%s certificate and key for '%s' generated.\n", certDef.Type, certDef.Subject) - if err := SaveCAState(); err != nil { - fmt.Printf("Error saving CA state: %v", err) - } - - return nil + _, err = issueSingleCertificate(def, 1, 1) + return err } // Helper: check if string looks like a DN (contains at least CN=...)