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

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

29
main.go
View File

@@ -16,6 +16,9 @@ var verbose bool
func main() { func main() {
// list command flags
var listRevoked bool
// issue command flags // issue command flags
var name string var name string
var subject string var subject string
@@ -60,6 +63,29 @@ func main() {
} }
rootCmd.AddCommand(initCmd) 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 // lab-ca issue command
var issueCmd = &cobra.Command{ var issueCmd = &cobra.Command{
Use: "issue", 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(&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(&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, 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") "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.") issueCmd.Flags().StringVar(&validity, "validity", "1y", "Certificate validity (e.g. 2y, 6m, 30d). Overrides config file for this certificate.")