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 3b01488c8d
commit 5b3762be08
6 changed files with 139 additions and 9 deletions

View file

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

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,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")
}

View file

@ -154,13 +154,13 @@ func (c *Certificate) AppendCertificates(certs map[string]map[string]*tls.Certif
var SANs []string var SANs []string
if parsedCert.Subject.CommonName != "" { if parsedCert.Subject.CommonName != "" {
SANs = append(SANs, parsedCert.Subject.CommonName) SANs = append(SANs, strings.ToLower(parsedCert.Subject.CommonName))
} }
if parsedCert.DNSNames != nil { if parsedCert.DNSNames != nil {
sort.Strings(parsedCert.DNSNames) sort.Strings(parsedCert.DNSNames)
for _, dnsName := range parsedCert.DNSNames { for _, dnsName := range parsedCert.DNSNames {
if dnsName != parsedCert.Subject.CommonName { 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 { if parsedCert.IPAddresses != nil {
for _, ip := range parsedCert.IPAddresses { for _, ip := range parsedCert.IPAddresses {
if ip.String() != parsedCert.Subject.CommonName { 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) {
domainToCheck string domainToCheck string
dynamicCert string dynamicCert string
expectedCert string expectedCert string
uppercase bool
}{ }{
{ {
desc: "Empty Store, returns no certs", desc: "Empty Store, returns no certs",
@ -45,6 +46,13 @@ func TestGetBestCertificate(t *testing.T) {
dynamicCert: "*.snitest.com", dynamicCert: "*.snitest.com",
expectedCert: "*.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 { for _, test := range testCases {
@ -54,9 +62,9 @@ func TestGetBestCertificate(t *testing.T) {
dynamicMap := map[string]*tls.Certificate{} dynamicMap := map[string]*tls.Certificate{}
if test.dynamicCert != "" { if test.dynamicCert != "" {
cert, err := loadTestCert(test.dynamicCert) cert, err := loadTestCert(test.dynamicCert, test.uppercase)
require.NoError(t, err) require.NoError(t, err)
dynamicMap[test.dynamicCert] = cert dynamicMap[strings.ToLower(test.dynamicCert)] = cert
} }
store := &CertificateStore{ store := &CertificateStore{
@ -66,7 +74,7 @@ func TestGetBestCertificate(t *testing.T) {
var expected *tls.Certificate var expected *tls.Certificate
if test.expectedCert != "" { if test.expectedCert != "" {
cert, err := loadTestCert(test.expectedCert) cert, err := loadTestCert(test.expectedCert, test.uppercase)
require.NoError(t, err) require.NoError(t, err)
expected = cert 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( staticCert, err := tls.LoadX509KeyPair(
fmt.Sprintf("../integration/fixtures/https/%s.cert", 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, "*", "wildcard", -1)), fmt.Sprintf("../integration/fixtures/https/%s.key", strings.Replace(certName, "*", replacement, -1)),
) )
if err != nil { if err != nil {
return nil, err return nil, err