From ef4f829572bc324d2f789143a96e8095e9c1dcce Mon Sep 17 00:00:00 2001 From: David Stotijn Date: Sun, 24 Nov 2019 00:14:49 +0100 Subject: [PATCH] Add MITM proxy for HTTPS requests --- cert.go | 176 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ go.sum | 0 main.go | 28 ++++++++- modd.conf | 5 +- net.go | 47 +++++++++++++++ proxy.go | 102 +++++++++++++++++++++---------- 6 files changed, 321 insertions(+), 37 deletions(-) create mode 100644 cert.go create mode 100644 go.sum create mode 100644 net.go diff --git a/cert.go b/cert.go new file mode 100644 index 0000000..ff432a8 --- /dev/null +++ b/cert.go @@ -0,0 +1,176 @@ +package main + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "errors" + "math/big" + "net" + "time" +) + +// MaxSerialNumber is the upper boundary that is used to create unique serial +// numbers for the certificate. This can be any unsigned integer up to 20 +// bytes (2^(8*20)-1). +var MaxSerialNumber = big.NewInt(0).SetBytes(bytes.Repeat([]byte{255}, 20)) + +// CertConfig is a set of configuration values that are used to build TLS configs +// capable of MITM +type CertConfig struct { + ca *x509.Certificate + caPriv crypto.PrivateKey + priv *rsa.PrivateKey + keyID []byte +} + +// NewCertConfig creates a MITM config using the CA certificate and +// private key to generate on-the-fly certificates. +func NewCertConfig(ca *x509.Certificate, caPrivKey crypto.PrivateKey) (*CertConfig, error) { + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, err + } + pub := priv.Public() + + // Subject Key Identifier support for end entity certificate. + // https://www.ietf.org/rfc/rfc3280.txt (section 4.2.1.2) + pkixPubKey, err := x509.MarshalPKIXPublicKey(pub) + if err != nil { + return nil, err + } + h := sha1.New() + h.Write(pkixPubKey) + keyID := h.Sum(nil) + + return &CertConfig{ + ca: ca, + caPriv: caPrivKey, + priv: priv, + keyID: keyID, + }, nil +} + +// NewCA creates a new CA certificate and associated private key. +func NewCA(name, organization string, validity time.Duration) (*x509.Certificate, *rsa.PrivateKey, error) { + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, err + } + pub := priv.Public() + + // Subject Key Identifier support for end entity certificate. + // https://www.ietf.org/rfc/rfc3280.txt (section 4.2.1.2) + pkixpub, err := x509.MarshalPKIXPublicKey(pub) + if err != nil { + return nil, nil, err + } + h := sha1.New() + h.Write(pkixpub) + keyID := h.Sum(nil) + + // TODO: keep a map of used serial numbers to avoid potentially reusing a + // serial multiple times. + serial, err := rand.Int(rand.Reader, MaxSerialNumber) + if err != nil { + return nil, nil, err + } + + tmpl := &x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{ + CommonName: name, + Organization: []string{organization}, + }, + SubjectKeyId: keyID, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + NotBefore: time.Now().Add(-24 * time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + DNSNames: []string{name}, + IsCA: true, + } + + raw, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, pub, priv) + if err != nil { + return nil, nil, err + } + + // Parse certificate bytes so that we have a leaf certificate. + x509c, err := x509.ParseCertificate(raw) + if err != nil { + return nil, nil, err + } + + return x509c, priv, nil +} + +// TLSConfig returns a *tls.Config that will generate certificates on-the-fly using +// the SNI extension in the TLS ClientHello. +func (c *CertConfig) TLSConfig() *tls.Config { + return &tls.Config{ + GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { + if clientHello.ServerName == "" { + return nil, errors.New("missing server name (SNI)") + } + return c.cert(clientHello.ServerName) + }, + NextProtos: []string{"http/1.1"}, + } +} + +func (c *CertConfig) cert(hostname string) (*tls.Certificate, error) { + // Remove the port if it exists. + host, _, err := net.SplitHostPort(hostname) + if err == nil { + hostname = host + } + + serial, err := rand.Int(rand.Reader, MaxSerialNumber) + if err != nil { + return nil, err + } + + tmpl := &x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{ + CommonName: hostname, + Organization: []string{"Gurp"}, + }, + SubjectKeyId: c.keyID, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + NotBefore: time.Now().Add(-24 * time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + } + + if ip := net.ParseIP(hostname); ip != nil { + tmpl.IPAddresses = []net.IP{ip} + } else { + tmpl.DNSNames = []string{hostname} + } + + raw, err := x509.CreateCertificate(rand.Reader, tmpl, c.ca, c.priv.Public(), c.caPriv) + if err != nil { + return nil, err + } + + // Parse certificate bytes so that we have a leaf certificate. + x509c, err := x509.ParseCertificate(raw) + if err != nil { + return nil, err + } + + return &tls.Certificate{ + Certificate: [][]byte{raw, c.ca.Raw}, + PrivateKey: c.priv, + Leaf: x509c, + }, nil +} diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/main.go b/main.go index c886107..cd66a91 100644 --- a/main.go +++ b/main.go @@ -2,12 +2,34 @@ package main import ( "crypto/tls" + "crypto/x509" + "flag" "log" "net/http" ) +var ( + caCertFile = flag.String("cert", "", "CA certificate file path") + caKeyFile = flag.String("key", "", "CA private key file path") +) + func main() { - proxy := NewProxy() + flag.Parse() + + tlsCA, err := tls.LoadX509KeyPair(*caCertFile, *caKeyFile) + if err != nil { + log.Fatalf("[FATAL] Could not load CA key pair: %v", err) + } + + caCert, err := x509.ParseCertificate(tlsCA.Certificate[0]) + if err != nil { + log.Fatalf("[FATAL] Could not parse CA: %v", err) + } + + proxy, err := NewProxy(caCert, tlsCA.PrivateKey) + if err != nil { + log.Fatalf("[FATAL] Could not create Proxy: %v", err) + } s := &http.Server{ Addr: ":8080", @@ -15,8 +37,8 @@ func main() { TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){}, // Disable HTTP/2 } - err := s.ListenAndServe() + err = s.ListenAndServe() if err != nil && err != http.ErrServerClosed { - log.Fatalf("HTTP server closed: %v", err) + log.Fatalf("[FATAL] HTTP server closed: %v", err) } } diff --git a/modd.conf b/modd.conf index dbe567b..2133a0b 100644 --- a/modd.conf +++ b/modd.conf @@ -1,3 +1,6 @@ +@cert = $HOME/.ssh/gurp_cert.pem +@key = $HOME/.ssh/gurp_key.pem + **/*.go { - daemon +sigterm: go run . + daemon +sigterm: go run . -cert @cert -key @key } \ No newline at end of file diff --git a/net.go b/net.go new file mode 100644 index 0000000..3104839 --- /dev/null +++ b/net.go @@ -0,0 +1,47 @@ +package main + +import ( + "errors" + "net" +) + +var ErrAlreadyAccepted = errors.New("listener already accepted") + +// OnceListener implements net.Listener. +// +// Accepts a connection once and returns an error on subsequent +// attempts. +type OnceAcceptListener struct { + c net.Conn +} + +func (l *OnceAcceptListener) Accept() (net.Conn, error) { + if l.c == nil { + return nil, ErrAlreadyAccepted + } + + c := l.c + l.c = nil + + return c, nil +} + +func (l *OnceAcceptListener) Close() error { + return nil +} + +func (l *OnceAcceptListener) Addr() net.Addr { + return l.c.LocalAddr() +} + +// ConnNotify embeds net.Conn and adds a channel field for notifying +// that the connection was closed. +type ConnNotify struct { + net.Conn + closed chan struct{} +} + +func (c *ConnNotify) Close() { + c.Conn.Close() + c.closed <- struct{}{} +} diff --git a/proxy.go b/proxy.go index 6536947..b1d7e98 100644 --- a/proxy.go +++ b/proxy.go @@ -1,79 +1,115 @@ package main import ( - "io" + "context" + "crypto" + "crypto/tls" + "crypto/x509" + "fmt" "log" "net" "net/http" "net/http/httputil" ) +var httpHandler = &httputil.ReverseProxy{ + Director: func(r *http.Request) { + r.URL.Host = r.Host + r.URL.Scheme = "http" + }, + ErrorHandler: proxyErrorHandler, +} + +var httpsHandler = &httputil.ReverseProxy{ + Director: func(r *http.Request) { + r.URL.Host = r.Host + r.URL.Scheme = "https" + }, + ErrorHandler: proxyErrorHandler, +} + +func proxyErrorHandler(w http.ResponseWriter, r *http.Request, err error) { + if err == context.Canceled { + return + } + log.Printf("[ERROR]: Proxy error: %v", err) + w.WriteHeader(http.StatusBadGateway) +} + // Proxy is used to forward HTTP requests. type Proxy struct { - rp httputil.ReverseProxy + certConfig *CertConfig } // NewProxy returns a new Proxy. -func NewProxy() *Proxy { - return &Proxy{ - rp: httputil.ReverseProxy{ - Director: func(r *http.Request) { - log.Printf("Director handled URL: %v", r.URL) - }, - }, +func NewProxy(ca *x509.Certificate, key crypto.PrivateKey) (*Proxy, error) { + certConfig, err := NewCertConfig(ca, key) + if err != nil { + return nil, err } + + return &Proxy{ + certConfig: certConfig, + }, nil } func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { - log.Printf("ServeHTTP: Received request (host: %v, url: %v", r.Host, r.URL) - if r.Method == http.MethodConnect { p.handleConnect(w, r) return } - p.rp.ServeHTTP(w, r) - log.Printf("ServeHTTP: Finished (host: %v, url: %v", r.Host, r.URL) + httpHandler.ServeHTTP(w, r) } +// handleConnect hijacks the incoming HTTP request and sets up an HTTP tunnel. +// During the TLS handshake with the client, we use the proxy's CA config to +// create a certificate on-the-fly. func (p *Proxy) handleConnect(w http.ResponseWriter, r *http.Request) { hj, ok := w.(http.Hijacker) if !ok { - log.Printf("handleConnect: ResponseWriter is not a http.Hijacker (type: %T)", w) + log.Printf("[ERROR] handleConnect: ResponseWriter is not a http.Hijacker (type: %T)", w) writeError(w, r, http.StatusServiceUnavailable) return } - // destConn is the TCP connection to the destination web server of the - // proxied HTTP request. - destConn, err := net.Dial("tcp", r.Host) - if err != nil { - log.Printf("handleConnect: Connect to destination host failed: %v", err) - writeError(w, r, http.StatusBadGateway) - return - } - defer destConn.Close() - w.WriteHeader(http.StatusOK) - // clientConn is the TCP connection to the client. clientConn, _, err := hj.Hijack() if err != nil { - log.Printf("handleConnect: Hijack failed: %v", err) + log.Printf("[ERROR] Hijacking client connection failed: %v", err) writeError(w, r, http.StatusServiceUnavailable) return } defer clientConn.Close() - errc := make(chan error, 1) - go tunnelData(destConn, clientConn, errc) - go tunnelData(clientConn, destConn, errc) - <-errc + // Secure connection to client. + clientConn, err = p.clientTLSConn(clientConn) + if err != nil { + log.Printf("[ERROR] Securing client connection failed: %v", err) + return + } + clientConnNotify := ConnNotify{clientConn, make(chan struct{})} + + l := &OnceAcceptListener{clientConnNotify.Conn} + + err = http.Serve(l, httpsHandler) + if err != nil && err != ErrAlreadyAccepted { + log.Printf("[ERROR] Serving HTTP request failed: %v", err) + } + <-clientConnNotify.closed } -func tunnelData(dst, src io.ReadWriter, errc chan<- error) { - _, err := io.Copy(dst, src) - errc <- err +func (p *Proxy) clientTLSConn(conn net.Conn) (*tls.Conn, error) { + tlsConfig := p.certConfig.TLSConfig() + + tlsConn := tls.Server(conn, tlsConfig) + if err := tlsConn.Handshake(); err != nil { + tlsConn.Close() + return nil, fmt.Errorf("handshake error: %v", err) + } + + return tlsConn, nil } func writeError(w http.ResponseWriter, r *http.Request, code int) {