diff --git a/.github/workflows/claude.yaml b/.github/workflows/claude.yaml new file mode 100644 index 0000000..3ab7dcb --- /dev/null +++ b/.github/workflows/claude.yaml @@ -0,0 +1,47 @@ +name: Claude PR Assistant + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude-code-action: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && contains(github.event.issue.body, '@claude')) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude PR Action + uses: anthropics/claude-code-action@beta + with: + # anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + # Or use OAuth token instead: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + timeout_minutes: "60" + # Optional: Restrict network access to specific domains only + # experimental_allowed_domains: | + # .anthropic.com + # .github.com + # api.github.com + # .githubusercontent.com + # bun.sh + # registry.npmjs.org + # .blob.core.windows.net diff --git a/ca-server/.gitignore b/ca-server/.gitignore new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/ca-server/.gitignore @@ -0,0 +1 @@ +build diff --git a/ca-server/CLAUDE.md b/ca-server/CLAUDE.md new file mode 100644 index 0000000..64ea7ba --- /dev/null +++ b/ca-server/CLAUDE.md @@ -0,0 +1,151 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is "xpki" (eXtensible PKI) - a Go-based Certificate Authority (CA) management system with both CLI and HTTP server components. The application provides PKI infrastructure with support for TLS and mTLS configurations. + +## Architecture + +The project follows a clean architecture pattern with: +- **CLI Interface**: Cobra-based command structure with `server` and `setup` subcommands +- **HTTP Server**: Gin framework with middleware, controllers, and routes +- **PKI Core**: Certificate Authority functionality in `internal/certificate_authority/` +- **Multi-server Support**: HTTP, TLS, and mTLS server modes + +### Key Components + +- `cmd/`: Cobra command definitions (server, setup) +- `internal/certificate_authority/`: Core CA functionality and types +- `controllers/`: HTTP request handlers (cert_controller, user_controller) +- `models/`: Data structures and in-memory store +- `middleware/`: Custom middleware (auth, logger) +- `routes/`: API route definitions +- `config/`: Application configuration management + +## Development Commands + +This project uses [Task](https://taskfile.dev) as a task runner. Install it first: + +**Quick Installation (using provided script):** +```bash +# Run the provided installation script +./scripts/install-task.sh +``` + +**Manual Installation:** +```bash +# Install Task (various methods available) +go install github.com/go-task/task/v3/cmd/task@latest +# or: brew install go-task/tap/go-task +# or: curl -sL https://taskfile.dev/install.sh | sh +``` + +### Common Tasks +```bash +# Show all available tasks +task + +# Development workflow +task workflow:dev # Complete dev workflow (clean, deps, lint, test, build) +task workflow:setup # Initial project setup + +# Build tasks +task build # Build the application +task build:debug # Build with debug symbols +task build:release # Build optimized release binary + +# Development +task dev # Run in development mode +task dev:setup # Setup PKI directory structure + +# Testing +task test # Run all tests +task test:coverage # Run tests with coverage +task test:race # Run tests with race detection + +# Code quality +task lint # Run linters +task lint:fix # Fix linting issues + +# Dependencies +task deps # Download dependencies +task deps:update # Update dependencies +``` + +### Server Tasks +```bash +# Start servers +task server # Start HTTP server +task server:dev # Start server in development mode +task server:tls # Start server with TLS +task server:mtls # Start server with mTLS + +# Test connectivity +task test:http # Test HTTP server +task test:tls # Test TLS server +task test:mtls # Test mTLS server +``` + +### PKI Tasks +```bash +# PKI management +task pki:setup # Setup PKI directory structure +task pki:clean # Clean PKI directory + +# Certificate generation +task cert:ca # Generate CA certificate +task cert:server # Generate server certificate +task cert:client # Generate client certificate + +# Clean up +task clean # Clean build artifacts +task clean:all # Clean everything including PKI and certs +``` + +### Legacy Commands (without Task) +```bash +# Install dependencies +go mod tidy + +# Build the application +go build -o xpki + +# Run CLI commands +go run main.go setup # Setup PKI directory structure +go run main.go server # Start the HTTP/TLS/mTLS server +``` + +## Configuration + +The application uses environment variables for configuration: +- `SERVER_PORT`: HTTP server port (default: 8080) +- `GIN_MODE`: Gin mode (debug/release) +- `LOG_LEVEL`: Logging level + +## Code Style (from .github/copilot-instructions.md) + +- Use camelCase for variables, PascalCase for exported functions +- Group imports: standard library, external packages, local packages +- Include comments for exported functions and types +- Follow Go best practices for error handling +- Use dependency injection for services and repositories +- New route handlers go in appropriate controllers +- New middleware registered in main.go +- New environment variables added to config.go + +## Server Modes + +The application supports three server modes: +1. **HTTP**: Standard HTTP server (port 8080) +2. **TLS**: HTTPS server with server certificates +3. **mTLS**: Mutual TLS with client certificate validation + +## Current Development Status + +Based on recent commits, the project is in active development with: +- CLI setup command implementation +- PKI directory structure creation +- CA certificate generation (work in progress) +- Server configuration for multiple TLS modes \ No newline at end of file diff --git a/ca-server/README.md b/ca-server/README.md index 6a1e7b7..2ed0e62 100644 --- a/ca-server/README.md +++ b/ca-server/README.md @@ -82,17 +82,6 @@ The server will start on port 8080 by default. - `GET /health`: Health check endpoint - `GET /api/ping`: Ping endpoint -## Todo - -- [x] Gen a private/public key pairs for a user - - - [x] validate signature - -- [ ] Sign a CSR for new user -- [x] Example calling http with tls -- [x] Example calling http with mtls -- [ ] Write tests for tls and mtls case - ### Notes - quick way to generate CA certs from Go diff --git a/ca-server/TODO.md b/ca-server/TODO.md new file mode 100644 index 0000000..f2e4856 --- /dev/null +++ b/ca-server/TODO.md @@ -0,0 +1,12 @@ +# A todo list for this project + +- [] setup PKI from cli + +## Previous todo list + +- [x] Gen a private/public key pairs for a user + - [x] validate signature +- [ ] Sign a CSR for new user +- [x] Example calling http with tls +- [x] Example calling http with mtls +- [ ] Write tests for tls and mtls case diff --git a/ca-server/Taskfile.yml b/ca-server/Taskfile.yml new file mode 100644 index 0000000..b754ab7 --- /dev/null +++ b/ca-server/Taskfile.yml @@ -0,0 +1,235 @@ +version: '3' + +vars: + APP_NAME: xpki + BINARY_NAME: xpki + BUILD_DIR: ./build + PKI_DIR: .xpki + CERTS_DIR: ./certs + +tasks: + default: + desc: Show available tasks + cmds: + - task --list + + # Build tasks + build: + desc: Build the application + cmds: + - go build -o {{.BUILD_DIR}}/{{.BINARY_NAME}} . + generates: + - "{{.BUILD_DIR}}/{{.BINARY_NAME}}" + + build:debug: + desc: Build with debug symbols + cmds: + - go build -gcflags="all=-N -l" -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-debug . + generates: + - "{{.BUILD_DIR}}/{{.BINARY_NAME}}-debug" + + build:release: + desc: Build optimized release binary + cmds: + - go build -ldflags="-s -w" -o {{.BUILD_DIR}}/{{.BINARY_NAME}} . + generates: + - "{{.BUILD_DIR}}/{{.BINARY_NAME}}" + + # Development tasks + dev: + desc: Run the application in development mode + cmds: + - go run main.go server + env: + GIN_MODE: debug + LOG_LEVEL: debug + + dev:setup: + desc: Setup PKI directory structure + cmds: + - go run main.go setup + + # Testing tasks + test: + desc: Run all tests + cmds: + - go test -v ./... + + test:coverage: + desc: Run tests with coverage + cmds: + - go test -v -coverprofile=coverage.out ./... + - go tool cover -html=coverage.out -o coverage.html + + test:race: + desc: Run tests with race detection + cmds: + - go test -race -v ./... + + # Code quality tasks + lint: + desc: Run linters + cmds: + - go fmt ./... + - go vet ./... + - test -z "$(gofmt -l .)" + + lint:fix: + desc: Fix linting issues + cmds: + - go fmt ./... + - go mod tidy + + # Dependencies + deps: + desc: Download dependencies + cmds: + - go mod download + + deps:update: + desc: Update dependencies + cmds: + - go get -u ./... + - go mod tidy + + deps:verify: + desc: Verify dependencies + cmds: + - go mod verify + + # Clean tasks + clean: + desc: Clean build artifacts + cmds: + - rm -rf {{.BUILD_DIR}} + - rm -f coverage.out coverage.html + + clean:all: + desc: Clean everything including PKI and certs + cmds: + - task: clean + - rm -rf {{.PKI_DIR}} + - rm -rf {{.CERTS_DIR}} + + # PKI tasks + pki:setup: + desc: Setup PKI directory structure + cmds: + - go run main.go setup + + pki:clean: + desc: Clean PKI directory + cmds: + - rm -rf {{.PKI_DIR}} + + # Certificate tasks + cert:ca: + desc: Generate CA certificate + cmds: + - curl -X POST http://localhost:8080/api/certs/ca + + cert:server: + desc: Generate server certificate + cmds: + - curl -X POST http://localhost:8080/api/certs/server + + cert:client: + desc: Generate client certificate + cmds: + - curl -X POST http://localhost:8080/api/certs/client + + # Server tasks + server: + desc: Start the HTTP server + cmds: + - go run main.go server + env: + GIN_MODE: release + + server:dev: + desc: Start server in development mode + cmds: + - go run main.go server + env: + GIN_MODE: debug + LOG_LEVEL: debug + + server:tls: + desc: Start server with TLS enabled + cmds: + - go run main.go server + env: + TLS_ENABLED: true + TLS_CERT_PATH: "{{.CERTS_DIR}}/cert.pem" + TLS_KEY_PATH: "{{.CERTS_DIR}}/key.pem" + + server:mtls: + desc: Start server with mTLS enabled + cmds: + - go run main.go server + env: + MTLS_ENABLED: true + TLS_CERT_PATH: "{{.CERTS_DIR}}/cert.pem" + TLS_KEY_PATH: "{{.CERTS_DIR}}/key.pem" + CLIENT_CA_CERT_PATH: "{{.CERTS_DIR}}/ca-cert.pem" + + # Testing connectivity + test:http: + desc: Test HTTP server connectivity + cmds: + - curl -v http://localhost:8080/health + + test:tls: + desc: Test TLS server connectivity + cmds: + - curl --cacert {{.CERTS_DIR}}/ca-cert.pem https://localhost:8080 -v + + test:mtls: + desc: Test mTLS server connectivity + cmds: + - curl --cert {{.CERTS_DIR}}/cert.pem --key {{.CERTS_DIR}}/key.pem --cacert {{.CERTS_DIR}}/ca-cert.pem https://localhost:8443 -v + + # Development workflow + workflow:dev: + desc: Complete development workflow + cmds: + - task: clean + - task: deps + - task: lint + - task: test + - task: build + + workflow:setup: + desc: Initial project setup + cmds: + - task: deps + - task: pki:setup + - task: build + + # Docker tasks (if needed) + docker:build: + desc: Build Docker image + cmds: + - docker build -t {{.APP_NAME}} . + + docker:run: + desc: Run Docker container + cmds: + - docker run -p 8080:8080 {{.APP_NAME}} + + # Install task + install: + desc: Install the binary to $GOPATH/bin + cmds: + - go install . + + # Watch tasks (requires entr or similar) + watch: + desc: Watch for changes and rebuild + cmds: + - find . -name "*.go" | entr -r task build + + watch:test: + desc: Watch for changes and run tests + cmds: + - find . -name "*.go" | entr -r task test \ No newline at end of file diff --git a/ca-server/cmd/cli/cli.go b/ca-server/cmd/cli/cli.go new file mode 100644 index 0000000..4ddaadc --- /dev/null +++ b/ca-server/cmd/cli/cli.go @@ -0,0 +1,33 @@ +package cli + +import ( + "ca-server/config" + "ca-server/internal/util" + "path/filepath" + + "github.com/spf13/cobra" +) + +var SetupCommand = &cobra.Command{ + Use: "setup", + Short: "Setup PKI Directory", + Long: "Setup PKI Directory with default Root CA or import existing RootCA", + Run: runSetup, +} + +func runSetup(cmd *cobra.Command, args []string) { + // Load configuration + cfg := config.New() + pkiPath, _ := filepath.Abs(cfg.PKIPath) + util.CreateDirectory(pkiPath) + + // create PKI root directory + rootDir := pkiPath + "/roots" + util.CreateDirectory(rootDir) + + // create PKI extra keys directory + keyDir := pkiPath + "/keys" + util.CreateDirectory(keyDir) + + // TODO: Setup Root CA +} diff --git a/ca-server/cmd/root.go b/ca-server/cmd/root.go new file mode 100644 index 0000000..e7c2035 --- /dev/null +++ b/ca-server/cmd/root.go @@ -0,0 +1,30 @@ +package cmd + +import ( + "ca-server/cmd/cli" + "ca-server/cmd/server" + "fmt" + + "github.com/spf13/cobra" +) + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "xpki", + Short: "eXtensible PKI (xpki) is a CLI application to manage your pki", + Long: `eXtensible PKI (xpki) is a command-line application CLI application to manage your pki`, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +func Execute() error { + if err := rootCmd.Execute(); err != nil { + return fmt.Errorf("failed to run comnad: %v", err) + } + return nil +} + +func init() { + // add subcommands here + rootCmd.AddCommand(server.ServerCommand) + rootCmd.AddCommand(cli.SetupCommand) +} diff --git a/ca-server/cmd/server/server.go b/ca-server/cmd/server/server.go new file mode 100644 index 0000000..425cf91 --- /dev/null +++ b/ca-server/cmd/server/server.go @@ -0,0 +1,200 @@ +package server + +import ( + "ca-server/config" + "ca-server/middleware" + "ca-server/models" + "ca-server/routes" + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "github.com/gin-gonic/gin" + "github.com/spf13/cobra" +) + +var ServerCommand = &cobra.Command{ + Use: "server", + Short: "start PKI mTLS server", + Run: runServer, +} + +// TODO: start server in https | mtls mode + +func runServer(cmd *cobra.Command, args []string) { + // Load configuration + cfg := config.New() + + // Set gin mode + gin.SetMode(cfg.Mode) + + // Create gin router with default middleware + r := gin.New() + + // Add custom middleware + r.Use(gin.Recovery()) + r.Use(middleware.Logger()) + + // Initialize store + store := models.NewMemoryStore() + + // Setup routes + routes.SetupRoutes(r, store) + + // WaitGroup to track active servers + var wg sync.WaitGroup + + // Start HTTP server if enabled + if !cfg.TLSEnabled && !cfg.MTLSEnabled { + serverAddr := fmt.Sprintf(":%d", cfg.ServerPort) + wg.Add(1) + go startHTTPServer(r, serverAddr, &wg) + } + + // Start TLS server + if cfg.TLSEnabled { + tlsAddr := fmt.Sprintf(":%d", cfg.TLSServerPort) + wg.Add(1) + go startTLSServer(r, tlsAddr, cfg.TLSCertPath, cfg.TLSKeyPath, &wg) + } + + // Start mTLS server + if cfg.MTLSEnabled { + mtlsAddr := fmt.Sprintf(":%d", cfg.MTLSServerPort) + wg.Add(1) + go startMTLSServer(r, mtlsAddr, cfg.TLSCertPath, cfg.TLSKeyPath, cfg.ClientCACertPath, &wg) + } + + // Set up signal handling for graceful shutdown + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + log.Println("Shutting down servers...") + + // Wait for servers to complete + wg.Wait() + log.Println("All servers shutdown complete") +} + +func startHTTPServer(handler http.Handler, addr string, wg *sync.WaitGroup) { + defer wg.Done() + + server := &http.Server{ + Addr: addr, + Handler: handler, + } + + // Startup server in a goroutine + go func() { + log.Printf("Starting HTTP server on %s", addr) + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("HTTP server error: %v", err) + } + }() + + // Wait for interrupt signal + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + log.Println("Shutting down HTTP server...") + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := server.Shutdown(ctx); err != nil { + log.Fatalf("HTTP server shutdown error: %v", err) + } +} + +// startTLSServer starts a TLS server without client certificate validation +func startTLSServer(handler http.Handler, addr, certPath, keyPath string, wg *sync.WaitGroup) { + defer wg.Done() + + server := &http.Server{ + Addr: addr, + Handler: handler, + } + + // Startup server in a goroutine + go func() { + log.Printf("Starting TLS server on %s", addr) + if err := server.ListenAndServeTLS(certPath, keyPath); err != nil && err != http.ErrServerClosed { + log.Fatalf("TLS server error: %v", err) + } + }() + + // Wait for interrupt signal + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + log.Println("Shutting down TLS server...") + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := server.Shutdown(ctx); err != nil { + log.Fatalf("TLS server shutdown error: %v", err) + } +} + +// startMTLSServer starts a server with mutual TLS (client certificate validation) +func startMTLSServer(handler http.Handler, addr, certPath, keyPath, clientCACertPath string, wg *sync.WaitGroup) { + defer wg.Done() + + // Load CA cert for client certificate validation + caCert, err := ioutil.ReadFile(clientCACertPath) + if err != nil { + log.Fatalf("Failed to load CA certificate: %v", err) + } + + caCertPool := x509.NewCertPool() + if !caCertPool.AppendCertsFromPEM(caCert) { + log.Fatalf("Failed to append CA certificate to pool") + } + + // Configure TLS with client certificate verification + tlsConfig := &tls.Config{ + ClientCAs: caCertPool, + ClientAuth: tls.RequireAndVerifyClientCert, + } + + server := &http.Server{ + Addr: addr, + Handler: handler, + TLSConfig: tlsConfig, + } + + // Startup server in a goroutine + go func() { + log.Printf("Starting mTLS server on %s", addr) + if err := server.ListenAndServeTLS(certPath, keyPath); err != nil && err != http.ErrServerClosed { + log.Fatalf("mTLS server error: %v", err) + } + }() + + // Wait for interrupt signal + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + log.Println("Shutting down mTLS server...") + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := server.Shutdown(ctx); err != nil { + log.Fatalf("mTLS server shutdown error: %v", err) + } +} diff --git a/ca-server/config/config.go b/ca-server/config/config.go index 7b98c56..bcbf8d5 100644 --- a/ca-server/config/config.go +++ b/ca-server/config/config.go @@ -19,6 +19,7 @@ type Config struct { // mTLS configuration MTLSEnabled bool ClientCACertPath string + PKIPath string } // New creates a new Config with values from environment @@ -36,6 +37,7 @@ func New() *Config { // mTLS configuration MTLSEnabled: getEnvAsBool("MTLS_ENABLED", false), ClientCACertPath: getEnv("CLIENT_CA_CERT_PATH", "cert.pem"), + PKIPath: getEnv("X_PKI_PATH", ".xpki"), } } diff --git a/ca-server/controllers/cert_controller.go b/ca-server/controllers/cert_controller.go index e5b2181..92f83a0 100644 --- a/ca-server/controllers/cert_controller.go +++ b/ca-server/controllers/cert_controller.go @@ -29,7 +29,8 @@ func (c *CertController) ListCerts(ctx *gin.Context) { // Logic to list certificates } -func (c *CertController) SignCSR(ctx *gin.Context) { +// CreateCSRFromCerts creates a certificate signing request (CSR) from existing certificates +func (c *CertController) CreateCSRFromCerts(ctx *gin.Context) { } func (c *CertController) CreateClientCert(ctx *gin.Context) { diff --git a/ca-server/docs/directory_structure.md b/ca-server/docs/directory_structure.md new file mode 100644 index 0000000..47ac1fb --- /dev/null +++ b/ca-server/docs/directory_structure.md @@ -0,0 +1,52 @@ +# Directory Structure + +We use a standardized directory Structure like OpenSSL. + +```cfg +# +# OpenSSL configuration for the Root Certification Authority. +# + +# +# This definition doesn't work if HOME isn't defined. +CA_HOME = . + +# +# Default Certification Authority +[ ca ] +default_ca = root_ca + +# +# Root Certification Authority +[ root_ca ] +dir = $ENV::CA_HOME +certs = $dir/certs +serial = $dir/ca.serial +database = $dir/ca.index +new_certs_dir = $dir/newcerts +certificate = $dir/ca.cert +private_key = $dir/private/ca.key.pem +default_days = 1826 # 5 years +crl = $dir/ca.crl +crl_dir = $dir/crl +crlnumber = $dir/ca.crlnum +``` + +We can represent a similar structure in Go + +```go +type CertificateAuthorityPaths struct { + RootCAPath string + RootCACertRequestsPath string + RootCACertsPath string + RootCACertRevListPath string + RootCANewCertsPath string + RootCACertKeysPath string + RootCAIntermediateCAPath string + RootCACertIndexFilePath string + RootCACertSerialFilePath string + RootCACrlnumFilePath string +} +``` + +This structure will be used to store the paths of the CA files. The paths will be set in the `PreparePKIDirectory` function and will be used throughout the application. diff --git a/ca-server/go.mod b/ca-server/go.mod index eb92058..5f22152 100644 --- a/ca-server/go.mod +++ b/ca-server/go.mod @@ -15,6 +15,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -22,6 +23,8 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/spf13/cobra v1.9.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect golang.org/x/arch v0.8.0 // indirect diff --git a/ca-server/go.sum b/ca-server/go.sum index 7f08abb..94d318b 100644 --- a/ca-server/go.sum +++ b/ca-server/go.sum @@ -6,6 +6,7 @@ github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/ github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -28,6 +29,8 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= @@ -47,6 +50,11 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6 github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/ca-server/internal/certificate_authority/ca.go b/ca-server/internal/certificate_authority/ca.go new file mode 100644 index 0000000..25791fc --- /dev/null +++ b/ca-server/internal/certificate_authority/ca.go @@ -0,0 +1,32 @@ +package ca + +import ( + "crypto/x509" + "fmt" +) + +func CreatRootCA() { +} + +func CreateNewCA(certConfig CertificateConfiguration) ([]string, *x509.Certificate, error) { + checkInputError := false + checkErorrs := make([]string, 0) + var rootSlug string + var caName string + var rsaPrivateKeyPassword string + + if certConfig.Subject.CommonName == "" { + checkInputError = true + checkErorrs = append(checkErorrs, "missing common name field") + } else { + caName = certConfig.Subject.CommonName + rootSlug = slugger(caName) // we do not really need to sligify it, just need for directory path building + } + + privKey := key.GetPrivateKey() + + if checkInputError { + return checkErorrs, nil, fmt.Errorf("cert config error") + } + return []string{}, nil, nil +} diff --git a/ca-server/internal/certificate_authority/types.go b/ca-server/internal/certificate_authority/types.go new file mode 100644 index 0000000..b6b18c8 --- /dev/null +++ b/ca-server/internal/certificate_authority/types.go @@ -0,0 +1,49 @@ +package ca + +import "net" + +/* +CertificateConfiguration is a struct to pass Certificate Config Information into the setup functions + +`Subject` is a CertificateConfigurationSubject object + +`ExpirationDate` is expressed as a slice of 3 ints [ years, months, days ] in the future + +`RSAPrivateKey` is optional - this is used to sign a certificate request with an external key instead of one generated in the PKI + +`RSAPrivateKeyPassphrase` is optional - this is used to secure the key if generated via PKI + +`SANData` is a SANData object + +`CertificateType` is a string representing what type of certificate is being requested or generated and is used in validation checks. Options: server|client|authority|authority-no-subs +*/ +type CertificateConfiguration struct { + Subject CertificateConfigurationSubject `json:"subject"` + ExpirationDate []int `json:"expiration_date,omitempty"` + RSAPrivateKey string `json:"rsa_private_key,omitempty"` + RSAPrivateKeyPassphrase string `json:"rsa_private_key_passphrase,omitempty"` + SerialNumber string `json:"serial_number,omitempty"` + SANData SANData `json:"san_data,omitempty"` + CertificateType string `json:"certificate_type,omitempty"` +} + +// SANData provides a collection of SANData for a certificate +type SANData struct { + IPAddresses []net.IP `json:"ip_addresses,omitempty"` + EmailAddresses []string `json:"email_addresses,omitempty"` + DNSNames []string `json:"dns_names,omitempty"` + URIs []string `json:"uris,omitempty"` + // URIs []*url.URL `json:"uris,omitempty"` +} + +// CertificateConfigurationSubject is simply a redefinition of pkix.Name +type CertificateConfigurationSubject struct { + CommonName string `json:"common_name"` + Organization []string `json:"organization,omitempty"` + OrganizationalUnit []string `json:"organizational_unit,omitempty"` + Country []string `json:"country,omitempty"` + Province []string `json:"province,omitempty"` + Locality []string `json:"locality,omitempty"` + StreetAddress []string `json:"street_address,omitempty"` + PostalCode []string `json:"postal_code,omitempty"` +} diff --git a/ca-server/internal/key/keys.go b/ca-server/internal/key/keys.go new file mode 100644 index 0000000..8238617 --- /dev/null +++ b/ca-server/internal/key/keys.go @@ -0,0 +1,20 @@ +package key + +import ( + "crypto/rand" + "crypto/rsa" +) + +const defaultKeySize = 4096 // 4096 bits + +// GenerateKeypair creates a new RSA key pair with the specified key size. +func GenerateKeypair(keySize int) (*rsa.PrivateKey, *rsa.PublicKey, error) { + if keySize == 0 { + keySize = defaultKeySize + } + privKey, err := rsa.GenerateKey(rand.Reader, keySize) + if err != nil { + return nil, nil, err + } + return privKey, &privKey.PublicKey, nil +} diff --git a/ca-server/internal/structure/pki_structure.go b/ca-server/internal/structure/pki_structure.go new file mode 100644 index 0000000..b406462 --- /dev/null +++ b/ca-server/internal/structure/pki_structure.go @@ -0,0 +1,98 @@ +package structure + +import ( + "ca-server/internal/util" + "fmt" +) + +type CertificateAuthorityPaths struct { + RootCAPath string + RootCACertRequestsPath string + RootCACertsPath string + RootCACertRevListPath string + RootCANewCertsPath string + RootCACertKeysPath string + RootCAIntermediateCAPath string + RootCACertIndexFilePath string + RootCACertSerialFilePath string + RootCACrlnumFilePath string +} + +func (c *CertificateAuthorityPaths) SetupCAFileStructure(basePath string) *CertificateAuthorityPaths { + // NOTE: the actual path to save the directory can be an SFTP server or blob storage + // create root CA directory + rootCAPath := basePath + util.CreateDirectory(rootCAPath) + + // Create certificate requests (CSR) path + rootCACertRequestsPath := rootCAPath + "/csrs" + util.CreateDirectory(rootCACertRequestsPath) + + // Create certs path + rootCACertsPath := rootCAPath + "/certs" + util.CreateDirectory(rootCACertsPath) + + // create crls path + rootCACertRevListPath := rootCAPath + "/crl" + util.CreateDirectory(rootCACertRevListPath) + + // Answer from https://unix.stackexchange.com/questions/398280/openssl-basic-configuration-new-certs-dir-certs + // new_certs_dir is used by the CA to output newly generated certs. + rootCANewCertsPath := rootCAPath + "/newcerts" + util.CreateDirectory(rootCANewCertsPath) + + // Create private path for CA keys + rootCAKeysPath := rootCAPath + "/private" + util.CreateDirectory(rootCAKeysPath) + + // Create private path for generated keys in the CA + rootCACertKeysPath := rootCAPath + "/keys" + util.CreateDirectory(rootCACertKeysPath) + + // Create intermediate CA path + rootCAIntermediateCAPath := rootCAPath + "/intermed-ca" + util.CreateDirectory(rootCAIntermediateCAPath) + + // create index database file + rootCACertIndexFilePath := rootCAPath + "/ca.index" + indexExists, err := util.WriteFile(rootCACertIndexFilePath, "", 0600, false) + if err != nil { + fmt.Println("Error creating index file:", err) + } + if !indexExists { + fmt.Println("Index file already exists") + } + + // Create serial file + rootCACertSerialFilePath := rootCAPath + "/ca.serial" + serialExists, err := util.WriteFile(rootCACertSerialFilePath, "01", 0600, false) + if err != nil { + fmt.Println("Error creating serial file:", err) + } + if !serialExists { + fmt.Println("Serial file already exists") + } + + // Create certificate revocation number file + rootCACrlnumFilePath := rootCAPath + "/ca.crlnum" + crlNumExists, err := util.WriteFile(rootCACrlnumFilePath, "01", 0600, false) + if err != nil { + fmt.Println("Error creating crlnum file:", err) + } + if !crlNumExists { + fmt.Println("Crlnum file already exists") + } + + return &CertificateAuthorityPaths{ + rootCAPath, + rootCACertRequestsPath, + rootCANewCertsPath, + rootCACertRevListPath, + rootCANewCertsPath, + rootCACertKeysPath, + rootCAIntermediateCAPath, + rootCACertIndexFilePath, + rootCACertSerialFilePath, + rootCACrlnumFilePath, + } +} diff --git a/ca-server/internal/util/files.go b/ca-server/internal/util/files.go new file mode 100644 index 0000000..e8c38a6 --- /dev/null +++ b/ca-server/internal/util/files.go @@ -0,0 +1,67 @@ +package util + +import ( + "os" +) + +// FileExists checks if a file or directory exists at the given path then returns boolean and error +func FileExists(fileName string) (bool, error) { + _, err := os.Stat(fileName) + if err != nil { + return true, nil + } + if os.IsNotExist(err) { + return false, nil + } + // Schrodinger: file may or may not exist. See err for details. + // do *NOT* use !os.IsNotExist(err) to test for file existence then + return false, err +} + +func CreateDirectory(path string) error { + // Check if the directory already exists + _, err := os.Stat(path) + if err != nil { + if os.IsNotExist(err) { + err = os.MkdirAll(path, 0755) + if err != nil { + return err + } + return nil + } + } + + return err +} + +func WriteFile(path, content string, mode int, overwrite bool) (bool, error) { + var fileMode os.FileMode + if mode == 0 { + fileMode = os.FileMode(0600) + } else { + fileMode = os.FileMode(mode) + } + fileExists, err := FileExists(path) + if err != nil { + return false, err + } + if !fileExists { + // if not exists, create file with context + err = os.WriteFile(path, []byte(content), fileMode) + if err != nil { + return false, err + } + return true, nil + } + if fileExists && overwrite { + // if exists and overwrite is true, create file with context + err = os.WriteFile(path, []byte(content), fileMode) + if err != nil { + return false, err + } + return true, nil + } + + // file exists and not overwrite with no error + return false, nil +} diff --git a/ca-server/main.go b/ca-server/main.go index 1c688e7..6ad85c0 100644 --- a/ca-server/main.go +++ b/ca-server/main.go @@ -1,193 +1,15 @@ package main import ( - "context" - "crypto/tls" - "crypto/x509" + "ca-server/cmd" "fmt" - "io/ioutil" - "log" - "net/http" "os" - "os/signal" - "sync" - "syscall" - "time" - - "ca-server/config" - "ca-server/middleware" - "ca-server/models" - "ca-server/routes" - - "github.com/gin-gonic/gin" ) func main() { - // Load configuration - cfg := config.New() - - // Set gin mode - gin.SetMode(cfg.Mode) - - // Create gin router with default middleware - r := gin.New() - - // Add custom middleware - r.Use(gin.Recovery()) - r.Use(middleware.Logger()) - - // Initialize store - store := models.NewMemoryStore() - - // Setup routes - routes.SetupRoutes(r, store) - - // WaitGroup to track active servers - var wg sync.WaitGroup - - // Start HTTP server if enabled - if !cfg.TLSEnabled && !cfg.MTLSEnabled { - serverAddr := fmt.Sprintf(":%d", cfg.ServerPort) - wg.Add(1) - go startHTTPServer(r, serverAddr, &wg) - } - - // Start TLS server - if cfg.TLSEnabled { - tlsAddr := fmt.Sprintf(":%d", cfg.TLSServerPort) - wg.Add(1) - go startTLSServer(r, tlsAddr, cfg.TLSCertPath, cfg.TLSKeyPath, &wg) - } - - // Start mTLS server - if cfg.MTLSEnabled { - mtlsAddr := fmt.Sprintf(":%d", cfg.MTLSServerPort) - wg.Add(1) - go startMTLSServer(r, mtlsAddr, cfg.TLSCertPath, cfg.TLSKeyPath, cfg.ClientCACertPath, &wg) - } - - // Set up signal handling for graceful shutdown - quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - <-quit - - log.Println("Shutting down servers...") - - // Wait for servers to complete - wg.Wait() - log.Println("All servers shutdown complete") -} - -// startHTTPServer starts a regular HTTP server -func startHTTPServer(handler http.Handler, addr string, wg *sync.WaitGroup) { - defer wg.Done() - - server := &http.Server{ - Addr: addr, - Handler: handler, - } - - // Startup server in a goroutine - go func() { - log.Printf("Starting HTTP server on %s", addr) - if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Fatalf("HTTP server error: %v", err) - } - }() - - // Wait for interrupt signal - quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - <-quit - - log.Println("Shutting down HTTP server...") - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - if err := server.Shutdown(ctx); err != nil { - log.Fatalf("HTTP server shutdown error: %v", err) - } -} - -// startTLSServer starts a TLS server without client certificate validation -func startTLSServer(handler http.Handler, addr, certPath, keyPath string, wg *sync.WaitGroup) { - defer wg.Done() - - server := &http.Server{ - Addr: addr, - Handler: handler, - } - - // Startup server in a goroutine - go func() { - log.Printf("Starting TLS server on %s", addr) - if err := server.ListenAndServeTLS(certPath, keyPath); err != nil && err != http.ErrServerClosed { - log.Fatalf("TLS server error: %v", err) - } - }() - - // Wait for interrupt signal - quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - <-quit - - log.Println("Shutting down TLS server...") - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - if err := server.Shutdown(ctx); err != nil { - log.Fatalf("TLS server shutdown error: %v", err) - } -} - -// startMTLSServer starts a server with mutual TLS (client certificate validation) -func startMTLSServer(handler http.Handler, addr, certPath, keyPath, clientCACertPath string, wg *sync.WaitGroup) { - defer wg.Done() - - // Load CA cert for client certificate validation - caCert, err := ioutil.ReadFile(clientCACertPath) - if err != nil { - log.Fatalf("Failed to load CA certificate: %v", err) - } - - caCertPool := x509.NewCertPool() - if !caCertPool.AppendCertsFromPEM(caCert) { - log.Fatalf("Failed to append CA certificate to pool") - } - - // Configure TLS with client certificate verification - tlsConfig := &tls.Config{ - ClientCAs: caCertPool, - ClientAuth: tls.RequireAndVerifyClientCert, - } - - server := &http.Server{ - Addr: addr, - Handler: handler, - TLSConfig: tlsConfig, - } - - // Startup server in a goroutine - go func() { - log.Printf("Starting mTLS server on %s", addr) - if err := server.ListenAndServeTLS(certPath, keyPath); err != nil && err != http.ErrServerClosed { - log.Fatalf("mTLS server error: %v", err) - } - }() - - // Wait for interrupt signal - quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - <-quit - - log.Println("Shutting down mTLS server...") - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - if err := server.Shutdown(ctx); err != nil { - log.Fatalf("mTLS server shutdown error: %v", err) + if err := cmd.Execute(); err != nil { + fmt.Println("Error executing command:", err) + os.Exit(1) } + os.Exit(0) } diff --git a/ca-server/models/certificate_authority_path.go b/ca-server/models/certificate_authority_path.go new file mode 100644 index 0000000..5977489 --- /dev/null +++ b/ca-server/models/certificate_authority_path.go @@ -0,0 +1,16 @@ +package models + +// CertificateAuthorityPaths returns all the default paths generated by a new CA +type CertificateAuthorityPaths struct { + RootCAPath string + RootCACertRequestsPath string + RootCACertsPath string + RootCACertRevListPath string + RootCANewCertsPath string + RootCACertKeysPath string + RootCAKeysPath string + RootCAIntermediateCAPath string + RootCACertIndexFilePath string + RootCACertSerialFilePath string + RootCACrlnumFilePath string +} diff --git a/ca-server/scripts/README.md b/ca-server/scripts/README.md new file mode 100644 index 0000000..75b6141 --- /dev/null +++ b/ca-server/scripts/README.md @@ -0,0 +1,84 @@ +# Scripts + +This directory contains utility scripts for the xpki project. + +## install-task.sh + +A bash script to install the latest Task CLI version using Go. + +### Features + +- **Automatic Version Detection**: Fetches the latest version from GitHub API +- **Go Integration**: Uses `go install` for installation +- **Error Handling**: Comprehensive error checking and validation +- **PATH Verification**: Checks if Go binary directory is in PATH +- **Interactive**: Prompts before reinstalling if Task is already present +- **Colored Output**: User-friendly colored terminal output +- **Fallback Support**: Works with both curl and wget + +### Usage + +```bash +# Make sure the script is executable (already done) +chmod +x scripts/install-task.sh + +# Run the installation script +./scripts/install-task.sh +``` + +### Prerequisites + +- Go must be installed and available in PATH +- Internet connection to fetch the latest version +- Either curl or wget for API requests + +### What it does + +1. **Validates Environment**: Checks if Go is installed +2. **Shows Go Info**: Displays GOPATH, GOBIN, and PATH information +3. **Checks Existing Installation**: Warns if Task is already installed +4. **Fetches Latest Version**: Gets the latest version from GitHub API +5. **Installs Task**: Uses `go install` to install the latest version +6. **Verifies Installation**: Confirms Task is properly installed and accessible +7. **Provides Guidance**: Shows PATH configuration if needed + +### Example Output + +``` +[INFO] Task CLI Installation Script +============================= +[SUCCESS] Go is available: go1.22.5 +[INFO] Go environment: +[INFO] GOPATH: /home/user/go +[INFO] GOBIN: /home/user/go/bin +[SUCCESS] Go binary directory is in PATH +[INFO] Fetching latest Task CLI version... +[SUCCESS] Latest version found: v3.35.1 +[INFO] Installing Task CLI version v3.35.1... +[SUCCESS] Task CLI v3.35.1 installed successfully! +[INFO] Verifying installation... +[SUCCESS] Task CLI is available: Task version: v3.35.1 +[INFO] Task CLI location: /home/user/go/bin/task +[SUCCESS] Task CLI is ready to use! +[INFO] +[INFO] Try running: task --version +[INFO] Or in this project: task +``` + +### Troubleshooting + +**Go not found:** +- Install Go from https://golang.org/dl/ +- Make sure Go is in your PATH + +**Task not in PATH after installation:** +- Add Go's bin directory to your PATH: + ```bash + export PATH="$PATH:$(go env GOPATH)/bin" + ``` +- Add this line to your shell profile (~/.bashrc, ~/.zshrc, etc.) + +**Network issues:** +- Check internet connection +- Verify firewall settings allow HTTPS requests to api.github.com +- The script will fall back to installing "latest" if version fetch fails \ No newline at end of file diff --git a/ca-server/scripts/install-task.sh b/ca-server/scripts/install-task.sh new file mode 100755 index 0000000..671a54d --- /dev/null +++ b/ca-server/scripts/install-task.sh @@ -0,0 +1,181 @@ +#!/bin/bash + +# install-task.sh - Install the latest Task CLI version using Go +# Usage: ./scripts/install-task.sh + +set -euo pipefail + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print colored output +print_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Function to check if command exists +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Function to get the latest version from GitHub API +get_latest_version() { + local repo="go-task/task" + local api_url="https://api.github.com/repos/${repo}/releases/latest" + + if command_exists curl; then + curl -s "$api_url" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' + elif command_exists wget; then + wget -qO- "$api_url" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' + else + print_error "Neither curl nor wget is available. Cannot fetch latest version." + return 1 + fi +} + +# Function to install Task using Go +install_task() { + local version="$1" + local package_url="github.com/go-task/task/v3/cmd/task@${version}" + + print_info "Installing Task CLI version ${version}..." + + # Install using go install + if go install "$package_url"; then + print_success "Task CLI ${version} installed successfully!" + return 0 + else + print_error "Failed to install Task CLI" + return 1 + fi +} + +# Function to verify installation +verify_installation() { + if command_exists task; then + local installed_version + installed_version=$(task --version 2>/dev/null | head -n1 || echo "unknown") + print_success "Task CLI is available: ${installed_version}" + + # Check if task is in PATH + local task_path + task_path=$(which task) + print_info "Task CLI location: ${task_path}" + + return 0 + else + print_error "Task CLI is not available in PATH" + return 1 + fi +} + +# Function to show Go binary path info +show_go_path_info() { + if command_exists go; then + local gopath + local gobin + gopath=$(go env GOPATH) + gobin=$(go env GOBIN) + + print_info "Go environment:" + print_info " GOPATH: ${gopath}" + print_info " GOBIN: ${gobin:-${gopath}/bin}" + + # Check if Go bin directory is in PATH + local go_bin_dir="${gobin:-${gopath}/bin}" + if [[ ":$PATH:" == *":${go_bin_dir}:"* ]]; then + print_success "Go binary directory is in PATH" + else + print_warning "Go binary directory is not in PATH" + print_info "Add this to your shell profile (~/.bashrc, ~/.zshrc, etc.):" + print_info " export PATH=\"\$PATH:${go_bin_dir}\"" + fi + fi +} + +# Main function +main() { + print_info "Task CLI Installation Script" + print_info "=============================" + + # Check if Go is installed + if ! command_exists go; then + print_error "Go is not installed or not in PATH" + print_error "Please install Go first: https://golang.org/dl/" + exit 1 + fi + + local go_version + go_version=$(go version | awk '{print $3}') + print_success "Go is available: ${go_version}" + + # Show Go path information + show_go_path_info + + # Check if Task is already installed + if command_exists task; then + local current_version + current_version=$(task --version 2>/dev/null | head -n1 || echo "unknown") + print_warning "Task CLI is already installed: ${current_version}" + + # Ask if user wants to reinstall + echo -n "Do you want to reinstall/update? (y/N): " + read -r response + if [[ ! "$response" =~ ^[Yy]$ ]]; then + print_info "Installation cancelled by user" + exit 0 + fi + fi + + # Get latest version + print_info "Fetching latest Task CLI version..." + local latest_version + if ! latest_version=$(get_latest_version); then + print_error "Failed to fetch latest version" + print_info "Falling back to installing latest available version..." + latest_version="latest" + else + print_success "Latest version found: ${latest_version}" + fi + + # Install Task + if install_task "$latest_version"; then + print_success "Installation completed!" + + # Verify installation + print_info "Verifying installation..." + if verify_installation; then + print_success "Task CLI is ready to use!" + print_info "" + print_info "Try running: task --version" + print_info "Or in this project: task" + else + print_error "Installation verification failed" + print_info "You may need to add Go's bin directory to your PATH" + show_go_path_info + exit 1 + fi + else + print_error "Installation failed" + exit 1 + fi +} + +# Run main function +main "$@" \ No newline at end of file diff --git a/ca-server/server/server.go b/ca-server/server/server.go new file mode 100644 index 0000000..e69de29