10 Commits

10 changed files with 47 additions and 83 deletions

View File

@@ -11,44 +11,17 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# 1. Checkout source code
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
# 2. Setup Go environment
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: '1.24.5' go-version: '1.24.5'
# 3. Build binary with Version injected
- name: Build binary - name: Build binary
run: | run: |
VERSION=${GITEA_REF_NAME} VERSION=${GITEA_REF_NAME}
echo "Building version $VERSION" echo "Building version $VERSION"
go mod tidy go mod tidy
go build -ldflags "-s -w -X main.Version=$VERSION" -o lab-ca . 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

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

@@ -1,5 +1,7 @@
# Lab CA # Lab CA
[![Build Status](https://gitea.koszewscy.waw.pl/slawek/lab-ca/actions/workflows/release.yml/badge.svg)](https://gitea.koszewscy.waw.pl/slawek/lab-ca/actions?workflow=release.yml)
This repository contains a simple CLI tool for managing a Certificate Authority (CA). This repository contains a simple CLI tool for managing a Certificate Authority (CA).
It is designed to be easy to use and provides a basic set of CA features: It is designed to be easy to use and provides a basic set of CA features:
@@ -86,7 +88,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 +96,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 +106,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 +224,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"