diff options
Diffstat (limited to 'internal/ca')
| -rw-r--r-- | internal/ca/ca.go | 85 |
1 files changed, 82 insertions, 3 deletions
diff --git a/internal/ca/ca.go b/internal/ca/ca.go index 949c2fd..fc227a5 100644 --- a/internal/ca/ca.go +++ b/internal/ca/ca.go @@ -1,20 +1,23 @@ -// Package ca manages mitmux's root CA: generating it on first run and -// loading it on subsequent runs. Leaf certificate signing for TLS -// interception is added in a later build step. +// Package ca manages mitmux's root CA: generating it on first run, +// loading it on subsequent runs, and signing per-host leaf certificates +// on demand for TLS interception. package ca import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "errors" "fmt" "math/big" + "net" "os" "path/filepath" + "sync" "time" ) @@ -30,6 +33,82 @@ type CA struct { Key *ecdsa.PrivateKey CertPEM []byte KeyPEM []byte + + leafMu sync.Mutex + leafCache map[string]*tls.Certificate +} + +// leafLifetime is kept well under the ~398 day limit modern browsers +// enforce on leaf certificates. +const leafLifetime = 300 * 24 * time.Hour + +// LeafFor returns a TLS certificate for host (a DNS name or IP address, +// no port), signed by the CA. Certificates are generated once and cached +// in memory for the life of the process. +func (c *CA) LeafFor(host string) (*tls.Certificate, error) { + c.leafMu.Lock() + defer c.leafMu.Unlock() + + if c.leafCache == nil { + c.leafCache = make(map[string]*tls.Certificate) + } + if cert, ok := c.leafCache[host]; ok { + return cert, nil + } + + cert, err := c.signLeaf(host) + if err != nil { + return nil, err + } + c.leafCache[host] = cert + return cert, nil +} + +func (c *CA) signLeaf(host string) (*tls.Certificate, error) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + + serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + return nil, err + } + + tmpl := &x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{ + CommonName: host, + Organization: []string{"mitmux"}, + }, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(leafLifetime), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + IsCA: false, + } + if ip := net.ParseIP(host); ip != nil { + tmpl.IPAddresses = []net.IP{ip} + } else { + tmpl.DNSNames = []string{host} + } + + der, err := x509.CreateCertificate(rand.Reader, tmpl, c.Cert, &key.PublicKey, c.Key) + if err != nil { + return nil, err + } + + leaf, err := x509.ParseCertificate(der) + if err != nil { + return nil, err + } + + return &tls.Certificate{ + Certificate: [][]byte{der, c.Cert.Raw}, + PrivateKey: key, + Leaf: leaf, + }, nil } // Dir returns the directory mitmux stores its CA material in |