Implement Case-insensitive SNI matching

This commit is contained in:
Daniel Tomcej
2018-11-26 03:38:03 -06:00
committed by Traefiker Bot
parent 479ee9af49
commit 4a5f5440d7
8 changed files with 232 additions and 14 deletions

View File

@@ -0,0 +1,37 @@
logLevel = "DEBUG"
defaultEntryPoints = ["https"]
[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]
[file]
[backends]
[backends.backend1]
[backends.backend1.servers.server1]
url = "http://127.0.0.1:9010"
weight = 1
[frontends]
[frontends.frontend1]
backend = "backend1"
[frontends.frontend1.routes.test_1]
rule = "HostRegexp: {subdomain:[a-z1-9-]+}.snitest.com"
[frontends.frontend2]
backend = "backend1"
[frontends.frontend2.routes.test_1]
rule = "HostRegexp: {subdomain:[a-z1-9-]+}.www.snitest.com"
[[tls]]
entryPoints = ["https"]
[tls.certificate]
certFile = "fixtures/https/uppercase_wildcard.www.snitest.com.cert"
keyFile = "fixtures/https/uppercase_wildcard.www.snitest.com.key"

View File

@@ -0,0 +1,34 @@
logLevel = "DEBUG"
defaultEntryPoints = ["https"]
[entryPoints]
[entryPoints.https]
address = ":4443"
[entryPoints.https.tls]
[[entryPoints.https.tls.certificates]]
certFile = "fixtures/https/uppercase_wildcard.www.snitest.com.cert"
keyFile = "fixtures/https/uppercase_wildcard.www.snitest.com.key"
[entryPoints.https.tls.defaultCertificate]
certFile = "fixtures/https/wildcard.snitest.com.cert"
keyFile = "fixtures/https/wildcard.snitest.com.key"
[api]
[file]
[backends]
[backends.backend1]
[backends.backend1.servers.server1]
url = "http://127.0.0.1:9010"
weight = 1
[frontends]
[frontends.frontend1]
backend = "backend1"
[frontends.frontend1.routes.test_1]
rule = "HostRegexp: {subdomain:[a-z1-9-]+}.snitest.com"
[frontends.frontend2]
backend = "backend1"
[frontends.frontend2.routes.test_2]
rule = "HostRegexp: {subdomain:[a-z1-9-]+}.www.snitest.com"

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -830,3 +830,71 @@ func (s *HTTPSSuite) TestEntrypointHttpsRedirectAndPathModification(c *check.C)
}
}
}
// TestWithSNIStaticCaseInsensitive 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) TestWithSNIStaticCaseInsensitive(c *check.C) {
cmd, display := s.traefikCmd(withConfigFile("fixtures/https/https_sni_case_insensitive_static.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", 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")
}
// 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", 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")
}

View File

@@ -13,6 +13,7 @@ import (
"net/url"
"os"
"os/signal"
"strings"
"sync"
"time"
@@ -711,13 +712,13 @@ func (s *Server) buildNameOrIPToCertificate(certs []tls.Certificate) map[string]
continue
}
if len(x509Cert.Subject.CommonName) > 0 {
certMap[x509Cert.Subject.CommonName] = cert
certMap[strings.ToLower(x509Cert.Subject.CommonName)] = cert
}
for _, san := range x509Cert.DNSNames {
certMap[san] = cert
certMap[strings.ToLower(san)] = cert
}
for _, ipSan := range x509Cert.IPAddresses {
certMap[ipSan.String()] = cert
certMap[strings.ToLower(ipSan.String())] = cert
}
}
return certMap

View File

@@ -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()))
}
}

View File

@@ -20,6 +20,7 @@ func TestGetBestCertificate(t *testing.T) {
staticCert string
dynamicCert string
expectedCert string
uppercase bool
}{
{
desc: "Empty Store, returns no certs",
@@ -27,6 +28,7 @@ func TestGetBestCertificate(t *testing.T) {
staticCert: "",
dynamicCert: "",
expectedCert: "",
uppercase: false,
},
{
desc: "Empty static cert store",
@@ -34,6 +36,7 @@ func TestGetBestCertificate(t *testing.T) {
staticCert: "",
dynamicCert: "snitest.com",
expectedCert: "snitest.com",
uppercase: false,
},
{
desc: "Empty dynamic cert store",
@@ -41,6 +44,7 @@ func TestGetBestCertificate(t *testing.T) {
staticCert: "snitest.com",
dynamicCert: "",
expectedCert: "snitest.com",
uppercase: false,
},
{
desc: "Best Match",
@@ -48,6 +52,7 @@ func TestGetBestCertificate(t *testing.T) {
staticCert: "snitest.com",
dynamicCert: "snitest.org",
expectedCert: "snitest.com",
uppercase: false,
},
{
desc: "Best Match with wildcard dynamic and exact static",
@@ -55,6 +60,7 @@ func TestGetBestCertificate(t *testing.T) {
staticCert: "www.snitest.com",
dynamicCert: "*.snitest.com",
expectedCert: "www.snitest.com",
uppercase: false,
},
{
desc: "Best Match with wildcard static and exact dynamic",
@@ -62,6 +68,7 @@ func TestGetBestCertificate(t *testing.T) {
staticCert: "*.snitest.com",
dynamicCert: "www.snitest.com",
expectedCert: "www.snitest.com",
uppercase: false,
},
{
desc: "Best Match with static wildcard only",
@@ -69,6 +76,7 @@ func TestGetBestCertificate(t *testing.T) {
staticCert: "*.snitest.com",
dynamicCert: "",
expectedCert: "*.snitest.com",
uppercase: false,
},
{
desc: "Best Match with dynamic wildcard only",
@@ -76,6 +84,7 @@ func TestGetBestCertificate(t *testing.T) {
staticCert: "",
dynamicCert: "*.snitest.com",
expectedCert: "*.snitest.com",
uppercase: false,
},
{
desc: "Best Match with two wildcard certs",
@@ -83,6 +92,23 @@ func TestGetBestCertificate(t *testing.T) {
staticCert: "*.www.snitest.com",
dynamicCert: "*.snitest.com",
expectedCert: "*.www.snitest.com",
uppercase: false,
},
{
desc: "Best Match with static wildcard only, case insensitive",
domainToCheck: "bar.www.snitest.com",
staticCert: "*.www.snitest.com",
dynamicCert: "",
expectedCert: "*.www.snitest.com",
uppercase: true,
},
{
desc: "Best Match with dynamic wildcard only, case insensitive",
domainToCheck: "bar.www.snitest.com",
staticCert: "",
dynamicCert: "*.www.snitest.com",
expectedCert: "*.www.snitest.com",
uppercase: true,
},
}
@@ -94,15 +120,15 @@ func TestGetBestCertificate(t *testing.T) {
dynamicMap := map[string]*tls.Certificate{}
if test.staticCert != "" {
cert, err := loadTestCert(test.staticCert)
cert, err := loadTestCert(test.staticCert, test.uppercase)
require.NoError(t, err)
staticMap[test.staticCert] = cert
staticMap[strings.ToLower(test.staticCert)] = cert
}
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{
@@ -113,7 +139,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
}
@@ -128,10 +154,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