mirror of
https://github.com/dstotijn/hetty.git
synced 2025-07-01 18:47:29 -04:00
Add MITM proxy for HTTPS requests
This commit is contained in:
176
cert.go
Normal file
176
cert.go
Normal file
@ -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
|
||||||
|
}
|
28
main.go
28
main.go
@ -2,12 +2,34 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"flag"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
caCertFile = flag.String("cert", "", "CA certificate file path")
|
||||||
|
caKeyFile = flag.String("key", "", "CA private key file path")
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
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{
|
s := &http.Server{
|
||||||
Addr: ":8080",
|
Addr: ":8080",
|
||||||
@ -15,8 +37,8 @@ func main() {
|
|||||||
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){}, // Disable HTTP/2
|
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 {
|
if err != nil && err != http.ErrServerClosed {
|
||||||
log.Fatalf("HTTP server closed: %v", err)
|
log.Fatalf("[FATAL] HTTP server closed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
@cert = $HOME/.ssh/gurp_cert.pem
|
||||||
|
@key = $HOME/.ssh/gurp_key.pem
|
||||||
|
|
||||||
**/*.go {
|
**/*.go {
|
||||||
daemon +sigterm: go run .
|
daemon +sigterm: go run . -cert @cert -key @key
|
||||||
}
|
}
|
47
net.go
Normal file
47
net.go
Normal file
@ -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{}{}
|
||||||
|
}
|
102
proxy.go
102
proxy.go
@ -1,79 +1,115 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"context"
|
||||||
|
"crypto"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"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.
|
// Proxy is used to forward HTTP requests.
|
||||||
type Proxy struct {
|
type Proxy struct {
|
||||||
rp httputil.ReverseProxy
|
certConfig *CertConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewProxy returns a new Proxy.
|
// NewProxy returns a new Proxy.
|
||||||
func NewProxy() *Proxy {
|
func NewProxy(ca *x509.Certificate, key crypto.PrivateKey) (*Proxy, error) {
|
||||||
return &Proxy{
|
certConfig, err := NewCertConfig(ca, key)
|
||||||
rp: httputil.ReverseProxy{
|
if err != nil {
|
||||||
Director: func(r *http.Request) {
|
return nil, err
|
||||||
log.Printf("Director handled URL: %v", r.URL)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return &Proxy{
|
||||||
|
certConfig: certConfig,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
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 {
|
if r.Method == http.MethodConnect {
|
||||||
p.handleConnect(w, r)
|
p.handleConnect(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
p.rp.ServeHTTP(w, r)
|
httpHandler.ServeHTTP(w, r)
|
||||||
log.Printf("ServeHTTP: Finished (host: %v, url: %v", r.Host, r.URL)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
func (p *Proxy) handleConnect(w http.ResponseWriter, r *http.Request) {
|
||||||
hj, ok := w.(http.Hijacker)
|
hj, ok := w.(http.Hijacker)
|
||||||
if !ok {
|
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)
|
writeError(w, r, http.StatusServiceUnavailable)
|
||||||
return
|
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)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
// clientConn is the TCP connection to the client.
|
|
||||||
clientConn, _, err := hj.Hijack()
|
clientConn, _, err := hj.Hijack()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("handleConnect: Hijack failed: %v", err)
|
log.Printf("[ERROR] Hijacking client connection failed: %v", err)
|
||||||
writeError(w, r, http.StatusServiceUnavailable)
|
writeError(w, r, http.StatusServiceUnavailable)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer clientConn.Close()
|
defer clientConn.Close()
|
||||||
|
|
||||||
errc := make(chan error, 1)
|
// Secure connection to client.
|
||||||
go tunnelData(destConn, clientConn, errc)
|
clientConn, err = p.clientTLSConn(clientConn)
|
||||||
go tunnelData(clientConn, destConn, errc)
|
if err != nil {
|
||||||
<-errc
|
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) {
|
func (p *Proxy) clientTLSConn(conn net.Conn) (*tls.Conn, error) {
|
||||||
_, err := io.Copy(dst, src)
|
tlsConfig := p.certConfig.TLSConfig()
|
||||||
errc <- err
|
|
||||||
|
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) {
|
func writeError(w http.ResponseWriter, r *http.Request, code int) {
|
||||||
|
Reference in New Issue
Block a user