feat: Add support for user certificates and enhance SAN handling in makeCert function
/ test-go (push) Successful in 38s

This commit is contained in:
2026-05-25 06:04:59 +02:00
parent ad6af575dc
commit c537ae5dd7
2 changed files with 98 additions and 24 deletions
+57 -20
View File
@@ -286,21 +286,25 @@ func makeCA(caDir, caName string, days int, issuingCA, aiaBaseURL string) error
// ---- makeCert --------------------------------------------------------------- // ---- makeCert ---------------------------------------------------------------
var ( var (
ipRE = regexp.MustCompile(`^[0-9]{1,3}(\.[0-9]{1,3}){3}$`) ipRE = regexp.MustCompile(`^[0-9]{1,3}(\.[0-9]{1,3}){3}$`)
dnsRE = regexp.MustCompile(`^[a-z0-9-]+(\.[a-z0-9-]+)*$`) dnsRE = regexp.MustCompile(`^[a-z0-9-]+(\.[a-z0-9-]+)*$`)
emailRE = regexp.MustCompile(`^[^@]+@[^@]+\.[^@]+$`)
) )
func makeCert(subjectName string, sans []string, caDir, certDir, issuingCA string, days int) error { func makeCert(subjectName string, sans []string, caDir, certDir, issuingCA, certType string, days int) error {
if issuingCA == "ca" { if issuingCA == "ca" {
return errors.New("--issuing-ca cannot be 'ca'") return errors.New("--issuing-ca cannot be 'ca'")
} }
if certType != "server" && certType != "user" {
return fmt.Errorf("--type must be 'server' or 'user', got '%s'", certType)
}
if !dirExists(caDir) { if !dirExists(caDir) {
return fmt.Errorf("CA directory %s does not exist", caDir) return fmt.Errorf("CA directory %s does not exist", caDir)
} }
if subjectName == "" { if subjectName == "" {
return errors.New("subject name is required") return errors.New("subject name is required")
} }
if !dnsRE.MatchString(subjectName) { if certType == "server" && !dnsRE.MatchString(subjectName) {
return fmt.Errorf("invalid subject name '%s'. Must be a valid DNS name", subjectName) return fmt.Errorf("invalid subject name '%s'. Must be a valid DNS name", subjectName)
} }
@@ -337,12 +341,20 @@ func makeCert(subjectName string, sans []string, caDir, certDir, issuingCA strin
certName = certName[:i] certName = certName[:i]
} }
dnsNames := []string{subjectName} var dnsNames []string
var emails []string
var ips []net.IP var ips []net.IP
if certType == "server" {
dnsNames = []string{subjectName}
}
for _, entry := range sans { for _, entry := range sans {
switch { switch {
case ipRE.MatchString(entry): case ipRE.MatchString(entry):
ips = append(ips, net.ParseIP(entry)) ips = append(ips, net.ParseIP(entry))
case emailRE.MatchString(entry):
emails = append(emails, entry)
case dnsRE.MatchString(entry): case dnsRE.MatchString(entry):
dnsNames = append(dnsNames, entry) dnsNames = append(dnsNames, entry)
default: default:
@@ -350,10 +362,17 @@ func makeCert(subjectName string, sans []string, caDir, certDir, issuingCA strin
} }
} }
fmt.Printf("Generating server certificate for '%s' with SANs:\n", subjectName) if certType == "server" {
fmt.Printf("Generating server certificate for '%s' with SANs:\n", subjectName)
} else {
fmt.Printf("Generating user certificate for '%s':\n", subjectName)
}
for _, dns := range dnsNames { for _, dns := range dnsNames {
fmt.Printf(" - DNS:%s\n", dns) fmt.Printf(" - DNS:%s\n", dns)
} }
for _, email := range emails {
fmt.Printf(" - email:%s\n", email)
}
for _, ip := range ips { for _, ip := range ips {
fmt.Printf(" - IP:%s\n", ip) fmt.Printf(" - IP:%s\n", ip)
} }
@@ -365,7 +384,7 @@ func makeCert(subjectName string, sans []string, caDir, certDir, issuingCA strin
return nil return nil
} }
fmt.Println("Generating server certificate and key...") fmt.Println("Generating certificate and key...")
caCert, err := loadCert(caCertPath) caCert, err := loadCert(caCertPath)
if err != nil { if err != nil {
return err return err
@@ -383,18 +402,35 @@ func makeCert(subjectName string, sans []string, caDir, certDir, issuingCA strin
return err return err
} }
now := time.Now().UTC() now := time.Now().UTC()
tmpl := &x509.Certificate{
SerialNumber: serial, var tmpl *x509.Certificate
Subject: pkix.Name{CommonName: subjectName}, if certType == "server" {
NotBefore: now, tmpl = &x509.Certificate{
NotAfter: now.AddDate(0, 0, days), SerialNumber: serial,
IsCA: false, Subject: pkix.Name{CommonName: subjectName},
BasicConstraintsValid: true, NotBefore: now,
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, NotAfter: now.AddDate(0, 0, days),
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, BasicConstraintsValid: true,
DNSNames: dnsNames, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
IPAddresses: ips, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
DNSNames: dnsNames,
IPAddresses: ips,
}
} else {
tmpl = &x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{CommonName: subjectName},
NotBefore: now,
NotAfter: now.AddDate(0, 0, days),
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageContentCommitment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageEmailProtection, x509.ExtKeyUsageCodeSigning},
EmailAddresses: emails,
DNSNames: dnsNames,
IPAddresses: ips,
}
} }
if aiaURL != "" { if aiaURL != "" {
tmpl.IssuingCertificateURL = []string{aiaURL} tmpl.IssuingCertificateURL = []string{aiaURL}
} }
@@ -575,19 +611,20 @@ func newMakeCACmd() *cobra.Command {
} }
func newMakeCertCmd() *cobra.Command { func newMakeCertCmd() *cobra.Command {
var certDir, caDir, issuingCA string var certDir, caDir, issuingCA, certType string
var days int var days int
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "make-cert SUBJECT [SAN...]", Use: "make-cert SUBJECT [SAN...]",
Short: "Create a server/client certificate signed by the CA.", Short: "Create a server/client certificate signed by the CA.",
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
RunE: func(_ *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
return makeCert(args[0], args[1:], resolveCADir(caDir), certDir, issuingCA, days) return makeCert(args[0], args[1:], resolveCADir(caDir), certDir, issuingCA, certType, days)
}, },
} }
cmd.Flags().StringVar(&certDir, "cert-dir", "", "output directory (default: signing CA directory)") cmd.Flags().StringVar(&certDir, "cert-dir", "", "output directory (default: signing CA directory)")
cmd.Flags().StringVar(&caDir, "ca-dir", "", "CA root directory") cmd.Flags().StringVar(&caDir, "ca-dir", "", "CA root directory")
cmd.Flags().StringVar(&issuingCA, "issuing-ca", "", "issuing CA directory name") cmd.Flags().StringVar(&issuingCA, "issuing-ca", "", "issuing CA directory name")
cmd.Flags().StringVar(&certType, "type", "server", "certificate type: server or user")
cmd.Flags().IntVar(&days, "days", 365, "validity period in days") cmd.Flags().IntVar(&days, "days", 365, "validity period in days")
return cmd return cmd
} }
+41 -4
View File
@@ -4,6 +4,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
) )
@@ -15,6 +16,17 @@ func verifyCert(t *testing.T, bundle, cert string) {
} }
} }
func assertEKU(t *testing.T, cert, eku string) {
t.Helper()
out, err := exec.Command("openssl", "x509", "-in", cert, "-noout", "-text").CombinedOutput()
if err != nil {
t.Fatalf("openssl x509 failed: %v", err)
}
if !strings.Contains(string(out), eku) {
t.Fatalf("EKU %q not found in %s", eku, cert)
}
}
func TestStandaloneCA(t *testing.T) { func TestStandaloneCA(t *testing.T) {
caDir := t.TempDir() caDir := t.TempDir()
@@ -29,7 +41,7 @@ func TestStandaloneCA(t *testing.T) {
} }
verifyCert(t, filepath.Join(caDir, "ca_bundle.pem"), filepath.Join(caDir, "ca_cert.pem")) verifyCert(t, filepath.Join(caDir, "ca_bundle.pem"), filepath.Join(caDir, "ca_cert.pem"))
if err := makeCert("test", []string{"test.example.com", "127.0.0.1"}, caDir, "", "", 365); err != nil { if err := makeCert("test", []string{"test.example.com", "127.0.0.1"}, caDir, "", "", "server", 365); err != nil {
t.Fatalf("makeCert: %v", err) t.Fatalf("makeCert: %v", err)
} }
certPath := filepath.Join(caDir, "test_cert.pem") certPath := filepath.Join(caDir, "test_cert.pem")
@@ -56,7 +68,7 @@ func TestTwoLevelCA(t *testing.T) {
} }
verifyCert(t, filepath.Join(caDir, "ca_bundle.pem"), issuingCert) verifyCert(t, filepath.Join(caDir, "ca_bundle.pem"), issuingCert)
if err := makeCert("test", []string{"test.example.com", "127.0.0.1"}, caDir, "", "issuing_ca", 365); err != nil { if err := makeCert("test", []string{"test.example.com", "127.0.0.1"}, caDir, "", "issuing_ca", "server", 365); err != nil {
t.Fatalf("makeCert: %v", err) t.Fatalf("makeCert: %v", err)
} }
certPath := filepath.Join(caDir, "issuing_ca", "test_cert.pem") certPath := filepath.Join(caDir, "issuing_ca", "test_cert.pem")
@@ -64,6 +76,8 @@ func TestTwoLevelCA(t *testing.T) {
t.Fatal("issuing_ca/test_cert.pem not created") t.Fatal("issuing_ca/test_cert.pem not created")
} }
verifyCert(t, filepath.Join(caDir, "ca_bundle.pem"), certPath) verifyCert(t, filepath.Join(caDir, "ca_bundle.pem"), certPath)
assertEKU(t, certPath, "TLS Web Server Authentication")
assertEKU(t, certPath, "TLS Web Client Authentication")
if err := makePFX(certPath, caDir, "issuing_ca", "s3cr3t", false); err != nil { if err := makePFX(certPath, caDir, "issuing_ca", "s3cr3t", false); err != nil {
t.Fatalf("makePFX: %v", err) t.Fatalf("makePFX: %v", err)
@@ -79,6 +93,29 @@ func TestTwoLevelCA(t *testing.T) {
} }
} }
func TestUserCert(t *testing.T) {
caDir := t.TempDir()
if err := makeCA(caDir, "Test CA", 3650, "", ""); err != nil {
t.Fatalf("makeCA: %v", err)
}
if err := makeCA(caDir, "Issuing CA", 3650, "issuing_ca", ""); err != nil {
t.Fatalf("makeCA issuing: %v", err)
}
if err := makeCert("Alice Example", []string{"alice@example.com"}, caDir, "", "issuing_ca", "user", 365); err != nil {
t.Fatalf("makeCert user: %v", err)
}
certPath := filepath.Join(caDir, "issuing_ca", "Alice Example_cert.pem")
if !fileExists(certPath) {
t.Fatal("Alice Example_cert.pem not created")
}
verifyCert(t, filepath.Join(caDir, "ca_bundle.pem"), certPath)
assertEKU(t, certPath, "TLS Web Client Authentication")
assertEKU(t, certPath, "E-mail Protection")
assertEKU(t, certPath, "Code Signing")
}
func TestCertDirOverride(t *testing.T) { func TestCertDirOverride(t *testing.T) {
caDir := t.TempDir() caDir := t.TempDir()
certDir := t.TempDir() certDir := t.TempDir()
@@ -86,7 +123,7 @@ func TestCertDirOverride(t *testing.T) {
if err := makeCA(caDir, "Test CA", 3650, "", ""); err != nil { if err := makeCA(caDir, "Test CA", 3650, "", ""); err != nil {
t.Fatalf("makeCA: %v", err) t.Fatalf("makeCA: %v", err)
} }
if err := makeCert("test", []string{"test.example.com"}, caDir, certDir, "", 365); err != nil { if err := makeCert("test", []string{"test.example.com"}, caDir, certDir, "", "server", 365); err != nil {
t.Fatalf("makeCert: %v", err) t.Fatalf("makeCert: %v", err)
} }
certPath := filepath.Join(certDir, "test_cert.pem") certPath := filepath.Join(certDir, "test_cert.pem")
@@ -109,7 +146,7 @@ func TestAppleOpenSSL(t *testing.T) {
if err := makeCA(caDir, "Issuing CA", 3650, "issuing_ca", ""); err != nil { if err := makeCA(caDir, "Issuing CA", 3650, "issuing_ca", ""); err != nil {
t.Fatalf("makeCA issuing: %v", err) t.Fatalf("makeCA issuing: %v", err)
} }
if err := makeCert("test", []string{"test.example.com"}, caDir, "", "issuing_ca", 365); err != nil { if err := makeCert("test", []string{"test.example.com"}, caDir, "", "issuing_ca", "server", 365); err != nil {
t.Fatalf("makeCert: %v", err) t.Fatalf("makeCert: %v", err)
} }
certPath := filepath.Join(caDir, "issuing_ca", "test_cert.pem") certPath := filepath.Join(caDir, "issuing_ca", "test_cert.pem")