This is an automated email from the ASF dual-hosted git repository. lgoldstein pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/mina-sshd.git
commit e1f57616b3ea65951ea9365d44fc44607211cb49 Author: Lyor Goldstein <lgoldst...@apache.org> AuthorDate: Wed Oct 16 11:20:03 2019 +0300 [SSHD-948] Do not accept password change request if the session does not use MAC(s) for packet validation --- CHANGES.md | 4 ++ .../apache/sshd/common/session/SessionContext.java | 24 ++++++++++++ .../client/auth/password/UserAuthPassword.java | 7 ++++ .../sshd/common/auth/UserAuthMethodFactory.java | 29 ++++++++++++++ .../auth/password/PasswordAuthenticator.java | 20 +++++++++- .../server/auth/password/UserAuthPassword.java | 38 +++++++++++++----- .../server/jaas/JaasPasswordAuthenticator.java | 16 ++++---- .../sshd/common/auth/AuthenticationTest.java | 45 +++++++++------------- .../server/jaas/JaasPasswordAuthenticatorTest.java | 14 ++++--- .../sshd/util/test/BogusPasswordAuthenticator.java | 6 ++- .../auth/password/LdapPasswordAuthenticator.java | 9 +++-- 11 files changed, 156 insertions(+), 56 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b9f692d..6ac150d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -40,6 +40,10 @@ of their invocation (if any) * Default MAC(s) list is set according to the [ssh_config(5)](https://www.freebsd.org/cgi/man.cgi?query=ssh_config&sektion=5) order as **first** ones, where the supported MAC(s) that do no appear in it come last. +* `PasswordAuthenticator` has a `handleClientPasswordChangeRequest` method that is invoked if +a password change has been indicated by the user during authentication via the "password" +method - by default throws `UnsupportedOperationException`. + ## Minor code helpers * `SessionListener` supports `sessionPeerIdentificationReceived` method that is invoked once successful diff --git a/sshd-common/src/main/java/org/apache/sshd/common/session/SessionContext.java b/sshd-common/src/main/java/org/apache/sshd/common/session/SessionContext.java index c6ab1fc..a394868 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/session/SessionContext.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/session/SessionContext.java @@ -131,6 +131,8 @@ public interface SessionContext * @param session The {@link SessionContext} to be examined * @return {@code true} if the context is not {@code null} and the ciphers * have been established to anything other than "none". + * @see #getNegotiatedKexParameter(KexProposalOption) getNegotiatedKexParameter + * @see KexProposalOption#CIPHER_PROPOSALS CIPHER_PROPOSALS */ static boolean isSecureSessionTransport(SessionContext session) { if (session == null) { @@ -147,4 +149,26 @@ public interface SessionContext return true; } + + /** + * @param session The {@link SessionContext} to be examined + * @return {@code true} if the context is not {@code null} and the MAC(s) + * used to verify packet integrity have been established. + * @see #getNegotiatedKexParameter(KexProposalOption) getNegotiatedKexParameter + * @see KexProposalOption#MAC_PROPOSALS MAC_PROPOSALS + */ + static boolean isDataIntegrityTransport(SessionContext session) { + if (session == null) { + return false; + } + + for (KexProposalOption opt : KexProposalOption.MAC_PROPOSALS) { + String value = session.getNegotiatedKexParameter(opt); + if (GenericUtils.isEmpty(value)) { + return false; + } + } + + return true; + } } diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/password/UserAuthPassword.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/password/UserAuthPassword.java index 119ad99..08db76c 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/auth/password/UserAuthPassword.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/password/UserAuthPassword.java @@ -99,6 +99,13 @@ public class UserAuthPassword extends AbstractUserAuth { return false; } + if (!UserAuthMethodFactory.isDataIntegrityAuthenticationTransport(session)) { + if (debugEnabled) { + log.debug("processAuthDataRequest({})[{}] session is not validated via MAC", session, service); + } + return false; + } + String prompt = buffer.getString(); String lang = buffer.getString(); UserInteraction ui = session.getUserInteraction(); diff --git a/sshd-core/src/main/java/org/apache/sshd/common/auth/UserAuthMethodFactory.java b/sshd-core/src/main/java/org/apache/sshd/common/auth/UserAuthMethodFactory.java index 37a8768..94b688c 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/auth/UserAuthMethodFactory.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/auth/UserAuthMethodFactory.java @@ -64,6 +64,14 @@ public interface UserAuthMethodFactory<S extends SessionContext, M extends UserA boolean DEFAULT_ALLOW_INSECURE_AUTH = false; /** + * If set to {@code true} then {@link #isDataIntegrityAuthenticationTransport(SessionContext)} + * returns {@code true} even if transport has no MAC(s) to verify message integrity + */ + String ALLOW_NON_INTEGRITY_AUTH = "allow-non-integrity-auth"; + + boolean DEFAULT_ALLOW_NON_INTEGRITY_AUTH = false; + + /** * @param session The session for which authentication is required * @return The authenticator instance * @throws IOException If failed to create the instance @@ -121,4 +129,25 @@ public interface UserAuthMethodFactory<S extends SessionContext, M extends UserA return SessionContext.isSecureSessionTransport(session); } + + /** + * @param session The {@link SessionContext} being used for authentication + * @return {@code true} if the context is not {@code null} and the MAC(s) + * used to verify packet integrity have been established. + * @see {@value #ALLOW_NON_INTEGRITY_AUTH} + * @see SessionContext#isDataIntegrityTransport(SessionContext) + */ + static boolean isDataIntegrityAuthenticationTransport(SessionContext session) { + if (session == null) { + return false; + } + + boolean allowNonValidated = PropertyResolverUtils.getBooleanProperty( + session, ALLOW_NON_INTEGRITY_AUTH, DEFAULT_ALLOW_NON_INTEGRITY_AUTH); + if (allowNonValidated) { + return true; + } + + return SessionContext.isDataIntegrityTransport(session); + } } diff --git a/sshd-core/src/main/java/org/apache/sshd/server/auth/password/PasswordAuthenticator.java b/sshd-core/src/main/java/org/apache/sshd/server/auth/password/PasswordAuthenticator.java index fcc91e1..9cf3d6c 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/auth/password/PasswordAuthenticator.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/auth/password/PasswordAuthenticator.java @@ -40,5 +40,23 @@ public interface PasswordAuthenticator { * @throws AsyncAuthException If the authentication is performed asynchronously */ boolean authenticate(String username, String password, ServerSession session) - throws PasswordChangeRequiredException, AsyncAuthException; + throws PasswordChangeRequiredException, AsyncAuthException; + + /** + * Invoked when the client sends a {@code SSH_MSG_USERAUTH_REQUEST} indicating + * a password change. This can happen if the {@code authenticate} method + * threw {@link PasswordChangeRequiredException} thus telling the client that + * it needs to provide a new password. Throws {@link UnsupportedOperationException} + * by default. + * + * @param session The {@link ServerSession} attempting the authentication + * @param username The username credential + * @param oldPassword The old password + * @param newPassword The new password + * @return {@code true} if password change accepted + */ + default boolean handleClientPasswordChangeRequest( + ServerSession session, String username, String oldPassword, String newPassword) { + throw new UnsupportedOperationException("Password change not supported"); + } } diff --git a/sshd-core/src/main/java/org/apache/sshd/server/auth/password/UserAuthPassword.java b/sshd-core/src/main/java/org/apache/sshd/server/auth/password/UserAuthPassword.java index 6c5be5e..cc94cb6 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/auth/password/UserAuthPassword.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/auth/password/UserAuthPassword.java @@ -43,13 +43,22 @@ public class UserAuthPassword extends AbstractUserAuth { public Boolean doAuth(Buffer buffer, boolean init) throws Exception { ValidateUtils.checkTrue(init, "Instance not initialized"); + ServerSession session = getServerSession(); + if (!UserAuthMethodFactory.isSecureAuthenticationTransport(session)) { + if (log.isDebugEnabled()) { + log.debug("doAuth({}) session is not secure", session); + } + return false; + } + + String username = getUsername(); boolean newPassword = buffer.getBoolean(); String password = buffer.getString(); if (newPassword) { return handleClientPasswordChangeRequest( - buffer, getServerSession(), getUsername(), password, buffer.getString()); + buffer, session, username, password, buffer.getString()); } else { - return checkPassword(buffer, getServerSession(), getUsername(), password); + return checkPassword(buffer, session, username, password); } } @@ -74,13 +83,6 @@ public class UserAuthPassword extends AbstractUserAuth { Buffer buffer, ServerSession session, String username, String password) throws Exception { boolean debugEnabled = log.isDebugEnabled(); - if (!UserAuthMethodFactory.isSecureAuthenticationTransport(session)) { - if (debugEnabled) { - log.debug("checkPassword({}) session is not secure", session); - } - return false; - } - PasswordAuthenticator auth = session.getPasswordAuthenticator(); if (auth == null) { if (debugEnabled) { @@ -132,7 +134,23 @@ public class UserAuthPassword extends AbstractUserAuth { protected Boolean handleClientPasswordChangeRequest( Buffer buffer, ServerSession session, String username, String oldPassword, String newPassword) throws Exception { - throw new UnsupportedOperationException("Password change not supported"); + boolean debugEnabled = log.isDebugEnabled(); + if (!UserAuthMethodFactory.isDataIntegrityAuthenticationTransport(session)) { + if (debugEnabled) { + log.debug("handleClientPasswordChangeRequest({}) session is not validated via MAC", session); + } + return false; + } + + PasswordAuthenticator auth = session.getPasswordAuthenticator(); + if (auth == null) { + if (debugEnabled) { + log.debug("handleClientPasswordChangeRequest({}) no password authenticator", session); + } + return false; + } + + return auth.handleClientPasswordChangeRequest(session, username, oldPassword, newPassword); } /** diff --git a/sshd-core/src/main/java/org/apache/sshd/server/jaas/JaasPasswordAuthenticator.java b/sshd-core/src/main/java/org/apache/sshd/server/jaas/JaasPasswordAuthenticator.java index 30d0d89..70e3594 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/jaas/JaasPasswordAuthenticator.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/jaas/JaasPasswordAuthenticator.java @@ -35,7 +35,6 @@ import org.apache.sshd.server.session.ServerSession; * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> */ public class JaasPasswordAuthenticator extends AbstractLoggingBean implements PasswordAuthenticator { - private String domain; public JaasPasswordAuthenticator() { @@ -55,14 +54,11 @@ public class JaasPasswordAuthenticator extends AbstractLoggingBean implements Pa } @Override - public boolean authenticate(String username, String password, ServerSession session) { - return authenticate(username, password); - } - - public boolean authenticate(final String username, final String password) { + public boolean authenticate( + String username, String password, ServerSession session) { try { Subject subject = new Subject(); - LoginContext loginContext = new LoginContext(domain, subject, callbacks -> { + LoginContext loginContext = new LoginContext(getDomain(), subject, callbacks -> { for (Callback callback : callbacks) { if (callback instanceof NameCallback) { ((NameCallback) callback).setName(username); @@ -77,7 +73,11 @@ public class JaasPasswordAuthenticator extends AbstractLoggingBean implements Pa loginContext.logout(); return true; } catch (Exception e) { - log.error("Authentication failed with error", e); + log.warn("authenticate({}) failed ({}) to authenticate user={}: {}", + session, e.getClass().getSimpleName(), username, e.getMessage()); + if (log.isDebugEnabled()) { + log.debug("authenticate(" + session + ")[" + username + "] failure details", e); + } return false; } } diff --git a/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTest.java b/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTest.java index 8082416..dd95792 100644 --- a/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTest.java @@ -174,37 +174,30 @@ public class AuthenticationTest extends BaseTestSupport { public void testChangePassword() throws Exception { PasswordAuthenticator delegate = sshd.getPasswordAuthenticator(); AtomicInteger attemptsCount = new AtomicInteger(0); - sshd.setPasswordAuthenticator((username, password, session) -> { - if (attemptsCount.incrementAndGet() == 1) { - throw new PasswordChangeRequiredException(attemptsCount.toString(), + AtomicInteger changesCount = new AtomicInteger(0); + sshd.setPasswordAuthenticator(new PasswordAuthenticator() { + @Override + public boolean authenticate(String username, String password, ServerSession session) { + if (attemptsCount.incrementAndGet() == 1) { + throw new PasswordChangeRequiredException(attemptsCount.toString(), getCurrentTestName(), ServerAuthenticationManager.DEFAULT_WELCOME_BANNER_LANGUAGE); - } + } - return delegate.authenticate(username, password, session); - }); + return delegate.authenticate(username, password, session); + } - AtomicInteger changesCount = new AtomicInteger(0); - sshd.setUserAuthFactories(Collections.singletonList( - new org.apache.sshd.server.auth.password.UserAuthPasswordFactory() { - @Override - public org.apache.sshd.server.auth.password.UserAuthPassword createUserAuth(ServerSession session) throws IOException { - return new org.apache.sshd.server.auth.password.UserAuthPassword() { - @Override - protected Boolean handleClientPasswordChangeRequest( - Buffer buffer, ServerSession session, String username, String oldPassword, String newPassword) - throws Exception { - if (changesCount.incrementAndGet() == 1) { - assertNotEquals("Non-different passwords", oldPassword, newPassword); - return checkPassword(buffer, session, username, newPassword); - } else { - return super.handleClientPasswordChangeRequest( - buffer, session, username, oldPassword, newPassword); - } - } - }; + @Override + public boolean handleClientPasswordChangeRequest( + ServerSession session, String username, String oldPassword, String newPassword) { + if (changesCount.incrementAndGet() == 1) { + assertNotEquals("Non-different passwords", oldPassword, newPassword); + return authenticate(username, newPassword, session); + } else { + return PasswordAuthenticator.super.handleClientPasswordChangeRequest( + session, username, oldPassword, newPassword); } } - )); + }); PropertyResolverUtils.updateProperty(sshd, ServerAuthenticationManager.AUTH_METHODS, UserAuthPasswordFactory.NAME); diff --git a/sshd-core/src/test/java/org/apache/sshd/server/jaas/JaasPasswordAuthenticatorTest.java b/sshd-core/src/test/java/org/apache/sshd/server/jaas/JaasPasswordAuthenticatorTest.java index cd0114e..f87a2f1 100644 --- a/sshd-core/src/test/java/org/apache/sshd/server/jaas/JaasPasswordAuthenticatorTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/server/jaas/JaasPasswordAuthenticatorTest.java @@ -82,11 +82,13 @@ public class JaasPasswordAuthenticatorTest extends BaseTestSupport { @Test public void testAuthenticator() { JaasPasswordAuthenticator auth = new JaasPasswordAuthenticator(); - assertNull(auth.getDomain()); + assertNull("Unexpected initial domain", auth.getDomain()); + auth.setDomain("domain"); - assertEquals("domain", auth.getDomain()); - assertTrue(auth.authenticate("sshd", "sshd")); - assertFalse(auth.authenticate("sshd", "dummy")); + assertEquals("Mismatched domain", "domain", auth.getDomain()); + + assertTrue(auth.authenticate("sshd", "sshd", null)); + assertFalse(auth.authenticate("sshd", "dummy", null)); } protected static class DummyLoginModule implements LoginModule { @@ -102,7 +104,9 @@ public class JaasPasswordAuthenticatorTest extends BaseTestSupport { } @Override - public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) { + public void initialize( + Subject subject, CallbackHandler callbackHandler, + Map<String, ?> sharedState, Map<String, ?> options) { this.subject = subject; this.callbackHandler = callbackHandler; } diff --git a/sshd-core/src/test/java/org/apache/sshd/util/test/BogusPasswordAuthenticator.java b/sshd-core/src/test/java/org/apache/sshd/util/test/BogusPasswordAuthenticator.java index 47df909..1382ac9 100644 --- a/sshd-core/src/test/java/org/apache/sshd/util/test/BogusPasswordAuthenticator.java +++ b/sshd-core/src/test/java/org/apache/sshd/util/test/BogusPasswordAuthenticator.java @@ -23,7 +23,8 @@ import org.apache.sshd.server.auth.password.PasswordAuthenticator; import org.apache.sshd.server.session.ServerSession; /** - * TODO Add javadoc + * A test {@link PasswordAuthenticator} that accepts an authentication + * attempt if the username is not {@code null} and same as password * * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> */ @@ -38,7 +39,8 @@ public class BogusPasswordAuthenticator extends AbstractLoggingBean implements P public boolean authenticate(String username, String password, ServerSession session) { boolean result = (username != null) && username.equals(password); if (log.isDebugEnabled()) { - log.debug("authenticate({}) {} / {} - success = {}", session, username, password, result); + log.debug("authenticate({}) {} / {} - success={}", + session, username, password, result); } return result; diff --git a/sshd-ldap/src/main/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticator.java b/sshd-ldap/src/main/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticator.java index 83165e7..6413d79 100644 --- a/sshd-ldap/src/main/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticator.java +++ b/sshd-ldap/src/main/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticator.java @@ -62,14 +62,14 @@ public class LdapPasswordAuthenticator extends LdapAuthenticator implements Pass } @Override - public boolean authenticate(String username, String password, ServerSession session) throws PasswordChangeRequiredException { + public boolean authenticate(String username, String password, ServerSession session) + throws PasswordChangeRequiredException { try { Map<String, ?> attrs = resolveAttributes(username, password, session); return authenticate(username, password, session, attrs); } catch (NamingException | RuntimeException e) { log.warn("authenticate({}@{}) failed ({}) to query: {}", - username, session, e.getClass().getSimpleName(), e.getMessage()); - + username, session, e.getClass().getSimpleName(), e.getMessage()); if (log.isDebugEnabled()) { log.debug("authenticate(" + username + "@" + session + ") query failure details", e); } @@ -78,7 +78,8 @@ public class LdapPasswordAuthenticator extends LdapAuthenticator implements Pass } } - protected boolean authenticate(String username, String password, ServerSession session, Map<String, ?> attrs) { + protected boolean authenticate( + String username, String password, ServerSession session, Map<String, ?> attrs) { /* * By default we assume that the user + password are the same for * accessing the LDAP as the user's account, so the very LDAP query