sshd: add support for host key certificates

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2022-04-01 08:03:56 +02:00
parent a7b159aebb
commit 55f8171dd1
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
5 changed files with 91 additions and 2 deletions

View file

@ -195,6 +195,7 @@ func Init() {
MaxAuthTries: 0,
Banner: defaultSFTPDBanner,
HostKeys: []string{},
HostCertificates: []string{},
KexAlgorithms: []string{},
Ciphers: []string{},
MACs: []string{},
@ -1539,6 +1540,7 @@ func setViperDefaults() {
viper.SetDefault("sftpd.max_auth_tries", globalConf.SFTPD.MaxAuthTries)
viper.SetDefault("sftpd.banner", globalConf.SFTPD.Banner)
viper.SetDefault("sftpd.host_keys", globalConf.SFTPD.HostKeys)
viper.SetDefault("sftpd.host_certificates", globalConf.SFTPD.HostCertificates)
viper.SetDefault("sftpd.kex_algorithms", globalConf.SFTPD.KexAlgorithms)
viper.SetDefault("sftpd.ciphers", globalConf.SFTPD.Ciphers)
viper.SetDefault("sftpd.macs", globalConf.SFTPD.MACs)

View file

@ -108,6 +108,7 @@ The configuration file contains the following sections:
- `max_auth_tries` integer. Maximum number of authentication attempts permitted per connection. If set to a negative number, the number of attempts is unlimited. If set to zero, the number of attempts is limited to 6.
- `banner`, string. Identification string used by the server. Leave empty to use the default banner. Default `SFTPGo_<version>`, for example `SSH-2.0-SFTPGo_0.9.5`
- `host_keys`, list of strings. It contains the daemon's private host keys. Each host key can be defined as a path relative to the configuration directory or an absolute one. If empty, the daemon will search or try to generate `id_rsa`, `id_ecdsa` and `id_ed25519` keys inside the configuration directory. If you configure absolute paths to files named `id_rsa`, `id_ecdsa` and/or `id_ed25519` then SFTPGo will try to generate these keys using the default settings.
- `host_certificates`, list of strings. Public host certificates. Each certificate can be defined as a path relative to the configuration directory or an absolute one. Certificate's public key must match a private host key otherwise it will be silently ignored. Default: empty.
- `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-group18-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.
- `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`.

View file

@ -102,6 +102,10 @@ type Configuration struct {
// If empty or missing, the daemon will search or try to generate "id_rsa" and "id_ecdsa" host keys
// inside the configuration directory.
HostKeys []string `json:"host_keys" mapstructure:"host_keys"`
// HostCertificates defines public host certificates.
// Each certificate can be defined as a path relative to the configuration directory or an absolute one.
// Certificate's public key must match a private host key otherwise it will be silently ignored.
HostCertificates []string `json:"host_certificates" mapstructure:"host_certificates"`
// KexAlgorithms specifies the available KEX (Key Exchange) algorithms in
// preference order.
KexAlgorithms []string `json:"kex_algorithms" mapstructure:"kex_algorithms"`
@ -790,6 +794,10 @@ func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh
if err := c.checkHostKeyAutoGeneration(configDir); err != nil {
return err
}
hostCertificates, err := c.loadHostCertificates(configDir)
if err != nil {
return err
}
serviceStatus.HostKeys = nil
for _, hostKey := range c.HostKeys {
if !util.IsFileInputValid(hostKey) {
@ -821,6 +829,14 @@ func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh
// Add private key to the server configuration.
serverConfig.AddHostKey(private)
for _, cert := range hostCertificates {
signer, err := ssh.NewCertSigner(cert, private)
if err == nil {
serverConfig.AddHostKey(signer)
logger.Info(logSender, "", "Host certificate loaded for host key %#v, fingerprint %#v",
hostKey, ssh.FingerprintSHA256(signer.PublicKey()))
}
}
}
var fp []string
for idx := range serviceStatus.HostKeys {
@ -831,11 +847,42 @@ func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh
return nil
}
func (c *Configuration) loadHostCertificates(configDir string) ([]*ssh.Certificate, error) {
var certs []*ssh.Certificate
for _, certPath := range c.HostCertificates {
if !util.IsFileInputValid(certPath) {
logger.Warn(logSender, "", "unable to load invalid host certificate %#v", certPath)
logger.WarnToConsole("unable to load invalid host certificate %#v", certPath)
continue
}
if !filepath.IsAbs(certPath) {
certPath = filepath.Join(configDir, certPath)
}
certBytes, err := os.ReadFile(certPath)
if err != nil {
return certs, fmt.Errorf("unable to load host certificate %#v: %w", certPath, err)
}
parsed, _, _, _, err := ssh.ParseAuthorizedKey(certBytes)
if err != nil {
return nil, fmt.Errorf("unable to parse host certificate %#v: %w", certPath, err)
}
cert, ok := parsed.(*ssh.Certificate)
if !ok {
return nil, fmt.Errorf("the file %#v is not an SSH certificate", certPath)
}
if cert.CertType != ssh.HostCert {
return nil, fmt.Errorf("the file %#v is not an host certificate", certPath)
}
certs = append(certs, cert)
}
return certs, nil
}
func (c *Configuration) initializeCertChecker(configDir string) error {
for _, keyPath := range c.TrustedUserCAKeys {
if !util.IsFileInputValid(keyPath) {
logger.Warn(logSender, "", "unable to load invalid trusted user CA key: %#v", keyPath)
logger.WarnToConsole("unable to load invalid trusted user CA key: %#v", keyPath)
logger.Warn(logSender, "", "unable to load invalid trusted user CA key %#v", keyPath)
logger.WarnToConsole("unable to load invalid trusted user CA key %#v", keyPath)
continue
}
if !filepath.IsAbs(keyPath) {

View file

@ -396,6 +396,44 @@ func TestInitialization(t *testing.T) {
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "unsupported key-exchange algorithm")
}
sftpdConf.HostCertificates = []string{"missing file"}
err = sftpdConf.Initialize(configDir)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "unable to load host certificate")
}
sftpdConf.HostCertificates = []string{"."}
err = sftpdConf.Initialize(configDir)
assert.Error(t, err)
hostCertPath := filepath.Join(os.TempDir(), "host_cert.pub")
err = os.WriteFile(hostCertPath, []byte(testCertValid), 0600)
assert.NoError(t, err)
sftpdConf.HostKeys = []string{privateKeyPath}
sftpdConf.HostCertificates = []string{hostCertPath}
err = sftpdConf.Initialize(configDir)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "is not an host certificate")
}
err = os.WriteFile(hostCertPath, []byte(testPubKey), 0600)
assert.NoError(t, err)
err = sftpdConf.Initialize(configDir)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "is not an SSH certificate")
}
err = os.WriteFile(hostCertPath, []byte("abc"), 0600)
assert.NoError(t, err)
err = sftpdConf.Initialize(configDir)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "unable to parse host certificate")
}
err = os.WriteFile(hostCertPath, []byte(testHostCert), 0600)
assert.NoError(t, err)
err = sftpdConf.Initialize(configDir)
assert.Error(t, err)
err = os.Remove(hostCertPath)
assert.NoError(t, err)
sftpdConf.HostKeys = nil
sftpdConf.HostCertificates = nil
sftpdConf.RevokedUserCertsFile = "."
err = sftpdConf.Initialize(configDir)
assert.Error(t, err)

View file

@ -63,6 +63,7 @@
"max_auth_tries": 0,
"banner": "",
"host_keys": [],
"host_certificates": [],
"kex_algorithms": [],
"ciphers": [],
"macs": [],