srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/internal/ca/ca.go
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-01-27 21:47:00 +0200
committersrdusr <[email protected]>2024-01-27 21:47:00 +0200
commitf2f0a2135a202e3e15d2a8cbfbd791aad9b04f3a (patch)
tree3850e9de9355e8ab3b99b68fb69e1ef6df50e0cf /internal/ca/ca.go
parent1125afc47b9d6e68d95d0ffdbb74514f4618e624 (diff)
downloadmitmux-f2f0a2135a202e3e15d2a8cbfbd791aad9b04f3a.tar.gz
mitmux-f2f0a2135a202e3e15d2a8cbfbd791aad9b04f3a.zip
TLS interception: per-host leaf certs, terminate-and-resign MITM, native HTTP/2
Implements build-order step 2. CA gains LeafFor(host), signing and caching per-host leaf certificates on demand. The proxy's CONNECT handler now terminates TLS with the client using a matching leaf cert instead of tunneling raw bytes, and forwards each request upstream over its own independently negotiated TLS connection. Client-side and upstream-side ALPN are negotiated separately rather than one being forced to mirror the other: an http.Transport configured via http2.ConfigureTransport auto-bridges HTTP/1.1 and HTTP/2 on each side independently, so e.g. an HTTP/1.1-only client reaching an HTTP/2-preferring origin still works instead of failing the handshake (caught by testing curl --http1.1 against example.com before this fix). Verified live: plain HTTP passthrough, HTTPS with default (H2) and forced HTTP/1.1 clients, and that requests without the mitmux CA trusted are correctly rejected.
Diffstat (limited to 'internal/ca/ca.go')
-rw-r--r--internal/ca/ca.go85
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