diff --git a/ca.go b/ca.go index 52392db..c7fe240 100644 --- a/ca.go +++ b/ca.go @@ -57,6 +57,62 @@ type CertificateDefinition struct { SAN []string `hcl:"san,optional"` } +func (def *CertificateDefinition) fillDefaultValues(defaults *CertificateDefaults) { + if defaults == nil { + return + } + if def.Subject == "" { + def.Subject = defaults.Subject + } + if def.Type == "" { + def.Type = defaults.Type + } + if def.Validity == "" { + def.Validity = defaults.Validity + } + if len(def.SAN) == 0 && len(defaults.SAN) > 0 { + def.SAN = defaults.SAN + } +} + +// Helper: renderTemplates applies Go template to a string +// using the provided variables map. It returns an error if the template execution fails. +func applyTemplateToString(s string, variables map[string]string) (string, error) { + tmpl, err := template.New("").Parse(s) + if err != nil { + return s, err + } + var buf bytes.Buffer + err = tmpl.Execute(&buf, variables) + if err != nil { + return s, err + } + return buf.String(), nil +} + +func (c *CertificateDefinition) RenderTemplates(variables map[string]string) error { + // Apply Go templates to Subject and SAN fields using + // the variables map + if c.Subject != "" { + renderedSubject, err := applyTemplateToString(c.Subject, variables) + if err != nil { + return fmt.Errorf("failed to render subject template: %v", err) + } + c.Subject = renderedSubject + } + + if len(c.SAN) > 0 { + for i, san := range c.SAN { + renderedSAN, err := applyTemplateToString(san, variables) + if err != nil { + return fmt.Errorf("failed to render SAN template: %v", err) + } + c.SAN[i] = renderedSAN + } + } + return nil +} + type CertificateDefaults struct { Subject string `hcl:"subject,optional"` Type string `hcl:"type,optional"` @@ -66,10 +122,27 @@ type CertificateDefaults struct { type Certificates struct { Defaults *CertificateDefaults `hcl:"defaults,block"` + Variables map[string]string `hcl:"variables,optional"` Certificates []CertificateDefinition `hcl:"certificate,block"` } +// Load certificate provisioning configuration from the given path. +func (c *Certificates) LoadFromFile(path string) error { + parser := hclparse.NewParser() + file, diags := parser.ParseHCLFile(path) + if diags.HasErrors() { + return fmt.Errorf("failed to parse HCL: %s", diags.Error()) + } + diags = gohcl.DecodeBody(file.Body, nil, c) + if diags.HasErrors() { + return fmt.Errorf("failed to decode HCL: %s", diags.Error()) + } + + return nil +} + // Global CA configuration and state variables +var CAConfigPath string var CAState *_CAState var CAConfig *_CAConfig var CAKey *rsa.PrivateKey @@ -77,9 +150,9 @@ var CACert *x509.Certificate // LoadCAConfig parses and validates the CA config from the given path and stores it in the CAConfig global variable func LoadCAConfig() error { - fmt.Printf("Loading CA config from %s\n", configPath) + fmt.Printf("Loading CA config from %s\n", CAConfigPath) parser := hclparse.NewParser() - file, diags := parser.ParseHCLFile(configPath) + file, diags := parser.ParseHCLFile(CAConfigPath) if diags.HasErrors() { return fmt.Errorf("failed to parse HCL: %s", diags.Error()) } @@ -162,10 +235,17 @@ func LoadCertificatesFile(path string) ([]CertificateDefinition, *CertificateDef return certsFile.Certificates, certsFile.Defaults, nil } +// Certificate definitions can have validity in various formats: +// - "1y" for 1 year +// - "6m" for 6 months +// - "30d" for 30 days +// Check the syntax and parse validity string into time.Duration func parseValidity(validity string) (time.Duration, error) { + // Return error is the function is called with an empty validity if validity == "" { - return time.Hour * 24 * 365 * 5, nil // default 5 years + return 0, fmt.Errorf("validity cannot be empty") } + var n int var unit rune _, err := fmt.Sscanf(validity, "%d%c", &n, &unit) @@ -173,10 +253,12 @@ func parseValidity(validity string) (time.Duration, error) { // If no unit, assume years _, err2 := fmt.Sscanf(validity, "%d", &n) if err2 != nil { + // Still no success, return error return 0, fmt.Errorf("invalid validity format: %s", validity) } unit = 'y' } + switch unit { case 'y': return time.Hour * 24 * 365 * time.Duration(n), nil @@ -283,6 +365,11 @@ func InitCA(overwrite bool) error { if err != nil { return fmt.Errorf("failed to generate serial number: %v", err) } + + if CAConfig.Validity == "" { + CAConfig.Validity = "5y" // Use default validity of 5 years + } + validity, err := parseValidity(CAConfig.Validity) if err != nil { return err @@ -381,7 +468,7 @@ func issueSingleCertificate(def CertificateDefinition, overwrite, verbose bool) var validityDur time.Duration validity := def.Validity if validity == "" { - validity = "1y" + validity = "1y" // default to 1 year } validityDur, err = parseValidity(validity) @@ -480,81 +567,115 @@ Certificate: return nil } -func IssueCertificate(configPath, subject, certType, validity string, san []string, name, fromFile string, overwrite, dryRun, verbose bool) error { - if fromFile != "" { - certDefs, defaults, err := LoadCertificatesFile(fromFile) - if err != nil { - return fmt.Errorf("Error loading certificates file: %v", err) - } - successes := 0 - errors := 0 - for i, def := range certDefs { - if defaults != nil { - if def.Type == "" { - def.Type = defaults.Type - } - if def.Validity == "" { - def.Validity = defaults.Validity - } - if len(def.SAN) == 0 && len(defaults.SAN) > 0 { - def.SAN = defaults.SAN - } - } - finalDef := renderCertificateDefTemplates(def, defaults) - fmt.Printf("[%d/%d] Issuing %s... ", i+1, len(certDefs), finalDef.Name) - if dryRun { - fmt.Printf("(dry run)\n") - successes++ - continue - } - err := issueSingleCertificate(finalDef, overwrite, verbose) - if err != nil { - fmt.Printf("error: %v\n", err) - errors++ - } else { - if !verbose { - fmt.Printf("done\n") - } - successes++ - } - } - fmt.Printf("Batch complete: %d succeeded, %d failed.\n", successes, errors) - if err := SaveCAState(); err != nil { - fmt.Printf("Error saving CA state: %v\n", err) - } - if errors > 0 { - return fmt.Errorf("%d certificate(s) failed to issue", errors) - } - return nil - } - // Single mode - finalDef := renderCertificateDefTemplates(CertificateDefinition{Name: name, Subject: subject, Type: certType, Validity: validity, SAN: san}, nil) - if dryRun { - fmt.Printf("Would issue %s certificate for '%s' (dry run)\n", finalDef.Type, finalDef.Subject) - return nil - } - err := issueSingleCertificate(finalDef, overwrite, verbose) +// A prototype of certificate provisioning function +func ProvisionCertificates(filePath string, overwrite bool, dryRun bool, verbose bool) error { + err := LoadCA() + if err != nil { - return err + fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) + os.Exit(1) } - fmt.Printf("%s certificate and key for '%s' generated.\n", finalDef.Type, finalDef.Subject) - if err := SaveCAState(); err != nil { + + // Make an empty Certificates struct to hold the definitions + certDefs := Certificates{} + + // Load certificates provisioning configuration from the file (HCL syntax) + err = certDefs.LoadFromFile(filePath) + if err != nil { + return fmt.Errorf("Error loading certificates file: %v", err) + } + + // The certificate provisioning file must contain at least one certificate definition + if len(certDefs.Certificates) < 1 { + return fmt.Errorf("No certificates defined in %s", filePath) + } + + // We will be counting successes and errors + successes := 0 + errors := 0 + + // Loop through all certificate definitions + // to render templates and fill missing fields from defaults + for i := range certDefs.Certificates { + // Fill missing fields from defaults, if provided + certDefs.Certificates[i].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) + if err != nil { + return fmt.Errorf("failed to render templates for certificate %s: %v", certDefs.Certificates[i].Name, err) + } + } + + // 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) + + if dryRun { + fmt.Printf("(dry run)\n") + successes++ + continue + } + + err = issueSingleCertificate(certDefs.Certificates[i], overwrite, verbose) + if err != nil { + fmt.Printf("error: %v\n", err) + errors++ + } else { + if !verbose { + fmt.Printf("done\n") + } + successes++ + } + } + + fmt.Printf("Provisioning complete: %d succeeded, %d failed.\n", successes, errors) + + err = SaveCAState() + if err != nil { fmt.Printf("Error saving CA state: %v\n", err) } + return nil } -// Extract defaults from certificates.hcl (now using new LoadCertificatesFile signature) -func GetCertificateDefaults(path string) CertificateDefinition { - _, defaults, err := LoadCertificatesFile(path) - if err != nil || defaults == nil { - return CertificateDefinition{} +func IssueCertificate(certDef CertificateDefinition, overwrite bool, dryRun bool, verbose bool) error { + err := LoadCA() + if err != nil { + fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) + os.Exit(1) } - return CertificateDefinition{ - Type: defaults.Type, - Validity: defaults.Validity, - SAN: defaults.SAN, + + if certDef.Subject == "" { + certDef.Subject = certDef.Name } + + // Render templates in the certificae subject and SAN fields + variables := map[string]string{"Name": certDef.Name} + certDef.RenderTemplates(variables) + + if dryRun { + fmt.Printf("Would issue %s certificate for '%s' (dry run)\n", certDef.Type, certDef.Subject) + return nil + } + + err = issueSingleCertificate(certDef, overwrite, verbose) + + 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\n", err) + } + + return nil } // Helper: check if string looks like a DN (contains at least CN=...) @@ -595,70 +716,6 @@ func parseDistinguishedName(dn string) pkix.Name { return name } -// Helper: apply Go template to a string using only the certificate label as data -func applyTemplate(s string, name string) (string, error) { - data := struct { - Name string - }{ - Name: name, - } - tmpl, err := template.New("").Parse(s) - if err != nil { - return s, err - } - var buf bytes.Buffer - if err := tmpl.Execute(&buf, data); err != nil { - return s, err - } - return buf.String(), nil -} - -// Render all string fields in CertificateDefinition using Go templates and return a new CertificateDefinition -func renderCertificateDefTemplates(def CertificateDefinition, defaults *CertificateDefaults) CertificateDefinition { - newDef := def - // Subject: use def.Subject if set, else defaults.Subject (rendered) - if def.Subject != "" { - if rendered, err := applyTemplate(def.Subject, def.Name); err == nil { - newDef.Subject = rendered - } - } else if defaults != nil && defaults.Subject != "" { - if rendered, err := applyTemplate(defaults.Subject, def.Name); err == nil { - newDef.Subject = rendered - } - } - // Type: use def.Type if set, else defaults.Type (no template) - if def.Type == "" && defaults != nil && defaults.Type != "" { - newDef.Type = defaults.Type - } - // Validity: use def.Validity if set, else defaults.Validity (no template) - if def.Validity == "" && defaults != nil && defaults.Validity != "" { - newDef.Validity = defaults.Validity - } - // SAN: use def.SAN if set, else defaults.SAN (rendered) - if len(def.SAN) > 0 { - newSAN := make([]string, len(def.SAN)) - for i, s := range def.SAN { - if rendered, err := applyTemplate(s, def.Name); err == nil { - newSAN[i] = rendered - } else { - newSAN[i] = s - } - } - newDef.SAN = newSAN - } else if defaults != nil && len(defaults.SAN) > 0 { - newSAN := make([]string, len(defaults.SAN)) - for i, s := range defaults.SAN { - if rendered, err := applyTemplate(s, def.Name); err == nil { - newSAN[i] = rendered - } else { - newSAN[i] = s - } - } - newDef.SAN = newSAN - } - return newDef -} - // Helper: convert optional string to []string or nil func optionalSlice(s string) []string { if s == "" { diff --git a/certdb.go b/certdb.go index ec11fa3..67fa444 100644 --- a/certdb.go +++ b/certdb.go @@ -15,7 +15,6 @@ import ( ) // _CAState represents the persisted CA state in JSON -// (matches the structure of example_ca.json) type _CAState struct { CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` @@ -24,6 +23,7 @@ type _CAState struct { Certificates []CertificateRecord `json:"certificates"` } +// CertificateRecord represents a single certificate record in the CA state type CertificateRecord struct { Name string `json:"name"` Issued string `json:"issued"` @@ -34,7 +34,7 @@ type CertificateRecord struct { } func caStatePath() string { - return filepath.Join(filepath.Dir(configPath), CAConfig.StateName()) + return filepath.Join(filepath.Dir(CAConfigPath), CAConfig.StateName()) } // LoadCAState loads the CA state from a JSON file diff --git a/examples/example-certificates.hcl b/examples/example-certificates.hcl index 7a3bddc..003be00 100644 --- a/examples/example-certificates.hcl +++ b/examples/example-certificates.hcl @@ -5,6 +5,11 @@ defaults { san = ["DNS:{{ .Name }}.koszewscy.waw.pl"] } +variables = { + Domain = "koszewscy.email" + Country = "PL" +} + certificate "grafana" { # from default: subject = "{{ .Name }}.koszewscy.waw.pl" # result: grafana.koszewscy.waw.pl # from default: type = "server" @@ -20,3 +25,8 @@ certificate "loki" { } certificate "alloy" {} + +certificate "prometheus" { + subject = "{{ .Name }}.{{ .Domain }}" # result: prometheus.koszewscy.email + san = ["DNS:{{ .Name }}.{{ .Domain }}"] # result: [ "DNS:prometheus.koszewscy.email" ] +} diff --git a/main.go b/main.go index 40e028d..f8d4208 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,6 @@ import ( ) var Version = "dev" -var configPath string func main() { @@ -18,7 +17,6 @@ func main() { var validity string var san []string var name string - var fromFile string var dryRun bool var verbose bool var crlFile string @@ -26,6 +24,7 @@ func main() { var revokeName string var revokeSerial string var revokeReasonStr string + var provisionFile string var rootCmd = &cobra.Command{ Use: "lab-ca", @@ -36,6 +35,13 @@ 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-path", "ca_config.hcl", "Path to CA configuration file") + + // lab-ca initca command var initCmd = &cobra.Command{ Use: "initca", Short: "Generate a new CA certificate and key", @@ -43,83 +49,57 @@ func main() { InitCA(overwrite) }, } + rootCmd.AddCommand(initCmd) - initCmd.Flags().StringVar(&configPath, "config", "ca_config.hcl", "Path to CA configuration file") - initCmd.Flags().BoolVar(&overwrite, "overwrite", false, "Allow overwriting existing files") - + // lab-ca issue command var issueCmd = &cobra.Command{ Use: "issue", Short: "Issue a new certificate (client, server, server-only, code-signing, email)", Run: func(cmd *cobra.Command, args []string) { - if err := LoadCA(); err != nil { - fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) - os.Exit(1) - } - if err := IssueCertificate(configPath, - subject, - certType, - validity, - san, - name, - fromFile, - overwrite, - dryRun, - verbose); err != nil { + err := IssueCertificate(CertificateDefinition{ + Name: name, + Subject: subject, + Type: certType, + Validity: validity, + SAN: san, + }, overwrite, dryRun, verbose) + + if err != nil { fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) os.Exit(1) } }, } - issueCmd.Flags().StringVar(&configPath, "config", "ca_config.hcl", "Path to CA configuration file") + 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().StringArrayVar(&san, "san", nil, - "Subject Alternative Name (SAN). Use multiple times for multiple values.\n"+ - "Format: 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().BoolVar(&overwrite, "overwrite", false, "Allow overwriting existing files") - issueCmd.Flags().StringVar(&fromFile, "from-file", "", "Path to HCL file with multiple certificate definitions (batch mode)") - issueCmd.Flags().BoolVar(&dryRun, "dry-run", false, "Validate and show what would be created, but do not write files (batch mode)") - issueCmd.Flags().BoolVar(&verbose, "verbose", false, "Print detailed information about each processed certificate") - // Only require --name in simple mode - issueCmd.Flags().StringVar(&name, "name", "", "Name for the certificate and key files (used as subject if --subject is omitted)") - issueCmd.PreRun = func(cmd *cobra.Command, args []string) { - if fromFile == "" { - cmd.MarkFlagRequired("name") - } - } + issueCmd.MarkFlagRequired("name") + rootCmd.AddCommand(issueCmd) - var versionCmd = &cobra.Command{ - Use: "version", - Short: "Show version information", + // lab-ca provision command + var provisionCmd = &cobra.Command{ + Use: "provision", + Short: "Provision certificates from a batch file (HCL)", Run: func(cmd *cobra.Command, args []string) { - fmt.Printf("lab-ca version: %s\n", Version) - }, - } - var crlCmd = &cobra.Command{ - Use: "crl", - Short: "Generate a Certificate Revocation List (CRL)", - Run: func(cmd *cobra.Command, args []string) { - if err := LoadCA(); err != nil { - fmt.Printf("ERROR: %v\n", err) - os.Exit(1) - } - if crlValidityDays <= 0 { - crlValidityDays = 30 // default to 30 days - } - err := CAState.GenerateCRL(crlFile, crlValidityDays) + err := ProvisionCertificates(provisionFile, overwrite, false, verbose) + if err != nil { - fmt.Printf("ERROR generating CRL: %v\n", err) + fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) os.Exit(1) } - fmt.Printf("CRL written to %s (valid for %d days)\n", crlFile, crlValidityDays) }, } - crlCmd.Flags().StringVar(&crlFile, "crl-file", "crl.pem", "Output path for CRL file (default: crl.pem)") - crlCmd.Flags().IntVar(&crlValidityDays, "validity-days", 30, "CRL validity in days (default: 30)") + provisionCmd.Flags().StringVar(&provisionFile, "file", "", "Path to HCL file with certificate definitions (required)") + provisionCmd.MarkFlagRequired("file") + rootCmd.AddCommand(provisionCmd) + + // lab-ca revoke command var revokeCmd = &cobra.Command{ Use: "revoke", Short: "Revoke a certificate by name or serial number", @@ -178,13 +158,42 @@ func main() { revokeCmd.Flags().StringVar(&revokeName, "name", "", "Certificate name to revoke (mutually exclusive with --serial)") revokeCmd.Flags().StringVar(&revokeSerial, "serial", "", "Certificate serial number to revoke (mutually exclusive with --name)") revokeCmd.Flags().StringVar(&revokeReasonStr, "reason", "cessationOfOperation", "Revocation reason (unspecified, keyCompromise, caCompromise, affiliationChanged, superseded, cessationOfOperation, certificateHold, removeFromCRL)") - - rootCmd.AddCommand(initCmd) - rootCmd.AddCommand(issueCmd) - rootCmd.AddCommand(versionCmd) - rootCmd.AddCommand(crlCmd) rootCmd.AddCommand(revokeCmd) + // lab-ca crl command + var crlCmd = &cobra.Command{ + Use: "crl", + Short: "Generate a Certificate Revocation List (CRL)", + Run: func(cmd *cobra.Command, args []string) { + if err := LoadCA(); err != nil { + fmt.Printf("ERROR: %v\n", err) + os.Exit(1) + } + if crlValidityDays <= 0 { + crlValidityDays = 30 // default to 30 days + } + err := CAState.GenerateCRL(crlFile, crlValidityDays) + if err != nil { + fmt.Printf("ERROR generating CRL: %v\n", err) + os.Exit(1) + } + fmt.Printf("CRL written to %s (valid for %d days)\n", crlFile, crlValidityDays) + }, + } + crlCmd.Flags().StringVar(&crlFile, "crl-file", "crl.pem", "Output path for CRL file (default: crl.pem)") + crlCmd.Flags().IntVar(&crlValidityDays, "validity-days", 30, "CRL validity in days (default: 30)") + rootCmd.AddCommand(crlCmd) + + // lab-ca version command + var versionCmd = &cobra.Command{ + Use: "version", + Short: "Show version information", + Run: func(cmd *cobra.Command, args []string) { + fmt.Printf("lab-ca version: %s\n", Version) + }, + } + rootCmd.AddCommand(versionCmd) + if err := rootCmd.Execute(); err != nil { os.Exit(1) } @@ -198,10 +207,11 @@ func printMainHelp() { fmt.Println() fmt.Println("Available commands:") fmt.Println(" initca Generate a new CA certificate and key") - fmt.Println(" issue Issue a new client/server certificate") - fmt.Println(" version Show version information") + fmt.Println(" issue Issue a new certificate") + fmt.Println(" version Show version information") fmt.Println(" crl Generate a Certificate Revocation List (CRL)") - fmt.Println(" revoke Revoke a certificate by name or serial number") + fmt.Println(" revoke Revoke a certificate by name or serial number") + fmt.Println(" provision Provision certificates from a batch file (HCL)") fmt.Println() fmt.Println("Use 'lab-ca --help' for more information about a command.") } diff --git a/run-test.sh b/run-test.sh index 7c331e2..8737158 100755 --- a/run-test.sh +++ b/run-test.sh @@ -2,14 +2,15 @@ GREEN='\033[0;32m' NC='\033[0m' # No Color -go build +# Build and install +go install rm -rf certs private *.json crl*.pem echo -e "\n${GREEN}Initializing CA...${NC}" -./lab-ca initca || exit 1 +lab-ca initca || exit 1 echo -e "\n${GREEN}Issuing single certificate with incorrect argument..${NC}" -./lab-ca issue --name "blackpanther2.koszewscy.waw.pl" +lab-ca issue --name "blackpanther2.koszewscy.waw.pl" if [ $? -ne 0 ]; then echo -e "${GREEN}Failed to issue certificate.${NC} - that's fine it was intended." else @@ -18,23 +19,23 @@ else fi echo -e "\n${GREEN}Issuing single certificate..${NC}" -./lab-ca issue --name "blackpanther2" --subject "blackpanther2.koszewscy.waw.pl" || exit 1 +lab-ca issue --name "blackpanther2" --subject "blackpanther2.koszewscy.waw.pl" || exit 1 echo -e "\n${GREEN}Issuing multiple certificates from file...${NC}" -./lab-ca issue --from-file examples/example-certificates.hcl --verbose || exit 1 +lab-ca provision --file examples/example-certificates.hcl --verbose || exit 1 echo -e "\n${GREEN}Revoking a certificate by name...${NC}" -./lab-ca revoke --name "loki" || exit 1 +lab-ca revoke --name "loki" || exit 1 echo -e "\n${GREEN}Generating CRL...${NC}" -./lab-ca crl --validity-days 7 --crl-file crl-1.pem || exit 1 +lab-ca crl --validity-days 7 --crl-file crl-1.pem || exit 1 openssl crl -noout -text -in crl-1.pem echo -e "\n${GREEN}Revoking a second certificate by name...${NC}" -./lab-ca revoke --name "alloy" || exit 1 +lab-ca revoke --name "alloy" || exit 1 echo -e "\n${GREEN}Generating a second CRL...${NC}" -./lab-ca crl --validity-days 7 --crl-file crl-2.pem || exit 1 +lab-ca crl --validity-days 7 --crl-file crl-2.pem || exit 1 openssl crl -noout -text -in crl-2.pem echo -e "\n${GREEN}Dumping CA state...${NC}"