From 5b3762be0862b41f0c0cc93338d98a9b87cce9f9 Mon Sep 17 00:00:00 2001 From: Daniel Tomcej Date: Mon, 26 Nov 2018 03:38:03 -0600 Subject: [PATCH] Implement Case-insensitive SNI matching --- .../https_sni_case_insensitive_dynamic.toml | 36 +++++++++++++++++++ .../uppercase_wildcard.www.snitest.com.cert | 19 ++++++++++ .../uppercase_wildcard.www.snitest.com.key | 28 +++++++++++++++ integration/https_test.go | 34 ++++++++++++++++++ tls/certificate.go | 6 ++-- tls/certificate_store_test.go | 25 +++++++++---- 6 files changed, 139 insertions(+), 9 deletions(-) create mode 100644 integration/fixtures/https/https_sni_case_insensitive_dynamic.toml create mode 100644 integration/fixtures/https/uppercase_wildcard.www.snitest.com.cert create mode 100644 integration/fixtures/https/uppercase_wildcard.www.snitest.com.key diff --git a/integration/fixtures/https/https_sni_case_insensitive_dynamic.toml b/integration/fixtures/https/https_sni_case_insensitive_dynamic.toml new file mode 100644 index 000000000..45f9f0a6d --- /dev/null +++ b/integration/fixtures/https/https_sni_case_insensitive_dynamic.toml @@ -0,0 +1,36 @@ +logLevel = "DEBUG" + +[entryPoints] + [entryPoints.https] + address = ":4443" + [entryPoints.https.tls] + [entryPoints.https.tls.defaultCertificate] + certFile = "fixtures/https/wildcard.snitest.com.cert" + keyFile = "fixtures/https/wildcard.snitest.com.key" + +[api] + +[providers] + [providers.file] + +[Routers] + [Routers.router1] + Service = "service1" + rule = "HostRegexp: {subdomain:[a-z1-9-]+}.snitest.com" + [Routers.router2] + Service = "service1" + rule = "HostRegexp: {subdomain:[a-z1-9-]+}.www.snitest.com" + +[Services] + [Services.service1] + [Services.service1.LoadBalancer] + + [[Services.service1.LoadBalancer.Servers]] + URL = "http://127.0.0.1:9010" + Weight = 1 + +[[tls]] + entryPoints = ["https"] + [tls.certificate] + certFile = "fixtures/https/uppercase_wildcard.www.snitest.com.cert" + keyFile = "fixtures/https/uppercase_wildcard.www.snitest.com.key" diff --git a/integration/fixtures/https/uppercase_wildcard.www.snitest.com.cert b/integration/fixtures/https/uppercase_wildcard.www.snitest.com.cert new file mode 100644 index 000000000..dfe9440ee --- /dev/null +++ b/integration/fixtures/https/uppercase_wildcard.www.snitest.com.cert @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDDDCCAfSgAwIBAgIJAI1YpPACcsQnMA0GCSqGSIb3DQEBCwUAMB4xHDAaBgNV +BAMME0ZPTy5XV1cuU05JVEVTVC5DT00wHhcNMTgxMDI5MTU1NDI4WhcNMjgxMDI2 +MTU1NDI4WjAeMRwwGgYDVQQDDBNGT08uV1dXLlNOSVRFU1QuQ09NMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxyWr+1O/tf4yjwhfp3/SDGT5fD0chhGs +Qc+QM7Ewb5SOmIL5UskxT5pCKc6Kuie5qqEp9xH8Rrfo18iEJQPdhFC1YkaBEI0L +l1qvN4jmXzAK/E/u4+X+FFprHyruXCmuXqsWQt/qEOqU1ciN47GE9+ZW4R+q70uB +zrEQ+dzN7IBsyf1lzzS3/TwDgj085QmiZYxKxX40d5hZW6AHxPEKJa2p+Gweqg74 +SpzBWL1DYQLcqHUuMKlbigHg+gleqcO8NiHT5UdeSPVokD5VJPO1La1PMqkLmJYr +3vVkQ9YzNQ615bX98VMIi17cmE7LE+vz+v287cdFT2f1pNXr3pCGzQIDAQABo00w +SzALBgNVHQ8EBAMCBDAwCQYDVR0TBAIwADATBgNVHSUEDDAKBggrBgEFBQcDATAc +BgNVHREEFTATghEqLldXVy5TTklURVNULkNPTTANBgkqhkiG9w0BAQsFAAOCAQEA +HJyMCj9oHwECmSGWHnYHkO42zeyj24RKlhNG5skUCqZmpmeDc2BRMYH4fjP75MD2 +kuasZBMAxyQnur/DEn8TzQ1mlKxYCqoza1ql5PkfcwNUp/tvQ7Jhf45Z5mQVeUM7 +RSiBhpeetjHY5/xQb7gXHa97+OjDoRJ6NL/dzGxqypf37kiQPw2jWI5RTFBkP+h/ +sPbeAZJjmiEzvw31SAw9IGj3VvIwcuTxbsdJQITU7hCXDSd1EIocmzAoobY7WRcT +B1pLmHlP/BaIsM7m0NF/HgUsgo/kgSsxnGA2MHMYQiTImR2DUgrJYzKlJ5acscLK +sMq9xUnjr6KF1C15R2FpDw== +-----END CERTIFICATE----- diff --git a/integration/fixtures/https/uppercase_wildcard.www.snitest.com.key b/integration/fixtures/https/uppercase_wildcard.www.snitest.com.key new file mode 100644 index 000000000..5b9a16173 --- /dev/null +++ b/integration/fixtures/https/uppercase_wildcard.www.snitest.com.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDHJav7U7+1/jKP +CF+nf9IMZPl8PRyGEaxBz5AzsTBvlI6YgvlSyTFPmkIpzoq6J7mqoSn3EfxGt+jX +yIQlA92EULViRoEQjQuXWq83iOZfMAr8T+7j5f4UWmsfKu5cKa5eqxZC3+oQ6pTV +yI3jsYT35lbhH6rvS4HOsRD53M3sgGzJ/WXPNLf9PAOCPTzlCaJljErFfjR3mFlb +oAfE8Qolran4bB6qDvhKnMFYvUNhAtyodS4wqVuKAeD6CV6pw7w2IdPlR15I9WiQ +PlUk87UtrU8yqQuYlive9WRD1jM1DrXltf3xUwiLXtyYTssT6/P6/bztx0VPZ/Wk +1evekIbNAgMBAAECggEAVOFEnTmD47D1oasjAgRj5a5/+6kcaDROJDqwrqeeCmDa +KjzgwZ1JLDGGc8U5scBOzWAlv83lpcqrLpWjZRdxqfywYrPEPOaxAxC+z7/E2Ntk +Q0hafL5BfjFPqRgmQhft3yGyukwvuogRadEyUNMP5o1BiHBz7cxUBmHH54dqKZuO +ueUMgqraJX/GK+Om2rIUst0oOT9yUED+f6ciIjVAmCx1EVxZmX7sxKig10e70eOJ +rfHlRguJWtxy0+Wl8R8TVrpI5r7qsE8y2fet9RqFOof/4ds8uA2nlZ3NpGkAq3Oo ++65h/2fjD5uQ7jmT+XZcbC7SGhboV42zIrmn0DyNIQKBgQDneeqzMlooNzLD6x+v +bXo6BJAHXuml440zS5i5RawKc3+/GxGQjBvnfhFH6AQ7cL4ohYyfuAo4srgifRle +x3Gl8yvFf0uLaQHj811HPWV0fU8bwekI77jmH7WZi2ED/qX7X06R2vvUPGshvJi5 +yPCmJpDQQA6wmxBG1U4SqNw0xQKBgQDcPu2DMAJpbMWWeb5xWv5/6h6TUF4tV7fV +eIBWuVfe9Jry3gAnb6YUOKYmA5xYJJ+fTz4Nhe4+LQbFS1esT/7ZIATvILogZc3S +X9+ZCYG/tmDDZvhZqIWWSzzdrjb7dseP1RI4Wp6OnRqHWErrkfzDJKuN15qgW5vR +FUR2ykV6aQKBgQCv5ZQ00dly3+ciu+QbAb00o0zzXOt91Lnytcp7V3dRhc0YYrBp +QB7gPYtSMfwtUxIdZsaihE64IQ8NnjSOMk6pRW0Iqh+083mtR7ylKwGSkLpxpFu6 +H7hInuX3pNN3HqXwq87fxSFCeRsLyu3fl9NO3tWCenrvNxYaTXMDeO/E5QKBgE7D +XlMU/zfOg1bN0PJe1TbPdgG+sv9KKF76CgN5otgD58nE5I812VHP9HMRxX6sEj15 +rDpP1CR+G7bAu+jObtgdIEaYEJf3cES0rpTfFnyF71LR5yzBHIzj+S9Z1yXUk4d3 +bl2i4qMjwdH3HEvkWF09JvDB0vVX7YA3N9W3fmNJAoGBALRi9EbkEBW1vMPwMzps +YoJ1lp/YyDGTFcg6KFgTfNaOYccb6EXL2Cd21qvDsJw6wthXS+cSqX3qlTLAVLY8 +az/NfyFmW1fUtGjs2s0ZtplStGBhv8VR+2fpt9fgDOOrGYiN2dtmPm7jCAmyQQq7 +JCg7Vq6f0q95DUwiUAo24CBn +-----END PRIVATE KEY----- diff --git a/integration/https_test.go b/integration/https_test.go index 1d940b1ab..b05874218 100644 --- a/integration/https_test.go +++ b/integration/https_test.go @@ -830,3 +830,37 @@ func (s *HTTPSSuite) TestEntrypointHttpsRedirectAndPathModification(c *check.C) } } } + +// TestWithSNIDynamicCaseInsensitive involves a client sending a SNI hostname of +// "bar.www.snitest.com", which matches the DNS SAN of '*.WWW.SNITEST.COM'. The test +// verifies that traefik presents the correct certificate. +func (s *HTTPSSuite) TestWithSNIDynamicCaseInsensitive(c *check.C) { + cmd, display := s.traefikCmd(withConfigFile("fixtures/https/https_sni_case_insensitive_dynamic.toml")) + defer display(c) + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + // wait for Traefik + err = try.GetRequest("http://127.0.0.1:8080/api/providers/file/routers", 500*time.Millisecond, try.BodyContains("HostRegexp: {subdomain:[a-z1-9-]+}.www.snitest.com")) + c.Assert(err, checker.IsNil) + + tlsConfig := &tls.Config{ + InsecureSkipVerify: true, + ServerName: "bar.www.snitest.com", + NextProtos: []string{"h2", "http/1.1"}, + } + conn, err := tls.Dial("tcp", "127.0.0.1:4443", tlsConfig) + c.Assert(err, checker.IsNil, check.Commentf("failed to connect to server")) + + defer conn.Close() + err = conn.Handshake() + c.Assert(err, checker.IsNil, check.Commentf("TLS handshake error")) + + cs := conn.ConnectionState() + err = cs.PeerCertificates[0].VerifyHostname("*.WWW.SNITEST.COM") + c.Assert(err, checker.IsNil, check.Commentf("certificate did not match SNI servername")) + + proto := conn.ConnectionState().NegotiatedProtocol + c.Assert(proto, checker.Equals, "h2") +} diff --git a/tls/certificate.go b/tls/certificate.go index 47b2a10eb..452502f68 100644 --- a/tls/certificate.go +++ b/tls/certificate.go @@ -154,13 +154,13 @@ func (c *Certificate) AppendCertificates(certs map[string]map[string]*tls.Certif var SANs []string if parsedCert.Subject.CommonName != "" { - SANs = append(SANs, parsedCert.Subject.CommonName) + SANs = append(SANs, strings.ToLower(parsedCert.Subject.CommonName)) } if parsedCert.DNSNames != nil { sort.Strings(parsedCert.DNSNames) for _, dnsName := range parsedCert.DNSNames { if dnsName != parsedCert.Subject.CommonName { - SANs = append(SANs, dnsName) + SANs = append(SANs, strings.ToLower(dnsName)) } } @@ -168,7 +168,7 @@ func (c *Certificate) AppendCertificates(certs map[string]map[string]*tls.Certif if parsedCert.IPAddresses != nil { for _, ip := range parsedCert.IPAddresses { if ip.String() != parsedCert.Subject.CommonName { - SANs = append(SANs, ip.String()) + SANs = append(SANs, strings.ToLower(ip.String())) } } diff --git a/tls/certificate_store_test.go b/tls/certificate_store_test.go index 31e1f9f02..a1f24d19b 100644 --- a/tls/certificate_store_test.go +++ b/tls/certificate_store_test.go @@ -20,6 +20,7 @@ func TestGetBestCertificate(t *testing.T) { domainToCheck string dynamicCert string expectedCert string + uppercase bool }{ { desc: "Empty Store, returns no certs", @@ -45,6 +46,13 @@ func TestGetBestCertificate(t *testing.T) { dynamicCert: "*.snitest.com", expectedCert: "*.snitest.com", }, + { + desc: "Best Match with dynamic wildcard only, case insensitive", + domainToCheck: "bar.www.snitest.com", + dynamicCert: "*.www.snitest.com", + expectedCert: "*.www.snitest.com", + uppercase: true, + }, } for _, test := range testCases { @@ -54,9 +62,9 @@ func TestGetBestCertificate(t *testing.T) { dynamicMap := map[string]*tls.Certificate{} if test.dynamicCert != "" { - cert, err := loadTestCert(test.dynamicCert) + cert, err := loadTestCert(test.dynamicCert, test.uppercase) require.NoError(t, err) - dynamicMap[test.dynamicCert] = cert + dynamicMap[strings.ToLower(test.dynamicCert)] = cert } store := &CertificateStore{ @@ -66,7 +74,7 @@ func TestGetBestCertificate(t *testing.T) { var expected *tls.Certificate if test.expectedCert != "" { - cert, err := loadTestCert(test.expectedCert) + cert, err := loadTestCert(test.expectedCert, test.uppercase) require.NoError(t, err) expected = cert } @@ -81,10 +89,15 @@ func TestGetBestCertificate(t *testing.T) { } } -func loadTestCert(certName string) (*tls.Certificate, error) { +func loadTestCert(certName string, uppercase bool) (*tls.Certificate, error) { + replacement := "wildcard" + if uppercase { + replacement = "uppercase_wildcard" + } + staticCert, err := tls.LoadX509KeyPair( - fmt.Sprintf("../integration/fixtures/https/%s.cert", strings.Replace(certName, "*", "wildcard", -1)), - fmt.Sprintf("../integration/fixtures/https/%s.key", strings.Replace(certName, "*", "wildcard", -1)), + fmt.Sprintf("../integration/fixtures/https/%s.cert", strings.Replace(certName, "*", replacement, -1)), + fmt.Sprintf("../integration/fixtures/https/%s.key", strings.Replace(certName, "*", replacement, -1)), ) if err != nil { return nil, err