Compare commits
	
		
			5 Commits
		
	
	
		
			a8308e0f4f
			...
			6682be6eb1
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 6682be6eb1 | |||
| 9b7b995e97 | |||
| b387a016be | |||
| e4469fde96 | |||
| bea0285007 | 
							
								
								
									
										167
									
								
								ca.go
									
									
									
									
									
								
							
							
						
						
									
										167
									
								
								ca.go
									
									
									
									
									
								
							@@ -24,9 +24,10 @@ import (
 | 
				
			|||||||
type Paths struct {
 | 
					type Paths struct {
 | 
				
			||||||
	Certificates string `hcl:"certificates"`
 | 
						Certificates string `hcl:"certificates"`
 | 
				
			||||||
	PrivateKeys  string `hcl:"private_keys"`
 | 
						PrivateKeys  string `hcl:"private_keys"`
 | 
				
			||||||
 | 
						StatePath    string `hcl:"state_file"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type _CAConfig struct {
 | 
					type CAConfig struct {
 | 
				
			||||||
	Label              string `hcl:",label"`
 | 
						Label              string `hcl:",label"`
 | 
				
			||||||
	Name               string `hcl:"name"`
 | 
						Name               string `hcl:"name"`
 | 
				
			||||||
	Country            string `hcl:"country"`
 | 
						Country            string `hcl:"country"`
 | 
				
			||||||
@@ -41,12 +42,12 @@ type _CAConfig struct {
 | 
				
			|||||||
	Paths              Paths  `hcl:"paths,block"`
 | 
						Paths              Paths  `hcl:"paths,block"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (c *_CAConfig) StateName() string {
 | 
					func (c *CAConfig) GetStateFileName() string {
 | 
				
			||||||
	return c.Label + "_state.json"
 | 
						return c.Label + "_state.json"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Configuration struct {
 | 
					type Configuration struct {
 | 
				
			||||||
	Current _CAConfig `hcl:"ca,block"`
 | 
						CA CAConfig `hcl:"ca,block"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type CertificateDefinition struct {
 | 
					type CertificateDefinition struct {
 | 
				
			||||||
@@ -57,7 +58,7 @@ type CertificateDefinition struct {
 | 
				
			|||||||
	SAN      []string `hcl:"san,optional"`
 | 
						SAN      []string `hcl:"san,optional"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (def *CertificateDefinition) fillDefaultValues(defaults *CertificateDefaults) {
 | 
					func (def *CertificateDefinition) FillDefaultValues(defaults *CertificateDefaults) {
 | 
				
			||||||
	if defaults == nil {
 | 
						if defaults == nil {
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -142,17 +143,27 @@ func (c *Certificates) LoadFromFile(path string) error {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Global CA configuration and state variables
 | 
					// Global CA configuration and state variables
 | 
				
			||||||
var CAConfigPath string
 | 
					var caConfigPath string
 | 
				
			||||||
var CAState *_CAState
 | 
					var caConfig *CAConfig
 | 
				
			||||||
var CAConfig *_CAConfig
 | 
					
 | 
				
			||||||
var CAKey *rsa.PrivateKey
 | 
					var caStatePath string
 | 
				
			||||||
var CACert *x509.Certificate
 | 
					var caState *CAState
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var caKey *rsa.PrivateKey
 | 
				
			||||||
 | 
					var caCert *x509.Certificate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// LoadCAConfig parses and validates the CA config from the given path and stores it in the CAConfig global variable
 | 
					// LoadCAConfig parses and validates the CA config from the given path and stores it in the CAConfig global variable
 | 
				
			||||||
func LoadCAConfig() error {
 | 
					func LoadCAConfig() error {
 | 
				
			||||||
	fmt.Printf("Loading CA config from %s\n", CAConfigPath)
 | 
						if verbose {
 | 
				
			||||||
 | 
							cwd, err := os.Getwd()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							fmt.Printf("The current working dirctory: \"%s\"\n", cwd)
 | 
				
			||||||
 | 
							fmt.Printf("Loading CA config from \"%s\"... ", caConfigPath)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	parser := hclparse.NewParser()
 | 
						parser := hclparse.NewParser()
 | 
				
			||||||
	file, diags := parser.ParseHCLFile(CAConfigPath)
 | 
						file, diags := parser.ParseHCLFile(caConfigPath)
 | 
				
			||||||
	if diags.HasErrors() {
 | 
						if diags.HasErrors() {
 | 
				
			||||||
		return fmt.Errorf("failed to parse HCL: %s", diags.Error())
 | 
							return fmt.Errorf("failed to parse HCL: %s", diags.Error())
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -161,16 +172,24 @@ func LoadCAConfig() error {
 | 
				
			|||||||
	if diags.HasErrors() {
 | 
						if diags.HasErrors() {
 | 
				
			||||||
		return fmt.Errorf("failed to decode HCL: %s", diags.Error())
 | 
							return fmt.Errorf("failed to decode HCL: %s", diags.Error())
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if (_CAConfig{}) == config.Current {
 | 
						if (CAConfig{}) == config.CA {
 | 
				
			||||||
		return fmt.Errorf("no 'ca' block found in config file")
 | 
							return fmt.Errorf("no 'ca' block found in config file")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if config.Current.Label == "" {
 | 
						if config.CA.Label == "" {
 | 
				
			||||||
		return fmt.Errorf("the 'ca' block must have a label (e.g., ca \"mylabel\" {...})")
 | 
							return fmt.Errorf("the 'ca' block must have a label (e.g., ca \"mylabel\" {...})")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if err := config.Current.Validate(); err != nil {
 | 
						if err := config.CA.Validate(); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	CAConfig = &config.Current
 | 
					
 | 
				
			||||||
 | 
						// If the state file is specified as an absolute path, use it directly.
 | 
				
			||||||
 | 
						if filepath.IsAbs(config.CA.Paths.StatePath) {
 | 
				
			||||||
 | 
							caStatePath = config.CA.Paths.StatePath
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							caStatePath = filepath.Join(filepath.Dir(caConfigPath), config.CA.Paths.StatePath)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						caConfig = &config.CA
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -184,8 +203,8 @@ func LoadCA() error {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Load CA key and certificate
 | 
						// Load CA key and certificate
 | 
				
			||||||
	caCertPath := filepath.Join(CAConfig.Paths.Certificates, "ca_cert.pem")
 | 
						caCertPath := filepath.Join(caConfig.Paths.Certificates, "ca_cert.pem")
 | 
				
			||||||
	caKeyPath := filepath.Join(CAConfig.Paths.PrivateKeys, "ca_key.pem")
 | 
						caKeyPath := filepath.Join(caConfig.Paths.PrivateKeys, "ca_key.pem")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	caCertPEM, err := os.ReadFile(caCertPath)
 | 
						caCertPEM, err := os.ReadFile(caCertPath)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@@ -200,7 +219,7 @@ func LoadCA() error {
 | 
				
			|||||||
	if caCertBlock == nil {
 | 
						if caCertBlock == nil {
 | 
				
			||||||
		return fmt.Errorf("failed to parse CA certificate PEM")
 | 
							return fmt.Errorf("failed to parse CA certificate PEM")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	CACert, err = x509.ParseCertificate(caCertBlock.Bytes)
 | 
						caCert, err = x509.ParseCertificate(caCertBlock.Bytes)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return fmt.Errorf("failed to parse CA certificate: %v", err)
 | 
							return fmt.Errorf("failed to parse CA certificate: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -208,7 +227,7 @@ func LoadCA() error {
 | 
				
			|||||||
	if caKeyBlock == nil {
 | 
						if caKeyBlock == nil {
 | 
				
			||||||
		return fmt.Errorf("failed to parse CA key PEM")
 | 
							return fmt.Errorf("failed to parse CA key PEM")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	CAKey, err = x509.ParsePKCS1PrivateKey(caKeyBlock.Bytes)
 | 
						caKey, err = x509.ParsePKCS1PrivateKey(caKeyBlock.Bytes)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return fmt.Errorf("failed to parse CA private key: %v", err)
 | 
							return fmt.Errorf("failed to parse CA private key: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -297,7 +316,7 @@ func (p *Paths) Validate() error {
 | 
				
			|||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (c *_CAConfig) Validate() error {
 | 
					func (c *CAConfig) Validate() error {
 | 
				
			||||||
	if c.Name == "" {
 | 
						if c.Name == "" {
 | 
				
			||||||
		return fmt.Errorf("CA 'name' is required")
 | 
							return fmt.Errorf("CA 'name' is required")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -327,32 +346,33 @@ func InitCA() error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	err = LoadCAConfig()
 | 
						err = LoadCAConfig()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
 | 
							fmt.Printf("ERROR: %v\n", err)
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Create certificates directory with 0755, private keys with 0700
 | 
						// Create certificates directory with 0755, private keys with 0700
 | 
				
			||||||
	if CAConfig.Paths.Certificates != "" {
 | 
						if caConfig.Paths.Certificates != "" {
 | 
				
			||||||
		if err := os.MkdirAll(CAConfig.Paths.Certificates, 0755); err != nil {
 | 
							if err := os.MkdirAll(caConfig.Paths.Certificates, 0755); err != nil {
 | 
				
			||||||
			fmt.Printf("Error creating certificates directory '%s': %v\n", CAConfig.Paths.Certificates, err)
 | 
								fmt.Printf("Error creating certificates directory '%s': %v\n", caConfig.Paths.Certificates, err)
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if CAConfig.Paths.PrivateKeys != "" {
 | 
						if caConfig.Paths.PrivateKeys != "" {
 | 
				
			||||||
		if err := os.MkdirAll(CAConfig.Paths.PrivateKeys, 0700); err != nil {
 | 
							if err := os.MkdirAll(caConfig.Paths.PrivateKeys, 0700); err != nil {
 | 
				
			||||||
			fmt.Printf("Error creating private keys directory '%s': %v\n", CAConfig.Paths.PrivateKeys, err)
 | 
								fmt.Printf("Error creating private keys directory '%s': %v\n", caConfig.Paths.PrivateKeys, err)
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Initialize CAState empty state with serial starting from 1
 | 
						// Initialize CAState empty state with serial starting from 1
 | 
				
			||||||
	CAState = &_CAState{
 | 
						caState = &CAState{
 | 
				
			||||||
		Serial:       1, // Start serial from 1
 | 
							Serial:       1, // Start serial from 1
 | 
				
			||||||
		CreatedAt:    time.Now().UTC().Format(time.RFC3339),
 | 
							CreatedAt:    time.Now().UTC().Format(time.RFC3339),
 | 
				
			||||||
		UpdatedAt:    time.Now().UTC().Format(time.RFC3339),
 | 
							UpdatedAt:    time.Now().UTC().Format(time.RFC3339),
 | 
				
			||||||
		Certificates: []CertificateRecord{},
 | 
							Certificates: []CertificateRecord{},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	keySize := CAConfig.KeySize
 | 
						keySize := caConfig.KeySize
 | 
				
			||||||
	if keySize == 0 {
 | 
						if keySize == 0 {
 | 
				
			||||||
		keySize = 4096
 | 
							keySize = 4096
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -366,28 +386,28 @@ func InitCA() error {
 | 
				
			|||||||
		return fmt.Errorf("failed to generate serial number: %v", err)
 | 
							return fmt.Errorf("failed to generate serial number: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if CAConfig.Validity == "" {
 | 
						if caConfig.Validity == "" {
 | 
				
			||||||
		CAConfig.Validity = "5y" // Use default validity of 5 years
 | 
							caConfig.Validity = "5y" // Use default validity of 5 years
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	validity, err := parseValidity(CAConfig.Validity)
 | 
						validity, err := parseValidity(caConfig.Validity)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	now := time.Now()
 | 
						now := time.Now()
 | 
				
			||||||
	// Store CA certificate creation time
 | 
						// Store CA certificate creation time
 | 
				
			||||||
	CAState.CreatedAt = now.UTC().Format(time.RFC3339)
 | 
						caState.CreatedAt = now.UTC().Format(time.RFC3339)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	tmpl := x509.Certificate{
 | 
						tmpl := x509.Certificate{
 | 
				
			||||||
		SerialNumber: serialNumber,
 | 
							SerialNumber: serialNumber,
 | 
				
			||||||
		Subject: pkix.Name{
 | 
							Subject: pkix.Name{
 | 
				
			||||||
			Country:            []string{CAConfig.Country},
 | 
								Country:            []string{caConfig.Country},
 | 
				
			||||||
			Organization:       []string{CAConfig.Organization},
 | 
								Organization:       []string{caConfig.Organization},
 | 
				
			||||||
			OrganizationalUnit: optionalSlice(CAConfig.OrganizationalUnit),
 | 
								OrganizationalUnit: optionalSlice(caConfig.OrganizationalUnit),
 | 
				
			||||||
			Locality:           optionalSlice(CAConfig.Locality),
 | 
								Locality:           optionalSlice(caConfig.Locality),
 | 
				
			||||||
			Province:           optionalSlice(CAConfig.Province),
 | 
								Province:           optionalSlice(caConfig.Province),
 | 
				
			||||||
			CommonName:         CAConfig.Name,
 | 
								CommonName:         caConfig.Name,
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		NotBefore:             now,
 | 
							NotBefore:             now,
 | 
				
			||||||
		NotAfter:              now.Add(validity),
 | 
							NotAfter:              now.Add(validity),
 | 
				
			||||||
@@ -396,10 +416,10 @@ func InitCA() error {
 | 
				
			|||||||
		IsCA:                  true,
 | 
							IsCA:                  true,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	// Add email if present
 | 
						// Add email if present
 | 
				
			||||||
	if CAConfig.Email != "" {
 | 
						if caConfig.Email != "" {
 | 
				
			||||||
		tmpl.Subject.ExtraNames = append(tmpl.Subject.ExtraNames, pkix.AttributeTypeAndValue{
 | 
							tmpl.Subject.ExtraNames = append(tmpl.Subject.ExtraNames, pkix.AttributeTypeAndValue{
 | 
				
			||||||
			Type:  []int{1, 2, 840, 113549, 1, 9, 1}, // emailAddress OID
 | 
								Type:  []int{1, 2, 840, 113549, 1, 9, 1}, // emailAddress OID
 | 
				
			||||||
			Value: CAConfig.Email,
 | 
								Value: caConfig.Email,
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	certDER, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, &priv.PublicKey, priv)
 | 
						certDER, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, &priv.PublicKey, priv)
 | 
				
			||||||
@@ -409,17 +429,17 @@ func InitCA() error {
 | 
				
			|||||||
	certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
 | 
						certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
 | 
				
			||||||
	keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
 | 
						keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := SavePEM(filepath.Join(CAConfig.Paths.Certificates, "ca_cert.pem"), certPEM, false); err != nil {
 | 
						if err := SavePEM(filepath.Join(caConfig.Paths.Certificates, "ca_cert.pem"), certPEM, false); err != nil {
 | 
				
			||||||
		fmt.Println("Error saving CA certificate:", err)
 | 
							fmt.Println("Error saving CA certificate:", err)
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if err := SavePEM(filepath.Join(CAConfig.Paths.PrivateKeys, "ca_key.pem"), keyPEM, true); err != nil {
 | 
						if err := SavePEM(filepath.Join(caConfig.Paths.PrivateKeys, "ca_key.pem"), keyPEM, true); err != nil {
 | 
				
			||||||
		fmt.Println("Error saving CA key:", err)
 | 
							fmt.Println("Error saving CA key:", err)
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// set last updated time in the CAState
 | 
						// set last updated time in the CAState
 | 
				
			||||||
	CAState.UpdatedAt = time.Now().UTC().Format(time.RFC3339)
 | 
						caState.UpdatedAt = time.Now().UTC().Format(time.RFC3339)
 | 
				
			||||||
	// Save the state
 | 
						// Save the state
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	err = SaveCAState()
 | 
						err = SaveCAState()
 | 
				
			||||||
@@ -445,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
 | 
				
			||||||
@@ -508,34 +533,35 @@ 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":
 | 
							case "client":
 | 
				
			||||||
		certTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}
 | 
								certTmpl.ExtKeyUsage = append(certTmpl.ExtKeyUsage, x509.ExtKeyUsageClientAuth)
 | 
				
			||||||
		case "server":
 | 
							case "server":
 | 
				
			||||||
		certTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}
 | 
								certTmpl.ExtKeyUsage = append(certTmpl.ExtKeyUsage, x509.ExtKeyUsageServerAuth)
 | 
				
			||||||
	case "server-only":
 | 
					 | 
				
			||||||
		certTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
 | 
					 | 
				
			||||||
		case "code-signing":
 | 
							case "code-signing":
 | 
				
			||||||
		certTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}
 | 
								certTmpl.ExtKeyUsage = append(certTmpl.ExtKeyUsage, x509.ExtKeyUsageCodeSigning)
 | 
				
			||||||
		case "email":
 | 
							case "email":
 | 
				
			||||||
		certTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageEmailProtection}
 | 
								certTmpl.ExtKeyUsage = append(certTmpl.ExtKeyUsage, x509.ExtKeyUsageEmailProtection)
 | 
				
			||||||
		default:
 | 
							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)
 | 
						certDER, err := x509.CreateCertificate(rand.Reader, &certTmpl, caCert, &priv.PublicKey, caKey)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return fmt.Errorf("failed to create certificate: %v", err)
 | 
							return fmt.Errorf("failed to create certificate: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
 | 
						certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
 | 
				
			||||||
	keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
 | 
						keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	basename := def.Name
 | 
						certFile := filepath.Join(caConfig.Paths.Certificates, def.Name+".crt.pem")
 | 
				
			||||||
	if basename == "" {
 | 
						keyFile := filepath.Join(caConfig.Paths.PrivateKeys, def.Name+".key.pem")
 | 
				
			||||||
		basename = def.Subject
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	certFile := filepath.Join(CAConfig.Paths.Certificates, basename+".crt.pem")
 | 
					 | 
				
			||||||
	keyFile := filepath.Join(CAConfig.Paths.PrivateKeys, basename+".key.pem")
 | 
					 | 
				
			||||||
	if err := SavePEM(certFile, certPEM, false); err != nil {
 | 
						if err := SavePEM(certFile, certPEM, false); err != nil {
 | 
				
			||||||
		return fmt.Errorf("error saving certificate: %v", err)
 | 
							return fmt.Errorf("error saving certificate: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -558,9 +584,11 @@ Certificate:
 | 
				
			|||||||
			def.SAN,
 | 
								def.SAN,
 | 
				
			||||||
		)
 | 
							)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	CAState.UpdateCAStateAfterIssue(
 | 
						caState.UpdateCAStateAfterIssue(
 | 
				
			||||||
		CAConfig.SerialType,
 | 
							caConfig.SerialType,
 | 
				
			||||||
		basename,
 | 
							def.Name,
 | 
				
			||||||
 | 
							def.Subject,
 | 
				
			||||||
 | 
							def.Type,
 | 
				
			||||||
		serialNumber,
 | 
							serialNumber,
 | 
				
			||||||
		validityDur,
 | 
							validityDur,
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
@@ -596,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")
 | 
				
			||||||
@@ -622,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++
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										78
									
								
								certdb.go
									
									
									
									
									
								
							
							
						
						
									
										78
									
								
								certdb.go
									
									
									
									
									
								
							@@ -10,12 +10,11 @@ import (
 | 
				
			|||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"math/big"
 | 
						"math/big"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"path/filepath"
 | 
					 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// _CAState represents the persisted CA state in JSON
 | 
					// CAState represents the persisted CA state in JSON
 | 
				
			||||||
type _CAState struct {
 | 
					type CAState struct {
 | 
				
			||||||
	CreatedAt    string              `json:"createdAt"`
 | 
						CreatedAt    string              `json:"createdAt"`
 | 
				
			||||||
	UpdatedAt    string              `json:"updatedAt"`
 | 
						UpdatedAt    string              `json:"updatedAt"`
 | 
				
			||||||
	Serial       int                 `json:"serial,omitempty"`
 | 
						Serial       int                 `json:"serial,omitempty"`
 | 
				
			||||||
@@ -26,6 +25,8 @@ type _CAState struct {
 | 
				
			|||||||
// CertificateRecord represents a single certificate record in the CA state
 | 
					// CertificateRecord represents a single certificate record in the CA state
 | 
				
			||||||
type CertificateRecord struct {
 | 
					type CertificateRecord struct {
 | 
				
			||||||
	Name         string `json:"name"`
 | 
						Name         string `json:"name"`
 | 
				
			||||||
 | 
						Subject      string `json:"subject"`
 | 
				
			||||||
 | 
						Type         string `json:"type"`
 | 
				
			||||||
	Issued       string `json:"issued"`
 | 
						Issued       string `json:"issued"`
 | 
				
			||||||
	Expires      string `json:"expires"`
 | 
						Expires      string `json:"expires"`
 | 
				
			||||||
	Serial       string `json:"serial"`
 | 
						Serial       string `json:"serial"`
 | 
				
			||||||
@@ -33,21 +34,48 @@ type CertificateRecord struct {
 | 
				
			|||||||
	RevokeReason int    `json:"revokeReason,omitempty"`
 | 
						RevokeReason int    `json:"revokeReason,omitempty"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func caStatePath() string {
 | 
					// Look for a certifcate by its name
 | 
				
			||||||
	return filepath.Join(filepath.Dir(CAConfigPath), CAConfig.StateName())
 | 
					func (c *CAState) FindByName(name string, all bool) *CertificateRecord {
 | 
				
			||||||
 | 
						for _, cert := range c.Certificates {
 | 
				
			||||||
 | 
							if cert.RevokedAt != "" && !all {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if cert.Name == name {
 | 
				
			||||||
 | 
								return &cert
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Look for a certificate by its serial
 | 
				
			||||||
 | 
					func (c *CAState) FindBySerial(serial string, all bool) *CertificateRecord {
 | 
				
			||||||
 | 
						for _, cert := range c.Certificates {
 | 
				
			||||||
 | 
							if cert.RevokedAt != "" && !all {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if cert.Serial == serial {
 | 
				
			||||||
 | 
								return &cert
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// func caStatePath() string {
 | 
				
			||||||
 | 
					// 	return filepath.Join(filepath.Dir(caConfigPath), caConfig.GetStateFileName())
 | 
				
			||||||
 | 
					// }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// LoadCAState loads the CA state from a JSON file
 | 
					// LoadCAState loads the CA state from a JSON file
 | 
				
			||||||
func LoadCAState() error {
 | 
					func LoadCAState() error {
 | 
				
			||||||
	path := caStatePath()
 | 
						fmt.Printf("Loading CA state from %s\n", caStatePath)
 | 
				
			||||||
	fmt.Printf("Loading CA state from %s\n", path)
 | 
						f, err := os.Open(caStatePath)
 | 
				
			||||||
	f, err := os.Open(path)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	defer f.Close()
 | 
						defer f.Close()
 | 
				
			||||||
	CAState = &_CAState{}
 | 
						caState = &CAState{}
 | 
				
			||||||
	if err := json.NewDecoder(f).Decode(CAState); err != nil {
 | 
						if err := json.NewDecoder(f).Decode(caState); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
@@ -55,19 +83,19 @@ func LoadCAState() error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// SaveCAState saves the CA state to a JSON file
 | 
					// SaveCAState saves the CA state to a JSON file
 | 
				
			||||||
func SaveCAState() error {
 | 
					func SaveCAState() error {
 | 
				
			||||||
	CAState.UpdatedAt = time.Now().UTC().Format(time.RFC3339)
 | 
						caState.UpdatedAt = time.Now().UTC().Format(time.RFC3339)
 | 
				
			||||||
	f, err := os.Create(caStatePath())
 | 
						f, err := os.Create(caStatePath)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	defer f.Close()
 | 
						defer f.Close()
 | 
				
			||||||
	enc := json.NewEncoder(f)
 | 
						enc := json.NewEncoder(f)
 | 
				
			||||||
	enc.SetIndent("", "    ")
 | 
						enc.SetIndent("", "    ")
 | 
				
			||||||
	return enc.Encode(CAState)
 | 
						return enc.Encode(caState)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// UpdateCAStateAfterIssue updates the CA state JSON after issuing a certificate
 | 
					// UpdateCAStateAfterIssue updates the CA state JSON after issuing a certificate
 | 
				
			||||||
func (s *_CAState) UpdateCAStateAfterIssue(serialType, basename string, serialNumber any, validity time.Duration) error {
 | 
					func (s *CAState) UpdateCAStateAfterIssue(serialType, name string, subject string, certType string, serialNumber any, validity time.Duration) error {
 | 
				
			||||||
	if s == nil {
 | 
						if s == nil {
 | 
				
			||||||
		fmt.Fprintf(os.Stderr, "FATAL: CAState is nil in UpdateCAStateAfterIssue. This indicates a programming error.\n")
 | 
							fmt.Fprintf(os.Stderr, "FATAL: CAState is nil in UpdateCAStateAfterIssue. This indicates a programming error.\n")
 | 
				
			||||||
		os.Exit(1)
 | 
							os.Exit(1)
 | 
				
			||||||
@@ -77,24 +105,26 @@ func (s *_CAState) UpdateCAStateAfterIssue(serialType, basename string, serialNu
 | 
				
			|||||||
	serialStr := ""
 | 
						serialStr := ""
 | 
				
			||||||
	switch serialType {
 | 
						switch serialType {
 | 
				
			||||||
	case "sequential":
 | 
						case "sequential":
 | 
				
			||||||
		serialStr = fmt.Sprintf("%d", CAState.Serial)
 | 
							serialStr = fmt.Sprintf("%d", caState.Serial)
 | 
				
			||||||
		CAState.Serial++
 | 
							caState.Serial++
 | 
				
			||||||
	case "random":
 | 
						case "random":
 | 
				
			||||||
		serialStr = fmt.Sprintf("%x", serialNumber)
 | 
							serialStr = fmt.Sprintf("%x", serialNumber)
 | 
				
			||||||
	default:
 | 
						default:
 | 
				
			||||||
		serialStr = fmt.Sprintf("%v", serialNumber)
 | 
							serialStr = fmt.Sprintf("%v", serialNumber)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	s.AddCertificate(basename, issued, expires, serialStr)
 | 
						s.AddCertificate(name, subject, certType, issued, expires, serialStr)
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (s *_CAState) AddCertificate(name, issued, expires, serial string) {
 | 
					func (s *CAState) AddCertificate(name, subject, certType, issued, expires, serial string) {
 | 
				
			||||||
	if s == nil {
 | 
						if s == nil {
 | 
				
			||||||
		fmt.Fprintf(os.Stderr, "FATAL: CAState is nil in AddCertificate. This indicates a programming error.\n")
 | 
							fmt.Fprintf(os.Stderr, "FATAL: CAState is nil in AddCertificate. This indicates a programming error.\n")
 | 
				
			||||||
		os.Exit(1)
 | 
							os.Exit(1)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	rec := CertificateRecord{
 | 
						rec := CertificateRecord{
 | 
				
			||||||
		Name:    name,
 | 
							Name:    name,
 | 
				
			||||||
 | 
							Subject: subject,
 | 
				
			||||||
 | 
							Type:    certType,
 | 
				
			||||||
		Issued:  issued,
 | 
							Issued:  issued,
 | 
				
			||||||
		Expires: expires,
 | 
							Expires: expires,
 | 
				
			||||||
		Serial:  serial,
 | 
							Serial:  serial,
 | 
				
			||||||
@@ -103,7 +133,7 @@ func (s *_CAState) AddCertificate(name, issued, expires, serial string) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// RevokeCertificate revokes a certificate by serial number and reason code, updates state, and saves to disk
 | 
					// RevokeCertificate revokes a certificate by serial number and reason code, updates state, and saves to disk
 | 
				
			||||||
func (s *_CAState) RevokeCertificate(serial string, reason int) error {
 | 
					func (s *CAState) RevokeCertificate(serial string, reason int) error {
 | 
				
			||||||
	if s == nil {
 | 
						if s == nil {
 | 
				
			||||||
		fmt.Fprintf(os.Stderr, "FATAL: CAState is nil in RevokeCertificate. This indicates a programming error.\n")
 | 
							fmt.Fprintf(os.Stderr, "FATAL: CAState is nil in RevokeCertificate. This indicates a programming error.\n")
 | 
				
			||||||
		os.Exit(1)
 | 
							os.Exit(1)
 | 
				
			||||||
@@ -129,11 +159,11 @@ func (s *_CAState) RevokeCertificate(serial string, reason int) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// GenerateCRL generates a CRL file from revoked certificates and writes it to the given path
 | 
					// GenerateCRL generates a CRL file from revoked certificates and writes it to the given path
 | 
				
			||||||
// validityDays defines the number of days for which the CRL is valid (NextUpdate - ThisUpdate)
 | 
					// validityDays defines the number of days for which the CRL is valid (NextUpdate - ThisUpdate)
 | 
				
			||||||
func (s *_CAState) GenerateCRL(crlPath string, validityDays int) error {
 | 
					func (s *CAState) GenerateCRL(crlPath string, validityDays int) error {
 | 
				
			||||||
	if s == nil {
 | 
						if s == nil {
 | 
				
			||||||
		return fmt.Errorf("CAState is nil in GenerateCRL")
 | 
							return fmt.Errorf("CAState is nil in GenerateCRL")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if CACert == nil || CAKey == nil {
 | 
						if caCert == nil || caKey == nil {
 | 
				
			||||||
		return fmt.Errorf("CA certificate or key not loaded")
 | 
							return fmt.Errorf("CA certificate or key not loaded")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	var revokedCerts []pkix.RevokedCertificate
 | 
						var revokedCerts []pkix.RevokedCertificate
 | 
				
			||||||
@@ -162,14 +192,14 @@ func (s *_CAState) GenerateCRL(crlPath string, validityDays int) error {
 | 
				
			|||||||
	now := time.Now().UTC()
 | 
						now := time.Now().UTC()
 | 
				
			||||||
	nextUpdate := now.Add(time.Duration(validityDays) * 24 * time.Hour) // validityDays * 24 * 60 * 60 * 1000 milliseconds
 | 
						nextUpdate := now.Add(time.Duration(validityDays) * 24 * time.Hour) // validityDays * 24 * 60 * 60 * 1000 milliseconds
 | 
				
			||||||
	template := &x509.RevocationList{
 | 
						template := &x509.RevocationList{
 | 
				
			||||||
		SignatureAlgorithm:  CACert.SignatureAlgorithm,
 | 
							SignatureAlgorithm:  caCert.SignatureAlgorithm,
 | 
				
			||||||
		RevokedCertificates: revokedCerts,
 | 
							RevokedCertificates: revokedCerts,
 | 
				
			||||||
		Number:              big.NewInt(int64(s.CRLNumber + 1)),
 | 
							Number:              big.NewInt(int64(s.CRLNumber + 1)),
 | 
				
			||||||
		ThisUpdate:          now,
 | 
							ThisUpdate:          now,
 | 
				
			||||||
		NextUpdate:          nextUpdate,
 | 
							NextUpdate:          nextUpdate,
 | 
				
			||||||
		Issuer:              CACert.Subject,
 | 
							Issuer:              caCert.Subject,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	crlBytes, err := x509.CreateRevocationList(nil, template, CACert, CAKey)
 | 
						crlBytes, err := x509.CreateRevocationList(nil, template, caCert, caKey)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return fmt.Errorf("failed to create CRL: %v", err)
 | 
							return fmt.Errorf("failed to create CRL: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,5 +9,6 @@ ca "example_ca" {
 | 
				
			|||||||
  paths {
 | 
					  paths {
 | 
				
			||||||
    certificates = "certs"
 | 
					    certificates = "certs"
 | 
				
			||||||
    private_keys = "private"
 | 
					    private_keys = "private"
 | 
				
			||||||
 | 
					    state_file   = "ca_state.json"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										37
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										37
									
								
								main.go
									
									
									
									
									
								
							@@ -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
 | 
				
			||||||
@@ -48,7 +51,7 @@ func main() {
 | 
				
			|||||||
	rootCmd.PersistentFlags().BoolVar(&overwrite, "overwrite", false, "Allow overwriting existing files")
 | 
						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(&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().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")
 | 
						rootCmd.PersistentFlags().StringVar(&caConfigPath, "config", "ca_config.hcl", "Path to CA configuration file")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// lab-ca initca command
 | 
						// lab-ca initca command
 | 
				
			||||||
	var initCmd = &cobra.Command{
 | 
						var initCmd = &cobra.Command{
 | 
				
			||||||
@@ -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.")
 | 
				
			||||||
@@ -124,7 +151,7 @@ func main() {
 | 
				
			|||||||
			serial := ""
 | 
								serial := ""
 | 
				
			||||||
			if revokeName != "" {
 | 
								if revokeName != "" {
 | 
				
			||||||
				found := false
 | 
									found := false
 | 
				
			||||||
				for _, rec := range CAState.Certificates {
 | 
									for _, rec := range caState.Certificates {
 | 
				
			||||||
					if rec.Name == revokeName {
 | 
										if rec.Name == revokeName {
 | 
				
			||||||
						serial = rec.Serial
 | 
											serial = rec.Serial
 | 
				
			||||||
						found = true
 | 
											found = true
 | 
				
			||||||
@@ -157,7 +184,7 @@ func main() {
 | 
				
			|||||||
				fmt.Println()
 | 
									fmt.Println()
 | 
				
			||||||
				os.Exit(1)
 | 
									os.Exit(1)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			if err := CAState.RevokeCertificate(serial, reasonCode); err != nil {
 | 
								if err := caState.RevokeCertificate(serial, reasonCode); err != nil {
 | 
				
			||||||
				fmt.Printf("ERROR: %v\n", err)
 | 
									fmt.Printf("ERROR: %v\n", err)
 | 
				
			||||||
				os.Exit(1)
 | 
									os.Exit(1)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
@@ -181,7 +208,7 @@ func main() {
 | 
				
			|||||||
			if crlValidityDays <= 0 {
 | 
								if crlValidityDays <= 0 {
 | 
				
			||||||
				crlValidityDays = 30 // default to 30 days
 | 
									crlValidityDays = 30 // default to 30 days
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			err := CAState.GenerateCRL(crlFile, crlValidityDays)
 | 
								err := caState.GenerateCRL(crlFile, crlValidityDays)
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				fmt.Printf("ERROR generating CRL: %v\n", err)
 | 
									fmt.Printf("ERROR generating CRL: %v\n", err)
 | 
				
			||||||
				os.Exit(1)
 | 
									os.Exit(1)
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user