sftpd: add support for SSH user certificate authentication
This add support for PROTOCOL.certkeys vendor extension: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?rev=1.8 Fixes #117 Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
82fb7f8cf0
commit
738c7ab43e
12 changed files with 346 additions and 56 deletions
|
@ -10,6 +10,7 @@ Fully featured and highly configurable SFTP server, written in Go
|
|||
- SFTP accounts are virtual accounts stored in a "data provider".
|
||||
- SQLite, MySQL, PostgreSQL, bbolt (key/value store in pure Go) and in-memory data providers are supported.
|
||||
- Public key and password authentication. Multiple public keys per user are supported.
|
||||
- SSH user [certificate authentication](https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?rev=1.8).
|
||||
- Keyboard interactive authentication. You can easily setup a customizable multi-factor authentication.
|
||||
- Partial authentication. You can configure multi-step authentication requiring, for example, the user password after successful public key authentication.
|
||||
- Per user authentication methods. You can, for example, deny one or more authentication methods to one or more users.
|
||||
|
|
|
@ -61,6 +61,7 @@ func init() {
|
|||
KexAlgorithms: []string{},
|
||||
Ciphers: []string{},
|
||||
MACs: []string{},
|
||||
TrustedUserCAKeys: []string{},
|
||||
LoginBannerFile: "",
|
||||
EnabledSSHCommands: sftpd.GetDefaultSSHCommands(),
|
||||
KeyboardInteractiveHook: "",
|
||||
|
|
|
@ -956,8 +956,13 @@ func checkUserAndPubKey(user User, pubKey []byte) (User, string, error) {
|
|||
return user, "", err
|
||||
}
|
||||
if bytes.Equal(storedPubKey.Marshal(), pubKey) {
|
||||
fp := ssh.FingerprintSHA256(storedPubKey)
|
||||
return user, fp + ":" + comment, nil
|
||||
certInfo := ""
|
||||
cert, ok := storedPubKey.(*ssh.Certificate)
|
||||
if ok {
|
||||
certInfo = fmt.Sprintf(" %v ID: %v Serial: %v CA: %v", cert.Type(), cert.KeyId, cert.Serial,
|
||||
ssh.FingerprintSHA256(cert.SignatureKey))
|
||||
}
|
||||
return user, fmt.Sprintf("%v:%v%v", ssh.FingerprintSHA256(storedPubKey), comment, certInfo), nil
|
||||
}
|
||||
}
|
||||
return user, "", errors.New("Invalid credentials")
|
||||
|
|
|
@ -54,7 +54,8 @@ The configuration file contains the following sections:
|
|||
- `private_key`, path to the private key file. It can be a path relative to the config dir or an absolute one.
|
||||
- `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")
|
||||
- `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")
|
||||
- `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.
|
||||
- `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.
|
||||
- `setstat_mode`, integer. 0 means "normal mode": requests for changing permissions, owner/group and access/modification times are executed. 1 means "ignore mode": requests for changing permissions, owner/group and access/modification times are silently ignored.
|
||||
- `enabled_ssh_commands`, list of enabled SSH commands. These SSH commands are enabled by default: `md5sum`, `sha1sum`, `cd`, `pwd`, `scp`. `*` enables all supported commands. Some commands are implemented directly inside SFTPGo, while for other commands we use system commands that need to be installed and in your system's `PATH`. For system commands we have no direct control on file creation/deletion and so we cannot support virtual folders, cloud storage filesystem, such as S3, and quota check is suboptimal: if quota is enabled, the number of files is checked at the command start and not while new files are created. The allowed size is calculated as the difference between the max quota and the used one, and it is checked against the bytes transferred via SSH. The command is aborted if it uploads more bytes than the remaining allowed size calculated at the command start. Anyway, we see the bytes that the remote command sends to the local command via SSH. These bytes contain both protocol commands and files, and so the size of the files is different from the size trasferred via SSH: for example, a command can send compressed files, or a protocol command (few bytes) could delete a big file. To mitigate this issue, quotas are recalculated at the command end with a full home directory scan. This could be heavy for big directories. If you need system commands and quotas you could consider disabling quota restrictions and periodically update quota usage yourself using the REST API. We support the following SSH commands:
|
||||
|
|
|
@ -1110,13 +1110,13 @@ components:
|
|||
password:
|
||||
type: string
|
||||
nullable: true
|
||||
description: password or public key are mandatory. If the password has no known hashing algo prefix it will be stored using argon2id. You can send a password hashed as bcrypt or pbkdf2 and it will be stored as is. For security reasons this field is omitted when you search/get users
|
||||
description: password or public key/SSH user certificate are mandatory. If the password has no known hashing algo prefix it will be stored using argon2id. You can send a password hashed as bcrypt or pbkdf2 and it will be stored as is. For security reasons this field is omitted when you search/get users
|
||||
public_keys:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
description: a password or at least one public key are mandatory.
|
||||
description: a password or at least one public key/SSH user certificate are mandatory.
|
||||
home_dir:
|
||||
type: string
|
||||
description: path to the user home directory. The user cannot upload or download files outside this directory. SFTPGo tries to automatically create this folder if missing. Must be an absolute path
|
||||
|
|
|
@ -496,7 +496,8 @@ def getDatetimeAsMillisSinceEpoch(dt):
|
|||
def addCommonUserArguments(parser):
|
||||
parser.add_argument('username', type=str)
|
||||
parser.add_argument('-P', '--password', type=str, default=None, help='Default: %(default)s')
|
||||
parser.add_argument('-K', '--public-keys', type=str, nargs='+', default=[], help='Default: %(default)s')
|
||||
parser.add_argument('-K', '--public-keys', type=str, nargs='+', default=[], help='Public keys or SSH user certificates. ' +
|
||||
'Default: %(default)s')
|
||||
parser.add_argument('-H', '--home-dir', type=str, default='', help='Default: %(default)s')
|
||||
parser.add_argument('--uid', type=int, default=0, help='Default: %(default)s')
|
||||
parser.add_argument('--gid', type=int, default=0, help='Default: %(default)s')
|
||||
|
|
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/eikenb/pipeat"
|
||||
"github.com/pkg/sftp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
|
@ -1734,3 +1735,41 @@ func TestProxyProtocolVersion(t *testing.T) {
|
|||
_, err = c.getProxyListener(nil)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestLoadHostKeys(t *testing.T) {
|
||||
c := Configuration{}
|
||||
c.Keys = []Key{
|
||||
{
|
||||
PrivateKey: "missing file",
|
||||
},
|
||||
}
|
||||
err := c.checkAndLoadHostKeys("..", &ssh.ServerConfig{})
|
||||
assert.Error(t, err)
|
||||
testfile := filepath.Join(os.TempDir(), "invalidkey")
|
||||
err = ioutil.WriteFile(testfile, []byte("some bytes"), 0666)
|
||||
assert.NoError(t, err)
|
||||
c.Keys = []Key{
|
||||
{
|
||||
PrivateKey: testfile,
|
||||
},
|
||||
}
|
||||
err = c.checkAndLoadHostKeys("..", &ssh.ServerConfig{})
|
||||
assert.Error(t, err)
|
||||
err = os.Remove(testfile)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCertCheckerInitErrors(t *testing.T) {
|
||||
c := Configuration{}
|
||||
c.TrustedUserCAKeys = append(c.TrustedUserCAKeys, "missing file")
|
||||
err := c.initializeCertChecker("")
|
||||
assert.Error(t, err)
|
||||
testfile := filepath.Join(os.TempDir(), "invalidkey")
|
||||
err = ioutil.WriteFile(testfile, []byte("some bytes"), 0666)
|
||||
assert.NoError(t, err)
|
||||
c.TrustedUserCAKeys = []string{testfile}
|
||||
err = c.initializeCertChecker("")
|
||||
assert.Error(t, err)
|
||||
err = os.Remove(testfile)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
|
128
sftpd/server.go
128
sftpd/server.go
|
@ -1,6 +1,7 @@
|
|||
package sftpd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
@ -24,8 +25,9 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
defaultPrivateRSAKeyName = "id_rsa"
|
||||
defaultPrivateECDSAKeyName = "id_ecdsa"
|
||||
defaultPrivateRSAKeyName = "id_rsa"
|
||||
defaultPrivateECDSAKeyName = "id_ecdsa"
|
||||
sourceAddressCriticalOption = "source-address"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -71,6 +73,10 @@ type Configuration struct {
|
|||
// MACs Specifies the available MAC (message authentication code) algorithms
|
||||
// in preference order
|
||||
MACs []string `json:"macs" mapstructure:"macs"`
|
||||
// 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
|
||||
TrustedUserCAKeys []string `json:"trusted_user_ca_keys" mapstructure:"trusted_user_ca_keys"`
|
||||
// LoginBannerFile the contents of the specified file, if any, are sent to
|
||||
// the remote user before authentication is allowed.
|
||||
LoginBannerFile string `json:"login_banner_file" mapstructure:"login_banner_file"`
|
||||
|
@ -119,12 +125,14 @@ type Configuration struct {
|
|||
// connection will be accepted and the header will be ignored.
|
||||
// If proxy protocol is set to 2 and we receive a proxy header from an IP that is not in the list then the
|
||||
// connection will be rejected.
|
||||
ProxyAllowed []string `json:"proxy_allowed" mapstructure:"proxy_allowed"`
|
||||
ProxyAllowed []string `json:"proxy_allowed" mapstructure:"proxy_allowed"`
|
||||
certChecker *ssh.CertChecker
|
||||
parsedUserCAKeys []ssh.PublicKey
|
||||
}
|
||||
|
||||
// Key contains information about host keys
|
||||
type Key struct {
|
||||
// The private key path relative to the configuration directory or absolute
|
||||
// The private key path as absolute path or relative to the configuration directory
|
||||
PrivateKey string `json:"private_key" mapstructure:"private_key"`
|
||||
}
|
||||
|
||||
|
@ -157,7 +165,7 @@ func (c Configuration) Initialize(configDir string) error {
|
|||
return sp, nil
|
||||
},
|
||||
PublicKeyCallback: func(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
|
||||
sp, err := c.validatePublicKeyCredentials(conn, pubKey.Marshal())
|
||||
sp, err := c.validatePublicKeyCredentials(conn, pubKey)
|
||||
if err == ssh.ErrPartialSuccess {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -178,8 +186,11 @@ func (c Configuration) Initialize(configDir string) error {
|
|||
ServerVersion: fmt.Sprintf("SSH-2.0-%v", c.Banner),
|
||||
}
|
||||
|
||||
err = c.checkAndLoadHostKeys(configDir, serverConfig)
|
||||
if err != nil {
|
||||
if err = c.checkAndLoadHostKeys(configDir, serverConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = c.initializeCertChecker(configDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -336,9 +347,9 @@ func (c Configuration) AcceptInboundConnection(conn net.Conn, config *ssh.Server
|
|||
var user dataprovider.User
|
||||
|
||||
// Unmarshal cannot fails here and even if it fails we'll have a user with no permissions
|
||||
json.Unmarshal([]byte(sconn.Permissions.Extensions["user"]), &user) //nolint:errcheck
|
||||
json.Unmarshal([]byte(sconn.Permissions.Extensions["sftpgo_user"]), &user) //nolint:errcheck
|
||||
|
||||
loginType := sconn.Permissions.Extensions["login_method"]
|
||||
loginType := sconn.Permissions.Extensions["sftpgo_login_method"]
|
||||
connectionID := hex.EncodeToString(sconn.SessionID())
|
||||
|
||||
fs, err := user.GetFilesystem(connectionID)
|
||||
|
@ -474,8 +485,8 @@ func loginUser(user dataprovider.User, loginMethod, publicKey string, conn ssh.C
|
|||
}
|
||||
p := &ssh.Permissions{}
|
||||
p.Extensions = make(map[string]string)
|
||||
p.Extensions["user"] = string(json)
|
||||
p.Extensions["login_method"] = loginMethod
|
||||
p.Extensions["sftpgo_user"] = string(json)
|
||||
p.Extensions["sftpgo_login_method"] = loginMethod
|
||||
return p, nil
|
||||
}
|
||||
|
||||
|
@ -540,26 +551,93 @@ func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c Configuration) validatePublicKeyCredentials(conn ssh.ConnMetadata, pubKey []byte) (*ssh.Permissions, error) {
|
||||
func (c *Configuration) initializeCertChecker(configDir string) error {
|
||||
for _, keyPath := range c.TrustedUserCAKeys {
|
||||
if !filepath.IsAbs(keyPath) {
|
||||
keyPath = filepath.Join(configDir, keyPath)
|
||||
}
|
||||
keyBytes, err := ioutil.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "", "error loading trusted user CA key %#v: %v", keyPath, err)
|
||||
logger.WarnToConsole("error loading trusted user CA key %#v: %v", keyPath, err)
|
||||
return err
|
||||
}
|
||||
parsedKey, _, _, _, err := ssh.ParseAuthorizedKey(keyBytes)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "", "error parsing trusted user CA key %#v: %v", keyPath, err)
|
||||
logger.WarnToConsole("error parsing trusted user CA key %#v: %v", keyPath, err)
|
||||
return err
|
||||
}
|
||||
c.parsedUserCAKeys = append(c.parsedUserCAKeys, parsedKey)
|
||||
}
|
||||
c.certChecker = &ssh.CertChecker{
|
||||
SupportedCriticalOptions: []string{
|
||||
sourceAddressCriticalOption,
|
||||
},
|
||||
IsUserAuthority: func(k ssh.PublicKey) bool {
|
||||
for _, key := range c.parsedUserCAKeys {
|
||||
if bytes.Equal(k.Marshal(), key.Marshal()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Configuration) validatePublicKeyCredentials(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
|
||||
var err error
|
||||
var user dataprovider.User
|
||||
var keyID string
|
||||
var sshPerm *ssh.Permissions
|
||||
var certPerm *ssh.Permissions
|
||||
|
||||
connectionID := hex.EncodeToString(conn.SessionID())
|
||||
method := dataprovider.SSHLoginMethodPublicKey
|
||||
if user, keyID, err = dataprovider.CheckUserAndPubKey(dataProvider, conn.User(), pubKey); err == nil {
|
||||
cert, ok := pubKey.(*ssh.Certificate)
|
||||
if ok {
|
||||
if cert.CertType != ssh.UserCert {
|
||||
err = fmt.Errorf("ssh: cert has type %d", cert.CertType)
|
||||
updateLoginMetrics(conn, method, err)
|
||||
return nil, err
|
||||
}
|
||||
if !c.certChecker.IsUserAuthority(cert.SignatureKey) {
|
||||
err = fmt.Errorf("ssh: certificate signed by unrecognized authority")
|
||||
updateLoginMetrics(conn, method, err)
|
||||
return nil, err
|
||||
}
|
||||
if err := c.certChecker.CheckCert(conn.User(), cert); err != nil {
|
||||
updateLoginMetrics(conn, method, err)
|
||||
return nil, err
|
||||
}
|
||||
// we need to check source address ourself since crypto/ssh will skip this check if we return partial success
|
||||
if cert.Permissions.CriticalOptions != nil && cert.Permissions.CriticalOptions[sourceAddressCriticalOption] != "" {
|
||||
if err := utils.CheckSourceAddress(conn.RemoteAddr(), cert.Permissions.CriticalOptions[sourceAddressCriticalOption]); err != nil {
|
||||
updateLoginMetrics(conn, method, err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
certPerm = &cert.Permissions
|
||||
}
|
||||
if user, keyID, err = dataprovider.CheckUserAndPubKey(dataProvider, conn.User(), pubKey.Marshal()); err == nil {
|
||||
if user.IsPartialAuth(method) {
|
||||
logger.Debug(logSender, connectionID, "user %#v authenticated with partial success", conn.User())
|
||||
return nil, ssh.ErrPartialSuccess
|
||||
}
|
||||
sshPerm, err = loginUser(user, method, keyID, conn)
|
||||
if err == nil && certPerm != nil {
|
||||
// if we have a SSH user cert we need to merge certificate permissions with our ones
|
||||
// we only set Extensions, so CriticalOptions are always the ones from the certificate
|
||||
sshPerm.CriticalOptions = certPerm.CriticalOptions
|
||||
if certPerm.Extensions != nil {
|
||||
for k, v := range certPerm.Extensions {
|
||||
sshPerm.Extensions[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
metrics.AddLoginAttempt(method)
|
||||
if err != nil {
|
||||
logger.ConnectionFailedLog(conn.User(), utils.GetIPFromRemoteAddress(conn.RemoteAddr().String()), method, err.Error())
|
||||
}
|
||||
metrics.AddLoginResult(method, err)
|
||||
updateLoginMetrics(conn, method, err)
|
||||
return sshPerm, err
|
||||
}
|
||||
|
||||
|
@ -572,14 +650,10 @@ func (c Configuration) validatePasswordCredentials(conn ssh.ConnMetadata, pass [
|
|||
if len(conn.PartialSuccessMethods()) == 1 {
|
||||
method = dataprovider.SSHLoginMethodKeyAndPassword
|
||||
}
|
||||
metrics.AddLoginAttempt(method)
|
||||
if user, err = dataprovider.CheckUserAndPass(dataProvider, conn.User(), string(pass)); err == nil {
|
||||
sshPerm, err = loginUser(user, method, "", conn)
|
||||
}
|
||||
if err != nil {
|
||||
logger.ConnectionFailedLog(conn.User(), utils.GetIPFromRemoteAddress(conn.RemoteAddr().String()), method, err.Error())
|
||||
}
|
||||
metrics.AddLoginResult(method, err)
|
||||
updateLoginMetrics(conn, method, err)
|
||||
return sshPerm, err
|
||||
}
|
||||
|
||||
|
@ -592,13 +666,17 @@ func (c Configuration) validateKeyboardInteractiveCredentials(conn ssh.ConnMetad
|
|||
if len(conn.PartialSuccessMethods()) == 1 {
|
||||
method = dataprovider.SSHLoginMethodKeyAndKeyboardInt
|
||||
}
|
||||
metrics.AddLoginAttempt(method)
|
||||
if user, err = dataprovider.CheckKeyboardInteractiveAuth(dataProvider, conn.User(), c.KeyboardInteractiveHook, client); err == nil {
|
||||
sshPerm, err = loginUser(user, method, "", conn)
|
||||
}
|
||||
updateLoginMetrics(conn, method, err)
|
||||
return sshPerm, err
|
||||
}
|
||||
|
||||
func updateLoginMetrics(conn ssh.ConnMetadata, method string, err error) {
|
||||
metrics.AddLoginAttempt(method)
|
||||
if err != nil {
|
||||
logger.ConnectionFailedLog(conn.User(), utils.GetIPFromRemoteAddress(conn.RemoteAddr().String()), method, err.Error())
|
||||
}
|
||||
metrics.AddLoginResult(method, err)
|
||||
return sshPerm, err
|
||||
}
|
||||
|
|
|
@ -88,24 +88,39 @@ NbbCNsVroqKlChT5wyPNGS+phi2bPARBno7WSDvshTZ7dAVEP2c9MJW0XwoSevwKlhgSdt
|
|||
RLFFQ/5nclJSdzPBOmQouC0OBcMFSrYtMeknJ4VvueVvve5HcHFaEsaMc7ABAGaLYaBQOm
|
||||
iixITGvaNZh/tjAAAACW5pY29sYUBwMQE=
|
||||
-----END OPENSSH PRIVATE KEY-----`
|
||||
configDir = ".."
|
||||
permissionErrorString = "Permission Denied"
|
||||
osWindows = "windows"
|
||||
// test CA user key.
|
||||
// % ssh-keygen -f ca_user_key
|
||||
testCAUserKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDF5fcwZHiyixmnE6IlOZJpZhWXoh62gN+yadAA0GJ509SAEaZVLPDP8S5RsE8mUikR3wxynVshxHeqMhrkS+RlNbhSlOXDdNg94yTrq/xF8Z/PgKRInvef74k5i7bAIytza7jERzFJ/ujTEy3537T5k5EYQJ15ZQGuvzynSdv+6o99SjI4jFplyQOZ2QcYbEAmhHm5GgQlIiEFG/RlDtLksOulKZxOY3qPzP0AyQxtZJXn/5vG40aW9LTbwxCJqWlgrkFXMqAAVCbuU5YspwhiXmKt1PsldiXw23oloa4caCKN1jzbFiGuZNXEU2Ebx7JIvjQCPaUYwLjEbkRDxDqN/vmwZqBuKYiuG9Eafx+nFSQkr7QYb5b+mT+/1IFHnmeRGn38731kBqtH7tpzC/t+soRX9p2HtJM+9MYhblO2OqTSPGTlxihWUkyiRBekpAhaiHld16TsG+A3bOJHrojGcX+5g6oGarKGLAMcykL1X+rZqT993Mo6d2Z7q43MOXE= root@p1"
|
||||
// this is testPubKey signed using testCAUserKey.
|
||||
// % ssh-keygen -s ca_user_key -I test_user_sftp -n test_user_sftp -V always:forever -O source-address=127.0.0.1 -z 1 /tmp/test.pub
|
||||
testCertValid = "ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgm2fil1IIoTixrA2QE9tk7Vbspj/JdEY90e3K2htxYv8AAAADAQABAAABgQC03jj0D+djk7pxIf/0OhrxrchJTRZklofJ1NoIu4752Sq02mdXmarMVsqJ1cAjV5LBVy3D1F5U6XW4rppkXeVtd04Pxb09ehtH0pRRPaoHHlALiJt8CoMpbKYMA8b3KXPPriGxgGomvtU2T2RMURSwOZbMtpsugfjYSWenyYX+VORYhylWnSXL961LTyC21ehd6d6QnW9G7E5hYMITMY9TuQZz3bROYzXiTsgN0+g6Hn7exFQp50p45StUMfV/SftCMdCxlxuyGny2CrN/vfjO7xxOo2uv7q1qm10Q46KPWJQv+pgZ/OfL+EDjy07n5QVSKHlbx+2nT4Q0EgOSQaCTYwn3YjtABfIxWwgAFdyj6YlPulCL22qU4MYhDcA6PSBwDdf8hvxBfvsiHdM+JcSHvv8/VeJhk6CmnZxGY0fxBupov27z3yEO8nAg8k+6PaUiW1MSUfuGMF/ktB8LOstXsEPXSszuyXiOv4DaryOXUiSn7bmRqKcEFlJusO6aZP0AAAAAAAAAAQAAAAEAAAAOdGVzdF91c2VyX3NmdHAAAAASAAAADnRlc3RfdXNlcl9zZnRwAAAAAAAAAAD//////////wAAACMAAAAOc291cmNlLWFkZHJlc3MAAAANAAAACTEyNy4wLjAuMQAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAZcAAAAHc3NoLXJzYQAAAAMBAAEAAAGBAMXl9zBkeLKLGacToiU5kmlmFZeiHraA37Jp0ADQYnnT1IARplUs8M/xLlGwTyZSKRHfDHKdWyHEd6oyGuRL5GU1uFKU5cN02D3jJOur/EXxn8+ApEie95/viTmLtsAjK3NruMRHMUn+6NMTLfnftPmTkRhAnXllAa6/PKdJ2/7qj31KMjiMWmXJA5nZBxhsQCaEebkaBCUiIQUb9GUO0uSw66UpnE5jeo/M/QDJDG1klef/m8bjRpb0tNvDEImpaWCuQVcyoABUJu5TliynCGJeYq3U+yV2JfDbeiWhrhxoIo3WPNsWIa5k1cRTYRvHski+NAI9pRjAuMRuREPEOo3++bBmoG4piK4b0Rp/H6cVJCSvtBhvlv6ZP7/UgUeeZ5EaffzvfWQGq0fu2nML+36yhFf2nYe0kz70xiFuU7Y6pNI8ZOXGKFZSTKJEF6SkCFqIeV3XpOwb4Dds4keuiMZxf7mDqgZqsoYsAxzKQvVf6tmpP33cyjp3Znurjcw5cQAAAZQAAAAMcnNhLXNoYTItNTEyAAABgMNenD7d1J9cF7JWgHA1DYpJ5+5knPtdXbbIgZAznsTxX7qOdptjeeYOuzhQ5Bwklh3fjewiJpGR1rBqbULP+6PAKeYqd7dNLH/upfKBfJweRf5pdXDpoknHaVuIhi4Uu6FeI4NkAzX9nqNKjFAflhJ+7GLGkLNb0UVZxgxr/t0rPmxc5iTg2ZRM+rk1Ij0S5RnGiKVsdAClqNA6h4TDzu5lJVdK5XvuNKBsKVRCvsVBOgJQTtRTLywQaqWR+HBfCiMj8X8EI7atDlJ6XIAlTLOO/f1sM8QPLjT0+tCHZaGFzg/lKPh3/yFQ4MvddZCptMy1Ll1xvj7cz2ynhGR4PiDfikV3YzgJU/KtL5y+ZB4jU08oPRiOP612PjwZZ+MqYOVOFCKUpMpZQs5UJHME+zNKr4LEj8M0x4YFKIciC+RsrCo4ujbJHmz61ionCadU+fmngvl3C3QjmUdgULBevODeUeIpJv4yFahNxrG1SKRTAa8VVDwJ9GdDTtmXM0mrwA== nicola@p1"
|
||||
// this is testPubKey signed using a CA user key different from testCAUserKey
|
||||
testCertUntrustedCA = "ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAg8oFPWpjYy/DowMmtOjWj7Dq20d2N/4Rxzr/c710tOOUAAAADAQABAAABgQC03jj0D+djk7pxIf/0OhrxrchJTRZklofJ1NoIu4752Sq02mdXmarMVsqJ1cAjV5LBVy3D1F5U6XW4rppkXeVtd04Pxb09ehtH0pRRPaoHHlALiJt8CoMpbKYMA8b3KXPPriGxgGomvtU2T2RMURSwOZbMtpsugfjYSWenyYX+VORYhylWnSXL961LTyC21ehd6d6QnW9G7E5hYMITMY9TuQZz3bROYzXiTsgN0+g6Hn7exFQp50p45StUMfV/SftCMdCxlxuyGny2CrN/vfjO7xxOo2uv7q1qm10Q46KPWJQv+pgZ/OfL+EDjy07n5QVSKHlbx+2nT4Q0EgOSQaCTYwn3YjtABfIxWwgAFdyj6YlPulCL22qU4MYhDcA6PSBwDdf8hvxBfvsiHdM+JcSHvv8/VeJhk6CmnZxGY0fxBupov27z3yEO8nAg8k+6PaUiW1MSUfuGMF/ktB8LOstXsEPXSszuyXiOv4DaryOXUiSn7bmRqKcEFlJusO6aZP0AAAAAAAAAAAAAAAEAAAAOdGVzdF91c2VyX3NmdHAAAAASAAAADnRlc3RfdXNlcl9zZnRwAAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAGXAAAAB3NzaC1yc2EAAAADAQABAAABgQCqgm2gVlptULThfpRR0oCb4SAU3368ULlJaiZOUdq6b94KTfgmu4hTLs7u3a8hyZnVxrKrJ93uAVCwa/HGtgiN96CNC6JUt/QnPqTJ8LQ207RdoE9fbOe6mGwOle5z45+5JFoIi5ZZuD8JsBGodVoa92UepoMyBcNtZyl9q2GP4yT2tIYRon79dtG9AXiDYyhSgePqaObN67dn3ivMc4ZGNukK3cG07cYPic5y0wxX16wSMG3pGQDyUkAu+s4AqpnV9EWHM4PE7SYkCXE99++tUK3QALYqvGZKrLHgzmDKi6n+e14vHYUppAeGDZzwlawiY4oGP9eOW2KUfjZe2ZeL22JTFDYzH2lNV2WtUpeKRGGTSGaUblRVC9hRt6hKCT4c7qpW4kO4kPhE39JpcNPGLql7srNkw+3xXBs8xghMPtH3nOl1Rz2mxnX5tAqmPBb+KiPepnrs+pBRu7i+nCVp8az+iN87STYHy+zPtvTR+QURC8BpNraPOfXwpwM2HaMAAAGUAAAADHJzYS1zaGEyLTUxMgAAAYBnTXCL6tXUO3/Gtsm7lnH9Sulzca8FOoI4Y/4bVYhq4iUNu7Ca452m+Xr9qmCEoIyIJF0LEEcJ8jcS4rfX15e7tNNoknv7JbYXBFAbp1Y/76iqVf89FjfVcbEyH2ToAf7eyQAWzQ3gEKS8mQIkLnAwmCboUXC4GRodSIiOXiTt5Q6T02MVc8TxkhmlTA0uVLd5XgstySgE/oLBnL59lhJcwQmdhHL+m480+PaW55CtMuC36RTwk/tOyuWCDC5qMXnoveNB3yu45o3L/U4hoyJ0/5FyP5C8ahgydY0LoRZQG/mNzuraY4433rK+IfkQvZTyaDtcjhxE6hCD5F40aDDh88i6XaKAPikD6fqra6BN8PoPgLuRHzOJuqsMXBWM99s7qPgSnBbmXlekz/1jvvFiCh3zvAFTxFz2KyE4+SbDcCrhpxkNL7idw6r/ZsHaI/2+zhDcgSs5MgBwYLJEj6zUqVdp5XsF8YfC7yNZV5/qy68qY2+zXrC57SPifU2SCPE= nicola@p1"
|
||||
// this is testPubKey signed as host certificate.
|
||||
// % ssh-keygen -s ca_user_key -I test_user_sftp -h -n test_user_sftp -V always:forever -z 2 /tmp/test.pub
|
||||
testHostCert = "ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAg7O2LDpLO1jGTX3SSzEMILoAYJb9DdggyyaUMXUUg3L4AAAADAQABAAABgQC03jj0D+djk7pxIf/0OhrxrchJTRZklofJ1NoIu4752Sq02mdXmarMVsqJ1cAjV5LBVy3D1F5U6XW4rppkXeVtd04Pxb09ehtH0pRRPaoHHlALiJt8CoMpbKYMA8b3KXPPriGxgGomvtU2T2RMURSwOZbMtpsugfjYSWenyYX+VORYhylWnSXL961LTyC21ehd6d6QnW9G7E5hYMITMY9TuQZz3bROYzXiTsgN0+g6Hn7exFQp50p45StUMfV/SftCMdCxlxuyGny2CrN/vfjO7xxOo2uv7q1qm10Q46KPWJQv+pgZ/OfL+EDjy07n5QVSKHlbx+2nT4Q0EgOSQaCTYwn3YjtABfIxWwgAFdyj6YlPulCL22qU4MYhDcA6PSBwDdf8hvxBfvsiHdM+JcSHvv8/VeJhk6CmnZxGY0fxBupov27z3yEO8nAg8k+6PaUiW1MSUfuGMF/ktB8LOstXsEPXSszuyXiOv4DaryOXUiSn7bmRqKcEFlJusO6aZP0AAAAAAAAAAgAAAAIAAAAOdGVzdF91c2VyX3NmdHAAAAASAAAADnRlc3RfdXNlcl9zZnRwAAAAAAAAAAD//////////wAAAAAAAAAAAAAAAAAAAZcAAAAHc3NoLXJzYQAAAAMBAAEAAAGBAMXl9zBkeLKLGacToiU5kmlmFZeiHraA37Jp0ADQYnnT1IARplUs8M/xLlGwTyZSKRHfDHKdWyHEd6oyGuRL5GU1uFKU5cN02D3jJOur/EXxn8+ApEie95/viTmLtsAjK3NruMRHMUn+6NMTLfnftPmTkRhAnXllAa6/PKdJ2/7qj31KMjiMWmXJA5nZBxhsQCaEebkaBCUiIQUb9GUO0uSw66UpnE5jeo/M/QDJDG1klef/m8bjRpb0tNvDEImpaWCuQVcyoABUJu5TliynCGJeYq3U+yV2JfDbeiWhrhxoIo3WPNsWIa5k1cRTYRvHski+NAI9pRjAuMRuREPEOo3++bBmoG4piK4b0Rp/H6cVJCSvtBhvlv6ZP7/UgUeeZ5EaffzvfWQGq0fu2nML+36yhFf2nYe0kz70xiFuU7Y6pNI8ZOXGKFZSTKJEF6SkCFqIeV3XpOwb4Dds4keuiMZxf7mDqgZqsoYsAxzKQvVf6tmpP33cyjp3Znurjcw5cQAAAZQAAAAMcnNhLXNoYTItNTEyAAABgHlAWMTTzNrE6pxHlkr09ZXsHgJi8U2p7eifs56DOLgklYIXVUJPEEcnzMKGdpPBnqJsvg3+PccqxgOr5L1dFuOmekQ/dGiHd1enrESiGvJOvDfm0WsuBjxEZkSNFWgC9Z2NltToMmRlhVBmb4ZRZtAmi9DAFlJ/BDV4t8ikXZ5oUsigwIeOeLkdPFx3C3x9KZIpuwuAIV4Nfmz75q1NMWY2K1hv682QCKwMYqOWSotz1vWunNmZ0yPRl9UwqAq+nqwO3AApnlrQ3MmHujWQ5tl65PyhfpI8oghhUtB6YrJIAuRXNI/S0+KewCpiYm7nbFBtv9lpecujxAeTibYBrFZ5VODEUm3sdQ/HMdTmkhi6xNgPDQVlvKFqBJAaqoO3tbhKTbEZ865tJMqhyxmZ4XY08wduvSVobrNr7s3rm42/FXLIpung+UOVXonHyeIv9zQ0iJ/bvqKQ1fOsTisZdcD0lz80ZGsjdgJt7yKfUNBnAyVbTXm048E3WsZslJIYCA== nicola@p1"
|
||||
// this is testPubKey signed using testCAUserKey but with source address 172.16.34.45.
|
||||
// % ssh-keygen -s ca_user_key -I test_user_sftp -n test_user_sftp -V always:forever -O source-address=172.16.34.45 -z 3 /tmp/test.pub
|
||||
testCertOtherSourceAddress = "ssh-rsa-cert-v01@openssh.com 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 nicola@p1"
|
||||
configDir = ".."
|
||||
permissionErrorString = "Permission Denied"
|
||||
osWindows = "windows"
|
||||
)
|
||||
|
||||
var (
|
||||
allPerms = []string{dataprovider.PermAny}
|
||||
homeBasePath string
|
||||
scpPath string
|
||||
gitPath string
|
||||
sshPath string
|
||||
pubKeyPath string
|
||||
privateKeyPath string
|
||||
gitWrapPath string
|
||||
extAuthPath string
|
||||
keyIntAuthPath string
|
||||
preLoginPath string
|
||||
logFilePath string
|
||||
allPerms = []string{dataprovider.PermAny}
|
||||
homeBasePath string
|
||||
scpPath string
|
||||
gitPath string
|
||||
sshPath string
|
||||
pubKeyPath string
|
||||
privateKeyPath string
|
||||
trustedCAUserKey string
|
||||
gitWrapPath string
|
||||
extAuthPath string
|
||||
keyIntAuthPath string
|
||||
preLoginPath string
|
||||
logFilePath string
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
@ -178,6 +193,7 @@ func TestMain(m *testing.M) {
|
|||
|
||||
pubKeyPath = filepath.Join(homeBasePath, "ssh_key.pub")
|
||||
privateKeyPath = filepath.Join(homeBasePath, "ssh_key")
|
||||
trustedCAUserKey = filepath.Join(homeBasePath, "ca_user_key")
|
||||
gitWrapPath = filepath.Join(homeBasePath, "gitwrap.sh")
|
||||
extAuthPath = filepath.Join(homeBasePath, "extauth.sh")
|
||||
preLoginPath = filepath.Join(homeBasePath, "prelogin.sh")
|
||||
|
@ -194,6 +210,11 @@ func TestMain(m *testing.M) {
|
|||
if err != nil {
|
||||
logger.WarnToConsole("unable to save gitwrap shell script: %v", err)
|
||||
}
|
||||
err = ioutil.WriteFile(trustedCAUserKey, []byte(testCAUserKey), 0600)
|
||||
if err != nil {
|
||||
logger.WarnToConsole("unable to save trusted CA user key: %v", err)
|
||||
}
|
||||
sftpdConf.TrustedUserCAKeys = append(sftpdConf.TrustedUserCAKeys, trustedCAUserKey)
|
||||
sftpd.SetDataProvider(dataProvider)
|
||||
httpd.SetDataProvider(dataProvider)
|
||||
|
||||
|
@ -240,6 +261,7 @@ func TestMain(m *testing.M) {
|
|||
os.Remove(loginBannerFile)
|
||||
os.Remove(pubKeyPath)
|
||||
os.Remove(privateKeyPath)
|
||||
os.Remove(trustedCAUserKey)
|
||||
os.Remove(gitWrapPath)
|
||||
os.Remove(extAuthPath)
|
||||
os.Remove(preLoginPath)
|
||||
|
@ -751,6 +773,63 @@ func TestLogin(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLoginUserCert(t *testing.T) {
|
||||
u := getTestUser(true)
|
||||
u.PublicKeys = []string{testCertValid, testCertUntrustedCA, testHostCert, testCertOtherSourceAddress}
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
// try login using a cert signed from a trusted CA
|
||||
signer, err := getSignerForUserCert([]byte(testCertValid))
|
||||
assert.NoError(t, err)
|
||||
client, err := getCustomAuthSftpClient(user, []ssh.AuthMethod{ssh.PublicKeys(signer)})
|
||||
if assert.NoError(t, err) {
|
||||
defer client.Close()
|
||||
assert.NoError(t, checkBasicSFTP(client))
|
||||
}
|
||||
// try login using a cert signed from an untrusted CA
|
||||
signer, err = getSignerForUserCert([]byte(testCertUntrustedCA))
|
||||
assert.NoError(t, err)
|
||||
client, err = getCustomAuthSftpClient(user, []ssh.AuthMethod{ssh.PublicKeys(signer)})
|
||||
if !assert.Error(t, err) {
|
||||
client.Close()
|
||||
}
|
||||
// try login using an host certificate instead of an user certificate
|
||||
signer, err = getSignerForUserCert([]byte(testHostCert))
|
||||
assert.NoError(t, err)
|
||||
client, err = getCustomAuthSftpClient(user, []ssh.AuthMethod{ssh.PublicKeys(signer)})
|
||||
if !assert.Error(t, err) {
|
||||
client.Close()
|
||||
}
|
||||
// try login using a user certificate with an authorized source address different from localhost
|
||||
signer, err = getSignerForUserCert([]byte(testCertOtherSourceAddress))
|
||||
assert.NoError(t, err)
|
||||
client, err = getCustomAuthSftpClient(user, []ssh.AuthMethod{ssh.PublicKeys(signer)})
|
||||
if !assert.Error(t, err) {
|
||||
client.Close()
|
||||
}
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
||||
// now login with a username not in the set of valid principals for given certificate
|
||||
u.Username += "1"
|
||||
user, _, err = httpd.AddUser(u, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
||||
signer, err = getSignerForUserCert([]byte(testCertValid))
|
||||
assert.NoError(t, err)
|
||||
client, err = getCustomAuthSftpClient(user, []ssh.AuthMethod{ssh.PublicKeys(signer)})
|
||||
if !assert.Error(t, err) {
|
||||
client.Close()
|
||||
}
|
||||
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMultiStepLoginKeyAndPwd(t *testing.T) {
|
||||
u := getTestUser(true)
|
||||
u.Password = defaultPassword
|
||||
|
@ -770,9 +849,9 @@ func TestMultiStepLoginKeyAndPwd(t *testing.T) {
|
|||
if !assert.Error(t, err, "login with password is disallowed and must fail") {
|
||||
client.Close()
|
||||
}
|
||||
key, _ := ssh.ParsePrivateKey([]byte(testPrivateKey))
|
||||
signer, _ := ssh.ParsePrivateKey([]byte(testPrivateKey))
|
||||
authMethods := []ssh.AuthMethod{
|
||||
ssh.PublicKeys(key),
|
||||
ssh.PublicKeys(signer),
|
||||
ssh.Password(defaultPassword),
|
||||
}
|
||||
client, err = getCustomAuthSftpClient(user, authMethods)
|
||||
|
@ -782,7 +861,7 @@ func TestMultiStepLoginKeyAndPwd(t *testing.T) {
|
|||
}
|
||||
authMethods = []ssh.AuthMethod{
|
||||
ssh.Password(defaultPassword),
|
||||
ssh.PublicKeys(key),
|
||||
ssh.PublicKeys(signer),
|
||||
}
|
||||
_, err = getCustomAuthSftpClient(user, authMethods)
|
||||
assert.Error(t, err, "multi step auth login with wrong order must fail")
|
||||
|
@ -813,9 +892,9 @@ func TestMultiStepLoginKeyAndKeyInt(t *testing.T) {
|
|||
client.Close()
|
||||
}
|
||||
|
||||
key, _ := ssh.ParsePrivateKey([]byte(testPrivateKey))
|
||||
signer, _ := ssh.ParsePrivateKey([]byte(testPrivateKey))
|
||||
authMethods := []ssh.AuthMethod{
|
||||
ssh.PublicKeys(key),
|
||||
ssh.PublicKeys(signer),
|
||||
ssh.KeyboardInteractive(func(user, instruction string, questions []string, echos []bool) ([]string, error) {
|
||||
return []string{"1", "2"}, nil
|
||||
}),
|
||||
|
@ -829,13 +908,13 @@ func TestMultiStepLoginKeyAndKeyInt(t *testing.T) {
|
|||
ssh.KeyboardInteractive(func(user, instruction string, questions []string, echos []bool) ([]string, error) {
|
||||
return []string{"1", "2"}, nil
|
||||
}),
|
||||
ssh.PublicKeys(key),
|
||||
ssh.PublicKeys(signer),
|
||||
}
|
||||
_, err = getCustomAuthSftpClient(user, authMethods)
|
||||
assert.Error(t, err, "multi step auth login with wrong order must fail")
|
||||
|
||||
authMethods = []ssh.AuthMethod{
|
||||
ssh.PublicKeys(key),
|
||||
ssh.PublicKeys(signer),
|
||||
ssh.Password(defaultPassword),
|
||||
}
|
||||
_, err = getCustomAuthSftpClient(user, authMethods)
|
||||
|
@ -847,6 +926,47 @@ func TestMultiStepLoginKeyAndKeyInt(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMultiStepLoginCertAndPwd(t *testing.T) {
|
||||
u := getTestUser(true)
|
||||
u.Password = defaultPassword
|
||||
u.PublicKeys = []string{testCertValid, testCertOtherSourceAddress}
|
||||
u.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, []string{
|
||||
dataprovider.SSHLoginMethodKeyAndKeyboardInt,
|
||||
dataprovider.SSHLoginMethodPublicKey,
|
||||
dataprovider.SSHLoginMethodPassword,
|
||||
dataprovider.SSHLoginMethodKeyboardInteractive,
|
||||
}...)
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
signer, err := getSignerForUserCert([]byte(testCertValid))
|
||||
assert.NoError(t, err)
|
||||
authMethods := []ssh.AuthMethod{
|
||||
ssh.PublicKeys(signer),
|
||||
ssh.Password(defaultPassword),
|
||||
}
|
||||
client, err := getCustomAuthSftpClient(user, authMethods)
|
||||
if assert.NoError(t, err) {
|
||||
defer client.Close()
|
||||
assert.NoError(t, checkBasicSFTP(client))
|
||||
}
|
||||
|
||||
signer, err = getSignerForUserCert([]byte(testCertOtherSourceAddress))
|
||||
assert.NoError(t, err)
|
||||
authMethods = []ssh.AuthMethod{
|
||||
ssh.PublicKeys(signer),
|
||||
ssh.Password(defaultPassword),
|
||||
}
|
||||
client, err = getCustomAuthSftpClient(user, authMethods)
|
||||
if !assert.Error(t, err) {
|
||||
client.Close()
|
||||
}
|
||||
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLoginUserStatus(t *testing.T) {
|
||||
usePubKey := true
|
||||
user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK)
|
||||
|
@ -3949,6 +4069,18 @@ func runSSHCommand(command string, user dataprovider.User, usePubKey bool) ([]by
|
|||
return stdout.Bytes(), err
|
||||
}
|
||||
|
||||
func getSignerForUserCert(certBytes []byte) (ssh.Signer, error) {
|
||||
signer, err := ssh.ParsePrivateKey([]byte(testPrivateKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert, _, _, _, err := ssh.ParseAuthorizedKey(certBytes) //nolint:dogsled
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ssh.NewCertSigner(cert.(*ssh.Certificate), signer)
|
||||
}
|
||||
|
||||
func getSftpClientWithAddr(user dataprovider.User, usePubKey bool, addr string) (*sftp.Client, error) {
|
||||
var sftpClient *sftp.Client
|
||||
config := &ssh.ClientConfig{
|
||||
|
@ -3958,11 +4090,11 @@ func getSftpClientWithAddr(user dataprovider.User, usePubKey bool, addr string)
|
|||
},
|
||||
}
|
||||
if usePubKey {
|
||||
key, err := ssh.ParsePrivateKey([]byte(testPrivateKey))
|
||||
signer, err := ssh.ParsePrivateKey([]byte(testPrivateKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Auth = []ssh.AuthMethod{ssh.PublicKeys(key)}
|
||||
config.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)}
|
||||
} else {
|
||||
if len(user.Password) > 0 {
|
||||
config.Auth = []ssh.AuthMethod{ssh.Password(user.Password)}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
"kex_algorithms": [],
|
||||
"ciphers": [],
|
||||
"macs": [],
|
||||
"trusted_user_ca_keys":[],
|
||||
"login_banner_file": "",
|
||||
"setstat_mode": 0,
|
||||
"enabled_ssh_commands": [
|
||||
|
|
|
@ -65,7 +65,7 @@
|
|||
<textarea class="form-control" id="idPublicKeys" name="public_keys" rows="3"
|
||||
aria-describedby="pkHelpBlock">{{range .User.PublicKeys}}{{.}} {{end}}</textarea>
|
||||
<small id="pkHelpBlock" class="form-text text-muted">
|
||||
One public key per line
|
||||
One public key or SSH user certificate per line
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -317,3 +317,34 @@ func CleanDirInput(dirInput string) string {
|
|||
}
|
||||
return filepath.Clean(dirInput)
|
||||
}
|
||||
|
||||
// CheckSourceAddress check the source address against the one defined inside an SSH user certificate
|
||||
func CheckSourceAddress(addr net.Addr, sourceAddrs string) error {
|
||||
if addr == nil {
|
||||
return errors.New("ssh: no address known for client, but source-address match required")
|
||||
}
|
||||
|
||||
tcpAddr, ok := addr.(*net.TCPAddr)
|
||||
if !ok {
|
||||
return fmt.Errorf("ssh: remote address %v is not an TCP address when checking source-address match", addr)
|
||||
}
|
||||
|
||||
for _, sourceAddr := range strings.Split(sourceAddrs, ",") {
|
||||
if allowedIP := net.ParseIP(sourceAddr); allowedIP != nil {
|
||||
if allowedIP.Equal(tcpAddr.IP) {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
_, ipNet, err := net.ParseCIDR(sourceAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ssh: error parsing source-address restriction %q: %v", sourceAddr, err)
|
||||
}
|
||||
|
||||
if ipNet.Contains(tcpAddr.IP) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("ssh: remote address %v is not allowed because of source-address restriction", addr)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue