diff --git a/.travis.yml b/.travis.yml index 2d62da78..69cb9ad4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,10 @@ language: go sudo: false go: - - 1.5 - - 1.6 - 1.7 + - 1.8 + - 1.9 + - 1.10.x - master install: diff --git a/README.md b/README.md index 78b0ae1d..67bc1b6e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ [![Build Status](https://travis-ci.org/flashmob/go-guerrilla.svg?branch=master)](https://travis-ci.org/flashmob/go-guerrilla) +Breaking change: The structure of the config has recently changed to accommodate more advanced TLS settings. + Go-Guerrilla SMTP Daemon ==================== diff --git a/api.go b/api.go index 7efd1f39..92238b47 100644 --- a/api.go +++ b/api.go @@ -93,7 +93,7 @@ func (d *Daemon) LoadConfig(path string) (AppConfig, error) { var ac AppConfig data, err := ioutil.ReadFile(path) if err != nil { - return ac, fmt.Errorf("Could not read config file: %s", err.Error()) + return ac, fmt.Errorf("could not read config file: %s", err.Error()) } err = ac.Load(data) if err != nil { diff --git a/api_test.go b/api_test.go index 4ac72448..0250e76d 100644 --- a/api_test.go +++ b/api_test.go @@ -133,13 +133,15 @@ func TestSMTPLoadFile(t *testing.T) { "is_enabled" : true, "host_name":"mail.guerrillamail.com", "max_size": 100017, - "private_key_file":"config_test.go", - "public_key_file":"config_test.go", "timeout":160, "listen_interface":"127.0.0.1:2526", - "start_tls_on":false, - "tls_always_on":false, - "max_clients": 2 + "max_clients": 2, + "tls" : { + "private_key_file":"config_test.go", + "public_key_file":"config_test.go", + "start_tls_on":false, + "tls_always_on":false + } } ] } @@ -161,13 +163,15 @@ func TestSMTPLoadFile(t *testing.T) { "is_enabled" : true, "host_name":"mail.guerrillamail.com", "max_size": 100017, - "private_key_file":"config_test.go", - "public_key_file":"config_test.go", "timeout":160, "listen_interface":"127.0.0.1:2526", - "start_tls_on":false, - "tls_always_on":false, - "max_clients": 2 + "max_clients": 2, + "tls" : { + "private_key_file":"config_test.go", + "public_key_file":"config_test.go", + "start_tls_on":false, + "tls_always_on":false + } } ] } @@ -310,9 +314,9 @@ func TestSetConfigError(t *testing.T) { // lets add a new server with bad TLS sc.ListenInterface = "127.0.0.1:2527" - sc.StartTLSOn = true - sc.PublicKeyFile = "tests/testlog" // totally wrong :-> - sc.PublicKeyFile = "tests/testlog" // totally wrong :-> + sc.TLS.StartTLSOn = true + sc.TLS.PublicKeyFile = "tests/testlog" // totally wrong :-> + sc.TLS.PrivateKeyFile = "tests/testlog" // totally wrong :-> cfg.Servers = append(cfg.Servers, sc) diff --git a/backends/p_guerrilla_db_redis.go b/backends/p_guerrilla_db_redis.go index 97e3c028..8c20dee3 100644 --- a/backends/p_guerrilla_db_redis.go +++ b/backends/p_guerrilla_db_redis.go @@ -319,7 +319,7 @@ func (g *GuerrillaDBAndRedisBackend) sqlConnect() (*sql.DB, error) { // do we have access? _, err = db.Query("SELECT mail_id FROM " + g.config.Table + " LIMIT 1") if err != nil { - Log().Error("cannot select table", err) + Log().Error("cannot select table:", err) return nil, err } return db, nil diff --git a/backends/p_sql.go b/backends/p_sql.go index 118900a8..24d971d9 100644 --- a/backends/p_sql.go +++ b/backends/p_sql.go @@ -246,9 +246,12 @@ func SQL() Decorator { recipient, s.ip2bint(e.RemoteIP).Bytes(), // ip_addr store as varbinary(16) trimToLimit(e.MailFrom.String(), 255), // return_path - e.TLS, // is_tls - mid, // message_id - replyTo, // reply_to + // is_tls + e.TLS, + // message_id + mid, + // reply_to + replyTo, sender, ) diff --git a/client.go b/client.go index dc74f047..a23bee32 100644 --- a/client.go +++ b/client.go @@ -178,7 +178,7 @@ func (c *client) getID() uint64 { // UpgradeToTLS upgrades a client connection to TLS func (client *client) upgradeToTLS(tlsConfig *tls.Config) error { var tlsConn *tls.Conn - // load the config thread-safely + // wrap client.conn in a new TLS server side connection tlsConn = tls.Server(client.conn, tlsConfig) // Call handshake here to get any handshake error before reading starts err := tlsConn.Handshake() diff --git a/cmd/guerrillad/serve_test.go b/cmd/guerrillad/serve_test.go index 3c5118a4..e991912e 100644 --- a/cmd/guerrillad/serve_test.go +++ b/cmd/guerrillad/serve_test.go @@ -43,27 +43,31 @@ var configJsonA = ` "is_enabled" : true, "host_name":"mail.test.com", "max_size": 1000000, - "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem", - "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem", "timeout":180, "listen_interface":"127.0.0.1:3536", - "start_tls_on":true, - "tls_always_on":false, "max_clients": 1000, - "log_file" : "../../tests/testlog" + "log_file" : "../../tests/testlog", + "tls" : { + "private_key_file":"../../tests/mail2.guerrillamail.com.key.pem", + "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem", + "start_tls_on":true, + "tls_always_on":false + } }, { "is_enabled" : false, "host_name":"enable.test.com", "max_size": 1000000, - "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem", - "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem", "timeout":180, "listen_interface":"127.0.0.1:2228", - "start_tls_on":true, - "tls_always_on":false, "max_clients": 1000, - "log_file" : "../../tests/testlog" + "log_file" : "../../tests/testlog", + "tls" : { + "private_key_file":"../../tests/mail2.guerrillamail.com.key.pem", + "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem", + "start_tls_on":true, + "tls_always_on":false + } } ] } @@ -92,20 +96,22 @@ var configJsonB = ` "is_enabled" : true, "host_name":"mail.test.com", "max_size": 1000000, - "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem", - "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem", "timeout":180, "listen_interface":"127.0.0.1:3536", - "start_tls_on":true, - "tls_always_on":false, "max_clients": 1000, - "log_file" : "../../tests/testlog" + "log_file" : "../../tests/testlog", + "tls" : { + "private_key_file":"../../tests/mail2.guerrillamail.com.key.pem", + "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem", + "start_tls_on":true, + "tls_always_on":false + } } ] } ` -// backend_name changed, is guerrilla-redis-db + added a server +// added a server var configJsonC = ` { "log_file" : "../../tests/testlog", @@ -118,46 +124,49 @@ var configJsonC = ` "guerrillamail.net", "guerrillamail.org" ], - "backend_name": "guerrilla-redis-db", "backend_config" : { "sql_driver": "mysql", - "sql_dsn": "root:ok@tcp(127.0.0.1:3306)/gmail_mail?readTimeout=10&writeTimeout=10", + "sql_dsn": "root:ok@tcp(127.0.0.1:3306)/gmail_mail?readTimeout=10s&writeTimeout=10s", "mail_table":"new_mail", "redis_interface" : "127.0.0.1:6379", "redis_expire_seconds" : 7200, "save_workers_size" : 3, "primary_mail_host":"sharklasers.com", "save_workers_size" : 1, - "save_process": "HeadersParser|Debugger", - "log_received_mails": true + "save_process": "HeadersParser|Debugger", + "log_received_mails": true }, "servers" : [ { "is_enabled" : true, "host_name":"mail.test.com", "max_size": 1000000, - "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem", - "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem", "timeout":180, "listen_interface":"127.0.0.1:25", - "start_tls_on":true, - "tls_always_on":false, "max_clients": 1000, - "log_file" : "../../tests/testlog" + "log_file" : "../../tests/testlog", + "tls" : { + "private_key_file":"../../tests/mail2.guerrillamail.com.key.pem", + "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem", + "start_tls_on":true, + "tls_always_on":false + } }, { "is_enabled" : true, "host_name":"mail.test.com", "max_size":1000000, - "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem", - "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem", "timeout":180, "listen_interface":"127.0.0.1:465", - "start_tls_on":false, - "tls_always_on":true, "max_clients":500, - "log_file" : "../../tests/testlog" + "log_file" : "../../tests/testlog", + "tls" : { + "private_key_file":"../../tests/mail2.guerrillamail.com.key.pem", + "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem", + "start_tls_on":false, + "tls_always_on":true + } } ] } @@ -186,27 +195,31 @@ var configJsonD = ` "is_enabled" : true, "host_name":"mail.test.com", "max_size": 1000000, - "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem", - "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem", "timeout":180, "listen_interface":"127.0.0.1:2552", - "start_tls_on":true, - "tls_always_on":false, "max_clients": 1000, - "log_file" : "../../tests/testlog" + "log_file" : "../../tests/testlog", + "tls" : { + "private_key_file":"../../tests/mail2.guerrillamail.com.key.pem", + "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem", + "start_tls_on":true, + "tls_always_on":false + } }, { "is_enabled" : true, "host_name":"secure.test.com", "max_size":1000000, - "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem", - "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem", "timeout":180, "listen_interface":"127.0.0.1:4655", - "start_tls_on":false, - "tls_always_on":true, "max_clients":500, - "log_file" : "../../tests/testlog" + "log_file" : "../../tests/testlog", + "tls" : { + "private_key_file":"../../tests/mail2.guerrillamail.com.key.pem", + "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem", + "start_tls_on":false, + "tls_always_on":true + } } ] } @@ -231,7 +244,7 @@ var configJsonE = ` "save_process": "GuerrillaRedisDB", "log_received_mails" : true, "sql_driver": "mysql", - "sql_dsn": "root:secret@tcp(127.0.0.1:3306)/gmail_mail?readTimeout=10&writeTimeout=10", + "sql_dsn": "root:secret@tcp(127.0.0.1:3306)/gmail_mail?readTimeout=10s&writeTimeout=10s", "mail_table":"new_mail", "redis_interface" : "127.0.0.1:6379", "redis_expire_seconds" : 7200, @@ -243,27 +256,31 @@ var configJsonE = ` "is_enabled" : true, "host_name":"mail.test.com", "max_size": 1000000, - "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem", - "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem", "timeout":180, "listen_interface":"127.0.0.1:2552", - "start_tls_on":true, - "tls_always_on":false, "max_clients": 1000, - "log_file" : "../../tests/testlog" + "log_file" : "../../tests/testlog", + "tls" : { + "private_key_file":"../../tests/mail2.guerrillamail.com.key.pem", + "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem", + "start_tls_on":true, + "tls_always_on":false + } }, { "is_enabled" : true, "host_name":"secure.test.com", "max_size":1000000, - "private_key_file":"../..//tests/mail2.guerrillamail.com.key.pem", - "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem", "timeout":180, "listen_interface":"127.0.0.1:4655", - "start_tls_on":false, - "tls_always_on":true, "max_clients":500, - "log_file" : "../../tests/testlog" + "log_file" : "../../tests/testlog", + "tls" : { + "private_key_file":"../../tests/mail2.guerrillamail.com.key.pem", + "public_key_file":"../../tests/mail2.guerrillamail.com.cert.pem", + "start_tls_on":false, + "tls_always_on":true + } } ] } @@ -667,6 +684,32 @@ func TestServerStopEvent(t *testing.T) { } +// just a utility for debugging when using the debugger, skipped by default +func TestDebug(t *testing.T) { + + t.SkipNow() + conf := guerrilla.ServerConfig{ListenInterface: "127.0.0.1:2526"} + if conn, buffin, err := test.Connect(conf, 20); err != nil { + t.Error("Could not connect to new server", conf.ListenInterface, err) + } else { + if result, err := test.Command(conn, buffin, "HELO"); err == nil { + expect := "250 mai1.guerrillamail.com Hello" + if strings.Index(result, expect) != 0 { + t.Error("Expected", expect, "but got", result) + } else { + if result, err = test.Command(conn, buffin, "RCPT TO:test@grr.la"); err == nil { + expect := "250 2.1.5 OK" + if strings.Index(result, expect) != 0 { + t.Error("Expected:", expect, "but got:", result) + } + } + } + } + conn.Close() + + } +} + // Start with configJsonD.json, // then connect to 127.0.0.1:4655 & HELO & try RCPT TO with an invalid host [grr.la] // then change the config to enable add new host [grr.la] to allowed_hosts diff --git a/config.go b/config.go index e3dfd351..2819b810 100644 --- a/config.go +++ b/config.go @@ -41,20 +41,13 @@ type ServerConfig struct { // MaxSize is the maximum size of an email that will be accepted for delivery. // Defaults to 10 Mebibytes MaxSize int64 `json:"max_size"` - // PrivateKeyFile path to cert private key in PEM format. Will be ignored if blank - PrivateKeyFile string `json:"private_key_file"` - // PublicKeyFile path to cert (public key) chain in PEM format. - // Will be ignored if blank - PublicKeyFile string `json:"public_key_file"` + // TLS Configuration + TLS ServerTLSConfig `json:"tls,omitempty"` // Timeout specifies the connection timeout in seconds. Defaults to 30 Timeout int `json:"timeout"` // Listen interface specified in : - defaults to 127.0.0.1:2525 ListenInterface string `json:"listen_interface"` - // StartTLSOn should we offer STARTTLS command. Cert must be valid. - // False by default - StartTLSOn bool `json:"start_tls_on,omitempty"` - // TLSAlwaysOn run this server as a pure TLS server, i.e. SMTPS - TLSAlwaysOn bool `json:"tls_always_on,omitempty"` + // MaxClients controls how many maxiumum clients we can handle at once. // Defaults to 100 MaxClients int `json:"max_clients"` @@ -64,10 +57,95 @@ type ServerConfig struct { // XClientOn when using a proxy such as Nginx, XCLIENT command is used to pass the // original client's IP address & client's HELO XClientOn bool `json:"xclient_on,omitempty"` +} + +type ServerTLSConfig struct { + + // StartTLSOn should we offer STARTTLS command. Cert must be valid. + // False by default + StartTLSOn bool `json:"start_tls_on,omitempty"` + // AlwaysOn run this server as a pure TLS server, i.e. SMTPS + AlwaysOn bool `json:"tls_always_on,omitempty"` + // PrivateKeyFile path to cert private key in PEM format. + PrivateKeyFile string `json:"private_key_file"` + // PublicKeyFile path to cert (public key) chain in PEM format. + PublicKeyFile string `json:"public_key_file"` + + // TLS Protocols to use. [0] = min, [1]max + // Use Go's default if empty + Protocols []string `json:"protocols,omitempty"` + // TLS Ciphers to use. + // Use Go's default if empty + Ciphers []string `json:"ciphers,omitempty"` + // TLS Curves to use. + // Use Go's default if empty + Curves []string `json:"curves,omitempty"` + // TLS Root cert authorities to use. "A PEM encoded CA's certificate file. + // Defaults to system's root CA file if empty + RootCAs string `json:"root_cas_file,omitempty"` + // declares the policy the server will follow for TLS Client Authentication. + // Use Go's default if empty + ClientAuthType string `json:"client_auth_type,omitempty"` + // controls whether the server selects the + // client's most preferred ciphersuite + PreferServerCipherSuites bool `json:"prefer_server_cipher_suites,omitempty"` // The following used to watch certificate changes so that the TLS can be reloaded - _privateKeyFile_mtime int - _publicKeyFile_mtime int + _privateKeyFile_mtime int64 + _publicKeyFile_mtime int64 +} + +// https://golang.org/pkg/crypto/tls/#pkg-constants +// Ciphers introduced before Go 1.7 are listed here, +// ciphers since Go 1.8, see tls_go1.8.go +var TLSCiphers = map[string]uint16{ + + // // Note: Generally avoid using CBC unless for compatibility + "TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, + "TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA, + "TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + + "TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA, + "TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256, + "TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + + "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, + "TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + + // Include to prevent downgrade attacks + "TLS_FALLBACK_SCSV": tls.TLS_FALLBACK_SCSV, +} + +// https://golang.org/pkg/crypto/tls/#pkg-constants +var TLSProtocols = map[string]uint16{ + "ssl3.0": tls.VersionSSL30, + "tls1.0": tls.VersionTLS10, + "tls1.1": tls.VersionTLS11, + "tls1.2": tls.VersionTLS12, +} + +// https://golang.org/pkg/crypto/tls/#CurveID +var TLSCurves = map[string]tls.CurveID{ + "P256": tls.CurveP256, + "P384": tls.CurveP384, + "P521": tls.CurveP521, +} + +// https://golang.org/pkg/crypto/tls/#ClientAuthType +var TLSClientAuthTypes = map[string]tls.ClientAuthType{ + "NoClientCert": tls.NoClientCert, + "RequestClientCert": tls.RequestClientCert, + "RequireAnyClientCert": tls.RequireAnyClientCert, + "VerifyClientCertIfGiven": tls.VerifyClientCertIfGiven, + "RequireAndVerifyClientCert": tls.RequireAndVerifyClientCert, } // Unmarshalls json data into AppConfig struct and any other initialization of the struct @@ -274,11 +352,16 @@ func (c *AppConfig) setBackendDefaults() error { // All events are fired and run synchronously func (sc *ServerConfig) emitChangeEvents(oldServer *ServerConfig, app Guerrilla) { // get a list of changes - changes := getDiff( + changes := getChanges( *oldServer, *sc, ) - if len(changes) > 0 { + tlsChanges := getChanges( + (*oldServer).TLS, + (*sc).TLS, + ) + + if len(changes) > 0 || len(tlsChanges) > 0 { // something changed in the server config app.Publish(EventConfigServerConfig, sc) } @@ -309,22 +392,7 @@ func (sc *ServerConfig) emitChangeEvents(oldServer *ServerConfig, app Guerrilla) app.Publish(EventConfigServerMaxClients, sc) } - // tls changed - if ok := func() bool { - if _, ok := changes["PrivateKeyFile"]; ok { - return true - } - if _, ok := changes["PublicKeyFile"]; ok { - return true - } - if _, ok := changes["StartTLSOn"]; ok { - return true - } - if _, ok := changes["TLSAlwaysOn"]; ok { - return true - } - return false - }(); ok { + if len(tlsChanges) > 0 { app.Publish(EventConfigServerTLSConfig, sc) } } @@ -338,37 +406,31 @@ func (sc *ServerConfig) loadTlsKeyTimestamps() error { iface, err.Error())) } - if info, err := os.Stat(sc.PrivateKeyFile); err == nil { - sc._privateKeyFile_mtime = info.ModTime().Second() + if info, err := os.Stat(sc.TLS.PrivateKeyFile); err == nil { + sc.TLS._privateKeyFile_mtime = info.ModTime().Unix() } else { return statErr(sc.ListenInterface, err) } - if info, err := os.Stat(sc.PublicKeyFile); err == nil { - sc._publicKeyFile_mtime = info.ModTime().Second() + if info, err := os.Stat(sc.TLS.PublicKeyFile); err == nil { + sc.TLS._publicKeyFile_mtime = info.ModTime().Unix() } else { return statErr(sc.ListenInterface, err) } return nil } -// Gets the timestamp of the TLS certificates. Returns a unix time of when they were last modified -// when the config was read. We use this info to determine if TLS needs to be re-loaded. -func (sc *ServerConfig) getTlsKeyTimestamps() (int, int) { - return sc._privateKeyFile_mtime, sc._publicKeyFile_mtime -} - // Validate validates the server's configuration. func (sc *ServerConfig) Validate() error { var errs Errors - if sc.StartTLSOn || sc.TLSAlwaysOn { - if sc.PublicKeyFile == "" { + if sc.TLS.StartTLSOn || sc.TLS.AlwaysOn { + if sc.TLS.PublicKeyFile == "" { errs = append(errs, errors.New("PublicKeyFile is empty")) } - if sc.PrivateKeyFile == "" { + if sc.TLS.PrivateKeyFile == "" { errs = append(errs, errors.New("PrivateKeyFile is empty")) } - if _, err := tls.LoadX509KeyPair(sc.PublicKeyFile, sc.PrivateKeyFile); err != nil { + if _, err := tls.LoadX509KeyPair(sc.TLS.PublicKeyFile, sc.TLS.PrivateKeyFile); err != nil { errs = append(errs, errors.New(fmt.Sprintf("cannot use TLS config for [%s], %v", sc.ListenInterface, err))) } @@ -380,28 +442,42 @@ func (sc *ServerConfig) Validate() error { return nil } -// Returns a diff between struct a & struct b. +// Gets the timestamp of the TLS certificates. Returns a unix time of when they were last modified +// when the config was read. We use this info to determine if TLS needs to be re-loaded. +func (stc *ServerTLSConfig) getTlsKeyTimestamps() (int64, int64) { + return stc._privateKeyFile_mtime, stc._publicKeyFile_mtime +} + +// Returns value changes between struct a & struct b. // Results are returned in a map, where each key is the name of the field that was different. // a and b are struct values, must not be pointer // and of the same struct type -func getDiff(a interface{}, b interface{}) map[string]interface{} { +func getChanges(a interface{}, b interface{}) map[string]interface{} { ret := make(map[string]interface{}, 5) compareWith := structtomap(b) for key, val := range structtomap(a) { + if sliceOfStr, ok := val.([]string); ok { + val, _ = json.Marshal(sliceOfStr) + val = string(val.([]uint8)) + } + if sliceOfStr, ok := compareWith[key].([]string); ok { + compareWith[key], _ = json.Marshal(sliceOfStr) + compareWith[key] = string(compareWith[key].([]uint8)) + } if val != compareWith[key] { ret[key] = compareWith[key] } } - // detect tls changes (have the key files been modified?) - if oldServer, ok := a.(ServerConfig); ok { - t1, t2 := oldServer.getTlsKeyTimestamps() - if newServer, ok := b.(ServerConfig); ok { - t3, t4 := newServer.getTlsKeyTimestamps() + // detect changes to TLS keys (have the key files been modified?) + if oldTLS, ok := a.(ServerTLSConfig); ok { + t1, t2 := oldTLS.getTlsKeyTimestamps() + if newTLS, ok := b.(ServerTLSConfig); ok { + t3, t4 := newTLS.getTlsKeyTimestamps() if t1 != t3 { - ret["PrivateKeyFile"] = newServer.PrivateKeyFile + ret["PrivateKeyFile"] = newTLS.PrivateKeyFile } if t2 != t4 { - ret["PublicKeyFile"] = newServer.PublicKeyFile + ret["PublicKeyFile"] = newTLS.PublicKeyFile } } } @@ -409,7 +485,8 @@ func getDiff(a interface{}, b interface{}) map[string]interface{} { } // Convert fields of a struct to a map -// only able to convert int, bool and string; not recursive +// only able to convert int, bool, slice-of-strings and string; not recursive +// slices are marshal'd to json for convenient comparison later func structtomap(obj interface{}) map[string]interface{} { ret := make(map[string]interface{}, 0) v := reflect.ValueOf(obj) @@ -417,9 +494,11 @@ func structtomap(obj interface{}) map[string]interface{} { for index := 0; index < v.NumField(); index++ { vField := v.Field(index) fName := t.Field(index).Name - - switch vField.Kind() { + k := vField.Kind() + switch k { case reflect.Int: + fallthrough + case reflect.Int64: value := vField.Int() ret[fName] = value case reflect.String: @@ -428,6 +507,8 @@ func structtomap(obj interface{}) map[string]interface{} { case reflect.Bool: value := vField.Bool() ret[fName] = value + case reflect.Slice: + ret[fName] = vField.Interface().([]string) } } return ret diff --git a/config_test.go b/config_test.go index b027a670..1fb90fd7 100644 --- a/config_test.go +++ b/config_test.go @@ -33,55 +33,59 @@ var configJsonA = ` "is_enabled" : true, "host_name":"mail.guerrillamail.com", "max_size": 100017, - "private_key_file":"config_test.go", - "public_key_file":"config_test.go", "timeout":160, "listen_interface":"127.0.0.1:2526", - "start_tls_on":false, - "tls_always_on":false, - "max_clients": 2 + "max_clients": 2, + "tls" : { + "start_tls_on":false, + "tls_always_on":false, + "private_key_file":"config_test.go", + "public_key_file":"config_test.go" + } }, - { "is_enabled" : true, "host_name":"mail2.guerrillamail.com", "max_size":1000001, - "private_key_file":"./tests/mail2.guerrillamail.com.key.pem", - "public_key_file":"./tests/mail2.guerrillamail.com.cert.pem", "timeout":180, "listen_interface":"127.0.0.1:2527", - "start_tls_on":true, - "tls_always_on":false, - "max_clients":1 + "max_clients":1, + "tls" : { + "private_key_file":"./tests/mail2.guerrillamail.com.key.pem", + "public_key_file":"./tests/mail2.guerrillamail.com.cert.pem", + "tls_always_on":false, + "start_tls_on":true + } }, { "is_enabled" : true, "host_name":"mail.stopme.com", - "max_size": 100017, - "private_key_file":"config_test.go", - "public_key_file":"config_test.go", + "max_size": 100017, "timeout":160, - "listen_interface":"127.0.0.1:9999", - "start_tls_on":false, - "tls_always_on":false, - "max_clients": 2 + "listen_interface":"127.0.0.1:9999", + "max_clients": 2, + "tls" : { + "private_key_file":"config_test.go", + "public_key_file":"config_test.go", + "start_tls_on":false, + "tls_always_on":false + } }, - { "is_enabled" : true, "host_name":"mail.disableme.com", "max_size": 100017, - "private_key_file":"config_test.go", - "public_key_file":"config_test.go", "timeout":160, "listen_interface":"127.0.0.1:3333", - "start_tls_on":false, - "tls_always_on":false, - "max_clients": 2 + "max_clients": 2, + "tls" : { + "private_key_file":"config_test.go", + "public_key_file":"config_test.go", + "start_tls_on":false, + "tls_always_on":false + } } - - ] } ` @@ -106,52 +110,60 @@ var configJsonB = ` "is_enabled" : true, "host_name":"mail.guerrillamail.com", "max_size": 100017, - "private_key_file":"config_test.go", - "public_key_file":"config_test.go", "timeout":161, "listen_interface":"127.0.0.1:2526", - "start_tls_on":false, - "tls_always_on":true, - "max_clients": 3 + "max_clients": 3, + "tls" : { + "private_key_file":"./config_test.go", + "public_key_file":"./config_test.go", + "start_tls_on":false, + "tls_always_on":true + } }, { "is_enabled" : true, "host_name":"mail2.guerrillamail.com", "max_size": 100017, - "private_key_file":"./tests/mail2.guerrillamail.com.key.pem", - "public_key_file": "./tests/mail2.guerrillamail.com.cert.pem", "timeout":160, "listen_interface":"127.0.0.1:2527", - "start_tls_on":true, - "tls_always_on":false, "log_file" : "./tests/testlog", - "max_clients": 2 + "max_clients": 2, + "tls" : { + "private_key_file":"./tests/mail2.guerrillamail.com.key.pem", + "public_key_file": "./tests/mail2.guerrillamail.com.cert.pem", + "start_tls_on":true, + "tls_always_on":false + } }, { "is_enabled" : true, "host_name":"mail.guerrillamail.com", "max_size":1000001, - "private_key_file":"config_test.go", - "public_key_file":"config_test.go", "timeout":180, "listen_interface":"127.0.0.1:4654", - "start_tls_on":false, - "tls_always_on":false, - "max_clients":1 + "max_clients":1, + "tls" : { + "private_key_file":"config_test.go", + "public_key_file":"config_test.go", + "start_tls_on":false, + "tls_always_on":false + } }, { "is_enabled" : false, "host_name":"mail.disbaleme.com", "max_size": 100017, - "private_key_file":"config_test.go", - "public_key_file":"config_test.go", "timeout":160, "listen_interface":"127.0.0.1:3333", - "start_tls_on":true, - "tls_always_on":false, - "max_clients": 2 + "max_clients": 2, + "tls" : { + "private_key_file":"config_test.go", + "public_key_file":"config_test.go", + "start_tls_on":true, + "tls_always_on":false + } } ] } @@ -169,8 +181,8 @@ func TestConfigLoad(t *testing.T) { t.SkipNow() } // did we got the timestamps? - if ac.Servers[0]._privateKeyFile_mtime <= 0 { - t.Error("failed to read timestamp for _privateKeyFile_mtime, got", ac.Servers[0]._privateKeyFile_mtime) + if ac.Servers[0].TLS._privateKeyFile_mtime <= 0 { + t.Error("failed to read timestamp for _privateKeyFile_mtime, got", ac.Servers[0].TLS._privateKeyFile_mtime) } } @@ -209,8 +221,8 @@ func TestConfigChangeEvents(t *testing.T) { // simulate timestamp change time.Sleep(time.Second + time.Millisecond*500) - os.Chtimes(oldconf.Servers[1].PrivateKeyFile, time.Now(), time.Now()) - os.Chtimes(oldconf.Servers[1].PublicKeyFile, time.Now(), time.Now()) + os.Chtimes(oldconf.Servers[1].TLS.PrivateKeyFile, time.Now(), time.Now()) + os.Chtimes(oldconf.Servers[1].TLS.PublicKeyFile, time.Now(), time.Now()) newconf := &AppConfig{} newconf.Load([]byte(configJsonB)) newconf.Servers[0].LogFile = log.OutputOff.String() // test for log file change diff --git a/goguerrilla.conf.sample b/goguerrilla.conf.sample index 66fb42ba..0914f814 100644 --- a/goguerrilla.conf.sample +++ b/goguerrilla.conf.sample @@ -22,27 +22,39 @@ "is_enabled" : true, "host_name":"mail.test.com", "max_size": 1000000, - "private_key_file":"/path/to/pem/file/test.com.key", - "public_key_file":"/path/to/pem/file/test.com.crt", "timeout":180, "listen_interface":"127.0.0.1:25", - "start_tls_on":true, - "tls_always_on":false, "max_clients": 1000, - "log_file" : "stderr" + "log_file" : "stderr", + "tls" : { + "start_tls_on":true, + "tls_always_on":false, + "private_key_file":"/path/to/pem/file/test.com.key", + "public_key_file":"/path/to/pem/file/test.com.crt", + "protocols" : ["ssl3.0", "tls1.2"], + "ciphers" : ["TLS_FALLBACK_SCSV", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", "TLS_RSA_WITH_RC4_128_SHA", "TLS_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", "TLS_ECDHE_RSA_WITH_RC4_128_SHA", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"], + "curves" : ["P256", "P384", "P521", "X25519"], + "client_auth_type" : "NoClientCert" + } }, { "is_enabled" : false, "host_name":"mail.test.com", "max_size":1000000, - "private_key_file":"/path/to/pem/file/test.com.key", - "public_key_file":"/path/to/pem/file/test.com.crt", "timeout":180, "listen_interface":"127.0.0.1:465", - "start_tls_on":false, - "tls_always_on":true, "max_clients":500, - "log_file" : "stderr" + "log_file" : "stderr", + "tls" : { + "private_key_file":"/path/to/pem/file/test.com.key", + "public_key_file":"/path/to/pem/file/test.com.crt", + "start_tls_on":false, + "tls_always_on":true, + "protocols" : ["ssl3.0", "tls1.2"], + "ciphers" : ["TLS_FALLBACK_SCSV", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", "TLS_RSA_WITH_RC4_128_SHA", "TLS_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", "TLS_ECDHE_RSA_WITH_RC4_128_SHA", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"], + "curves" : ["P256", "P384", "P521", "X25519"], + "client_auth_type" : "NoClientCert" + } } ] } diff --git a/guerrilla.go b/guerrilla.go index 714be608..1dd47773 100644 --- a/guerrilla.go +++ b/guerrilla.go @@ -247,6 +247,7 @@ func (g *guerrilla) subscribeEvents() { // server config was updated g.Subscribe(EventConfigServerConfig, func(sc *ServerConfig) { g.setServerConfig(sc) + g.mainlog().Infof("server %s config change event, a new config has been saved", sc.ListenInterface) }) // add a new server to the config & start diff --git a/server.go b/server.go index cd6391d2..ce85486f 100644 --- a/server.go +++ b/server.go @@ -11,10 +11,12 @@ import ( "sync/atomic" "time" + "crypto/x509" "github.com/flashmob/go-guerrilla/backends" "github.com/flashmob/go-guerrilla/log" "github.com/flashmob/go-guerrilla/mail" "github.com/flashmob/go-guerrilla/response" + "io/ioutil" ) const ( @@ -100,8 +102,8 @@ func newServer(sc *ServerConfig, b backends.Backend, l log.Logger) (*server, err func (s *server) configureSSL() error { sConfig := s.configStore.Load().(ServerConfig) - if sConfig.TLSAlwaysOn || sConfig.StartTLSOn { - cert, err := tls.LoadX509KeyPair(sConfig.PublicKeyFile, sConfig.PrivateKeyFile) + if sConfig.TLS.AlwaysOn || sConfig.TLS.StartTLSOn { + cert, err := tls.LoadX509KeyPair(sConfig.TLS.PublicKeyFile, sConfig.TLS.PrivateKeyFile) if err != nil { return fmt.Errorf("error while loading the certificate: %s", err) } @@ -110,6 +112,47 @@ func (s *server) configureSSL() error { ClientAuth: tls.VerifyClientCertIfGiven, ServerName: sConfig.Hostname, } + if len(sConfig.TLS.Protocols) > 0 { + if min, ok := TLSProtocols[sConfig.TLS.Protocols[0]]; ok { + tlsConfig.MinVersion = min + } + } + if len(sConfig.TLS.Protocols) > 1 { + if max, ok := TLSProtocols[sConfig.TLS.Protocols[1]]; ok { + tlsConfig.MaxVersion = max + } + } + if len(sConfig.TLS.Ciphers) > 0 { + for _, val := range sConfig.TLS.Ciphers { + if c, ok := TLSCiphers[val]; ok { + tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, c) + } + } + } + if len(sConfig.TLS.Curves) > 0 { + for _, val := range sConfig.TLS.Curves { + if c, ok := TLSCurves[val]; ok { + tlsConfig.CurvePreferences = append(tlsConfig.CurvePreferences, c) + } + } + } + if len(sConfig.TLS.RootCAs) > 0 { + caCert, err := ioutil.ReadFile(sConfig.TLS.RootCAs) + if err != nil { + s.log().WithError(err).Errorf("failed opening TLSRootCAs file [%s]", sConfig.TLS.RootCAs) + } else { + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + tlsConfig.RootCAs = caCertPool + } + + } + if len(sConfig.TLS.ClientAuthType) > 0 { + if ca, ok := TLSClientAuthTypes[sConfig.TLS.ClientAuthType]; ok { + tlsConfig.ClientAuth = ca + } + } + tlsConfig.PreferServerCipherSuites = sConfig.TLS.PreferServerCipherSuites tlsConfig.Rand = rand.Reader s.tlsConfigStore.Store(tlsConfig) } @@ -303,7 +346,7 @@ func (server *server) handleClient(client *client) { // Also, Last line has no dash - help := "250 HELP" - if sc.TLSAlwaysOn { + if sc.TLS.AlwaysOn { tlsConfig, ok := server.tlsConfigStore.Load().(*tls.Config) if !ok { server.mainlog().Error("Failed to load *tls.Config") @@ -315,7 +358,7 @@ func (server *server) handleClient(client *client) { client.kill() } } - if !sc.StartTLSOn { + if !sc.TLS.StartTLSOn { // STARTTLS turned off, don't advertise it advertiseTLS = "" } @@ -460,7 +503,7 @@ func (server *server) handleClient(client *client) { client.sendResponse(response.Canned.SuccessDataCmd) client.state = ClientData - case sc.StartTLSOn && strings.Index(cmd, "STARTTLS") == 0: + case sc.TLS.StartTLSOn && strings.Index(cmd, "STARTTLS") == 0: client.sendResponse(response.Canned.SuccessStartTLSCmd) client.state = ClientStartTLS @@ -512,7 +555,7 @@ func (server *server) handleClient(client *client) { client.resetTransaction() case ClientStartTLS: - if !client.TLS && sc.StartTLSOn { + if !client.TLS && sc.TLS.StartTLSOn { tlsConfig, ok := server.tlsConfigStore.Load().(*tls.Config) if !ok { server.mainlog().Error("Failed to load *tls.Config") diff --git a/server_test.go b/server_test.go index 23a24a07..8fd27236 100644 --- a/server_test.go +++ b/server_test.go @@ -8,26 +8,31 @@ import ( "strings" "sync" + "crypto/tls" "fmt" "github.com/flashmob/go-guerrilla/backends" "github.com/flashmob/go-guerrilla/log" "github.com/flashmob/go-guerrilla/mail" "github.com/flashmob/go-guerrilla/mocks" + "io/ioutil" "net" + "os" ) // getMockServerConfig gets a mock ServerConfig struct used for creating a new server func getMockServerConfig() *ServerConfig { sc := &ServerConfig{ - IsEnabled: true, // not tested here - Hostname: "saggydimes.test.com", - MaxSize: 1024, // smtp message max size - PrivateKeyFile: "./tests/mail.guerrillamail.com.key.pem", - PublicKeyFile: "./tests/mail.guerrillamail.com.cert.pem", + IsEnabled: true, // not tested here + Hostname: "saggydimes.test.com", + MaxSize: 1024, // smtp message max size + TLS: ServerTLSConfig{ + PrivateKeyFile: "./tests/mail.guerrillamail.com.key.pem", + PublicKeyFile: "./tests/mail.guerrillamail.com.cert.pem", + StartTLSOn: true, + AlwaysOn: false, + }, Timeout: 5, ListenInterface: "127.0.0.1:2529", - StartTLSOn: true, - TLSAlwaysOn: false, MaxClients: 30, // not tested here LogFile: "./tests/testlog", } @@ -60,6 +65,150 @@ func getMockServerConn(sc *ServerConfig, t *testing.T) (*mocks.Conn, *server) { return conn, server } +// test the RootCAs tls config setting +var rootCAPK = `-----BEGIN CERTIFICATE----- +MIIDqjCCApKgAwIBAgIJALh2TrsBR5MiMA0GCSqGSIb3DQEBCwUAMGkxCzAJBgNV +BAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEhMB8G +A1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDDAlsb2NhbGhv +c3QwIBcNMTgwNTE4MDYzOTU2WhgPMjExODA0MjQwNjM5NTZaMGkxCzAJBgNVBAYT +AlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEhMB8GA1UE +CgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDDAlsb2NhbGhvc3Qw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCcb0ulYT1o5ysor5UtWYW +q/ZY3PyK3/4YBZq5JoX4xk7GNQQ+3p/Km7QPoBXfgjFLZXEV2R0bE5hHMXfLa5Xb +64acb9VqCqDvPFXcaNP4rEdBKDVN2p0PEi917tcKBSrZn5Yl+iOhtcBpQDvhHgn/ +9MdmIAKB3+yK+4l9YhT40XfDXCQqzfg4XcNaEgTzZHcDJz+KjWJuJChprcx27MTI +Ndxs9nmFA2rK16rjgjtwjZ4t9dXsljdOcx59s6dIQ0GnEM8qdKxi/vEx4+M/hbGf +v7H75LsuKRrVJINAmfy9fmc6VAXjFU0ZVxGK5eVnzsh/hY08TSSrlCCKAJpksjJz +AgMBAAGjUzBRMB0GA1UdDgQWBBSZsYWs+8FYe4z4c6LLmFB4TeeV/jAfBgNVHSME +GDAWgBSZsYWs+8FYe4z4c6LLmFB4TeeV/jAPBgNVHRMBAf8EBTADAQH/MA0GCSqG +SIb3DQEBCwUAA4IBAQAcXt/FaILkOCMj8bTUx42vi2N9ZTiEuRbYi24IyGOokbDR +pSsIxiz+HDPUuX6/X/mHidl24aS9wdv5JTXMr44/BeGK1WC7gMueZBxAqONpaG1Q +VU0e3q1YwXKcupKQ7kVWl0fuY3licv0+s4zBcTLKkmWAYqsb/n0KtCMyqewi+Rqa +Zj5Z3OcWOq9Ad9fZWKcG8k/sgeTk9z0X1mZcEyWWxqsUmxvN+SdWLoug1xJVVbMN +CipZ0vBIi9KOhQgzuIFhoTcd6myUtov52/EFqlX6UuFpY2gEWw/f/yu+SI08v4w9 +KwxgAKBkhx2JYZKtu1EsPIMDyS0aahcDnHqnrGAi +-----END CERTIFICATE-----` + +var clientPrvKey = `-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA5ZLmMBdKkVyVmN0VhDSFGvgKp24ejHPCv+wfuf3vlU9cwKfH +R3vejleZAVRcidscfA0Jsub/Glsr0XwecagtpvTI+Fp1ik6sICOz+VW3958qaAi8 +TjbUMjcDHJeSLcjr725CH5uIvhRzR+daYaJQhAcL2MEt8M9WIF6AjtDZEH9R6oM8 +t5FkO0amImlnipYXNBFghmzkZzfGXXRQLw2A+u6keLcjCrn9h2BaofGIjQfYcu/3 +fH4cIFR4z/soGKameqnCUz7dWmbf4tAI+8QR0VXXBKhiHDm98tPSeH994hC52Uul +rjEVcM5Uox5hazS2PK06oSc1YuFZONqeeGqj6wIDAQABAoIBADERzRHKaK3ZVEBw +QQEZGLpC+kP/TZhHxgCvv7hJhsQrSnADbJzi5RcXsiSOm5j7tILvZntO1IgVpLAK +D5fLkrZ069/pteXyGuhjuTw6DjBnXPEPrPAq2ABDse6SlzQiFgv/TTLkU74NMPbV +hIQJ5ZvSxb12zRMDviz9Bg2ApmTX6k2iPjQBnEHgKzb64IdMcEb5HE1qNt0v0lRA +sGBMZZKQWbt2m0pSbAbnB3S9GcpJkRgFFMdTaUScIWO6ICT2hBP2pw2/4M2Zrmlt +bsyWu9uswBzhvu+/pg2E66V6mji0EzDMlXqjlO5jro6t7P33t1zkd/i/ykKmtDLp +IpR94UECgYEA9Y4EIjOyaBWJ6TRQ6a/tehGPbwIOgvEiTYXRJqdU49qn/i4YZjSm +F4iibJz+JeOIQXSwa9F7gRlaspIuHgIJoer7BrITMuhr+afqMLkxK0pijul/qAbm +HdpFn8IxjpNu4/GoAENbEVy50SMST9yWh5ulEkHHftd4/NJKoJQ2PZ8CgYEA71bb +lFVh1MFclxRKECmpyoqUAzwGlMoHJy/jaBYuWG4X7rzxqDRrgPH3as6gXpRiSZ+K +5fC+wcU7dKnHtJOkBDk6J5ev2+hbwg+yq3w4+l3bPDvf2TJyXjXjRDZo12pxFD58 +ybCOF6ItbIDXqT5pvo3PMjgMwu1Ycie+h6hA3jUCgYEAsq93XpQT/R2/T44cWxEE +VFG2+GacvLhP5+26ttAJPA1/Nb3BT458Vp+84iCT6GpcWpVZU/wKTXVvxIYPPRLq +g4MEzGiFBASRngiMqIv6ta/ZbHmJxXHPvmV5SLn9aezrQsA1KovZFxdMuF03FBpH +B8NBKbnoO+r8Ra2ZVKTFm60CgYAZw8Dpi/N3IsWj4eRDLyj/C8H5Qyn2NHVmq4oQ +d2rPzDI5Wg+tqs7z15hp4Ap1hAW8pTcfn7X5SBEpculzr/0VE1AGWRbuVmoiTuxN +95ZupVHnfw6O5BZZu/VWL4FDx0qbAksOrznso2G+b3RH3NcnUz69yjjddw1xZIPn +OJ6bDQKBgDUcWYu/2amU18D5vJpppUgRq2084WPUeXsaniTbmWfOC8NAn8CKLY0N +V4yGSu98apDuqEVqL0VFQEgqK+5KTvRdXXYi36XYRbbVUgV13xveq2YTvjNbPM60 +QWG9YmgH7hVYGusuh5nQeS0qiIpwyws2H5mBVrGXrQ1Xb0MLWj8/ +-----END RSA PRIVATE KEY-----` + +// signed using the Root (rootCAPK) +var clientPubKey = `-----BEGIN CERTIFICATE----- +MIIDWDCCAkACCQCHoh4OvUySOzANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MCAX +DTE4MDUxODA2NDQ0NVoYDzMwMTcwOTE4MDY0NDQ1WjBxMQswCQYDVQQGEwJVUzET +MBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEhMB8G +A1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDDAlsb2NhbGhv +c3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDlkuYwF0qRXJWY3RWE +NIUa+Aqnbh6Mc8K/7B+5/e+VT1zAp8dHe96OV5kBVFyJ2xx8DQmy5v8aWyvRfB5x +qC2m9Mj4WnWKTqwgI7P5Vbf3nypoCLxONtQyNwMcl5ItyOvvbkIfm4i+FHNH51ph +olCEBwvYwS3wz1YgXoCO0NkQf1Hqgzy3kWQ7RqYiaWeKlhc0EWCGbORnN8ZddFAv +DYD67qR4tyMKuf2HYFqh8YiNB9hy7/d8fhwgVHjP+ygYpqZ6qcJTPt1aZt/i0Aj7 +xBHRVdcEqGIcOb3y09J4f33iELnZS6WuMRVwzlSjHmFrNLY8rTqhJzVi4Vk42p54 +aqPrAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAIQmlo8iCpyYggkbpfDmThBPHfy1 +cZcCi/tRFoFe1ccwn2ezLMIKmW38ZebiroawwqrZgU6AP+dMxVKLMjpyLPSrpFKa +3o/LbVF7qMfH8/y2q8t7javd6rxoENH9uxLyHhauzI1iWy0whoDWBNiZrPBTBCjq +jDGZARZqGyrPeXi+RNe1cMvZCxAFy7gqEtWFLWWrp0gYNPvxkHhhQBrUcF+8T/Nf +9G4hKZSN/KAgC0CNBVuNrdyNc3l8H66BfwwL5X0+pesBYZM+MEfmBZOo+p7OWx2r +ug8tR8eSL1vGleONtFRBUVG7NbtjhBf9FhvPZcSRR10od/vWHku9E01i4xg= +-----END CERTIFICATE-----` + +func TestTLSConfig(t *testing.T) { + + if err := ioutil.WriteFile("rootca.test.pem", []byte(rootCAPK), 0644); err != nil { + t.Fatal("couldn't create rootca.test.pem file.", err) + return + } + if err := ioutil.WriteFile("client.test.key", []byte(clientPrvKey), 0644); err != nil { + t.Fatal("couldn't create client.test.key file.", err) + return + } + if err := ioutil.WriteFile("client.test.pem", []byte(clientPubKey), 0644); err != nil { + t.Fatal("couldn't create client.test.pem file.", err) + return + } + + s := server{} + s.setConfig(&ServerConfig{ + TLS: ServerTLSConfig{ + StartTLSOn: true, + PrivateKeyFile: "client.test.key", + PublicKeyFile: "client.test.pem", + RootCAs: "rootca.test.pem", + ClientAuthType: "NoClientCert", + Curves: []string{"P521", "P384"}, + Ciphers: []string{"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA"}, + Protocols: []string{"tls1.0", "tls1.2"}, + }, + }) + s.configureSSL() + + c := s.tlsConfigStore.Load().(*tls.Config) + + if len(c.CurvePreferences) != 2 { + t.Error("c.CurvePreferences should have two elements") + } else if c.CurvePreferences[0] != tls.CurveP521 && c.CurvePreferences[1] != tls.CurveP384 { + t.Error("c.CurvePreferences curves not setup") + } + if strings.Index(string(c.RootCAs.Subjects()[0]), "Mountain View") == -1 { + t.Error("c.RootCAs not correctly set") + } + if c.ClientAuth != tls.NoClientCert { + t.Error("c.ClientAuth should be tls.NoClientCert") + } + + if len(c.CipherSuites) != 2 { + t.Error("c.CipherSuites length should be 2") + } + + if c.CipherSuites[0] != tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 && c.CipherSuites[1] != tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA { + t.Error("c.CipherSuites not correctly set ") + } + + if c.MinVersion != tls.VersionTLS10 { + t.Error("c.MinVersion should be tls.VersionTLS10") + } + + if c.MaxVersion != tls.VersionTLS12 { + t.Error("c.MinVersion should be tls.VersionTLS10") + } + + if c.PreferServerCipherSuites != false { + t.Error("PreferServerCipherSuites should be false") + } + + os.Remove("rootca.test.pem") + os.Remove("client.test.key") + os.Remove("client.test.pem") + +} + func TestHandleClient(t *testing.T) { var mainlog log.Logger var logOpenError error diff --git a/tests/client.go b/tests/client.go index 7b2bc364..b6bab608 100644 --- a/tests/client.go +++ b/tests/client.go @@ -14,7 +14,7 @@ func Connect(serverConfig guerrilla.ServerConfig, deadline time.Duration) (net.C var bufin *bufio.Reader var conn net.Conn var err error - if serverConfig.TLSAlwaysOn { + if serverConfig.TLS.AlwaysOn { // start tls automatically conn, err = tls.Dial("tcp", serverConfig.ListenInterface, &tls.Config{ InsecureSkipVerify: true, diff --git a/tests/guerrilla_test.go b/tests/guerrilla_test.go index 0742ad54..e3a380c4 100644 --- a/tests/guerrilla_test.go +++ b/tests/guerrilla_test.go @@ -40,7 +40,6 @@ import ( type TestConfig struct { guerrilla.AppConfig - BackendName string `json:"backend_name"` BackendConfig map[string]interface{} `json:"backend_config"` } @@ -86,28 +85,32 @@ var configJson = ` "is_enabled" : true, "host_name":"mail.guerrillamail.com", "max_size": 100017, - "private_key_file":"/vagrant/projects/htdocs/guerrilla/config/ssl/guerrillamail.com.key", - "public_key_file":"/vagrant/projects/htdocs/guerrilla/config/ssl/guerrillamail.com.crt", "timeout":160, - "listen_interface":"127.0.0.1:2526", - "start_tls_on":true, - "tls_always_on":false, + "listen_interface":"127.0.0.1:2526", "max_clients": 2, - "log_file" : "" + "log_file" : "", + "tls" : { + "private_key_file":"/vagrant/projects/htdocs/guerrilla/config/ssl/guerrillamail.com.key", + "public_key_file":"/vagrant/projects/htdocs/guerrilla/config/ssl/guerrillamail.com.crt", + "start_tls_on":true, + "tls_always_on":false + } }, { "is_enabled" : true, "host_name":"mail.guerrillamail.com", "max_size":1000001, - "private_key_file":"/vagrant/projects/htdocs/guerrilla/config/ssl/guerrillamail.com.key", - "public_key_file":"/vagrant/projects/htdocs/guerrilla/config/ssl/guerrillamail.com.crt", "timeout":180, "listen_interface":"127.0.0.1:4654", - "start_tls_on":false, - "tls_always_on":true, "max_clients":1, - "log_file" : "" + "log_file" : "", + "tls" : { + "private_key_file":"/vagrant/projects/htdocs/guerrilla/config/ssl/guerrillamail.com.key", + "public_key_file":"/vagrant/projects/htdocs/guerrilla/config/ssl/guerrillamail.com.crt", + "start_tls_on":false, + "tls_always_on":true + } } ] } @@ -125,8 +128,8 @@ func getBackend(backendConfig map[string]interface{}, l log.Logger) (backends.Ba func setupCerts(c *TestConfig) { for i := range c.Servers { testcert.GenerateCert(c.Servers[i].Hostname, "", 365*24*time.Hour, false, 2048, "P256", "./") - c.Servers[i].PrivateKeyFile = c.Servers[i].Hostname + ".key.pem" - c.Servers[i].PublicKeyFile = c.Servers[i].Hostname + ".cert.pem" + c.Servers[i].TLS.PrivateKeyFile = c.Servers[i].Hostname + ".key.pem" + c.Servers[i].TLS.PublicKeyFile = c.Servers[i].Hostname + ".cert.pem" } } diff --git a/tls_go1.8.go b/tls_go1.8.go new file mode 100644 index 00000000..2cdc0bf8 --- /dev/null +++ b/tls_go1.8.go @@ -0,0 +1,17 @@ +// +build go1.8 + +package guerrilla + +import "crypto/tls" + +// add ciphers introduced since Go 1.8 +func init() { + TLSCiphers["TLS_RSA_WITH_AES_128_CBC_SHA256"] = tls.TLS_RSA_WITH_AES_128_CBC_SHA256 + TLSCiphers["TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256"] = tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 + TLSCiphers["TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256"] = tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 + TLSCiphers["TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"] = tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 + TLSCiphers["TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305"] = tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305 + TLSCiphers["TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305"] = tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305 + + TLSCurves["X25519"] = tls.X25519 +}