feat: Add support for user certificates and enhance SAN handling in makeCert function
/ test-go (push) Successful in 38s
/ test-go (push) Successful in 38s
This commit is contained in:
+45
-8
@@ -288,19 +288,23 @@ func makeCA(caDir, caName string, days int, issuingCA, aiaBaseURL string) error
|
||||
var (
|
||||
ipRE = regexp.MustCompile(`^[0-9]{1,3}(\.[0-9]{1,3}){3}$`)
|
||||
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" {
|
||||
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) {
|
||||
return fmt.Errorf("CA directory %s does not exist", caDir)
|
||||
}
|
||||
if subjectName == "" {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -337,12 +341,20 @@ func makeCert(subjectName string, sans []string, caDir, certDir, issuingCA strin
|
||||
certName = certName[:i]
|
||||
}
|
||||
|
||||
dnsNames := []string{subjectName}
|
||||
var dnsNames []string
|
||||
var emails []string
|
||||
var ips []net.IP
|
||||
|
||||
if certType == "server" {
|
||||
dnsNames = []string{subjectName}
|
||||
}
|
||||
|
||||
for _, entry := range sans {
|
||||
switch {
|
||||
case ipRE.MatchString(entry):
|
||||
ips = append(ips, net.ParseIP(entry))
|
||||
case emailRE.MatchString(entry):
|
||||
emails = append(emails, entry)
|
||||
case dnsRE.MatchString(entry):
|
||||
dnsNames = append(dnsNames, entry)
|
||||
default:
|
||||
@@ -350,10 +362,17 @@ func makeCert(subjectName string, sans []string, caDir, certDir, issuingCA strin
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
fmt.Printf(" - DNS:%s\n", dns)
|
||||
}
|
||||
for _, email := range emails {
|
||||
fmt.Printf(" - email:%s\n", email)
|
||||
}
|
||||
for _, ip := range ips {
|
||||
fmt.Printf(" - IP:%s\n", ip)
|
||||
}
|
||||
@@ -365,7 +384,7 @@ func makeCert(subjectName string, sans []string, caDir, certDir, issuingCA strin
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Println("Generating server certificate and key...")
|
||||
fmt.Println("Generating certificate and key...")
|
||||
caCert, err := loadCert(caCertPath)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -383,18 +402,35 @@ func makeCert(subjectName string, sans []string, caDir, certDir, issuingCA strin
|
||||
return err
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
tmpl := &x509.Certificate{
|
||||
|
||||
var tmpl *x509.Certificate
|
||||
if certType == "server" {
|
||||
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,
|
||||
}
|
||||
} 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 != "" {
|
||||
tmpl.IssuingCertificateURL = []string{aiaURL}
|
||||
}
|
||||
@@ -575,19 +611,20 @@ func newMakeCACmd() *cobra.Command {
|
||||
}
|
||||
|
||||
func newMakeCertCmd() *cobra.Command {
|
||||
var certDir, caDir, issuingCA string
|
||||
var certDir, caDir, issuingCA, certType string
|
||||
var days int
|
||||
cmd := &cobra.Command{
|
||||
Use: "make-cert SUBJECT [SAN...]",
|
||||
Short: "Create a server/client certificate signed by the CA.",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
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(&caDir, "ca-dir", "", "CA root directory")
|
||||
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")
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"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) {
|
||||
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"))
|
||||
|
||||
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)
|
||||
}
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
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")
|
||||
}
|
||||
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 {
|
||||
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) {
|
||||
caDir := t.TempDir()
|
||||
certDir := t.TempDir()
|
||||
@@ -86,7 +123,7 @@ func TestCertDirOverride(t *testing.T) {
|
||||
if err := makeCA(caDir, "Test CA", 3650, "", ""); err != nil {
|
||||
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)
|
||||
}
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
certPath := filepath.Join(caDir, "issuing_ca", "test_cert.pem")
|
||||
|
||||
Reference in New Issue
Block a user