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  }