From f727e2581a3fe87e216a0cb2144c344a7f3741f2 Mon Sep 17 00:00:00 2001 From: Anton Kovalenko Date: Mon, 20 May 2024 19:32:15 +0300 Subject: [PATCH] reverseproxy: add tls_server_cert_sha256 Unfortunately there *are* some production setups requiring tls_insecure_skip_verify in reverse_proxy, like old devices with outdated firmware. In many such cases, the devices aren't supposed to regenerate or update their certificates. This patch adds tls_server_cert_sha256 directive for reverse_proxy, making MITM impossible even with tls_insecure_skip_verify. --- modules/caddyhttp/reverseproxy/caddyfile.go | 11 +++++ .../caddyhttp/reverseproxy/httptransport.go | 42 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go index 9de7aedd9443..c041ab24285d 100644 --- a/modules/caddyhttp/reverseproxy/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/caddyfile.go @@ -938,6 +938,7 @@ func (h *Handler) FinalizeUnmarshalCaddyfile(helper httpcaddyfile.Helper) error // tls // tls_client_auth | // tls_insecure_skip_verify +// tls_server_cert_sha256 // tls_timeout // tls_trusted_ca_certs // tls_server_name @@ -1101,6 +1102,16 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } h.TLS.InsecureSkipVerify = true + case "tls_server_cert_sha256": + args := d.RemainingArgs() + if len(args) != 1 { + return d.ArgErr() + } + if h.TLS == nil { + h.TLS = new(TLSConfig) + } + h.TLS.ServerCertSha256 = args[0] + case "tls_curves": args := d.RemainingArgs() if len(args) == 0 { diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go index f52a0805373f..994f91d3c50d 100644 --- a/modules/caddyhttp/reverseproxy/httptransport.go +++ b/modules/caddyhttp/reverseproxy/httptransport.go @@ -15,10 +15,13 @@ package reverseproxy import ( + "bytes" "context" + "crypto/sha256" "crypto/tls" "crypto/x509" "encoding/base64" + "encoding/hex" "encoding/json" "fmt" weakrand "math/rand" @@ -506,6 +509,11 @@ type TLSConfig struct { // option except in testing or local development environments. InsecureSkipVerify bool `json:"insecure_skip_verify,omitempty"` + // If non-empty, TLS compares the SHA-256 fingerprint of the + // server certificate to a fixed value, specified as + // hexadecimal string. + ServerCertSha256 string `json:"server_cert_sha256,omitempty"` + // The duration to allow a TLS handshake to a server. Default: No timeout. HandshakeTimeout caddy.Duration `json:"handshake_timeout,omitempty"` @@ -640,6 +648,14 @@ func (t *TLSConfig) MakeTLSClientConfig(ctx caddy.Context) (*tls.Config, error) // throw all security out the window cfg.InsecureSkipVerify = t.InsecureSkipVerify + if t.ServerCertSha256 != "" { + verifier, err := makeFixedCertVerifier(t.ServerCertSha256) + if err != nil { + return nil, err + } + cfg.VerifyPeerCertificate = verifier + } + curvesAdded := make(map[tls.CurveID]struct{}) for _, curveName := range t.Curves { curveID := caddytls.SupportedCurves[curveName] @@ -727,6 +743,32 @@ func sliceContains(haystack []string, needle string) bool { return false } +func makeFixedCertVerifier(fingerprint string) ( + func([][]byte, [][]*x509.Certificate) error, error) { + fpHex := strings.ReplaceAll(fingerprint, ":", "") + fpBytes, err := hex.DecodeString(fpHex) + if err != nil { + return nil, err + } + if len(fpBytes) != 32 { + return nil, fmt.Errorf( + "sha256 fingerprint expected to be 32 bytes, got %v", + len(fpBytes)) + } + errWrongCert := fmt.Errorf("fixed certificate expected: sha256=%v", + fingerprint) + return func(certs [][]byte, vchain [][]*x509.Certificate) error { + if len(certs) < 1 { + return errWrongCert + } + certFp := sha256.Sum256(certs[0]) + if !bytes.Equal(fpBytes, certFp[:]) { + return errWrongCert + } + return nil + }, nil +} + // Interface guards var ( _ caddy.Provisioner = (*HTTPTransport)(nil)