5 Commits

9 changed files with 45 additions and 56 deletions

19
Makefile Normal file
View File

@@ -0,0 +1,19 @@
# Get version from git tags
VERSION := $(shell git describe --tags --always 2>/dev/null)
.PHONY: clean
build/lab-ca: main.go ca.go certdb.go
@mkdir -p build
ifneq ($(VERSION),)
@echo "Building version: $(VERSION)"
go build -o build/lab-ca -ldflags "-X main.Version=$(VERSION)"
else
@echo "Building without version information"
go build -o build/lab-ca .
endif
lab-ca: build/lab-ca
clean:
rm -f build/lab-ca

View File

@@ -86,7 +86,7 @@ lab-ca list --revoked
Issue a new certificate from the command line: Issue a new certificate from the command line:
```bash ```bash
lab-ca issue --name <name> [--subject <subject>] [--type <type>] [--validity <period>] [--san <SAN> ...] [--overwrite] [--dry-run] [--verbose] lab-ca issue --name <name> [--subject <subject>] [--type <type>] [--validity <period>] [--san <SAN> ...] [--dry-run] [--verbose]
``` ```
- `--name` (required): Name for the certificate and key files (used as subject if `--subject` is omitted) - `--name` (required): Name for the certificate and key files (used as subject if `--subject` is omitted)
@@ -94,7 +94,6 @@ lab-ca issue --name <name> [--subject <subject>] [--type <type>] [--validity <pe
- `--type`: Certificate type: `client`, `server`, `code-signing`, `email` (comma-separated for multiple usages; default: `server`) - `--type`: Certificate type: `client`, `server`, `code-signing`, `email` (comma-separated for multiple usages; default: `server`)
- `--validity`: Validity period (e.g. `2y`, `6m`, `30d`; default: `1y`) - `--validity`: Validity period (e.g. `2y`, `6m`, `30d`; default: `1y`)
- `--san`: Subject Alternative Name (repeatable; e.g. `dns:example.com`, `ip:1.2.3.4`, `email:user@example.com`) - `--san`: Subject Alternative Name (repeatable; e.g. `dns:example.com`, `ip:1.2.3.4`, `email:user@example.com`)
- `--overwrite`: Allow overwriting existing files
- `--dry-run`: Validate and show what would be created, but do not write files - `--dry-run`: Validate and show what would be created, but do not write files
- `--verbose`: Print detailed information - `--verbose`: Print detailed information
@@ -105,7 +104,7 @@ lab-ca issue --name <name> [--subject <subject>] [--type <type>] [--validity <pe
Provision multiple certificates from a batch file (HCL): Provision multiple certificates from a batch file (HCL):
```bash ```bash
lab-ca provision --file <certificates.hcl> [--overwrite] [--verbose] lab-ca provision --file <certificates.hcl> [--verbose]
``` ```
#### Example HCL Provisioning File #### Example HCL Provisioning File
@@ -223,13 +222,9 @@ See `examples/example-certificates.hcl` for a more advanced provisioning file wi
## Building the Tool ## 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. The repository includes a `Makefile` to build the CLI tool. It automatically determines the version from Git tags and builds the binary.
To ignore changes made to `version.go` in Git, you can run: To build the tool, run the `make` command. The binary will be created as `build/lab-ca`.
```bash
git update-index --assume-unchanged version.go
```
--- ---

View File

@@ -1,26 +0,0 @@
#!/bin/sh
# Build script for lab-ca with version injection from git tag
git describe --tags --always --dirty > /dev/null 2>&1
if [ $? -eq 0 ]; then
VERSION=$(git describe --tags --always --dirty)
else
VERSION="dev"
fi
# 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

20
ca.go
View File

@@ -460,9 +460,19 @@ func issueSingleCertificate(def CertificateDefinition, i int, n int) (bool, erro
return false, fmt.Errorf("certificate name must be specified and contain only letters, numbers, dash, or underscore") return false, 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. // Check if the certificate is in database, skip if it already exists and is valid.
if caState.FindByName(def.Name, false) != nil { if caState.FindByName(def.Name, false) != nil {
return false, fmt.Errorf("certificate %s already exists and is valid.", def.Name) if !dryRun {
fmt.Printf("skipped (already exists).\n")
} else {
msg := fmt.Sprintf("Certificate '%s' already exists and is valid (would skip).", def.Name)
if n > 1 {
fmt.Printf("[%d/%d] %s\n", i+1, n, msg)
} else {
fmt.Printf("%s\n", msg)
}
}
return false, nil
} }
// Initialize Subject if not specified // Initialize Subject if not specified
@@ -634,7 +644,7 @@ Certificate generated:
// If saving CA state fails, we still return success for the certificate issuance // If saving CA state fails, we still return success for the certificate issuance
fmt.Printf("WARNING: %v\n", err) fmt.Printf("WARNING: %v\n", err)
fmt.Println("CA state not saved, but certificate issued and saved successfully.") fmt.Println("CA state not saved, but certificate issued and saved successfully.")
return true, fmt.Errorf("Error saving CA state: %v", err) return true, fmt.Errorf("error saving CA state: %v", err)
} }
return true, nil return true, nil
@@ -655,12 +665,12 @@ func ProvisionCertificates(filePath string) error {
// Load certificates provisioning configuration from the file (HCL syntax) // Load certificates provisioning configuration from the file (HCL syntax)
err = certDefs.LoadFromFile(filePath) err = certDefs.LoadFromFile(filePath)
if err != nil { if err != nil {
return fmt.Errorf("Error loading certificates file: %v", err) return fmt.Errorf("error loading certificates file: %v", err)
} }
// The certificate provisioning file must contain at least one certificate definition // The certificate provisioning file must contain at least one certificate definition
if len(certDefs.Certificates) < 1 { if len(certDefs.Certificates) < 1 {
return fmt.Errorf("No certificates defined in %s", filePath) return fmt.Errorf("no certificates defined in %s", filePath)
} }
// We will be counting successes and errors // We will be counting successes and errors

View File

@@ -98,7 +98,7 @@ func SaveCAState() error {
// 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, name string, subject string, certType 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 {
return fmt.Errorf("CAState is nil in UpdateCAStateAfterIssue. This indicates a programming error.") return fmt.Errorf("CAState is nil in UpdateCAStateAfterIssue. This indicates a programming error")
} }
issued := time.Now().UTC().Format(time.RFC3339) issued := time.Now().UTC().Format(time.RFC3339)
expires := time.Now().Add(validity).UTC().Format(time.RFC3339) expires := time.Now().Add(validity).UTC().Format(time.RFC3339)
@@ -135,7 +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 // 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 {
return fmt.Errorf("CAState is nil in RevokeCertificate. This indicates a programming error.") return fmt.Errorf("CAState is nil in RevokeCertificate. This indicates a programming error")
} }
revoked := false revoked := false
revokedAt := time.Now().UTC().Format(time.RFC3339) revokedAt := time.Now().UTC().Format(time.RFC3339)

View File

@@ -1,3 +0,0 @@
#!/bin/bash
git update-index --assume-unchanged version.go

View File

@@ -10,6 +10,7 @@ import (
// Global flags available to all commands // Global flags available to all commands
var dryRun bool var dryRun bool
var verbose bool var verbose bool
var Version = "v0.4.0"
func main() { func main() {
@@ -70,10 +71,13 @@ func main() {
os.Exit(1) os.Exit(1)
} }
for _, certDef := range caState.Certificates { for _, certDef := range caState.Certificates {
if certDef.RevokedAt != "" { if certDef.RevokedAt != "" && !listRevoked {
continue continue
} }
fmt.Printf("Certificate %s\n", certDef.Name) fmt.Printf("Certificate %s\n", certDef.Name)
if certDef.RevokedAt != "" {
fmt.Printf("\tStatus: REVOKED (at %s)\n", certDef.RevokedAt)
}
fmt.Printf("\tSubject: %s\n\tType: %s\n\tIssued at: %s\n", fmt.Printf("\tSubject: %s\n\tType: %s\n\tIssued at: %s\n",
certDef.Subject, certDef.Type, certDef.Issued) certDef.Subject, certDef.Type, certDef.Issued)
} }

View File

@@ -1,7 +0,0 @@
#!/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

View File

@@ -1,3 +0,0 @@
package main
var Version = "v0.4.0"