From 5c1727174d67f479fd8980b9102834c20bf60f6b Mon Sep 17 00:00:00 2001 From: Slawek Koszewski Date: Sun, 27 Jul 2025 11:21:05 +0200 Subject: [PATCH] A working initial prototype. --- .gitignore | 8 +++ README.md | 16 ++++++ go.mod | 17 ++++++ main.go | 152 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 193 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 go.mod create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..090701a --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# Don't store the Go summary file +go.sum +# Ignore the binary output +lab-ca* +# Ignore any certificate files +*.pem +# Ignore the default CA configuration file +ca_config.hcl diff --git a/README.md b/README.md new file mode 100644 index 0000000..b485d27 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# Lab CA + +This repository contains a simple CLI tool for managing a Certificate Authority (CA). + +It has been designed to as easy to use as possible and provides a basic set of CA features: + +- Create a CA and a self-signed CA certificate +- Create and sing a few most common types of certificates: + - Server certificate + - Client certificate + - Code signing certificate + - Email certificate +- Sign a CSR to create a certificate +- Revoke a certificate +- List issued certificates +- Create a CRL (Certificate Revocation List) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f8a67c0 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module koszewscy.waw.pl/slawek/lab-ca + +go 1.24.5 + +require github.com/hashicorp/hcl/v2 v2.24.0 + +require ( + github.com/agext/levenshtein v1.2.1 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/zclconf/go-cty v1.16.3 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/sync v0.14.0 // indirect + golang.org/x/text v0.25.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect +) diff --git a/main.go b/main.go new file mode 100644 index 0000000..f006d67 --- /dev/null +++ b/main.go @@ -0,0 +1,152 @@ +package main + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "flag" + "fmt" + "math/big" + "os" + "time" + + gohcl "github.com/hashicorp/hcl/v2/gohcl" + hclparse "github.com/hashicorp/hcl/v2/hclparse" +) + +// CAConfig represents a CA block in HCL +// Example HCL: +// +// ca "example_ca" { +// name = "My CA Name" +// country = "PL" +// organization = "ACME Corp." +// } +type CAConfig struct { + Label string `hcl:",label"` + Name string `hcl:"name"` + Country string `hcl:"country"` + Organization string `hcl:"organization"` + SerialType string `hcl:"serial_type"` +} + +// ConfigFile represents the root of the HCL file +// It can contain multiple CA blocks +// Example: ca "example_ca" { ... } +type ConfigFile struct { + CAs []CAConfig `hcl:"ca,block"` +} + +// LoadCAConfig loads the CA configuration from an HCL file (first CA block) +func LoadCAConfig(path string) (*CAConfig, error) { + parser := hclparse.NewParser() + file, diags := parser.ParseHCLFile(path) + if diags.HasErrors() { + return nil, fmt.Errorf("failed to parse HCL: %s", diags.Error()) + } + var configFile ConfigFile + diags = gohcl.DecodeBody(file.Body, nil, &configFile) + if diags.HasErrors() { + return nil, fmt.Errorf("failed to decode HCL: %s", diags.Error()) + } + if len(configFile.CAs) == 0 { + return nil, fmt.Errorf("no 'ca' block found in config file") + } + ca := &configFile.CAs[0] + if err := ca.Validate(); err != nil { + return nil, err + } + return ca, nil +} + +// GenerateCA generates a new CA certificate and private key +func GenerateCA(config *CAConfig) ([]byte, []byte, error) { + priv, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, nil, err + } + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, nil, fmt.Errorf("failed to generate serial number: %v", err) + } + tmpl := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Country: []string{config.Country}, + Organization: []string{config.Organization}, + CommonName: config.Name, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + } + certDER, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, &priv.PublicKey, priv) + if err != nil { + return nil, nil, err + } + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}) + return certPEM, keyPEM, nil +} + +func FileMode(secure bool) os.FileMode { + if secure { + return 0600 // Read/write for owner only + } else { + return 0644 // Read/write for owner, read for group and others + } +} + +func SavePEM(filename string, data []byte, secure bool) error { + return os.WriteFile(filename, data, FileMode(secure)) +} + +// Validate checks required fields and sets defaults for CAConfig +func (c *CAConfig) Validate() error { + if c.Name == "" { + return fmt.Errorf("CA 'name' is required") + } + if c.Country == "" { + return fmt.Errorf("CA 'country' is required") + } + if c.Organization == "" { + return fmt.Errorf("CA 'organization' is required") + } + if c.SerialType == "" { + c.SerialType = "random" + } + if c.SerialType != "random" && c.SerialType != "sequential" { + return fmt.Errorf("CA 'serial_type' must be 'random' or 'sequential'") + } + return nil +} + +func main() { + initCA := flag.Bool("initca", false, "Generate a new CA certificate and key") + configPath := flag.String("config", "ca_config.hcl", "Path to CA configuration file") + flag.Parse() + + if *initCA { + config, err := LoadCAConfig(*configPath) + if err != nil { + fmt.Println("Error loading config:", err) + return + } + certPEM, keyPEM, err := GenerateCA(config) + if err != nil { + fmt.Println("Error generating CA:", err) + return + } + SavePEM("ca_cert.pem", certPEM, false) + SavePEM("ca_key.pem", keyPEM, true) + fmt.Println("CA certificate and key generated.") + return + } + + fmt.Println("No action specified. Use -initca to generate a CA.") +}