From b51d795e049e15df03495644d1b0f7f51206583d Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Mon, 19 Oct 2020 14:30:40 +0200 Subject: [PATCH] sftpd: auto generate an ed25519 host key too --- docs/full-configuration.md | 11 ++++--- sftpd/internal_test.go | 9 +++++- sftpd/server.go | 63 +++++++++++++++++++++++++------------- utils/utils.go | 45 +++++++++++++++++++++++---- 4 files changed, 95 insertions(+), 33 deletions(-) diff --git a/docs/full-configuration.md b/docs/full-configuration.md index 0158235e..021e81d5 100644 --- a/docs/full-configuration.md +++ b/docs/full-configuration.md @@ -35,7 +35,7 @@ The `serve` command supports the following flags: Log file can be rotated on demand sending a `SIGUSR1` signal on Unix based systems and using the command `sftpgo service rotatelogs` on Windows. -If you don't configure any private host key, the daemon will use `id_rsa` and `id_ecdsa` in the configuration directory. If these files don't exist, the daemon will attempt to autogenerate them (if the user that executes SFTPGo has write access to the `config-dir`). The server supports any private key format supported by [`crypto/ssh`](https://github.com/golang/crypto/blob/master/ssh/keys.go#L33). +If you don't configure any private host key, the daemon will use `id_rsa`, `id_ecdsa` and `id_ed25519` in the configuration directory. If these files don't exist, the daemon will attempt to autogenerate them (if the user that executes SFTPGo has write access to the `config-dir`). The server supports any private key format supported by [`crypto/ssh`](https://github.com/golang/crypto/blob/master/ssh/keys.go#L33). The `gen` command allows to generate completion scripts for your shell and man pages. Currently the man pages visual representation is wrong, take a look at this upstream [bug](https://github.com/spf13/cobra/issues/1049) for more details. @@ -68,7 +68,7 @@ The configuration file contains the following sections: - `actions`, struct. Deprecated, please use the same key in `common` section. - `keys`, struct array. Deprecated, please use `host_keys`. - `private_key`, path to the private key file. It can be a path relative to the config dir or an absolute one. - - `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` and `id_ecdsa` keys inside the configuration directory. If you configure absolute paths to files named `id_rsa` and/or `id_ecdsa` then SFTPGo will try to generate these keys using the default settings. + - `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. - `kex_algorithms`, list of strings. Available KEX (Key Exchange) algorithms in preference order. Leave empty to use default values. The supported values can be found here: [`crypto/ssh`](https://github.com/golang/crypto/blob/master/ssh/common.go#L46 "Supported kex algos") - `ciphers`, list of strings. Allowed ciphers. Leave empty to use default values. The supported values can be found here: [crypto/ssh](https://github.com/golang/crypto/blob/master/ssh/common.go#L28 "Supported ciphers") - `macs`, list of strings. Available MAC (message authentication code) algorithms in preference order. Leave empty to use default values. The supported values can be found here: [crypto/ssh](https://github.com/golang/crypto/blob/master/ssh/common.go#L84 "Supported MACs") @@ -172,16 +172,17 @@ If you want to use a private host key that uses an algorithm/setting different f where `id_rsa`, `id_ecdsa` and `id_ed25519`, in this example, are files containing your generated keys. You can use absolute paths or paths relative to the configuration directory. -If you want the default host keys generation in a directory different from the config dir, please specify absolute paths to files named `id_rsa` or `id_ecdsa` like this: +If you want the default host keys generation in a directory different from the config dir, please specify absolute paths to files named `id_rsa`, `id_ecdsa` or `id_ed25519` like this: ```json "host_keys": [ "/etc/sftpgo/keys/id_rsa", - "/etc/sftpgo/keys/id_ecdsa" + "/etc/sftpgo/keys/id_ecdsa", + "/etc/sftpgo/keys/id_ed25519" ] ``` -then SFTPGo will try to create `id_rsa` and `id_ecdsa`, if they are missing, inside the existing directory `/etc/sftpgo/keys`. +then SFTPGo will try to create `id_rsa`, `id_ecdsa` and `id_ed25519`, if they are missing, inside the existing directory `/etc/sftpgo/keys`. The configuration can be read from JSON, TOML, YAML, HCL, envfile and Java properties config files. If your `config-file` flag is set to `sftpgo` (default value), you need to create a configuration file called `sftpgo.json` or `sftpgo.yaml` and so on inside `config-dir`. diff --git a/sftpd/internal_test.go b/sftpd/internal_test.go index 39bbe8c9..57e02fb9 100644 --- a/sftpd/internal_test.go +++ b/sftpd/internal_test.go @@ -1741,17 +1741,21 @@ func TestLoadHostKeys(t *testing.T) { assert.NoError(t, err) rsaKeyName := filepath.Join(keysDir, defaultPrivateRSAKeyName) ecdsaKeyName := filepath.Join(keysDir, defaultPrivateECDSAKeyName) + ed25519KeyName := filepath.Join(keysDir, defaultPrivateEd25519KeyName) nonDefaultKeyName := filepath.Join(keysDir, "akey") - c.HostKeys = []string{nonDefaultKeyName, rsaKeyName, ecdsaKeyName} + c.HostKeys = []string{nonDefaultKeyName, rsaKeyName, ecdsaKeyName, ed25519KeyName} err = c.checkAndLoadHostKeys(configDir, serverConfig) assert.Error(t, err) assert.FileExists(t, rsaKeyName) assert.FileExists(t, ecdsaKeyName) + assert.FileExists(t, ed25519KeyName) assert.NoFileExists(t, nonDefaultKeyName) err = os.Remove(rsaKeyName) assert.NoError(t, err) err = os.Remove(ecdsaKeyName) assert.NoError(t, err) + err = os.Remove(ed25519KeyName) + assert.NoError(t, err) if runtime.GOOS != osWindows { err = os.Chmod(keysDir, 0551) assert.NoError(t, err) @@ -1764,6 +1768,9 @@ func TestLoadHostKeys(t *testing.T) { c.HostKeys = []string{ecdsaKeyName, rsaKeyName} err = c.checkAndLoadHostKeys(configDir, serverConfig) assert.Error(t, err) + c.HostKeys = []string{ed25519KeyName} + err = c.checkAndLoadHostKeys(configDir, serverConfig) + assert.Error(t, err) err = os.Chmod(keysDir, 0755) assert.NoError(t, err) } diff --git a/sftpd/server.go b/sftpd/server.go index 9891d322..7bf22db1 100644 --- a/sftpd/server.go +++ b/sftpd/server.go @@ -25,9 +25,10 @@ import ( ) const ( - defaultPrivateRSAKeyName = "id_rsa" - defaultPrivateECDSAKeyName = "id_ecdsa" - sourceAddressCriticalOption = "source-address" + defaultPrivateRSAKeyName = "id_rsa" + defaultPrivateECDSAKeyName = "id_ecdsa" + defaultPrivateEd25519KeyName = "id_ed25519" + sourceAddressCriticalOption = "source-address" ) var ( @@ -471,6 +472,33 @@ func (c *Configuration) checkSSHCommands() { c.EnabledSSHCommands = sshCommands } +func (c *Configuration) generateDefaultHostKeys(configDir string) error { + var err error + defaultHostKeys := []string{defaultPrivateRSAKeyName, defaultPrivateECDSAKeyName, defaultPrivateEd25519KeyName} + for _, k := range defaultHostKeys { + autoFile := filepath.Join(configDir, k) + if _, err = os.Stat(autoFile); os.IsNotExist(err) { + logger.Info(logSender, "", "No host keys configured and %#v does not exist; try to create a new host key", autoFile) + logger.InfoToConsole("No host keys configured and %#v does not exist; try to create a new host key", autoFile) + if k == defaultPrivateRSAKeyName { + err = utils.GenerateRSAKeys(autoFile) + } else if k == defaultPrivateECDSAKeyName { + err = utils.GenerateECDSAKeys(autoFile) + } else { + err = utils.GenerateEd25519Keys(autoFile) + } + if err != nil { + logger.Warn(logSender, "", "error creating host key %#v: %v", autoFile, err) + logger.WarnToConsole("error creating host key %#v: %v", autoFile, err) + return err + } + } + c.HostKeys = append(c.HostKeys, k) + } + + return err +} + func (c *Configuration) checkHostKeyAutoGeneration(configDir string) error { for _, k := range c.HostKeys { if filepath.IsAbs(k) { @@ -495,6 +523,15 @@ func (c *Configuration) checkHostKeyAutoGeneration(configDir string) error { logger.WarnToConsole("error creating host key %#v: %v", k, err) return err } + case defaultPrivateEd25519KeyName: + logger.Info(logSender, "", "try to create non-existent host key %#v", k) + logger.InfoToConsole("try to create non-existent host key %#v", k) + err = utils.GenerateEd25519Keys(k) + if err != nil { + logger.Warn(logSender, "", "error creating host key %#v: %v", k, err) + logger.WarnToConsole("error creating host key %#v: %v", k, err) + return err + } default: logger.Warn(logSender, "", "non-existent host key %#v will not be created", k) logger.WarnToConsole("non-existent host key %#v will not be created", k) @@ -503,24 +540,8 @@ func (c *Configuration) checkHostKeyAutoGeneration(configDir string) error { } } if len(c.HostKeys) == 0 { - defaultKeys := []string{defaultPrivateRSAKeyName, defaultPrivateECDSAKeyName} - for _, k := range defaultKeys { - autoFile := filepath.Join(configDir, k) - if _, err := os.Stat(autoFile); os.IsNotExist(err) { - logger.Info(logSender, "", "No host keys configured and %#v does not exist; try to create a new host key", autoFile) - logger.InfoToConsole("No host keys configured and %#v does not exist; try to create a new host key", autoFile) - if k == defaultPrivateRSAKeyName { - err = utils.GenerateRSAKeys(autoFile) - } else { - err = utils.GenerateECDSAKeys(autoFile) - } - if err != nil { - logger.Warn(logSender, "", "error creating host key %#v: %v", autoFile, err) - logger.WarnToConsole("error creating host key %#v: %v", autoFile, err) - return err - } - } - c.HostKeys = append(c.HostKeys, k) + if err := c.generateDefaultHostKeys(configDir); err != nil { + return err } } return nil diff --git a/utils/utils.go b/utils/utils.go index cf0e19c5..1a1cbb36 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -5,6 +5,7 @@ import ( "crypto/aes" "crypto/cipher" "crypto/ecdsa" + "crypto/ed25519" "crypto/elliptic" "crypto/rand" "crypto/rsa" @@ -230,12 +231,6 @@ func GenerateECDSAKeys(file string) error { return err } - o, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) - if err != nil { - return err - } - defer o.Close() - keyBytes, err := x509.MarshalECPrivateKey(key) if err != nil { return err @@ -245,6 +240,12 @@ func GenerateECDSAKeys(file string) error { Bytes: keyBytes, } + o, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer o.Close() + if err := pem.Encode(o, priv); err != nil { return err } @@ -256,6 +257,38 @@ func GenerateECDSAKeys(file string) error { return ioutil.WriteFile(file+".pub", ssh.MarshalAuthorizedKey(pub), 0600) } +// GenerateEd25519Keys generate ed25519 private and public keys and write the +// private key to specified file and the public key to the specified +// file adding the .pub suffix +func GenerateEd25519Keys(file string) error { + pubKey, privKey, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return err + } + keyBytes, err := x509.MarshalPKCS8PrivateKey(privKey) + if err != nil { + return err + } + priv := &pem.Block{ + Type: "PRIVATE KEY", + Bytes: keyBytes, + } + o, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer o.Close() + + if err := pem.Encode(o, priv); err != nil { + return err + } + pub, err := ssh.NewPublicKey(pubKey) + if err != nil { + return err + } + return ioutil.WriteFile(file+".pub", ssh.MarshalAuthorizedKey(pub), 0600) +} + // GetDirsForSFTPPath returns all the directory for the given path in reverse order // for example if the path is: /1/2/3/4 it returns: // [ "/1/2/3/4", "/1/2/3", "/1/2", "/1", "/" ]