From cee8bbdce74c33bbd4a820c8c9a1e9459b34920f Mon Sep 17 00:00:00 2001 From: "fox.cpp" Date: Wed, 15 Jul 2020 17:58:47 +0300 Subject: [PATCH] Migrate TLS certificate loading to use modules for sources --- docs/man/maddy-tls.5.scd | 42 ++++- docs/man/maddy.5.scd | 9 +- .../config/{tls_client.go => tls/client.go} | 7 +- .../config/{tls_general.go => tls/general.go} | 25 +-- .../config/{tls_server.go => tls/server.go} | 153 +++++++----------- framework/module/tls_loader.go | 21 +++ internal/check/rspamd/rspamd.go | 3 +- internal/endpoint/imap/imap.go | 3 +- internal/endpoint/smtp/smtp.go | 3 +- internal/target/remote/remote.go | 3 +- internal/target/smtp/smtp_downstream.go | 3 +- internal/tls/file.go | 147 +++++++++++++++++ internal/tls/self_signed.go | 92 +++++++++++ maddy.go | 4 +- 14 files changed, 387 insertions(+), 128 deletions(-) rename framework/config/{tls_client.go => tls/client.go} (90%) rename framework/config/{tls_general.go => tls/general.go} (80%) rename framework/config/{tls_server.go => tls/server.go} (50%) create mode 100644 framework/module/tls_loader.go create mode 100644 internal/tls/file.go create mode 100644 internal/tls/self_signed.go diff --git a/docs/man/maddy-tls.5.scd b/docs/man/maddy-tls.5.scd index d98802c..dd791e8 100644 --- a/docs/man/maddy-tls.5.scd +++ b/docs/man/maddy-tls.5.scd @@ -4,17 +4,47 @@ maddy-tls(5) "maddy mail server" "maddy reference documentation" # TLS server configuration -You can specify other TLS-related options in a configuration block for 'tls' -directive: +TLS certificates are obtained by modules called "certificate loaders". 'tls' directive +arguments specify name of loader to use and arguments. Due to syntax limitations +advanced configuration for loader should be specified using 'loader' directive, see +below. ``` -tls cert.pem cert.pem { - protocols tls1.2 tls1.3 - curve X25519 - ciphers ... +tls file cert.pem key.pem { + protocols tls1.2 tls1.3 + curve X25519 + ciphers ... +} + +tls { + loader file cert.pem key.pem { + # Options for loader go here. + } + protocols tls1.2 tls1.3 + curve X25519 + ciphers ... } ``` +## Available certificate loaders + +- file + + Accepts argument pairs specifying certificate and then key. + E.g. 'tls file certA.pem keyA.pem certB.pem keyB.pem' + + If multiple certificates are listed, SNI will be used. + +- off + + Not really a loader but a special value for tls directive, explicitly disables TLS for + endpoint(s). + +## Advanced TLS configuration + +*Note: maddy uses secure defaults and TLS handshake is resistant to active downgrade attacks.* +*There is no need to change anything in most cases.* + *Syntax*: ++ protocols _min_version_ _max_version_ ++ protocols _version_ ++ diff --git a/docs/man/maddy.5.scd b/docs/man/maddy.5.scd index 3bad42d..573bb64 100644 --- a/docs/man/maddy.5.scd +++ b/docs/man/maddy.5.scd @@ -119,8 +119,8 @@ Domain that is used in From field for auto-generated messages (such as Delivery Status Notifications). *Syntax*: ++ - tls _cert_file_ _pkey_file_ ++ - tls self_signed ++ + tls file _cert_file_ _pkey_file_ ++ + tls _module reference_ ++ tls off ++ *Default*: not specified @@ -129,11 +129,8 @@ Default TLS certificate to use for all endpoints. Must be present in either all endpoint modules configuration blocks or as global directive. -Use of 'self_signed' generates temporary self-signed certificate, this useful -for testing but should be used only for it. - You can also specify other configuration options such as cipher suites and TLS -version. See TLS server configuration for details. maddy uses reasonable +version. See maddy-tls(5) for details. maddy uses reasonable cipher suites and TLS versions by default so you generally don't have to worry about it. diff --git a/framework/config/tls_client.go b/framework/config/tls/client.go similarity index 90% rename from framework/config/tls_client.go rename to framework/config/tls/client.go index 49daca1..d89abaa 100644 --- a/framework/config/tls_client.go +++ b/framework/config/tls/client.go @@ -1,4 +1,4 @@ -package config +package tls import ( "crypto/tls" @@ -6,13 +6,14 @@ import ( "fmt" "io/ioutil" + "github.com/foxcpp/maddy/framework/config" "github.com/foxcpp/maddy/framework/log" ) -func TLSClientBlock(m *Map, node Node) (interface{}, error) { +func TLSClientBlock(m *config.Map, node config.Node) (interface{}, error) { cfg := tls.Config{} - childM := NewMap(nil, node) + childM := config.NewMap(nil, node) var ( tlsVersions [2]uint16 rootCAPaths []string diff --git a/framework/config/tls_general.go b/framework/config/tls/general.go similarity index 80% rename from framework/config/tls_general.go rename to framework/config/tls/general.go index fe3b6eb..425a254 100644 --- a/framework/config/tls_general.go +++ b/framework/config/tls/general.go @@ -1,8 +1,9 @@ -package config +package tls import ( "crypto/tls" + "github.com/foxcpp/maddy/framework/config" "github.com/foxcpp/maddy/framework/log" ) @@ -51,26 +52,26 @@ var strCurvesMap = map[string]tls.CurveID{ // minimum and maximum supported TLS versions. // // It returns [2]uint16 value for use in corresponding fields from tls.Config. -func TLSVersionsDirective(m *Map, node Node) (interface{}, error) { +func TLSVersionsDirective(m *config.Map, node config.Node) (interface{}, error) { switch len(node.Args) { case 1: value, ok := strVersionsMap[node.Args[0]] if !ok { - return nil, NodeErr(node, "invalid TLS version value: %s", node.Args[0]) + return nil, config.NodeErr(node, "invalid TLS version value: %s", node.Args[0]) } return [2]uint16{value, value}, nil case 2: minValue, ok := strVersionsMap[node.Args[0]] if !ok { - return nil, NodeErr(node, "invalid TLS version value: %s", node.Args[0]) + return nil, config.NodeErr(node, "invalid TLS version value: %s", node.Args[0]) } maxValue, ok := strVersionsMap[node.Args[1]] if !ok { - return nil, NodeErr(node, "invalid TLS version value: %s", node.Args[1]) + return nil, config.NodeErr(node, "invalid TLS version value: %s", node.Args[1]) } return [2]uint16{minValue, maxValue}, nil default: - return nil, NodeErr(node, "expected 1 or 2 arguments") + return nil, config.NodeErr(node, "expected 1 or 2 arguments") } } @@ -78,16 +79,16 @@ func TLSVersionsDirective(m *Map, node Node) (interface{}, error) { // list of ciphers to offer to clients (or to use for outgoing connections). // // It returns list of []uint16 with corresponding cipher IDs. -func TLSCiphersDirective(m *Map, node Node) (interface{}, error) { +func TLSCiphersDirective(m *config.Map, node config.Node) (interface{}, error) { if len(node.Args) == 0 { - return nil, NodeErr(node, "expected at least 1 argument, got 0") + return nil, config.NodeErr(node, "expected at least 1 argument, got 0") } res := make([]uint16, 0, len(node.Args)) for _, arg := range node.Args { cipherId, ok := strCiphersMap[arg] if !ok { - return nil, NodeErr(node, "unknown cipher: %s", arg) + return nil, config.NodeErr(node, "unknown cipher: %s", arg) } res = append(res, cipherId) } @@ -99,16 +100,16 @@ func TLSCiphersDirective(m *Map, node Node) (interface{}, error) { // elliptic curves to use during TLS key exchange. // // It returns []tls.CurveID. -func TLSCurvesDirective(m *Map, node Node) (interface{}, error) { +func TLSCurvesDirective(m *config.Map, node config.Node) (interface{}, error) { if len(node.Args) == 0 { - return nil, NodeErr(node, "expected at least 1 argument, got 0") + return nil, config.NodeErr(node, "expected at least 1 argument, got 0") } res := make([]tls.CurveID, 0, len(node.Args)) for _, arg := range node.Args { curveId, ok := strCurvesMap[arg] if !ok { - return nil, NodeErr(node, "unknown curve: %s", arg) + return nil, config.NodeErr(node, "unknown curve: %s", arg) } res = append(res, curveId) } diff --git a/framework/config/tls_server.go b/framework/config/tls/server.go similarity index 50% rename from framework/config/tls_server.go rename to framework/config/tls/server.go index 83c070e..77c758e 100644 --- a/framework/config/tls_server.go +++ b/framework/config/tls/server.go @@ -1,4 +1,4 @@ -package config +package tls import ( "crypto/ecdsa" @@ -10,68 +10,33 @@ import ( "math/big" "net" "os" - "sync" + "strings" "time" - "github.com/foxcpp/maddy/framework/hooks" + "github.com/foxcpp/maddy/framework/config" + modconfig "github.com/foxcpp/maddy/framework/config/module" "github.com/foxcpp/maddy/framework/log" + "github.com/foxcpp/maddy/framework/module" ) type TLSConfig struct { - initCfg Node - - l sync.Mutex - cfg *tls.Config + loader module.TLSLoader + baseCfg tls.Config } -func (cfg *TLSConfig) Get() *tls.Config { - cfg.l.Lock() - defer cfg.l.Unlock() - if cfg.cfg == nil { - return nil +func (cfg *TLSConfig) Get() (*tls.Config, error) { + if cfg.loader == nil { + return nil, nil } - return cfg.cfg.Clone() -} + tlsCfg := cfg.baseCfg.Clone() -func (cfg *TLSConfig) read(m *Map, node Node, generateSelfSig bool) error { - cfg.l.Lock() - defer cfg.l.Unlock() - - switch len(node.Args) { - case 1: - switch node.Args[0] { - case "off": - cfg.cfg = nil - return nil - case "self_signed": - if !generateSelfSig { - return nil - } - - tlsCfg := &tls.Config{ - MinVersion: tls.VersionTLS10, - MaxVersion: tls.VersionTLS13, - } - if err := makeSelfSignedCert(tlsCfg); err != nil { - return err - } - log.Println("tls: using self-signed certificate, this is not secure!") - cfg.cfg = tlsCfg - return nil - default: - log.Println(node.Name, node.Args) - return NodeErr(node, "unexpected argument (%s), want 'off' or 'self_signed'", node.Args[0]) - } - case 2: - tlsCfg, err := readTLSBlock(m, node) - if err != nil { - return err - } - cfg.cfg = tlsCfg - return nil - default: - return NodeErr(node, "expected 1 or 2 arguments") + certs, err := cfg.loader.LoadCerts() + if err != nil { + return nil, err } + tlsCfg.Certificates = certs + + return tlsCfg, nil } // TLSDirective reads the TLS configuration and adds the reload handler to @@ -79,55 +44,54 @@ func (cfg *TLSConfig) read(m *Map, node Node, generateSelfSig bool) error { // // The returned value is *tls.TLSConfig with GetConfigForClient set. // If the 'tls off' is used, returned value is nil. -func TLSDirective(m *Map, node Node) (interface{}, error) { - cfg := TLSConfig{ - initCfg: node, - } - if err := cfg.read(m, node, true); err != nil { +func TLSDirective(m *config.Map, node config.Node) (interface{}, error) { + cfg, err := readTLSBlock(m.Globals, node) + if err != nil { return nil, err } - hooks.AddHook(hooks.EventReload, func() { - log.Debugln("tls: reloading certificates") - if err := cfg.read(NewMap(nil, cfg.initCfg), cfg.initCfg, false); err != nil { - log.DefaultLogger.Error("tls: failed to load new certs", err) - } - }) - go func() { - t := time.NewTicker(1 * time.Minute) - for range t.C { - log.Debugln("tls: reloading certificates") - if err := cfg.read(NewMap(nil, cfg.initCfg), cfg.initCfg, false); err != nil { - log.DefaultLogger.Error("tls: failed to load new certs", err) - } - } - }() - // Return nil so callers can check whether TLS is enabled easier. - if cfg.cfg == nil { + if cfg.loader == nil { return nil, nil } return &tls.Config{ GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) { - return cfg.Get(), nil + return cfg.Get() }, }, nil } -func readTLSBlock(m *Map, blockNode Node) (*tls.Config, error) { - cfg := tls.Config{ - PreferServerCipherSuites: true, +func readTLSBlock(globals map[string]interface{}, blockNode config.Node) (*TLSConfig, error) { + baseCfg := tls.Config{} + + var loader module.TLSLoader + if len(blockNode.Args) > 0 { + if blockNode.Args[0] == "off" { + return nil, nil + } + + if _, err := os.Stat(blockNode.Args[0]); err == nil || strings.Contains(blockNode.Args[0], "/") { + log.Println("'tls cert_path key_path' syntax is deprecated, use 'tls file cert_path key_path'") + blockNode.Args = append([]string{"file"}, blockNode.Args...) + } + + err := modconfig.ModuleFromNode("tls.loader", blockNode.Args, config.Node{}, globals, &loader) + if err != nil { + return nil, err + } } - childM := NewMap(nil, blockNode) + childM := config.NewMap(globals, blockNode) var tlsVersions [2]uint16 - if len(blockNode.Args) != 2 { - return nil, NodeErr(blockNode, "two arguments required") - } - certPath := blockNode.Args[0] - keyPath := blockNode.Args[1] + childM.Custom("loader", false, false, func() (interface{}, error) { + return loader, nil + }, func(m *config.Map, node config.Node) (interface{}, error) { + var l module.TLSLoader + err := modconfig.ModuleFromNode("tls.loader", blockNode.Args, config.Node{}, globals, &l) + return l, err + }, &loader) childM.Custom("protocols", false, false, func() (interface{}, error) { return [2]uint16{0, 0}, nil @@ -135,29 +99,28 @@ func readTLSBlock(m *Map, blockNode Node) (*tls.Config, error) { childM.Custom("ciphers", false, false, func() (interface{}, error) { return nil, nil - }, TLSCiphersDirective, &cfg.CipherSuites) + }, TLSCiphersDirective, &baseCfg.CipherSuites) childM.Custom("curves", false, false, func() (interface{}, error) { return nil, nil - }, TLSCurvesDirective, &cfg.CurvePreferences) + }, TLSCurvesDirective, &baseCfg.CurvePreferences) if _, err := childM.Process(); err != nil { return nil, err } - cert, err := tls.LoadX509KeyPair(certPath, keyPath) - if err != nil { - return nil, err + if len(baseCfg.CipherSuites) != 0 { + baseCfg.PreferServerCipherSuites = true } - log.Debugf("tls: using %s : %s", certPath, keyPath) - cfg.Certificates = append(cfg.Certificates, cert) - - cfg.MinVersion = tlsVersions[0] - cfg.MaxVersion = tlsVersions[1] + baseCfg.MinVersion = tlsVersions[0] + baseCfg.MaxVersion = tlsVersions[1] log.Debugf("tls: min version: %x, max version: %x", tlsVersions[0], tlsVersions[1]) - return &cfg, nil + return &TLSConfig{ + loader: loader, + baseCfg: baseCfg, + }, nil } func makeSelfSignedCert(config *tls.Config) error { diff --git a/framework/module/tls_loader.go b/framework/module/tls_loader.go new file mode 100644 index 0000000..91a5167 --- /dev/null +++ b/framework/module/tls_loader.go @@ -0,0 +1,21 @@ +package module + +import "crypto/tls" + +// TLSLoader interface is module interface that can be used to supply TLS +// certificates to TLS-enabled endpoints. +// +// The interface is intentionally kept simple, all configuration and parameters +// necessary are to be provided using conventional module configuration. +// +// If loader returns multiple certificate chains - endpoint will serve them +// based on SNI matching. +// +// Note that loading function will be called for each connections - it is +// highly recommended to cache parsed form. +// +// Modules implementing this interface should be registered with prefix +// "tls.loader." in name. +type TLSLoader interface { + LoadCerts() ([]tls.Certificate, error) +} diff --git a/internal/check/rspamd/rspamd.go b/internal/check/rspamd/rspamd.go index 1e53fa6..e57cad3 100644 --- a/internal/check/rspamd/rspamd.go +++ b/internal/check/rspamd/rspamd.go @@ -14,6 +14,7 @@ import ( "github.com/foxcpp/maddy/framework/buffer" "github.com/foxcpp/maddy/framework/config" modconfig "github.com/foxcpp/maddy/framework/config/module" + tls2 "github.com/foxcpp/maddy/framework/config/tls" "github.com/foxcpp/maddy/framework/exterrors" "github.com/foxcpp/maddy/framework/log" "github.com/foxcpp/maddy/framework/module" @@ -74,7 +75,7 @@ func (c *Check) Init(cfg *config.Map) error { cfg.Custom("tls_client", true, false, func() (interface{}, error) { return tls.Config{}, nil - }, config.TLSClientBlock, &tlsConfig) + }, tls2.TLSClientBlock, &tlsConfig) cfg.String("api_path", false, false, c.apiPath, &c.apiPath) cfg.String("settings_id", false, false, "", &c.settingsID) cfg.String("tag", false, false, "maddy", &c.tag) diff --git a/internal/endpoint/imap/imap.go b/internal/endpoint/imap/imap.go index 81bd159..b066251 100644 --- a/internal/endpoint/imap/imap.go +++ b/internal/endpoint/imap/imap.go @@ -24,6 +24,7 @@ import ( "github.com/foxcpp/go-imap-sql/children" "github.com/foxcpp/maddy/framework/config" modconfig "github.com/foxcpp/maddy/framework/config/module" + tls2 "github.com/foxcpp/maddy/framework/config/tls" "github.com/foxcpp/maddy/framework/log" "github.com/foxcpp/maddy/framework/module" "github.com/foxcpp/maddy/internal/auth" @@ -68,7 +69,7 @@ func (endp *Endpoint) Init(cfg *config.Map) error { return endp.saslAuth.AddProvider(m, node) }) cfg.Custom("storage", false, true, nil, modconfig.StorageDirective, &endp.Store) - cfg.Custom("tls", true, true, nil, config.TLSDirective, &endp.tlsConfig) + cfg.Custom("tls", true, true, nil, tls2.TLSDirective, &endp.tlsConfig) cfg.Bool("insecure_auth", false, false, &insecureAuth) cfg.Bool("io_debug", false, false, &ioDebug) cfg.Bool("io_errors", false, false, &ioErrors) diff --git a/internal/endpoint/smtp/smtp.go b/internal/endpoint/smtp/smtp.go index 46ed4c3..7cf60bb 100644 --- a/internal/endpoint/smtp/smtp.go +++ b/internal/endpoint/smtp/smtp.go @@ -19,6 +19,7 @@ import ( "github.com/foxcpp/maddy/framework/buffer" "github.com/foxcpp/maddy/framework/config" modconfig "github.com/foxcpp/maddy/framework/config/module" + tls2 "github.com/foxcpp/maddy/framework/config/tls" "github.com/foxcpp/maddy/framework/dns" "github.com/foxcpp/maddy/framework/exterrors" "github.com/foxcpp/maddy/framework/future" @@ -224,7 +225,7 @@ func (endp *Endpoint) setConfig(cfg *config.Map) error { } return autoBufferMode(1*1024*1024 /* 1 MiB */, path), nil }, bufferModeDirective, &endp.buffer) - cfg.Custom("tls", true, endp.name != "lmtp", nil, config.TLSDirective, &endp.serv.TLSConfig) + cfg.Custom("tls", true, endp.name != "lmtp", nil, tls2.TLSDirective, &endp.serv.TLSConfig) cfg.Bool("insecure_auth", endp.name == "lmtp", false, &endp.serv.AllowInsecureAuth) cfg.Bool("io_debug", false, false, &ioDebug) cfg.Bool("debug", true, false, &endp.Log.Debug) diff --git a/internal/target/remote/remote.go b/internal/target/remote/remote.go index f911ac5..5c1b5b0 100644 --- a/internal/target/remote/remote.go +++ b/internal/target/remote/remote.go @@ -20,6 +20,7 @@ import ( "github.com/foxcpp/maddy/framework/buffer" "github.com/foxcpp/maddy/framework/config" modconfig "github.com/foxcpp/maddy/framework/config/module" + tls2 "github.com/foxcpp/maddy/framework/config/tls" "github.com/foxcpp/maddy/framework/dns" "github.com/foxcpp/maddy/framework/exterrors" "github.com/foxcpp/maddy/framework/log" @@ -86,7 +87,7 @@ func (rt *Target) Init(cfg *config.Map) error { cfg.Bool("debug", true, false, &rt.Log.Debug) cfg.Custom("tls_client", true, false, func() (interface{}, error) { return &tls.Config{}, nil - }, config.TLSClientBlock, &rt.tlsConfig) + }, tls2.TLSClientBlock, &rt.tlsConfig) cfg.Custom("mx_auth", false, false, func() (interface{}, error) { // Default is "no policies" to follow the principles of explicit // configuration (if it is not requested - it is not done). diff --git a/internal/target/smtp/smtp_downstream.go b/internal/target/smtp/smtp_downstream.go index ee72d77..cd4a036 100644 --- a/internal/target/smtp/smtp_downstream.go +++ b/internal/target/smtp/smtp_downstream.go @@ -20,6 +20,7 @@ import ( "github.com/emersion/go-smtp" "github.com/foxcpp/maddy/framework/buffer" "github.com/foxcpp/maddy/framework/config" + tls2 "github.com/foxcpp/maddy/framework/config/tls" "github.com/foxcpp/maddy/framework/exterrors" "github.com/foxcpp/maddy/framework/log" "github.com/foxcpp/maddy/framework/module" @@ -82,7 +83,7 @@ func (u *Downstream) Init(cfg *config.Map) error { }, saslAuthDirective, &u.saslFactory) cfg.Custom("tls_client", true, false, func() (interface{}, error) { return tls.Config{}, nil - }, config.TLSClientBlock, &u.tlsConfig) + }, tls2.TLSClientBlock, &u.tlsConfig) if _, err := cfg.Process(); err != nil { return err diff --git a/internal/tls/file.go b/internal/tls/file.go new file mode 100644 index 0000000..7a48dbd --- /dev/null +++ b/internal/tls/file.go @@ -0,0 +1,147 @@ +package tls + +import ( + "crypto/tls" + "errors" + "fmt" + "path/filepath" + "sync" + "time" + + "github.com/foxcpp/maddy/framework/config" + "github.com/foxcpp/maddy/framework/hooks" + "github.com/foxcpp/maddy/framework/log" + "github.com/foxcpp/maddy/framework/module" +) + +type FileLoader struct { + instName string + inlineArgs []string + certPaths []string + keyPaths []string + log log.Logger + + certs []tls.Certificate + certsLock sync.RWMutex + + reloadTick *time.Ticker + stopTick chan struct{} +} + +func NewFileLoader(_, instName string, _, inlineArgs []string) (module.Module, error) { + return &FileLoader{ + instName: instName, + inlineArgs: inlineArgs, + log: log.Logger{Name: "tls.loader.file", Debug: log.DefaultLogger.Debug}, + stopTick: make(chan struct{}), + }, nil +} + +func (f *FileLoader) Init(cfg *config.Map) error { + cfg.StringList("certs", false, false, nil, &f.certPaths) + cfg.StringList("keys", false, false, nil, &f.keyPaths) + if _, err := cfg.Process(); err != nil { + return err + } + + if len(f.certPaths) != len(f.keyPaths) { + return errors.New("tls.loader.file: mismatch in certs and keys count") + } + + if len(f.inlineArgs)%2 != 0 { + return errors.New("tls.loader.file: odd amount of arguments") + } + for i := 0; i < len(f.inlineArgs); i += 2 { + f.certPaths = append(f.certPaths, f.inlineArgs[i]) + f.keyPaths = append(f.keyPaths, f.inlineArgs[i+1]) + } + + for _, certPath := range f.certPaths { + if !filepath.IsAbs(certPath) { + return fmt.Errorf("tls.loader.file: only absolute paths allowed in certificate paths: sorry :(") + } + } + + if err := f.loadCerts(); err != nil { + return err + } + + hooks.AddHook(hooks.EventReload, func() { + f.log.Println("reloading certificates") + if err := f.loadCerts(); err != nil { + f.log.Error("reload failed", err) + } + }) + + f.reloadTick = time.NewTicker(time.Minute) + go f.reloadTicker() + return nil +} + +func (f *FileLoader) Close() error { + f.reloadTick.Stop() + f.stopTick <- struct{}{} + return nil +} + +func (f *FileLoader) Name() string { + return "tls.loader.file" +} + +func (f *FileLoader) InstanceName() string { + return f.instName +} + +func (f *FileLoader) reloadTicker() { + for { + select { + case <-f.reloadTick.C: + f.log.Debugln("reloading certs") + if err := f.loadCerts(); err != nil { + f.log.Error("reload failed", err) + } + case <-f.stopTick: + return + } + } +} + +func (f *FileLoader) loadCerts() error { + if len(f.certPaths) != len(f.keyPaths) { + return errors.New("mismatch in certs and keys count") + } + + if len(f.certPaths) == 0 { + return errors.New("tls.loader.file: at least one certificate required") + } + + certs := make([]tls.Certificate, 0, len(f.certPaths)) + + for i := range f.certPaths { + certPath := f.certPaths[i] + keyPath := f.keyPaths[i] + + cert, err := tls.LoadX509KeyPair(certPath, keyPath) + if err != nil { + return fmt.Errorf("failed to load %s and %s: %v", certPath, keyPath, err) + } + certs = append(certs, cert) + } + + f.certsLock.Lock() + defer f.certsLock.Unlock() + f.certs = certs + + return nil +} + +func (f *FileLoader) LoadCerts() ([]tls.Certificate, error) { + // Loader function replaces only the whole slice. + f.certsLock.RLock() + defer f.certsLock.RUnlock() + return f.certs, nil +} + +func init() { + module.Register("tls.loader.file", NewFileLoader) +} diff --git a/internal/tls/self_signed.go b/internal/tls/self_signed.go new file mode 100644 index 0000000..5d981af --- /dev/null +++ b/internal/tls/self_signed.go @@ -0,0 +1,92 @@ +package tls + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "math/big" + "net" + "time" + + "github.com/foxcpp/maddy/framework/config" + "github.com/foxcpp/maddy/framework/module" +) + +type SelfSignedLoader struct { + instName string + serverNames []string + + cert tls.Certificate +} + +func NewSelfSignedLoader(_, instName string, _, inlineArgs []string) (module.Module, error) { + return &SelfSignedLoader{ + instName: instName, + serverNames: inlineArgs, + }, nil +} + +func (f *SelfSignedLoader) Init(cfg *config.Map) error { + if _, err := cfg.Process(); err != nil { + return err + } + + privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return err + } + + notBefore := time.Now() + notAfter := notBefore.Add(24 * time.Hour * 7) + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return err + } + cert := &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{Organization: []string{"Maddy Self-Signed"}}, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + } + + for _, name := range f.serverNames { + if ip := net.ParseIP(name); ip != nil { + cert.IPAddresses = append(cert.IPAddresses, ip) + } else { + cert.DNSNames = append(cert.DNSNames, name) + } + } + derBytes, err := x509.CreateCertificate(rand.Reader, cert, cert, &privKey.PublicKey, privKey) + if err != nil { + return err + } + + f.cert = tls.Certificate{ + Certificate: [][]byte{derBytes}, + PrivateKey: privKey, + Leaf: cert, + } + return nil +} + +func (f *SelfSignedLoader) Name() string { + return "tls.loader.self_signed" +} + +func (f *SelfSignedLoader) InstanceName() string { + return f.instName +} + +func (f *SelfSignedLoader) LoadCerts() ([]tls.Certificate, error) { + return []tls.Certificate{f.cert}, nil +} + +func init() { + module.Register("tls.loader.self_signed", NewSelfSignedLoader) +} diff --git a/maddy.go b/maddy.go index 9c06968..f0a4032 100644 --- a/maddy.go +++ b/maddy.go @@ -15,6 +15,7 @@ import ( parser "github.com/foxcpp/maddy/framework/cfgparser" "github.com/foxcpp/maddy/framework/config" + "github.com/foxcpp/maddy/framework/config/tls" "github.com/foxcpp/maddy/framework/hooks" "github.com/foxcpp/maddy/framework/log" "github.com/foxcpp/maddy/framework/module" @@ -45,6 +46,7 @@ import ( _ "github.com/foxcpp/maddy/internal/target/queue" _ "github.com/foxcpp/maddy/internal/target/remote" _ "github.com/foxcpp/maddy/internal/target/smtp" + _ "github.com/foxcpp/maddy/internal/tls" ) var ( @@ -285,7 +287,7 @@ func ReadGlobals(cfg []config.Node) (map[string]interface{}, []config.Node, erro globals.String("prometheus_endpoint", false, false, "", &prometheusEndpoint) globals.String("hostname", false, false, "", nil) globals.String("autogenerated_msg_domain", false, false, "", nil) - globals.Custom("tls", false, false, nil, config.TLSDirective, nil) + globals.Custom("tls", false, false, nil, tls.TLSDirective, nil) globals.Bool("storage_perdomain", false, false, nil) globals.Bool("auth_perdomain", false, false, nil) globals.StringList("auth_domains", false, false, nil, nil)