Преглед изворни кода

allow different TLS certificates for each binding

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Nicola Murino пре 3 година
родитељ
комит
1a33b5bb53

+ 18 - 4
cmd/portable.go

@@ -123,8 +123,15 @@ Please take a look at the usage below to customize the serving parameters`,
 				}
 				}
 				portableSFTPPrivateKey = contents
 				portableSFTPPrivateKey = contents
 			}
 			}
-			if portableFTPDPort >= 0 && len(portableFTPSCert) > 0 && len(portableFTPSKey) > 0 {
-				_, err := common.NewCertManager(portableFTPSCert, portableFTPSKey, filepath.Clean(defaultConfigDir),
+			if portableFTPDPort >= 0 && portableFTPSCert != "" && portableFTPSKey != "" {
+				keyPairs := []common.TLSKeyPair{
+					{
+						Cert: portableFTPSCert,
+						Key:  portableFTPSKey,
+						ID:   common.DefaultTLSKeyPaidID,
+					},
+				}
+				_, err := common.NewCertManager(keyPairs, filepath.Clean(defaultConfigDir),
 					"FTP portable")
 					"FTP portable")
 				if err != nil {
 				if err != nil {
 					fmt.Printf("Unable to load FTPS key pair, cert file %#v key file %#v error: %v\n",
 					fmt.Printf("Unable to load FTPS key pair, cert file %#v key file %#v error: %v\n",
@@ -132,8 +139,15 @@ Please take a look at the usage below to customize the serving parameters`,
 					os.Exit(1)
 					os.Exit(1)
 				}
 				}
 			}
 			}
-			if portableWebDAVPort > 0 && len(portableWebDAVCert) > 0 && len(portableWebDAVKey) > 0 {
-				_, err := common.NewCertManager(portableWebDAVCert, portableWebDAVKey, filepath.Clean(defaultConfigDir),
+			if portableWebDAVPort > 0 && portableWebDAVCert != "" && portableWebDAVKey != "" {
+				keyPairs := []common.TLSKeyPair{
+					{
+						Cert: portableWebDAVCert,
+						Key:  portableWebDAVKey,
+						ID:   common.DefaultTLSKeyPaidID,
+					},
+				}
+				_, err := common.NewCertManager(keyPairs, filepath.Clean(defaultConfigDir),
 					"WebDAV portable")
 					"WebDAV portable")
 				if err != nil {
 				if err != nil {
 					fmt.Printf("Unable to load WebDAV key pair, cert file %#v key file %#v error: %v\n",
 					fmt.Printf("Unable to load WebDAV key pair, cert file %#v key file %#v error: %v\n",

+ 49 - 20
common/tlsutils.go

@@ -4,6 +4,7 @@ import (
 	"crypto/tls"
 	"crypto/tls"
 	"crypto/x509"
 	"crypto/x509"
 	"crypto/x509/pkix"
 	"crypto/x509/pkix"
+	"errors"
 	"fmt"
 	"fmt"
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
@@ -14,23 +15,34 @@ import (
 	"github.com/drakkan/sftpgo/v2/util"
 	"github.com/drakkan/sftpgo/v2/util"
 )
 )
 
 
+const (
+	// DefaultTLSKeyPaidID defines the id to use for non-binding specific key pairs
+	DefaultTLSKeyPaidID = "default"
+)
+
+// TLSKeyPair defines the paths and the unique identifier for a TLS key pair
+type TLSKeyPair struct {
+	Cert string
+	Key  string
+	ID   string
+}
+
 // CertManager defines a TLS certificate manager
 // CertManager defines a TLS certificate manager
 type CertManager struct {
 type CertManager struct {
-	certPath  string
-	keyPath   string
+	keyPairs  []TLSKeyPair
 	configDir string
 	configDir string
 	logSender string
 	logSender string
 	sync.RWMutex
 	sync.RWMutex
 	caCertificates    []string
 	caCertificates    []string
 	caRevocationLists []string
 	caRevocationLists []string
-	cert              *tls.Certificate
+	certs             map[string]*tls.Certificate
 	rootCAs           *x509.CertPool
 	rootCAs           *x509.CertPool
 	crls              []*pkix.CertificateList
 	crls              []*pkix.CertificateList
 }
 }
 
 
 // Reload tries to reload certificate and CRLs
 // Reload tries to reload certificate and CRLs
 func (m *CertManager) Reload() error {
 func (m *CertManager) Reload() error {
-	errCrt := m.loadCertificate()
+	errCrt := m.loadCertificates()
 	errCRLs := m.LoadCRLs()
 	errCRLs := m.LoadCRLs()
 
 
 	if errCrt != nil {
 	if errCrt != nil {
@@ -39,30 +51,48 @@ func (m *CertManager) Reload() error {
 	return errCRLs
 	return errCRLs
 }
 }
 
 
-// LoadCertificate loads the configured x509 key pair
-func (m *CertManager) loadCertificate() error {
-	newCert, err := tls.LoadX509KeyPair(m.certPath, m.keyPath)
-	if err != nil {
-		logger.Warn(m.logSender, "", "unable to load X509 key pair, cert file %#v key file %#v error: %v",
-			m.certPath, m.keyPath, err)
-		return err
+// LoadCertificates tries to load the configured x509 key pairs
+func (m *CertManager) loadCertificates() error {
+	if len(m.keyPairs) == 0 {
+		return errors.New("no key pairs defined")
+	}
+	certs := make(map[string]*tls.Certificate)
+	for _, keyPair := range m.keyPairs {
+		if keyPair.ID == "" {
+			return errors.New("TLS certificate without ID")
+		}
+		newCert, err := tls.LoadX509KeyPair(keyPair.Cert, keyPair.Key)
+		if err != nil {
+			logger.Warn(m.logSender, "", "unable to load X509 key pair, cert file %#v key file %#v error: %v",
+				keyPair.Cert, keyPair.Key, err)
+			return err
+		}
+		if _, ok := certs[keyPair.ID]; ok {
+			return fmt.Errorf("TLS certificate with id %#v is duplicated", keyPair.ID)
+		}
+		logger.Debug(m.logSender, "", "TLS certificate %#v successfully loaded, id %v", keyPair.Cert, keyPair.ID)
+		certs[keyPair.ID] = &newCert
 	}
 	}
-	logger.Debug(m.logSender, "", "TLS certificate %#v successfully loaded", m.certPath)
 
 
 	m.Lock()
 	m.Lock()
 	defer m.Unlock()
 	defer m.Unlock()
 
 
-	m.cert = &newCert
+	m.certs = certs
 	return nil
 	return nil
 }
 }
 
 
 // GetCertificateFunc returns the loaded certificate
 // GetCertificateFunc returns the loaded certificate
-func (m *CertManager) GetCertificateFunc() func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
+func (m *CertManager) GetCertificateFunc(certID string) func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
 	return func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
 	return func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
 		m.RLock()
 		m.RLock()
 		defer m.RUnlock()
 		defer m.RUnlock()
 
 
-		return m.cert, nil
+		val, ok := m.certs[certID]
+		if !ok {
+			return nil, fmt.Errorf("no certificate for id %v", certID)
+		}
+
+		return val, nil
 	}
 	}
 }
 }
 
 
@@ -184,15 +214,14 @@ func (m *CertManager) SetCARevocationLists(caRevocationLists []string) {
 }
 }
 
 
 // NewCertManager creates a new certificate manager
 // NewCertManager creates a new certificate manager
-func NewCertManager(certificateFile, certificateKeyFile, configDir, logSender string) (*CertManager, error) {
+func NewCertManager(keyPairs []TLSKeyPair, configDir, logSender string) (*CertManager, error) {
 	manager := &CertManager{
 	manager := &CertManager{
-		cert:      nil,
-		certPath:  certificateFile,
-		keyPath:   certificateKeyFile,
+		keyPairs:  keyPairs,
+		certs:     make(map[string]*tls.Certificate),
 		configDir: configDir,
 		configDir: configDir,
 		logSender: logSender,
 		logSender: logSender,
 	}
 	}
-	err := manager.loadCertificate()
+	err := manager.loadCertificates()
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}

+ 66 - 5
common/tlsutils_test.go

@@ -280,9 +280,35 @@ func TestLoadCertificate(t *testing.T) {
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 	err = os.WriteFile(keyPath, []byte(serverKey), os.ModePerm)
 	err = os.WriteFile(keyPath, []byte(serverKey), os.ModePerm)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
-	certManager, err := NewCertManager(certPath, keyPath, configDir, logSenderTest)
+	keyPairs := []TLSKeyPair{
+		{
+			Cert: certPath,
+			Key:  keyPath,
+			ID:   DefaultTLSKeyPaidID,
+		},
+		{
+			Cert: certPath,
+			Key:  keyPath,
+			ID:   DefaultTLSKeyPaidID,
+		},
+	}
+	certManager, err := NewCertManager(keyPairs, configDir, logSenderTest)
+	if assert.Error(t, err) {
+		assert.Contains(t, err.Error(), "is duplicated")
+	}
+	assert.Nil(t, certManager)
+
+	keyPairs = []TLSKeyPair{
+		{
+			Cert: certPath,
+			Key:  keyPath,
+			ID:   DefaultTLSKeyPaidID,
+		},
+	}
+
+	certManager, err = NewCertManager(keyPairs, configDir, logSenderTest)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
-	certFunc := certManager.GetCertificateFunc()
+	certFunc := certManager.GetCertificateFunc(DefaultTLSKeyPaidID)
 	if assert.NotNil(t, certFunc) {
 	if assert.NotNil(t, certFunc) {
 		hello := &tls.ClientHelloInfo{
 		hello := &tls.ClientHelloInfo{
 			ServerName:   "localhost",
 			ServerName:   "localhost",
@@ -290,9 +316,19 @@ func TestLoadCertificate(t *testing.T) {
 		}
 		}
 		cert, err := certFunc(hello)
 		cert, err := certFunc(hello)
 		assert.NoError(t, err)
 		assert.NoError(t, err)
-		assert.Equal(t, certManager.cert, cert)
+		assert.Equal(t, certManager.certs[DefaultTLSKeyPaidID], cert)
+	}
+	certFunc = certManager.GetCertificateFunc("unknownID")
+	if assert.NotNil(t, certFunc) {
+		hello := &tls.ClientHelloInfo{
+			ServerName:   "localhost",
+			CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305},
+		}
+		_, err = certFunc(hello)
+		if assert.Error(t, err) {
+			assert.Contains(t, err.Error(), "no certificate for id unknownID")
+		}
 	}
 	}
-
 	certManager.SetCACertificates(nil)
 	certManager.SetCACertificates(nil)
 	err = certManager.LoadRootCAs()
 	err = certManager.LoadRootCAs()
 	assert.NoError(t, err)
 	assert.NoError(t, err)
@@ -380,7 +416,32 @@ func TestLoadCertificate(t *testing.T) {
 }
 }
 
 
 func TestLoadInvalidCert(t *testing.T) {
 func TestLoadInvalidCert(t *testing.T) {
-	certManager, err := NewCertManager("test.crt", "test.key", configDir, logSenderTest)
+	certManager, err := NewCertManager(nil, configDir, logSenderTest)
+	if assert.Error(t, err) {
+		assert.Contains(t, err.Error(), "no key pairs defined")
+	}
+	assert.Nil(t, certManager)
+
+	keyPairs := []TLSKeyPair{
+		{
+			Cert: "test.crt",
+			Key:  "test.key",
+			ID:   DefaultTLSKeyPaidID,
+		},
+	}
+	certManager, err = NewCertManager(keyPairs, configDir, logSenderTest)
 	assert.Error(t, err)
 	assert.Error(t, err)
 	assert.Nil(t, certManager)
 	assert.Nil(t, certManager)
+
+	keyPairs = []TLSKeyPair{
+		{
+			Cert: "test.crt",
+			Key:  "test.key",
+		},
+	}
+	certManager, err = NewCertManager(keyPairs, configDir, logSenderTest)
+	if assert.Error(t, err) {
+		assert.Contains(t, err.Error(), "TLS certificate without ID")
+	}
+	assert.Nil(t, certManager)
 }
 }

+ 54 - 8
config/config.go

@@ -54,6 +54,8 @@ var (
 		Port:                       0,
 		Port:                       0,
 		ApplyProxyConfig:           true,
 		ApplyProxyConfig:           true,
 		TLSMode:                    0,
 		TLSMode:                    0,
+		CertificateFile:            "",
+		CertificateKeyFile:         "",
 		MinTLSVersion:              12,
 		MinTLSVersion:              12,
 		ForcePassiveIP:             "",
 		ForcePassiveIP:             "",
 		PassiveIPOverrides:         nil,
 		PassiveIPOverrides:         nil,
@@ -64,14 +66,16 @@ var (
 		Debug:                      false,
 		Debug:                      false,
 	}
 	}
 	defaultWebDAVDBinding = webdavd.Binding{
 	defaultWebDAVDBinding = webdavd.Binding{
-		Address:         "",
-		Port:            0,
-		EnableHTTPS:     false,
-		MinTLSVersion:   12,
-		ClientAuthType:  0,
-		TLSCipherSuites: nil,
-		Prefix:          "",
-		ProxyAllowed:    nil,
+		Address:            "",
+		Port:               0,
+		EnableHTTPS:        false,
+		CertificateFile:    "",
+		CertificateKeyFile: "",
+		MinTLSVersion:      12,
+		ClientAuthType:     0,
+		TLSCipherSuites:    nil,
+		Prefix:             "",
+		ProxyAllowed:       nil,
 	}
 	}
 	defaultHTTPDBinding = httpd.Binding{
 	defaultHTTPDBinding = httpd.Binding{
 		Address:               "",
 		Address:               "",
@@ -79,6 +83,8 @@ var (
 		EnableWebAdmin:        true,
 		EnableWebAdmin:        true,
 		EnableWebClient:       true,
 		EnableWebClient:       true,
 		EnableHTTPS:           false,
 		EnableHTTPS:           false,
+		CertificateFile:       "",
+		CertificateKeyFile:    "",
 		MinTLSVersion:         12,
 		MinTLSVersion:         12,
 		ClientAuthType:        0,
 		ClientAuthType:        0,
 		TLSCipherSuites:       nil,
 		TLSCipherSuites:       nil,
@@ -1016,6 +1022,18 @@ func getFTPDBindingFromEnv(idx int) {
 		isSet = true
 		isSet = true
 	}
 	}
 
 
+	certificateFile, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__CERTIFICATE_FILE", idx))
+	if ok {
+		binding.CertificateFile = certificateFile
+		isSet = true
+	}
+
+	certificateKeyFile, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__CERTIFICATE_KEY_FILE", idx))
+	if ok {
+		binding.CertificateKeyFile = certificateKeyFile
+		isSet = true
+	}
+
 	tlsMode, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__TLS_MODE", idx))
 	tlsMode, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__TLS_MODE", idx))
 	if ok {
 	if ok {
 		binding.TLSMode = int(tlsMode)
 		binding.TLSMode = int(tlsMode)
@@ -1070,6 +1088,10 @@ func getFTPDBindingFromEnv(idx int) {
 		isSet = true
 		isSet = true
 	}
 	}
 
 
+	applyFTPDBindingFromEnv(idx, isSet, binding)
+}
+
+func applyFTPDBindingFromEnv(idx int, isSet bool, binding ftpd.Binding) {
 	if isSet {
 	if isSet {
 		if len(globalConf.FTPD.Bindings) > idx {
 		if len(globalConf.FTPD.Bindings) > idx {
 			globalConf.FTPD.Bindings[idx] = binding
 			globalConf.FTPD.Bindings[idx] = binding
@@ -1101,6 +1123,18 @@ func getWebDAVDBindingFromEnv(idx int) {
 		isSet = true
 		isSet = true
 	}
 	}
 
 
+	certificateFile, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__CERTIFICATE_FILE", idx))
+	if ok {
+		binding.CertificateFile = certificateFile
+		isSet = true
+	}
+
+	certificateKeyFile, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__CERTIFICATE_KEY_FILE", idx))
+	if ok {
+		binding.CertificateKeyFile = certificateKeyFile
+		isSet = true
+	}
+
 	enableHTTPS, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__ENABLE_HTTPS", idx))
 	enableHTTPS, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__ENABLE_HTTPS", idx))
 	if ok {
 	if ok {
 		binding.EnableHTTPS = enableHTTPS
 		binding.EnableHTTPS = enableHTTPS
@@ -1470,6 +1504,18 @@ func getHTTPDBindingFromEnv(idx int) {
 		isSet = true
 		isSet = true
 	}
 	}
 
 
+	certificateFile, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__CERTIFICATE_FILE", idx))
+	if ok {
+		binding.CertificateFile = certificateFile
+		isSet = true
+	}
+
+	certificateKeyFile, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__CERTIFICATE_KEY_FILE", idx))
+	if ok {
+		binding.CertificateKeyFile = certificateKeyFile
+		isSet = true
+	}
+
 	enableWebAdmin, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__ENABLE_WEB_ADMIN", idx))
 	enableWebAdmin, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__ENABLE_WEB_ADMIN", idx))
 	if ok {
 	if ok {
 		binding.EnableWebAdmin = enableWebAdmin
 		binding.EnableWebAdmin = enableWebAdmin

+ 20 - 0
config/config_test.go

@@ -764,6 +764,8 @@ func TestFTPDBindingsFromEnv(t *testing.T) {
 	os.Setenv("SFTPGO_FTPD__BINDINGS__9__CLIENT_AUTH_TYPE", "2")
 	os.Setenv("SFTPGO_FTPD__BINDINGS__9__CLIENT_AUTH_TYPE", "2")
 	os.Setenv("SFTPGO_FTPD__BINDINGS__9__DEBUG", "1")
 	os.Setenv("SFTPGO_FTPD__BINDINGS__9__DEBUG", "1")
 	os.Setenv("SFTPGO_FTPD__BINDINGS__9__ACTIVE_CONNECTIONS_SECURITY", "1")
 	os.Setenv("SFTPGO_FTPD__BINDINGS__9__ACTIVE_CONNECTIONS_SECURITY", "1")
+	os.Setenv("SFTPGO_FTPD__BINDINGS__9__CERTIFICATE_FILE", "cert.crt")
+	os.Setenv("SFTPGO_FTPD__BINDINGS__9__CERTIFICATE_KEY_FILE", "cert.key")
 
 
 	t.Cleanup(func() {
 	t.Cleanup(func() {
 		os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__ADDRESS")
 		os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__ADDRESS")
@@ -784,6 +786,8 @@ func TestFTPDBindingsFromEnv(t *testing.T) {
 		os.Unsetenv("SFTPGO_FTPD__BINDINGS__9__CLIENT_AUTH_TYPE")
 		os.Unsetenv("SFTPGO_FTPD__BINDINGS__9__CLIENT_AUTH_TYPE")
 		os.Unsetenv("SFTPGO_FTPD__BINDINGS__9__DEBUG")
 		os.Unsetenv("SFTPGO_FTPD__BINDINGS__9__DEBUG")
 		os.Unsetenv("SFTPGO_FTPD__BINDINGS__9__ACTIVE_CONNECTIONS_SECURITY")
 		os.Unsetenv("SFTPGO_FTPD__BINDINGS__9__ACTIVE_CONNECTIONS_SECURITY")
+		os.Unsetenv("SFTPGO_FTPD__BINDINGS__9__CERTIFICATE_FILE")
+		os.Unsetenv("SFTPGO_FTPD__BINDINGS__9__CERTIFICATE_KEY_FILE")
 	})
 	})
 
 
 	configDir := ".."
 	configDir := ".."
@@ -821,6 +825,8 @@ func TestFTPDBindingsFromEnv(t *testing.T) {
 	require.Equal(t, 0, bindings[1].PassiveConnectionsSecurity)
 	require.Equal(t, 0, bindings[1].PassiveConnectionsSecurity)
 	require.Equal(t, 1, bindings[1].ActiveConnectionsSecurity)
 	require.Equal(t, 1, bindings[1].ActiveConnectionsSecurity)
 	require.True(t, bindings[1].Debug)
 	require.True(t, bindings[1].Debug)
+	require.Equal(t, "cert.crt", bindings[1].CertificateFile)
+	require.Equal(t, "cert.key", bindings[1].CertificateKeyFile)
 }
 }
 
 
 func TestWebDAVBindingsFromEnv(t *testing.T) {
 func TestWebDAVBindingsFromEnv(t *testing.T) {
@@ -837,6 +843,9 @@ func TestWebDAVBindingsFromEnv(t *testing.T) {
 	os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__MIN_TLS_VERSION", "13")
 	os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__MIN_TLS_VERSION", "13")
 	os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__CLIENT_AUTH_TYPE", "1")
 	os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__CLIENT_AUTH_TYPE", "1")
 	os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__PREFIX", "/dav2")
 	os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__PREFIX", "/dav2")
+	os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__CERTIFICATE_FILE", "webdav.crt")
+	os.Setenv("SFTPGO_WEBDAVD__BINDINGS__2__CERTIFICATE_KEY_FILE", "webdav.key")
+
 	t.Cleanup(func() {
 	t.Cleanup(func() {
 		os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__ADDRESS")
 		os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__ADDRESS")
 		os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__PORT")
 		os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__PORT")
@@ -849,6 +858,8 @@ func TestWebDAVBindingsFromEnv(t *testing.T) {
 		os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__MIN_TLS_VERSION")
 		os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__MIN_TLS_VERSION")
 		os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__CLIENT_AUTH_TYPE")
 		os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__CLIENT_AUTH_TYPE")
 		os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__PREFIX")
 		os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__PREFIX")
+		os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__CERTIFICATE_FILE")
+		os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__2__CERTIFICATE_KEY_FILE")
 	})
 	})
 
 
 	configDir := ".."
 	configDir := ".."
@@ -878,6 +889,8 @@ func TestWebDAVBindingsFromEnv(t *testing.T) {
 	require.Equal(t, 1, bindings[2].ClientAuthType)
 	require.Equal(t, 1, bindings[2].ClientAuthType)
 	require.Nil(t, bindings[2].TLSCipherSuites)
 	require.Nil(t, bindings[2].TLSCipherSuites)
 	require.Equal(t, "/dav2", bindings[2].Prefix)
 	require.Equal(t, "/dav2", bindings[2].Prefix)
+	require.Equal(t, "webdav.crt", bindings[2].CertificateFile)
+	require.Equal(t, "webdav.key", bindings[2].CertificateKeyFile)
 }
 }
 
 
 func TestHTTPDBindingsFromEnv(t *testing.T) {
 func TestHTTPDBindingsFromEnv(t *testing.T) {
@@ -941,6 +954,9 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__DISCLAIMER_PATH", "disclaimer.html")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__DISCLAIMER_PATH", "disclaimer.html")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__DEFAULT_CSS", "default.css")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__DEFAULT_CSS", "default.css")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__EXTRA_CSS", "1.css,2.css")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__EXTRA_CSS", "1.css,2.css")
+	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__CERTIFICATE_FILE", "httpd.crt")
+	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__CERTIFICATE_KEY_FILE", "httpd.key")
+
 	t.Cleanup(func() {
 	t.Cleanup(func() {
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__0__ADDRESS")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__0__ADDRESS")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__0__PORT")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__0__PORT")
@@ -999,6 +1015,8 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__DISCLAIMER_PATH")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__DISCLAIMER_PATH")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__DEFAULT_CSS")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__DEFAULT_CSS")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__EXTRA_CSS")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__EXTRA_CSS")
+		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__CERTIFICATE_FILE")
+		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__CERTIFICATE_KEY_FILE")
 	})
 	})
 
 
 	configDir := ".."
 	configDir := ".."
@@ -1087,6 +1105,8 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 	require.Len(t, bindings[2].Branding.WebClient.ExtraCSS, 2)
 	require.Len(t, bindings[2].Branding.WebClient.ExtraCSS, 2)
 	require.Equal(t, "1.css", bindings[2].Branding.WebClient.ExtraCSS[0])
 	require.Equal(t, "1.css", bindings[2].Branding.WebClient.ExtraCSS[0])
 	require.Equal(t, "2.css", bindings[2].Branding.WebClient.ExtraCSS[1])
 	require.Equal(t, "2.css", bindings[2].Branding.WebClient.ExtraCSS[1])
+	require.Equal(t, "httpd.crt", bindings[2].CertificateFile)
+	require.Equal(t, "httpd.key", bindings[2].CertificateKeyFile)
 }
 }
 
 
 func TestHTTPClientCertificatesFromEnv(t *testing.T) {
 func TestHTTPClientCertificatesFromEnv(t *testing.T) {

+ 7 - 1
docs/full-configuration.md

@@ -129,6 +129,8 @@ The configuration file contains the following sections:
     - `address`, string. Leave blank to listen on all available network interfaces. Default: "".
     - `address`, string. Leave blank to listen on all available network interfaces. Default: "".
     - `apply_proxy_config`, boolean. If enabled the common proxy configuration, if any, will be applied. Please note that we expect the proxy header on control and data connections. Default `true`.
     - `apply_proxy_config`, boolean. If enabled the common proxy configuration, if any, will be applied. Please note that we expect the proxy header on control and data connections. Default `true`.
     - `tls_mode`, integer. 0 means accept both cleartext and encrypted sessions. 1 means TLS is required for both control and data connection. 2 means implicit TLS. Do not enable this blindly, please check that a proper TLS config is in place if you set `tls_mode` is different from 0.
     - `tls_mode`, integer. 0 means accept both cleartext and encrypted sessions. 1 means TLS is required for both control and data connection. 2 means implicit TLS. Do not enable this blindly, please check that a proper TLS config is in place if you set `tls_mode` is different from 0.
+    - `certificate_file`, string. Binding specific TLS certificate. This can be an absolute path or a path relative to the config dir.
+    - `certificate_key_file`, string. Binding specific private key matching the above certificate. This can be an absolute path or a path relative to the config dir. If not set the global ones will be used, if any.
     - `min_tls_version`, integer. Defines the minimum version of TLS to be enabled. `12` means TLS 1.2 (and therefore TLS 1.2 and TLS 1.3 will be enabled),`13` means TLS 1.3. Default: `12`.
     - `min_tls_version`, integer. Defines the minimum version of TLS to be enabled. `12` means TLS 1.2 (and therefore TLS 1.2 and TLS 1.3 will be enabled),`13` means TLS 1.3. Default: `12`.
     - `force_passive_ip`, ip address. External IP address to expose for passive connections. Leave empty to autodetect. If not empty, it must be a valid IPv4 address. Default: "".
     - `force_passive_ip`, ip address. External IP address to expose for passive connections. Leave empty to autodetect. If not empty, it must be a valid IPv4 address. Default: "".
     - `passive_ip_overrides`, list of struct that allows to return a different passive ip based on the client IP address. Each struct has the following fields:
     - `passive_ip_overrides`, list of struct that allows to return a different passive ip based on the client IP address. Each struct has the following fields:
@@ -156,6 +158,8 @@ The configuration file contains the following sections:
     - `port`, integer. The port used for serving WebDAV requests. 0 means disabled. Default: 0.
     - `port`, integer. The port used for serving WebDAV requests. 0 means disabled. Default: 0.
     - `address`, string. Leave blank to listen on all available network interfaces. Default: "".
     - `address`, string. Leave blank to listen on all available network interfaces. Default: "".
     - `enable_https`, boolean. Set to `true` and provide both a certificate and a key file to enable HTTPS connection for this binding. Default `false`.
     - `enable_https`, boolean. Set to `true` and provide both a certificate and a key file to enable HTTPS connection for this binding. Default `false`.
+    - `certificate_file`, string. Binding specific TLS certificate. This can be an absolute path or a path relative to the config dir.
+    - `certificate_key_file`, string. Binding specific private key matching the above certificate. This can be an absolute path or a path relative to the config dir. If not set the global ones will be used, if any.
     - `min_tls_version`, integer. Defines the minimum version of TLS to be enabled. `12` means TLS 1.2 (and therefore TLS 1.2 and TLS 1.3 will be enabled),`13` means TLS 1.3. Default: `12`.
     - `min_tls_version`, integer. Defines the minimum version of TLS to be enabled. `12` means TLS 1.2 (and therefore TLS 1.2 and TLS 1.3 will be enabled),`13` means TLS 1.3. Default: `12`.
     - `client_auth_type`, integer. Set to `1` to require a client certificate and verify it. Set to `2` to request a client certificate during the TLS handshake and verify it if given, in this mode the client is allowed not to send a certificate. At least one certification authority must be defined in order to verify client certificates. If no certification authority is defined, this setting is ignored. Default: 0.
     - `client_auth_type`, integer. Set to `1` to require a client certificate and verify it. Set to `2` to request a client certificate during the TLS handshake and verify it if given, in this mode the client is allowed not to send a certificate. At least one certification authority must be defined in order to verify client certificates. If no certification authority is defined, this setting is ignored. Default: 0.
     - `tls_cipher_suites`, list of strings. List of supported cipher suites for TLS version 1.2. If empty, a default list of secure cipher suites is used, with a preference order based on hardware performance. Note that TLS 1.3 ciphersuites are not configurable. The supported ciphersuites names are defined [here](https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L52). Any invalid name will be silently ignored. The order matters, the ciphers listed first will be the preferred ones. Default: empty.
     - `tls_cipher_suites`, list of strings. List of supported cipher suites for TLS version 1.2. If empty, a default list of secure cipher suites is used, with a preference order based on hardware performance. Note that TLS 1.3 ciphersuites are not configurable. The supported ciphersuites names are defined [here](https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L52). Any invalid name will be silently ignored. The order matters, the ciphers listed first will be the preferred ones. Default: empty.
@@ -239,6 +243,8 @@ The configuration file contains the following sections:
     - `enable_web_admin`, boolean. Set to `false` to disable the built-in web admin for this binding. You also need to define `templates_path` and `static_files_path` to use the built-in web admin interface. Default `true`.
     - `enable_web_admin`, boolean. Set to `false` to disable the built-in web admin for this binding. You also need to define `templates_path` and `static_files_path` to use the built-in web admin interface. Default `true`.
     - `enable_web_client`, boolean. Set to `false` to disable the built-in web client for this binding. You also need to define `templates_path` and `static_files_path` to use the built-in web client interface. Default `true`.
     - `enable_web_client`, boolean. Set to `false` to disable the built-in web client for this binding. You also need to define `templates_path` and `static_files_path` to use the built-in web client interface. Default `true`.
     - `enable_https`, boolean. Set to `true` and provide both a certificate and a key file to enable HTTPS connection for this binding. Default `false`.
     - `enable_https`, boolean. Set to `true` and provide both a certificate and a key file to enable HTTPS connection for this binding. Default `false`.
+    - `certificate_file`, string. Binding specific TLS certificate. This can be an absolute path or a path relative to the config dir.
+    - `certificate_key_file`, string. Binding specific private key matching the above certificate. This can be an absolute path or a path relative to the config dir. If not set the global ones will be used, if any.
     - `min_tls_version`, integer. Defines the minimum version of TLS to be enabled. `12` means TLS 1.2 (and therefore TLS 1.2 and TLS 1.3 will be enabled),`13` means TLS 1.3. Default: `12`.
     - `min_tls_version`, integer. Defines the minimum version of TLS to be enabled. `12` means TLS 1.2 (and therefore TLS 1.2 and TLS 1.3 will be enabled),`13` means TLS 1.3. Default: `12`.
     - `client_auth_type`, integer. Set to `1` to require client certificate authentication in addition to JWT/Web authentication. You need to define at least a certificate authority for this to work. Default: 0.
     - `client_auth_type`, integer. Set to `1` to require client certificate authentication in addition to JWT/Web authentication. You need to define at least a certificate authority for this to work. Default: 0.
     - `tls_cipher_suites`, list of strings. List of supported cipher suites for TLS version 1.2. If empty, a default list of secure cipher suites is used, with a preference order based on hardware performance. Note that TLS 1.3 ciphersuites are not configurable. The supported ciphersuites names are defined [here](https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L52). Any invalid name will be silently ignored. The order matters, the ciphers listed first will be the preferred ones. Default: blank.
     - `tls_cipher_suites`, list of strings. List of supported cipher suites for TLS version 1.2. If empty, a default list of secure cipher suites is used, with a preference order based on hardware performance. Note that TLS 1.3 ciphersuites are not configurable. The supported ciphersuites names are defined [here](https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L52). Any invalid name will be silently ignored. The order matters, the ciphers listed first will be the preferred ones. Default: blank.
@@ -287,7 +293,7 @@ The configuration file contains the following sections:
   - `openapi_path`, string. Path to the directory that contains the OpenAPI schema and the default renderer. This can be an absolute path or a path relative to the config dir. If empty the OpenAPI schema and the renderer will not be served regardless of the `render_openapi` directive
   - `openapi_path`, string. Path to the directory that contains the OpenAPI schema and the default renderer. This can be an absolute path or a path relative to the config dir. If empty the OpenAPI schema and the renderer will not be served regardless of the `render_openapi` directive
   - `web_root`, string.  Defines a base URL for the web admin and client interfaces. If empty web admin and client resources will be available at the root ("/") URI. If defined it must be an absolute URI or it will be ignored
   - `web_root`, string.  Defines a base URL for the web admin and client interfaces. If empty web admin and client resources will be available at the root ("/") URI. If defined it must be an absolute URI or it will be ignored
   - `certificate_file`, string. Certificate for HTTPS. This can be an absolute path or a path relative to the config dir.
   - `certificate_file`, string. Certificate for HTTPS. This can be an absolute path or a path relative to the config dir.
-  - `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. If both the certificate and the private key are provided, the server will expect HTTPS connections. Certificate and key files can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows.
+  - `certificate_key_file`, string. Private key matching the above certificate. This can be an absolute path or a path relative to the config dir. If both the certificate and the private key are provided, you can enable HTTPS for the configured bindings. Certificate and key files can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows.
   - `ca_certificates`, list of strings. Set of root certificate authorities to be used to verify client certificates.
   - `ca_certificates`, list of strings. Set of root certificate authorities to be used to verify client certificates.
   - `ca_revocation_lists`, list of strings. Set a revocation lists, one for each root CA, to be used to check if a client certificate has been revoked. The revocation lists can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows.
   - `ca_revocation_lists`, list of strings. Set a revocation lists, one for each root CA, to be used to check if a client certificate has been revoked. The revocation lists can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows.
   - `signing_passphrase`, string. Passphrase to use to derive the signing key for JWT and CSRF tokens. If empty a random signing key will be generated each time SFTPGo starts. If you set a signing passphrase you should consider rotating it periodically for added security.
   - `signing_passphrase`, string. Passphrase to use to derive the signing key for JWT and CSRF tokens. If empty a random signing key will be generated each time SFTPGo starts. If you set a signing passphrase you should consider rotating it periodically for added security.

+ 33 - 4
ftpd/ftpd.go

@@ -48,6 +48,10 @@ type Binding struct {
 	// Set to 1 to require TLS for both data and control connection.
 	// Set to 1 to require TLS for both data and control connection.
 	// Set to 2 to enable implicit TLS
 	// Set to 2 to enable implicit TLS
 	TLSMode int `json:"tls_mode" mapstructure:"tls_mode"`
 	TLSMode int `json:"tls_mode" mapstructure:"tls_mode"`
+	// Certificate and matching private key for this specific binding, if empty the global
+	// ones will be used, if any
+	CertificateFile    string `json:"certificate_file" mapstructure:"certificate_file"`
+	CertificateKeyFile string `json:"certificate_key_file" mapstructure:"certificate_key_file"`
 	// Defines the minimum TLS version. 13 means TLS 1.3, default is TLS 1.2
 	// Defines the minimum TLS version. 13 means TLS 1.3, default is TLS 1.2
 	MinTLSVersion int `json:"min_tls_version" mapstructure:"min_tls_version"`
 	MinTLSVersion int `json:"min_tls_version" mapstructure:"min_tls_version"`
 	// External IP address to expose for passive connections.
 	// External IP address to expose for passive connections.
@@ -262,6 +266,32 @@ func (c *Configuration) ShouldBind() bool {
 	return false
 	return false
 }
 }
 
 
+func (c *Configuration) getKeyPairs(configDir string) []common.TLSKeyPair {
+	var keyPairs []common.TLSKeyPair
+
+	for _, binding := range c.Bindings {
+		certificateFile := getConfigPath(binding.CertificateFile, configDir)
+		certificateKeyFile := getConfigPath(binding.CertificateKeyFile, configDir)
+		if certificateFile != "" && certificateKeyFile != "" {
+			keyPairs = append(keyPairs, common.TLSKeyPair{
+				Cert: certificateFile,
+				Key:  certificateKeyFile,
+				ID:   binding.GetAddress(),
+			})
+		}
+	}
+	certificateFile := getConfigPath(c.CertificateFile, configDir)
+	certificateKeyFile := getConfigPath(c.CertificateKeyFile, configDir)
+	if certificateFile != "" && certificateKeyFile != "" {
+		keyPairs = append(keyPairs, common.TLSKeyPair{
+			Cert: certificateFile,
+			Key:  certificateKeyFile,
+			ID:   common.DefaultTLSKeyPaidID,
+		})
+	}
+	return keyPairs
+}
+
 // Initialize configures and starts the FTP server
 // Initialize configures and starts the FTP server
 func (c *Configuration) Initialize(configDir string) error {
 func (c *Configuration) Initialize(configDir string) error {
 	logger.Info(logSender, "", "initializing FTP server with config %+v", *c)
 	logger.Info(logSender, "", "initializing FTP server with config %+v", *c)
@@ -269,10 +299,9 @@ func (c *Configuration) Initialize(configDir string) error {
 		return common.ErrNoBinding
 		return common.ErrNoBinding
 	}
 	}
 
 
-	certificateFile := getConfigPath(c.CertificateFile, configDir)
-	certificateKeyFile := getConfigPath(c.CertificateKeyFile, configDir)
-	if certificateFile != "" && certificateKeyFile != "" {
-		mgr, err := common.NewCertManager(certificateFile, certificateKeyFile, configDir, logSender)
+	keyPairs := c.getKeyPairs(configDir)
+	if len(keyPairs) > 0 {
+		mgr, err := common.NewCertManager(keyPairs, configDir, logSender)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}

+ 4 - 4
ftpd/ftpd_test.go

@@ -325,15 +325,15 @@ func TestMain(m *testing.M) {
 	ftpdConf := config.GetFTPDConfig()
 	ftpdConf := config.GetFTPDConfig()
 	ftpdConf.Bindings = []ftpd.Binding{
 	ftpdConf.Bindings = []ftpd.Binding{
 		{
 		{
-			Port:           2121,
-			ClientAuthType: 2,
+			Port:               2121,
+			ClientAuthType:     2,
+			CertificateFile:    certPath,
+			CertificateKeyFile: keyPath,
 		},
 		},
 	}
 	}
 	ftpdConf.PassivePortRange.Start = 0
 	ftpdConf.PassivePortRange.Start = 0
 	ftpdConf.PassivePortRange.End = 0
 	ftpdConf.PassivePortRange.End = 0
 	ftpdConf.BannerFile = bannerFileName
 	ftpdConf.BannerFile = bannerFileName
-	ftpdConf.CertificateFile = certPath
-	ftpdConf.CertificateKeyFile = keyPath
 	ftpdConf.CACertificates = []string{caCrtPath}
 	ftpdConf.CACertificates = []string{caCrtPath}
 	ftpdConf.CARevocationLists = []string{caCRLPath}
 	ftpdConf.CARevocationLists = []string{caCRLPath}
 	ftpdConf.EnableSite = true
 	ftpdConf.EnableSite = true

+ 8 - 2
ftpd/internal_test.go

@@ -875,8 +875,14 @@ func TestVerifyTLSConnection(t *testing.T) {
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 	err = os.WriteFile(keyPath, []byte(ftpsKey), os.ModePerm)
 	err = os.WriteFile(keyPath, []byte(ftpsKey), os.ModePerm)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
-
-	certMgr, err = common.NewCertManager(certPath, keyPath, "", "ftp_test")
+	keyPairs := []common.TLSKeyPair{
+		{
+			Cert: certPath,
+			Key:  keyPath,
+			ID:   common.DefaultTLSKeyPaidID,
+		},
+	}
+	certMgr, err = common.NewCertManager(keyPairs, "", "ftp_test")
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 
 
 	certMgr.SetCARevocationLists([]string{caCrlPath})
 	certMgr.SetCARevocationLists([]string{caCrlPath})

+ 7 - 1
ftpd/server.go

@@ -262,12 +262,18 @@ func (s *Server) VerifyConnection(cc ftpserver.ClientContext, user string, tlsCo
 
 
 func (s *Server) buildTLSConfig() {
 func (s *Server) buildTLSConfig() {
 	if certMgr != nil {
 	if certMgr != nil {
+		certID := common.DefaultTLSKeyPaidID
+		if getConfigPath(s.binding.CertificateFile, "") != "" && getConfigPath(s.binding.CertificateKeyFile, "") != "" {
+			certID = s.binding.GetAddress()
+		}
 		s.tlsConfig = &tls.Config{
 		s.tlsConfig = &tls.Config{
-			GetCertificate:           certMgr.GetCertificateFunc(),
+			GetCertificate:           certMgr.GetCertificateFunc(certID),
 			MinVersion:               util.GetTLSVersion(s.binding.MinTLSVersion),
 			MinVersion:               util.GetTLSVersion(s.binding.MinTLSVersion),
 			CipherSuites:             s.binding.ciphers,
 			CipherSuites:             s.binding.ciphers,
 			PreferServerCipherSuites: true,
 			PreferServerCipherSuites: true,
 		}
 		}
+		logger.Debug(logSender, "", "configured TLS cipher suites for binding %#v: %v, certID: %v",
+			s.binding.GetAddress(), s.binding.ciphers, certID)
 		if s.binding.isMutualTLSEnabled() {
 		if s.binding.isMutualTLSEnabled() {
 			s.tlsConfig.ClientCAs = certMgr.GetRootCAs()
 			s.tlsConfig.ClientCAs = certMgr.GetRootCAs()
 			s.tlsConfig.VerifyConnection = s.verifyTLSConnection
 			s.tlsConfig.VerifyConnection = s.verifyTLSConnection

+ 1 - 1
go.mod

@@ -158,7 +158,7 @@ require (
 	gopkg.in/ini.v1 v1.66.4 // indirect
 	gopkg.in/ini.v1 v1.66.4 // indirect
 	gopkg.in/square/go-jose.v2 v2.6.0 // indirect
 	gopkg.in/square/go-jose.v2 v2.6.0 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
-	gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99 // indirect
+	gopkg.in/yaml.v3 v3.0.0 // indirect
 )
 )
 
 
 replace (
 replace (

+ 2 - 2
go.sum

@@ -1270,8 +1270,8 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99 h1:dbuHpmKjkDzSOMKAWl10QNlgaZUd3V1q99xc81tt2Kc=
-gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
+gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gorm.io/driver/postgres v1.3.5/go.mod h1:EGCWefLFQSVFrHGy4J8EtiHCWX5Q8t0yz2Jt9aKkGzU=
 gorm.io/driver/postgres v1.3.5/go.mod h1:EGCWefLFQSVFrHGy4J8EtiHCWX5Q8t0yz2Jt9aKkGzU=
 gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
 gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
 gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
 gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=

+ 35 - 6
httpd/httpd.go

@@ -397,6 +397,10 @@ type Binding struct {
 	EnableWebClient bool `json:"enable_web_client" mapstructure:"enable_web_client"`
 	EnableWebClient bool `json:"enable_web_client" mapstructure:"enable_web_client"`
 	// you also need to provide a certificate for enabling HTTPS
 	// you also need to provide a certificate for enabling HTTPS
 	EnableHTTPS bool `json:"enable_https" mapstructure:"enable_https"`
 	EnableHTTPS bool `json:"enable_https" mapstructure:"enable_https"`
+	// Certificate and matching private key for this specific binding, if empty the global
+	// ones will be used, if any
+	CertificateFile    string `json:"certificate_file" mapstructure:"certificate_file"`
+	CertificateKeyFile string `json:"certificate_key_file" mapstructure:"certificate_key_file"`
 	// Defines the minimum TLS version. 13 means TLS 1.3, default is TLS 1.2
 	// Defines the minimum TLS version. 13 means TLS 1.3, default is TLS 1.2
 	MinTLSVersion int `json:"min_tls_version" mapstructure:"min_tls_version"`
 	MinTLSVersion int `json:"min_tls_version" mapstructure:"min_tls_version"`
 	// set to 1 to require client certificate authentication in addition to basic auth.
 	// set to 1 to require client certificate authentication in addition to basic auth.
@@ -563,8 +567,8 @@ type Conf struct {
 	// Defines a base URL for the web admin and client interfaces. If empty web admin and client resources will
 	// Defines a base URL for the web admin and client interfaces. If empty web admin and client resources will
 	// be available at the root ("/") URI. If defined it must be an absolute URI or it will be ignored.
 	// be available at the root ("/") URI. If defined it must be an absolute URI or it will be ignored.
 	WebRoot string `json:"web_root" mapstructure:"web_root"`
 	WebRoot string `json:"web_root" mapstructure:"web_root"`
-	// If files containing a certificate and matching private key for the server are provided the server will expect
-	// HTTPS connections.
+	// If files containing a certificate and matching private key for the server are provided you can enable
+	// HTTPS connections for the configured bindings.
 	// Certificate and key files can be reloaded on demand sending a "SIGHUP" signal on Unix based systems and a
 	// Certificate and key files can be reloaded on demand sending a "SIGHUP" signal on Unix based systems and a
 	// "paramchange" request to the running service on Windows.
 	// "paramchange" request to the running service on Windows.
 	CertificateFile    string `json:"certificate_file" mapstructure:"certificate_file"`
 	CertificateFile    string `json:"certificate_file" mapstructure:"certificate_file"`
@@ -651,6 +655,32 @@ func (c *Conf) getRedacted() Conf {
 	return conf
 	return conf
 }
 }
 
 
+func (c *Conf) getKeyPairs(configDir string) []common.TLSKeyPair {
+	var keyPairs []common.TLSKeyPair
+
+	for _, binding := range c.Bindings {
+		certificateFile := getConfigPath(binding.CertificateFile, configDir)
+		certificateKeyFile := getConfigPath(binding.CertificateKeyFile, configDir)
+		if certificateFile != "" && certificateKeyFile != "" {
+			keyPairs = append(keyPairs, common.TLSKeyPair{
+				Cert: certificateFile,
+				Key:  certificateKeyFile,
+				ID:   binding.GetAddress(),
+			})
+		}
+	}
+	certificateFile := getConfigPath(c.CertificateFile, configDir)
+	certificateKeyFile := getConfigPath(c.CertificateKeyFile, configDir)
+	if certificateFile != "" && certificateKeyFile != "" {
+		keyPairs = append(keyPairs, common.TLSKeyPair{
+			Cert: certificateFile,
+			Key:  certificateKeyFile,
+			ID:   common.DefaultTLSKeyPaidID,
+		})
+	}
+	return keyPairs
+}
+
 // Initialize configures and starts the HTTP server
 // Initialize configures and starts the HTTP server
 func (c *Conf) Initialize(configDir string, isShared int) error {
 func (c *Conf) Initialize(configDir string, isShared int) error {
 	logger.Info(logSender, "", "initializing HTTP server with config %+v", c.getRedacted())
 	logger.Info(logSender, "", "initializing HTTP server with config %+v", c.getRedacted())
@@ -662,8 +692,6 @@ func (c *Conf) Initialize(configDir string, isShared int) error {
 	if err := c.checkRequiredDirs(staticFilesPath, templatesPath); err != nil {
 	if err := c.checkRequiredDirs(staticFilesPath, templatesPath); err != nil {
 		return err
 		return err
 	}
 	}
-	certificateFile := getConfigPath(c.CertificateFile, configDir)
-	certificateKeyFile := getConfigPath(c.CertificateKeyFile, configDir)
 	if c.isWebAdminEnabled() {
 	if c.isWebAdminEnabled() {
 		updateWebAdminURLs(c.WebRoot)
 		updateWebAdminURLs(c.WebRoot)
 		loadAdminTemplates(templatesPath)
 		loadAdminTemplates(templatesPath)
@@ -676,8 +704,9 @@ func (c *Conf) Initialize(configDir string, isShared int) error {
 	} else {
 	} else {
 		logger.Info(logSender, "", "built-in web client interface disabled")
 		logger.Info(logSender, "", "built-in web client interface disabled")
 	}
 	}
-	if certificateFile != "" && certificateKeyFile != "" {
-		mgr, err := common.NewCertManager(certificateFile, certificateKeyFile, configDir, logSender)
+	keyPairs := c.getKeyPairs(configDir)
+	if len(keyPairs) > 0 {
+		mgr, err := common.NewCertManager(keyPairs, configDir, logSender)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}

+ 2 - 2
httpd/httpd_test.go

@@ -407,8 +407,8 @@ func TestMain(m *testing.M) {
 	}
 	}
 	httpdConf.Bindings[0].Port = 8443
 	httpdConf.Bindings[0].Port = 8443
 	httpdConf.Bindings[0].EnableHTTPS = true
 	httpdConf.Bindings[0].EnableHTTPS = true
-	httpdConf.CertificateFile = certPath
-	httpdConf.CertificateKeyFile = keyPath
+	httpdConf.Bindings[0].CertificateFile = certPath
+	httpdConf.Bindings[0].CertificateKeyFile = keyPath
 	httpdConf.Bindings = append(httpdConf.Bindings, httpd.Binding{})
 	httpdConf.Bindings = append(httpdConf.Bindings, httpd.Binding{})
 
 
 	go func() {
 	go func() {

+ 8 - 1
httpd/internal_test.go

@@ -1358,7 +1358,14 @@ func TestVerifyTLSConnection(t *testing.T) {
 	err = os.WriteFile(keyPath, []byte(httpdKey), os.ModePerm)
 	err = os.WriteFile(keyPath, []byte(httpdKey), os.ModePerm)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 
 
-	certMgr, err = common.NewCertManager(certPath, keyPath, "", "webdav_test")
+	keyPairs := []common.TLSKeyPair{
+		{
+			Cert: certPath,
+			Key:  keyPath,
+			ID:   common.DefaultTLSKeyPaidID,
+		},
+	}
+	certMgr, err = common.NewCertManager(keyPairs, "", "httpd_test")
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 
 
 	certMgr.SetCARevocationLists([]string{caCrlPath})
 	certMgr.SetCARevocationLists([]string{caCrlPath})

+ 7 - 3
httpd/server.go

@@ -79,16 +79,20 @@ func (s *httpdServer) listenAndServe() error {
 		ErrorLog:          log.New(&logger.StdLoggerWrapper{Sender: logSender}, "", 0),
 		ErrorLog:          log.New(&logger.StdLoggerWrapper{Sender: logSender}, "", 0),
 	}
 	}
 	if certMgr != nil && s.binding.EnableHTTPS {
 	if certMgr != nil && s.binding.EnableHTTPS {
+		certID := common.DefaultTLSKeyPaidID
+		if getConfigPath(s.binding.CertificateFile, "") != "" && getConfigPath(s.binding.CertificateKeyFile, "") != "" {
+			certID = s.binding.GetAddress()
+		}
 		config := &tls.Config{
 		config := &tls.Config{
-			GetCertificate:           certMgr.GetCertificateFunc(),
+			GetCertificate:           certMgr.GetCertificateFunc(certID),
 			MinVersion:               util.GetTLSVersion(s.binding.MinTLSVersion),
 			MinVersion:               util.GetTLSVersion(s.binding.MinTLSVersion),
 			NextProtos:               []string{"http/1.1", "h2"},
 			NextProtos:               []string{"http/1.1", "h2"},
 			CipherSuites:             util.GetTLSCiphersFromNames(s.binding.TLSCipherSuites),
 			CipherSuites:             util.GetTLSCiphersFromNames(s.binding.TLSCipherSuites),
 			PreferServerCipherSuites: true,
 			PreferServerCipherSuites: true,
 		}
 		}
-		logger.Debug(logSender, "", "configured TLS cipher suites for binding %#v: %v", s.binding.GetAddress(),
-			config.CipherSuites)
 		httpServer.TLSConfig = config
 		httpServer.TLSConfig = config
+		logger.Debug(logSender, "", "configured TLS cipher suites for binding %#v: %v, certID: %v",
+			s.binding.GetAddress(), httpServer.TLSConfig.CipherSuites, certID)
 		if s.binding.ClientAuthType == 1 {
 		if s.binding.ClientAuthType == 1 {
 			httpServer.TLSConfig.ClientCAs = certMgr.GetRootCAs()
 			httpServer.TLSConfig.ClientCAs = certMgr.GetRootCAs()
 			httpServer.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert
 			httpServer.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert

+ 6 - 0
sftpgo.json

@@ -93,6 +93,8 @@
         "address": "",
         "address": "",
         "apply_proxy_config": true,
         "apply_proxy_config": true,
         "tls_mode": 0,
         "tls_mode": 0,
+        "certificate_file": "",
+        "certificate_key_file": "",
         "min_tls_version": 12,
         "min_tls_version": 12,
         "force_passive_ip": "",
         "force_passive_ip": "",
         "passive_ip_overrides": [],
         "passive_ip_overrides": [],
@@ -125,6 +127,8 @@
         "port": 0,
         "port": 0,
         "address": "",
         "address": "",
         "enable_https": false,
         "enable_https": false,
+        "certificate_file": "",
+        "certificate_key_file": "",
         "min_tls_version": 12,
         "min_tls_version": 12,
         "client_auth_type": 0,
         "client_auth_type": 0,
         "tls_cipher_suites": [],
         "tls_cipher_suites": [],
@@ -225,6 +229,8 @@
         "enable_web_admin": true,
         "enable_web_admin": true,
         "enable_web_client": true,
         "enable_web_client": true,
         "enable_https": false,
         "enable_https": false,
+        "certificate_file": "",
+        "certificate_key_file": "",
         "min_tls_version": 12,
         "min_tls_version": 12,
         "client_auth_type": 0,
         "client_auth_type": 0,
         "tls_cipher_suites": [],
         "tls_cipher_suites": [],

+ 9 - 2
telemetry/telemetry.go

@@ -100,12 +100,19 @@ func (c Conf) Initialize(configDir string) error {
 		ErrorLog:          log.New(&logger.StdLoggerWrapper{Sender: logSender}, "", 0),
 		ErrorLog:          log.New(&logger.StdLoggerWrapper{Sender: logSender}, "", 0),
 	}
 	}
 	if certificateFile != "" && certificateKeyFile != "" {
 	if certificateFile != "" && certificateKeyFile != "" {
-		certMgr, err = common.NewCertManager(certificateFile, certificateKeyFile, configDir, logSender)
+		keyPairs := []common.TLSKeyPair{
+			{
+				Cert: certificateFile,
+				Key:  certificateKeyFile,
+				ID:   common.DefaultTLSKeyPaidID,
+			},
+		}
+		certMgr, err = common.NewCertManager(keyPairs, configDir, logSender)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
 		config := &tls.Config{
 		config := &tls.Config{
-			GetCertificate:           certMgr.GetCertificateFunc(),
+			GetCertificate:           certMgr.GetCertificateFunc(common.DefaultTLSKeyPaidID),
 			MinVersion:               util.GetTLSVersion(c.MinTLSVersion),
 			MinVersion:               util.GetTLSVersion(c.MinTLSVersion),
 			NextProtos:               []string{"http/1.1", "h2"},
 			NextProtos:               []string{"http/1.1", "h2"},
 			CipherSuites:             util.GetTLSCiphersFromNames(c.TLSCipherSuites),
 			CipherSuites:             util.GetTLSCiphersFromNames(c.TLSCipherSuites),

+ 8 - 1
webdavd/internal_test.go

@@ -1480,7 +1480,14 @@ func TestVerifyTLSConnection(t *testing.T) {
 	err = os.WriteFile(keyPath, []byte(webDavKey), os.ModePerm)
 	err = os.WriteFile(keyPath, []byte(webDavKey), os.ModePerm)
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 
 
-	certMgr, err = common.NewCertManager(certPath, keyPath, "", "webdav_test")
+	keyPairs := []common.TLSKeyPair{
+		{
+			Cert: certPath,
+			Key:  keyPath,
+			ID:   common.DefaultTLSKeyPaidID,
+		},
+	}
+	certMgr, err = common.NewCertManager(keyPairs, "", "webdav_test")
 	assert.NoError(t, err)
 	assert.NoError(t, err)
 
 
 	certMgr.SetCARevocationLists([]string{caCrlPath})
 	certMgr.SetCARevocationLists([]string{caCrlPath})

+ 7 - 3
webdavd/server.go

@@ -56,15 +56,19 @@ func (s *webDavServer) listenAndServe(compressor *middleware.Compressor) error {
 	httpServer.Handler = handler
 	httpServer.Handler = handler
 	if certMgr != nil && s.binding.EnableHTTPS {
 	if certMgr != nil && s.binding.EnableHTTPS {
 		serviceStatus.Bindings = append(serviceStatus.Bindings, s.binding)
 		serviceStatus.Bindings = append(serviceStatus.Bindings, s.binding)
+		certID := common.DefaultTLSKeyPaidID
+		if getConfigPath(s.binding.CertificateFile, "") != "" && getConfigPath(s.binding.CertificateKeyFile, "") != "" {
+			certID = s.binding.GetAddress()
+		}
 		httpServer.TLSConfig = &tls.Config{
 		httpServer.TLSConfig = &tls.Config{
-			GetCertificate:           certMgr.GetCertificateFunc(),
+			GetCertificate:           certMgr.GetCertificateFunc(certID),
 			MinVersion:               util.GetTLSVersion(s.binding.MinTLSVersion),
 			MinVersion:               util.GetTLSVersion(s.binding.MinTLSVersion),
 			NextProtos:               []string{"http/1.1", "h2"},
 			NextProtos:               []string{"http/1.1", "h2"},
 			CipherSuites:             util.GetTLSCiphersFromNames(s.binding.TLSCipherSuites),
 			CipherSuites:             util.GetTLSCiphersFromNames(s.binding.TLSCipherSuites),
 			PreferServerCipherSuites: true,
 			PreferServerCipherSuites: true,
 		}
 		}
-		logger.Debug(logSender, "", "configured TLS cipher suites for binding %#v: %v", s.binding.GetAddress(),
-			httpServer.TLSConfig.CipherSuites)
+		logger.Debug(logSender, "", "configured TLS cipher suites for binding %#v: %v, certID: %v",
+			s.binding.GetAddress(), httpServer.TLSConfig.CipherSuites, certID)
 		if s.binding.isMutualTLSEnabled() {
 		if s.binding.isMutualTLSEnabled() {
 			httpServer.TLSConfig.ClientCAs = certMgr.GetRootCAs()
 			httpServer.TLSConfig.ClientCAs = certMgr.GetRootCAs()
 			httpServer.TLSConfig.VerifyConnection = s.verifyTLSConnection
 			httpServer.TLSConfig.VerifyConnection = s.verifyTLSConnection

+ 35 - 6
webdavd/webdavd.go

@@ -73,6 +73,10 @@ type Binding struct {
 	Port int `json:"port" mapstructure:"port"`
 	Port int `json:"port" mapstructure:"port"`
 	// you also need to provide a certificate for enabling HTTPS
 	// you also need to provide a certificate for enabling HTTPS
 	EnableHTTPS bool `json:"enable_https" mapstructure:"enable_https"`
 	EnableHTTPS bool `json:"enable_https" mapstructure:"enable_https"`
+	// Certificate and matching private key for this specific binding, if empty the global
+	// ones will be used, if any
+	CertificateFile    string `json:"certificate_file" mapstructure:"certificate_file"`
+	CertificateKeyFile string `json:"certificate_key_file" mapstructure:"certificate_key_file"`
 	// Defines the minimum TLS version. 13 means TLS 1.3, default is TLS 1.2
 	// Defines the minimum TLS version. 13 means TLS 1.3, default is TLS 1.2
 	MinTLSVersion int `json:"min_tls_version" mapstructure:"min_tls_version"`
 	MinTLSVersion int `json:"min_tls_version" mapstructure:"min_tls_version"`
 	// set to 1 to require client certificate authentication in addition to basic auth.
 	// set to 1 to require client certificate authentication in addition to basic auth.
@@ -124,8 +128,8 @@ func (b *Binding) IsValid() bool {
 type Configuration struct {
 type Configuration struct {
 	// Addresses and ports to bind to
 	// Addresses and ports to bind to
 	Bindings []Binding `json:"bindings" mapstructure:"bindings"`
 	Bindings []Binding `json:"bindings" mapstructure:"bindings"`
-	// If files containing a certificate and matching private key for the server are provided the server will expect
-	// HTTPS connections.
+	// If files containing a certificate and matching private key for the server are provided you
+	// can enable HTTPS connections for the configured bindings
 	// Certificate and key files can be reloaded on demand sending a "SIGHUP" signal on Unix based systems and a
 	// Certificate and key files can be reloaded on demand sending a "SIGHUP" signal on Unix based systems and a
 	// "paramchange" request to the running service on Windows.
 	// "paramchange" request to the running service on Windows.
 	CertificateFile    string `json:"certificate_file" mapstructure:"certificate_file"`
 	CertificateFile    string `json:"certificate_file" mapstructure:"certificate_file"`
@@ -157,6 +161,32 @@ func (c *Configuration) ShouldBind() bool {
 	return false
 	return false
 }
 }
 
 
+func (c *Configuration) getKeyPairs(configDir string) []common.TLSKeyPair {
+	var keyPairs []common.TLSKeyPair
+
+	for _, binding := range c.Bindings {
+		certificateFile := getConfigPath(binding.CertificateFile, configDir)
+		certificateKeyFile := getConfigPath(binding.CertificateKeyFile, configDir)
+		if certificateFile != "" && certificateKeyFile != "" {
+			keyPairs = append(keyPairs, common.TLSKeyPair{
+				Cert: certificateFile,
+				Key:  certificateKeyFile,
+				ID:   binding.GetAddress(),
+			})
+		}
+	}
+	certificateFile := getConfigPath(c.CertificateFile, configDir)
+	certificateKeyFile := getConfigPath(c.CertificateKeyFile, configDir)
+	if certificateFile != "" && certificateKeyFile != "" {
+		keyPairs = append(keyPairs, common.TLSKeyPair{
+			Cert: certificateFile,
+			Key:  certificateKeyFile,
+			ID:   common.DefaultTLSKeyPaidID,
+		})
+	}
+	return keyPairs
+}
+
 // Initialize configures and starts the WebDAV server
 // Initialize configures and starts the WebDAV server
 func (c *Configuration) Initialize(configDir string) error {
 func (c *Configuration) Initialize(configDir string) error {
 	logger.Info(logSender, "", "initializing WebDAV server with config %+v", *c)
 	logger.Info(logSender, "", "initializing WebDAV server with config %+v", *c)
@@ -171,10 +201,9 @@ func (c *Configuration) Initialize(configDir string) error {
 		return common.ErrNoBinding
 		return common.ErrNoBinding
 	}
 	}
 
 
-	certificateFile := getConfigPath(c.CertificateFile, configDir)
-	certificateKeyFile := getConfigPath(c.CertificateKeyFile, configDir)
-	if certificateFile != "" && certificateKeyFile != "" {
-		mgr, err := common.NewCertManager(certificateFile, certificateKeyFile, configDir, logSender)
+	keyPairs := c.getKeyPairs(configDir)
+	if len(keyPairs) > 0 {
+		mgr, err := common.NewCertManager(keyPairs, configDir, logSender)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}

+ 5 - 5
webdavd/webdavd_test.go

@@ -336,8 +336,6 @@ func TestMain(m *testing.M) {
 	sftpdConf.HostKeys = []string{hostKeyPath}
 	sftpdConf.HostKeys = []string{hostKeyPath}
 
 
 	webDavConf := config.GetWebDAVDConfig()
 	webDavConf := config.GetWebDAVDConfig()
-	webDavConf.CertificateFile = certPath
-	webDavConf.CertificateKeyFile = keyPath
 	webDavConf.CACertificates = []string{caCrtPath}
 	webDavConf.CACertificates = []string{caCrtPath}
 	webDavConf.CARevocationLists = []string{caCRLPath}
 	webDavConf.CARevocationLists = []string{caCRLPath}
 	webDavConf.Bindings = []webdavd.Binding{
 	webDavConf.Bindings = []webdavd.Binding{
@@ -345,9 +343,11 @@ func TestMain(m *testing.M) {
 			Port: webDavServerPort,
 			Port: webDavServerPort,
 		},
 		},
 		{
 		{
-			Port:           webDavTLSServerPort,
-			EnableHTTPS:    true,
-			ClientAuthType: 2,
+			Port:               webDavTLSServerPort,
+			EnableHTTPS:        true,
+			CertificateFile:    certPath,
+			CertificateKeyFile: keyPath,
+			ClientAuthType:     2,
 		},
 		},
 	}
 	}
 	webDavConf.Cors = webdavd.CorsConfig{
 	webDavConf.Cors = webdavd.CorsConfig{