Listening for TLS Handshakes
Golang’s standard library has support for TLS servers out of the box. For testing purposes, you can generate a cert with openssl
openssl req -new -newkey rsa:4096 -x509 -sha256 -days 365 -nodes -out server.crt -keyout server.key
and use http.ListenAndServeTLS to spin up a sever
package main
import (
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /", func(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "welcome to my secure server\n")
})
fmt.Println("starting server")
err := http.ListenAndServeTLS(":443", "server.crt", "server.key", mux)
if err != nil {
fmt.Printf("Server failed: %s\n", err)
}
}
confirm that it works with curl.
curl -k https://localhost:443
You can also pass a Config from the crypto/tls package for fine grained control of what ciphers you want to use. Here is a minimal example where we tell the server to only support X25519 for key exchange.
server := &http.Server{
Addr: ":443",
Handler: mux,
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS13,
CurvePreferences: []tls.CurveID{tls.X25519},
CipherSuites: []uint16{tls.TLS_AES_128_GCM_SHA256},
},
}
Listening for ClientHello
The tls.Config struct allows you to define a function that runs every time a ClientHello is received by the server.
&http.Server{
...
TlsConfig: &tls.Config{
GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
fmt.Printf("received client hello from: %s\n", hello.Conn.RemoteAddr())
fmt.Printf("client supports the following cipherSuites: %x", hello.CipherSuites)
return nil, nil
}
},
}
Or if you prefer, you can listen at the byte level with our own custom listener. Following the types from the TLS 1.3 RFC: look for a handshake record type, starting with 22 (hex: 0x16), and then checking that the message type is of type client_hello (0x01)
const (
ContentTypeHandshake = 0x16
HandshakeClientHello = 0x01
)
type handshakeConn struct {
net.Conn
}
func (c *handshakeConn) Read(b []byte) (n int, err error) {
n, err = c.Conn.Read(b)
n, err = c.Conn.Read(b)
if n > 0 && b[0] == ContentTypeHandshake {
handshake := b[5:]
if handshake[0] == HandshakeClientHello {
fmt.Println("received clientHello!")
}
}
return n, err
}
type handshakeListener struct {
net.Listener
}
func (l *handshakeListener) Accept() (net.Conn, error) {
c, err := l.Listener.Accept()
if err != nil {
return nil, err
}
return &handshakeConn{c}, nil
}
Then manually define your listener and pass to server.ServeTLS
func main() {
listener, err := net.Listen("tcp", ":443")
if err != nil {
panic(err)
}
handshakeListener := &handshakeListener{listener}
server := &http.Server{
Addr: ":443",
Handler: mux,
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS13,
CurvePreferences: []tls.CurveID{tls.X25519},
CipherSuites: []uint16{tls.TLS_AES_128_GCM_SHA256},
},
}
server.ServeTLS(handshakeListener, "server.crt", "server.key")
}