diff --git a/src/simple-ca/main.go b/src/simple-ca/main.go index d6f177e..7bcf2ce 100644 --- a/src/simple-ca/main.go +++ b/src/simple-ca/main.go @@ -286,21 +286,25 @@ func makeCA(caDir, caName string, days int, issuingCA, aiaBaseURL string) error // ---- makeCert --------------------------------------------------------------- var ( - ipRE = regexp.MustCompile(`^[0-9]{1,3}(\.[0-9]{1,3}){3}$`) - dnsRE = regexp.MustCompile(`^[a-z0-9-]+(\.[a-z0-9-]+)*$`) + 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 } } - 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 { 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{ - 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, + + 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), + 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 } diff --git a/src/simple-ca/main_test.go b/src/simple-ca/main_test.go index 5347b11..b94696d 100644 --- a/src/simple-ca/main_test.go +++ b/src/simple-ca/main_test.go @@ -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")