Added Go convertion.

This commit is contained in:
2026-04-24 23:40:43 +02:00
parent 938bcfd05d
commit e71b71ac49
3 changed files with 572 additions and 0 deletions

14
src/simple-ca/go.mod Normal file
View File

@@ -0,0 +1,14 @@
module simple-ca
go 1.25
require (
github.com/spf13/cobra v1.10.2
software.sslmate.com/src/go-pkcs12 v0.5.0
)
require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.9 // indirect
golang.org/x/crypto v0.37.0 // indirect
)

14
src/simple-ca/go.sum Normal file
View File

@@ -0,0 +1,14 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
software.sslmate.com/src/go-pkcs12 v0.5.0 h1:EC6R394xgENTpZ4RltKydeDUjtlM5drOYIG9c6TVj2M=
software.sslmate.com/src/go-pkcs12 v0.5.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=

544
src/simple-ca/main.go Normal file
View File

@@ -0,0 +1,544 @@
// MIT License
//
// Copyright (c) 2026 Sławomir Koszewski
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package main
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"math/big"
"net"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/spf13/cobra"
"software.sslmate.com/src/go-pkcs12"
)
func dirExists(path string) bool {
info, err := os.Stat(path)
return err == nil && info.IsDir()
}
func fileExists(path string) bool {
info, err := os.Stat(path)
return err == nil && !info.IsDir()
}
func randomSerial() (*big.Int, error) {
limit := new(big.Int).Lsh(big.NewInt(1), 159)
return rand.Int(rand.Reader, limit)
}
func writePEMFile(path, blockType string, der []byte, mode os.FileMode) error {
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
if err != nil {
return err
}
defer f.Close()
return pem.Encode(f, &pem.Block{Type: blockType, Bytes: der})
}
func writeKey(key *rsa.PrivateKey, path string) error {
der, err := x509.MarshalPKCS8PrivateKey(key)
if err != nil {
return err
}
return writePEMFile(path, "PRIVATE KEY", der, 0o600)
}
func writeCert(der []byte, path string) error {
return writePEMFile(path, "CERTIFICATE", der, 0o644)
}
func loadCert(path string) (*x509.Certificate, error) {
b, err := os.ReadFile(path)
if err != nil {
return nil, err
}
block, _ := pem.Decode(b)
if block == nil || block.Type != "CERTIFICATE" {
return nil, fmt.Errorf("no CERTIFICATE PEM block in %s", path)
}
return x509.ParseCertificate(block.Bytes)
}
func loadKey(path string) (*rsa.PrivateKey, error) {
b, err := os.ReadFile(path)
if err != nil {
return nil, err
}
block, _ := pem.Decode(b)
if block == nil {
return nil, fmt.Errorf("no PEM block in %s", path)
}
if k, err := x509.ParsePKCS8PrivateKey(block.Bytes); err == nil {
if rsaKey, ok := k.(*rsa.PrivateKey); ok {
return rsaKey, nil
}
return nil, fmt.Errorf("not an RSA key in %s", path)
}
return x509.ParsePKCS1PrivateKey(block.Bytes)
}
func rebuildCABundle(caDir string) error {
entries, err := os.ReadDir(caDir)
if err != nil {
return err
}
var bundle []byte
rootPath := filepath.Join(caDir, "ca_cert.pem")
if fileExists(rootPath) {
data, err := os.ReadFile(rootPath)
if err != nil {
return err
}
bundle = append(bundle, data...)
}
for _, e := range entries {
name := e.Name()
if e.IsDir() || name == "ca_cert.pem" || !strings.HasSuffix(name, "_cert.pem") {
continue
}
data, err := os.ReadFile(filepath.Join(caDir, name))
if err != nil {
return err
}
bundle = append(bundle, data...)
}
return os.WriteFile(filepath.Join(caDir, "ca_bundle.pem"), bundle, 0o644)
}
func makeCA(caDir, caName string, days int, issuingCA, aiaBaseURL string) error {
if issuingCA == "ca" {
return errors.New("--issuing-ca cannot be 'ca' as it is reserved for the root CA")
}
if caDir == "" || !dirExists(caDir) {
return fmt.Errorf("certificate directory %s does not exist", caDir)
}
if caName == "" {
return errors.New("CA name is required")
}
aiaFile := filepath.Join(caDir, "aia_base_url.txt")
if aiaBaseURL == "" {
if data, err := os.ReadFile(aiaFile); err == nil {
aiaBaseURL = strings.TrimSpace(string(data))
}
}
prefix := issuingCA
if prefix == "" {
prefix = "ca"
}
rootCertPath := filepath.Join(caDir, "ca_cert.pem")
rootKeyPath := filepath.Join(caDir, "ca_key.pem")
caCertPath := filepath.Join(caDir, prefix+"_cert.pem")
caKeyPath := filepath.Join(caDir, prefix+"_key.pem")
isRoot := prefix == "ca"
if !fileExists(rootCertPath) || !fileExists(rootKeyPath) {
if !isRoot {
return fmt.Errorf("cannot create issuing CA '%s' without existing root CA certificate and key. Please create the root CA first", caName)
}
fmt.Printf("Generating CA certificate '%s' and key...\n", caName)
key, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return err
}
serial, err := randomSerial()
if err != nil {
return err
}
now := time.Now().UTC()
tmpl := &x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{CommonName: caName},
NotBefore: now,
NotAfter: now.AddDate(0, 0, days),
IsCA: true,
BasicConstraintsValid: true,
MaxPathLen: 1,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
if err != nil {
return err
}
if err := writeKey(key, rootKeyPath); err != nil {
return err
}
if err := writeCert(der, rootCertPath); err != nil {
return err
}
if err := rebuildCABundle(caDir); err != nil {
return err
}
if aiaBaseURL != "" {
if err := os.WriteFile(aiaFile, []byte(aiaBaseURL), 0o644); err != nil {
return err
}
}
return nil
}
if !fileExists(caCertPath) || !fileExists(caKeyPath) {
fmt.Printf("Generating issuing CA certificate '%s' and key...\n", caName)
rootCert, err := loadCert(rootCertPath)
if err != nil {
return err
}
rootKey, err := loadKey(rootKeyPath)
if err != nil {
return err
}
key, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return err
}
serial, err := randomSerial()
if err != nil {
return err
}
now := time.Now().UTC()
tmpl := &x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{CommonName: caName},
NotBefore: now,
NotAfter: now.AddDate(0, 0, days),
IsCA: true,
BasicConstraintsValid: true,
MaxPathLen: 0,
MaxPathLenZero: true,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
}
if aiaBaseURL != "" {
tmpl.IssuingCertificateURL = []string{aiaBaseURL + "/ca_cert.crt"}
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, rootCert, &key.PublicKey, rootKey)
if err != nil {
return err
}
if err := writeKey(key, caKeyPath); err != nil {
return err
}
if err := writeCert(der, caCertPath); err != nil {
return err
}
}
if err := rebuildCABundle(caDir); err != nil {
return err
}
if aiaBaseURL != "" {
if err := os.WriteFile(aiaFile, []byte(aiaBaseURL), 0o644); err != nil {
return err
}
}
return nil
}
var (
ipRE = regexp.MustCompile(`^[0-9]{1,3}(\.[0-9]{1,3}){3}$`)
dnsRE = regexp.MustCompile(`^[a-z0-9-]+(\.[a-z0-9-]+)*$`)
)
func makeCert(certDir, subjectName string, sans []string, caDir, issuingCA string, days int) error {
if issuingCA == "ca" {
return errors.New("--issuing-ca cannot be 'ca' as it is reserved for the root CA")
}
prefix := issuingCA
if prefix == "" {
prefix = "ca"
}
if caDir == "" {
caDir = certDir
}
if certDir == "" || !dirExists(certDir) {
return fmt.Errorf("certificate directory %s does not exist", certDir)
}
if !dirExists(caDir) {
return fmt.Errorf("CA directory %s does not exist", caDir)
}
if subjectName == "" {
return errors.New("subject name is required")
}
if !dnsRE.MatchString(subjectName) {
return fmt.Errorf("invalid subject name '%s'. Must be a valid DNS name", subjectName)
}
aiaFile := filepath.Join(caDir, "aia_base_url.txt")
aiaURL := ""
if data, err := os.ReadFile(aiaFile); err == nil {
aiaURL = strings.TrimSpace(string(data)) + "/" + prefix + "_cert.crt"
}
caCertPath := filepath.Join(caDir, prefix+"_cert.pem")
caKeyPath := filepath.Join(caDir, prefix+"_key.pem")
if !fileExists(caCertPath) || !fileExists(caKeyPath) {
return fmt.Errorf("signing CA certificate and key not found in %s. Please call setup a signing CA first", caDir)
}
certName := subjectName
if i := strings.Index(certName, "."); i >= 0 {
certName = certName[:i]
}
dnsNames := []string{subjectName}
var ips []net.IP
for _, entry := range sans {
switch {
case ipRE.MatchString(entry):
ips = append(ips, net.ParseIP(entry))
case dnsRE.MatchString(entry):
dnsNames = append(dnsNames, entry)
default:
return fmt.Errorf("invalid SAN entry '%s'", entry)
}
}
fmt.Printf("Generating server certificate for '%s' with SANs:\n", subjectName)
for _, dns := range dnsNames {
fmt.Printf(" - DNS:%s\n", dns)
}
for _, ip := range ips {
fmt.Printf(" - IP:%s\n", ip)
}
certOut := filepath.Join(certDir, certName+"_cert.pem")
keyOut := filepath.Join(certDir, certName+"_key.pem")
if fileExists(certOut) && fileExists(keyOut) {
return nil
}
fmt.Println("Generating server certificate and key...")
caCert, err := loadCert(caCertPath)
if err != nil {
return err
}
caKey, err := loadKey(caKeyPath)
if err != nil {
return err
}
key, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return err
}
serial, err := randomSerial()
if err != nil {
return err
}
now := time.Now().UTC()
tmpl := &x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{CommonName: subjectName},
NotBefore: now,
NotAfter: now.AddDate(0, 0, days),
IsCA: false,
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
DNSNames: dnsNames,
IPAddresses: ips,
}
if aiaURL != "" {
tmpl.IssuingCertificateURL = []string{aiaURL}
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, caCert, &key.PublicKey, caKey)
if err != nil {
return err
}
if err := writeKey(key, keyOut); err != nil {
return err
}
return writeCert(der, certOut)
}
func makePFX(certPath, caDir, issuingCA, password string) error {
if issuingCA == "ca" {
return errors.New("--issuing-ca cannot be 'ca' as it is reserved for the root CA")
}
prefix := issuingCA
if prefix == "" {
prefix = "ca"
}
rootCertPath := filepath.Join(caDir, "ca_cert.pem")
rootKeyPath := filepath.Join(caDir, "ca_key.pem")
caCertPath := filepath.Join(caDir, prefix+"_cert.pem")
caKeyPath := filepath.Join(caDir, prefix+"_key.pem")
certDir := filepath.Dir(certPath)
certName := strings.TrimSuffix(filepath.Base(certPath), "_cert.pem")
keyPath := filepath.Join(certDir, certName+"_key.pem")
if !dirExists(certDir) {
return fmt.Errorf("certificate directory %s does not exist", certDir)
}
if !dirExists(caDir) {
return fmt.Errorf("CA directory %s does not exist", caDir)
}
if !fileExists(certPath) || !fileExists(keyPath) {
return errors.New("server certificate or key not found")
}
if !fileExists(rootCertPath) || !fileExists(rootKeyPath) {
return fmt.Errorf("CA certificate or key not found in %s", caDir)
}
if issuingCA != "" {
if !fileExists(caCertPath) || !fileExists(caKeyPath) {
return fmt.Errorf("issuing CA certificate or key not found in %s", caDir)
}
}
if password == "" {
password = "changeit"
}
pfxPath := filepath.Join(certDir, certName+".pfx")
if fileExists(pfxPath) {
fmt.Println("PKCS#12 (PFX) file already exists, aborting generation.")
return errors.New("PFX file already exists")
}
fmt.Print("Generating PKCS#12 (PFX) file...")
cert, err := loadCert(certPath)
if err != nil {
return err
}
key, err := loadKey(keyPath)
if err != nil {
return err
}
chain := []*x509.Certificate{}
root, err := loadCert(rootCertPath)
if err != nil {
return err
}
chain = append(chain, root)
if issuingCA != "" {
issuing, err := loadCert(caCertPath)
if err != nil {
return err
}
chain = append(chain, issuing)
}
pfxBytes, err := pkcs12.Modern.Encode(key, cert, chain, password)
if err != nil {
return err
}
if err := os.WriteFile(pfxPath, pfxBytes, 0o600); err != nil {
return err
}
fmt.Println("done.")
return nil
}
func newRootCmd() *cobra.Command {
root := &cobra.Command{
Use: "simple-ca",
Short: "Create and manage a simple Certificate Authority.",
SilenceUsage: true,
SilenceErrors: false,
}
root.AddCommand(newMakeCACmd(), newMakeCertCmd(), newMakePFXCmd())
return root
}
func newMakeCACmd() *cobra.Command {
var (
days int
issuingCA string
aiaBaseURL string
)
cmd := &cobra.Command{
Use: "make-ca CA_DIR CA_NAME",
Short: "Create a root or issuing CA.",
Args: cobra.ExactArgs(2),
RunE: func(_ *cobra.Command, args []string) error {
return makeCA(args[0], args[1], days, issuingCA, aiaBaseURL)
},
}
cmd.Flags().IntVar(&days, "days", 3650, "validity period in days")
cmd.Flags().StringVar(&issuingCA, "issuing-ca", "", "issuing CA file prefix (creates an issuing CA signed by the root)")
cmd.Flags().StringVar(&aiaBaseURL, "aia-base-url", "", "base URL for the AIA caIssuers extension")
return cmd
}
func newMakeCertCmd() *cobra.Command {
var (
caDir string
issuingCA string
days int
)
cmd := &cobra.Command{
Use: "make-cert CERT_DIR SUBJECT [SAN...]",
Short: "Create a server/client certificate signed by the CA.",
Args: cobra.MinimumNArgs(2),
RunE: func(_ *cobra.Command, args []string) error {
return makeCert(args[0], args[1], args[2:], caDir, issuingCA, days)
},
}
cmd.Flags().StringVar(&caDir, "ca-dir", "", "CA directory (defaults to CERT_DIR)")
cmd.Flags().StringVar(&issuingCA, "issuing-ca", "", "issuing CA file prefix")
cmd.Flags().IntVar(&days, "days", 365, "validity period in days")
return cmd
}
func newMakePFXCmd() *cobra.Command {
var (
caDir string
issuingCA string
certPath string
password string
)
cmd := &cobra.Command{
Use: "make-pfx --ca-dir DIR --path CERT_PATH [flags]",
Short: "Create a PKCS#12 (PFX) bundle for a leaf certificate.",
Args: cobra.NoArgs,
RunE: func(_ *cobra.Command, _ []string) error {
return makePFX(certPath, caDir, issuingCA, password)
},
}
cmd.Flags().StringVar(&caDir, "ca-dir", "", "CA directory")
cmd.Flags().StringVar(&issuingCA, "issuing-ca", "", "issuing CA file prefix")
cmd.Flags().StringVar(&certPath, "path", "", "path to the leaf certificate PEM")
cmd.Flags().StringVar(&password, "password", "", "PFX password (default: changeit)")
_ = cmd.MarkFlagRequired("ca-dir")
_ = cmd.MarkFlagRequired("path")
return cmd
}
func main() {
if err := newRootCmd().Execute(); err != nil {
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
os.Exit(1)
}
}