diff --git a/README.md b/README.md index 4bced365..35085801 100644 --- a/README.md +++ b/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 diff --git a/cert.go b/cert.go index 98e21d06..b85829df 100644 --- a/cert.go +++ b/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}, diff --git a/main.go b/main.go index 0d41e332..f25e164e 100644 --- a/main.go +++ b/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