Compare commits
7 Commits
v0.2
...
6682be6eb1
Author | SHA1 | Date | |
---|---|---|---|
6682be6eb1 | |||
9b7b995e97 | |||
b387a016be | |||
e4469fde96 | |||
bea0285007 | |||
a8308e0f4f | |||
911d33deb2 |
179
ca.go
179
ca.go
@@ -24,9 +24,10 @@ import (
|
||||
type Paths struct {
|
||||
Certificates string `hcl:"certificates"`
|
||||
PrivateKeys string `hcl:"private_keys"`
|
||||
StatePath string `hcl:"state_file"`
|
||||
}
|
||||
|
||||
type _CAConfig struct {
|
||||
type CAConfig struct {
|
||||
Label string `hcl:",label"`
|
||||
Name string `hcl:"name"`
|
||||
Country string `hcl:"country"`
|
||||
@@ -41,12 +42,12 @@ type _CAConfig struct {
|
||||
Paths Paths `hcl:"paths,block"`
|
||||
}
|
||||
|
||||
func (c *_CAConfig) StateName() string {
|
||||
func (c *CAConfig) GetStateFileName() string {
|
||||
return c.Label + "_state.json"
|
||||
}
|
||||
|
||||
type Configuration struct {
|
||||
Current _CAConfig `hcl:"ca,block"`
|
||||
CA CAConfig `hcl:"ca,block"`
|
||||
}
|
||||
|
||||
type CertificateDefinition struct {
|
||||
@@ -57,7 +58,7 @@ type CertificateDefinition struct {
|
||||
SAN []string `hcl:"san,optional"`
|
||||
}
|
||||
|
||||
func (def *CertificateDefinition) fillDefaultValues(defaults *CertificateDefaults) {
|
||||
func (def *CertificateDefinition) FillDefaultValues(defaults *CertificateDefaults) {
|
||||
if defaults == nil {
|
||||
return
|
||||
}
|
||||
@@ -142,17 +143,27 @@ func (c *Certificates) LoadFromFile(path string) error {
|
||||
}
|
||||
|
||||
// Global CA configuration and state variables
|
||||
var CAConfigPath string
|
||||
var CAState *_CAState
|
||||
var CAConfig *_CAConfig
|
||||
var CAKey *rsa.PrivateKey
|
||||
var CACert *x509.Certificate
|
||||
var caConfigPath string
|
||||
var caConfig *CAConfig
|
||||
|
||||
var caStatePath string
|
||||
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
|
||||
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()
|
||||
file, diags := parser.ParseHCLFile(CAConfigPath)
|
||||
file, diags := parser.ParseHCLFile(caConfigPath)
|
||||
if diags.HasErrors() {
|
||||
return fmt.Errorf("failed to parse HCL: %s", diags.Error())
|
||||
}
|
||||
@@ -161,16 +172,24 @@ func LoadCAConfig() error {
|
||||
if diags.HasErrors() {
|
||||
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")
|
||||
}
|
||||
if config.Current.Label == "" {
|
||||
if config.CA.Label == "" {
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -184,8 +203,8 @@ func LoadCA() error {
|
||||
}
|
||||
|
||||
// Load CA key and certificate
|
||||
caCertPath := filepath.Join(CAConfig.Paths.Certificates, "ca_cert.pem")
|
||||
caKeyPath := filepath.Join(CAConfig.Paths.PrivateKeys, "ca_key.pem")
|
||||
caCertPath := filepath.Join(caConfig.Paths.Certificates, "ca_cert.pem")
|
||||
caKeyPath := filepath.Join(caConfig.Paths.PrivateKeys, "ca_key.pem")
|
||||
|
||||
caCertPEM, err := os.ReadFile(caCertPath)
|
||||
if err != nil {
|
||||
@@ -200,7 +219,7 @@ func LoadCA() error {
|
||||
if caCertBlock == nil {
|
||||
return fmt.Errorf("failed to parse CA certificate PEM")
|
||||
}
|
||||
CACert, err = x509.ParseCertificate(caCertBlock.Bytes)
|
||||
caCert, err = x509.ParseCertificate(caCertBlock.Bytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse CA certificate: %v", err)
|
||||
}
|
||||
@@ -208,7 +227,7 @@ func LoadCA() error {
|
||||
if caKeyBlock == nil {
|
||||
return fmt.Errorf("failed to parse CA key PEM")
|
||||
}
|
||||
CAKey, err = x509.ParsePKCS1PrivateKey(caKeyBlock.Bytes)
|
||||
caKey, err = x509.ParsePKCS1PrivateKey(caKeyBlock.Bytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse CA private key: %v", err)
|
||||
}
|
||||
@@ -271,7 +290,7 @@ func parseValidity(validity string) (time.Duration, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func SavePEM(filename string, data []byte, secure bool, overwrite bool) error {
|
||||
func SavePEM(filename string, data []byte, secure bool) error {
|
||||
if !overwrite {
|
||||
if _, err := os.Stat(filename); err == nil {
|
||||
return fmt.Errorf("file %s already exists (overwrite not allowed)", filename)
|
||||
@@ -297,7 +316,7 @@ func (p *Paths) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *_CAConfig) Validate() error {
|
||||
func (c *CAConfig) Validate() error {
|
||||
if c.Name == "" {
|
||||
return fmt.Errorf("CA 'name' is required")
|
||||
}
|
||||
@@ -321,38 +340,39 @@ func (c *_CAConfig) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func InitCA(overwrite bool) error {
|
||||
func InitCA() error {
|
||||
|
||||
var err error
|
||||
|
||||
err = LoadCAConfig()
|
||||
if err != nil {
|
||||
fmt.Printf("ERROR: %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Create certificates directory with 0755, private keys with 0700
|
||||
if CAConfig.Paths.Certificates != "" {
|
||||
if err := os.MkdirAll(CAConfig.Paths.Certificates, 0755); err != nil {
|
||||
fmt.Printf("Error creating certificates directory '%s': %v\n", CAConfig.Paths.Certificates, err)
|
||||
if caConfig.Paths.Certificates != "" {
|
||||
if err := os.MkdirAll(caConfig.Paths.Certificates, 0755); err != nil {
|
||||
fmt.Printf("Error creating certificates directory '%s': %v\n", caConfig.Paths.Certificates, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
if CAConfig.Paths.PrivateKeys != "" {
|
||||
if err := os.MkdirAll(CAConfig.Paths.PrivateKeys, 0700); err != nil {
|
||||
fmt.Printf("Error creating private keys directory '%s': %v\n", CAConfig.Paths.PrivateKeys, err)
|
||||
if caConfig.Paths.PrivateKeys != "" {
|
||||
if err := os.MkdirAll(caConfig.Paths.PrivateKeys, 0700); err != nil {
|
||||
fmt.Printf("Error creating private keys directory '%s': %v\n", caConfig.Paths.PrivateKeys, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize CAState empty state with serial starting from 1
|
||||
CAState = &_CAState{
|
||||
caState = &CAState{
|
||||
Serial: 1, // Start serial from 1
|
||||
CreatedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
UpdatedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
Certificates: []CertificateRecord{},
|
||||
}
|
||||
|
||||
keySize := CAConfig.KeySize
|
||||
keySize := caConfig.KeySize
|
||||
if keySize == 0 {
|
||||
keySize = 4096
|
||||
}
|
||||
@@ -366,28 +386,28 @@ func InitCA(overwrite bool) error {
|
||||
return fmt.Errorf("failed to generate serial number: %v", err)
|
||||
}
|
||||
|
||||
if CAConfig.Validity == "" {
|
||||
CAConfig.Validity = "5y" // Use default validity of 5 years
|
||||
if caConfig.Validity == "" {
|
||||
caConfig.Validity = "5y" // Use default validity of 5 years
|
||||
}
|
||||
|
||||
validity, err := parseValidity(CAConfig.Validity)
|
||||
validity, err := parseValidity(caConfig.Validity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
// Store CA certificate creation time
|
||||
CAState.CreatedAt = now.UTC().Format(time.RFC3339)
|
||||
caState.CreatedAt = now.UTC().Format(time.RFC3339)
|
||||
|
||||
tmpl := x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
Country: []string{CAConfig.Country},
|
||||
Organization: []string{CAConfig.Organization},
|
||||
OrganizationalUnit: optionalSlice(CAConfig.OrganizationalUnit),
|
||||
Locality: optionalSlice(CAConfig.Locality),
|
||||
Province: optionalSlice(CAConfig.Province),
|
||||
CommonName: CAConfig.Name,
|
||||
Country: []string{caConfig.Country},
|
||||
Organization: []string{caConfig.Organization},
|
||||
OrganizationalUnit: optionalSlice(caConfig.OrganizationalUnit),
|
||||
Locality: optionalSlice(caConfig.Locality),
|
||||
Province: optionalSlice(caConfig.Province),
|
||||
CommonName: caConfig.Name,
|
||||
},
|
||||
NotBefore: now,
|
||||
NotAfter: now.Add(validity),
|
||||
@@ -396,10 +416,10 @@ func InitCA(overwrite bool) error {
|
||||
IsCA: true,
|
||||
}
|
||||
// Add email if present
|
||||
if CAConfig.Email != "" {
|
||||
if caConfig.Email != "" {
|
||||
tmpl.Subject.ExtraNames = append(tmpl.Subject.ExtraNames, pkix.AttributeTypeAndValue{
|
||||
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)
|
||||
@@ -409,17 +429,17 @@ func InitCA(overwrite bool) error {
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
||||
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, overwrite); err != nil {
|
||||
if err := SavePEM(filepath.Join(caConfig.Paths.Certificates, "ca_cert.pem"), certPEM, false); err != nil {
|
||||
fmt.Println("Error saving CA certificate:", err)
|
||||
return err
|
||||
}
|
||||
if err := SavePEM(filepath.Join(CAConfig.Paths.PrivateKeys, "ca_key.pem"), keyPEM, true, overwrite); err != nil {
|
||||
if err := SavePEM(filepath.Join(caConfig.Paths.PrivateKeys, "ca_key.pem"), keyPEM, true); err != nil {
|
||||
fmt.Println("Error saving CA key:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
err = SaveCAState()
|
||||
@@ -433,7 +453,7 @@ func InitCA(overwrite bool) error {
|
||||
}
|
||||
|
||||
// Helper: issue a single certificate and key, save to files, return error if any
|
||||
func issueSingleCertificate(def CertificateDefinition, overwrite, verbose bool) error {
|
||||
func issueSingleCertificate(def CertificateDefinition) error {
|
||||
// Validate Name
|
||||
|
||||
isValidName, err := regexp.MatchString(`^[A-Za-z0-9_-]+$`, def.Name)
|
||||
@@ -445,6 +465,11 @@ func issueSingleCertificate(def CertificateDefinition, overwrite, verbose bool)
|
||||
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
|
||||
@@ -508,38 +533,39 @@ func issueSingleCertificate(def CertificateDefinition, overwrite, verbose bool)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, &certTmpl, caCert, &priv.PublicKey, caKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create certificate: %v", err)
|
||||
}
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
||||
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
|
||||
|
||||
basename := def.Name
|
||||
if basename == "" {
|
||||
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, overwrite); err != nil {
|
||||
certFile := filepath.Join(caConfig.Paths.Certificates, def.Name+".crt.pem")
|
||||
keyFile := filepath.Join(caConfig.Paths.PrivateKeys, def.Name+".key.pem")
|
||||
if err := SavePEM(certFile, certPEM, false); err != nil {
|
||||
return fmt.Errorf("error saving certificate: %v", err)
|
||||
}
|
||||
if err := SavePEM(keyFile, keyPEM, true, overwrite); err != nil {
|
||||
if err := SavePEM(keyFile, keyPEM, true); err != nil {
|
||||
return fmt.Errorf("error saving key: %v", err)
|
||||
}
|
||||
if verbose {
|
||||
@@ -558,9 +584,11 @@ Certificate:
|
||||
def.SAN,
|
||||
)
|
||||
}
|
||||
CAState.UpdateCAStateAfterIssue(
|
||||
CAConfig.SerialType,
|
||||
basename,
|
||||
caState.UpdateCAStateAfterIssue(
|
||||
caConfig.SerialType,
|
||||
def.Name,
|
||||
def.Subject,
|
||||
def.Type,
|
||||
serialNumber,
|
||||
validityDur,
|
||||
)
|
||||
@@ -596,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")
|
||||
@@ -622,7 +651,7 @@ func ProvisionCertificates(filePath string, overwrite bool, dryRun bool, verbose
|
||||
continue
|
||||
}
|
||||
|
||||
err = issueSingleCertificate(certDefs.Certificates[i], overwrite, verbose)
|
||||
err = issueSingleCertificate(def)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
errors++
|
||||
@@ -664,7 +693,7 @@ func IssueCertificate(certDef CertificateDefinition, overwrite bool, dryRun bool
|
||||
return nil
|
||||
}
|
||||
|
||||
err = issueSingleCertificate(certDef, overwrite, verbose)
|
||||
err = issueSingleCertificate(certDef)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
|
78
certdb.go
78
certdb.go
@@ -10,12 +10,11 @@ import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
// _CAState represents the persisted CA state in JSON
|
||||
type _CAState struct {
|
||||
// CAState represents the persisted CA state in JSON
|
||||
type CAState struct {
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Serial int `json:"serial,omitempty"`
|
||||
@@ -26,6 +25,8 @@ type _CAState struct {
|
||||
// CertificateRecord represents a single certificate record in the CA state
|
||||
type CertificateRecord struct {
|
||||
Name string `json:"name"`
|
||||
Subject string `json:"subject"`
|
||||
Type string `json:"type"`
|
||||
Issued string `json:"issued"`
|
||||
Expires string `json:"expires"`
|
||||
Serial string `json:"serial"`
|
||||
@@ -33,21 +34,48 @@ type CertificateRecord struct {
|
||||
RevokeReason int `json:"revokeReason,omitempty"`
|
||||
}
|
||||
|
||||
func caStatePath() string {
|
||||
return filepath.Join(filepath.Dir(CAConfigPath), CAConfig.StateName())
|
||||
// Look for a certifcate by its name
|
||||
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
|
||||
func LoadCAState() error {
|
||||
path := caStatePath()
|
||||
fmt.Printf("Loading CA state from %s\n", path)
|
||||
f, err := os.Open(path)
|
||||
fmt.Printf("Loading CA state from %s\n", caStatePath)
|
||||
f, err := os.Open(caStatePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
CAState = &_CAState{}
|
||||
if err := json.NewDecoder(f).Decode(CAState); err != nil {
|
||||
caState = &CAState{}
|
||||
if err := json.NewDecoder(f).Decode(caState); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -55,19 +83,19 @@ func LoadCAState() error {
|
||||
|
||||
// SaveCAState saves the CA state to a JSON file
|
||||
func SaveCAState() error {
|
||||
CAState.UpdatedAt = time.Now().UTC().Format(time.RFC3339)
|
||||
f, err := os.Create(caStatePath())
|
||||
caState.UpdatedAt = time.Now().UTC().Format(time.RFC3339)
|
||||
f, err := os.Create(caStatePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
enc := json.NewEncoder(f)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(CAState)
|
||||
return enc.Encode(caState)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
fmt.Fprintf(os.Stderr, "FATAL: CAState is nil in UpdateCAStateAfterIssue. This indicates a programming error.\n")
|
||||
os.Exit(1)
|
||||
@@ -77,24 +105,26 @@ func (s *_CAState) UpdateCAStateAfterIssue(serialType, basename string, serialNu
|
||||
serialStr := ""
|
||||
switch serialType {
|
||||
case "sequential":
|
||||
serialStr = fmt.Sprintf("%d", CAState.Serial)
|
||||
CAState.Serial++
|
||||
serialStr = fmt.Sprintf("%d", caState.Serial)
|
||||
caState.Serial++
|
||||
case "random":
|
||||
serialStr = fmt.Sprintf("%x", serialNumber)
|
||||
default:
|
||||
serialStr = fmt.Sprintf("%v", serialNumber)
|
||||
}
|
||||
s.AddCertificate(basename, issued, expires, serialStr)
|
||||
s.AddCertificate(name, subject, certType, issued, expires, serialStr)
|
||||
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 {
|
||||
fmt.Fprintf(os.Stderr, "FATAL: CAState is nil in AddCertificate. This indicates a programming error.\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
rec := CertificateRecord{
|
||||
Name: name,
|
||||
Subject: subject,
|
||||
Type: certType,
|
||||
Issued: issued,
|
||||
Expires: expires,
|
||||
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
|
||||
func (s *_CAState) RevokeCertificate(serial string, reason int) error {
|
||||
func (s *CAState) RevokeCertificate(serial string, reason int) error {
|
||||
if s == nil {
|
||||
fmt.Fprintf(os.Stderr, "FATAL: CAState is nil in RevokeCertificate. This indicates a programming error.\n")
|
||||
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
|
||||
// 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 {
|
||||
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")
|
||||
}
|
||||
var revokedCerts []pkix.RevokedCertificate
|
||||
@@ -162,14 +192,14 @@ func (s *_CAState) GenerateCRL(crlPath string, validityDays int) error {
|
||||
now := time.Now().UTC()
|
||||
nextUpdate := now.Add(time.Duration(validityDays) * 24 * time.Hour) // validityDays * 24 * 60 * 60 * 1000 milliseconds
|
||||
template := &x509.RevocationList{
|
||||
SignatureAlgorithm: CACert.SignatureAlgorithm,
|
||||
SignatureAlgorithm: caCert.SignatureAlgorithm,
|
||||
RevokedCertificates: revokedCerts,
|
||||
Number: big.NewInt(int64(s.CRLNumber + 1)),
|
||||
ThisUpdate: now,
|
||||
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 {
|
||||
return fmt.Errorf("failed to create CRL: %v", err)
|
||||
}
|
||||
|
@@ -9,5 +9,6 @@ ca "example_ca" {
|
||||
paths {
|
||||
certificates = "certs"
|
||||
private_keys = "private"
|
||||
state_file = "ca_state.json"
|
||||
}
|
||||
}
|
||||
|
2
go.mod
2
go.mod
@@ -1,4 +1,4 @@
|
||||
module koszewscy.waw.pl/slawek/lab-ca
|
||||
module gitea.koszewscy.waw.pl/slawek/lab-ca
|
||||
|
||||
go 1.24.5
|
||||
|
||||
|
58
main.go
58
main.go
@@ -9,22 +9,34 @@ import (
|
||||
|
||||
var Version = "dev"
|
||||
|
||||
// Global flags available to all commands
|
||||
var overwrite bool
|
||||
var dryRun bool
|
||||
var verbose bool
|
||||
|
||||
func main() {
|
||||
|
||||
var overwrite bool
|
||||
// list command flags
|
||||
var listRevoked bool
|
||||
|
||||
// issue command flags
|
||||
var name string
|
||||
var subject string
|
||||
var certType string
|
||||
var validity string
|
||||
var san []string
|
||||
var name string
|
||||
var dryRun bool
|
||||
var verbose bool
|
||||
|
||||
// provision command flags
|
||||
var provisionFile string
|
||||
|
||||
// crl command flags
|
||||
var crlFile string
|
||||
var crlValidityDays int
|
||||
|
||||
// revoke command flags
|
||||
var revokeName string
|
||||
var revokeSerial string
|
||||
var revokeReasonStr string
|
||||
var provisionFile string
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "lab-ca",
|
||||
@@ -39,18 +51,41 @@ func main() {
|
||||
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")
|
||||
rootCmd.PersistentFlags().StringVar(&caConfigPath, "config", "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",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
InitCA(overwrite)
|
||||
InitCA()
|
||||
},
|
||||
}
|
||||
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",
|
||||
@@ -73,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.")
|
||||
@@ -115,7 +151,7 @@ func main() {
|
||||
serial := ""
|
||||
if revokeName != "" {
|
||||
found := false
|
||||
for _, rec := range CAState.Certificates {
|
||||
for _, rec := range caState.Certificates {
|
||||
if rec.Name == revokeName {
|
||||
serial = rec.Serial
|
||||
found = true
|
||||
@@ -148,7 +184,7 @@ func main() {
|
||||
fmt.Println()
|
||||
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)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -172,7 +208,7 @@ func main() {
|
||||
if crlValidityDays <= 0 {
|
||||
crlValidityDays = 30 // default to 30 days
|
||||
}
|
||||
err := CAState.GenerateCRL(crlFile, crlValidityDays)
|
||||
err := caState.GenerateCRL(crlFile, crlValidityDays)
|
||||
if err != nil {
|
||||
fmt.Printf("ERROR generating CRL: %v\n", err)
|
||||
os.Exit(1)
|
||||
|
Reference in New Issue
Block a user