Mocking HTTPS responses in Go - testing

I'm trying to write tests for a package that makes requests to a web service. I'm running into issues probably due to my lack of understanding of TLS.
Currently my test looks something like this:
func TestSimple() {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
fmt.Fprintf(w, `{ "fake" : "json data here" }`)
}))
transport := &http.Transport{
Proxy: func(req *http.Request) (*url.URL, error) {
return url.Parse(server.URL)
},
}
// Client is the type in my package that makes requests
client := Client{
c: http.Client{Transport: transport},
}
client.DoRequest() // ...
}
My package has a package variable (I'd like for it to be a constant..) for the base address of the web service to query. It is an https URL. The test server I created above is plain HTTP, no TLS.
By default, my test fails with the error "tls: first record does not look like a TLS handshake."
To get this to work, my tests change the package variable to a plain http URL instead of https before making the query.
Is there any way around this? Can I make the package variable a constant (https), and either set up a http.Transport that "downgrades" to unencrypted HTTP, or use httptest.NewTLSServer() instead?
(When I try to use NewTLSServer() I get "http: TLS handshake error from 127.0.0.1:45678: tls: oversized record received with length 20037")

Most of the behavior in net/http can be mocked, extended, or altered. Although http.Client is a concrete type that implements HTTP client semantics, all of its fields are exported and may be customized.
The Client.Transport field, in particular, may be replaced to make the Client do anything from using custom protocols (such as ftp:// or file://) to connecting directly to local handlers (without generating HTTP protocol bytes or sending anything over the network).
The client functions, such as http.Get, all utilize the exported http.DefaultClient package variable (which you may modify), so code that utilizes these convenience functions does not, for example, have to be changed to call methods on a custom Client variable. Note that while it would be unreasonable to modify global behavior in a publicly-available library, it's very useful to do so in applications and tests (including library tests).
http://play.golang.org/p/afljO086iB contains a custom http.RoundTripper that rewrites the request URL so that it'll be routed to a locally hosted httptest.Server, and another example that directly passes the request to an http.Handler, along with a custom http.ResponseWriter implementation, in order to create an http.Response. The second approach isn't as diligent as the first (it doesn't fill out as many fields in the Response value) but is more efficient, and should be compatible enough to work with most handlers and client callers.
The above-linked code is included below as well:
package main
import (
"fmt"
"io"
"log"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path"
"strings"
)
func Handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello %s\n", path.Base(r.URL.Path))
}
func main() {
s := httptest.NewServer(http.HandlerFunc(Handler))
u, err := url.Parse(s.URL)
if err != nil {
log.Fatalln("failed to parse httptest.Server URL:", err)
}
http.DefaultClient.Transport = RewriteTransport{URL: u}
resp, err := http.Get("https://google.com/path-one")
if err != nil {
log.Fatalln("failed to send first request:", err)
}
fmt.Println("[First Response]")
resp.Write(os.Stdout)
fmt.Print("\n", strings.Repeat("-", 80), "\n\n")
http.DefaultClient.Transport = HandlerTransport{http.HandlerFunc(Handler)}
resp, err = http.Get("https://google.com/path-two")
if err != nil {
log.Fatalln("failed to send second request:", err)
}
fmt.Println("[Second Response]")
resp.Write(os.Stdout)
}
// RewriteTransport is an http.RoundTripper that rewrites requests
// using the provided URL's Scheme and Host, and its Path as a prefix.
// The Opaque field is untouched.
// If Transport is nil, http.DefaultTransport is used
type RewriteTransport struct {
Transport http.RoundTripper
URL *url.URL
}
func (t RewriteTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// note that url.URL.ResolveReference doesn't work here
// since t.u is an absolute url
req.URL.Scheme = t.URL.Scheme
req.URL.Host = t.URL.Host
req.URL.Path = path.Join(t.URL.Path, req.URL.Path)
rt := t.Transport
if rt == nil {
rt = http.DefaultTransport
}
return rt.RoundTrip(req)
}
type HandlerTransport struct{ h http.Handler }
func (t HandlerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
r, w := io.Pipe()
resp := &http.Response{
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: make(http.Header),
Body: r,
Request: req,
}
ready := make(chan struct{})
prw := &pipeResponseWriter{r, w, resp, ready}
go func() {
defer w.Close()
t.h.ServeHTTP(prw, req)
}()
<-ready
return resp, nil
}
type pipeResponseWriter struct {
r *io.PipeReader
w *io.PipeWriter
resp *http.Response
ready chan<- struct{}
}
func (w *pipeResponseWriter) Header() http.Header {
return w.resp.Header
}
func (w *pipeResponseWriter) Write(p []byte) (int, error) {
if w.ready != nil {
w.WriteHeader(http.StatusOK)
}
return w.w.Write(p)
}
func (w *pipeResponseWriter) WriteHeader(status int) {
if w.ready == nil {
// already called
return
}
w.resp.StatusCode = status
w.resp.Status = fmt.Sprintf("%d %s", status, http.StatusText(status))
close(w.ready)
w.ready = nil
}

The reason you're getting the error http: TLS handshake error from 127.0.0.1:45678: tls: oversized record received with length 20037 is because https requires a domain name (not an IP Address). Domain names are SSL certificates are assigned to.
Start the httptest server in TLS mode with your own certs
cert, err := tls.LoadX509KeyPair("cert.pem", "key.pem")
if err != nil {
log.Panic("bad server certs: ", err)
}
certs := []tls.Certificate{cert}
server = httptest.NewUnstartedServer(router)
server.TLS = &tls.Config{Certificates: certs}
server.StartTLS()
serverPort = ":" + strings.Split(server.URL, ":")[2] // it's always https://127.0.0.1:<port>
server.URL = "https://sub.domain.com" + serverPort
To provide a valid SSL certificate for a connection are the options of:
Not supplying a cert and key
Supplying a self-signed cert and key
Supplying a real valid cert and key
No Cert
If you don't supply your own cert, then an example.com cert is loaded as default.
Self-Signed Cert
To create a testing cert can use the included self-signed cert generator at $GOROOT/src/crypto/tls/generate_cert.go --host "*.domain.name"
You'll get x509: certificate signed by unknown authority warnings because it's self-signed so you'll need to have your client skip those warnings, by adding the following to your http.Transport field:
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}
Valid Real Cert
Finally, if you're going to use a real cert, then save the valid cert and key where they can be loaded.
The key here is to use server.URL = https://sub.domain.com to supply your own domain.

From Go 1.9+ you can use func (s *Server) Client() *http.Client in the httptest package:
Client returns an HTTP client configured for making requests to the server. It is configured to trust the server's TLS test certificate and will close its idle connections on Server.Close.
Example from the package:
package main
import (
"fmt"
"io"
"log"
"net/http"
"net/http/httptest"
)
func main() {
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, client")
}))
defer ts.Close()
client := ts.Client()
res, err := client.Get(ts.URL)
if err != nil {
log.Fatal(err)
}
greeting, err := io.ReadAll(res.Body)
res.Body.Close()
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s", greeting)
}

Related

Understanding on Mutual TLS , client config with servername

I am trying to understand the mutual TLS working, I have the following example:
I have a client who wants to connect to server "svc1.example.com"
but the server has a
server certificate with a commonName as "svc1.example.cloud" and a SAN
as "svc.example.test.cloud".
Now when I make a GET request, I get the following:
x509: certificate is valid for svc.example.test.cloud, not svc1.example.com.
So, my question is should I make a the TLS clientConfig changes to include the servername? or should I add a custom verifyPeerCertificate function in the TLS client config, something like below?
Please, let me know, what should be the Servername and what should I check for in the verifyPeerCertificate function.
func customverify(customCName func(*x509.Certificate) bool) func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
if customCName == nil {
return nil
}
return func(_ [][]byte, verifiedChains [][]*x509.Certificate) error {
for _, certs := range verifiedChains {
leaf := certs[0]
if customCName(leaf) {
return nil
}
}
return fmt.Errorf("client identity verification failed")
}
}
func configureClient(certFile, keyFile string) (*http.Client, error) {
certpool, err := addRootCA()
if err != nil {
return nil, err
}
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, err
}
transport := ytls.NewClientTransport()
transport.TLSClientConfig.Certificates = []tls.Certificate{cert}
transport.TLSClientConfig.RootCAs = certpool
//transport.TLSClientConfig.ServerName = expectedCName
transport.TLSClientConfig.VerifyPeerCertificate = customverify(func(cert *x509.Certificate) bool {
return cert.Subject.CommonName == "svc1.example.cloud"
})
httpClient := &http.Client{Transport: transport}
return httpClient, nil
}
Since x509: certificate is valid for svc.example.test.cloud, so transport.TLSClientConfig.ServerName = "svc.example.test.cloud"
From https://golang.org/pkg/crypto/tls/#Config
VerifyPeerCertificate, if not nil, is called after normal
certificate verification by either a TLS client or server. It
receives the raw ASN.1 certificates provided by the peer and also
any verified chains that normal processing found. If it returns a
non-nil error, the handshake is aborted and that error results.
If normal verification fails then the handshake will abort before
considering this callback. If normal verification is disabled by
setting InsecureSkipVerify, or (for a server) when ClientAuth is
RequestClientCert or RequireAnyClientCert, then this callback will
be considered but the verifiedChains argument will always be nil.
VerifyPeerCertificate func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error
So if normal verification fails, then VerifyPeerCertificate won't get called. Also if normal verification is passed, i don't think you need this extra check VerifyPeerCertificate.

Stripping SNI information from TLS WebSocket connections

I find myself needing to set up a WebSocket connection in a hostile environment in which a firewall sniffs SNI information from TLS which I'd rather it didn't. In my particular case, the WebSocket server does not use SNI for request handling, so as such, the SNI part of the handshake could be safely removed.
My question then becomes: In the golang.org WebSocket package, golang.org/x/net/websocket, what is the simplest way to strip SNI information while retaining validation of the provided chain?
The best I have been able to come up with is to simply replace the hostname of the URL to be dialled with its corresponding IP. This causes crypto/tls to never add the problematic SNI information, but, in the solution I was able to come up with, a custom validator ends up having to be provided to validate the chain:
func dial(url string, origin string) (*websocket.Conn, error) {
// Use system resolver to get IP of host
hostRegExp := regexp.MustCompile("//([^/]+)/")
host := hostRegExp.FindStringSubmatch(url)[1]
addrs, err := net.LookupHost(host)
if err != nil {
return fmt.Errorf("Could not resolve address of %s: %v", host, err)
}
ip := addrs[0]
// Replace the hostname in the given URL with its IP instead
newURL := strings.Replace(url, host, ip, 1)
config, _ := websocket.NewConfig(newURL, origin)
// As we have removed the hostname, the Go TLS package will not know what to
// validate the certificate DNS names against, so we have to provide a custom
// verifier based on the hostname we threw away.
config.TlsConfig = &tls.Config{
InsecureSkipVerify: true,
VerifyPeerCertificate: verifier(host),
}
return websocket.DialConfig(config)
}
func verifier(host string) func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
// For simplicity, let us only consider the case in which the first certificate is the one
// to validate, and in which it is signed directly by a CA, with no parsing of
// intermediate certificates required.
opts := x509.VerifyOptions{
DNSName: host,
}
rawCert := rawCerts[0]
cert, err := x509.ParseCertificate(rawCert)
if err != nil {
return err
}
_, err = cert.Verify(opts)
return err
}
}
This totally works but seems rather clunky. Is there a simpler approach? (Ideally one that is not specific to WebSocket applications but works for TLS in general; the exact same idea as above could be applied to HTTPS.)

Proxy golang https

i am trying to make one get in https://www.google.com using proxy with authentication, i already passing header parameter
Proxy-Authorization
but proxy server return
Proxy Authentication Required
code:
package main
import (
"crypto/tls"
"encoding/base64"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
)
func main() {
req, _ := http.NewRequest("GET", "https://www.google.com.br", nil)
req.Header.Set("Host", "www.google.com.br")
proxyURL := url.URL{
Host: "IP-HERE:PORT-HEERE"}
transport := &http.Transport{
Proxy: http.ProxyURL(&proxyURL),
TLSClientConfig: &tls.Config{},
}
client := &http.Client{Transport: transport}
req.RequestURI = ""
auth := fmt.Sprintf("USER:PASSWORD")
basic := "Basic " + base64.StdEncoding.EncodeToString([]byte(auth))
req.Header.Add("Proxy-Authorization", basic)
resp, err := client.Do(req)
if err != nil {
fmt.Printf("erro: %s", err)
}
fmt.Printf("code: %s", resp.StatusCode)
htmlData, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Println(os.Stdout, string(htmlData))
}
I have to pass another parameter?
When i perform one get in http://www.google.com.br, without https.. proxy authentication with success. why?
I found the solution. Golang don't pass header parameters in method CONNECT, then parameter Proxy-Authorization aren't sent to proxy server.
to resolve this, field ProxyConnectHeader was added to struct Transport. but this change until released, this change are only in master.
to test this new field using golang from master, i made on project in github and worked.
link to golang path
link to project in github that test this new field

Get remote ssl certificate in golang

I want to receive a TCP connection over TLS. I want to validate client certificate and use it to authenticate the client to my application.
Go has the standard crypto/tls package. It can validate client/server certificates. But I can't find way to get details of the remote (client) certificate, like the common name.
Have to call crypto/tls/Conn.Handshake.
Then you can read peer certificate:
tlsconn.ConnectionState().PeerCertificates[0].Subject.CommonName
Following code may help you get your answer
package main
import (
"crypto/tls"
"fmt"
"log"
)
func main() {
conf := &tls.Config{
InsecureSkipVerify: true,
}
conn, err := tls.Dial("tcp", "www.google.com:443", conf)
if err != nil {
log.Println("Error in Dial", err)
return
}
defer conn.Close()
certs := conn.ConnectionState().PeerCertificates
for _, cert := range certs {
fmt.Printf("Issuer Name: %s\n", cert.Issuer)
fmt.Printf("Expiry: %s \n", cert.NotAfter.Format("2006-January-02"))
fmt.Printf("Common Name: %s \n", cert.Issuer.CommonName)
}
}
When working with crypto/tls you can query any Conn object for ConnectionState:
func (c *Conn) ConnectionState() ConnectionState
The ConnectionState struct contains information about the client certificate:
type ConnectionState struct {
PeerCertificates []*x509.Certificate // certificate chain presented by remote peer
}
The x509.Certificate should be pretty straightforward to work with.
Before the server requests for client authentication, you have to configure the connection with the server certificate, client CA (otherwise you will have to verify the trust chain manually, you really don't want that), and tls.RequireAndVerifyClientCert. For example:
// Load my SSL key and certificate
cert, err := tls.LoadX509KeyPair(settings.MyCertificateFile, settings.MyKeyFile)
checkError(err, "LoadX509KeyPair")
// Load the CA certificate for client certificate validation
capool := x509.NewCertPool()
cacert, err := ioutil.ReadFile(settings.CAKeyFile)
checkError(err, "loadCACert")
capool.AppendCertsFromPEM(cacert)
// Prepare server configuration
config := tls.Config{Certificates: []tls.Certificate{cert}, ClientCAs: capool, ClientAuth: tls.RequireAndVerifyClientCert}
config.NextProtos = []string{"http/1.1"}
config.Rand = rand.Reader
There is an easier way to do that:
func renewCert(w http.ResponseWriter, r *http.Request) {
if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
cn := strings.ToLower(r.TLS.PeerCertificates[0].Subject.CommonName)
fmt.Println("CN: %s", cn)
}
}

Issues with TLS connection in Golang

I have the following certificate hierarchy:
Root-->CA-->3 leaf certificates
The entire chain has both serverAuth and clientAuth as extended key usages explicitly defined.
In my go code, I create a tls.Config object like so:
func parseCert(certFile, keyFile string) (cert tls.Certificate, err error) {
certPEMBlock , err := ioutil.ReadFile(certFile)
if err != nil {
return
}
var certDERBlock *pem.Block
for {
certDERBlock, certPEMBlock = pem.Decode(certPEMBlock)
if certDERBlock == nil {
break
}
if certDERBlock.Type == "CERTIFICATE" {
cert.Certificate = append(cert.Certificate, certDERBlock.Bytes)
}
}
// Need to flip the array because openssl gives it to us in the opposite format than golang tls expects.
cpy := make([][]byte, len(cert.Certificate))
copy(cpy, cert.Certificate)
var j = 0
for i := len(cpy)-1; i >=0; i-- {
cert.Certificate[j] = cert.Certificate[i]
j++
}
keyData, err := ioutil.ReadFile(keyFile)
if err != nil {
return
}
block, _ := pem.Decode(keyData)
if err != nil {
return
}
ecdsaKey, err := x509.ParseECPrivateKey(block.Bytes)
if err != nil {
return
}
cert.PrivateKey = ecdsaKey
return
}
// configure and create a tls.Config instance using the provided cert, key, and ca cert files.
func configureTLS(certFile, keyFile, caCertFile string) (tlsConfig *tls.Config, err error) {
c, err := parseCert(certFile, keyFile)
if err != nil {
return
}
ciphers := []uint16 {
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
}
certPool := x509.NewCertPool()
buf, err := ioutil.ReadFile(caCertFile)
if nil != err {
log.Println("failed to load ca cert")
log.Fatal(seelog.Errorf("failed to load ca cert.\n%s", err))
}
if !certPool.AppendCertsFromPEM(buf) {
log.Fatalln("Failed to parse truststore")
}
tlsConfig = &tls.Config {
CipherSuites: ciphers,
ClientAuth: tls.RequireAndVerifyClientCert,
PreferServerCipherSuites: true,
RootCAs: certPool,
ClientCAs: certPool,
Certificates: []tls.Certificate{c},
}
return
}
certFile is the certificate chain file and keyFile is the private key file. caCertFile is the truststore and consists of just the root certificate
So basically, here is what I expect to have inside of my tls.Config object that comes out of this function:
RootCAs: Just the root certificate from caCertFile
ClientCAs: Again, just the root certificate from caCertFile, same as RootCAs
Certificates: A single certificate chain, containing all of the certificates in certFile, ordered to be leaf first.
Now, I have 3 pieces here. A server, a relay, and a client. The client connects directly to the relay, which in turn forwards the request to the server. All three pieces use the same configuration code, of course using different certs/keys. The caCertFile is the same between all 3 pieces.
Now, if I stand up the server and the relay and connect to the relay from my browser, all goes well, so I can assume that the connection between relay and server is fine. The issue comes about when I try to connect my client to the relay. When I do so, the TLS handshake fails and the following error is returned:
x509: certificate signed by unknown authority
On the relay side of things, I get the following error:
http: TLS handshake error from : remote error: bad certificate
I am really at a loss here. I obviously have something setup incorrectly, but I am not sure what. It's really weird that it works from the browser (meaning that the config is correct from relay to server), but it doesn't work with the same config from my client.
Update:
So if I add InsecureSkipVerify: true to my tls.Config object on both the relay and the client, the errors change to:
on the client: remote error: bad certificate
and on the relay: http: TLS handshake error from : tls: client didn't provide a certificate
So it looks like the client is rejecting the certificate on from the server (the relay) due to it being invalid for some reason and thus never sending its certificate to the server (the relay).
I really wish go had better logging. I can't even hook into this process to see what, exactly, is going on.
When you say
Need to flip the array because openssl gives it to us in the opposite format than golang tls expects.
I have used certificates generated by openssl and had no problem opening them with:
tls.LoadX509KeyPair(cert, key)
Anyway, the error message bad certificate is due to the server not managing to match the client-provided certificate against its RootCAs. I have also had this problem in Go using self-signed certificats and the only work-around I've found is to install the caCertFile into the machines system certs, and use x509.SystemCertPool() instead of x509.NewCertPool().
Maybe someone else will have another solution?
Beside what beldin0 suggested.
I have tried another way to do this.
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(crt)
client := &http.Client{
//some config
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: caCertPool,
},
},
}
Here, the variable "crt" is the content in your certificate.
Basically, you just add it into your code(or read as a config file).
Then everything would be fine.