3 Commits

Author SHA1 Message Date
2ddd22631b Version v0.4.0
Some checks failed
Release / release (push) Failing after 38s
2025-08-04 22:13:35 +02:00
a2528e0526 Added a new test script.
Some checks failed
Release / release (push) Failing after 1m6s
2025-08-04 22:09:23 +02:00
9b0d1eceae Re-enginnered information output during issuance phase. 2025-08-04 22:09:11 +02:00
3 changed files with 95 additions and 68 deletions

146
ca.go
View File

@@ -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++
}
}
}
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 {
_, err = issueSingleCertificate(def, 1, 1)
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
}
// Helper: check if string looks like a DN (contains at least CN=...)

13
run-test-2.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/bash
if [ "-v" == "$1" ]; then
VERBOSE="--verbose"
shift
else
VERBOSE=""
fi
./build.sh
rm -rf docker_ca
build/lab-ca $VERBOSE --config docker_ca.hcl initca
build/lab-ca $VERBOSE --config docker_ca.hcl provision --file docker.hcl

View File

@@ -1,3 +1,3 @@
package main
var Version = "v0.3.2"
var Version = "v0.4.0"