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 6f0513c18876476cc5602f1489597e27d7552fa6 Author: Lyor Goldstein <lgoldst...@apache.org> AuthorDate: Fri Jan 1 06:58:35 2021 +0200 [SSHD-1114] Split AuthenticationTest class into several classes --- .../sshd/common/auth/AuthenticationTest.java | 1046 +------------------- .../common/auth/AuthenticationTestSupport.java | 105 ++ .../common/auth/HostBasedAuthenticationTest.java | 150 +++ .../KeyboardInteractiveAuthenticationTest.java | 233 +++++ .../common/auth/PasswordAuthenticationTest.java | 440 ++++++++ .../common/auth/PublicKeyAuthenticationTest.java | 327 ++++++ .../sshd/util/test/server/TestServerSession.java | 39 + 7 files changed, 1295 insertions(+), 1045 deletions(-) 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 beb3141..d3aece6 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 @@ -19,556 +19,33 @@ package org.apache.sshd.common.auth; import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.security.GeneralSecurityException; import java.security.KeyPair; import java.security.PublicKey; -import java.security.spec.InvalidKeySpecException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.EnumSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; import org.apache.sshd.client.SshClient; -import org.apache.sshd.client.auth.hostbased.HostBasedAuthenticationReporter; -import org.apache.sshd.client.auth.hostbased.HostKeyIdentityProvider; -import org.apache.sshd.client.auth.keyboard.UserInteraction; -import org.apache.sshd.client.auth.password.PasswordAuthenticationReporter; -import org.apache.sshd.client.auth.password.PasswordIdentityProvider; -import org.apache.sshd.client.auth.pubkey.PublicKeyAuthenticationReporter; import org.apache.sshd.client.future.AuthFuture; import org.apache.sshd.client.session.ClientSession; -import org.apache.sshd.common.AttributeRepository; -import org.apache.sshd.common.NamedFactory; -import org.apache.sshd.common.NamedResource; -import org.apache.sshd.common.SshConstants; -import org.apache.sshd.common.SshException; -import org.apache.sshd.common.config.keys.FilePasswordProvider; import org.apache.sshd.common.config.keys.KeyUtils; -import org.apache.sshd.common.io.IoSession; -import org.apache.sshd.common.io.IoWriteFuture; import org.apache.sshd.common.kex.KeyExchange; -import org.apache.sshd.common.keyprovider.KeyIdentityProvider; import org.apache.sshd.common.keyprovider.KeyPairProvider; import org.apache.sshd.common.session.Session; -import org.apache.sshd.common.session.SessionContext; import org.apache.sshd.common.session.SessionListener; -import org.apache.sshd.common.signature.BuiltinSignatures; -import org.apache.sshd.common.signature.Signature; -import org.apache.sshd.common.util.GenericUtils; -import org.apache.sshd.common.util.MapEntryUtils.NavigableMapBuilder; -import org.apache.sshd.common.util.ValidateUtils; -import org.apache.sshd.common.util.buffer.Buffer; -import org.apache.sshd.common.util.io.resource.URLResource; -import org.apache.sshd.common.util.net.SshdSocketAddress; -import org.apache.sshd.common.util.security.SecurityUtils; -import org.apache.sshd.core.CoreModuleProperties; -import org.apache.sshd.server.ServerFactoryManager; -import org.apache.sshd.server.SshServer; -import org.apache.sshd.server.auth.hostbased.HostBasedAuthenticator; -import org.apache.sshd.server.auth.keyboard.DefaultKeyboardInteractiveAuthenticator; -import org.apache.sshd.server.auth.keyboard.InteractiveChallenge; import org.apache.sshd.server.auth.keyboard.KeyboardInteractiveAuthenticator; -import org.apache.sshd.server.auth.keyboard.PromptEntry; -import org.apache.sshd.server.auth.keyboard.UserAuthKeyboardInteractiveFactory; import org.apache.sshd.server.auth.password.PasswordAuthenticator; -import org.apache.sshd.server.auth.password.PasswordChangeRequiredException; -import org.apache.sshd.server.auth.password.RejectAllPasswordAuthenticator; -import org.apache.sshd.server.auth.password.UserAuthPasswordFactory; import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator; -import org.apache.sshd.server.auth.pubkey.RejectAllPublickeyAuthenticator; -import org.apache.sshd.server.session.ServerSession; -import org.apache.sshd.server.session.ServerSessionImpl; -import org.apache.sshd.server.session.SessionFactory; -import org.apache.sshd.util.test.BaseTestSupport; import org.apache.sshd.util.test.CommonTestSupportUtils; -import org.apache.sshd.util.test.CoreTestSupportUtils; -import org.junit.After; -import org.junit.Before; import org.junit.FixMethodOrder; import org.junit.Test; import org.junit.runners.MethodSorters; @FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class AuthenticationTest extends BaseTestSupport { - - private static final AttributeRepository.AttributeKey<Boolean> PASSWORD_ATTR = new AttributeRepository.AttributeKey<>(); - - private SshServer sshd; - private int port; - +public class AuthenticationTest extends AuthenticationTestSupport { public AuthenticationTest() { super(); } - @Before - public void setUp() throws Exception { - sshd = setupTestServer(); - sshd.setSessionFactory(new SessionFactory(sshd) { - @Override - protected ServerSessionImpl doCreateSession(IoSession ioSession) throws Exception { - return new TestSession(getServer(), ioSession); - } - }); - sshd.start(); - port = sshd.getPort(); - } - - @After - public void tearDown() throws Exception { - if (sshd != null) { - sshd.stop(true); - } - } - - @Test - public void testWrongPassword() throws Exception { - try (SshClient client = setupTestClient()) { - client.start(); - try (ClientSession s = client.connect("user", TEST_LOCALHOST, port) - .verify(CONNECT_TIMEOUT) - .getSession()) { - s.addPasswordIdentity("bad password"); - assertAuthenticationResult(getCurrentTestName(), s.auth(), false); - } - } - } - - @Test - public void testChangeUser() throws Exception { - try (SshClient client = setupTestClient()) { - client.start(); - - try (ClientSession s = client.connect(null, TEST_LOCALHOST, port) - .verify(CONNECT_TIMEOUT) - .getSession()) { - Collection<ClientSession.ClientSessionEvent> mask - = EnumSet.of(ClientSession.ClientSessionEvent.CLOSED, ClientSession.ClientSessionEvent.WAIT_AUTH); - Collection<ClientSession.ClientSessionEvent> result = s.waitFor(mask, DEFAULT_TIMEOUT); - assertFalse("Timeout while waiting on session events", - result.contains(ClientSession.ClientSessionEvent.TIMEOUT)); - - String password = "the-password"; - for (String username : new String[] { "user1", "user2" }) { - try { - assertAuthenticationResult(username, authPassword(s, username, password), false); - } finally { - s.removePasswordIdentity(password); - } - } - - // Note that WAIT_AUTH flag should be false, but since the internal - // authentication future is not updated, it's still returned - result = s.waitFor(EnumSet.of(ClientSession.ClientSessionEvent.CLOSED), DEFAULT_TIMEOUT); - assertTrue("Mismatched client session close mask: " + result, result.containsAll(mask)); - } finally { - client.stop(); - } - } - } - - @Test // see SSHD-196 - public void testChangePassword() throws Exception { - PasswordAuthenticator delegate = sshd.getPasswordAuthenticator(); - AtomicInteger attemptsCount = new AtomicInteger(0); - 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(), CoreModuleProperties.WELCOME_BANNER_LANGUAGE.getRequiredDefault()); - } - - return delegate.authenticate(username, password, session); - } - - @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); - } - } - }); - CoreModuleProperties.AUTH_METHODS.set(sshd, UserAuthPasswordFactory.NAME); - - try (SshClient client = setupTestClient()) { - AtomicInteger updatesCount = new AtomicInteger(0); - client.setUserInteraction(new UserInteraction() { - @Override - public boolean isInteractionAllowed(ClientSession session) { - return true; - } - - @Override - public String[] interactive( - ClientSession session, String name, String instruction, - String lang, String[] prompt, boolean[] echo) { - throw new UnsupportedOperationException("Unexpected call"); - } - - @Override - public String getUpdatedPassword(ClientSession session, String prompt, String lang) { - assertEquals("Mismatched prompt", getCurrentTestName(), prompt); - assertEquals("Mismatched language", - CoreModuleProperties.WELCOME_BANNER_LANGUAGE.getRequiredDefault(), lang); - assertEquals("Unexpected repeated call", 1, updatesCount.incrementAndGet()); - return getCurrentTestName(); - } - }); - - AtomicInteger sentCount = new AtomicInteger(0); - client.setUserAuthFactories(Collections.singletonList( - new org.apache.sshd.client.auth.password.UserAuthPasswordFactory() { - @Override - public org.apache.sshd.client.auth.password.UserAuthPassword createUserAuth(ClientSession session) - throws IOException { - return new org.apache.sshd.client.auth.password.UserAuthPassword() { - @Override - protected IoWriteFuture sendPassword( - Buffer buffer, ClientSession session, String oldPassword, String newPassword) - throws Exception { - int count = sentCount.incrementAndGet(); - // 1st one is the original one (which is denied by the server) - // 2nd one is the updated one retrieved from the user interaction - if (count == 2) { - return super.sendPassword(buffer, session, getClass().getName(), newPassword); - } else { - return super.sendPassword(buffer, session, oldPassword, newPassword); - } - } - }; - } - })); - CoreModuleProperties.AUTH_METHODS.set(client, UserAuthPasswordFactory.NAME); - - client.start(); - - try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port) - .verify(CONNECT_TIMEOUT) - .getSession()) { - s.addPasswordIdentity(getCurrentTestName()); - s.auth().verify(AUTH_TIMEOUT); - assertEquals("No password change request generated", 2, attemptsCount.get()); - assertEquals("No password change handled", 1, changesCount.get()); - assertEquals("No user interaction invoked", 1, updatesCount.get()); - } finally { - client.stop(); - } - } - } - - @Test - public void testAuthPasswordOnly() throws Exception { - try (SshClient client = setupTestClient()) { - sshd.setPasswordAuthenticator(RejectAllPasswordAuthenticator.INSTANCE); - - client.start(); - try (ClientSession s = client.connect(null, TEST_LOCALHOST, port) - .verify(CONNECT_TIMEOUT) - .getSession()) { - Collection<ClientSession.ClientSessionEvent> result = s.waitFor( - EnumSet.of(ClientSession.ClientSessionEvent.CLOSED, ClientSession.ClientSessionEvent.WAIT_AUTH), - DEFAULT_TIMEOUT); - assertFalse("Timeout while waiting for session", result.contains(ClientSession.ClientSessionEvent.TIMEOUT)); - - String password = getCurrentTestName(); - try { - assertAuthenticationResult(getCurrentTestName(), - authPassword(s, getCurrentTestName(), password), false); - } finally { - s.removePasswordIdentity(password); - } - } finally { - client.stop(); - } - } - } - - @Test - public void testAuthKeyPassword() throws Exception { - try (SshClient client = setupTestClient()) { - sshd.setPublickeyAuthenticator(RejectAllPublickeyAuthenticator.INSTANCE); - sshd.setKeyboardInteractiveAuthenticator(KeyboardInteractiveAuthenticator.NONE); - - client.start(); - - try (ClientSession s = client.connect(null, TEST_LOCALHOST, port) - .verify(CONNECT_TIMEOUT) - .getSession()) { - Collection<ClientSession.ClientSessionEvent> result = s.waitFor( - EnumSet.of(ClientSession.ClientSessionEvent.CLOSED, ClientSession.ClientSessionEvent.WAIT_AUTH), - DEFAULT_TIMEOUT); - assertFalse("Timeout while waiting for session", result.contains(ClientSession.ClientSessionEvent.TIMEOUT)); - - KeyPairProvider provider = createTestHostKeyProvider(); - KeyPair pair = provider.loadKey(s, CommonTestSupportUtils.DEFAULT_TEST_HOST_KEY_TYPE); - try { - assertAuthenticationResult(UserAuthMethodFactory.PUBLIC_KEY, - authPublicKey(s, getCurrentTestName(), pair), false); - } finally { - s.removePublicKeyIdentity(pair); - } - - String password = getCurrentTestName(); - try { - assertAuthenticationResult(UserAuthMethodFactory.PASSWORD, - authPassword(s, getCurrentTestName(), password), true); - } finally { - s.removePasswordIdentity(password); - } - } finally { - client.stop(); - } - } - } - - @Test // see SSHD-612 - public void testAuthDefaultKeyInteractive() throws Exception { - try (SshClient client = setupTestClient()) { - sshd.setPublickeyAuthenticator(RejectAllPublickeyAuthenticator.INSTANCE); - sshd.setKeyboardInteractiveAuthenticator(new DefaultKeyboardInteractiveAuthenticator() { - @Override - public InteractiveChallenge generateChallenge( - ServerSession session, String username, String lang, String subMethods) - throws Exception { - assertEquals("Mismatched user language", - CoreModuleProperties.INTERACTIVE_LANGUAGE_TAG.getRequired(client), - lang); - assertEquals("Mismatched client sub-methods", - CoreModuleProperties.INTERACTIVE_SUBMETHODS.getRequired(client), - subMethods); - - InteractiveChallenge challenge = super.generateChallenge(session, username, lang, subMethods); - assertEquals("Mismatched interaction name", getInteractionName(session), challenge.getInteractionName()); - assertEquals("Mismatched interaction instruction", getInteractionInstruction(session), - challenge.getInteractionInstruction()); - assertEquals("Mismatched language tag", getInteractionLanguage(session), challenge.getLanguageTag()); - - List<PromptEntry> entries = challenge.getPrompts(); - assertEquals("Mismatched prompts count", 1, GenericUtils.size(entries)); - - PromptEntry entry = entries.get(0); - assertEquals("Mismatched prompt", getInteractionPrompt(session), entry.getPrompt()); - assertEquals("Mismatched echo", isInteractionPromptEchoEnabled(session), entry.isEcho()); - - return challenge; - } - - @Override - public boolean authenticate( - ServerSession session, String username, List<String> responses) - throws Exception { - return super.authenticate(session, username, responses); - } - - }); - client.start(); - - try (ClientSession s = client.connect(null, TEST_LOCALHOST, port) - .verify(CONNECT_TIMEOUT) - .getSession()) { - Collection<ClientSession.ClientSessionEvent> result = s.waitFor( - EnumSet.of(ClientSession.ClientSessionEvent.CLOSED, ClientSession.ClientSessionEvent.WAIT_AUTH), - DEFAULT_TIMEOUT); - assertFalse("Timeout while waiting for session", result.contains(ClientSession.ClientSessionEvent.TIMEOUT)); - - KeyPairProvider provider = createTestHostKeyProvider(); - KeyPair pair = provider.loadKey(s, CommonTestSupportUtils.DEFAULT_TEST_HOST_KEY_TYPE); - try { - assertAuthenticationResult(UserAuthMethodFactory.PUBLIC_KEY, - authPublicKey(s, getCurrentTestName(), pair), false); - } finally { - s.removePublicKeyIdentity(pair); - } - - try { - assertAuthenticationResult(UserAuthMethodFactory.KB_INTERACTIVE, - authInteractive(s, getCurrentTestName(), getCurrentTestName()), true); - } finally { - s.setUserInteraction(null); - } - } finally { - client.stop(); - } - } - } - - @Test // see SSHD-563 - public void testAuthMultiChallengeKeyInteractive() throws Exception { - Class<?> anchor = getClass(); - InteractiveChallenge challenge = new InteractiveChallenge(); - challenge.setInteractionName(getCurrentTestName()); - challenge.setInteractionInstruction(anchor.getPackage().getName()); - challenge.setLanguageTag(Locale.getDefault().getLanguage()); - - Map<String, String> rspMap = NavigableMapBuilder.<String, String> builder(String.CASE_INSENSITIVE_ORDER) - .put("class", anchor.getSimpleName()) - .put("package", anchor.getPackage().getName()) - .put("test", getCurrentTestName()) - .build(); - for (String prompt : rspMap.keySet()) { - challenge.addPrompt(prompt, (GenericUtils.size(challenge.getPrompts()) & 0x1) != 0); - } - - CoreModuleProperties.AUTH_METHODS.set(sshd, UserAuthKeyboardInteractiveFactory.NAME); - AtomicInteger genCount = new AtomicInteger(0); - AtomicInteger authCount = new AtomicInteger(0); - sshd.setKeyboardInteractiveAuthenticator(new KeyboardInteractiveAuthenticator() { - @Override - public InteractiveChallenge generateChallenge( - ServerSession session, String username, String lang, String subMethods) - throws Exception { - assertEquals("Unexpected challenge call", 1, genCount.incrementAndGet()); - return challenge; - } - - @Override - public boolean authenticate( - ServerSession session, String username, List<String> responses) - throws Exception { - assertEquals("Unexpected authenticate call", 1, authCount.incrementAndGet()); - assertEquals("Mismatched number of responses", GenericUtils.size(rspMap), GenericUtils.size(responses)); - - int index = 0; - // Cannot use forEach because the index is not effectively final - for (Map.Entry<String, String> re : rspMap.entrySet()) { - String prompt = re.getKey(); - String expected = re.getValue(); - String actual = responses.get(index); - assertEquals("Mismatched response for prompt=" + prompt, expected, actual); - index++; - } - return true; - } - }); - CoreModuleProperties.AUTH_METHODS.set(sshd, UserAuthKeyboardInteractiveFactory.NAME); - - try (SshClient client = setupTestClient()) { - AtomicInteger interactiveCount = new AtomicInteger(0); - client.setUserInteraction(new UserInteraction() { - @Override - public boolean isInteractionAllowed(ClientSession session) { - return true; - } - - @Override - public String[] interactive( - ClientSession session, String name, String instruction, - String lang, String[] prompt, boolean[] echo) { - assertEquals("Unexpected multiple calls", 1, interactiveCount.incrementAndGet()); - assertEquals("Mismatched name", challenge.getInteractionName(), name); - assertEquals("Mismatched instruction", challenge.getInteractionInstruction(), instruction); - assertEquals("Mismatched language", challenge.getLanguageTag(), lang); - - List<PromptEntry> entries = challenge.getPrompts(); - assertEquals("Mismatched prompts count", GenericUtils.size(entries), GenericUtils.length(prompt)); - - String[] responses = new String[prompt.length]; - for (int index = 0; index < prompt.length; index++) { - PromptEntry e = entries.get(index); - String key = e.getPrompt(); - assertEquals("Mismatched prompt at index=" + index, key, prompt[index]); - assertEquals("Mismatched echo at index=" + index, e.isEcho(), echo[index]); - responses[index] = ValidateUtils.checkNotNull(rspMap.get(key), "No value for prompt=%s", key); - } - - return responses; - } - - @Override - public String getUpdatedPassword(ClientSession session, String prompt, String lang) { - throw new UnsupportedOperationException("Unexpected call"); - } - }); - CoreModuleProperties.AUTH_METHODS.set(client, UserAuthKeyboardInteractiveFactory.NAME); - - client.start(); - - try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port) - .verify(CONNECT_TIMEOUT) - .getSession()) { - s.auth().verify(AUTH_TIMEOUT); - assertEquals("Bad generated challenge count", 1, genCount.get()); - assertEquals("Bad authentication count", 1, authCount.get()); - assertEquals("Bad interactive count", 1, interactiveCount.get()); - } finally { - client.stop(); - } - } - } - - @Test // see SSHD-196 - public void testAuthPasswordChangeRequest() throws Exception { - PasswordAuthenticator delegate = Objects.requireNonNull(sshd.getPasswordAuthenticator(), "No password authenticator"); - AtomicInteger attemptsCount = new AtomicInteger(0); - sshd.setPasswordAuthenticator((username, password, session) -> { - if (attemptsCount.incrementAndGet() == 1) { - throw new PasswordChangeRequiredException( - attemptsCount.toString(), - getCurrentTestName(), CoreModuleProperties.WELCOME_BANNER_LANGUAGE.getRequiredDefault()); - } - - return delegate.authenticate(username, password, session); - }); - CoreModuleProperties.AUTH_METHODS.set(sshd, UserAuthPasswordFactory.NAME); - - try (SshClient client = setupTestClient()) { - AtomicInteger updatesCount = new AtomicInteger(0); - client.setUserInteraction(new UserInteraction() { - @Override - public boolean isInteractionAllowed(ClientSession session) { - return true; - } - - @Override - public String[] interactive( - ClientSession session, String name, String instruction, - String lang, String[] prompt, boolean[] echo) { - throw new UnsupportedOperationException("Unexpected call"); - } - - @Override - public String getUpdatedPassword(ClientSession session, String prompt, String lang) { - assertEquals("Mismatched prompt", getCurrentTestName(), prompt); - assertEquals("Mismatched language", - CoreModuleProperties.WELCOME_BANNER_LANGUAGE.getRequiredDefault(), lang); - assertEquals("Unexpected repeated call", 1, updatesCount.incrementAndGet()); - return getCurrentTestName(); - } - }); - CoreModuleProperties.AUTH_METHODS.set(client, UserAuthPasswordFactory.NAME); - - client.start(); - - try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port) - .verify(CONNECT_TIMEOUT) - .getSession()) { - s.addPasswordIdentity(getCurrentTestName()); - s.auth().verify(AUTH_TIMEOUT); - assertEquals("No password change request generated", 2, attemptsCount.get()); - assertEquals("No user interaction invoked", 1, updatesCount.get()); - } finally { - client.stop(); - } - } - } - @Test // see SSHD-600 public void testAuthExceptionPropagation() throws Exception { try (SshClient client = setupTestClient()) { @@ -619,257 +96,6 @@ public class AuthenticationTest extends BaseTestSupport { } } - @Test - public void testPasswordIdentityProviderPropagation() throws Exception { - try (SshClient client = setupTestClient()) { - List<String> passwords = Collections.singletonList(getCurrentTestName()); - AtomicInteger loadCount = new AtomicInteger(0); - PasswordIdentityProvider provider = () -> { - loadCount.incrementAndGet(); - outputDebugMessage("loadPasswords - count=%s", loadCount); - return passwords; - }; - client.setPasswordIdentityProvider(provider); - - client.start(); - try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port) - .verify(CONNECT_TIMEOUT) - .getSession()) { - s.auth().verify(AUTH_TIMEOUT); - assertEquals("Mismatched load passwords count", 1, loadCount.get()); - assertSame("Mismatched passwords identity provider", provider, s.getPasswordIdentityProvider()); - } finally { - client.stop(); - } - } - } - - @Test // see SSHD-618 - public void testPublicKeyAuthDifferentThanKex() throws Exception { - KeyPairProvider serverKeys = KeyPairProvider.wrap( - CommonTestSupportUtils.generateKeyPair(KeyUtils.RSA_ALGORITHM, 1024), - CommonTestSupportUtils.generateKeyPair(KeyUtils.DSS_ALGORITHM, 512), - CommonTestSupportUtils.generateKeyPair(KeyUtils.EC_ALGORITHM, 256)); - sshd.setKeyPairProvider(serverKeys); - sshd.setKeyboardInteractiveAuthenticator(KeyboardInteractiveAuthenticator.NONE); - sshd.setPasswordAuthenticator(RejectAllPasswordAuthenticator.INSTANCE); - - KeyPair clientIdentity = CommonTestSupportUtils.generateKeyPair(KeyUtils.EC_ALGORITHM, 256); - sshd.setPublickeyAuthenticator((username, key, session) -> { - String keyType = KeyUtils.getKeyType(key); - String expType = KeyUtils.getKeyType(clientIdentity); - assertEquals("Mismatched client key types", expType, keyType); - assertKeyEquals("Mismatched authentication public keys", clientIdentity.getPublic(), key); - return true; - }); - - // since we need to use RSA - CoreTestSupportUtils.setupFullSignaturesSupport(sshd); - try (SshClient client = setupTestClient()) { - // force server to use only RSA - NamedFactory<Signature> kexSignature = BuiltinSignatures.rsa; - client.setSignatureFactories(Collections.singletonList(kexSignature)); - client.setServerKeyVerifier((sshClientSession, remoteAddress, serverKey) -> { - String keyType = KeyUtils.getKeyType(serverKey); - String expType = kexSignature.getName(); - assertEquals("Mismatched server key type", expType, keyType); - - KeyPair kp; - try { - kp = ValidateUtils.checkNotNull(serverKeys.loadKey(null, keyType), "No server key for type=%s", keyType); - } catch (IOException | GeneralSecurityException e) { - throw new RuntimeException( - "Unexpected " + e.getClass().getSimpleName() + ")" - + " keys loading exception: " + e.getMessage(), - e); - } - assertKeyEquals("Mismatched server public keys", kp.getPublic(), serverKey); - return true; - }); - - // allow only EC keys for public key authentication - org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyFactory factory - = new org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyFactory(); - factory.setSignatureFactories( - Arrays.asList( - BuiltinSignatures.nistp256, BuiltinSignatures.nistp384, BuiltinSignatures.nistp521)); - client.setUserAuthFactories(Collections.singletonList(factory)); - - client.start(); - try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port) - .verify(CONNECT_TIMEOUT) - .getSession()) { - s.addPublicKeyIdentity(clientIdentity); - s.auth().verify(AUTH_TIMEOUT); - } finally { - client.stop(); - } - } - } - - @Test // see SSHD-624 - public void testMismatchedUserAuthPkOkData() throws Exception { - AtomicInteger challengeCounter = new AtomicInteger(0); - sshd.setUserAuthFactories(Collections.singletonList( - new org.apache.sshd.server.auth.pubkey.UserAuthPublicKeyFactory() { - @Override - public org.apache.sshd.server.auth.pubkey.UserAuthPublicKey createUserAuth(ServerSession session) - throws IOException { - return new org.apache.sshd.server.auth.pubkey.UserAuthPublicKey() { - @Override - protected void sendPublicKeyResponse( - ServerSession session, String username, String alg, PublicKey key, - byte[] keyBlob, int offset, int blobLen, Buffer buffer) - throws Exception { - int count = challengeCounter.incrementAndGet(); - outputDebugMessage("sendPublicKeyChallenge(%s)[%s]: count=%d", session, alg, count); - if (count == 1) { - // send wrong key type - super.sendPublicKeyResponse(session, username, - KeyPairProvider.SSH_DSS, key, keyBlob, offset, blobLen, buffer); - } else if (count == 2) { - // send another key - KeyPair otherPair = org.apache.sshd.util.test.CommonTestSupportUtils - .generateKeyPair(KeyUtils.RSA_ALGORITHM, 1024); - PublicKey otherKey = otherPair.getPublic(); - Buffer buf = session.createBuffer(SshConstants.SSH_MSG_USERAUTH_PK_OK, - blobLen + alg.length() + Long.SIZE); - buf.putString(alg); - buf.putPublicKey(otherKey); - session.writePacket(buf); - } else { - super.sendPublicKeyResponse(session, username, alg, key, keyBlob, offset, blobLen, buffer); - } - } - }; - } - - })); - - try (SshClient client = setupTestClient()) { - KeyPair clientIdentity = CommonTestSupportUtils.generateKeyPair( - CommonTestSupportUtils.DEFAULT_TEST_HOST_KEY_PROVIDER_ALGORITHM, - CommonTestSupportUtils.DEFAULT_TEST_HOST_KEY_SIZE); - client.start(); - - try { - for (int index = 1; index <= 4; index++) { - try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port) - .verify(CONNECT_TIMEOUT) - .getSession()) { - s.addPublicKeyIdentity(clientIdentity); - s.auth().verify(AUTH_TIMEOUT); - assertEquals("Mismatched number of challenges", 3, challengeCounter.get()); - break; - } catch (SshException e) { // expected - outputDebugMessage("%s on retry #%d: %s", e.getClass().getSimpleName(), index, e.getMessage()); - - Throwable t = e.getCause(); - assertObjectInstanceOf("Unexpected failure cause at retry #" + index, InvalidKeySpecException.class, t); - } - } - } finally { - client.stop(); - } - } - } - - @Test // see SSHD-620 - public void testHostBasedAuthentication() throws Exception { - AtomicInteger invocationCount = new AtomicInteger(0); - testHostBasedAuthentication( - ( - session, username, clientHostKey, clientHostName, clientUsername, - certificates) -> invocationCount.incrementAndGet() > 0, - session -> { - /* ignored */ }); - assertEquals("Mismatched authenticator invocation count", 1, invocationCount.get()); - } - - @Test // see SSHD-1114 - public void testHostBasedAuthenticationReporter() throws Exception { - AtomicReference<String> hostnameClientHolder = new AtomicReference<>(); - AtomicReference<String> usernameClientHolder = new AtomicReference<>(); - AtomicReference<PublicKey> keyClientHolder = new AtomicReference<>(); - HostBasedAuthenticator authenticator - = (session, username, clientHostKey, clientHostName, clientUsername, certificates) -> { - return Objects.equals(clientHostName, hostnameClientHolder.get()) - && Objects.equals(clientUsername, usernameClientHolder.get()) - && KeyUtils.compareKeys(clientHostKey, keyClientHolder.get()); - }; - - HostBasedAuthenticationReporter reporter = new HostBasedAuthenticationReporter() { - @Override - public void signalAuthenticationAttempt( - ClientSession session, String service, KeyPair identity, String hostname, String username, byte[] signature) - throws Exception { - hostnameClientHolder.set(hostname); - usernameClientHolder.set(username); - keyClientHolder.set(identity.getPublic()); - } - - @Override - public void signalAuthenticationSuccess( - ClientSession session, String service, KeyPair identity, String hostname, String username) - throws Exception { - assertEquals("Host", hostname, hostnameClientHolder.get()); - assertEquals("User", username, usernameClientHolder.get()); - assertKeyEquals("Identity", identity.getPublic(), keyClientHolder.get()); - } - - @Override - public void signalAuthenticationFailure( - ClientSession session, String service, KeyPair identity, - String hostname, String username, boolean partial, List<String> serverMethods) - throws Exception { - fail("Unexpected failure signalled"); - } - }; - - testHostBasedAuthentication(authenticator, session -> session.setHostBasedAuthenticationReporter(reporter)); - } - - private void testHostBasedAuthentication( - HostBasedAuthenticator delegate, Consumer<? super ClientSession> preAuthInitializer) - throws Exception { - String hostClientUser = getClass().getSimpleName(); - String hostClientName = SshdSocketAddress.toAddressString(SshdSocketAddress.getFirstExternalNetwork4Address()); - KeyPair hostClientKey = CommonTestSupportUtils.generateKeyPair( - CommonTestSupportUtils.DEFAULT_TEST_HOST_KEY_PROVIDER_ALGORITHM, - CommonTestSupportUtils.DEFAULT_TEST_HOST_KEY_SIZE); - sshd.setHostBasedAuthenticator((session, username, clientHostKey, clientHostName, clientUsername, certificates) -> { - return hostClientUser.equals(clientUsername) - && hostClientName.equals(clientHostName) - && KeyUtils.compareKeys(hostClientKey.getPublic(), clientHostKey) - && delegate.authenticate(session, username, clientHostKey, clientHostName, clientUsername, certificates); - }); - sshd.setPasswordAuthenticator(RejectAllPasswordAuthenticator.INSTANCE); - sshd.setKeyboardInteractiveAuthenticator(KeyboardInteractiveAuthenticator.NONE); - sshd.setPublickeyAuthenticator(RejectAllPublickeyAuthenticator.INSTANCE); - sshd.setUserAuthFactories( - Collections.singletonList( - org.apache.sshd.server.auth.hostbased.UserAuthHostBasedFactory.INSTANCE)); - - try (SshClient client = setupTestClient()) { - org.apache.sshd.client.auth.hostbased.UserAuthHostBasedFactory factory - = new org.apache.sshd.client.auth.hostbased.UserAuthHostBasedFactory(); - // TODO factory.setClientHostname(CLIENT_HOSTNAME); - factory.setClientUsername(hostClientUser); - factory.setClientHostKeys(HostKeyIdentityProvider.wrap(hostClientKey)); - - client.setUserAuthFactories(Collections.singletonList(factory)); - client.start(); - try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port) - .verify(CONNECT_TIMEOUT) - .getSession()) { - preAuthInitializer.accept(session); - session.auth().verify(AUTH_TIMEOUT); - } finally { - client.stop(); - } - } - } - @Test // see SSHD-625 public void testRuntimeErrorsInAuthenticators() throws Exception { Error thrown = new OutOfMemoryError(getCurrentTestName()); @@ -925,97 +151,6 @@ public class AuthenticationTest extends BaseTestSupport { } } - @Test // see SSHD-714 - public void testPasswordIdentityWithSpacesPrefixOrSuffix() throws Exception { - sshd.setPasswordAuthenticator((username, password, session) -> { - return (username != null) && (!username.trim().isEmpty()) - && (password != null) && (!password.isEmpty()) - && ((password.charAt(0) == ' ') || (password.charAt(password.length() - 1) == ' ')); - }); - - try (SshClient client = setupTestClient()) { - client.start(); - - try { - for (String password : new String[] { - " ", " ", " " + getCurrentTestName(), getCurrentTestName() + " " - }) { - try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port) - .verify(CONNECT_TIMEOUT) - .getSession()) { - s.addPasswordIdentity(password); - - AuthFuture auth = s.auth(); - assertTrue("No authentication result in time for password='" + password + "'", - auth.await(AUTH_TIMEOUT)); - assertTrue("Failed to authenticate with password='" + password + "'", auth.isSuccess()); - } - } - } finally { - client.stop(); - } - } - } - - @Test // see SSHD-862 - public void testSessionContextPropagatedToKeyFilePasswordProvider() throws Exception { - try (SshClient client = setupTestClient()) { - client.start(); - - try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port) - .verify(CONNECT_TIMEOUT) - .getSession()) { - String keyLocation = "super-secret-passphrase-ec256-key"; - FilePasswordProvider passwordProvider = new FilePasswordProvider() { - @Override - @SuppressWarnings("synthetic-access") - public String getPassword( - SessionContext session, NamedResource resourceKey, int retryIndex) - throws IOException { - assertSame("Mismatched session context", s, session); - assertEquals("Mismatched retry index", 0, retryIndex); - - String name = resourceKey.getName(); - int pos = name.lastIndexOf('/'); - if (pos >= 0) { - name = name.substring(pos + 1); - } - assertEquals("Mismatched location", keyLocation, name); - - Boolean passwordRequested = session.getAttribute(PASSWORD_ATTR); - assertNull("Password already requested", passwordRequested); - session.setAttribute(PASSWORD_ATTR, Boolean.TRUE); - return "super secret passphrase"; - } - }; - s.setKeyIdentityProvider(new KeyIdentityProvider() { - @Override - public Iterable<KeyPair> loadKeys(SessionContext session) throws IOException, GeneralSecurityException { - assertSame("Mismatched session context", s, session); - URL location = getClass().getResource(keyLocation); - assertNotNull("Missing key file " + keyLocation, location); - - URLResource resourceKey = new URLResource(location); - Iterable<KeyPair> ids; - try (InputStream keyData = resourceKey.openInputStream()) { - ids = SecurityUtils.loadKeyPairIdentities(session, resourceKey, keyData, passwordProvider); - } - KeyPair kp = GenericUtils.head(ids); - assertNotNull("No identity loaded from " + resourceKey, kp); - return Collections.singletonList(kp); - } - }); - s.auth().verify(AUTH_TIMEOUT); - - Boolean passwordRequested = s.getAttribute(PASSWORD_ATTR); - assertNotNull("Password provider not invoked", passwordRequested); - assertTrue("Password not requested", passwordRequested.booleanValue()); - } finally { - client.stop(); - } - } - } - @Test // see SSHD-1040 public void testServerKeyAvailableAfterAuth() throws Exception { KeyPairProvider keyPairProvider = sshd.getKeyPairProvider(); @@ -1050,183 +185,4 @@ public class AuthenticationTest extends BaseTestSupport { fail("No matching server key found for " + actualKey); } - - @Test // see SSHD-1114 - public void testPasswordAuthenticationReporter() throws Exception { - String goodPassword = getCurrentTestName(); - String badPassword = getClass().getSimpleName(); - List<String> attempted = new ArrayList<>(); - sshd.setPasswordAuthenticator((user, password, session) -> { - attempted.add(password); - return goodPassword.equals(password); - }); - sshd.setKeyboardInteractiveAuthenticator(KeyboardInteractiveAuthenticator.NONE); - sshd.setPublickeyAuthenticator(RejectAllPublickeyAuthenticator.INSTANCE); - - List<String> reported = new ArrayList<>(); - PasswordAuthenticationReporter reporter = new PasswordAuthenticationReporter() { - @Override - public void signalAuthenticationAttempt( - ClientSession session, String service, String oldPassword, boolean modified, String newPassword) - throws Exception { - reported.add(oldPassword); - } - - @Override - public void signalAuthenticationSuccess(ClientSession session, String service, String password) - throws Exception { - assertEquals("Mismatched succesful password", goodPassword, password); - } - - @Override - public void signalAuthenticationFailure( - ClientSession session, String service, String password, boolean partial, List<String> serverMethods) - throws Exception { - assertEquals("Mismatched failed password", badPassword, password); - } - }; - - try (SshClient client = setupTestClient()) { - client.setUserAuthFactories( - Collections.singletonList(new org.apache.sshd.client.auth.password.UserAuthPasswordFactory())); - client.start(); - - try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port) - .verify(CONNECT_TIMEOUT).getSession()) { - session.addPasswordIdentity(badPassword); - session.addPasswordIdentity(goodPassword); - session.setPasswordAuthenticationReporter(reporter); - session.auth().verify(AUTH_TIMEOUT); - } finally { - client.stop(); - } - } - - List<String> expected = Arrays.asList(badPassword, goodPassword); - assertListEquals("Attempted passwords", expected, attempted); - assertListEquals("Reported passwords", expected, reported); - } - - @Test // see SSHD-1114 - public void testPublicKeyAuthenticationReporter() throws Exception { - KeyPair goodIdentity = CommonTestSupportUtils.generateKeyPair(KeyUtils.EC_ALGORITHM, 256); - KeyPair badIdentity = CommonTestSupportUtils.generateKeyPair(KeyUtils.EC_ALGORITHM, 256); - List<PublicKey> attempted = new ArrayList<>(); - sshd.setPublickeyAuthenticator((username, key, session) -> { - attempted.add(key); - return KeyUtils.compareKeys(goodIdentity.getPublic(), key); - }); - sshd.setPasswordAuthenticator(RejectAllPasswordAuthenticator.INSTANCE); - sshd.setKeyboardInteractiveAuthenticator(KeyboardInteractiveAuthenticator.NONE); - - List<PublicKey> reported = new ArrayList<>(); - List<PublicKey> signed = new ArrayList<>(); - PublicKeyAuthenticationReporter reporter = new PublicKeyAuthenticationReporter() { - @Override - public void signalAuthenticationAttempt( - ClientSession session, String service, KeyPair identity, String signature) - throws Exception { - reported.add(identity.getPublic()); - } - - @Override - public void signalSignatureAttempt( - ClientSession session, String service, KeyPair identity, String signature, byte[] sigData) - throws Exception { - signed.add(identity.getPublic()); - } - - @Override - public void signalAuthenticationSuccess(ClientSession session, String service, KeyPair identity) - throws Exception { - assertTrue("Mismatched success identity", KeyUtils.compareKeys(goodIdentity.getPublic(), identity.getPublic())); - } - - @Override - public void signalAuthenticationFailure( - ClientSession session, String service, KeyPair identity, boolean partial, List<String> serverMethods) - throws Exception { - assertTrue("Mismatched failed identity", KeyUtils.compareKeys(badIdentity.getPublic(), identity.getPublic())); - } - }; - - try (SshClient client = setupTestClient()) { - client.setUserAuthFactories( - Collections.singletonList(new org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyFactory())); - client.start(); - - try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port) - .verify(CONNECT_TIMEOUT).getSession()) { - session.addPublicKeyIdentity(badIdentity); - session.addPublicKeyIdentity(goodIdentity); - session.setPublicKeyAuthenticationReporter(reporter); - session.auth().verify(AUTH_TIMEOUT); - } finally { - client.stop(); - } - } - - List<PublicKey> expected = Arrays.asList(badIdentity.getPublic(), goodIdentity.getPublic()); - // The server public key authenticator is called twice with the good identity - int numAttempted = attempted.size(); - assertKeyListEquals("Attempted", expected, (numAttempted > 0) ? attempted.subList(0, numAttempted - 1) : attempted); - assertKeyListEquals("Reported", expected, reported); - // The signing is attempted only if the initial public key is accepted - assertKeyListEquals("Signed", Collections.singletonList(goodIdentity.getPublic()), signed); - } - - private static void assertAuthenticationResult(String message, AuthFuture future, boolean expected) throws IOException { - assertTrue(message + ": failed to get result on time", future.await(AUTH_TIMEOUT)); - assertEquals(message + ": mismatched authentication result", expected, future.isSuccess()); - } - - private static AuthFuture authPassword(ClientSession s, String user, String pswd) throws IOException { - s.setUsername(user); - s.addPasswordIdentity(pswd); - return s.auth(); - } - - private static AuthFuture authInteractive(ClientSession s, String user, String pswd) throws IOException { - s.setUsername(user); - final String[] response = { pswd }; - s.setUserInteraction(new UserInteraction() { - @Override - public boolean isInteractionAllowed(ClientSession session) { - return true; - } - - @Override - public String[] interactive( - ClientSession session, String name, String instruction, - String lang, String[] prompt, boolean[] echo) { - assertSame("Mismatched session instance", s, session); - assertEquals("Mismatched prompt size", 1, GenericUtils.length(prompt)); - assertTrue("Mismatched prompt: " + prompt[0], prompt[0].toLowerCase().contains("password")); - return response; - } - - @Override - public String getUpdatedPassword(ClientSession session, String prompt, String lang) { - throw new UnsupportedOperationException("Unexpected password update request"); - } - }); - return s.auth(); - } - - private static AuthFuture authPublicKey(ClientSession s, String user, KeyPair pair) throws IOException { - s.setUsername(user); - s.addPublicKeyIdentity(pair); - return s.auth(); - } - - public static class TestSession extends ServerSessionImpl { - public TestSession(ServerFactoryManager server, IoSession ioSession) throws Exception { - super(server, ioSession); - } - - @Override - public void handleMessage(Buffer buffer) throws Exception { - super.handleMessage(buffer); // debug breakpoint - } - } } diff --git a/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTestSupport.java b/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTestSupport.java new file mode 100644 index 0000000..3fab5de --- /dev/null +++ b/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTestSupport.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sshd.common.auth; + +import java.io.IOException; +import java.security.KeyPair; + +import org.apache.sshd.client.auth.keyboard.UserInteraction; +import org.apache.sshd.client.future.AuthFuture; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.AttributeRepository; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.server.SshServer; +import org.apache.sshd.util.test.BaseTestSupport; +import org.junit.After; +import org.junit.Before; + +/** + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +public abstract class AuthenticationTestSupport extends BaseTestSupport { + protected static final AttributeRepository.AttributeKey<Boolean> PASSWORD_ATTR = new AttributeRepository.AttributeKey<>(); + + protected SshServer sshd; + protected int port; + + protected AuthenticationTestSupport() { + super(); + } + + @Before + public void setUp() throws Exception { + sshd = setupTestServer(); + sshd.start(); + port = sshd.getPort(); + } + + @After + public void tearDown() throws Exception { + if (sshd != null) { + sshd.stop(true); + } + } + + protected static void assertAuthenticationResult(String message, AuthFuture future, boolean expected) throws IOException { + assertTrue(message + ": failed to get result on time", future.await(AUTH_TIMEOUT)); + assertEquals(message + ": mismatched authentication result", expected, future.isSuccess()); + } + + protected static AuthFuture authPassword(ClientSession s, String user, String pswd) throws IOException { + s.setUsername(user); + s.addPasswordIdentity(pswd); + return s.auth(); + } + + protected static AuthFuture authInteractive(ClientSession s, String user, String pswd) throws IOException { + s.setUsername(user); + final String[] response = { pswd }; + s.setUserInteraction(new UserInteraction() { + @Override + public boolean isInteractionAllowed(ClientSession session) { + return true; + } + + @Override + public String[] interactive( + ClientSession session, String name, String instruction, + String lang, String[] prompt, boolean[] echo) { + assertSame("Mismatched session instance", s, session); + assertEquals("Mismatched prompt size", 1, GenericUtils.length(prompt)); + assertTrue("Mismatched prompt: " + prompt[0], prompt[0].toLowerCase().contains("password")); + return response; + } + + @Override + public String getUpdatedPassword(ClientSession session, String prompt, String lang) { + throw new UnsupportedOperationException("Unexpected password update request"); + } + }); + return s.auth(); + } + + protected static AuthFuture authPublicKey(ClientSession s, String user, KeyPair pair) throws IOException { + s.setUsername(user); + s.addPublicKeyIdentity(pair); + return s.auth(); + } +} diff --git a/sshd-core/src/test/java/org/apache/sshd/common/auth/HostBasedAuthenticationTest.java b/sshd-core/src/test/java/org/apache/sshd/common/auth/HostBasedAuthenticationTest.java new file mode 100644 index 0000000..7568bc8 --- /dev/null +++ b/sshd-core/src/test/java/org/apache/sshd/common/auth/HostBasedAuthenticationTest.java @@ -0,0 +1,150 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sshd.common.auth; + +import java.security.KeyPair; +import java.security.PublicKey; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.auth.hostbased.HostBasedAuthenticationReporter; +import org.apache.sshd.client.auth.hostbased.HostKeyIdentityProvider; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.util.net.SshdSocketAddress; +import org.apache.sshd.server.auth.hostbased.HostBasedAuthenticator; +import org.apache.sshd.server.auth.keyboard.KeyboardInteractiveAuthenticator; +import org.apache.sshd.server.auth.password.RejectAllPasswordAuthenticator; +import org.apache.sshd.server.auth.pubkey.RejectAllPublickeyAuthenticator; +import org.apache.sshd.util.test.CommonTestSupportUtils; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runners.MethodSorters; + +/** + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class HostBasedAuthenticationTest extends AuthenticationTestSupport { + public HostBasedAuthenticationTest() { + super(); + } + + @Test // see SSHD-620 + public void testHostBasedAuthentication() throws Exception { + AtomicInteger invocationCount = new AtomicInteger(0); + testHostBasedAuthentication( + ( + session, username, clientHostKey, clientHostName, clientUsername, + certificates) -> invocationCount.incrementAndGet() > 0, + session -> { + /* ignored */ }); + assertEquals("Mismatched authenticator invocation count", 1, invocationCount.get()); + } + + @Test // see SSHD-1114 + public void testHostBasedAuthenticationReporter() throws Exception { + AtomicReference<String> hostnameClientHolder = new AtomicReference<>(); + AtomicReference<String> usernameClientHolder = new AtomicReference<>(); + AtomicReference<PublicKey> keyClientHolder = new AtomicReference<>(); + HostBasedAuthenticator authenticator + = (session, username, clientHostKey, clientHostName, clientUsername, certificates) -> { + return Objects.equals(clientHostName, hostnameClientHolder.get()) + && Objects.equals(clientUsername, usernameClientHolder.get()) + && KeyUtils.compareKeys(clientHostKey, keyClientHolder.get()); + }; + + HostBasedAuthenticationReporter reporter = new HostBasedAuthenticationReporter() { + @Override + public void signalAuthenticationAttempt( + ClientSession session, String service, KeyPair identity, String hostname, String username, byte[] signature) + throws Exception { + hostnameClientHolder.set(hostname); + usernameClientHolder.set(username); + keyClientHolder.set(identity.getPublic()); + } + + @Override + public void signalAuthenticationSuccess( + ClientSession session, String service, KeyPair identity, String hostname, String username) + throws Exception { + assertEquals("Host", hostname, hostnameClientHolder.get()); + assertEquals("User", username, usernameClientHolder.get()); + assertKeyEquals("Identity", identity.getPublic(), keyClientHolder.get()); + } + + @Override + public void signalAuthenticationFailure( + ClientSession session, String service, KeyPair identity, + String hostname, String username, boolean partial, List<String> serverMethods) + throws Exception { + fail("Unexpected failure signalled"); + } + }; + + testHostBasedAuthentication(authenticator, session -> session.setHostBasedAuthenticationReporter(reporter)); + } + + private void testHostBasedAuthentication( + HostBasedAuthenticator delegate, Consumer<? super ClientSession> preAuthInitializer) + throws Exception { + String hostClientUser = getClass().getSimpleName(); + String hostClientName = SshdSocketAddress.toAddressString(SshdSocketAddress.getFirstExternalNetwork4Address()); + KeyPair hostClientKey = CommonTestSupportUtils.generateKeyPair( + CommonTestSupportUtils.DEFAULT_TEST_HOST_KEY_PROVIDER_ALGORITHM, + CommonTestSupportUtils.DEFAULT_TEST_HOST_KEY_SIZE); + sshd.setHostBasedAuthenticator((session, username, clientHostKey, clientHostName, clientUsername, certificates) -> { + return hostClientUser.equals(clientUsername) + && hostClientName.equals(clientHostName) + && KeyUtils.compareKeys(hostClientKey.getPublic(), clientHostKey) + && delegate.authenticate(session, username, clientHostKey, clientHostName, clientUsername, certificates); + }); + sshd.setPasswordAuthenticator(RejectAllPasswordAuthenticator.INSTANCE); + sshd.setKeyboardInteractiveAuthenticator(KeyboardInteractiveAuthenticator.NONE); + sshd.setPublickeyAuthenticator(RejectAllPublickeyAuthenticator.INSTANCE); + sshd.setUserAuthFactories( + Collections.singletonList( + org.apache.sshd.server.auth.hostbased.UserAuthHostBasedFactory.INSTANCE)); + + try (SshClient client = setupTestClient()) { + org.apache.sshd.client.auth.hostbased.UserAuthHostBasedFactory factory + = new org.apache.sshd.client.auth.hostbased.UserAuthHostBasedFactory(); + // TODO factory.setClientHostname(CLIENT_HOSTNAME); + factory.setClientUsername(hostClientUser); + factory.setClientHostKeys(HostKeyIdentityProvider.wrap(hostClientKey)); + + client.setUserAuthFactories(Collections.singletonList(factory)); + client.start(); + try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port) + .verify(CONNECT_TIMEOUT) + .getSession()) { + preAuthInitializer.accept(session); + session.auth().verify(AUTH_TIMEOUT); + } finally { + client.stop(); + } + } + } +} diff --git a/sshd-core/src/test/java/org/apache/sshd/common/auth/KeyboardInteractiveAuthenticationTest.java b/sshd-core/src/test/java/org/apache/sshd/common/auth/KeyboardInteractiveAuthenticationTest.java new file mode 100644 index 0000000..feeabf3 --- /dev/null +++ b/sshd-core/src/test/java/org/apache/sshd/common/auth/KeyboardInteractiveAuthenticationTest.java @@ -0,0 +1,233 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sshd.common.auth; + +import java.security.KeyPair; +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.auth.keyboard.UserInteraction; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.keyprovider.KeyPairProvider; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.MapEntryUtils.NavigableMapBuilder; +import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.core.CoreModuleProperties; +import org.apache.sshd.server.auth.keyboard.DefaultKeyboardInteractiveAuthenticator; +import org.apache.sshd.server.auth.keyboard.InteractiveChallenge; +import org.apache.sshd.server.auth.keyboard.KeyboardInteractiveAuthenticator; +import org.apache.sshd.server.auth.keyboard.PromptEntry; +import org.apache.sshd.server.auth.keyboard.UserAuthKeyboardInteractiveFactory; +import org.apache.sshd.server.auth.pubkey.RejectAllPublickeyAuthenticator; +import org.apache.sshd.server.session.ServerSession; +import org.apache.sshd.util.test.CommonTestSupportUtils; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runners.MethodSorters; + +/** + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class KeyboardInteractiveAuthenticationTest extends AuthenticationTestSupport { + public KeyboardInteractiveAuthenticationTest() { + super(); + } + + @Test // see SSHD-612 + public void testAuthDefaultKeyInteractive() throws Exception { + try (SshClient client = setupTestClient()) { + sshd.setPublickeyAuthenticator(RejectAllPublickeyAuthenticator.INSTANCE); + sshd.setKeyboardInteractiveAuthenticator(new DefaultKeyboardInteractiveAuthenticator() { + @Override + public InteractiveChallenge generateChallenge( + ServerSession session, String username, String lang, String subMethods) + throws Exception { + assertEquals("Mismatched user language", + CoreModuleProperties.INTERACTIVE_LANGUAGE_TAG.getRequired(client), + lang); + assertEquals("Mismatched client sub-methods", + CoreModuleProperties.INTERACTIVE_SUBMETHODS.getRequired(client), + subMethods); + + InteractiveChallenge challenge = super.generateChallenge(session, username, lang, subMethods); + assertEquals("Mismatched interaction name", getInteractionName(session), challenge.getInteractionName()); + assertEquals("Mismatched interaction instruction", getInteractionInstruction(session), + challenge.getInteractionInstruction()); + assertEquals("Mismatched language tag", getInteractionLanguage(session), challenge.getLanguageTag()); + + List<PromptEntry> entries = challenge.getPrompts(); + assertEquals("Mismatched prompts count", 1, GenericUtils.size(entries)); + + PromptEntry entry = entries.get(0); + assertEquals("Mismatched prompt", getInteractionPrompt(session), entry.getPrompt()); + assertEquals("Mismatched echo", isInteractionPromptEchoEnabled(session), entry.isEcho()); + + return challenge; + } + + @Override + public boolean authenticate( + ServerSession session, String username, List<String> responses) + throws Exception { + return super.authenticate(session, username, responses); + } + + }); + client.start(); + + try (ClientSession s = client.connect(null, TEST_LOCALHOST, port) + .verify(CONNECT_TIMEOUT) + .getSession()) { + Collection<ClientSession.ClientSessionEvent> result = s.waitFor( + EnumSet.of(ClientSession.ClientSessionEvent.CLOSED, ClientSession.ClientSessionEvent.WAIT_AUTH), + DEFAULT_TIMEOUT); + assertFalse("Timeout while waiting for session", result.contains(ClientSession.ClientSessionEvent.TIMEOUT)); + + KeyPairProvider provider = createTestHostKeyProvider(); + KeyPair pair = provider.loadKey(s, CommonTestSupportUtils.DEFAULT_TEST_HOST_KEY_TYPE); + try { + assertAuthenticationResult(UserAuthMethodFactory.PUBLIC_KEY, + authPublicKey(s, getCurrentTestName(), pair), false); + } finally { + s.removePublicKeyIdentity(pair); + } + + try { + assertAuthenticationResult(UserAuthMethodFactory.KB_INTERACTIVE, + authInteractive(s, getCurrentTestName(), getCurrentTestName()), true); + } finally { + s.setUserInteraction(null); + } + } finally { + client.stop(); + } + } + } + + @Test // see SSHD-563 + public void testAuthMultiChallengeKeyInteractive() throws Exception { + Class<?> anchor = getClass(); + InteractiveChallenge challenge = new InteractiveChallenge(); + challenge.setInteractionName(getCurrentTestName()); + challenge.setInteractionInstruction(anchor.getPackage().getName()); + challenge.setLanguageTag(Locale.getDefault().getLanguage()); + + Map<String, String> rspMap = NavigableMapBuilder.<String, String> builder(String.CASE_INSENSITIVE_ORDER) + .put("class", anchor.getSimpleName()) + .put("package", anchor.getPackage().getName()) + .put("test", getCurrentTestName()) + .build(); + for (String prompt : rspMap.keySet()) { + challenge.addPrompt(prompt, (GenericUtils.size(challenge.getPrompts()) & 0x1) != 0); + } + + CoreModuleProperties.AUTH_METHODS.set(sshd, UserAuthKeyboardInteractiveFactory.NAME); + AtomicInteger genCount = new AtomicInteger(0); + AtomicInteger authCount = new AtomicInteger(0); + sshd.setKeyboardInteractiveAuthenticator(new KeyboardInteractiveAuthenticator() { + @Override + public InteractiveChallenge generateChallenge( + ServerSession session, String username, String lang, String subMethods) + throws Exception { + assertEquals("Unexpected challenge call", 1, genCount.incrementAndGet()); + return challenge; + } + + @Override + public boolean authenticate( + ServerSession session, String username, List<String> responses) + throws Exception { + assertEquals("Unexpected authenticate call", 1, authCount.incrementAndGet()); + assertEquals("Mismatched number of responses", GenericUtils.size(rspMap), GenericUtils.size(responses)); + + int index = 0; + // Cannot use forEach because the index is not effectively final + for (Map.Entry<String, String> re : rspMap.entrySet()) { + String prompt = re.getKey(); + String expected = re.getValue(); + String actual = responses.get(index); + assertEquals("Mismatched response for prompt=" + prompt, expected, actual); + index++; + } + return true; + } + }); + CoreModuleProperties.AUTH_METHODS.set(sshd, UserAuthKeyboardInteractiveFactory.NAME); + + try (SshClient client = setupTestClient()) { + AtomicInteger interactiveCount = new AtomicInteger(0); + client.setUserInteraction(new UserInteraction() { + @Override + public boolean isInteractionAllowed(ClientSession session) { + return true; + } + + @Override + public String[] interactive( + ClientSession session, String name, String instruction, + String lang, String[] prompt, boolean[] echo) { + assertEquals("Unexpected multiple calls", 1, interactiveCount.incrementAndGet()); + assertEquals("Mismatched name", challenge.getInteractionName(), name); + assertEquals("Mismatched instruction", challenge.getInteractionInstruction(), instruction); + assertEquals("Mismatched language", challenge.getLanguageTag(), lang); + + List<PromptEntry> entries = challenge.getPrompts(); + assertEquals("Mismatched prompts count", GenericUtils.size(entries), GenericUtils.length(prompt)); + + String[] responses = new String[prompt.length]; + for (int index = 0; index < prompt.length; index++) { + PromptEntry e = entries.get(index); + String key = e.getPrompt(); + assertEquals("Mismatched prompt at index=" + index, key, prompt[index]); + assertEquals("Mismatched echo at index=" + index, e.isEcho(), echo[index]); + responses[index] = ValidateUtils.checkNotNull(rspMap.get(key), "No value for prompt=%s", key); + } + + return responses; + } + + @Override + public String getUpdatedPassword(ClientSession session, String prompt, String lang) { + throw new UnsupportedOperationException("Unexpected call"); + } + }); + CoreModuleProperties.AUTH_METHODS.set(client, UserAuthKeyboardInteractiveFactory.NAME); + + client.start(); + + try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port) + .verify(CONNECT_TIMEOUT) + .getSession()) { + s.auth().verify(AUTH_TIMEOUT); + assertEquals("Bad generated challenge count", 1, genCount.get()); + assertEquals("Bad authentication count", 1, authCount.get()); + assertEquals("Bad interactive count", 1, interactiveCount.get()); + } finally { + client.stop(); + } + } + } +} diff --git a/sshd-core/src/test/java/org/apache/sshd/common/auth/PasswordAuthenticationTest.java b/sshd-core/src/test/java/org/apache/sshd/common/auth/PasswordAuthenticationTest.java new file mode 100644 index 0000000..628338d --- /dev/null +++ b/sshd-core/src/test/java/org/apache/sshd/common/auth/PasswordAuthenticationTest.java @@ -0,0 +1,440 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sshd.common.auth; + +import java.io.IOException; +import java.security.KeyPair; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.auth.keyboard.UserInteraction; +import org.apache.sshd.client.auth.password.PasswordAuthenticationReporter; +import org.apache.sshd.client.auth.password.PasswordIdentityProvider; +import org.apache.sshd.client.future.AuthFuture; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.io.IoWriteFuture; +import org.apache.sshd.common.keyprovider.KeyPairProvider; +import org.apache.sshd.common.util.buffer.Buffer; +import org.apache.sshd.core.CoreModuleProperties; +import org.apache.sshd.server.auth.keyboard.KeyboardInteractiveAuthenticator; +import org.apache.sshd.server.auth.password.PasswordAuthenticator; +import org.apache.sshd.server.auth.password.PasswordChangeRequiredException; +import org.apache.sshd.server.auth.password.RejectAllPasswordAuthenticator; +import org.apache.sshd.server.auth.password.UserAuthPasswordFactory; +import org.apache.sshd.server.auth.pubkey.RejectAllPublickeyAuthenticator; +import org.apache.sshd.server.session.ServerSession; +import org.apache.sshd.util.test.CommonTestSupportUtils; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runners.MethodSorters; + +/** + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class PasswordAuthenticationTest extends AuthenticationTestSupport { + public PasswordAuthenticationTest() { + super(); + } + + @Test + public void testWrongPassword() throws Exception { + try (SshClient client = setupTestClient()) { + client.start(); + try (ClientSession s = client.connect("user", TEST_LOCALHOST, port) + .verify(CONNECT_TIMEOUT) + .getSession()) { + s.addPasswordIdentity("bad password"); + assertAuthenticationResult(getCurrentTestName(), s.auth(), false); + } + } + } + + @Test + public void testChangeUser() throws Exception { + try (SshClient client = setupTestClient()) { + client.start(); + + try (ClientSession s = client.connect(null, TEST_LOCALHOST, port) + .verify(CONNECT_TIMEOUT) + .getSession()) { + Collection<ClientSession.ClientSessionEvent> mask + = EnumSet.of(ClientSession.ClientSessionEvent.CLOSED, ClientSession.ClientSessionEvent.WAIT_AUTH); + Collection<ClientSession.ClientSessionEvent> result = s.waitFor(mask, DEFAULT_TIMEOUT); + assertFalse("Timeout while waiting on session events", + result.contains(ClientSession.ClientSessionEvent.TIMEOUT)); + + String password = "the-password"; + for (String username : new String[] { "user1", "user2" }) { + try { + assertAuthenticationResult(username, authPassword(s, username, password), false); + } finally { + s.removePasswordIdentity(password); + } + } + + // Note that WAIT_AUTH flag should be false, but since the internal + // authentication future is not updated, it's still returned + result = s.waitFor(EnumSet.of(ClientSession.ClientSessionEvent.CLOSED), DEFAULT_TIMEOUT); + assertTrue("Mismatched client session close mask: " + result, result.containsAll(mask)); + } finally { + client.stop(); + } + } + } + + @Test // see SSHD-196 + public void testChangePassword() throws Exception { + PasswordAuthenticator delegate = sshd.getPasswordAuthenticator(); + AtomicInteger attemptsCount = new AtomicInteger(0); + 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(), CoreModuleProperties.WELCOME_BANNER_LANGUAGE.getRequiredDefault()); + } + + return delegate.authenticate(username, password, session); + } + + @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); + } + } + }); + CoreModuleProperties.AUTH_METHODS.set(sshd, UserAuthPasswordFactory.NAME); + + try (SshClient client = setupTestClient()) { + AtomicInteger updatesCount = new AtomicInteger(0); + client.setUserInteraction(new UserInteraction() { + @Override + public boolean isInteractionAllowed(ClientSession session) { + return true; + } + + @Override + public String[] interactive( + ClientSession session, String name, String instruction, + String lang, String[] prompt, boolean[] echo) { + throw new UnsupportedOperationException("Unexpected call"); + } + + @Override + public String getUpdatedPassword(ClientSession session, String prompt, String lang) { + assertEquals("Mismatched prompt", getCurrentTestName(), prompt); + assertEquals("Mismatched language", + CoreModuleProperties.WELCOME_BANNER_LANGUAGE.getRequiredDefault(), lang); + assertEquals("Unexpected repeated call", 1, updatesCount.incrementAndGet()); + return getCurrentTestName(); + } + }); + + AtomicInteger sentCount = new AtomicInteger(0); + client.setUserAuthFactories(Collections.singletonList( + new org.apache.sshd.client.auth.password.UserAuthPasswordFactory() { + @Override + public org.apache.sshd.client.auth.password.UserAuthPassword createUserAuth(ClientSession session) + throws IOException { + return new org.apache.sshd.client.auth.password.UserAuthPassword() { + @Override + protected IoWriteFuture sendPassword( + Buffer buffer, ClientSession session, String oldPassword, String newPassword) + throws Exception { + int count = sentCount.incrementAndGet(); + // 1st one is the original one (which is denied by the server) + // 2nd one is the updated one retrieved from the user interaction + if (count == 2) { + return super.sendPassword(buffer, session, getClass().getName(), newPassword); + } else { + return super.sendPassword(buffer, session, oldPassword, newPassword); + } + } + }; + } + })); + CoreModuleProperties.AUTH_METHODS.set(client, UserAuthPasswordFactory.NAME); + + client.start(); + + try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port) + .verify(CONNECT_TIMEOUT) + .getSession()) { + s.addPasswordIdentity(getCurrentTestName()); + s.auth().verify(AUTH_TIMEOUT); + assertEquals("No password change request generated", 2, attemptsCount.get()); + assertEquals("No password change handled", 1, changesCount.get()); + assertEquals("No user interaction invoked", 1, updatesCount.get()); + } finally { + client.stop(); + } + } + } + + @Test + public void testAuthPasswordOnly() throws Exception { + try (SshClient client = setupTestClient()) { + sshd.setPasswordAuthenticator(RejectAllPasswordAuthenticator.INSTANCE); + + client.start(); + try (ClientSession s = client.connect(null, TEST_LOCALHOST, port) + .verify(CONNECT_TIMEOUT) + .getSession()) { + Collection<ClientSession.ClientSessionEvent> result = s.waitFor( + EnumSet.of(ClientSession.ClientSessionEvent.CLOSED, ClientSession.ClientSessionEvent.WAIT_AUTH), + DEFAULT_TIMEOUT); + assertFalse("Timeout while waiting for session", result.contains(ClientSession.ClientSessionEvent.TIMEOUT)); + + String password = getCurrentTestName(); + try { + assertAuthenticationResult(getCurrentTestName(), + authPassword(s, getCurrentTestName(), password), false); + } finally { + s.removePasswordIdentity(password); + } + } finally { + client.stop(); + } + } + } + + @Test + public void testAuthKeyPassword() throws Exception { + try (SshClient client = setupTestClient()) { + sshd.setPublickeyAuthenticator(RejectAllPublickeyAuthenticator.INSTANCE); + sshd.setKeyboardInteractiveAuthenticator(KeyboardInteractiveAuthenticator.NONE); + + client.start(); + + try (ClientSession s = client.connect(null, TEST_LOCALHOST, port) + .verify(CONNECT_TIMEOUT) + .getSession()) { + Collection<ClientSession.ClientSessionEvent> result = s.waitFor( + EnumSet.of(ClientSession.ClientSessionEvent.CLOSED, ClientSession.ClientSessionEvent.WAIT_AUTH), + DEFAULT_TIMEOUT); + assertFalse("Timeout while waiting for session", result.contains(ClientSession.ClientSessionEvent.TIMEOUT)); + + KeyPairProvider provider = createTestHostKeyProvider(); + KeyPair pair = provider.loadKey(s, CommonTestSupportUtils.DEFAULT_TEST_HOST_KEY_TYPE); + try { + assertAuthenticationResult(UserAuthMethodFactory.PUBLIC_KEY, + authPublicKey(s, getCurrentTestName(), pair), false); + } finally { + s.removePublicKeyIdentity(pair); + } + + String password = getCurrentTestName(); + try { + assertAuthenticationResult(UserAuthMethodFactory.PASSWORD, + authPassword(s, getCurrentTestName(), password), true); + } finally { + s.removePasswordIdentity(password); + } + } finally { + client.stop(); + } + } + } + + @Test // see SSHD-196 + public void testAuthPasswordChangeRequest() throws Exception { + PasswordAuthenticator delegate = Objects.requireNonNull(sshd.getPasswordAuthenticator(), "No password authenticator"); + AtomicInteger attemptsCount = new AtomicInteger(0); + sshd.setPasswordAuthenticator((username, password, session) -> { + if (attemptsCount.incrementAndGet() == 1) { + throw new PasswordChangeRequiredException( + attemptsCount.toString(), + getCurrentTestName(), CoreModuleProperties.WELCOME_BANNER_LANGUAGE.getRequiredDefault()); + } + + return delegate.authenticate(username, password, session); + }); + CoreModuleProperties.AUTH_METHODS.set(sshd, UserAuthPasswordFactory.NAME); + + try (SshClient client = setupTestClient()) { + AtomicInteger updatesCount = new AtomicInteger(0); + client.setUserInteraction(new UserInteraction() { + @Override + public boolean isInteractionAllowed(ClientSession session) { + return true; + } + + @Override + public String[] interactive( + ClientSession session, String name, String instruction, + String lang, String[] prompt, boolean[] echo) { + throw new UnsupportedOperationException("Unexpected call"); + } + + @Override + public String getUpdatedPassword(ClientSession session, String prompt, String lang) { + assertEquals("Mismatched prompt", getCurrentTestName(), prompt); + assertEquals("Mismatched language", + CoreModuleProperties.WELCOME_BANNER_LANGUAGE.getRequiredDefault(), lang); + assertEquals("Unexpected repeated call", 1, updatesCount.incrementAndGet()); + return getCurrentTestName(); + } + }); + CoreModuleProperties.AUTH_METHODS.set(client, UserAuthPasswordFactory.NAME); + + client.start(); + + try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port) + .verify(CONNECT_TIMEOUT) + .getSession()) { + s.addPasswordIdentity(getCurrentTestName()); + s.auth().verify(AUTH_TIMEOUT); + assertEquals("No password change request generated", 2, attemptsCount.get()); + assertEquals("No user interaction invoked", 1, updatesCount.get()); + } finally { + client.stop(); + } + } + } + + @Test + public void testPasswordIdentityProviderPropagation() throws Exception { + try (SshClient client = setupTestClient()) { + List<String> passwords = Collections.singletonList(getCurrentTestName()); + AtomicInteger loadCount = new AtomicInteger(0); + PasswordIdentityProvider provider = () -> { + loadCount.incrementAndGet(); + outputDebugMessage("loadPasswords - count=%s", loadCount); + return passwords; + }; + client.setPasswordIdentityProvider(provider); + + client.start(); + try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port) + .verify(CONNECT_TIMEOUT) + .getSession()) { + s.auth().verify(AUTH_TIMEOUT); + assertEquals("Mismatched load passwords count", 1, loadCount.get()); + assertSame("Mismatched passwords identity provider", provider, s.getPasswordIdentityProvider()); + } finally { + client.stop(); + } + } + } + + @Test // see SSHD-714 + public void testPasswordIdentityWithSpacesPrefixOrSuffix() throws Exception { + sshd.setPasswordAuthenticator((username, password, session) -> { + return (username != null) && (!username.trim().isEmpty()) + && (password != null) && (!password.isEmpty()) + && ((password.charAt(0) == ' ') || (password.charAt(password.length() - 1) == ' ')); + }); + + try (SshClient client = setupTestClient()) { + client.start(); + + try { + for (String password : new String[] { + " ", " ", " " + getCurrentTestName(), getCurrentTestName() + " " + }) { + try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port) + .verify(CONNECT_TIMEOUT) + .getSession()) { + s.addPasswordIdentity(password); + + AuthFuture auth = s.auth(); + assertTrue("No authentication result in time for password='" + password + "'", + auth.await(AUTH_TIMEOUT)); + assertTrue("Failed to authenticate with password='" + password + "'", auth.isSuccess()); + } + } + } finally { + client.stop(); + } + } + } + + @Test // see SSHD-1114 + public void testPasswordAuthenticationReporter() throws Exception { + String goodPassword = getCurrentTestName(); + String badPassword = getClass().getSimpleName(); + List<String> attempted = new ArrayList<>(); + sshd.setPasswordAuthenticator((user, password, session) -> { + attempted.add(password); + return goodPassword.equals(password); + }); + sshd.setKeyboardInteractiveAuthenticator(KeyboardInteractiveAuthenticator.NONE); + sshd.setPublickeyAuthenticator(RejectAllPublickeyAuthenticator.INSTANCE); + + List<String> reported = new ArrayList<>(); + PasswordAuthenticationReporter reporter = new PasswordAuthenticationReporter() { + @Override + public void signalAuthenticationAttempt( + ClientSession session, String service, String oldPassword, boolean modified, String newPassword) + throws Exception { + reported.add(oldPassword); + } + + @Override + public void signalAuthenticationSuccess(ClientSession session, String service, String password) + throws Exception { + assertEquals("Mismatched succesful password", goodPassword, password); + } + + @Override + public void signalAuthenticationFailure( + ClientSession session, String service, String password, boolean partial, List<String> serverMethods) + throws Exception { + assertEquals("Mismatched failed password", badPassword, password); + } + }; + + try (SshClient client = setupTestClient()) { + client.setUserAuthFactories( + Collections.singletonList(new org.apache.sshd.client.auth.password.UserAuthPasswordFactory())); + client.start(); + + try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port) + .verify(CONNECT_TIMEOUT).getSession()) { + session.addPasswordIdentity(badPassword); + session.addPasswordIdentity(goodPassword); + session.setPasswordAuthenticationReporter(reporter); + session.auth().verify(AUTH_TIMEOUT); + } finally { + client.stop(); + } + } + + List<String> expected = Arrays.asList(badPassword, goodPassword); + assertListEquals("Attempted passwords", expected, attempted); + assertListEquals("Reported passwords", expected, reported); + } +} diff --git a/sshd-core/src/test/java/org/apache/sshd/common/auth/PublicKeyAuthenticationTest.java b/sshd-core/src/test/java/org/apache/sshd/common/auth/PublicKeyAuthenticationTest.java new file mode 100644 index 0000000..300b77e --- /dev/null +++ b/sshd-core/src/test/java/org/apache/sshd/common/auth/PublicKeyAuthenticationTest.java @@ -0,0 +1,327 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sshd.common.auth; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.auth.pubkey.PublicKeyAuthenticationReporter; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.NamedFactory; +import org.apache.sshd.common.NamedResource; +import org.apache.sshd.common.SshConstants; +import org.apache.sshd.common.SshException; +import org.apache.sshd.common.config.keys.FilePasswordProvider; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.keyprovider.KeyIdentityProvider; +import org.apache.sshd.common.keyprovider.KeyPairProvider; +import org.apache.sshd.common.session.SessionContext; +import org.apache.sshd.common.signature.BuiltinSignatures; +import org.apache.sshd.common.signature.Signature; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.common.util.buffer.Buffer; +import org.apache.sshd.common.util.io.resource.URLResource; +import org.apache.sshd.common.util.security.SecurityUtils; +import org.apache.sshd.server.auth.keyboard.KeyboardInteractiveAuthenticator; +import org.apache.sshd.server.auth.password.RejectAllPasswordAuthenticator; +import org.apache.sshd.server.session.ServerSession; +import org.apache.sshd.util.test.CommonTestSupportUtils; +import org.apache.sshd.util.test.CoreTestSupportUtils; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runners.MethodSorters; + +/** + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class PublicKeyAuthenticationTest extends AuthenticationTestSupport { + public PublicKeyAuthenticationTest() { + super(); + } + + @Test // see SSHD-618 + public void testPublicKeyAuthDifferentThanKex() throws Exception { + KeyPairProvider serverKeys = KeyPairProvider.wrap( + CommonTestSupportUtils.generateKeyPair(KeyUtils.RSA_ALGORITHM, 1024), + CommonTestSupportUtils.generateKeyPair(KeyUtils.DSS_ALGORITHM, 512), + CommonTestSupportUtils.generateKeyPair(KeyUtils.EC_ALGORITHM, 256)); + sshd.setKeyPairProvider(serverKeys); + sshd.setKeyboardInteractiveAuthenticator(KeyboardInteractiveAuthenticator.NONE); + sshd.setPasswordAuthenticator(RejectAllPasswordAuthenticator.INSTANCE); + + KeyPair clientIdentity = CommonTestSupportUtils.generateKeyPair(KeyUtils.EC_ALGORITHM, 256); + sshd.setPublickeyAuthenticator((username, key, session) -> { + String keyType = KeyUtils.getKeyType(key); + String expType = KeyUtils.getKeyType(clientIdentity); + assertEquals("Mismatched client key types", expType, keyType); + assertKeyEquals("Mismatched authentication public keys", clientIdentity.getPublic(), key); + return true; + }); + + // since we need to use RSA + CoreTestSupportUtils.setupFullSignaturesSupport(sshd); + try (SshClient client = setupTestClient()) { + // force server to use only RSA + NamedFactory<Signature> kexSignature = BuiltinSignatures.rsa; + client.setSignatureFactories(Collections.singletonList(kexSignature)); + client.setServerKeyVerifier((sshClientSession, remoteAddress, serverKey) -> { + String keyType = KeyUtils.getKeyType(serverKey); + String expType = kexSignature.getName(); + assertEquals("Mismatched server key type", expType, keyType); + + KeyPair kp; + try { + kp = ValidateUtils.checkNotNull(serverKeys.loadKey(null, keyType), "No server key for type=%s", keyType); + } catch (IOException | GeneralSecurityException e) { + throw new RuntimeException( + "Unexpected " + e.getClass().getSimpleName() + ")" + + " keys loading exception: " + e.getMessage(), + e); + } + assertKeyEquals("Mismatched server public keys", kp.getPublic(), serverKey); + return true; + }); + + // allow only EC keys for public key authentication + org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyFactory factory + = new org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyFactory(); + factory.setSignatureFactories( + Arrays.asList( + BuiltinSignatures.nistp256, BuiltinSignatures.nistp384, BuiltinSignatures.nistp521)); + client.setUserAuthFactories(Collections.singletonList(factory)); + + client.start(); + try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port) + .verify(CONNECT_TIMEOUT) + .getSession()) { + s.addPublicKeyIdentity(clientIdentity); + s.auth().verify(AUTH_TIMEOUT); + } finally { + client.stop(); + } + } + } + + @Test // see SSHD-624 + public void testMismatchedUserAuthPkOkData() throws Exception { + AtomicInteger challengeCounter = new AtomicInteger(0); + sshd.setUserAuthFactories(Collections.singletonList( + new org.apache.sshd.server.auth.pubkey.UserAuthPublicKeyFactory() { + @Override + public org.apache.sshd.server.auth.pubkey.UserAuthPublicKey createUserAuth(ServerSession session) + throws IOException { + return new org.apache.sshd.server.auth.pubkey.UserAuthPublicKey() { + @Override + protected void sendPublicKeyResponse( + ServerSession session, String username, String alg, PublicKey key, + byte[] keyBlob, int offset, int blobLen, Buffer buffer) + throws Exception { + int count = challengeCounter.incrementAndGet(); + outputDebugMessage("sendPublicKeyChallenge(%s)[%s]: count=%d", session, alg, count); + if (count == 1) { + // send wrong key type + super.sendPublicKeyResponse(session, username, + KeyPairProvider.SSH_DSS, key, keyBlob, offset, blobLen, buffer); + } else if (count == 2) { + // send another key + KeyPair otherPair = org.apache.sshd.util.test.CommonTestSupportUtils + .generateKeyPair(KeyUtils.RSA_ALGORITHM, 1024); + PublicKey otherKey = otherPair.getPublic(); + Buffer buf = session.createBuffer(SshConstants.SSH_MSG_USERAUTH_PK_OK, + blobLen + alg.length() + Long.SIZE); + buf.putString(alg); + buf.putPublicKey(otherKey); + session.writePacket(buf); + } else { + super.sendPublicKeyResponse(session, username, alg, key, keyBlob, offset, blobLen, buffer); + } + } + }; + } + + })); + + try (SshClient client = setupTestClient()) { + KeyPair clientIdentity = CommonTestSupportUtils.generateKeyPair( + CommonTestSupportUtils.DEFAULT_TEST_HOST_KEY_PROVIDER_ALGORITHM, + CommonTestSupportUtils.DEFAULT_TEST_HOST_KEY_SIZE); + client.start(); + + try { + for (int index = 1; index <= 4; index++) { + try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port) + .verify(CONNECT_TIMEOUT) + .getSession()) { + s.addPublicKeyIdentity(clientIdentity); + s.auth().verify(AUTH_TIMEOUT); + assertEquals("Mismatched number of challenges", 3, challengeCounter.get()); + break; + } catch (SshException e) { // expected + outputDebugMessage("%s on retry #%d: %s", e.getClass().getSimpleName(), index, e.getMessage()); + + Throwable t = e.getCause(); + assertObjectInstanceOf("Unexpected failure cause at retry #" + index, InvalidKeySpecException.class, t); + } + } + } finally { + client.stop(); + } + } + } + + @Test // see SSHD-862 + public void testSessionContextPropagatedToKeyFilePasswordProvider() throws Exception { + try (SshClient client = setupTestClient()) { + client.start(); + + try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port) + .verify(CONNECT_TIMEOUT) + .getSession()) { + String keyLocation = "super-secret-passphrase-ec256-key"; + FilePasswordProvider passwordProvider = new FilePasswordProvider() { + @Override + public String getPassword( + SessionContext session, NamedResource resourceKey, int retryIndex) + throws IOException { + assertSame("Mismatched session context", s, session); + assertEquals("Mismatched retry index", 0, retryIndex); + + String name = resourceKey.getName(); + int pos = name.lastIndexOf('/'); + if (pos >= 0) { + name = name.substring(pos + 1); + } + assertEquals("Mismatched location", keyLocation, name); + + Boolean passwordRequested = session.getAttribute(PASSWORD_ATTR); + assertNull("Password already requested", passwordRequested); + session.setAttribute(PASSWORD_ATTR, Boolean.TRUE); + return "super secret passphrase"; + } + }; + s.setKeyIdentityProvider(new KeyIdentityProvider() { + @Override + public Iterable<KeyPair> loadKeys(SessionContext session) throws IOException, GeneralSecurityException { + assertSame("Mismatched session context", s, session); + URL location = getClass().getResource(keyLocation); + assertNotNull("Missing key file " + keyLocation, location); + + URLResource resourceKey = new URLResource(location); + Iterable<KeyPair> ids; + try (InputStream keyData = resourceKey.openInputStream()) { + ids = SecurityUtils.loadKeyPairIdentities(session, resourceKey, keyData, passwordProvider); + } + KeyPair kp = GenericUtils.head(ids); + assertNotNull("No identity loaded from " + resourceKey, kp); + return Collections.singletonList(kp); + } + }); + s.auth().verify(AUTH_TIMEOUT); + + Boolean passwordRequested = s.getAttribute(PASSWORD_ATTR); + assertNotNull("Password provider not invoked", passwordRequested); + assertTrue("Password not requested", passwordRequested.booleanValue()); + } finally { + client.stop(); + } + } + } + + @Test // see SSHD-1114 + public void testPublicKeyAuthenticationReporter() throws Exception { + KeyPair goodIdentity = CommonTestSupportUtils.generateKeyPair(KeyUtils.EC_ALGORITHM, 256); + KeyPair badIdentity = CommonTestSupportUtils.generateKeyPair(KeyUtils.EC_ALGORITHM, 256); + List<PublicKey> attempted = new ArrayList<>(); + sshd.setPublickeyAuthenticator((username, key, session) -> { + attempted.add(key); + return KeyUtils.compareKeys(goodIdentity.getPublic(), key); + }); + sshd.setPasswordAuthenticator(RejectAllPasswordAuthenticator.INSTANCE); + sshd.setKeyboardInteractiveAuthenticator(KeyboardInteractiveAuthenticator.NONE); + + List<PublicKey> reported = new ArrayList<>(); + List<PublicKey> signed = new ArrayList<>(); + PublicKeyAuthenticationReporter reporter = new PublicKeyAuthenticationReporter() { + @Override + public void signalAuthenticationAttempt( + ClientSession session, String service, KeyPair identity, String signature) + throws Exception { + reported.add(identity.getPublic()); + } + + @Override + public void signalSignatureAttempt( + ClientSession session, String service, KeyPair identity, String signature, byte[] sigData) + throws Exception { + signed.add(identity.getPublic()); + } + + @Override + public void signalAuthenticationSuccess(ClientSession session, String service, KeyPair identity) + throws Exception { + assertTrue("Mismatched success identity", KeyUtils.compareKeys(goodIdentity.getPublic(), identity.getPublic())); + } + + @Override + public void signalAuthenticationFailure( + ClientSession session, String service, KeyPair identity, boolean partial, List<String> serverMethods) + throws Exception { + assertTrue("Mismatched failed identity", KeyUtils.compareKeys(badIdentity.getPublic(), identity.getPublic())); + } + }; + + try (SshClient client = setupTestClient()) { + client.setUserAuthFactories( + Collections.singletonList(new org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyFactory())); + client.start(); + + try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port) + .verify(CONNECT_TIMEOUT).getSession()) { + session.addPublicKeyIdentity(badIdentity); + session.addPublicKeyIdentity(goodIdentity); + session.setPublicKeyAuthenticationReporter(reporter); + session.auth().verify(AUTH_TIMEOUT); + } finally { + client.stop(); + } + } + + List<PublicKey> expected = Arrays.asList(badIdentity.getPublic(), goodIdentity.getPublic()); + // The server public key authenticator is called twice with the good identity + int numAttempted = attempted.size(); + assertKeyListEquals("Attempted", expected, (numAttempted > 0) ? attempted.subList(0, numAttempted - 1) : attempted); + assertKeyListEquals("Reported", expected, reported); + // The signing is attempted only if the initial public key is accepted + assertKeyListEquals("Signed", Collections.singletonList(goodIdentity.getPublic()), signed); + } +} diff --git a/sshd-core/src/test/java/org/apache/sshd/util/test/server/TestServerSession.java b/sshd-core/src/test/java/org/apache/sshd/util/test/server/TestServerSession.java new file mode 100644 index 0000000..652092b --- /dev/null +++ b/sshd-core/src/test/java/org/apache/sshd/util/test/server/TestServerSession.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sshd.util.test.server; + +import org.apache.sshd.common.io.IoSession; +import org.apache.sshd.common.util.buffer.Buffer; +import org.apache.sshd.server.ServerFactoryManager; +import org.apache.sshd.server.session.ServerSessionImpl; + +/** + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +public class TestServerSession extends ServerSessionImpl { + public TestServerSession(ServerFactoryManager server, IoSession ioSession) throws Exception { + super(server, ioSession); + } + + @Override + public void handleMessage(Buffer buffer) throws Exception { + super.handleMessage(buffer); // debug breakpoint + } +}