From c5c586001206abc3bac0b0a0cd03b7dd08de2506 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Thu, 9 Nov 2023 20:03:04 +0100 Subject: [PATCH] ssh: allow to configure public key auth algorithms Signed-off-by: Nicola Murino --- docs/full-configuration.md | 1 + go.mod | 2 +- go.sum | 4 +- internal/config/config.go | 2 + internal/dataprovider/configs.go | 42 +++++++++++++++------ internal/httpd/httpd_test.go | 13 ++++++- internal/httpd/webadmin.go | 11 +++--- internal/sftpd/internal_test.go | 14 ++++--- internal/sftpd/server.go | 65 +++++++++++++++++++++++++++----- internal/sftpd/sftpd.go | 23 +++++++---- internal/sftpd/sftpd_test.go | 7 ++++ openapi/openapi.yaml | 4 ++ sftpgo.json | 1 + templates/webadmin/configs.html | 11 ++++++ templates/webadmin/status.html | 6 ++- 15 files changed, 160 insertions(+), 46 deletions(-) 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 . +
+ +
+ +
+
+
diff --git a/templates/webadmin/status.html b/templates/webadmin/status.html index 244a0d60..c1438247 100644 --- a/templates/webadmin/status.html +++ b/templates/webadmin/status.html @@ -50,9 +50,11 @@ along with this program. If not, see .
{{end}}
- MAC algorithms: "{{.Status.SSH.GetMACsAsString}}" + Public key authentication algorithms: "{{.Status.SSH.GetPublicKeysAlgosAsString}}"

- KEX algorithms: "{{.Status.SSH.GetKEXsAsString}}" + Message authentication algorithms: "{{.Status.SSH.GetMACsAsString}}" +

+ Key exchange algorithms: "{{.Status.SSH.GetKEXsAsString}}"

Ciphers: "{{.Status.SSH.GetCiphersAsString}}"