package main import ( "crypto/tls" "crypto/x509" "errors" "flag" "fmt" "io" "log" "os" "strings" ) var tlsVersionToName = map[uint16]string{ tls.VersionTLS10: "1.0", tls.VersionTLS11: "1.1", tls.VersionTLS12: "1.2", tls.VersionTLS13: "1.3", } var cipherSuiteIdToName = map[uint16]string{ tls.TLS_RSA_WITH_AES_128_CBC_SHA: "TLS_RSA_WITH_AES_128_CBC_SHA", tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", tls.TLS_AES_128_GCM_SHA256: "TLS_AES_128_GCM_SHA256", tls.TLS_AES_256_GCM_SHA384: "TLS_AES_256_GCM_SHA384", tls.TLS_CHACHA20_POLY1305_SHA256: "TLS_CHACHA20_POLY1305_SHA256", } var namedGroupsToName = map[uint16]string{ uint16(tls.HybridSIDHp503Curve25519): "X25519-SIDHp503", uint16(tls.HybridSIKEp503Curve25519): "X25519-SIKEp503", uint16(tls.X25519): "X25519", uint16(tls.CurveP256): "P-256", uint16(tls.CurveP384): "P-384", uint16(tls.CurveP521): "P-521", } func getIDByName(m map[uint16]string, name string) (uint16, error) { for key, value := range m { if value == name { return key, nil } } return 0, errors.New("Unknown value") } var failed uint type Client struct { TLS tls.Config addr string } func NewClient() *Client { var c Client c.TLS.InsecureSkipVerify = true return &c } func (c *Client) clone() *Client { var clone Client clone.TLS = *c.TLS.Clone() clone.addr = c.addr return &clone } func (c *Client) setMinMaxTLS(ver uint16) { c.TLS.MinVersion = ver c.TLS.MaxVersion = ver } func (c *Client) run() { fmt.Printf("TLS %s with %s\n", tlsVersionToName[c.TLS.MinVersion], cipherSuiteIdToName[c.TLS.CipherSuites[0]]) con, err := tls.Dial("tcp", c.addr, &c.TLS) if err != nil { fmt.Printf("handshake failed: %v\n\n", err) failed++ return } defer con.Close() _, err = con.Write([]byte("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n")) if err != nil { fmt.Printf("Write failed: %v\n\n", err) failed++ return } buf := make([]byte, 1024) n, err := con.Read(buf) // A non-zero read with EOF is acceptable and occurs when a close_notify // is received right after reading data (observed with NSS selfserv). if !(n > 0 && err == io.EOF) && err != nil { fmt.Printf("Read failed: %v\n\n", err) failed++ return } fmt.Printf("[TLS: %s] Read %d bytes\n", tlsVersionToName[con.ConnectionState().Version], n) fmt.Println("OK\n") } func result() { if failed > 0 { log.Fatalf("Failed handshakes: %d\n", failed) } else { fmt.Println("All handshakes passed") } } // Usage client args host:port func main() { var keylog_file, tls_version, named_groups, named_ciphers string var enable_rsa, enable_ecdsa, client_auth bool flag.StringVar(&keylog_file, "keylogfile", "", "Secrets will be logged here") flag.BoolVar(&enable_rsa, "rsa", true, "Whether to enable RSA cipher suites") flag.BoolVar(&enable_ecdsa, "ecdsa", true, "Whether to enable ECDSA cipher suites") flag.BoolVar(&client_auth, "cliauth", false, "Whether to enable client authentication") flag.StringVar(&tls_version, "tls_version", "1.3", "TLS version to use") flag.StringVar(&named_groups, "groups", "X25519:P-256:P-384:P-521", "NamedGroups IDs to use") flag.StringVar(&named_ciphers, "ciphers", "TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384", "Named cipher IDs to use") flag.Parse() if flag.NArg() != 1 { flag.Usage() os.Exit(1) } client := NewClient() client.addr = flag.Arg(0) if !strings.Contains(client.addr, ":") { client.addr += ":443" } if keylog_file == "" { keylog_file = os.Getenv("SSLKEYLOGFILE") } if keylog_file != "" { keylog_writer, err := os.OpenFile(keylog_file, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) if err != nil { log.Fatalf("Cannot open keylog file: %v", err) } client.TLS.KeyLogWriter = keylog_writer log.Println("Enabled keylog") } if client_auth { var err error client_cert, err := tls.X509KeyPair([]byte(client_crt), []byte(client_key)) if err != nil { panic("Can't load client certificate") } client.TLS.Certificates = []tls.Certificate{client_cert} client.TLS.RootCAs = x509.NewCertPool() if !client.TLS.RootCAs.AppendCertsFromPEM([]byte(client_ca)) { panic("Can't load client CA cert") } } if enable_rsa { // Sanity check: TLS 1.2 with the mandatory cipher suite from RFC 5246 c := client.clone() c.TLS.CipherSuites = []uint16{tls.TLS_RSA_WITH_AES_128_CBC_SHA} c.setMinMaxTLS(tls.VersionTLS12) c.run() } if enable_ecdsa { // Sane cipher suite for TLS 1.2 with an ECDSA cert (as used by boringssl) c := client.clone() c.TLS.CipherSuites = []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256} c.setMinMaxTLS(tls.VersionTLS12) c.run() } // Set requested DH groups client.TLS.CurvePreferences = []tls.CurveID{} for _, ng := range strings.Split(named_groups, ":") { id, err := getIDByName(namedGroupsToName, ng) if err != nil { panic("Wrong TLS version provided") } client.TLS.CurvePreferences = append(client.TLS.CurvePreferences, tls.CurveID(id)) } // Perform TLS handshake with each each requested CipherSuite tlsID, err := getIDByName(tlsVersionToName, tls_version) if err != nil { panic("Unknown TLS version") } for _, cn := range strings.Split(named_ciphers, ":") { id, err := getIDByName(cipherSuiteIdToName, cn) if err != nil { panic("Wrong cipher name provided") } client.setMinMaxTLS(tlsID) client.TLS.CipherSuites = []uint16{id} client.run() } // TODO test other kex methods besides X25519, like MTI secp256r1 // TODO limit supported groups? result() } const ( client_ca = `-----BEGIN CERTIFICATE----- MIIF6zCCA9OgAwIBAgIUC4U4HlbkVMrKKTFK0mNrMFDpRskwDQYJKoZIhvcNAQEL BQAwfTELMAkGA1UEBhMCRlIxDTALBgNVBAgMBFBBQ0ExFzAVBgNVBAcMDkNhZ25l cyBzdXIgTWVyMSIwIAYDVQQLDBlDZXJ0IFRlc3RpbmcgT3JnYW5pemF0aW9uMSIw IAYDVQQDDBlDZXJ0IFRlc3RpbmcgT3JnYW5pemF0aW9uMB4XDTE5MDIyMjAwNDIz OVoXDTQ2MDcwOTAwNDIzOVowfTELMAkGA1UEBhMCRlIxDTALBgNVBAgMBFBBQ0Ex FzAVBgNVBAcMDkNhZ25lcyBzdXIgTWVyMSIwIAYDVQQLDBlDZXJ0IFRlc3Rpbmcg T3JnYW5pemF0aW9uMSIwIAYDVQQDDBlDZXJ0IFRlc3RpbmcgT3JnYW5pemF0aW9u MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0z+DMLb7YIMFFNZpn+ve NdT7GL9DPyV9ZWSHpUuDyme6Og6Mp5IpCLlKjNXtizX5aQ9xQ746slt70fivSV/r tiEtayZkcwS7zHtc5f+U/S0hR1q5Zh3DaLQH9diSeuNFQN5pg7zQT5csJFlxf6EB j/ioSBC+J1E8A2FAh0qDq+TvPPyZEEjcJy0oBuNHUnkC3rwjt24DAUI26rN/Qk9P a6KR9bBOdHFFul3DEP/uPqWV9TvV5tJhP3J2RbfS79WljFy/lFIwvJvfQHYEjMt4 /gq8yTSUgJ8zmgJQ1sgOKH1FzJd4EdAMquSYbElkc35jX8gggUNOUcwsIfJBnu41 SC51JQruNT256zse76o8Dx3lSHiz5c6luZyJnZWWt6xWtfGEGMnckpn6cVvcbbgq eWqmttgE2QTpgYoYUVcX/XFtsmZVTu05r8MZoqje5rgW9nEvvW+3M+eT5h0M9eGQ bIT3D3tdXB2XWCjUWqxpZscFwyumGu7vdykBKLhMVR3nEpFfORnH+534vwi49fjz WnN6fXAZZLPnGtEdWXNgs9JtgI5UheAQbcA3FT+M3maa88V2JrETLps405NYp6hJ 6msbS/AmV/eSilRmbGVj9TfKHb/BVHNYwVQ0Bu/QN2YQNQ9olOpIxXgKr6Y4tKZt wTOMiCxZrnDQneQOTnW0NAMCAwEAAaNjMGEwHQYDVR0OBBYEFNLiS6YezH2bWiZ3 TNbkQzMoBBB2MB8GA1UdIwQYMBaAFNLiS6YezH2bWiZ3TNbkQzMoBBB2MA8GA1Ud EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQAQ cWIFOusSLKizqcqMsbMIY/Jzy0Tq5jzeOAQEQBztu7eJb208SG2EJtD6ylBQCF8t FCxUbvWNrly1MJoMSXdn3uMz3kLKNQa6RENckwA1UuYZpdhvTUtmPun9QqFPJdqm oi0paOVut9q3dplDy6MUknGN4tNWp2ZDfyvom3mUMfYGEO/FCWTy8eFd6cHRE9bw tHkcX5r7GpDHH5vKXOF/deMp1Xgep5ZTasL13YwPiYgctst91pEfdcztjHW0mQNT ZH/TUQDgs2UCjcvyeOlgoZixWOpkf1Qyje15k9qMb89/5hdarxvAQbG2BezQtzyk bbCu1MQa2DBdAKbhQxas/DPSvSkA/y8v+hiovTWtPKErPnQqZqVy59KUTBWj8ZAj 5dkDVjBvUcsJ/6zHv0X9puEnIDZ8pK+Xn9LbcbPE7Nf1ikDyOqHmLmhGfWlEGvoD 3Q8f8zUySZ40mfqtVhc7OYqA66Q9quNQ4VBESVNiEJ/LuWHRXe74KqFdggsQqtS6 UQQgw5lFnKHZ9pk2VlKzgpkmd5fLMOhcHWQbsah9TFOuW5vEhWGHNhGCyGouWTzD mkwlPS8arj/ymUn6t/oiwSOA6GbjQLnTXvoAjdBxnukQlNY6TUDk+lSQw0qfZGIA xZywUgRbLZH8TFUnuEQps35XnWrY8rrXVj9+9h0B4g== -----END CERTIFICATE-----` client_crt = `-----BEGIN CERTIFICATE----- MIIFSjCCAzKgAwIBAgIUCKk2npLYEX2Z3Ceu1CwSKK50j04wDQYJKoZIhvcNAQEL BQAwfTELMAkGA1UEBhMCRlIxDTALBgNVBAgMBFBBQ0ExFzAVBgNVBAcMDkNhZ25l cyBzdXIgTWVyMSIwIAYDVQQLDBlDZXJ0IFRlc3RpbmcgT3JnYW5pemF0aW9uMSIw IAYDVQQDDBlDZXJ0IFRlc3RpbmcgT3JnYW5pemF0aW9uMB4XDTE5MDIyMjAwNDMz MloXDTQ2MDcwOTAwNDMzMlowfTELMAkGA1UEBhMCRlIxDTALBgNVBAgMBFBBQ0Ex FzAVBgNVBAcMDkNhZ25lcyBzdXIgTWVyMSIwIAYDVQQLDBlDZXJ0IFRlc3Rpbmcg T3JnYW5pemF0aW9uMSIwIAYDVQQDDBlDZXJ0IFRlc3RpbmcgT3JnYW5pemF0aW9u MIIBIDANBgkqhkiG9w0BAQEFAAOCAQ0AMIIBCAKCAQEA0DQrWlCfijylutz28aTB T9WDnvBWpJ535t/Clt4o2nv3Cp6JxvUVzYkdKuaLR295gyEBx9JSHiZxPiJoPVfp wigmG0R9HvByAG5rhaQbQt99npoBaHMps1i12VxxFy1yaqZW6mrwrHMfV716rZ2M AzWx7UfhutloBYeeluiziDWUSEuGeJG7kHdvUtGYlbRd/ElFWHOfAQ7Oc8UUjEHW sorkqciqyAERV/H9hr5Rap/J/ERcFC8bNecS4t1Yh98WgIun/MbcBKQzo1LsWmOQ dmaMBoG8g1mYRbNap8G/+aQbjfRi1zN0yaW1wtlLoBJmNgLjwYgaS/5Uey5NZMdb NwIBA6OBwzCBwDAJBgNVHRMEAjAAMBEGCWCGSAGG+EIBAQQEAwIFoDAxBglghkgB hvhCAQ0EJBYiQ2VydCBUZXN0aW5nIEludGVybWVkaWF0ZSAtIENsaWVudDAdBgNV HQ4EFgQULlrGFPbxwz525ywQYPs72P7YnL4wHwYDVR0jBBgwFoAU0uJLph7MfZta JndM1uRDMygEEHYwDgYDVR0PAQH/BAQDAgXgMB0GA1UdJQQWMBQGCCsGAQUFBwMC BggrBgEFBQcDBDANBgkqhkiG9w0BAQsFAAOCAgEAwLc+Xg5Fyfbgu5iFs1y7E0Et 4E/5lF0A4iqDVX3e7/upoUIBFZFv2PlAqIlhQ49NgGlIfrBlwEijZJ9kgVmUcKDS UrqBvKUn+99dTC8Zn/Py9ofLNcJy+qNJg4TpbpBxXaP1MXdZYXdYkGtyyPIGo31U oHibNLQDCtKFMoEPCvFuCBtJgyT46l5KN7VQCA0ZDm84fVmIgEEOXWwz0mDIhGWm hDhmqONznl0+aHirqJxsBaplBaFVV1N02ksR53sPPy/UfDsAD3Fpp8R1DAMEyy0o kTqm8QINVL961YT1Y/oI+GlypjPq9cL0dEHdxwu6gyCHPMMGGGIDHmLoqJJuj/Kr /T08jhtDv8D7e9m3wfSW/RqHKE31Yy21SXv/gpcHGunwzDoj/QUvRl/xTjJfx+S8 2NHxSU8QOdexhJumsNFJe8kH8cRJMCMB8/hfiBpI0QANkUBJ1aaa/p7vZuEKJm+/ 85m3Yz+zn58/Bube06z6QzFeR8Edi+6hXk4/WoHltgXiNowD3d4xI48sPWEbe+QZ 6u60sEdpY2a+3Xwt9m9R2R+sGP3QyDFd9GVaUPt21TeeLdfS3kPqwO2k+UXB8nV3 Yh1Hvyx67u0tX3wBVe40CNaAu7iW+e4aXjksG2dxk71lNq5CHJCOtbRK4LUArjy5 cw4KAXWoaR8YIC3BWgg= -----END CERTIFICATE-----` client_key = `-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDQNCtaUJ+KPKW6 3PbxpMFP1YOe8Faknnfm38KW3ijae/cKnonG9RXNiR0q5otHb3mDIQHH0lIeJnE+ Img9V+nCKCYbRH0e8HIAbmuFpBtC332emgFocymzWLXZXHEXLXJqplbqavCscx9X vXqtnYwDNbHtR+G62WgFh56W6LOINZRIS4Z4kbuQd29S0ZiVtF38SUVYc58BDs5z xRSMQdayiuSpyKrIARFX8f2GvlFqn8n8RFwULxs15xLi3ViH3xaAi6f8xtwEpDOj UuxaY5B2ZowGgbyDWZhFs1qnwb/5pBuN9GLXM3TJpbXC2UugEmY2AuPBiBpL/lR7 Lk1kx1s3AgEDAoIBAQCKzXI8Nb+xfcPR6KShGIDf460UoDnDFE/vP9cPPsXm/U9c abEvTg6JBhNx7weE9PuswKvajDa+xEt+wZrTj/EsGsQSLai/Svaq9EeubWeB6lO/ EVZFohvM5c6Q6EtkyPbxxDnxnKBy92o6flHJE7KsznaeL+vR5kVZBRRkmyJazS4s Z5rbrN9AhSIfyHs9GCQGgsXT6HMsyoJYFastwQ2qj+9L2ypcM8TW+KGzGfJipoJb l/N/8WHb4ZumA67lfWq4v5JTA5qAUKcfPszEBrUfQ34Tk+73Iiov9f7SXPYxWxVJ g9PuzfewvJrp6CPv+/mKNt8PmBYkaXlnyjr9tCwLAoGBAPjAVZapQVuIqftcOZtf Re9fAV9Vvv1FEO8bKJeIsPDlRkdg+TfTMgxhZU0I3P4XdEj7Fa87w4wkA6GkIrOO W9/usPOYzSdTP5aVEsdGbT8yD2vTST7Aw/GESKTRJA/Fe1PIb5Nz3OijyTusvFE+ XSR3EXb1myX+2rFS0Wbiz2U5AoGBANZFWoeFzREnBcDG60RayjiTg71E1/T4zhvU e/w+71FNbLZXBrNqgV20F73xOme/Mb13yr+YgXxIEQfFtR6hRxZ8u1jndEzw66Jf YfHt7EGVceMV2pdP4md5ebebEj7qICfXPxF9IZicwZG3QMR5u0tvnx40iNMWhW0M rY4FabPvAoGBAKXVjmRw1j0FxqeS0RI/g/TqAOo5Kf4uC0oSGw+wdfXuLtpApiU3 drLrmN4F6Klk+DCnY8on17LCrRZtbHe0PT/0dfe7M2+M1Q8ODITZniohX503hinV 1/ZYMG3gwrUuUjfa9Qz36JsX230d0uDUPhhPYPn5EhlUkcuMi5nsikN7AoGBAI7Y 5wUD3gtvWSsvR4LnMXsNAn4t5U37NBKNp/1/SjYznc7kryJHAOkiun6g0Zp/dn5P 3H+7AP2FYK/ZI2nA2g790jtE+DNLR8GU6/aenYEOS+y5PGTf7ET7pnpnYX9GwBqP f2D+FmW91mEk1dhRJ4efv2l4WzdkWPNdyQlY8SKfAoGAEIWmowo7EpbR5Boxc2o3 Tl0JbGi1CNJAHtDjEJCd1OxKrMUrK07hOeEKF6y8K/WBuQhFvI2pu16oT4sMal9Z mEiJdJAFErefPLQGomHLXfq9mDEY13Ug/xAd9aMyYcubIg5XjAqLMrB60HcrLr7Q 2hMCSDdVP2V/F3QVh8DEirE= -----END PRIVATE KEY-----` )