diff --git a/docs/full-configuration.md b/docs/full-configuration.md
index 63bd2f52..6a5cbd81 100644
--- a/docs/full-configuration.md
+++ b/docs/full-configuration.md
@@ -147,6 +147,7 @@ The configuration file contains the following sections:
- `kex_algorithms`, list of strings. Available KEX (Key Exchange) algorithms in preference order. Leave empty to use default values. The supported values are: `curve25519-sha256`, `curve25519-sha256@libssh.org`, `ecdh-sha2-nistp256`, `ecdh-sha2-nistp384`, `ecdh-sha2-nistp521`, `diffie-hellman-group14-sha256`, `diffie-hellman-group16-sha512`, `diffie-hellman-group14-sha1`, `diffie-hellman-group1-sha1`. Default values: `curve25519-sha256`, `curve25519-sha256@libssh.org`, `ecdh-sha2-nistp256`, `ecdh-sha2-nistp384`, `ecdh-sha2-nistp521`, `diffie-hellman-group14-sha256`. SHA512 based KEXs are disabled by default because they are slow. If you set one or more moduli files, `diffie-hellman-group-exchange-sha256` and `diffie-hellman-group-exchange-sha1` will be available.
- `ciphers`, list of strings. Allowed ciphers in preference order. Leave empty to use default values. The supported values are: `aes128-gcm@openssh.com`, `aes256-gcm@openssh.com`, `chacha20-poly1305@openssh.com`, `aes128-ctr`, `aes192-ctr`, `aes256-ctr`, `aes128-cbc`, `aes192-cbc`, `aes256-cbc`, `3des-cbc`, `arcfour256`, `arcfour128`, `arcfour`. Default values: `aes128-gcm@openssh.com`, `aes256-gcm@openssh.com`, `chacha20-poly1305@openssh.com`, `aes128-ctr`, `aes192-ctr`, `aes256-ctr`. Please note that the ciphers disabled by default are insecure, you should expect that an active attacker can recover plaintext if you enable them.
- `macs`, list of strings. Available MAC (message authentication code) algorithms in preference order. Leave empty to use default values. The supported values are: `hmac-sha2-256-etm@openssh.com`, `hmac-sha2-256`, `hmac-sha2-512-etm@openssh.com`, `hmac-sha2-512`, `hmac-sha1`, `hmac-sha1-96`. Default values: `hmac-sha2-256-etm@openssh.com`, `hmac-sha2-256`.
+ - `public_key_algorithms`, list of strings. Public key algorithms that the server will accept for client authentication. The supported values are: `ecdsa-sha2-nistp256`, `ecdsa-sha2-nistp384`, `ecdsa-sha2-nistp521`, `rsa-sha2-512`, `rsa-sha2-256`, `ssh-rsa`, `ssh-dss`, `ssh-ed25519`, `sk-ssh-ed25519@openssh.com`, `sk-ecdsa-sha2-nistp256@openssh.com`. Default values: `ecdsa-sha2-nistp256`, `ecdsa-sha2-nistp384`, `ecdsa-sha2-nistp521`, `rsa-sha2-512`, `rsa-sha2-256`, `ssh-ed25519`, `sk-ssh-ed25519@openssh.com`, `sk-ecdsa-sha2-nistp256@openssh.com`.
- `trusted_user_ca_keys`, list of public keys paths of certificate authorities that are trusted to sign user certificates for authentication. The paths can be absolute or relative to the configuration directory.
- `revoked_user_certs_file`, path to a file containing the revoked user certificates. The path can be absolute or relative to the configuration directory. It must contain a JSON list with the public key fingerprints of the revoked certificates. Example content: `["SHA256:bsBRHC/xgiqBJdSuvSTNpJNLTISP/G356jNMCRYC5Es","SHA256:119+8cL/HH+NLMawRsJx6CzPF1I3xC+jpM60bQHXGE8"]`. The revocation list can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows. Default: "".
- `login_banner_file`, path to the login banner file. The contents of the specified file, if any, are sent to the remote user before authentication is allowed. It can be a path relative to the config dir or an absolute one. Leave empty to disable login banner.
diff --git a/go.mod b/go.mod
index ed59c30a..7a2bfb4a 100644
--- a/go.mod
+++ b/go.mod
@@ -177,5 +177,5 @@ replace (
github.com/fclairamb/ftpserverlib => github.com/drakkan/ftpserverlib v0.0.0-20230820193955-e7243edeb89b
github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
github.com/robfig/cron/v3 => github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0
- golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20231109082937-60ac5813bca0
+ golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20231109180513-aa0daef37eeb
)
diff --git a/go.sum b/go.sum
index d26137d6..d5288e30 100644
--- a/go.sum
+++ b/go.sum
@@ -153,8 +153,8 @@ github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0 h1:EW9gIJRmt9lzk66Fhh4S8VEtURA6QHZqGeSRE9Nb2/U=
github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
-github.com/drakkan/crypto v0.0.0-20231109082937-60ac5813bca0 h1:5UwG68raSmbWbZdCDOxxaQpzfRS/2/4XLjP+o5lOvt0=
-github.com/drakkan/crypto v0.0.0-20231109082937-60ac5813bca0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
+github.com/drakkan/crypto v0.0.0-20231109180513-aa0daef37eeb h1:2CkHnBtgdS29SoGR4SI9wkE711HRkC9983PNYi+vtKQ=
+github.com/drakkan/crypto v0.0.0-20231109180513-aa0daef37eeb/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 h1:LPH1dEblAOO/LoG7yHPMtBLXhQmjaga91/DDjWk9jWA=
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9/go.mod h1:2lmrmq866uF2tnje75wQHzmPXhmSWUt7Gyx2vgK1RCU=
github.com/drakkan/ftpserverlib v0.0.0-20230820193955-e7243edeb89b h1:sCtiYerLxfOQrSludkwGwwXLlSVHxpvfmyOxjCOf0ec=
diff --git a/internal/config/config.go b/internal/config/config.go
index f1bbaf76..3b6e8bea 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -263,6 +263,7 @@ func Init() {
KexAlgorithms: []string{},
Ciphers: []string{},
MACs: []string{},
+ PublicKeyAlgorithms: []string{},
TrustedUserCAKeys: []string{},
RevokedUserCertsFile: "",
LoginBannerFile: "",
@@ -2020,6 +2021,7 @@ func setViperDefaults() {
viper.SetDefault("sftpd.kex_algorithms", globalConf.SFTPD.KexAlgorithms)
viper.SetDefault("sftpd.ciphers", globalConf.SFTPD.Ciphers)
viper.SetDefault("sftpd.macs", globalConf.SFTPD.MACs)
+ viper.SetDefault("sftpd.public_key_algorithms", globalConf.SFTPD.PublicKeyAlgorithms)
viper.SetDefault("sftpd.trusted_user_ca_keys", globalConf.SFTPD.TrustedUserCAKeys)
viper.SetDefault("sftpd.revoked_user_certs_file", globalConf.SFTPD.RevokedUserCertsFile)
viper.SetDefault("sftpd.login_banner_file", globalConf.SFTPD.LoginBannerFile)
diff --git a/internal/dataprovider/configs.go b/internal/dataprovider/configs.go
index 3417d28b..e4d560c2 100644
--- a/internal/dataprovider/configs.go
+++ b/internal/dataprovider/configs.go
@@ -28,8 +28,9 @@ import (
// Supported values for host keys, KEXs, ciphers, MACs
var (
- supportedHostKeyAlgos = []string{ssh.KeyAlgoRSA}
- supportedKexAlgos = []string{
+ supportedHostKeyAlgos = []string{ssh.KeyAlgoRSA}
+ supportedPublicKeyAlgos = []string{ssh.KeyAlgoRSA, ssh.KeyAlgoDSA}
+ supportedKexAlgos = []string{
"diffie-hellman-group16-sha512", "diffie-hellman-group14-sha1", "diffie-hellman-group1-sha1",
"diffie-hellman-group-exchange-sha256", "diffie-hellman-group-exchange-sha1",
}
@@ -45,17 +46,21 @@ var (
// SFTPDConfigs defines configurations for SFTPD
type SFTPDConfigs struct {
- HostKeyAlgos []string `json:"host_key_algos,omitempty"`
- Moduli []string `json:"moduli,omitempty"`
- KexAlgorithms []string `json:"kex_algorithms,omitempty"`
- Ciphers []string `json:"ciphers,omitempty"`
- MACs []string `json:"macs,omitempty"`
+ HostKeyAlgos []string `json:"host_key_algos,omitempty"`
+ PublicKeyAlgos []string `json:"public_key_algos,omitempty"`
+ Moduli []string `json:"moduli,omitempty"`
+ KexAlgorithms []string `json:"kex_algorithms,omitempty"`
+ Ciphers []string `json:"ciphers,omitempty"`
+ MACs []string `json:"macs,omitempty"`
}
func (c *SFTPDConfigs) isEmpty() bool {
if len(c.HostKeyAlgos) > 0 {
return false
}
+ if len(c.PublicKeyAlgos) > 0 {
+ return false
+ }
if len(c.Moduli) > 0 {
return false
}
@@ -76,6 +81,11 @@ func (*SFTPDConfigs) GetSupportedHostKeyAlgos() []string {
return supportedHostKeyAlgos
}
+// GetSupportedPublicKeyAlgos returns the supported legacy public key algos
+func (*SFTPDConfigs) GetSupportedPublicKeyAlgos() []string {
+ return supportedPublicKeyAlgos
+}
+
// GetSupportedKEXAlgos returns the supported KEX algos
func (*SFTPDConfigs) GetSupportedKEXAlgos() []string {
return supportedKexAlgos
@@ -129,12 +139,19 @@ func (c *SFTPDConfigs) validate() error {
return util.NewValidationError(fmt.Sprintf("unsupported MAC algorithm %q", mac))
}
}
+ for _, algo := range c.PublicKeyAlgos {
+ if !util.Contains(supportedPublicKeyAlgos, algo) {
+ return util.NewValidationError(fmt.Sprintf("unsupported public key algorithm %q", algo))
+ }
+ }
return nil
}
func (c *SFTPDConfigs) getACopy() *SFTPDConfigs {
hostKeys := make([]string, len(c.HostKeyAlgos))
copy(hostKeys, c.HostKeyAlgos)
+ publicKeys := make([]string, len(c.PublicKeyAlgos))
+ copy(publicKeys, c.PublicKeyAlgos)
moduli := make([]string, len(c.Moduli))
copy(moduli, c.Moduli)
kexs := make([]string, len(c.KexAlgorithms))
@@ -145,11 +162,12 @@ func (c *SFTPDConfigs) getACopy() *SFTPDConfigs {
copy(macs, c.MACs)
return &SFTPDConfigs{
- HostKeyAlgos: hostKeys,
- Moduli: moduli,
- KexAlgorithms: kexs,
- Ciphers: ciphers,
- MACs: macs,
+ HostKeyAlgos: hostKeys,
+ PublicKeyAlgos: publicKeys,
+ Moduli: moduli,
+ KexAlgorithms: kexs,
+ Ciphers: ciphers,
+ MACs: macs,
}
}
diff --git a/internal/httpd/httpd_test.go b/internal/httpd/httpd_test.go
index ae3c35d3..59582081 100644
--- a/internal/httpd/httpd_test.go
+++ b/internal/httpd/httpd_test.go
@@ -7823,7 +7823,8 @@ func TestLoaddata(t *testing.T) {
}
configs := dataprovider.Configs{
SFTPD: &dataprovider.SFTPDConfigs{
- HostKeyAlgos: []string{ssh.KeyAlgoRSA, ssh.CertAlgoRSAv01},
+ HostKeyAlgos: []string{ssh.KeyAlgoRSA, ssh.CertAlgoRSAv01},
+ PublicKeyAlgos: []string{ssh.KeyAlgoDSA},
},
SMTP: &dataprovider.SMTPConfigs{
Host: "mail.example.com",
@@ -7890,6 +7891,7 @@ func TestLoaddata(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, configs.SMTP, configsGet.SMTP)
assert.Equal(t, []string{ssh.KeyAlgoRSA}, configsGet.SFTPD.HostKeyAlgos)
+ assert.Equal(t, []string{ssh.KeyAlgoDSA}, configsGet.SFTPD.PublicKeyAlgos)
assert.Len(t, configsGet.SFTPD.Moduli, 0)
assert.Len(t, configsGet.SFTPD.KexAlgorithms, 0)
assert.Len(t, configsGet.SFTPD.Ciphers, 0)
@@ -12722,6 +12724,7 @@ func TestWebConfigsMock(t *testing.T) {
// save SFTP configs
form.Set("sftp_host_key_algos", ssh.KeyAlgoRSA)
form.Add("sftp_host_key_algos", ssh.CertAlgoDSAv01)
+ form.Set("sftp_pub_key_algos", ssh.KeyAlgoDSA)
form.Set("sftp_moduli", "path 1 , path 2")
form.Set("form_action", "sftp_submit")
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
@@ -12733,6 +12736,7 @@ func TestWebConfigsMock(t *testing.T) {
assert.Contains(t, rr.Body.String(), ssh.CertAlgoDSAv01) // invalid algo
form.Set("sftp_host_key_algos", ssh.KeyAlgoRSA)
form.Add("sftp_host_key_algos", ssh.CertAlgoRSAv01)
+ form.Set("sftp_pub_key_algos", ssh.KeyAlgoDSA)
form.Set("sftp_kex_algos", "diffie-hellman-group18-sha512")
form.Add("sftp_kex_algos", "diffie-hellman-group16-sha512")
req, err = http.NewRequest(http.MethodPost, webConfigsPath, bytes.NewBuffer([]byte(form.Encode())))
@@ -12747,6 +12751,8 @@ func TestWebConfigsMock(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, configs.SFTPD.HostKeyAlgos, 1)
assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.KeyAlgoRSA)
+ assert.Len(t, configs.SFTPD.PublicKeyAlgos, 1)
+ assert.Contains(t, configs.SFTPD.PublicKeyAlgos, ssh.KeyAlgoDSA)
assert.Len(t, configs.SFTPD.Moduli, 2)
assert.Contains(t, configs.SFTPD.Moduli, "path 1")
assert.Contains(t, configs.SFTPD.Moduli, "path 2")
@@ -12795,6 +12801,8 @@ func TestWebConfigsMock(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, configs.SFTPD.HostKeyAlgos, 1)
assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.KeyAlgoRSA)
+ assert.Len(t, configs.SFTPD.PublicKeyAlgos, 1)
+ assert.Contains(t, configs.SFTPD.PublicKeyAlgos, ssh.KeyAlgoDSA)
assert.Len(t, configs.SFTPD.Moduli, 2)
assert.Equal(t, "mail.example.net", configs.SMTP.Host)
assert.Equal(t, 587, configs.SMTP.Port)
@@ -12865,6 +12873,8 @@ func TestWebConfigsMock(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, configs.SFTPD.HostKeyAlgos, 1)
assert.Contains(t, configs.SFTPD.HostKeyAlgos, ssh.KeyAlgoRSA)
+ assert.Len(t, configs.SFTPD.PublicKeyAlgos, 1)
+ assert.Contains(t, configs.SFTPD.PublicKeyAlgos, ssh.KeyAlgoDSA)
assert.Len(t, configs.SFTPD.Moduli, 2)
assert.Equal(t, 80, configs.ACME.HTTP01Challenge.Port)
assert.Equal(t, 7, configs.ACME.Protocols)
@@ -12896,6 +12906,7 @@ func TestWebConfigsMock(t *testing.T) {
configs, err = dataprovider.GetConfigs()
assert.NoError(t, err)
assert.Len(t, configs.SFTPD.HostKeyAlgos, 1)
+ assert.Len(t, configs.SFTPD.PublicKeyAlgos, 1)
assert.Equal(t, 402, configs.ACME.HTTP01Challenge.Port)
assert.Equal(t, 1, configs.ACME.Protocols)
assert.Equal(t, domain, configs.ACME.Domain)
diff --git a/internal/httpd/webadmin.go b/internal/httpd/webadmin.go
index cf5e816a..ca491495 100644
--- a/internal/httpd/webadmin.go
+++ b/internal/httpd/webadmin.go
@@ -2553,11 +2553,12 @@ func getIPListEntryFromPostFields(r *http.Request, listType dataprovider.IPListT
func getSFTPConfigsFromPostFields(r *http.Request) *dataprovider.SFTPDConfigs {
return &dataprovider.SFTPDConfigs{
- HostKeyAlgos: r.Form["sftp_host_key_algos"],
- Moduli: getSliceFromDelimitedValues(r.Form.Get("sftp_moduli"), ","),
- KexAlgorithms: r.Form["sftp_kex_algos"],
- Ciphers: r.Form["sftp_ciphers"],
- MACs: r.Form["sftp_macs"],
+ HostKeyAlgos: r.Form["sftp_host_key_algos"],
+ PublicKeyAlgos: r.Form["sftp_pub_key_algos"],
+ Moduli: getSliceFromDelimitedValues(r.Form.Get("sftp_moduli"), ","),
+ KexAlgorithms: r.Form["sftp_kex_algos"],
+ Ciphers: r.Form["sftp_ciphers"],
+ MACs: r.Form["sftp_macs"],
}
}
diff --git a/internal/sftpd/internal_test.go b/internal/sftpd/internal_test.go
index a0ba915d..14cf4934 100644
--- a/internal/sftpd/internal_test.go
+++ b/internal/sftpd/internal_test.go
@@ -1879,13 +1879,15 @@ func TestConfigsFromProvider(t *testing.T) {
assert.Len(t, c.KexAlgorithms, 0)
assert.Len(t, c.Ciphers, 0)
assert.Len(t, c.MACs, 0)
+ assert.Len(t, c.PublicKeyAlgorithms, 0)
configs := dataprovider.Configs{
SFTPD: &dataprovider.SFTPDConfigs{
- HostKeyAlgos: []string{ssh.KeyAlgoRSA},
- Moduli: []string{"/etc/ssh/moduli"},
- KexAlgorithms: []string{kexDHGroupExchangeSHA256},
- Ciphers: []string{"aes128-cbc", "aes192-cbc", "aes256-cbc"},
- MACs: []string{"hmac-sha2-512-etm@openssh.com"},
+ HostKeyAlgos: []string{ssh.KeyAlgoRSA},
+ Moduli: []string{"/etc/ssh/moduli"},
+ KexAlgorithms: []string{kexDHGroupExchangeSHA256},
+ Ciphers: []string{"aes128-cbc", "aes192-cbc", "aes256-cbc"},
+ MACs: []string{"hmac-sha2-512-etm@openssh.com"},
+ PublicKeyAlgos: []string{ssh.KeyAlgoDSA},
},
}
err = dataprovider.UpdateConfigs(&configs, "", "", "")
@@ -1896,11 +1898,13 @@ func TestConfigsFromProvider(t *testing.T) {
expectedKEXs := append(preferredKexAlgos, configs.SFTPD.KexAlgorithms...)
expectedCiphers := append(preferredCiphers, configs.SFTPD.Ciphers...)
expectedMACs := append(preferredMACs, configs.SFTPD.MACs...)
+ expectedPublicKeyAlgos := append(preferredPublicKeyAlgos, configs.SFTPD.PublicKeyAlgos...)
assert.Equal(t, expectedHostKeyAlgos, c.HostKeyAlgorithms)
assert.Equal(t, expectedKEXs, c.KexAlgorithms)
assert.Equal(t, expectedCiphers, c.Ciphers)
assert.Equal(t, expectedMACs, c.MACs)
assert.Equal(t, configs.SFTPD.Moduli, c.Moduli)
+ assert.Equal(t, expectedPublicKeyAlgos, c.PublicKeyAlgorithms)
err = dataprovider.UpdateConfigs(nil, "", "", "")
assert.NoError(t, err)
diff --git a/internal/sftpd/server.go b/internal/sftpd/server.go
index 8b1cb19d..02115570 100644
--- a/internal/sftpd/server.go
+++ b/internal/sftpd/server.go
@@ -69,6 +69,19 @@ var (
ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521,
ssh.KeyAlgoED25519,
}
+ supportedPublicKeyAlgos = []string{
+ ssh.KeyAlgoED25519,
+ ssh.KeyAlgoSKED25519, ssh.KeyAlgoSKECDSA256,
+ ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521,
+ ssh.KeyAlgoRSASHA256, ssh.KeyAlgoRSASHA512, ssh.KeyAlgoRSA,
+ ssh.KeyAlgoDSA,
+ }
+ preferredPublicKeyAlgos = []string{
+ ssh.KeyAlgoED25519,
+ ssh.KeyAlgoSKED25519, ssh.KeyAlgoSKECDSA256,
+ ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521,
+ ssh.KeyAlgoRSASHA256, ssh.KeyAlgoRSASHA512,
+ }
supportedKexAlgos = []string{
"curve25519-sha256", "curve25519-sha256@libssh.org",
"ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521",
@@ -171,6 +184,8 @@ type Configuration struct {
// MACs Specifies the available MAC (message authentication code) algorithms
// in preference order
MACs []string `json:"macs" mapstructure:"macs"`
+ // PublicKeyAlgorithms lists the supported public key algorithms for client authentication.
+ PublicKeyAlgorithms []string `json:"public_key_algorithms" mapstructure:"public_key_algorithms"`
// TrustedUserCAKeys specifies a list of public keys paths of certificate authorities
// that are trusted to sign user certificates for authentication.
// The paths can be absolute or relative to the configuration directory
@@ -318,6 +333,12 @@ func (c *Configuration) loadFromProvider() error {
}
c.HostKeyAlgorithms = append(c.HostKeyAlgorithms, configs.SFTPD.HostKeyAlgos...)
}
+ if len(configs.SFTPD.PublicKeyAlgos) > 0 {
+ if len(c.PublicKeyAlgorithms) == 0 {
+ c.PublicKeyAlgorithms = preferredPublicKeyAlgos
+ }
+ c.PublicKeyAlgorithms = append(c.PublicKeyAlgorithms, configs.SFTPD.PublicKeyAlgos...)
+ }
c.Moduli = append(c.Moduli, configs.SFTPD.Moduli...)
if len(configs.SFTPD.KexAlgorithms) > 0 {
if len(c.KexAlgorithms) == 0 {
@@ -441,7 +462,7 @@ func (c *Configuration) serve(listener net.Listener, serverConfig *ssh.ServerCon
}
}
-func (c *Configuration) configureSecurityOptions(serverConfig *ssh.ServerConfig) error {
+func (c *Configuration) configureKeyAlgos(serverConfig *ssh.ServerConfig) error {
if len(c.HostKeyAlgorithms) == 0 {
c.HostKeyAlgorithms = preferredHostKeyAlgos
} else {
@@ -453,6 +474,27 @@ func (c *Configuration) configureSecurityOptions(serverConfig *ssh.ServerConfig)
}
}
+ if len(c.PublicKeyAlgorithms) > 0 {
+ c.PublicKeyAlgorithms = util.RemoveDuplicates(c.PublicKeyAlgorithms, true)
+ for _, algo := range c.PublicKeyAlgorithms {
+ if !util.Contains(supportedPublicKeyAlgos, algo) {
+ return fmt.Errorf("unsupported public key authentication algorithm %q", algo)
+ }
+ }
+ } else {
+ c.PublicKeyAlgorithms = preferredPublicKeyAlgos
+ }
+ serverConfig.PublicKeyAuthAlgorithms = c.PublicKeyAlgorithms
+ serviceStatus.PublicKeyAlgorithms = c.PublicKeyAlgorithms
+
+ return nil
+}
+
+func (c *Configuration) configureSecurityOptions(serverConfig *ssh.ServerConfig) error {
+ if err := c.configureKeyAlgos(serverConfig); err != nil {
+ return err
+ }
+
if len(c.KexAlgorithms) > 0 {
hasDHGroupKEX := util.Contains(supportedKexAlgos, kexDHGroupExchangeSHA256)
if !hasDHGroupKEX {
@@ -468,11 +510,12 @@ func (c *Configuration) configureSecurityOptions(serverConfig *ssh.ServerConfig)
return fmt.Errorf("unsupported key-exchange algorithm %q", kex)
}
}
- serverConfig.KeyExchanges = c.KexAlgorithms
- serviceStatus.KexAlgorithms = c.KexAlgorithms
} else {
- serviceStatus.KexAlgorithms = preferredKexAlgos
+ c.KexAlgorithms = preferredKexAlgos
}
+ serverConfig.KeyExchanges = c.KexAlgorithms
+ serviceStatus.KexAlgorithms = c.KexAlgorithms
+
if len(c.Ciphers) > 0 {
c.Ciphers = util.RemoveDuplicates(c.Ciphers, true)
for _, cipher := range c.Ciphers {
@@ -480,11 +523,12 @@ func (c *Configuration) configureSecurityOptions(serverConfig *ssh.ServerConfig)
return fmt.Errorf("unsupported cipher %q", cipher)
}
}
- serverConfig.Ciphers = c.Ciphers
- serviceStatus.Ciphers = c.Ciphers
} else {
- serviceStatus.Ciphers = preferredCiphers
+ c.Ciphers = preferredCiphers
}
+ serverConfig.Ciphers = c.Ciphers
+ serviceStatus.Ciphers = c.Ciphers
+
if len(c.MACs) > 0 {
c.MACs = util.RemoveDuplicates(c.MACs, true)
for _, mac := range c.MACs {
@@ -492,11 +536,12 @@ func (c *Configuration) configureSecurityOptions(serverConfig *ssh.ServerConfig)
return fmt.Errorf("unsupported MAC algorithm %q", mac)
}
}
- serverConfig.MACs = c.MACs
- serviceStatus.MACs = c.MACs
} else {
- serviceStatus.MACs = preferredMACs
+ c.MACs = preferredMACs
}
+ serverConfig.MACs = c.MACs
+ serviceStatus.MACs = c.MACs
+
return nil
}
diff --git a/internal/sftpd/sftpd.go b/internal/sftpd/sftpd.go
index ae49e908..dea8a349 100644
--- a/internal/sftpd/sftpd.go
+++ b/internal/sftpd/sftpd.go
@@ -77,14 +77,15 @@ func (h *HostKey) GetAlgosAsString() string {
// ServiceStatus defines the service status
type ServiceStatus struct {
- IsActive bool `json:"is_active"`
- Bindings []Binding `json:"bindings"`
- SSHCommands []string `json:"ssh_commands"`
- HostKeys []HostKey `json:"host_keys"`
- Authentications []string `json:"authentications"`
- MACs []string `json:"macs"`
- KexAlgorithms []string `json:"kex_algorithms"`
- Ciphers []string `json:"ciphers"`
+ IsActive bool `json:"is_active"`
+ Bindings []Binding `json:"bindings"`
+ SSHCommands []string `json:"ssh_commands"`
+ HostKeys []HostKey `json:"host_keys"`
+ Authentications []string `json:"authentications"`
+ MACs []string `json:"macs"`
+ KexAlgorithms []string `json:"kex_algorithms"`
+ Ciphers []string `json:"ciphers"`
+ PublicKeyAlgorithms []string `json:"public_key_algorithms"`
}
// GetSSHCommandsAsString returns enabled SSH commands as comma separated string
@@ -112,6 +113,12 @@ func (s *ServiceStatus) GetCiphersAsString() string {
return strings.Join(s.Ciphers, ", ")
}
+// GetPublicKeysAlgosAsString returns enabled public key authentication
+// algorithms as comma separated string
+func (s *ServiceStatus) GetPublicKeysAlgosAsString() string {
+ return strings.Join(s.PublicKeyAlgorithms, ", ")
+}
+
// GetStatus returns the server status
func GetStatus() ServiceStatus {
return serviceStatus
diff --git a/internal/sftpd/sftpd_test.go b/internal/sftpd/sftpd_test.go
index c0e09368..5812f23e 100644
--- a/internal/sftpd/sftpd_test.go
+++ b/internal/sftpd/sftpd_test.go
@@ -438,6 +438,12 @@ func TestInitialization(t *testing.T) {
assert.Contains(t, err.Error(), "unsupported key-exchange algorithm")
}
sftpdConf.KexAlgorithms = nil
+ sftpdConf.PublicKeyAlgorithms = []string{"not a pub key algo"}
+ err = sftpdConf.Initialize(configDir)
+ if assert.Error(t, err) {
+ assert.Contains(t, err.Error(), "unsupported public key authentication algorithm")
+ }
+ sftpdConf.PublicKeyAlgorithms = nil
sftpdConf.HostKeyAlgorithms = []string{"not a host key algo"}
err = sftpdConf.Initialize(configDir)
if assert.Error(t, err) {
@@ -581,6 +587,7 @@ func TestBasicSFTPHandling(t *testing.T) {
assert.NotEmpty(t, status.GetMACsAsString())
assert.NotEmpty(t, status.GetKEXsAsString())
assert.NotEmpty(t, status.GetCiphersAsString())
+ assert.NotEmpty(t, status.GetPublicKeysAlgosAsString())
}
func TestBasicSFTPFsHandling(t *testing.T) {
diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml
index 0ed89dd2..2f87c60b 100644
--- a/openapi/openapi.yaml
+++ b/openapi/openapi.yaml
@@ -6505,6 +6505,10 @@ components:
type: array
items:
$ref: '#/components/schemas/SSHAuthentications'
+ public_key_algorithms:
+ type: array
+ items:
+ type: string
macs:
type: array
items:
diff --git a/sftpgo.json b/sftpgo.json
index 8d722e52..21d59c37 100644
--- a/sftpgo.json
+++ b/sftpgo.json
@@ -91,6 +91,7 @@
"kex_algorithms": [],
"ciphers": [],
"macs": [],
+ "public_key_algorithms": [],
"trusted_user_ca_keys": [],
"revoked_user_certs_file": "",
"login_banner_file": "",
diff --git a/templates/webadmin/configs.html b/templates/webadmin/configs.html
index c66b14e7..9051ad65 100644
--- a/templates/webadmin/configs.html
+++ b/templates/webadmin/configs.html
@@ -65,6 +65,17 @@ along with this program. If not, see