httptest.NewTLSServer uses a cert that is not valid for localhost

What version of Go are you using (go version)?

$ go version
go version go1.12.1 darwin/amd64

Does this issue reproduce with the latest release?

Yes.

What operating system and processor architecture are you using (go env)?

go env Output
$ go env
GOARCH="amd64"
GOBIN=""
GOCACHE="/Users/danielcormier/Library/Caches/go-build"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOOS="darwin"
GOPATH="/Users/danielcormier/go"
GOPROXY=""
GORACE=""
GOROOT="/usr/local/Cellar/go/1.12.1/libexec"
GOTMPDIR=""
GOTOOLDIR="/usr/local/Cellar/go/1.12.1/libexec/pkg/tool/darwin_amd64"
GCCGO="gccgo"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
GOMOD=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/84/_6l41bt970l9fmsrwc_p1gv00000gn/T/go-build114647702=/tmp/go-build -gno-record-gcc-switches -fno-common"

What did you do?

I'm trying to test something that involves cookies that set the Domain attribute. As discussed in #12610, *net/http/cookiejar.Jar won't return cookies for an IP (as per the relevant RFCs). (*httptest.Server).URL has the host set to an IP address (defaults to 127.0.0.1 or ::1).

To do the test I needed, I spun up an *httptest.Server using httptest.NewTLSServer(...), replaced the IP in (*httptest.Server).URL with localhost and attempted to send a request to it with (*httptest.Server).Client().

What did you expect to see?

I expected the httptest.NewTLSServer(...) to use a TLS cert that could be valid for localhost, as well as the loopback IP addresses.

I expected to be able to successfully make an HTTPS request to localhost at the correct port that the *httptest.Server was listening on by using (*httptest.Server).Client().

What did you see instead?

x509: certificate is valid for example.com, not localhost

Example

For completeness, I'm including the suite of tests showing the different behaviors with *cookiejar.Jar and the various combinations of *httptest.Server. The problematic test here is TestCookies/tls/localhost/default_cert (line 174). The test at line 185 shows that the original issue with cookies is resolved if I send requests to localhost with a cert valid for that hostname.

package cookies_test

import (
    "crypto/tls"
    "fmt"
    "io"
    "io/ioutil"
    "net"
    "net/http"
    "net/http/cookiejar"
    "net/http/httptest"
    "net/url"
    "testing"
    "time"
)

func TestCookies(t *testing.T) {
    const (
        routeSetCookie    = "/set-cookie"
        routeExpectCookie = "/expect-cookie"

        cookieName = "token"
    )

    handler := func(tb testing.TB) http.Handler {
        setCookie := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            host, _, err := net.SplitHostPort(r.Host)
            if err != nil {
                host = r.Host
            }

            cookie := &http.Cookie{
                Name:     cookieName,
                Value:    "the value",
                Domain:   host,
                Path:     "/",
                HttpOnly: true,
            }

            tb.Logf("Setting cookie: %s", cookie)

            http.SetCookie(w, cookie)
        })

        expectCookie := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            _, err := r.Cookie(cookieName)
            switch err {
            case nil:
                // Success!

            case http.ErrNoCookie:
                msg := "No cookie"
                tb.Log(msg)
                http.Error(w, msg, http.StatusBadRequest)
                return

            default:
                msg := fmt.Sprintf("Failed to get cookie: %v", err)
                tb.Log(msg)
                http.Error(w, msg, http.StatusInternalServerError)
                return
            }

            tb.Log("The cookie was set")
        })

        mux := http.NewServeMux()
        mux.Handle(routeSetCookie, setCookie)
        mux.Handle(routeExpectCookie, expectCookie)

        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            target := r.RequestURI
            tb.Logf("---- START %s", target)
            mux.ServeHTTP(w, r)
            tb.Logf("---- END %s", target)
        })
    }

    testCookies := func(tb testing.TB, svr *httptest.Server) {
        jar, err := cookiejar.New(nil)
        if err != nil {
            tb.Fatal(err)
        }

        httpClient := svr.Client()
        httpClient.Timeout = 1 * time.Second
        httpClient.Jar = jar

        resp, err := httpClient.Get(svr.URL + routeExpectCookie)
        if err != nil {
            tb.Fatal(err)
        }

        defer func() {
            io.Copy(ioutil.Discard, resp.Body)
            resp.Body.Close()
        }()

        if resp.StatusCode != http.StatusBadRequest {
            body, _ := ioutil.ReadAll(resp.Body)
            tb.Fatalf("Should not have cookie: %s\n%s", resp.Status, body)
        }

        resp, err = httpClient.Get(svr.URL + routeSetCookie)
        if err != nil {
            tb.Fatal(err)
        }

        if resp.StatusCode != http.StatusOK {
            body, _ := ioutil.ReadAll(resp.Body)
            tb.Fatalf("%s\n%s", resp.Status, body)
        }

        resp, err = httpClient.Get(svr.URL + routeExpectCookie)
        if err != nil {
            tb.Fatal(err)
        }

        if resp.StatusCode != http.StatusOK {
            body, _ := ioutil.ReadAll(resp.Body)
            tb.Fatalf("%s\n%s", resp.Status, body)
        }
    }

    useLocalhost := func(tb testing.TB, svr *httptest.Server) {
        svrURL, err := url.Parse(svr.URL)
        if err != nil {
            tb.Fatal(err)
        }

        svrURL.Host = net.JoinHostPort("localhost", svrURL.Port())

        svr.URL = svrURL.String()
    }

    t.Run("no tls", func(t *testing.T) {
        t.Run("ip", func(t *testing.T) {
            // This fails because `cookiejar.CookieJar` follows the RFCs and drops the `Domain` of a
            // cookie where its set to an IP, rather than a domain. We'll skip it, but it's here if
            // you want to see for yourself.
            t.SkipNow()

            svr := httptest.NewServer(handler(t))
            defer svr.Close()

            testCookies(t, svr)
        })

        t.Run("localhost", func(t *testing.T) {
            // This works.

            svr := httptest.NewServer(handler(t))
            defer svr.Close()

            useLocalhost(t, svr)
            testCookies(t, svr)
        })
    })

    t.Run("tls", func(t *testing.T) {
        t.Run("ip", func(t *testing.T) {
            // This fails because `cookiejar.CookieJar` follows the RFCs and drops the `Domain` of a
            // cookie where its set to an IP, rather than a domain. We'll skip it, but it's here if
            // you want to see for yourself.
            t.SkipNow()

            svr := httptest.NewTLSServer(handler(t))
            defer svr.Close()

            testCookies(t, svr)
        })

        t.Run("localhost", func(t *testing.T) {
            t.Run("default cert", func(t *testing.T) {
                // This fails because the cert `httptest.NewTLSServer` serves up is valid for
                // 127.0.0.1, ::1, and examlple.com. Not localhost.

                svr := httptest.NewTLSServer(handler(t))
                defer svr.Close()

                useLocalhost(t, svr)
                testCookies(t, svr)
            })

            t.Run("localhost cert", func(t *testing.T) {
                // This works. But, you need to generate your own cert for localhost.

                svr := httptest.NewUnstartedServer(handler(t))

                certPEM := []byte(`-----BEGIN CERTIFICATE-----
MIIDJTCCAg2gAwIBAgIQas3l/GRJkOGvAZP1CLIvRzANBgkqhkiG9w0BAQsFADAb
MRkwFwYDVQQKExBBY21lIENvcnBvcmF0aW9uMCAXDTAwMDEwMTAwMDAwMFoYDzIx
MDAwMTAxMDAwMDAwWjAbMRkwFwYDVQQKExBBY21lIENvcnBvcmF0aW9uMIIBIjAN
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn46okXbPHDmuwHMcQHyPtDl1qoKL
WA/U5x1VcLHGOR6vQKjkNUXbW0yU0HYcyBtmr5gugdmlaFvCRlMSaG1pyC5iCCha
HlTyFyaZi0o2zGT34fS8Jj2WUKE/pR9pOqEoWx8UezBHw/NBZjGCjKe4ASzCQqbn
KA6DxQfRBypU+OFAIK3KsRP6Xvwqd2N/a5FybL9ixKYNbAj7b2vAhW7NIWw++m2T
Hif+bTsEhLAGUG3KGW9OGcJiAewyZb4DgZPgE1ourEud9goVbcCTZBYbpV3U0tZa
XxqJIOfhsfCe4fDcqe1Hspq47SLdvP8FP/qKTbFOqoA/NAlrmboxOw+mXwIDAQAB
o2MwYTAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0T
AQH/BAIwADAsBgNVHREEJTAjgglsb2NhbGhvc3SHEAAAAAAAAAAAAAAAAAAAAAGH
BH8AAAEwDQYJKoZIhvcNAQELBQADggEBAG2cVK1TZvHoaiqA40QEjKehKqq4vKLc
At/FrITEgvNTIkvguEvLw5wsUO/3Nt/atjWtFdSJCLWCLzrgiLOLtJubkrDzzbus
/OsI0cf/fMTyCnjt64efSz2RDPPllRbJd3zZBkuOWhPoxx/Sz0VRvQKGFb9mvPoI
PTZ22ugwZdS/3PnMEoVO46iQumGARXQEbiGApeXPObK0E6Fs7pqwomU9Ny2XsyXS
je06pfouDv8UlLzZLY/fVJLHN6aM7odw5iPp2p7ttFdgn1l/LVlZVX9FBwegHXet
5OSC7pDc+kbLg1cJE8/7dF47VBEVKSvr5ldgRuvDtEf4PupKRl4rhik=
-----END CERTIFICATE-----`)

                keyPEM := []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAn46okXbPHDmuwHMcQHyPtDl1qoKLWA/U5x1VcLHGOR6vQKjk
NUXbW0yU0HYcyBtmr5gugdmlaFvCRlMSaG1pyC5iCChaHlTyFyaZi0o2zGT34fS8
Jj2WUKE/pR9pOqEoWx8UezBHw/NBZjGCjKe4ASzCQqbnKA6DxQfRBypU+OFAIK3K
sRP6Xvwqd2N/a5FybL9ixKYNbAj7b2vAhW7NIWw++m2THif+bTsEhLAGUG3KGW9O
GcJiAewyZb4DgZPgE1ourEud9goVbcCTZBYbpV3U0tZaXxqJIOfhsfCe4fDcqe1H
spq47SLdvP8FP/qKTbFOqoA/NAlrmboxOw+mXwIDAQABAoIBAQCLVxlNF5WdT56W
ALDOfDk/KeLhSmoIOKM0RkDETuwOHAbuj8/j2iLLo6BeQJe4BX3yoRMUYQ77iQ6r
PYbY3ZxAroj8GMlCrepRX2s94kziyNZVZNYfCy/HMFqViE3sXqsQkJ7hSfOSY1Bc
v6YD0cB2fjET5g/+wlY+7imUeVqFkUd5+CuSa4MVheWRiXCFydm+GdUMbHGJauZk
KYSz6oE5vXkDCbcjpyH2Ay7QuHiE00wI2DqsvkZJy8et+XgYL8iNj10JulnDXJg6
MmSf0ZsDfhfJW9AQDzZjXfbSRsskztnehN4UcJH8enLaLbanlYisPpIsj9jpqLwt
EDcsHX+ZAoGBAMC0nO9MSoxu4Q9WP8Lq5qT1QYrRvmSO9mHz+4wmYvConofucqlK
M6HXD/qXIU8pTHZ5WHjnnEyNOvVdsK/P6uYkdqWRXig8/zgoi6DGuujlthJ7BKYW
I7Fvh2z217p3y0IHQvHYjxQk0ag9kOxkdqiYW6WxNcUj2QeXgDkEjcddAoGBANP2
0XI1tEm+IThXHnnT8DYci4xIi9JBvkWgq+atw8ZNicNfN+a0RX5f42fFUCkGE3F6
JgQgSwIAr6zbLKH8RzwU/V5dpO7vuPrgsCRwFsovKAhyCpW0PflJXIKPY6xrbRnc
t2cSOitZzWBdGQJQANGcd+qdGDG/NBcsYdchKfTrAoGAMS/ovsviW2YR3DBPphj/
NivDxwMybchv6yCznFpP9s2TaW7bpYpjE3Qph/T7c5E/Cx5+Dp5Prtp9qhN3/eg8
NPIptqkcN3kaS+NNgIQ5QSkhCCaOUTZldezZzF5VQitBnmDsHX8BRkr/mMneK/iY
sP/ypKBO8TrtMprhB6y546ECgYEAgFXwejYJ8pwrgPE+goTP6/NcipNiFOu5SG7/
pauP3YEU6DW+ovCDIwDrrujIoA4Nt6c9XUIwKAZCV2Zcn7cfakFLJteMBR8f4MYp
3+X95mym0HY78mgvHcBNQr+OmdZxODdq0/01OwokTzQO8FeAJ2mVMXfsLjKWV3GH
y7lIrgECgYEAiZIEx3fBc3TBcaZb5vbWyAQyfC5vgI0N25ZaIwoG5g6CkjKt8nfH
Xfl1da9pWbcAgRLlq+XhqAJQdUjZ0NfKeWSQxT8TQob8ZfiAHXwjTf20qFrarsPl
jVyKqKuj7Vl7evexIhY03RL6S/koyDGJWdUt9myZB6mdFJBBFQIuv8U=
-----END RSA PRIVATE KEY-----`)

                cert, err := tls.X509KeyPair(certPEM, keyPEM)
                if err != nil {
                    t.Fatal(err)
                }

                svr.TLS = &tls.Config{
                    Certificates: []tls.Certificate{cert},
                }

                svr.StartTLS()
                defer svr.Close()

                useLocalhost(t, svr)
                testCookies(t, svr)
            })
        })
    })
}

I attempted to have a conversation about the hosts included in the cert used by httptest.NewTLSServer() in the golang-nuts group, but it went nowhere.

Comment From: ohir

This is a key pair that is available to the general public. Ie. its private part is known to all. Making it match on localhost or loopback interface would be a huge security hole for millions of developers who would add it to the trusted certs store then their machines would be then susceptible to a wide class of threats via localhost MITM.

Please follow howto and make a cert for yourself. Note the minica link down the page.

Comment From: dcormier

Making it match on localhost or loopback interface would be a huge security hole for millions of developers who would add it to the trusted certs store then their machines would be then susceptible to a wide class of threats via localhost MITM.

It already matches IPv4 and IPv6 loopback addresses.

Adding a cert from a well-known key pair to ones trusted certs is unwise for the reasons you mentioned. Safer to generate your own to trust.

Comment From: agnivade

@bradfitz

Comment From: bradfitz

Dup of #30774, it seems? That's not supported. The httptest.NewTLSServer is for testing and you can only hit it with the URL it gives you.

Comment From: dcormier

Dup of #30774, it seems? That's not supported. The httptest.NewTLSServer is for testing and you can only hit it with the URL it gives you.

Then this is a feature request, I guess. The goal being to make it easier use httptest.NewTLSServer for testing with cookies.

Comment From: bradfitz

Feature request SGTM. I'm fine if the returned Client would have a special-cased dialer that makes a certain name(s) ("*.test.example") dial towards that IP, and make the TLS work.

Anybody want to work on it?

Comment From: dcormier

Anybody want to work on it?

I'm interested.

Comment From: gopherbot

Change https://golang.org/cl/182917 mentions this issue: net/http/httptest: make it possible to use a Server (TLS or not) to test cookies

Comment From: gopherbot

Change https://go.dev/cl/666855 mentions this issue: net/http/httptest: redirect example.com requests to server