diff --git a/config/setup/tls.go b/config/setup/tls.go index f084e6c6..74732310 100644 --- a/config/setup/tls.go +++ b/config/setup/tls.go @@ -1,10 +1,38 @@ package setup import ( - "github.com/mholt/caddy/middleware" + "crypto/tls" "log" + "strconv" + "strings" + + "github.com/mholt/caddy/middleware" ) +// Map of supported protocols +// SSLv3 will be not supported in next release +var supportedProtocols = map[string]uint16{ + "ssl3.0": tls.VersionSSL30, + "tls1.0": tls.VersionTLS10, + "tls1.1": tls.VersionTLS11, + "tls1.2": tls.VersionTLS12, +} + +// Map of supported ciphers +// For security reasons caddy will not support RC4 ciphers +var supportedCiphers = map[string]uint16{ + "ECDHE-RSA-AES128-GCM-SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + "ECDHE-ECDSA-AES128-GCM-SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + "ECDHE-RSA-AES128-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + "ECDHE-RSA-AES256-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + "ECDHE-ECDSA-AES256-CBC-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + "ECDHE-ECDSA-AES128-CBC-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + "RSA-AES128-CBC-SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA, + "RSA-AES256-CBC-SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA, + "ECDHE-RSA-3DES-EDE-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, + "RSA-3DES-EDE-CBC-SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, +} + func TLS(c *Controller) (middleware.Middleware, error) { c.TLS.Enabled = true if c.Port == "http" { @@ -12,7 +40,7 @@ func TLS(c *Controller) (middleware.Middleware, error) { log.Printf("Warning: TLS was disabled on host http://%s."+ " Make sure you are specifying https://%s in your config (if you haven't already)."+ " If you meant to serve tls on port 80,"+ - " specify port 80 in your config (http://%s:80).", c.Host, c.Host, c.Host) + " specify port 80 in your config (https://%s:80).", c.Host, c.Host, c.Host) } for c.Next() { @@ -25,6 +53,69 @@ func TLS(c *Controller) (middleware.Middleware, error) { return nil, c.ArgErr() } c.TLS.Key = c.Val() + + // Optional block + for c.NextBlock() { + switch c.Val() { + case "protocols": + args := c.RemainingArgs() + if len(args) != 2 { + return nil, c.ArgErr() + } + value, ok := supportedProtocols[strings.ToLower(args[0])] + if !ok { + return nil, c.Errf("Wrong protocol name or protocol not supported '%s'", c.Val()) + + } + c.TLS.ProtocolMinVersion = value + value, ok = supportedProtocols[strings.ToLower(args[1])] + if !ok { + return nil, c.Errf("Wrong protocol name or protocol not supported '%s'", c.Val()) + } + c.TLS.ProtocolMaxVersion = value + case "ciphers": + for c.NextArg() { + value, ok := supportedCiphers[strings.ToUpper(c.Val())] + if !ok { + return nil, c.Errf("Wrong cipher name or cipher not supported '%s'", c.Val()) + } + c.TLS.Ciphers = append(c.TLS.Ciphers, value) + } + case "cache": + if !c.NextArg() { + return nil, c.ArgErr() + } + size, err := strconv.Atoi(c.Val()) + if err != nil { + return nil, c.Errf("Cache parameter should be an number '%s': %v", c.Val(), err) + } + c.TLS.CacheSize = size + default: + return nil, c.Errf("Unknown keyword '%s'") + } + } + } + + // If no Ciphers provided, use all caddy supportedCiphers + if len(c.TLS.Ciphers) == 0 { + for _, v := range supportedCiphers { + c.TLS.Ciphers = append(c.TLS.Ciphers, v) + } + } + + // If no ProtocolMin provided, set default MinVersion to TLSv1.1 for security reasons + if c.TLS.ProtocolMinVersion == 0 { + c.TLS.ProtocolMinVersion = tls.VersionTLS11 + } + + //If no ProtocolMax provided, use crypto/tls default MaxVersion(tls1.2) + if c.TLS.ProtocolMaxVersion == 0 { + c.TLS.ProtocolMaxVersion = tls.VersionTLS12 + } + + //If no cachesize provided, set default to 64 + if c.TLS.CacheSize == 0 { + c.TLS.CacheSize = 64 } return nil, nil diff --git a/config/setup/tls_test.go b/config/setup/tls_test.go new file mode 100644 index 00000000..7ba337de --- /dev/null +++ b/config/setup/tls_test.go @@ -0,0 +1,109 @@ +package setup + +import ( + "crypto/tls" + "testing" +) + +func TestTLSParseNoOptional(t *testing.T) { + c := newTestController(`tls cert.crt cert.key`) + + _, err := TLS(c) + if err != nil { + t.Errorf("Expected no errors, got: %v", err) + } + + if len(c.TLS.Ciphers) != len(supportedCiphers) { + t.Errorf("Expected %v Ciphers, got %v", len(supportedCiphers), len(c.TLS.Ciphers)) + } + + if c.TLS.ProtocolMinVersion != tls.VersionTLS11 { + t.Errorf("Expected 'tls1.1 (0x0302)' as ProtocolMinVersion, got %#v", c.TLS.ProtocolMinVersion) + } + + if c.TLS.ProtocolMaxVersion != tls.VersionTLS12 { + t.Errorf("Expected 'tls1.2 (0x0303)' as ProtocolMaxVersion, got %v", c.TLS.ProtocolMaxVersion) + } + + if c.TLS.CacheSize != 64 { + t.Errorf("Expected CacheSize 64, got %v", c.TLS.CacheSize) + } +} + +func TestTLSParseIncompleteParams(t *testing.T) { + c := newTestController(`tls`) + + _, err := TLS(c) + if err == nil { + t.Errorf("Expected errors, but no error returned") + } + + c = newTestController(`tls cert.key`) + + _, err = TLS(c) + if err == nil { + t.Errorf("Expected errors, but no error returned") + } + +} + +func TestTLSParseWithOptionalParams(t *testing.T) { + params := `tls cert.crt cert.key { + protocols ssl3.0 tls1.2 + ciphers RSA-3DES-EDE-CBC-SHA RSA-AES256-CBC-SHA ECDHE-RSA-AES128-GCM-SHA256 + cache 128 + }` + c := newTestController(params) + + _, err := TLS(c) + if err != nil { + t.Errorf("Expected no errors, got: %v", err) + } + + if c.TLS.ProtocolMinVersion != tls.VersionSSL30 { + t.Errorf("Expected 'ssl3.0 (0x0300)' as ProtocolMinVersion, got %#v", c.TLS.ProtocolMinVersion) + } + + if c.TLS.ProtocolMaxVersion != tls.VersionTLS12 { + t.Errorf("Expected 'tls1.2 (0x0302)' as ProtocolMaxVersion, got %#v", c.TLS.ProtocolMaxVersion) + } + + if len(c.TLS.Ciphers) != 3 { + t.Errorf("Expected 3 Ciphers, got %v", len(c.TLS.Ciphers)) + } + + if c.TLS.CacheSize != 128 { + t.Errorf("Expected CacheSize 128, got %v", c.TLS.CacheSize) + } +} + +func TestTLSParseWithWrongOptionalParams(t *testing.T) { + params := `tls cert.crt cert.key { + cache a + }` + c := newTestController(params) + _, err := TLS(c) + if err == nil { + t.Errorf("Expected errors, but no error returned") + } + + // Test protocols wrong params + params = `tls cert.crt cert.key { + protocols ssl tls + }` + c = newTestController(params) + _, err = TLS(c) + if err == nil { + t.Errorf("Expected errors, but no error returned") + } + + // Test ciphers wrong params + params = `tls cert.crt cert.key { + ciphers not-valid-cipher + }` + c = newTestController(params) + _, err = TLS(c) + if err == nil { + t.Errorf("Expected errors, but no error returned") + } +} diff --git a/server/config.go b/server/config.go index b037b613..502ba423 100644 --- a/server/config.go +++ b/server/config.go @@ -55,8 +55,13 @@ func (c Config) Address() string { // TLSConfig describes how TLS should be configured and used, // if at all. A certificate and key are both required. +// Ciphers, Protocols and CacheSize are optional type TLSConfig struct { - Enabled bool - Certificate string - Key string + Enabled bool + Certificate string + Key string + Ciphers []uint16 + ProtocolMinVersion uint16 + ProtocolMaxVersion uint16 + CacheSize int } diff --git a/server/server.go b/server/server.go index 2416f21d..e139e760 100644 --- a/server/server.go +++ b/server/server.go @@ -132,8 +132,18 @@ func ListenAndServeTLSWithSNI(srv *http.Server, tlsConfigs []TLSConfig) error { } config.BuildNameToCertificate() - // Add a session cache LRU algorithm with default capacity (64) - config.ClientSessionCache = tls.NewLRUClientSessionCache(0) + // Here we change some crypto/tls defaults based on caddyfile + // If no config provided, we set defaults focused in security + + // Add a session cache LRU algorithm + config.ClientSessionCache = tls.NewLRUClientSessionCache(tlsConfigs[0].CacheSize) + + config.MinVersion = tlsConfigs[0].ProtocolMinVersion + config.MaxVersion = tlsConfigs[0].ProtocolMaxVersion + config.CipherSuites = tlsConfigs[0].Ciphers + + // Server ciphers have priority over client ciphers + config.PreferServerCipherSuites = true conn, err := net.Listen("tcp", addr) if err != nil {