Compare commits
	
		
			16 Commits
		
	
	
		
			v0.3.1
			...
			a2528e0526
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| a2528e0526 | |||
| 9b0d1eceae | |||
| 45dfdf0afc | |||
| 6e69377d1a | |||
| 8114d667ec | |||
| 176901d960 | |||
| 1991963cab | |||
| b0f0467346 | |||
| 028788f357 | |||
| 090fb4b423 | |||
| eb5c5c0e43 | |||
| 8181ac8287 | |||
| 3fe908226d | |||
| 8a36588c62 | |||
| 90ce7edd28 | |||
| 11bed9c8b1 | 
							
								
								
									
										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
 | 
				
			||||||
							
								
								
									
										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.
 | 
					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
 | 
					## Notes
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										6
									
								
								build.sh
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								build.sh
									
									
									
									
									
								
							@@ -7,6 +7,9 @@ else
 | 
				
			|||||||
  VERSION="dev"
 | 
					  VERSION="dev"
 | 
				
			||||||
fi
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Hardcode the version into main.go
 | 
				
			||||||
 | 
					sed -i '' "s/^var Version = .*/var Version = \"$VERSION\"/" version.go
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if echo $VERSION | grep -q 'dirty$'; then
 | 
					if echo $VERSION | grep -q 'dirty$'; then
 | 
				
			||||||
  echo "Building in development mode, output directory is set to 'build'."
 | 
					  echo "Building in development mode, output directory is set to 'build'."
 | 
				
			||||||
  OUTPUT_DIR=build
 | 
					  OUTPUT_DIR=build
 | 
				
			||||||
@@ -19,4 +22,5 @@ else
 | 
				
			|||||||
fi
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Build the Lab CA binary with version information
 | 
					# Build the Lab CA binary with version information
 | 
				
			||||||
go build -ldflags "-X main.Version=$VERSION" -o $OUTPUT_DIR/lab-ca
 | 
					# go build -ldflags "-X main.Version=$VERSION" -o $OUTPUT_DIR/lab-ca
 | 
				
			||||||
 | 
					go build -o $OUTPUT_DIR/lab-ca
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										173
									
								
								ca.go
									
									
									
									
									
								
							
							
						
						
									
										173
									
								
								ca.go
									
									
									
									
									
								
							@@ -72,7 +72,7 @@ func (def *CertificateDefinition) FillDefaultValues(defaults *CertificateDefault
 | 
				
			|||||||
		def.Validity = defaults.Validity
 | 
							def.Validity = defaults.Validity
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if len(def.SAN) == 0 && len(defaults.SAN) > 0 {
 | 
						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 {
 | 
					func SavePEM(filename string, data []byte, secure bool) error {
 | 
				
			||||||
	if !overwrite {
 | 
						if _, err := os.Stat(filename); err == nil {
 | 
				
			||||||
		if _, err := os.Stat(filename); err == nil {
 | 
							return fmt.Errorf("file %s already exists", filename)
 | 
				
			||||||
			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)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					 | 
				
			||||||
	if secure {
 | 
						if secure {
 | 
				
			||||||
		return os.WriteFile(filename, data, 0600)
 | 
							return os.WriteFile(filename, data, 0600)
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
@@ -453,22 +448,21 @@ func InitCA() error {
 | 
				
			|||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Helper: issue a single certificate and key, save to files, return error if any
 | 
					func issueSingleCertificate(def CertificateDefinition, i int, n int) (bool, error) {
 | 
				
			||||||
func issueSingleCertificate(def CertificateDefinition) error {
 | 
					 | 
				
			||||||
	// Validate Name
 | 
						// Validate Name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	isValidName, err := regexp.MatchString(`^[A-Za-z0-9_-]+$`, def.Name)
 | 
						isValidName, err := regexp.MatchString(`^[A-Za-z0-9_-]+$`, def.Name)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return fmt.Errorf("error validating certificate name: %v", err)
 | 
							return false, fmt.Errorf("error validating certificate name: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if !isValidName {
 | 
						if !isValidName {
 | 
				
			||||||
		return 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, fail if it is.
 | 
				
			||||||
	if caState.FindByName(def.Name, false) != nil {
 | 
						if caState.FindByName(def.Name, false) != nil {
 | 
				
			||||||
		return fmt.Errorf("certificate %s already exists and is valid.", def.Name)
 | 
							return false, fmt.Errorf("certificate %s already exists and is valid.", def.Name)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Initialize Subject if not specified
 | 
						// Initialize Subject if not specified
 | 
				
			||||||
@@ -477,18 +471,44 @@ func issueSingleCertificate(def CertificateDefinition) error {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Add default dns SAN for server/server-only if none specified
 | 
						// Add default dns SAN for server/server-only if none specified
 | 
				
			||||||
	if (def.Type == "server" || def.Type == "server-only") && len(def.SAN) == 0 {
 | 
						if strings.Contains(def.Type, "server") && len(def.SAN) == 0 {
 | 
				
			||||||
		def.SAN = append(def.SAN, "dns:"+def.Subject)
 | 
							// 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)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check if the certificate is being issued in dry run mode
 | 
				
			||||||
 | 
						if dryRun {
 | 
				
			||||||
 | 
							msg := fmt.Sprintf("Would issue certificate for '%s' (dry run).", def.Subject)
 | 
				
			||||||
 | 
							if n > 1 {
 | 
				
			||||||
 | 
								fmt.Printf("[%d/%d] %s\n", i+1, n, msg)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								fmt.Printf("%s\n", msg)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return false, nil
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							msg := fmt.Sprintf("Issuing certificate for '%s'... ", def.Subject)
 | 
				
			||||||
 | 
							if n > 1 {
 | 
				
			||||||
 | 
								fmt.Printf("[%d/%d] %s", i+1, n, msg)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								fmt.Printf("%s", msg)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	priv, err := rsa.GenerateKey(rand.Reader, 4096)
 | 
						priv, err := rsa.GenerateKey(rand.Reader, 4096)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return fmt.Errorf("failed to generate private key: %v", err)
 | 
							return false, fmt.Errorf("failed to generate private key: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
 | 
						serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
 | 
				
			||||||
	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
 | 
						serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return fmt.Errorf("failed to generate serial number: %v", err)
 | 
							return false, fmt.Errorf("failed to generate serial number: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var validityDur time.Duration
 | 
						var validityDur time.Duration
 | 
				
			||||||
@@ -499,7 +519,7 @@ func issueSingleCertificate(def CertificateDefinition) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	validityDur, err = parseValidity(validity)
 | 
						validityDur, err = parseValidity(validity)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return fmt.Errorf("invalid validity value: %v", err)
 | 
							return false, fmt.Errorf("invalid validity value: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var subjectPKIX pkix.Name
 | 
						var subjectPKIX pkix.Name
 | 
				
			||||||
@@ -532,7 +552,7 @@ func issueSingleCertificate(def CertificateDefinition) error {
 | 
				
			|||||||
		} else if n, _ := fmt.Sscanf(sLower, "email:%s", &val); n == 1 {
 | 
							} else if n, _ := fmt.Sscanf(sLower, "email:%s", &val); n == 1 {
 | 
				
			||||||
			certTmpl.EmailAddresses = append(certTmpl.EmailAddresses, val)
 | 
								certTmpl.EmailAddresses = append(certTmpl.EmailAddresses, val)
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			return fmt.Errorf("invalid SAN format: %s", s)
 | 
								return false, fmt.Errorf("invalid SAN format: %s", s)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -552,13 +572,13 @@ func issueSingleCertificate(def CertificateDefinition) error {
 | 
				
			|||||||
		case "email":
 | 
							case "email":
 | 
				
			||||||
			certTmpl.ExtKeyUsage = append(certTmpl.ExtKeyUsage, x509.ExtKeyUsageEmailProtection)
 | 
								certTmpl.ExtKeyUsage = append(certTmpl.ExtKeyUsage, x509.ExtKeyUsageEmailProtection)
 | 
				
			||||||
		default:
 | 
							default:
 | 
				
			||||||
			return fmt.Errorf("unknown certificate type. Use one of: client, server, code-signing, email")
 | 
								return false, fmt.Errorf("unknown certificate type. Use one of: client, server, code-signing, email")
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	certDER, err := x509.CreateCertificate(rand.Reader, &certTmpl, caCert, &priv.PublicKey, caKey)
 | 
						certDER, err := x509.CreateCertificate(rand.Reader, &certTmpl, caCert, &priv.PublicKey, caKey)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return fmt.Errorf("failed to create certificate: %v", err)
 | 
							return false, fmt.Errorf("failed to create certificate: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
 | 
						certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
 | 
				
			||||||
	keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
 | 
						keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
 | 
				
			||||||
@@ -566,29 +586,12 @@ func issueSingleCertificate(def CertificateDefinition) error {
 | 
				
			|||||||
	certFile := filepath.Join(caConfig.Paths.Certificates, def.Name+".crt.pem")
 | 
						certFile := filepath.Join(caConfig.Paths.Certificates, def.Name+".crt.pem")
 | 
				
			||||||
	keyFile := filepath.Join(caConfig.Paths.PrivateKeys, def.Name+".key.pem")
 | 
						keyFile := filepath.Join(caConfig.Paths.PrivateKeys, def.Name+".key.pem")
 | 
				
			||||||
	if err := SavePEM(certFile, certPEM, false); err != nil {
 | 
						if err := SavePEM(certFile, certPEM, false); err != nil {
 | 
				
			||||||
		return fmt.Errorf("error saving certificate: %v", err)
 | 
							return false, fmt.Errorf("error saving certificate: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if err := SavePEM(keyFile, keyPEM, true); err != nil {
 | 
						if err := SavePEM(keyFile, keyPEM, true); err != nil {
 | 
				
			||||||
		return fmt.Errorf("error saving key: %v", err)
 | 
							return false, fmt.Errorf("error saving key: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if verbose {
 | 
						err = caState.UpdateCAStateAfterIssue(
 | 
				
			||||||
		fmt.Printf(`
 | 
					 | 
				
			||||||
Certificate:
 | 
					 | 
				
			||||||
    Name:     %s
 | 
					 | 
				
			||||||
    Subject:  %s
 | 
					 | 
				
			||||||
    Type:     %s
 | 
					 | 
				
			||||||
    Validity: %s
 | 
					 | 
				
			||||||
    SAN:      %v
 | 
					 | 
				
			||||||
`,
 | 
					 | 
				
			||||||
			def.Name,
 | 
					 | 
				
			||||||
			def.Subject,
 | 
					 | 
				
			||||||
			def.Type,
 | 
					 | 
				
			||||||
			def.Validity,
 | 
					 | 
				
			||||||
			def.SAN,
 | 
					 | 
				
			||||||
		)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	caState.UpdateCAStateAfterIssue(
 | 
					 | 
				
			||||||
		caConfig.SerialType,
 | 
							caConfig.SerialType,
 | 
				
			||||||
		def.Name,
 | 
							def.Name,
 | 
				
			||||||
		def.Subject,
 | 
							def.Subject,
 | 
				
			||||||
@@ -596,11 +599,49 @@ Certificate:
 | 
				
			|||||||
		serialNumber,
 | 
							serialNumber,
 | 
				
			||||||
		validityDur,
 | 
							validityDur,
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	return nil
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return false, fmt.Errorf("error updating CA state: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !verbose {
 | 
				
			||||||
 | 
							fmt.Printf("done.\n")
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							fmt.Printf(`done.
 | 
				
			||||||
 | 
					Certificate generated:
 | 
				
			||||||
 | 
					    Name:     %s
 | 
				
			||||||
 | 
					    Subject:  %s
 | 
				
			||||||
 | 
					    Type:     %s
 | 
				
			||||||
 | 
					    Validity: %s
 | 
				
			||||||
 | 
					    SANs:
 | 
				
			||||||
 | 
					`,
 | 
				
			||||||
 | 
								def.Name,
 | 
				
			||||||
 | 
								def.Subject,
 | 
				
			||||||
 | 
								def.Type,
 | 
				
			||||||
 | 
								def.Validity,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							for _, san := range def.SAN {
 | 
				
			||||||
 | 
								parts := strings.SplitN(san, ":", 2)
 | 
				
			||||||
 | 
								if len(parts) == 2 {
 | 
				
			||||||
 | 
									fmt.Printf("        %s (%s)\n", parts[1], parts[0])
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									fmt.Printf("        %s\n", san)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := SaveCAState(); err != nil {
 | 
				
			||||||
 | 
							// If saving CA state fails, we still return success for the certificate issuance
 | 
				
			||||||
 | 
							fmt.Printf("WARNING: %v\n", err)
 | 
				
			||||||
 | 
							fmt.Println("CA state not saved, but certificate issued and saved successfully.")
 | 
				
			||||||
 | 
							return true, fmt.Errorf("Error saving CA state: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return true, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// A prototype of certificate provisioning function
 | 
					// A prototype of certificate provisioning function
 | 
				
			||||||
func ProvisionCertificates(filePath string, overwrite bool, dryRun bool, verbose bool) error {
 | 
					func ProvisionCertificates(filePath string) error {
 | 
				
			||||||
	err := LoadCA()
 | 
						err := LoadCA()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@@ -647,27 +688,18 @@ func ProvisionCertificates(filePath string, overwrite bool, dryRun bool, verbose
 | 
				
			|||||||
	n := len(certDefs.Certificates)
 | 
						n := len(certDefs.Certificates)
 | 
				
			||||||
	// No errors so far, now we can issue certificates
 | 
						// No errors so far, now we can issue certificates
 | 
				
			||||||
	for i := range certDefs.Certificates {
 | 
						for i := range certDefs.Certificates {
 | 
				
			||||||
		fmt.Printf("[%d/%d] Issuing %s... ", i+1, n, certDefs.Certificates[i].Name)
 | 
							issued, err := issueSingleCertificate(certDefs.Certificates[i], i, n)
 | 
				
			||||||
 | 
					 | 
				
			||||||
		if dryRun {
 | 
					 | 
				
			||||||
			fmt.Printf("(dry run)\n")
 | 
					 | 
				
			||||||
			successes++
 | 
					 | 
				
			||||||
			continue
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		err = issueSingleCertificate(certDefs.Certificates[i])
 | 
					 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			fmt.Printf("error: %v\n", err)
 | 
								fmt.Printf("error: %v\n", err)
 | 
				
			||||||
			errors++
 | 
								errors++
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			if !verbose {
 | 
								if issued {
 | 
				
			||||||
				fmt.Printf("done\n")
 | 
									successes++
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			successes++
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	fmt.Printf("Provisioning complete: %d succeeded, %d failed.\n", successes, errors)
 | 
						fmt.Printf("Provisioning complete: %d succeeded, %d failed, %d skipped.\n", successes, errors, n-successes-errors)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	err = SaveCAState()
 | 
						err = SaveCAState()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@@ -677,38 +709,23 @@ func ProvisionCertificates(filePath string, overwrite bool, dryRun bool, verbose
 | 
				
			|||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func IssueCertificate(certDef CertificateDefinition, overwrite bool, dryRun bool, verbose bool) error {
 | 
					func IssueCertificate(def CertificateDefinition) error {
 | 
				
			||||||
	err := LoadCA()
 | 
						err := LoadCA()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
 | 
							fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
 | 
				
			||||||
		os.Exit(1)
 | 
							os.Exit(1)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if certDef.Subject == "" {
 | 
						if def.Subject == "" {
 | 
				
			||||||
		certDef.Subject = certDef.Name
 | 
							def.Subject = def.Name
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Render templates in the certificae subject and SAN fields
 | 
						// Render templates in the certificae subject and SAN fields
 | 
				
			||||||
	variables := map[string]string{"Name": certDef.Name}
 | 
						variables := map[string]string{"Name": def.Name}
 | 
				
			||||||
	certDef.RenderTemplates(variables)
 | 
						def.RenderTemplates(variables)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if dryRun {
 | 
						_, err = issueSingleCertificate(def, 1, 1)
 | 
				
			||||||
		fmt.Printf("Would issue %s certificate for '%s' (dry run)\n", certDef.Type, certDef.Subject)
 | 
						return err
 | 
				
			||||||
		return nil
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	err = issueSingleCertificate(certDef)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	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)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Helper: check if string looks like a DN (contains at least CN=...)
 | 
					// Helper: check if string looks like a DN (contains at least CN=...)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										29
									
								
								certdb.go
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								certdb.go
									
									
									
									
									
								
							@@ -36,41 +36,42 @@ type CertificateRecord struct {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// Look for a certifcate by its name
 | 
					// Look for a certifcate by its name
 | 
				
			||||||
func (c *CAState) FindByName(name string, all bool) *CertificateRecord {
 | 
					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 {
 | 
							if cert.RevokedAt != "" && !all {
 | 
				
			||||||
			continue
 | 
								continue
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if cert.Name == name {
 | 
							if cert.Name == name {
 | 
				
			||||||
			return &cert
 | 
								return cert
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Look for a certificate by its serial
 | 
					// Look for a certificate by its serial
 | 
				
			||||||
func (c *CAState) FindBySerial(serial string, all bool) *CertificateRecord {
 | 
					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 {
 | 
							if cert.RevokedAt != "" && !all {
 | 
				
			||||||
			continue
 | 
								continue
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if cert.Serial == serial {
 | 
							if cert.Serial == serial {
 | 
				
			||||||
			return &cert
 | 
								return cert
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// func caStatePath() string {
 | 
					 | 
				
			||||||
// 	return filepath.Join(filepath.Dir(caConfigPath), caConfig.GetStateFileName())
 | 
					 | 
				
			||||||
// }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// LoadCAState loads the CA state from a JSON file
 | 
					// LoadCAState loads the CA state from a JSON file
 | 
				
			||||||
func LoadCAState() error {
 | 
					func LoadCAState() error {
 | 
				
			||||||
	fmt.Printf("Loading CA state from %s\n", caStatePath)
 | 
						fmt.Printf("Loading CA state from %s\n", caStatePath)
 | 
				
			||||||
	f, err := os.Open(caStatePath)
 | 
						f, err := os.Open(caStatePath)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if os.IsNotExist(err) {
 | 
				
			||||||
 | 
								// File does not exist, treat as empty state
 | 
				
			||||||
 | 
								caState = &CAState{}
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	defer f.Close()
 | 
						defer f.Close()
 | 
				
			||||||
@@ -97,8 +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 {
 | 
				
			||||||
		fmt.Fprintf(os.Stderr, "FATAL: CAState is nil in UpdateCAStateAfterIssue. This indicates a programming error.\n")
 | 
							return fmt.Errorf("CAState is nil in UpdateCAStateAfterIssue. This indicates a programming error.")
 | 
				
			||||||
		os.Exit(1)
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	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)
 | 
				
			||||||
@@ -119,7 +119,7 @@ func (s *CAState) UpdateCAStateAfterIssue(serialType, name string, subject strin
 | 
				
			|||||||
func (s *CAState) AddCertificate(name, subject, certType, issued, expires, serial string) {
 | 
					func (s *CAState) AddCertificate(name, subject, certType, issued, expires, serial string) {
 | 
				
			||||||
	if s == nil {
 | 
						if s == nil {
 | 
				
			||||||
		fmt.Fprintf(os.Stderr, "FATAL: CAState is nil in AddCertificate. This indicates a programming error.\n")
 | 
							fmt.Fprintf(os.Stderr, "FATAL: CAState is nil in AddCertificate. This indicates a programming error.\n")
 | 
				
			||||||
		os.Exit(1)
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	rec := CertificateRecord{
 | 
						rec := CertificateRecord{
 | 
				
			||||||
		Name:    name,
 | 
							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
 | 
					// 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 {
 | 
				
			||||||
		fmt.Fprintf(os.Stderr, "FATAL: CAState is nil in RevokeCertificate. This indicates a programming error.\n")
 | 
							return fmt.Errorf("CAState is nil in RevokeCertificate. This indicates a programming error.")
 | 
				
			||||||
		os.Exit(1)
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	revoked := false
 | 
						revoked := false
 | 
				
			||||||
	revokedAt := time.Now().UTC().Format(time.RFC3339)
 | 
						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
 | 
				
			||||||
							
								
								
									
										8
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								main.go
									
									
									
									
									
								
							@@ -7,10 +7,7 @@ import (
 | 
				
			|||||||
	"github.com/spf13/cobra"
 | 
						"github.com/spf13/cobra"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var Version = ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Global flags available to all commands
 | 
					// Global flags available to all commands
 | 
				
			||||||
var overwrite bool
 | 
					 | 
				
			||||||
var dryRun bool
 | 
					var dryRun bool
 | 
				
			||||||
var verbose bool
 | 
					var verbose bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -48,7 +45,6 @@ func main() {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Define persistent flags (global for all commands)
 | 
						// 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(&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().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")
 | 
						rootCmd.PersistentFlags().StringVar(&caConfigPath, "config", "ca_config.hcl", "Path to CA configuration file")
 | 
				
			||||||
@@ -97,7 +93,7 @@ func main() {
 | 
				
			|||||||
				Type:     certType,
 | 
									Type:     certType,
 | 
				
			||||||
				Validity: validity,
 | 
									Validity: validity,
 | 
				
			||||||
				SAN:      san,
 | 
									SAN:      san,
 | 
				
			||||||
			}, overwrite, dryRun, verbose)
 | 
								})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
 | 
									fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
 | 
				
			||||||
@@ -122,7 +118,7 @@ func main() {
 | 
				
			|||||||
		Short: "Provision certificates from a batch file (HCL)",
 | 
							Short: "Provision certificates from a batch file (HCL)",
 | 
				
			||||||
		Run: func(cmd *cobra.Command, args []string) {
 | 
							Run: func(cmd *cobra.Command, args []string) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			err := ProvisionCertificates(provisionFile, overwrite, false, verbose)
 | 
								err := ProvisionCertificates(provisionFile)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
 | 
									fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										13
									
								
								run-test-2.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										13
									
								
								run-test-2.sh
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					#!/bin/bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if [ "-v" == "$1" ]; then
 | 
				
			||||||
 | 
					    VERBOSE="--verbose"
 | 
				
			||||||
 | 
					    shift
 | 
				
			||||||
 | 
					else
 | 
				
			||||||
 | 
					    VERBOSE=""
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					./build.sh
 | 
				
			||||||
 | 
					rm -rf docker_ca
 | 
				
			||||||
 | 
					build/lab-ca $VERBOSE --config docker_ca.hcl initca
 | 
				
			||||||
 | 
					build/lab-ca $VERBOSE --config docker_ca.hcl provision --file docker.hcl
 | 
				
			||||||
							
								
								
									
										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