Bladeren bron

Add support for certificate signing requests with -csr

Closes #55
Filippo Valsorda 6 jaren geleden
bovenliggende
commit
99e15e29f9
3 gewijzigde bestanden met toevoegingen van 102 en 19 verwijderingen
  1. 4 0
      README.md
  2. 80 18
      cert.go
  3. 18 1
      main.go

+ 4 - 0
README.md

@@ -136,6 +136,10 @@ To only install the local root CA into a subset of them, you can set the `TRUST_
 	-pkcs12
 	    Generate a ".p12" PKCS #12 file, also know as a ".pfx" file,
 	    containing certificate and key for legacy applications.
+
+	-csr CSR
+	    Generate a certificate based on the supplied CSR. Conflicts with
+	    all other flags and arguments except -install and -cert-file.
 ```
 
 ### Mobile devices

+ 80 - 18
cert.go

@@ -51,12 +51,8 @@ func (m *mkcert) makeCert(hosts []string) {
 	fatalIfErr(err, "failed to generate certificate key")
 	pub := priv.(crypto.Signer).Public()
 
-	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
-	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
-	fatalIfErr(err, "failed to generate serial number")
-
 	tpl := &x509.Certificate{
-		SerialNumber: serialNumber,
+		SerialNumber: randomSerialNumber(),
 		Subject: pkix.Name{
 			Organization:       []string{"mkcert development certificate"},
 			OrganizationalUnit: []string{userAndHostname},
@@ -100,7 +96,7 @@ func (m *mkcert) makeCert(hosts []string) {
 
 		err = ioutil.WriteFile(certFile, pem.EncodeToMemory(
 			&pem.Block{Type: "CERTIFICATE", Bytes: cert}), 0644)
-		fatalIfErr(err, "failed to save certificate key")
+		fatalIfErr(err, "failed to save certificate")
 	} else {
 		domainCert, _ := x509.ParseCertificate(cert)
 		pfxData, err := pkcs12.Encode(rand.Reader, priv, domainCert, []*x509.Certificate{m.caCert}, "changeit")
@@ -109,6 +105,17 @@ func (m *mkcert) makeCert(hosts []string) {
 		fatalIfErr(err, "failed to save PKCS#12")
 	}
 
+	m.printHosts(hosts)
+
+	if !m.pkcs12 {
+		log.Printf("\nThe certificate is at \"%s\" and the key at \"%s\" ✅\n\n", certFile, keyFile)
+	} else {
+		log.Printf("\nThe PKCS#12 bundle is at \"%s\" ✅\n", p12File)
+		log.Printf("\nThe legacy PKCS#12 encryption password is the often hardcoded default \"changeit\" ℹ️\n\n")
+	}
+}
+
+func (m *mkcert) printHosts(hosts []string) {
 	secondLvlWildcardRegexp := regexp.MustCompile(`(?i)^\*\.[0-9a-z_-]+$`)
 	log.Printf("\nCreated a new certificate valid for the following names 📜")
 	for _, h := range hosts {
@@ -124,13 +131,6 @@ func (m *mkcert) makeCert(hosts []string) {
 			break
 		}
 	}
-
-	if !m.pkcs12 {
-		log.Printf("\nThe certificate is at \"%s\" and the key at \"%s\" ✅\n\n", certFile, keyFile)
-	} else {
-		log.Printf("\nThe PKCS#12 bundle is at \"%s\" ✅\n", p12File)
-		log.Printf("\nThe legacy PKCS#12 encryption password is the often hardcoded default \"changeit\" ℹ️\n\n")
-	}
 }
 
 func (m *mkcert) generateKey(rootCA bool) (crypto.PrivateKey, error) {
@@ -166,6 +166,72 @@ func (m *mkcert) fileNames(hosts []string) (certFile, keyFile, p12File string) {
 	return
 }
 
+func randomSerialNumber() *big.Int {
+	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
+	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
+	fatalIfErr(err, "failed to generate serial number")
+	return serialNumber
+}
+
+func (m *mkcert) makeCertFromCSR() {
+	if m.caKey == nil {
+		log.Fatalln("ERROR: can't create new certificates because the CA key (rootCA-key.pem) is missing")
+	}
+
+	csrPEMBytes, err := ioutil.ReadFile(m.csrPath)
+	fatalIfErr(err, "failed to read the CSR")
+	csrPEM, _ := pem.Decode(csrPEMBytes)
+	if csrPEM == nil {
+		log.Fatalln("ERROR: failed to read the CSR: unexpected content")
+	}
+	if csrPEM.Type != "CERTIFICATE REQUEST" {
+		log.Fatalln("ERROR: failed to read the CSR: expected CERTIFICATE REQUEST, got " + csrPEM.Type)
+	}
+	csr, err := x509.ParseCertificateRequest(csrPEM.Bytes)
+	fatalIfErr(err, "failed to parse the CSR")
+	fatalIfErr(csr.CheckSignature(), "invalid CSR signature")
+
+	tpl := &x509.Certificate{
+		SerialNumber:    randomSerialNumber(),
+		Subject:         csr.Subject,
+		ExtraExtensions: csr.Extensions, // includes requested SANs
+
+		NotAfter:  time.Now().AddDate(10, 0, 0),
+		NotBefore: time.Now(),
+
+		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
+		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+		BasicConstraintsValid: true,
+
+		// If the CSR does not request a SAN extension, fix it up for them as
+		// the Common Name field does not work in modern browsers. Otherwise,
+		// this will get overridden.
+		DNSNames: []string{csr.Subject.CommonName},
+	}
+
+	cert, err := x509.CreateCertificate(rand.Reader, tpl, m.caCert, csr.PublicKey, m.caKey)
+	fatalIfErr(err, "failed to generate certificate")
+
+	var hosts []string
+	hosts = append(hosts, csr.DNSNames...)
+	hosts = append(hosts, csr.EmailAddresses...)
+	for _, ip := range csr.IPAddresses {
+		hosts = append(hosts, ip.String())
+	}
+	if len(hosts) == 0 {
+		hosts = []string{csr.Subject.CommonName}
+	}
+	certFile, _, _ := m.fileNames(hosts)
+
+	err = ioutil.WriteFile(certFile, pem.EncodeToMemory(
+		&pem.Block{Type: "CERTIFICATE", Bytes: cert}), 0644)
+	fatalIfErr(err, "failed to save certificate")
+
+	m.printHosts(hosts)
+
+	log.Printf("\nThe certificate is at \"%s\" ✅\n\n", certFile)
+}
+
 // loadCA will load or create the CA at CAROOT.
 func (m *mkcert) loadCA() {
 	if _, err := os.Stat(filepath.Join(m.CAROOT, rootName)); os.IsNotExist(err) {
@@ -202,10 +268,6 @@ func (m *mkcert) newCA() {
 	fatalIfErr(err, "failed to generate the CA key")
 	pub := priv.(crypto.Signer).Public()
 
-	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
-	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
-	fatalIfErr(err, "failed to generate serial number")
-
 	spkiASN1, err := x509.MarshalPKIXPublicKey(pub)
 	fatalIfErr(err, "failed to encode public key")
 
@@ -219,7 +281,7 @@ func (m *mkcert) newCA() {
 	skid := sha1.Sum(spki.SubjectPublicKey.Bytes)
 
 	tpl := &x509.Certificate{
-		SerialNumber: serialNumber,
+		SerialNumber: randomSerialNumber(),
 		Subject: pkix.Name{
 			Organization:       []string{"mkcert development CA"},
 			OrganizationalUnit: []string{userAndHostname},

+ 18 - 1
main.go

@@ -55,6 +55,10 @@ const advancedUsage = `Advanced options:
 	    Generate a ".p12" PKCS #12 file, also know as a ".pfx" file,
 	    containing certificate and key for legacy applications.
 
+	-csr CSR
+	    Generate a certificate based on the supplied CSR. Conflicts with
+	    all other flags and arguments except -install and -cert-file.
+
 	-CAROOT
 	    Print the CA certificate and key storage location.
 
@@ -79,6 +83,7 @@ func main() {
 		clientFlag    = flag.Bool("client", false, "")
 		helpFlag      = flag.Bool("help", false, "")
 		carootFlag    = flag.Bool("CAROOT", false, "")
+		csrFlag       = flag.String("csr", "", "")
 		certFileFlag  = flag.String("cert-file", "", "")
 		keyFileFlag   = flag.String("key-file", "", "")
 		p12FileFlag   = flag.String("p12-file", "", "")
@@ -103,8 +108,14 @@ func main() {
 	if *installFlag && *uninstallFlag {
 		log.Fatalln("ERROR: you can't set -install and -uninstall at the same time")
 	}
+	if *csrFlag != "" && (*pkcs12Flag || *ecdsaFlag || *clientFlag) {
+		log.Fatalln("ERROR: can only combine -csr with -install and -cert-file")
+	}
+	if *csrFlag != "" && flag.NArg() != 0 {
+		log.Fatalln("ERROR: can't specify extra arguments when using -csr")
+	}
 	(&mkcert{
-		installMode: *installFlag, uninstallMode: *uninstallFlag,
+		installMode: *installFlag, uninstallMode: *uninstallFlag, csrPath: *csrFlag,
 		pkcs12: *pkcs12Flag, ecdsa: *ecdsaFlag, client: *clientFlag,
 		certFile: *certFileFlag, keyFile: *keyFileFlag, p12File: *p12FileFlag,
 	}).Run(flag.Args())
@@ -117,6 +128,7 @@ type mkcert struct {
 	installMode, uninstallMode bool
 	pkcs12, ecdsa, client      bool
 	keyFile, certFile, p12File string
+	csrPath                    string
 
 	CAROOT string
 	caCert *x509.Certificate
@@ -163,6 +175,11 @@ func (m *mkcert) Run(args []string) {
 		}
 	}
 
+	if m.csrPath != "" {
+		m.makeCertFromCSR()
+		return
+	}
+
 	if len(args) == 0 {
 		flag.Usage()
 		return