Compare commits
	
		
			15 Commits
		
	
	
		
			v0.3
			...
			45dfdf0afc
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 45dfdf0afc | |||
| 6e69377d1a | |||
| 8114d667ec | |||
| 176901d960 | |||
| 1991963cab | |||
| b0f0467346 | |||
| 028788f357 | |||
| 090fb4b423 | |||
| eb5c5c0e43 | |||
| 8181ac8287 | |||
| 3fe908226d | |||
| 8a36588c62 | |||
| 90ce7edd28 | |||
| 11bed9c8b1 | |||
| 2fe228858f | 
							
								
								
									
										54
									
								
								.gitea/workflows/release.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								.gitea/workflows/release.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,54 @@
 | 
			
		||||
name: Release
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
  push:
 | 
			
		||||
    tags:
 | 
			
		||||
      - 'v*.*.*'
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  release:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      # 1. Checkout source code
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
 | 
			
		||||
      # 2. Setup Go environment
 | 
			
		||||
      - name: Setup Go
 | 
			
		||||
        uses: actions/setup-go@v5
 | 
			
		||||
        with:
 | 
			
		||||
          go-version: '1.24.5'
 | 
			
		||||
 | 
			
		||||
      # 3. Build binary with Version injected
 | 
			
		||||
      - name: Build binary
 | 
			
		||||
        run: |
 | 
			
		||||
          VERSION=${GITEA_REF_NAME}
 | 
			
		||||
          echo "Building version $VERSION"
 | 
			
		||||
          go mod tidy
 | 
			
		||||
          go build -ldflags "-s -w -X main.Version=$VERSION" -o lab-ca .
 | 
			
		||||
 | 
			
		||||
      # 4. Install the tea CLI
 | 
			
		||||
      - name: Install tea CLI
 | 
			
		||||
        run: go install code.gitea.io/tea@latest
 | 
			
		||||
 | 
			
		||||
      # 5. Authenticate tea CLI
 | 
			
		||||
      - name: Login to Gitea
 | 
			
		||||
        run: |
 | 
			
		||||
          tea login add --name ci --url $GITEA_URL --token $GITEA_TOKEN
 | 
			
		||||
        env:
 | 
			
		||||
          GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
 | 
			
		||||
          GITEA_URL: ${{ secrets.GITEA_URL }}
 | 
			
		||||
 | 
			
		||||
      # 6. Create or update release
 | 
			
		||||
      - name: Create or update release
 | 
			
		||||
        run: |
 | 
			
		||||
          tea release create $GITEA_REF_NAME \
 | 
			
		||||
            --title "$GITEA_REF_NAME" \
 | 
			
		||||
            --note "Automated release for $GITEA_REF_NAME" || \
 | 
			
		||||
          echo "Release already exists, skipping create."
 | 
			
		||||
 | 
			
		||||
      # 7. Upload binary to the release
 | 
			
		||||
      - name: Upload binary
 | 
			
		||||
        run: tea release upload $GITEA_REF_NAME lab-ca
 | 
			
		||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -2,6 +2,7 @@
 | 
			
		||||
**/go.sum
 | 
			
		||||
# Ignore the binary output
 | 
			
		||||
lab-ca*
 | 
			
		||||
build
 | 
			
		||||
# Ignore any certificate files
 | 
			
		||||
*.pem
 | 
			
		||||
# Ignore CA configuration and certificate definitions.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								README.md
									
									
									
									
									
								
							@@ -221,6 +221,16 @@ The tool checks that SANs are valid for the selected certificate type(s). Certif
 | 
			
		||||
 | 
			
		||||
See `examples/example-certificates.hcl` for a more advanced provisioning file with templates and variables.
 | 
			
		||||
 | 
			
		||||
## Building the Tool
 | 
			
		||||
 | 
			
		||||
The repository includes a `build.sh` script to build the CLI tool. It updates the version in `version.go` and builds the binary.
 | 
			
		||||
 | 
			
		||||
To ignore changes made to `version.go` in Git, you can run:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
git update-index --assume-unchanged version.go
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Notes
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										19
									
								
								build.sh
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								build.sh
									
									
									
									
									
								
							@@ -6,4 +6,21 @@ if [ $? -eq 0 ]; then
 | 
			
		||||
else
 | 
			
		||||
  VERSION="dev"
 | 
			
		||||
fi
 | 
			
		||||
go build -ldflags "-X main.Version=$VERSION" -o lab-ca
 | 
			
		||||
 | 
			
		||||
# Hardcode the version into main.go
 | 
			
		||||
sed -i '' "s/^var Version = .*/var Version = \"$VERSION\"/" version.go
 | 
			
		||||
 | 
			
		||||
if echo $VERSION | grep -q 'dirty$'; then
 | 
			
		||||
  echo "Building in development mode, output directory is set to 'build'."
 | 
			
		||||
  OUTPUT_DIR=build
 | 
			
		||||
 | 
			
		||||
  # Make sure the output directory exists, create it if it is not
 | 
			
		||||
  mkdir -p $OUTPUT_DIR
 | 
			
		||||
else
 | 
			
		||||
  echo "Building with version: $VERSION"
 | 
			
		||||
  OUTPUT_DIR=$GOHOME/bin
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
# Build the Lab CA binary with version information
 | 
			
		||||
# go build -ldflags "-X main.Version=$VERSION" -o $OUTPUT_DIR/lab-ca
 | 
			
		||||
go build -o $OUTPUT_DIR/lab-ca
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										27
									
								
								ca.go
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								ca.go
									
									
									
									
									
								
							@@ -72,7 +72,7 @@ func (def *CertificateDefinition) FillDefaultValues(defaults *CertificateDefault
 | 
			
		||||
		def.Validity = defaults.Validity
 | 
			
		||||
	}
 | 
			
		||||
	if len(def.SAN) == 0 && len(defaults.SAN) > 0 {
 | 
			
		||||
		def.SAN = defaults.SAN
 | 
			
		||||
		def.SAN = append([]string(nil), defaults.SAN...)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -291,14 +291,9 @@ func parseValidity(validity string) (time.Duration, 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)
 | 
			
		||||
		} else if !os.IsNotExist(err) {
 | 
			
		||||
			return fmt.Errorf("could not check file %s: %v", filename, err)
 | 
			
		||||
		return fmt.Errorf("file %s already exists", filename)
 | 
			
		||||
	}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if secure {
 | 
			
		||||
		return os.WriteFile(filename, data, 0600)
 | 
			
		||||
	} else {
 | 
			
		||||
@@ -477,8 +472,16 @@ func issueSingleCertificate(def CertificateDefinition) error {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Add default dns SAN for server/server-only if none specified
 | 
			
		||||
	if (def.Type == "server" || def.Type == "server-only") && len(def.SAN) == 0 {
 | 
			
		||||
		def.SAN = append(def.SAN, "dns:"+def.Subject)
 | 
			
		||||
	if strings.Contains(def.Type, "server") && len(def.SAN) == 0 {
 | 
			
		||||
		// Extract CN if subject is a DN, else use subject as is
 | 
			
		||||
		cn := def.Subject
 | 
			
		||||
		if isDNFormat(def.Subject) {
 | 
			
		||||
			dn := parseDistinguishedName(def.Subject)
 | 
			
		||||
			if dn.CommonName != "" {
 | 
			
		||||
				cn = dn.CommonName
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		def.SAN = append(def.SAN, "dns:"+cn)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	priv, err := rsa.GenerateKey(rand.Reader, 4096)
 | 
			
		||||
@@ -600,7 +603,7 @@ Certificate:
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// A prototype of certificate provisioning function
 | 
			
		||||
func ProvisionCertificates(filePath string, overwrite bool, dryRun bool, verbose bool) error {
 | 
			
		||||
func ProvisionCertificates(filePath string) error {
 | 
			
		||||
	err := LoadCA()
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -677,7 +680,7 @@ func ProvisionCertificates(filePath string, overwrite bool, dryRun bool, verbose
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func IssueCertificate(certDef CertificateDefinition, overwrite bool, dryRun bool, verbose bool) error {
 | 
			
		||||
func IssueCertificate(certDef CertificateDefinition) error {
 | 
			
		||||
	err := LoadCA()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
 | 
			
		||||
@@ -705,7 +708,7 @@ func IssueCertificate(certDef CertificateDefinition, overwrite bool, dryRun bool
 | 
			
		||||
 | 
			
		||||
	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)
 | 
			
		||||
		fmt.Printf("Error saving CA state: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										29
									
								
								certdb.go
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								certdb.go
									
									
									
									
									
								
							@@ -36,41 +36,42 @@ type CertificateRecord struct {
 | 
			
		||||
 | 
			
		||||
// Look for a certifcate by its name
 | 
			
		||||
func (c *CAState) FindByName(name string, all bool) *CertificateRecord {
 | 
			
		||||
	for _, cert := range c.Certificates {
 | 
			
		||||
	for i := range c.Certificates {
 | 
			
		||||
		cert := &c.Certificates[i]
 | 
			
		||||
		if cert.RevokedAt != "" && !all {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		if cert.Name == name {
 | 
			
		||||
			return &cert
 | 
			
		||||
			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 {
 | 
			
		||||
	for i := range c.Certificates {
 | 
			
		||||
		cert := &c.Certificates[i]
 | 
			
		||||
		if cert.RevokedAt != "" && !all {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		if cert.Serial == serial {
 | 
			
		||||
			return &cert
 | 
			
		||||
			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 {
 | 
			
		||||
	fmt.Printf("Loading CA state from %s\n", caStatePath)
 | 
			
		||||
	f, err := os.Open(caStatePath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if os.IsNotExist(err) {
 | 
			
		||||
			// File does not exist, treat as empty state
 | 
			
		||||
			caState = &CAState{}
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer f.Close()
 | 
			
		||||
@@ -97,8 +98,7 @@ func SaveCAState() error {
 | 
			
		||||
// UpdateCAStateAfterIssue updates the CA state JSON after issuing a certificate
 | 
			
		||||
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)
 | 
			
		||||
		return fmt.Errorf("CAState is nil in UpdateCAStateAfterIssue. This indicates a programming error.")
 | 
			
		||||
	}
 | 
			
		||||
	issued := time.Now().UTC().Format(time.RFC3339)
 | 
			
		||||
	expires := time.Now().Add(validity).UTC().Format(time.RFC3339)
 | 
			
		||||
@@ -119,7 +119,7 @@ func (s *CAState) UpdateCAStateAfterIssue(serialType, name string, subject strin
 | 
			
		||||
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)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	rec := CertificateRecord{
 | 
			
		||||
		Name:    name,
 | 
			
		||||
@@ -135,8 +135,7 @@ func (s *CAState) AddCertificate(name, subject, certType, issued, expires, seria
 | 
			
		||||
// 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 {
 | 
			
		||||
	if s == nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "FATAL: CAState is nil in RevokeCertificate. This indicates a programming error.\n")
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
		return fmt.Errorf("CAState is nil in RevokeCertificate. This indicates a programming error.")
 | 
			
		||||
	}
 | 
			
		||||
	revoked := false
 | 
			
		||||
	revokedAt := time.Now().UTC().Format(time.RFC3339)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								ignore-changes-to-version-go.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										3
									
								
								ignore-changes-to-version-go.sh
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
git update-index --assume-unchanged version.go
 | 
			
		||||
							
								
								
									
										20
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								main.go
									
									
									
									
									
								
							@@ -7,10 +7,7 @@ import (
 | 
			
		||||
	"github.com/spf13/cobra"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var Version = "dev"
 | 
			
		||||
 | 
			
		||||
// Global flags available to all commands
 | 
			
		||||
var overwrite bool
 | 
			
		||||
var dryRun bool
 | 
			
		||||
var verbose bool
 | 
			
		||||
 | 
			
		||||
@@ -48,7 +45,6 @@ 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", "ca_config.hcl", "Path to CA configuration file")
 | 
			
		||||
@@ -97,7 +93,7 @@ func main() {
 | 
			
		||||
				Type:     certType,
 | 
			
		||||
				Validity: validity,
 | 
			
		||||
				SAN:      san,
 | 
			
		||||
			}, overwrite, dryRun, verbose)
 | 
			
		||||
			})
 | 
			
		||||
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
 | 
			
		||||
@@ -122,7 +118,7 @@ func main() {
 | 
			
		||||
		Short: "Provision certificates from a batch file (HCL)",
 | 
			
		||||
		Run: func(cmd *cobra.Command, args []string) {
 | 
			
		||||
 | 
			
		||||
			err := ProvisionCertificates(provisionFile, overwrite, false, verbose)
 | 
			
		||||
			err := ProvisionCertificates(provisionFile)
 | 
			
		||||
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
 | 
			
		||||
@@ -225,7 +221,7 @@ func main() {
 | 
			
		||||
		Use:   "version",
 | 
			
		||||
		Short: "Show version information",
 | 
			
		||||
		Run: func(cmd *cobra.Command, args []string) {
 | 
			
		||||
			fmt.Printf("lab-ca version: %s\n", Version)
 | 
			
		||||
			fmt.Printf("lab-ca version: %s\n", getVersionDescription())
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	rootCmd.AddCommand(versionCmd)
 | 
			
		||||
@@ -235,15 +231,23 @@ func main() {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getVersionDescription() string {
 | 
			
		||||
	if Version == "" {
 | 
			
		||||
		return "no version information was compiled in"
 | 
			
		||||
	}
 | 
			
		||||
	return Version
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func printMainHelp() {
 | 
			
		||||
	fmt.Printf("lab-ca - Certificate Authority Utility\n")
 | 
			
		||||
	fmt.Printf("Version: %s\n", Version)
 | 
			
		||||
	fmt.Printf("Version: %s\n", getVersionDescription())
 | 
			
		||||
	fmt.Println()
 | 
			
		||||
	fmt.Println("Usage:")
 | 
			
		||||
	fmt.Println("  lab-ca <command> [options]")
 | 
			
		||||
	fmt.Println()
 | 
			
		||||
	fmt.Println("Available commands:")
 | 
			
		||||
	fmt.Println("  initca     Generate a new CA certificate and key")
 | 
			
		||||
	fmt.Println("  list       List issued certificates")
 | 
			
		||||
	fmt.Println("  issue      Issue a new certificate")
 | 
			
		||||
	fmt.Println("  provision  Provision certificates from a batch file (HCL)")
 | 
			
		||||
	fmt.Println("  revoke     Revoke a certificate by name or serial number")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
GREEN='\033[0;32m'
 | 
			
		||||
NC='\033[0m' # No Color
 | 
			
		||||
LAB_CA="./lab-ca"
 | 
			
		||||
LAB_CA="build/lab-ca"
 | 
			
		||||
PROVISION_CONFIG="examples/example-certificates.hcl"
 | 
			
		||||
# Build and install
 | 
			
		||||
# Build script for lab-ca with version injection from git tag
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										7
									
								
								set-version.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										7
									
								
								set-version.sh
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
VERSION=${1:-$(git describe --tags --always --dirty 2>/dev/null || echo "dev")}
 | 
			
		||||
# Allow git to track changes to version.go
 | 
			
		||||
git update-index --no-assume-unchanged version.go
 | 
			
		||||
# Hardcode the version into main.go
 | 
			
		||||
sed -i '' "s/^var Version = .*/var Version = \"$VERSION\"/" version.go
 | 
			
		||||
							
								
								
									
										3
									
								
								version.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								version.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
var Version = "v0.3.2"
 | 
			
		||||
		Reference in New Issue
	
	Block a user