mirror of
https://github.com/drakkan/sftpgo.git
synced 2025-12-06 14:20:55 +03:00
sftpd: use VerifiedPublicKeyCallback
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
2
go.mod
2
go.mod
@@ -64,7 +64,7 @@ require (
|
|||||||
github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a
|
github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a
|
||||||
go.etcd.io/bbolt v1.4.3
|
go.etcd.io/bbolt v1.4.3
|
||||||
gocloud.dev v0.43.0
|
gocloud.dev v0.43.0
|
||||||
golang.org/x/crypto v0.42.0
|
golang.org/x/crypto v0.42.1-0.20250927194341-2beaa59a3c99
|
||||||
golang.org/x/net v0.44.0
|
golang.org/x/net v0.44.0
|
||||||
golang.org/x/oauth2 v0.31.0
|
golang.org/x/oauth2 v0.31.0
|
||||||
golang.org/x/sys v0.36.0
|
golang.org/x/sys v0.36.0
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -420,8 +420,8 @@ golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf
|
|||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
golang.org/x/crypto v0.42.1-0.20250927194341-2beaa59a3c99 h1:zYtc2MFSothOSrO593KfVa10ypmGMc4q31CirElMBFQ=
|
||||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
golang.org/x/crypto v0.42.1-0.20250927194341-2beaa59a3c99/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"maps"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -32,7 +33,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/sftp"
|
"github.com/pkg/sftp"
|
||||||
"github.com/rs/xid"
|
|
||||||
"github.com/sftpgo/sdk/plugin/notifier"
|
"github.com/sftpgo/sdk/plugin/notifier"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
|
|
||||||
@@ -52,6 +52,10 @@ const (
|
|||||||
defaultPrivateEd25519KeyName = "id_ed25519"
|
defaultPrivateEd25519KeyName = "id_ed25519"
|
||||||
sourceAddressCriticalOption = "source-address"
|
sourceAddressCriticalOption = "source-address"
|
||||||
keyExchangeCurve25519SHA256LibSSH = "curve25519-sha256@libssh.org"
|
keyExchangeCurve25519SHA256LibSSH = "curve25519-sha256@libssh.org"
|
||||||
|
extraDataPartialSuccessErrKey = "partialSuccessErr"
|
||||||
|
extraDataUserKey = "user"
|
||||||
|
extraDataKeyIDKey = "keyID"
|
||||||
|
extraDataLoginMethodKey = "login_method"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -233,10 +237,6 @@ func (c *Configuration) getServerConfig() *ssh.ServerConfig {
|
|||||||
MaxAuthTries: c.MaxAuthTries,
|
MaxAuthTries: c.MaxAuthTries,
|
||||||
PublicKeyCallback: func(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
|
PublicKeyCallback: func(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
|
||||||
sp, err := c.validatePublicKeyCredentials(conn, pubKey)
|
sp, err := c.validatePublicKeyCredentials(conn, pubKey)
|
||||||
var partialSuccess *ssh.PartialSuccessError
|
|
||||||
if errors.As(err, &partialSuccess) {
|
|
||||||
return sp, err
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, newAuthenticationError(fmt.Errorf("could not validate public key credentials: %w", err),
|
return nil, newAuthenticationError(fmt.Errorf("could not validate public key credentials: %w", err),
|
||||||
dataprovider.SSHLoginMethodPublicKey, conn.User())
|
dataprovider.SSHLoginMethodPublicKey, conn.User())
|
||||||
@@ -244,6 +244,35 @@ func (c *Configuration) getServerConfig() *ssh.ServerConfig {
|
|||||||
|
|
||||||
return sp, nil
|
return sp, nil
|
||||||
},
|
},
|
||||||
|
VerifiedPublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey, permissions *ssh.Permissions, signatureAlgorithm string) (*ssh.Permissions, error) {
|
||||||
|
if partialErr, ok := permissions.ExtraData[extraDataPartialSuccessErrKey]; ok {
|
||||||
|
logger.Info(logSender, hex.EncodeToString(conn.SessionID()), "user %q authenticated with partial success, signature algorithm %q",
|
||||||
|
conn.User(), signatureAlgorithm)
|
||||||
|
return nil, partialErr.(error)
|
||||||
|
}
|
||||||
|
method := dataprovider.SSHLoginMethodPublicKey
|
||||||
|
user := permissions.ExtraData[extraDataUserKey].(dataprovider.User)
|
||||||
|
keyID := permissions.ExtraData[extraDataKeyIDKey].(string)
|
||||||
|
sshPerm, err := loginUser(&user, method, fmt.Sprintf("%s (%s)", keyID, signatureAlgorithm), conn)
|
||||||
|
if err == 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 = permissions.CriticalOptions
|
||||||
|
if permissions.Extensions != nil {
|
||||||
|
if sshPerm.Extensions == nil {
|
||||||
|
sshPerm.Extensions = make(map[string]string)
|
||||||
|
}
|
||||||
|
maps.Copy(sshPerm.Extensions, permissions.Extensions)
|
||||||
|
}
|
||||||
|
if sshPerm.ExtraData == nil {
|
||||||
|
sshPerm.ExtraData = make(map[any]any)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
user.Username = conn.User()
|
||||||
|
ipAddr := util.GetIPFromRemoteAddress(conn.RemoteAddr().String())
|
||||||
|
updateLoginMetrics(&user, ipAddr, method, err)
|
||||||
|
return sshPerm, err
|
||||||
|
},
|
||||||
ServerVersion: fmt.Sprintf("SSH-2.0-%s", version.GetServerVersion("_", false)),
|
ServerVersion: fmt.Sprintf("SSH-2.0-%s", version.GetServerVersion("_", false)),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -591,13 +620,9 @@ func (c *Configuration) AcceptInboundConnection(conn net.Conn, config *ssh.Serve
|
|||||||
|
|
||||||
defer sconn.Close()
|
defer sconn.Close()
|
||||||
|
|
||||||
var user dataprovider.User
|
user := sconn.Permissions.ExtraData[extraDataUserKey].(dataprovider.User)
|
||||||
|
loginType := sconn.Permissions.ExtraData[extraDataLoginMethodKey].(string)
|
||||||
// Unmarshal cannot fails here and even if it fails we'll have a user with no permissions
|
connectionID := hex.EncodeToString(sconn.SessionID())
|
||||||
json.Unmarshal(util.StringToBytes(sconn.Permissions.Extensions["sftpgo_user"]), &user) //nolint:errcheck
|
|
||||||
|
|
||||||
loginType := sconn.Permissions.Extensions["sftpgo_login_method"]
|
|
||||||
connectionID := xid.New().String()
|
|
||||||
|
|
||||||
defer user.CloseFs() //nolint:errcheck
|
defer user.CloseFs() //nolint:errcheck
|
||||||
if err = user.CheckFsRoot(connectionID); err != nil {
|
if err = user.CheckFsRoot(connectionID); err != nil {
|
||||||
@@ -815,18 +840,13 @@ func loginUser(user *dataprovider.User, loginMethod, publicKey string, conn ssh.
|
|||||||
return nil, fmt.Errorf("login for user %q is not allowed from this address: %v", user.Username, remoteAddr)
|
return nil, fmt.Errorf("login for user %q is not allowed from this address: %v", user.Username, remoteAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
json, err := json.Marshal(user)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn(logSender, connectionID, "error serializing user info: %v, authentication rejected", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if publicKey != "" {
|
if publicKey != "" {
|
||||||
loginMethod = fmt.Sprintf("%v: %v", loginMethod, publicKey)
|
loginMethod = fmt.Sprintf("%v: %v", loginMethod, publicKey)
|
||||||
}
|
}
|
||||||
p := &ssh.Permissions{}
|
p := &ssh.Permissions{}
|
||||||
p.Extensions = make(map[string]string)
|
p.ExtraData = make(map[any]any)
|
||||||
p.Extensions["sftpgo_user"] = util.BytesToString(json)
|
p.ExtraData[extraDataUserKey] = *user
|
||||||
p.Extensions["sftpgo_login_method"] = loginMethod
|
p.ExtraData[extraDataLoginMethodKey] = loginMethod
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1114,13 +1134,9 @@ func (c *Configuration) getPartialSuccessError(nextAuthMethods []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Configuration) validatePublicKeyCredentials(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
|
func (c *Configuration) validatePublicKeyCredentials(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
|
||||||
var err error
|
|
||||||
var user dataprovider.User
|
var user dataprovider.User
|
||||||
var keyID string
|
|
||||||
var sshPerm *ssh.Permissions
|
|
||||||
var certPerm *ssh.Permissions
|
var certPerm *ssh.Permissions
|
||||||
|
|
||||||
connectionID := hex.EncodeToString(conn.SessionID())
|
|
||||||
method := dataprovider.SSHLoginMethodPublicKey
|
method := dataprovider.SSHLoginMethodPublicKey
|
||||||
ipAddr := util.GetIPFromRemoteAddress(conn.RemoteAddr().String())
|
ipAddr := util.GetIPFromRemoteAddress(conn.RemoteAddr().String())
|
||||||
cert, ok := pubKey.(*ssh.Certificate)
|
cert, ok := pubKey.(*ssh.Certificate)
|
||||||
@@ -1128,25 +1144,25 @@ func (c *Configuration) validatePublicKeyCredentials(conn ssh.ConnMetadata, pubK
|
|||||||
if ok {
|
if ok {
|
||||||
certFingerprint = ssh.FingerprintSHA256(cert.Key)
|
certFingerprint = ssh.FingerprintSHA256(cert.Key)
|
||||||
if cert.CertType != ssh.UserCert {
|
if cert.CertType != ssh.UserCert {
|
||||||
err = fmt.Errorf("ssh: cert has type %d", cert.CertType)
|
err := fmt.Errorf("ssh: cert has type %d", cert.CertType)
|
||||||
user.Username = conn.User()
|
user.Username = conn.User()
|
||||||
updateLoginMetrics(&user, ipAddr, method, err)
|
updateLoginMetrics(&user, ipAddr, method, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !c.certChecker.IsUserAuthority(cert.SignatureKey) {
|
if !c.certChecker.IsUserAuthority(cert.SignatureKey) {
|
||||||
err = errors.New("ssh: certificate signed by unrecognized authority")
|
err := errors.New("ssh: certificate signed by unrecognized authority")
|
||||||
user.Username = conn.User()
|
user.Username = conn.User()
|
||||||
updateLoginMetrics(&user, ipAddr, method, err)
|
updateLoginMetrics(&user, ipAddr, method, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(cert.ValidPrincipals) == 0 {
|
if len(cert.ValidPrincipals) == 0 {
|
||||||
err = fmt.Errorf("ssh: certificate %s has no valid principals, user: \"%s\"", certFingerprint, conn.User())
|
err := fmt.Errorf("ssh: certificate %s has no valid principals, user: \"%s\"", certFingerprint, conn.User())
|
||||||
user.Username = conn.User()
|
user.Username = conn.User()
|
||||||
updateLoginMetrics(&user, ipAddr, method, err)
|
updateLoginMetrics(&user, ipAddr, method, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if revokedCertManager.isRevoked(certFingerprint) {
|
if revokedCertManager.isRevoked(certFingerprint) {
|
||||||
err = fmt.Errorf("ssh: certificate %s is revoked", certFingerprint)
|
err := fmt.Errorf("ssh: certificate %s is revoked", certFingerprint)
|
||||||
user.Username = conn.User()
|
user.Username = conn.User()
|
||||||
updateLoginMetrics(&user, ipAddr, method, err)
|
updateLoginMetrics(&user, ipAddr, method, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1158,30 +1174,26 @@ func (c *Configuration) validatePublicKeyCredentials(conn ssh.ConnMetadata, pubK
|
|||||||
}
|
}
|
||||||
certPerm = &cert.Permissions
|
certPerm = &cert.Permissions
|
||||||
}
|
}
|
||||||
if user, keyID, err = dataprovider.CheckUserAndPubKey(conn.User(), pubKey.Marshal(), ipAddr, common.ProtocolSSH, ok); err == nil {
|
user, keyID, err := dataprovider.CheckUserAndPubKey(conn.User(), pubKey.Marshal(), ipAddr, common.ProtocolSSH, ok)
|
||||||
|
if err != nil {
|
||||||
|
user.Username = conn.User()
|
||||||
|
updateLoginMetrics(&user, ipAddr, method, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if ok {
|
if ok {
|
||||||
keyID = fmt.Sprintf("%s: ID: %s, serial: %v, CA %s %s", certFingerprint,
|
keyID = fmt.Sprintf("%s: ID: %s, serial: %v, CA %s %s", certFingerprint,
|
||||||
cert.KeyId, cert.Serial, cert.Type(), ssh.FingerprintSHA256(cert.SignatureKey))
|
cert.KeyId, cert.Serial, cert.Type(), ssh.FingerprintSHA256(cert.SignatureKey))
|
||||||
}
|
}
|
||||||
|
if certPerm == nil {
|
||||||
|
certPerm = &ssh.Permissions{}
|
||||||
|
}
|
||||||
|
certPerm.ExtraData = make(map[any]any)
|
||||||
|
certPerm.ExtraData[extraDataKeyIDKey] = keyID
|
||||||
|
certPerm.ExtraData[extraDataUserKey] = user
|
||||||
if user.IsPartialAuth() {
|
if user.IsPartialAuth() {
|
||||||
logger.Debug(logSender, connectionID, "user %q authenticated with partial success", conn.User())
|
certPerm.ExtraData[extraDataPartialSuccessErrKey] = c.getPartialSuccessError(user.GetNextAuthMethods())
|
||||||
return certPerm, c.getPartialSuccessError(user.GetNextAuthMethods())
|
|
||||||
}
|
}
|
||||||
sshPerm, err = loginUser(&user, method, keyID, conn)
|
return certPerm, nil
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
user.Username = conn.User()
|
|
||||||
updateLoginMetrics(&user, ipAddr, method, err)
|
|
||||||
return sshPerm, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Configuration) validatePasswordCredentials(conn ssh.ConnMetadata, pass []byte, method string) (*ssh.Permissions, error) {
|
func (c *Configuration) validatePasswordCredentials(conn ssh.ConnMetadata, pass []byte, method string) (*ssh.Permissions, error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user