mic.git


mic

~ proxy for JA4-TLS fingerprinting evasion
git clone https://github.com/djnnvx/mic.git
log | files | refs | archive | README | LICENSE

ca.go (5318B)

      1 package proxy
      2 
      3 import (
      4 	"crypto/ecdsa"
      5 	"crypto/elliptic"
      6 	"crypto/rand"
      7 	"crypto/tls"
      8 	"crypto/x509"
      9 	"crypto/x509/pkix"
     10 	"encoding/pem"
     11 	"fmt"
     12 	"math/big"
     13 	"net"
     14 	"os"
     15 	"sync"
     16 	"time"
     17 )
     18 
     19 const (
     20 	leafLifetime = 24 * time.Hour
     21 	// Re-issue this long before expiry so a leaf cannot expire mid-handshake.
     22 	leafRenewMargin = time.Minute
     23 )
     24 
     25 type cachedLeaf struct {
     26 	cert     tls.Certificate
     27 	notAfter time.Time
     28 }
     29 
     30 // LocalCA is a self-signed CA used to issue per-host certificates for
     31 // MitM TLS interception in client-front mode.
     32 type LocalCA struct {
     33 	cert    *x509.Certificate
     34 	key     *ecdsa.PrivateKey
     35 	derCert []byte // raw DER, appended as chain in issued certs
     36 
     37 	mu    sync.Mutex
     38 	cache map[string]cachedLeaf
     39 }
     40 
     41 func GenerateCA() (*LocalCA, error) {
     42 	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
     43 	if err != nil {
     44 		return nil, err
     45 	}
     46 	return buildCA(key)
     47 }
     48 
     49 // LoadOrGenerateCA loads the CA from certPath + keyPath. If the files do not
     50 // exist it generates a new CA and writes it to those paths. Pass empty strings
     51 // for both to get an ephemeral in-memory CA.
     52 func LoadOrGenerateCA(certPath, keyPath string) (*LocalCA, error) {
     53 	if certPath == "" && keyPath == "" {
     54 		return GenerateCA()
     55 	}
     56 	if certPath == "" || keyPath == "" {
     57 		return nil, fmt.Errorf("CA cert path and key path must both be set or both be empty")
     58 	}
     59 
     60 	ca, err := loadCA(certPath, keyPath)
     61 	if err == nil {
     62 		return ca, nil
     63 	}
     64 	if !os.IsNotExist(err) {
     65 		return nil, fmt.Errorf("loading CA: %w", err)
     66 	}
     67 
     68 	ca, err = GenerateCA()
     69 	if err != nil {
     70 		return nil, err
     71 	}
     72 	if err := ca.Save(certPath, keyPath); err != nil {
     73 		return nil, fmt.Errorf("saving CA: %w", err)
     74 	}
     75 	return ca, nil
     76 }
     77 
     78 func loadCA(certPath, keyPath string) (*LocalCA, error) {
     79 	pair, err := tls.LoadX509KeyPair(certPath, keyPath)
     80 	if err != nil {
     81 		return nil, err
     82 	}
     83 	cert, err := x509.ParseCertificate(pair.Certificate[0])
     84 	if err != nil {
     85 		return nil, err
     86 	}
     87 	key, ok := pair.PrivateKey.(*ecdsa.PrivateKey)
     88 	if !ok {
     89 		return nil, fmt.Errorf("CA private key must be ECDSA")
     90 	}
     91 	return &LocalCA{
     92 		cert:    cert,
     93 		key:     key,
     94 		derCert: pair.Certificate[0],
     95 		cache:   make(map[string]cachedLeaf),
     96 	}, nil
     97 }
     98 
     99 func buildCA(key *ecdsa.PrivateKey) (*LocalCA, error) {
    100 	serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
    101 	if err != nil {
    102 		return nil, err
    103 	}
    104 	tmpl := &x509.Certificate{
    105 		SerialNumber:          serial,
    106 		Subject:               pkix.Name{CommonName: "mic local CA"},
    107 		NotBefore:             time.Now().Add(-time.Hour),
    108 		NotAfter:              time.Now().Add(10 * 365 * 24 * time.Hour),
    109 		IsCA:                  true,
    110 		KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
    111 		BasicConstraintsValid: true,
    112 	}
    113 	der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
    114 	if err != nil {
    115 		return nil, err
    116 	}
    117 	cert, err := x509.ParseCertificate(der)
    118 	if err != nil {
    119 		return nil, err
    120 	}
    121 	return &LocalCA{
    122 		cert:    cert,
    123 		key:     key,
    124 		derCert: der,
    125 		cache:   make(map[string]cachedLeaf),
    126 	}, nil
    127 }
    128 
    129 func (ca *LocalCA) Save(certPath, keyPath string) error {
    130 	cf, err := os.Create(certPath)
    131 	if err != nil {
    132 		return err
    133 	}
    134 	defer cf.Close()
    135 	if err := pem.Encode(cf, &pem.Block{Type: "CERTIFICATE", Bytes: ca.derCert}); err != nil {
    136 		return err
    137 	}
    138 
    139 	keyBytes, err := x509.MarshalECPrivateKey(ca.key)
    140 	if err != nil {
    141 		return err
    142 	}
    143 	kf, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
    144 	if err != nil {
    145 		return err
    146 	}
    147 	defer kf.Close()
    148 	// O_CREATE's mode is ignored for an existing file and masked by umask for a
    149 	// new one, so set 0600 explicitly.
    150 	if err := kf.Chmod(0o600); err != nil {
    151 		return err
    152 	}
    153 	return pem.Encode(kf, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes})
    154 }
    155 
    156 // CertPEM returns the CA certificate as PEM for curl --cacert or a trust store.
    157 func (ca *LocalCA) CertPEM() []byte {
    158 	return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.derCert})
    159 }
    160 
    161 // issueCert returns a cached leaf for host, re-issuing when it is unseen or
    162 // near expiry.
    163 func (ca *LocalCA) issueCert(host string) (tls.Certificate, error) {
    164 	ca.mu.Lock()
    165 	defer ca.mu.Unlock()
    166 
    167 	if c, ok := ca.cache[host]; ok && time.Now().Before(c.notAfter.Add(-leafRenewMargin)) {
    168 		return c.cert, nil
    169 	}
    170 
    171 	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    172 	if err != nil {
    173 		return tls.Certificate{}, err
    174 	}
    175 	serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
    176 	if err != nil {
    177 		return tls.Certificate{}, err
    178 	}
    179 	notAfter := time.Now().Add(leafLifetime)
    180 	tmpl := &x509.Certificate{
    181 		SerialNumber: serial,
    182 		Subject:      pkix.Name{CommonName: host},
    183 		NotBefore:    time.Now().Add(-time.Hour),
    184 		NotAfter:     notAfter,
    185 		KeyUsage:     x509.KeyUsageDigitalSignature,
    186 		ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
    187 	}
    188 	if ip := net.ParseIP(host); ip != nil {
    189 		tmpl.IPAddresses = []net.IP{ip}
    190 	} else {
    191 		tmpl.DNSNames = []string{host}
    192 	}
    193 
    194 	der, err := x509.CreateCertificate(rand.Reader, tmpl, ca.cert, &key.PublicKey, ca.key)
    195 	if err != nil {
    196 		return tls.Certificate{}, err
    197 	}
    198 	cert := tls.Certificate{
    199 		Certificate: [][]byte{der, ca.derCert}, // CA appended so clients can verify the chain
    200 		PrivateKey:  key,
    201 	}
    202 	ca.cache[host] = cachedLeaf{cert: cert, notAfter: notAfter}
    203 	return cert, nil
    204 }