sshd: we don't need the user certificate

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2022-03-31 18:16:50 +02:00
parent e079444e8a
commit 5c114b28e3
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
10 changed files with 70 additions and 37 deletions

View file

@ -118,7 +118,7 @@ func (p *BoltProvider) validateAdminAndPass(username, password, ip string) (Admi
return admin, err
}
func (p *BoltProvider) validateUserAndPubKey(username string, pubKey []byte) (User, string, error) {
func (p *BoltProvider) validateUserAndPubKey(username string, pubKey []byte, isSSHCert bool) (User, string, error) {
var user User
if len(pubKey) == 0 {
return user, "", errors.New("credentials cannot be null or empty")
@ -128,7 +128,7 @@ func (p *BoltProvider) validateUserAndPubKey(username string, pubKey []byte) (Us
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)
return user, "", err
}
return checkUserAndPubKey(&user, pubKey)
return checkUserAndPubKey(&user, pubKey, isSSHCert)
}
func (p *BoltProvider) updateAPIKeyLastUse(keyID string) error {

View file

@ -615,7 +615,7 @@ func HasUsersBaseDir() bool {
// Provider defines the interface that data providers must implement.
type Provider interface {
validateUserAndPass(username, password, ip, protocol string) (User, error)
validateUserAndPubKey(username string, pubKey []byte) (User, string, error)
validateUserAndPubKey(username string, pubKey []byte, isSSHCert bool) (User, string, error)
validateUserAndTLSCert(username, protocol string, tlsCert *x509.Certificate) (User, error)
updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error
updateTransferQuota(username string, uploadSize, downloadSize int64, reset bool) error
@ -1054,30 +1054,30 @@ func CheckUserAndPass(username, password, ip, protocol string) (User, error) {
}
// CheckUserAndPubKey retrieves the SFTP user with the given username and public key if a match is found or an error
func CheckUserAndPubKey(username string, pubKey []byte, ip, protocol string) (User, string, error) {
func CheckUserAndPubKey(username string, pubKey []byte, ip, protocol string, isSSHCert bool) (User, string, error) {
username = config.convertName(username)
if plugin.Handler.HasAuthScope(plugin.AuthScopePublicKey) {
user, err := doPluginAuth(username, "", pubKey, ip, protocol, nil, plugin.AuthScopePublicKey)
if err != nil {
return user, "", err
}
return checkUserAndPubKey(&user, pubKey)
return checkUserAndPubKey(&user, pubKey, isSSHCert)
}
if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&2 != 0) {
user, err := doExternalAuth(username, "", pubKey, "", ip, protocol, nil)
if err != nil {
return user, "", err
}
return checkUserAndPubKey(&user, pubKey)
return checkUserAndPubKey(&user, pubKey, isSSHCert)
}
if config.PreLoginHook != "" {
user, err := executePreLoginHook(username, SSHLoginMethodPublicKey, ip, protocol)
if err != nil {
return user, "", err
}
return checkUserAndPubKey(&user, pubKey)
return checkUserAndPubKey(&user, pubKey, isSSHCert)
}
return provider.validateUserAndPubKey(username, pubKey)
return provider.validateUserAndPubKey(username, pubKey, isSSHCert)
}
// CheckKeyboardInteractiveAuth checks the keyboard interactive authentication and returns
@ -2178,9 +2178,10 @@ func validateBaseParams(user *User) error {
if user.HomeDir == "" {
return util.NewValidationError("home_dir is mandatory")
}
if user.Password == "" && len(user.PublicKeys) == 0 {
// we can have users with no passwords and public keys, they can authenticate via SSH user certs or OIDC
/*if user.Password == "" && len(user.PublicKeys) == 0 {
return util.NewValidationError("please set a password or at least a public_key")
}
}*/
if !filepath.IsAbs(user.HomeDir) {
return util.NewValidationError(fmt.Sprintf("home_dir must be an absolute path, actual value: %v", user.HomeDir))
}
@ -2429,28 +2430,25 @@ func checkUserPasscode(user *User, password, protocol string) (string, error) {
return password, nil
}
func checkUserAndPubKey(user *User, pubKey []byte) (User, string, error) {
func checkUserAndPubKey(user *User, pubKey []byte, isSSHCert bool) (User, string, error) {
err := user.CheckLoginConditions()
if err != nil {
return *user, "", err
}
if isSSHCert {
return *user, "", nil
}
if len(user.PublicKeys) == 0 {
return *user, "", ErrInvalidCredentials
}
for i, k := range user.PublicKeys {
storedPubKey, comment, _, _, err := ssh.ParseAuthorizedKey([]byte(k))
if err != nil {
providerLog(logger.LevelError, "error parsing stored public key %d for user %v: %v", i, user.Username, err)
providerLog(logger.LevelError, "error parsing stored public key %d for user %s: %v", i, user.Username, err)
return *user, "", err
}
if bytes.Equal(storedPubKey.Marshal(), pubKey) {
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("%s:%s%s", ssh.FingerprintSHA256(storedPubKey), comment, certInfo), nil
return *user, fmt.Sprintf("%s:%s", ssh.FingerprintSHA256(storedPubKey), comment), nil
}
}
return *user, "", ErrInvalidCredentials

View file

@ -126,7 +126,7 @@ func (p *MemoryProvider) validateUserAndPass(username, password, ip, protocol st
return checkUserAndPass(&user, password, ip, protocol)
}
func (p *MemoryProvider) validateUserAndPubKey(username string, pubKey []byte) (User, string, error) {
func (p *MemoryProvider) validateUserAndPubKey(username string, pubKey []byte, isSSHCert bool) (User, string, error) {
var user User
if len(pubKey) == 0 {
return user, "", errors.New("credentials cannot be null or empty")
@ -136,7 +136,7 @@ func (p *MemoryProvider) validateUserAndPubKey(username string, pubKey []byte) (
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)
return user, "", err
}
return checkUserAndPubKey(&user, pubKey)
return checkUserAndPubKey(&user, pubKey, isSSHCert)
}
func (p *MemoryProvider) validateAdminAndPass(username, password, ip string) (Admin, error) {

View file

@ -203,8 +203,8 @@ func (p *MySQLProvider) validateUserAndTLSCert(username, protocol string, tlsCer
return sqlCommonValidateUserAndTLSCertificate(username, protocol, tlsCert, p.dbHandle)
}
func (p *MySQLProvider) validateUserAndPubKey(username string, publicKey []byte) (User, string, error) {
return sqlCommonValidateUserAndPubKey(username, publicKey, p.dbHandle)
func (p *MySQLProvider) validateUserAndPubKey(username string, publicKey []byte, isSSHCert bool) (User, string, error) {
return sqlCommonValidateUserAndPubKey(username, publicKey, isSSHCert, p.dbHandle)
}
func (p *MySQLProvider) updateTransferQuota(username string, uploadSize, downloadSize int64, reset bool) error {

View file

@ -179,8 +179,8 @@ func (p *PGSQLProvider) validateUserAndTLSCert(username, protocol string, tlsCer
return sqlCommonValidateUserAndTLSCertificate(username, protocol, tlsCert, p.dbHandle)
}
func (p *PGSQLProvider) validateUserAndPubKey(username string, publicKey []byte) (User, string, error) {
return sqlCommonValidateUserAndPubKey(username, publicKey, p.dbHandle)
func (p *PGSQLProvider) validateUserAndPubKey(username string, publicKey []byte, isSSHCert bool) (User, string, error) {
return sqlCommonValidateUserAndPubKey(username, publicKey, isSSHCert, p.dbHandle)
}
func (p *PGSQLProvider) updateTransferQuota(username string, uploadSize, downloadSize int64, reset bool) error {

View file

@ -620,7 +620,7 @@ func sqlCommonValidateUserAndTLSCertificate(username, protocol string, tlsCert *
return checkUserAndTLSCertificate(&user, protocol, tlsCert)
}
func sqlCommonValidateUserAndPubKey(username string, pubKey []byte, dbHandle *sql.DB) (User, string, error) {
func sqlCommonValidateUserAndPubKey(username string, pubKey []byte, isSSHCert bool, dbHandle *sql.DB) (User, string, error) {
var user User
if len(pubKey) == 0 {
return user, "", errors.New("credentials cannot be null or empty")
@ -630,7 +630,7 @@ func sqlCommonValidateUserAndPubKey(username string, pubKey []byte, dbHandle *sq
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)
return user, "", err
}
return checkUserAndPubKey(&user, pubKey)
return checkUserAndPubKey(&user, pubKey, isSSHCert)
}
func sqlCommonCheckAvailability(dbHandle *sql.DB) error {

View file

@ -153,8 +153,8 @@ func (p *SQLiteProvider) validateUserAndTLSCert(username, protocol string, tlsCe
return sqlCommonValidateUserAndTLSCertificate(username, protocol, tlsCert, p.dbHandle)
}
func (p *SQLiteProvider) validateUserAndPubKey(username string, publicKey []byte) (User, string, error) {
return sqlCommonValidateUserAndPubKey(username, publicKey, p.dbHandle)
func (p *SQLiteProvider) validateUserAndPubKey(username string, publicKey []byte, isSSHCert bool) (User, string, error) {
return sqlCommonValidateUserAndPubKey(username, publicKey, isSSHCert, p.dbHandle)
}
func (p *SQLiteProvider) updateTransferQuota(username string, uploadSize, downloadSize int64, reset bool) error {

View file

@ -1583,7 +1583,13 @@ func TestAddUserNoCredentials(t *testing.T) {
u := getTestUser()
u.Password = ""
u.PublicKeys = []string{}
_, _, err := httpdtest.AddUser(u, http.StatusBadRequest)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
// this user cannot login with an empty password but it still can use an SSH cert
_, err = getJWTAPITokenFromTestServer(defaultTokenAuthUser, "")
assert.Error(t, err)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
}

View file

@ -885,6 +885,13 @@ func (c *Configuration) validatePublicKeyCredentials(conn ssh.ConnMetadata, pubK
updateLoginMetrics(&user, ipAddr, method, err)
return nil, err
}
if len(cert.ValidPrincipals) == 0 {
err = fmt.Errorf("ssh: certificate %s has no valid principals, user: \"%s\"",
ssh.FingerprintSHA256(pubKey), conn.User())
user.Username = conn.User()
updateLoginMetrics(&user, ipAddr, method, err)
return nil, err
}
if err := c.certChecker.CheckCert(conn.User(), cert); err != nil {
user.Username = conn.User()
updateLoginMetrics(&user, ipAddr, method, err)
@ -892,7 +899,11 @@ func (c *Configuration) validatePublicKeyCredentials(conn ssh.ConnMetadata, pubK
}
certPerm = &cert.Permissions
}
if user, keyID, err = dataprovider.CheckUserAndPubKey(conn.User(), pubKey.Marshal(), ipAddr, common.ProtocolSSH); err == nil {
if user, keyID, err = dataprovider.CheckUserAndPubKey(conn.User(), pubKey.Marshal(), ipAddr, common.ProtocolSSH, ok); err == nil {
if ok {
keyID = fmt.Sprintf("%s: ID: %s, serial: %v, CA %s %s", ssh.FingerprintSHA256(pubKey),
cert.KeyId, cert.Serial, cert.Type(), ssh.FingerprintSHA256(cert.SignatureKey))
}
if user.IsPartialAuth(method) {
logger.Debug(logSender, connectionID, "user %#v authenticated with partial success", conn.User())
return certPerm, ssh.ErrPartialSuccess

View file

@ -116,10 +116,13 @@ iixITGvaNZh/tjAAAACW5pY29sYUBwMQE=
// this is testPubKey signed using testCAUserKey but expired.
// % ssh-keygen -s ca_user_key -I test_user_sftp -n test_user_sftp -V 20100101123000:20110101123000 -z 4 /tmp/test.pub
testCertExpired = "ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgU3TLP5285k20fBSsdZioI78oJUpaRXFlgx5IPg6gWg8AAAADAQABAAABgQC03jj0D+djk7pxIf/0OhrxrchJTRZklofJ1NoIu4752Sq02mdXmarMVsqJ1cAjV5LBVy3D1F5U6XW4rppkXeVtd04Pxb09ehtH0pRRPaoHHlALiJt8CoMpbKYMA8b3KXPPriGxgGomvtU2T2RMURSwOZbMtpsugfjYSWenyYX+VORYhylWnSXL961LTyC21ehd6d6QnW9G7E5hYMITMY9TuQZz3bROYzXiTsgN0+g6Hn7exFQp50p45StUMfV/SftCMdCxlxuyGny2CrN/vfjO7xxOo2uv7q1qm10Q46KPWJQv+pgZ/OfL+EDjy07n5QVSKHlbx+2nT4Q0EgOSQaCTYwn3YjtABfIxWwgAFdyj6YlPulCL22qU4MYhDcA6PSBwDdf8hvxBfvsiHdM+JcSHvv8/VeJhk6CmnZxGY0fxBupov27z3yEO8nAg8k+6PaUiW1MSUfuGMF/ktB8LOstXsEPXSszuyXiOv4DaryOXUiSn7bmRqKcEFlJusO6aZP0AAAAAAAAABAAAAAEAAAAOdGVzdF91c2VyX3NmdHAAAAASAAAADnRlc3RfdXNlcl9zZnRwAAAAAEs93LgAAAAATR8QOAAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAGXAAAAB3NzaC1yc2EAAAADAQABAAABgQDF5fcwZHiyixmnE6IlOZJpZhWXoh62gN+yadAA0GJ509SAEaZVLPDP8S5RsE8mUikR3wxynVshxHeqMhrkS+RlNbhSlOXDdNg94yTrq/xF8Z/PgKRInvef74k5i7bAIytza7jERzFJ/ujTEy3537T5k5EYQJ15ZQGuvzynSdv+6o99SjI4jFplyQOZ2QcYbEAmhHm5GgQlIiEFG/RlDtLksOulKZxOY3qPzP0AyQxtZJXn/5vG40aW9LTbwxCJqWlgrkFXMqAAVCbuU5YspwhiXmKt1PsldiXw23oloa4caCKN1jzbFiGuZNXEU2Ebx7JIvjQCPaUYwLjEbkRDxDqN/vmwZqBuKYiuG9Eafx+nFSQkr7QYb5b+mT+/1IFHnmeRGn38731kBqtH7tpzC/t+soRX9p2HtJM+9MYhblO2OqTSPGTlxihWUkyiRBekpAhaiHld16TsG+A3bOJHrojGcX+5g6oGarKGLAMcykL1X+rZqT993Mo6d2Z7q43MOXEAAAGUAAAADHJzYS1zaGEyLTUxMgAAAYAlH3hhj8J6xLyVpeLZjblzwDKrxp/MWiH30hQ965ExPrPRcoAZFEKVqOYdj6bp4Q19Q4Yzqdobg3aN5ym2iH0b2TlOY0mM901CAoHbNJyiLs+0KiFRoJ+30EDj/hcKusg6v8ln2yixPagAyQu3zyiWo4t1ZuO3I86xchGlptStxSdHAHPFCfpbhcnzWFZctiMqUutl82C4ROWyjOZcRzdVdWHeN5h8wnooXuvba2VkT8QPmjYYyRGuQ3Hg+ySdh8Tel4wiix1Dg5MX7Wjh4hKEx80No9UPy+0iyZMNc07lsWAtrY6NRxGM5CzB6mklscB8TzFrVSnIl9u3bquLfaCrFt/Mft5dR7Yy4jmF+zUhjia6h6giCZ91J+FZ4hV+WkBtPCvTfrGWoA1BgEB/iI2xOq/NPqJ7UXRoMXk/l0NPgRPT2JS1adegqnt4ddr6IlmPyZxaSEvXhanjKdfMlEFYO1wz7ouqpYUozQVy4KXBlzFlNwyD1hI+k4+/A6AIYeI= nicola@p1"
configDir = ".."
osWindows = "windows"
testFileName = "test_file_sftp.dat"
testDLFileName = "test_download_sftp.dat"
// this is testPubKey signed without a principal
// ssh-keygen -s ca_user_key -I test_user_sftp -V always:forever -O source-address=127.0.0.1 -z 1 /tmp/test.pub
testCertNoPrincipals = "ssh-rsa-cert-v01@openssh.com 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 nicola@p1"
configDir = ".."
osWindows = "windows"
testFileName = "test_file_sftp.dat"
testDLFileName = "test_download_sftp.dat"
)
var (
@ -1788,7 +1791,6 @@ func TestLogin(t *testing.T) {
func TestLoginUserCert(t *testing.T) {
u := getTestUser(true)
u.PublicKeys = []string{testCertValid, testCertUntrustedCA, testHostCert, testCertOtherSourceAddress, testCertExpired}
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
// try login using a cert signed from a trusted CA
@ -1832,11 +1834,28 @@ func TestLoginUserCert(t *testing.T) {
client.Close()
conn.Close()
}
// try login using a certificate with no principals
signer, err = getSignerForUserCert([]byte(testCertNoPrincipals))
assert.NoError(t, err)
conn, client, err = getCustomAuthSftpClient(user, []ssh.AuthMethod{ssh.PublicKeys(signer)}, "")
if !assert.Error(t, err) {
client.Close()
conn.Close()
}
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
// the user does not exist
signer, err = getSignerForUserCert([]byte(testCertValid))
assert.NoError(t, err)
conn, client, err = getCustomAuthSftpClient(user, []ssh.AuthMethod{ssh.PublicKeys(signer)}, "")
if !assert.Error(t, err) {
client.Close()
conn.Close()
}
// now login with a username not in the set of valid principals for the given certificate
u.Username += "1"
user, _, err = httpdtest.AddUser(u, http.StatusCreated)
@ -1989,7 +2008,6 @@ func TestMultiStepLoginKeyAndKeyInt(t *testing.T) {
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,