diff --git a/ca.go b/ca.go index d0d0cb8..385c5a5 100644 --- a/ca.go +++ b/ca.go @@ -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 { - case "client": - certTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth} - case "server": - certTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth} - case "server-only": - certTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth} - case "code-signing": - certTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning} - case "email": - certTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageEmailProtection} - default: - return fmt.Errorf("unknown certificate type. Use one of: client, server, server-only, code-signing, email") + // 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 = append(certTmpl.ExtKeyUsage, x509.ExtKeyUsageClientAuth) + case "server": + certTmpl.ExtKeyUsage = append(certTmpl.ExtKeyUsage, x509.ExtKeyUsageServerAuth) + case "code-signing": + 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) @@ -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++ diff --git a/main.go b/main.go index 66e8ded..b13769e 100644 --- a/main.go +++ b/main.go @@ -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.")