github.com/bingoohuang/gg@v0.0.0-20240325092523-45da7dee9335/pkg/netx/mkcert.go (about) 1 package netx 2 3 import ( 4 "crypto" 5 "crypto/ecdsa" 6 "crypto/elliptic" 7 "crypto/rand" 8 "crypto/rsa" 9 "crypto/sha1" 10 "crypto/x509" 11 "crypto/x509/pkix" 12 "encoding/asn1" 13 "encoding/pem" 14 "fmt" 15 "io/ioutil" 16 "log" 17 "math/big" 18 "net" 19 "net/mail" 20 "net/url" 21 "os" 22 "os/user" 23 "path/filepath" 24 "regexp" 25 "runtime" 26 "strconv" 27 "strings" 28 "time" 29 30 "golang.org/x/net/idna" 31 "software.sslmate.com/src/go-pkcs12" 32 ) 33 34 var ( 35 userAndHostname string 36 37 RootCaName = "root.pem" 38 RootKeyName = "root.key" 39 ) 40 41 type MkCert struct { 42 Pkcs12, Ecdsa, Client, Silent bool 43 KeyFile, CertFile, P12File string 44 CaRoot, CsrPath string 45 RootYears, CertYears int 46 47 caCert *x509.Certificate 48 caKey crypto.PrivateKey 49 } 50 51 func (m *MkCert) Run(hosts ...string) error { 52 if m.CaRoot == "" { 53 m.CaRoot = getCaRoot() 54 } 55 if m.CaRoot == "" { 56 return fmt.Errorf("failed to find the default CA location, set env CAROOT") 57 } 58 if err := os.MkdirAll(m.CaRoot, 0o755); err != nil { 59 return fmt.Errorf("create the CaRoot failed: %w", err) 60 } 61 62 if err := m.loadCaRoot(); err != nil { 63 return err 64 } 65 66 if m.CsrPath != "" { 67 return m.makeCertFromCSR() 68 } 69 70 if len(hosts) == 0 { 71 return fmt.Errorf("at least one IP/host/email should be specified") 72 } 73 74 hostnameRegexp := regexp.MustCompile(`(?i)^(\*\.)?[0-9a-z_-]([0-9a-z._-]*[0-9a-z_-])?$`) 75 for i, name := range hosts { 76 if ip := net.ParseIP(name); ip != nil { 77 continue 78 } 79 if email, err := mail.ParseAddress(name); err == nil && email.Address == name { 80 continue 81 } 82 if uriName, err := url.Parse(name); err == nil && uriName.Scheme != "" && uriName.Host != "" { 83 continue 84 } 85 punycode, err := idna.ToASCII(name) 86 if err != nil { 87 return fmt.Errorf("%q is not a valid hostname, IP, URL or email, failed: %w", name, err) 88 } 89 hosts[i] = punycode 90 if !hostnameRegexp.MatchString(punycode) { 91 return fmt.Errorf("%q is not a valid hostname, IP, URL or email", name) 92 } 93 } 94 95 return m.makeCert(hosts) 96 } 97 98 func getCaRoot() string { 99 if env := os.Getenv("CAROOT"); env != "" { 100 return env 101 } 102 103 var dir string 104 switch { 105 case runtime.GOOS == "windows": 106 dir = os.Getenv("LocalAppData") 107 case os.Getenv("XDG_DATA_HOME") != "": 108 dir = os.Getenv("XDG_DATA_HOME") 109 case runtime.GOOS == "darwin": 110 dir = os.Getenv("HOME") 111 if dir == "" { 112 return "" 113 } 114 dir = filepath.Join(dir, "Library", "Application Support") 115 default: // Unix 116 dir = os.Getenv("HOME") 117 if dir == "" { 118 return "" 119 } 120 dir = filepath.Join(dir, ".local", "share") 121 } 122 return filepath.Join(dir, ".cert") 123 } 124 125 func init() { 126 u, err := user.Current() 127 if err == nil { 128 userAndHostname = u.Username + "@" 129 } 130 if h, err := os.Hostname(); err == nil { 131 userAndHostname += h 132 } 133 if err == nil && u.Name != "" && u.Name != u.Username { 134 userAndHostname += " (" + u.Name + ")" 135 } 136 } 137 138 func (m *MkCert) makeCert(hosts []string) error { 139 if m.caKey == nil { 140 return fmt.Errorf("CA key (%s) is missing", RootKeyName) 141 } 142 143 priv, err := m.generateKey(false) 144 if err != nil { 145 return fmt.Errorf("generate certificate key failed: %w", err) 146 } 147 148 pub := priv.(crypto.Signer).Public() 149 150 // Certificates last for 2 years and 3 months, which is always less than 151 // 825 days, the limit that macOS/iOS apply to all certificates, 152 // including custom roots. See https://support.apple.com/en-us/HT210176. 153 if m.CertYears == 0 { 154 m.CertYears = 2 155 } 156 157 serialNumber, err := randomSerialNumber() 158 if err != nil { 159 return err 160 } 161 162 start := time.Now().UTC() 163 expiration := start.AddDate(m.CertYears, 0, 0) 164 165 c := &x509.Certificate{ 166 SerialNumber: serialNumber, 167 Subject: pkix.Name{ 168 Organization: []string{"mkcert dev certificate"}, 169 OrganizationalUnit: []string{userAndHostname}, 170 }, 171 172 NotBefore: start.Add(-24 * time.Hour), NotAfter: expiration, 173 KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 174 } 175 176 for _, h := range hosts { 177 if ip := net.ParseIP(h); ip != nil { 178 c.IPAddresses = append(c.IPAddresses, ip) 179 } else if email, err := mail.ParseAddress(h); err == nil && email.Address == h { 180 c.EmailAddresses = append(c.EmailAddresses, h) 181 } else if uriName, err := url.Parse(h); err == nil && uriName.Scheme != "" && uriName.Host != "" { 182 c.URIs = append(c.URIs, uriName) 183 } else { 184 c.DNSNames = append(c.DNSNames, h) 185 } 186 } 187 188 if m.Client { 189 c.ExtKeyUsage = append(c.ExtKeyUsage, x509.ExtKeyUsageClientAuth) 190 } 191 if len(c.IPAddresses) > 0 || len(c.DNSNames) > 0 || len(c.URIs) > 0 { 192 c.ExtKeyUsage = append(c.ExtKeyUsage, x509.ExtKeyUsageServerAuth) 193 } 194 if len(c.EmailAddresses) > 0 { 195 c.ExtKeyUsage = append(c.ExtKeyUsage, x509.ExtKeyUsageEmailProtection) 196 } 197 198 // IIS (the main target of PKCS #12 files), only shows the deprecated 199 // Common Name in the UI. See issue #115. 200 if m.Pkcs12 { 201 c.Subject.CommonName = hosts[0] 202 } 203 204 cert, err := x509.CreateCertificate(rand.Reader, c, m.caCert, pub, m.caKey) 205 if err != nil { 206 return fmt.Errorf("generate certificat failed: %w", err) 207 } 208 209 m.CertFile, m.KeyFile, m.P12File = m.fileNames(hosts) 210 211 changeIt := "changeit" 212 if m.Pkcs12 { 213 domainCert, _ := x509.ParseCertificate(cert) 214 pfxData, err := pkcs12.Encode(rand.Reader, priv, domainCert, []*x509.Certificate{m.caCert}, changeIt) 215 if err != nil { 216 return fmt.Errorf("generate PKCS#12 failed: %w", err) 217 } 218 219 if err := ioutil.WriteFile(m.P12File, pfxData, 0o644); err != nil { 220 return fmt.Errorf("save PKCS#12 failed: %w", err) 221 } 222 } else { 223 certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert}) 224 privDER, err := x509.MarshalPKCS8PrivateKey(priv) 225 if err != nil { 226 return fmt.Errorf("encode certificate key failed: %w", err) 227 } 228 privPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privDER}) 229 230 if m.CertFile == m.KeyFile { 231 if err := ioutil.WriteFile(m.KeyFile, append(certPEM, privPEM...), 0o600); err != nil { 232 return fmt.Errorf("save certificate and key failed: %w", err) 233 } 234 } else { 235 if err := ioutil.WriteFile(m.CertFile, certPEM, 0o644); err != nil { 236 return fmt.Errorf("save certificate failed: %w", err) 237 } 238 if err := ioutil.WriteFile(m.KeyFile, privPEM, 0o600); err != nil { 239 return fmt.Errorf("save certificate key failed: %w", err) 240 } 241 } 242 } 243 244 m.printHosts(hosts) 245 246 if !m.Silent { 247 if m.Pkcs12 { 248 log.Printf("The PKCS#12 bundle is at %q ✅", m.P12File) 249 log.Printf("The legacy PKCS#12 encryption password is the often hardcoded default %q ℹ️", changeIt) 250 } else { 251 if m.CertFile == m.KeyFile { 252 log.Printf("The certificate and key are at %q ✅", m.CertFile) 253 } else { 254 log.Printf("The certificate is at %q and the key at %q ✅", m.CertFile, m.KeyFile) 255 } 256 } 257 258 log.Printf("The certificate will expire at %s 🗓", expiration.Format("2006-01-02 15:04:05")) 259 } 260 261 return nil 262 } 263 264 func (m *MkCert) printHosts(hosts []string) { 265 if m.Silent { 266 return 267 } 268 269 secondLvlWildcardRegexp := regexp.MustCompile(`(?i)^\*\.[0-9a-z_-]+$`) 270 for _, h := range hosts { 271 log.Printf("Created a new certificate valid for the name - %q 📜", h) 272 if secondLvlWildcardRegexp.MatchString(h) { 273 log.Printf("Warning: many browsers don't support second-level wildcards like %q ⚠️", h) 274 } 275 } 276 277 for _, h := range hosts { 278 if strings.HasPrefix(h, "*.") { 279 log.Printf("Reminder: X.509 wildcards only go one level deep, so this won't match a.b.%s ℹ️", h[2:]) 280 break 281 } 282 } 283 } 284 285 func (m *MkCert) generateKey(rootCA bool) (crypto.PrivateKey, error) { 286 if m.Ecdsa { 287 return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 288 } 289 290 if rootCA { 291 return rsa.GenerateKey(rand.Reader, 3072) 292 } 293 294 return rsa.GenerateKey(rand.Reader, 2048) 295 } 296 297 func (m *MkCert) fileNames(hosts []string) (certFile, keyFile, p12File string) { 298 defaultName := strings.Replace(hosts[0], ":", "_", -1) 299 defaultName = strings.Replace(defaultName, "*", "_wildcard", -1) 300 if len(hosts) > 1 { 301 defaultName += "+" + strconv.Itoa(len(hosts)-1) 302 } 303 if m.Client { 304 defaultName += "-client" 305 } 306 307 certFile = filepath.Join(m.CaRoot, defaultName+".pem") 308 if m.CertFile != "" { 309 certFile = m.CertFile 310 } 311 keyFile = filepath.Join(m.CaRoot, defaultName+".key") 312 if m.KeyFile != "" { 313 keyFile = m.KeyFile 314 } 315 p12File = filepath.Join(m.CaRoot, defaultName+".p12") 316 if m.P12File != "" { 317 p12File = m.P12File 318 } 319 320 return 321 } 322 323 func randomSerialNumber() (*big.Int, error) { 324 serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) 325 serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) 326 if err != nil { 327 return nil, fmt.Errorf("generate serial number failed: %w", err) 328 } 329 return serialNumber, nil 330 } 331 332 func (m *MkCert) makeCertFromCSR() error { 333 if m.caKey == nil { 334 return fmt.Errorf("can't create new certificates because the CA key (rootCA-key.pem) is missing") 335 } 336 337 csrPEMBytes, err := ioutil.ReadFile(m.CsrPath) 338 if err != nil { 339 return fmt.Errorf("read the CSR %s failed: %w", m.CsrPath, err) 340 } 341 342 csrPEM, _ := pem.Decode(csrPEMBytes) 343 if csrPEM == nil { 344 return fmt.Errorf("read the CSR failed: unexpected content") 345 } 346 347 if csrPEM.Type != "CERTIFICATE REQUEST" && csrPEM.Type != "NEW CERTIFICATE REQUEST" { 348 return fmt.Errorf("read the CSR failed, expect CERTIFICATE REQUEST, got %s", csrPEM.Type) 349 } 350 csr, err := x509.ParseCertificateRequest(csrPEM.Bytes) 351 if err != nil { 352 return fmt.Errorf("parse the CSR %s failed: %w", m.CsrPath, err) 353 } 354 if err := csr.CheckSignature(); err != nil { 355 return fmt.Errorf("check CSR %s signature failed: %w", m.CsrPath, err) 356 } 357 358 if m.CertYears == 0 { 359 m.CertYears = 2 360 } 361 serialNumber, err := randomSerialNumber() 362 if err != nil { 363 return err 364 } 365 366 start := time.Now().UTC() 367 expiration := start.AddDate(m.CertYears, 0, 0) 368 369 tpl := &x509.Certificate{ 370 SerialNumber: serialNumber, 371 Subject: csr.Subject, 372 ExtraExtensions: csr.Extensions, // includes requested SANs, KUs and EKUs 373 374 NotBefore: start.Add(-24 * time.Hour), NotAfter: expiration, 375 376 // If the CSR does not request a SAN extension, fix it up for them as 377 // the Common Name field does not work in modern browsers. Otherwise, 378 // this will get overridden. 379 DNSNames: []string{csr.Subject.CommonName}, 380 381 // Likewise, if the CSR does not set KUs and EKUs, fix it up as Apple 382 // platforms require serverAuth for TLS. 383 KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 384 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 385 } 386 387 if m.Client { 388 tpl.ExtKeyUsage = append(tpl.ExtKeyUsage, x509.ExtKeyUsageClientAuth) 389 } 390 if len(csr.EmailAddresses) > 0 { 391 tpl.ExtKeyUsage = append(tpl.ExtKeyUsage, x509.ExtKeyUsageEmailProtection) 392 } 393 394 cert, err := x509.CreateCertificate(rand.Reader, tpl, m.caCert, csr.PublicKey, m.caKey) 395 if err != nil { 396 return fmt.Errorf("generate certificate failed: %w", err) 397 } 398 399 var hosts []string 400 hosts = append(hosts, csr.DNSNames...) 401 hosts = append(hosts, csr.EmailAddresses...) 402 for _, ip := range csr.IPAddresses { 403 hosts = append(hosts, ip.String()) 404 } 405 for _, uri := range csr.URIs { 406 hosts = append(hosts, uri.String()) 407 } 408 certFile, _, _ := m.fileNames(hosts) 409 410 pemMem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert}) 411 if err := ioutil.WriteFile(certFile, pemMem, 0o644); err != nil { 412 return fmt.Errorf("save certificate failed: %w", err) 413 } 414 415 m.printHosts(hosts) 416 417 if !m.Silent { 418 log.Printf("The certificate is at %q ✅, it will expire at %s 🗓", 419 certFile, expiration.Format("2006-01-02 15:04:05")) 420 } 421 422 return nil 423 } 424 425 func PathExists(path string) bool { 426 _, err := os.Stat(path) 427 return err == nil 428 } 429 430 // loadCaRoot will load or create the CA at CaRoot. 431 func (m *MkCert) loadCaRoot() error { 432 if !PathExists(filepath.Join(m.CaRoot, RootCaName)) { 433 if err := m.newCA(); err != nil { 434 return err 435 } 436 } 437 438 certPEMBlock, err := ioutil.ReadFile(filepath.Join(m.CaRoot, RootCaName)) 439 if err != nil { 440 return fmt.Errorf("read the CA certificat failed: %w", err) 441 } 442 certDERBlock, _ := pem.Decode(certPEMBlock) 443 if certDERBlock == nil || certDERBlock.Type != "CERTIFICATE" { 444 return fmt.Errorf("read the CA certificat failed: unexpected content") 445 } 446 m.caCert, err = x509.ParseCertificate(certDERBlock.Bytes) 447 if err != nil { 448 return fmt.Errorf("parse the CA certificat failed: %w", err) 449 } 450 451 if !PathExists(filepath.Join(m.CaRoot, RootKeyName)) { 452 return nil // keyless mode, where only -install works 453 } 454 455 keyPEMBlock, err := ioutil.ReadFile(filepath.Join(m.CaRoot, RootKeyName)) 456 if err != nil { 457 return fmt.Errorf("read the CA key failed: %w", err) 458 } 459 keyDERBlock, _ := pem.Decode(keyPEMBlock) 460 if keyDERBlock == nil || keyDERBlock.Type != "PRIVATE KEY" { 461 return fmt.Errorf("read the CA key failed: unexpected content") 462 } 463 m.caKey, err = x509.ParsePKCS8PrivateKey(keyDERBlock.Bytes) 464 if err != nil { 465 return fmt.Errorf("parse the CA key failed: %w", err) 466 } 467 468 return nil 469 } 470 471 func (m *MkCert) newCA() error { 472 priv, err := m.generateKey(true) 473 if err != nil { 474 return fmt.Errorf("generate the CA key failed: %w", err) 475 } 476 477 pub := priv.(crypto.Signer).Public() 478 479 spkiASN1, err := x509.MarshalPKIXPublicKey(pub) 480 if err != nil { 481 return fmt.Errorf("encode public key failed: %w", err) 482 } 483 484 var spki struct { 485 Algorithm pkix.AlgorithmIdentifier 486 SubjectPublicKey asn1.BitString 487 } 488 _, err = asn1.Unmarshal(spkiASN1, &spki) 489 if err != nil { 490 return fmt.Errorf("decode public key failed: %w", err) 491 } 492 493 skid := sha1.Sum(spki.SubjectPublicKey.Bytes) 494 serialNumber, err := randomSerialNumber() 495 if err != nil { 496 return err 497 } 498 499 if m.RootYears == 0 { 500 m.RootYears = 10 501 } 502 503 start := time.Now().UTC() 504 expiration := start.AddDate(10, 0, 0) 505 506 tpl := &x509.Certificate{ 507 SerialNumber: serialNumber, 508 Subject: pkix.Name{ 509 Organization: []string{"mkcert development CA"}, 510 OrganizationalUnit: []string{userAndHostname}, 511 512 // The CommonName is required by iOS to show the certificate in the 513 // "Certificate Trust Settings" menu. 514 // https://github.com/FiloSottile/mkcert/issues/47 515 CommonName: "mkcert " + userAndHostname, 516 }, 517 SubjectKeyId: skid[:], 518 519 NotBefore: start.Add(-24 * time.Hour), NotAfter: expiration, 520 521 KeyUsage: x509.KeyUsageCertSign, 522 523 BasicConstraintsValid: true, 524 IsCA: true, 525 MaxPathLenZero: true, 526 } 527 528 cert, err := x509.CreateCertificate(rand.Reader, tpl, tpl, pub, priv) 529 if err != nil { 530 return fmt.Errorf("generate CA certificate failed: %w", err) 531 } 532 533 privDER, err := x509.MarshalPKCS8PrivateKey(priv) 534 if err != nil { 535 return fmt.Errorf("encode CA key failed: %w", err) 536 } 537 if err = ioutil.WriteFile(filepath.Join(m.CaRoot, RootKeyName), pem.EncodeToMemory( 538 &pem.Block{Type: "PRIVATE KEY", Bytes: privDER}), 0o400); err != nil { 539 return fmt.Errorf("save CA key failed: %w", err) 540 } 541 if err = ioutil.WriteFile(filepath.Join(m.CaRoot, RootCaName), pem.EncodeToMemory( 542 &pem.Block{Type: "CERTIFICATE", Bytes: cert}), 0o644); err != nil { 543 return fmt.Errorf("save CA certificate failed: %w", err) 544 } 545 546 if !m.Silent { 547 log.Printf("Created a new local CA ✅") 548 } 549 return nil 550 }