Added list command and ability to combine certificate usages.

This commit is contained in:
2025-07-28 18:58:37 +02:00
parent 9b7b995e97
commit 6682be6eb1
2 changed files with 60 additions and 22 deletions

43
ca.go
View File

@@ -465,6 +465,11 @@ func issueSingleCertificate(def CertificateDefinition) error {
return 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)
}
// Initialize Subject if not specified
if def.Subject == "" {
def.Subject = def.Name
@@ -528,19 +533,24 @@ func issueSingleCertificate(def CertificateDefinition) error {
}
}
switch def.Type {
// Split usage types by comma
types := strings.SplitSeq(def.Type, ",")
certTmpl.ExtKeyUsage = []x509.ExtKeyUsage{}
// Collect selected usage types
for certType := range types {
switch certType {
case "client":
certTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}
certTmpl.ExtKeyUsage = append(certTmpl.ExtKeyUsage, x509.ExtKeyUsageClientAuth)
case "server":
certTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}
case "server-only":
certTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
certTmpl.ExtKeyUsage = append(certTmpl.ExtKeyUsage, x509.ExtKeyUsageServerAuth)
case "code-signing":
certTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}
certTmpl.ExtKeyUsage = append(certTmpl.ExtKeyUsage, x509.ExtKeyUsageCodeSigning)
case "email":
certTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageEmailProtection}
certTmpl.ExtKeyUsage = append(certTmpl.ExtKeyUsage, x509.ExtKeyUsageEmailProtection)
default:
return fmt.Errorf("unknown certificate type. Use one of: client, server, server-only, code-signing, email")
return fmt.Errorf("unknown certificate type. Use one of: client, server, code-signing, email")
}
}
certDER, err := x509.CreateCertificate(rand.Reader, &certTmpl, caCert, &priv.PublicKey, caKey)
@@ -614,25 +624,26 @@ func ProvisionCertificates(filePath string, overwrite bool, dryRun bool, verbose
// Loop through all certificate definitions
// to render templates and fill missing fields from defaults
for i := range certDefs.Certificates {
for _, def := range certDefs.Certificates {
// Fill missing fields from defaults, if provided
certDefs.Certificates[i].FillDefaultValues(certDefs.Defaults)
def.FillDefaultValues(certDefs.Defaults)
// Render templates in the definition using the variables map
// with added definition name.
variables := certDefs.Variables
if variables == nil {
variables = make(map[string]string)
}
variables["Name"] = certDefs.Certificates[i].Name
err = certDefs.Certificates[i].RenderTemplates(variables)
variables["Name"] = def.Name
err = def.RenderTemplates(variables)
if err != nil {
return fmt.Errorf("failed to render templates for certificate %s: %v", certDefs.Certificates[i].Name, err)
return fmt.Errorf("failed to render templates for certificate %s: %v", def.Name, err)
}
}
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, len(certDefs.Certificates), certDefs.Certificates[i].Name)
for i, def := range certDefs.Certificates {
fmt.Printf("[%d/%d] Issuing %s... ", i+1, n, def.Name)
if dryRun {
fmt.Printf("(dry run)\n")
@@ -640,7 +651,7 @@ func ProvisionCertificates(filePath string, overwrite bool, dryRun bool, verbose
continue
}
err = issueSingleCertificate(certDefs.Certificates[i])
err = issueSingleCertificate(def)
if err != nil {
fmt.Printf("error: %v\n", err)
errors++

29
main.go
View File

@@ -16,6 +16,9 @@ var verbose bool
func main() {
// list command flags
var listRevoked bool
// issue command flags
var name string
var subject string
@@ -60,6 +63,29 @@ func main() {
}
rootCmd.AddCommand(initCmd)
// lab-ca list command
var listCmd = &cobra.Command{
Use: "list",
Short: "List issued certificates",
Run: func(cmd *cobra.Command, args []string) {
err := LoadCA()
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
os.Exit(1)
}
for _, certDef := range caState.Certificates {
if certDef.RevokedAt != "" {
continue
}
fmt.Printf("Certificate %s\n", certDef.Name)
fmt.Printf("\tSubject: %s\n\tType: %s\n\tIssued at: %s\n",
certDef.Subject, certDef.Type, certDef.Issued)
}
},
}
listCmd.Flags().BoolVar(&listRevoked, "revoked", false, "List all certificates, including revoked ones")
rootCmd.AddCommand(listCmd)
// lab-ca issue command
var issueCmd = &cobra.Command{
Use: "issue",
@@ -82,7 +108,8 @@ func main() {
issueCmd.Flags().StringVar(&name, "name", "", "Name for the certificate and key files (used as subject if --subject is omitted)")
issueCmd.Flags().StringVar(&subject, "subject", "", "Subject Common Name for the certificate (optional, defaults to --name)")
issueCmd.Flags().StringVar(&certType, "type", "server", "Certificate type: client, server, server-only, code-signing, email")
issueCmd.Flags().StringVar(&certType, "type", "server",
"Certificate type: client, server, code-signing, email.\nCombine by specifying more than one separated by comma.")
issueCmd.Flags().StringArrayVar(&san, "san", nil,
"Subject Alternative Name (SAN). Use multiple times for multiple values.\nFormat: dns:example.com, ip:1.2.3.4, email:user@example.com")
issueCmd.Flags().StringVar(&validity, "validity", "1y", "Certificate validity (e.g. 2y, 6m, 30d). Overrides config file for this certificate.")