diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go index 9de7aedd944..c041ab24285 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 80a498066cd..99891c373b7 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" @@ -528,6 +531,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"` @@ -662,6 +670,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] @@ -749,6 +765,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)