Skip to content

Commit 99e15e2

Browse files
committed
Add support for certificate signing requests with -csr
Closes #55
1 parent 592400a commit 99e15e2

File tree

3 files changed

+102
-19
lines changed

3 files changed

+102
-19
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ To only install the local root CA into a subset of them, you can set the `TRUST_
136136
-pkcs12
137137
Generate a ".p12" PKCS #12 file, also know as a ".pfx" file,
138138
containing certificate and key for legacy applications.
139+
140+
-csr CSR
141+
Generate a certificate based on the supplied CSR. Conflicts with
142+
all other flags and arguments except -install and -cert-file.
139143
```
140144

141145
### Mobile devices

cert.go

Lines changed: 80 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,8 @@ func (m *mkcert) makeCert(hosts []string) {
5151
fatalIfErr(err, "failed to generate certificate key")
5252
pub := priv.(crypto.Signer).Public()
5353

54-
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
55-
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
56-
fatalIfErr(err, "failed to generate serial number")
57-
5854
tpl := &x509.Certificate{
59-
SerialNumber: serialNumber,
55+
SerialNumber: randomSerialNumber(),
6056
Subject: pkix.Name{
6157
Organization: []string{"mkcert development certificate"},
6258
OrganizationalUnit: []string{userAndHostname},
@@ -100,7 +96,7 @@ func (m *mkcert) makeCert(hosts []string) {
10096

10197
err = ioutil.WriteFile(certFile, pem.EncodeToMemory(
10298
&pem.Block{Type: "CERTIFICATE", Bytes: cert}), 0644)
103-
fatalIfErr(err, "failed to save certificate key")
99+
fatalIfErr(err, "failed to save certificate")
104100
} else {
105101
domainCert, _ := x509.ParseCertificate(cert)
106102
pfxData, err := pkcs12.Encode(rand.Reader, priv, domainCert, []*x509.Certificate{m.caCert}, "changeit")
@@ -109,6 +105,17 @@ func (m *mkcert) makeCert(hosts []string) {
109105
fatalIfErr(err, "failed to save PKCS#12")
110106
}
111107

108+
m.printHosts(hosts)
109+
110+
if !m.pkcs12 {
111+
log.Printf("\nThe certificate is at \"%s\" and the key at \"%s\"\n\n", certFile, keyFile)
112+
} else {
113+
log.Printf("\nThe PKCS#12 bundle is at \"%s\"\n", p12File)
114+
log.Printf("\nThe legacy PKCS#12 encryption password is the often hardcoded default \"changeit\" ℹ️\n\n")
115+
}
116+
}
117+
118+
func (m *mkcert) printHosts(hosts []string) {
112119
secondLvlWildcardRegexp := regexp.MustCompile(`(?i)^\*\.[0-9a-z_-]+$`)
113120
log.Printf("\nCreated a new certificate valid for the following names 📜")
114121
for _, h := range hosts {
@@ -124,13 +131,6 @@ func (m *mkcert) makeCert(hosts []string) {
124131
break
125132
}
126133
}
127-
128-
if !m.pkcs12 {
129-
log.Printf("\nThe certificate is at \"%s\" and the key at \"%s\"\n\n", certFile, keyFile)
130-
} else {
131-
log.Printf("\nThe PKCS#12 bundle is at \"%s\"\n", p12File)
132-
log.Printf("\nThe legacy PKCS#12 encryption password is the often hardcoded default \"changeit\" ℹ️\n\n")
133-
}
134134
}
135135

136136
func (m *mkcert) generateKey(rootCA bool) (crypto.PrivateKey, error) {
@@ -166,6 +166,72 @@ func (m *mkcert) fileNames(hosts []string) (certFile, keyFile, p12File string) {
166166
return
167167
}
168168

169+
func randomSerialNumber() *big.Int {
170+
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
171+
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
172+
fatalIfErr(err, "failed to generate serial number")
173+
return serialNumber
174+
}
175+
176+
func (m *mkcert) makeCertFromCSR() {
177+
if m.caKey == nil {
178+
log.Fatalln("ERROR: can't create new certificates because the CA key (rootCA-key.pem) is missing")
179+
}
180+
181+
csrPEMBytes, err := ioutil.ReadFile(m.csrPath)
182+
fatalIfErr(err, "failed to read the CSR")
183+
csrPEM, _ := pem.Decode(csrPEMBytes)
184+
if csrPEM == nil {
185+
log.Fatalln("ERROR: failed to read the CSR: unexpected content")
186+
}
187+
if csrPEM.Type != "CERTIFICATE REQUEST" {
188+
log.Fatalln("ERROR: failed to read the CSR: expected CERTIFICATE REQUEST, got " + csrPEM.Type)
189+
}
190+
csr, err := x509.ParseCertificateRequest(csrPEM.Bytes)
191+
fatalIfErr(err, "failed to parse the CSR")
192+
fatalIfErr(csr.CheckSignature(), "invalid CSR signature")
193+
194+
tpl := &x509.Certificate{
195+
SerialNumber: randomSerialNumber(),
196+
Subject: csr.Subject,
197+
ExtraExtensions: csr.Extensions, // includes requested SANs
198+
199+
NotAfter: time.Now().AddDate(10, 0, 0),
200+
NotBefore: time.Now(),
201+
202+
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
203+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
204+
BasicConstraintsValid: true,
205+
206+
// If the CSR does not request a SAN extension, fix it up for them as
207+
// the Common Name field does not work in modern browsers. Otherwise,
208+
// this will get overridden.
209+
DNSNames: []string{csr.Subject.CommonName},
210+
}
211+
212+
cert, err := x509.CreateCertificate(rand.Reader, tpl, m.caCert, csr.PublicKey, m.caKey)
213+
fatalIfErr(err, "failed to generate certificate")
214+
215+
var hosts []string
216+
hosts = append(hosts, csr.DNSNames...)
217+
hosts = append(hosts, csr.EmailAddresses...)
218+
for _, ip := range csr.IPAddresses {
219+
hosts = append(hosts, ip.String())
220+
}
221+
if len(hosts) == 0 {
222+
hosts = []string{csr.Subject.CommonName}
223+
}
224+
certFile, _, _ := m.fileNames(hosts)
225+
226+
err = ioutil.WriteFile(certFile, pem.EncodeToMemory(
227+
&pem.Block{Type: "CERTIFICATE", Bytes: cert}), 0644)
228+
fatalIfErr(err, "failed to save certificate")
229+
230+
m.printHosts(hosts)
231+
232+
log.Printf("\nThe certificate is at \"%s\"\n\n", certFile)
233+
}
234+
169235
// loadCA will load or create the CA at CAROOT.
170236
func (m *mkcert) loadCA() {
171237
if _, err := os.Stat(filepath.Join(m.CAROOT, rootName)); os.IsNotExist(err) {
@@ -202,10 +268,6 @@ func (m *mkcert) newCA() {
202268
fatalIfErr(err, "failed to generate the CA key")
203269
pub := priv.(crypto.Signer).Public()
204270

205-
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
206-
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
207-
fatalIfErr(err, "failed to generate serial number")
208-
209271
spkiASN1, err := x509.MarshalPKIXPublicKey(pub)
210272
fatalIfErr(err, "failed to encode public key")
211273

@@ -219,7 +281,7 @@ func (m *mkcert) newCA() {
219281
skid := sha1.Sum(spki.SubjectPublicKey.Bytes)
220282

221283
tpl := &x509.Certificate{
222-
SerialNumber: serialNumber,
284+
SerialNumber: randomSerialNumber(),
223285
Subject: pkix.Name{
224286
Organization: []string{"mkcert development CA"},
225287
OrganizationalUnit: []string{userAndHostname},

main.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ const advancedUsage = `Advanced options:
5555
Generate a ".p12" PKCS #12 file, also know as a ".pfx" file,
5656
containing certificate and key for legacy applications.
5757
58+
-csr CSR
59+
Generate a certificate based on the supplied CSR. Conflicts with
60+
all other flags and arguments except -install and -cert-file.
61+
5862
-CAROOT
5963
Print the CA certificate and key storage location.
6064
@@ -79,6 +83,7 @@ func main() {
7983
clientFlag = flag.Bool("client", false, "")
8084
helpFlag = flag.Bool("help", false, "")
8185
carootFlag = flag.Bool("CAROOT", false, "")
86+
csrFlag = flag.String("csr", "", "")
8287
certFileFlag = flag.String("cert-file", "", "")
8388
keyFileFlag = flag.String("key-file", "", "")
8489
p12FileFlag = flag.String("p12-file", "", "")
@@ -103,8 +108,14 @@ func main() {
103108
if *installFlag && *uninstallFlag {
104109
log.Fatalln("ERROR: you can't set -install and -uninstall at the same time")
105110
}
111+
if *csrFlag != "" && (*pkcs12Flag || *ecdsaFlag || *clientFlag) {
112+
log.Fatalln("ERROR: can only combine -csr with -install and -cert-file")
113+
}
114+
if *csrFlag != "" && flag.NArg() != 0 {
115+
log.Fatalln("ERROR: can't specify extra arguments when using -csr")
116+
}
106117
(&mkcert{
107-
installMode: *installFlag, uninstallMode: *uninstallFlag,
118+
installMode: *installFlag, uninstallMode: *uninstallFlag, csrPath: *csrFlag,
108119
pkcs12: *pkcs12Flag, ecdsa: *ecdsaFlag, client: *clientFlag,
109120
certFile: *certFileFlag, keyFile: *keyFileFlag, p12File: *p12FileFlag,
110121
}).Run(flag.Args())
@@ -117,6 +128,7 @@ type mkcert struct {
117128
installMode, uninstallMode bool
118129
pkcs12, ecdsa, client bool
119130
keyFile, certFile, p12File string
131+
csrPath string
120132

121133
CAROOT string
122134
caCert *x509.Certificate
@@ -163,6 +175,11 @@ func (m *mkcert) Run(args []string) {
163175
}
164176
}
165177

178+
if m.csrPath != "" {
179+
m.makeCertFromCSR()
180+
return
181+
}
182+
166183
if len(args) == 0 {
167184
flag.Usage()
168185
return

0 commit comments

Comments
 (0)