Compare commits
5 Commits
dev
...
a2528e0526
Author | SHA1 | Date | |
---|---|---|---|
a2528e0526 | |||
9b0d1eceae | |||
45dfdf0afc | |||
6e69377d1a | |||
8114d667ec |
171
ca.go
171
ca.go
@@ -291,14 +291,9 @@ func parseValidity(validity string) (time.Duration, error) {
|
||||
}
|
||||
|
||||
func SavePEM(filename string, data []byte, secure bool) error {
|
||||
if !overwrite {
|
||||
if _, err := os.Stat(filename); err == nil {
|
||||
return fmt.Errorf("file %s already exists (overwrite not allowed)", filename)
|
||||
} else if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("could not check file %s: %v", filename, err)
|
||||
}
|
||||
if _, err := os.Stat(filename); err == nil {
|
||||
return fmt.Errorf("file %s already exists", filename)
|
||||
}
|
||||
|
||||
if secure {
|
||||
return os.WriteFile(filename, data, 0600)
|
||||
} else {
|
||||
@@ -453,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
|
||||
@@ -477,18 +471,44 @@ func issueSingleCertificate(def CertificateDefinition) error {
|
||||
}
|
||||
|
||||
// Add default dns SAN for server/server-only if none specified
|
||||
if (def.Type == "server" || def.Type == "server-only") && len(def.SAN) == 0 {
|
||||
def.SAN = append(def.SAN, "dns:"+def.Subject)
|
||||
if strings.Contains(def.Type, "server") && len(def.SAN) == 0 {
|
||||
// Extract CN if subject is a DN, else use subject as is
|
||||
cn := def.Subject
|
||||
if isDNFormat(def.Subject) {
|
||||
dn := parseDistinguishedName(def.Subject)
|
||||
if dn.CommonName != "" {
|
||||
cn = dn.CommonName
|
||||
}
|
||||
}
|
||||
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
|
||||
@@ -499,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
|
||||
@@ -532,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -552,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)})
|
||||
@@ -566,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,
|
||||
@@ -596,11 +599,49 @@ 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
|
||||
func ProvisionCertificates(filePath string, overwrite bool, dryRun bool, verbose bool) error {
|
||||
func ProvisionCertificates(filePath string) error {
|
||||
err := LoadCA()
|
||||
|
||||
if err != nil {
|
||||
@@ -647,27 +688,18 @@ func ProvisionCertificates(filePath string, overwrite bool, dryRun bool, verbose
|
||||
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 {
|
||||
@@ -677,38 +709,23 @@ func ProvisionCertificates(filePath string, overwrite bool, dryRun bool, verbose
|
||||
return nil
|
||||
}
|
||||
|
||||
func IssueCertificate(certDef CertificateDefinition, overwrite bool, dryRun bool, verbose bool) 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=...)
|
||||
|
6
main.go
6
main.go
@@ -8,7 +8,6 @@ import (
|
||||
)
|
||||
|
||||
// Global flags available to all commands
|
||||
var overwrite bool
|
||||
var dryRun bool
|
||||
var verbose bool
|
||||
|
||||
@@ -46,7 +45,6 @@ func main() {
|
||||
}
|
||||
|
||||
// Define persistent flags (global for all commands)
|
||||
rootCmd.PersistentFlags().BoolVar(&overwrite, "overwrite", false, "Allow overwriting existing files")
|
||||
rootCmd.PersistentFlags().BoolVar(&verbose, "verbose", false, "Print detailed information about each processed certificate")
|
||||
rootCmd.PersistentFlags().BoolVar(&dryRun, "dry-run", false, "Validate and show what would be created, but do not write files (batch mode)")
|
||||
rootCmd.PersistentFlags().StringVar(&caConfigPath, "config", "ca_config.hcl", "Path to CA configuration file")
|
||||
@@ -95,7 +93,7 @@ func main() {
|
||||
Type: certType,
|
||||
Validity: validity,
|
||||
SAN: san,
|
||||
}, overwrite, dryRun, verbose)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
|
||||
@@ -120,7 +118,7 @@ func main() {
|
||||
Short: "Provision certificates from a batch file (HCL)",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
err := ProvisionCertificates(provisionFile, overwrite, false, verbose)
|
||||
err := ProvisionCertificates(provisionFile)
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
|
||||
|
13
run-test-2.sh
Executable file
13
run-test-2.sh
Executable 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
|
Reference in New Issue
Block a user