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 a8ee3aa1e3fdcf7014073e2ac4c1bf4a8eea4b8c Author: Lyor Goldstein <lgoldst...@apache.org> AuthorDate: Fri Jan 1 01:08:05 2021 +0200 [SSHD-1114] Added callbacks for client-side host-based authentication progress --- CHANGES.md | 1 + docs/event-listeners.md | 6 ++ .../sshd/client/ClientAuthenticationManager.java | 5 ++ .../java/org/apache/sshd/client/SshClient.java | 12 ++++ .../hostbased/HostBasedAuthenticationReporter.java | 81 ++++++++++++++++++++++ .../client/auth/hostbased/UserAuthHostBased.java | 43 ++++++++++-- .../sshd/client/session/AbstractClientSession.java | 14 ++++ .../client/ClientAuthenticationManagerTest.java | 11 +++ .../sshd/common/auth/AuthenticationTest.java | 76 +++++++++++++++++--- 9 files changed, 235 insertions(+), 14 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c3aebd2..6a084dc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -28,3 +28,4 @@ * [SSHD-1109](https://issues.apache.org/jira/browse/SSHD-1109) Replace log4j with logback as the slf4j logger implementation for tests * [SSHD-1114](https://issues.apache.org/jira/browse/SSHD-1114) Added callbacks for client-side password authentication progress * [SSHD-1114](https://issues.apache.org/jira/browse/SSHD-1114) Added callbacks for client-side public key authentication progress +* [SSHD-1114](https://issues.apache.org/jira/browse/SSHD-1114) Added callbacks for client-side host-based authentication progress diff --git a/docs/event-listeners.md b/docs/event-listeners.md index 50869ff..1f17a98 100644 --- a/docs/event-listeners.md +++ b/docs/event-listeners.md @@ -205,3 +205,9 @@ overriding any globally registered instance. Used to inform about the progress of the client-side public key authentication as described in [RFC-4252 section 7](https://tools.ietf.org/html/rfc4252#section-7). Can be registered globally on the `SshClient` and also for a specific `ClientSession` after it is established but before its `auth()` method is called - thus overriding any globally registered instance. + +### `HostBasedAuthenticationReporter` + +Used to inform about the progress of the client-side host-based authentication as described in [RFC-4252 section 9](https://tools.ietf.org/html/rfc4252#section-9). +Can be registered globally on the `SshClient` and also for a specific `ClientSession` after it is established but before its `auth()` method is called - thus +overriding any globally registered instance. diff --git a/sshd-core/src/main/java/org/apache/sshd/client/ClientAuthenticationManager.java b/sshd-core/src/main/java/org/apache/sshd/client/ClientAuthenticationManager.java index b6c6706..4dff328 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/ClientAuthenticationManager.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/ClientAuthenticationManager.java @@ -27,6 +27,7 @@ import org.apache.sshd.client.auth.AuthenticationIdentitiesProvider; import org.apache.sshd.client.auth.BuiltinUserAuthFactories; import org.apache.sshd.client.auth.UserAuth; import org.apache.sshd.client.auth.UserAuthFactory; +import org.apache.sshd.client.auth.hostbased.HostBasedAuthenticationReporter; import org.apache.sshd.client.auth.keyboard.UserInteraction; import org.apache.sshd.client.auth.password.PasswordAuthenticationReporter; import org.apache.sshd.client.auth.password.PasswordIdentityProvider; @@ -116,6 +117,10 @@ public interface ClientAuthenticationManager void setPublicKeyAuthenticationReporter(PublicKeyAuthenticationReporter reporter); + HostBasedAuthenticationReporter getHostBasedAuthenticationReporter(); + + void setHostBasedAuthenticationReporter(HostBasedAuthenticationReporter reporter); + @Override default void setUserAuthFactoriesNames(Collection<String> names) { BuiltinUserAuthFactories.ParseResult result = BuiltinUserAuthFactories.parseFactoriesList(names); diff --git a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java index 2d07cfa..9660510 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java @@ -44,6 +44,7 @@ import java.util.stream.Collectors; import org.apache.sshd.agent.SshAgentFactory; import org.apache.sshd.client.auth.AuthenticationIdentitiesProvider; import org.apache.sshd.client.auth.UserAuthFactory; +import org.apache.sshd.client.auth.hostbased.HostBasedAuthenticationReporter; import org.apache.sshd.client.auth.keyboard.UserAuthKeyboardInteractiveFactory; import org.apache.sshd.client.auth.keyboard.UserInteraction; import org.apache.sshd.client.auth.password.PasswordAuthenticationReporter; @@ -183,6 +184,7 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa private FilePasswordProvider filePasswordProvider; private PasswordIdentityProvider passwordIdentityProvider; private PasswordAuthenticationReporter passwordAuthenticationReporter; + private HostBasedAuthenticationReporter hostBasedAuthenticationReporter; private UserInteraction userInteraction; private final List<Object> identities = new CopyOnWriteArrayList<>(); @@ -272,6 +274,16 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa } @Override + public HostBasedAuthenticationReporter getHostBasedAuthenticationReporter() { + return hostBasedAuthenticationReporter; + } + + @Override + public void setHostBasedAuthenticationReporter(HostBasedAuthenticationReporter reporter) { + this.hostBasedAuthenticationReporter = reporter; + } + + @Override public List<UserAuthFactory> getUserAuthFactories() { return userAuthFactories; } diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/hostbased/HostBasedAuthenticationReporter.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/hostbased/HostBasedAuthenticationReporter.java new file mode 100644 index 0000000..18c2193 --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/hostbased/HostBasedAuthenticationReporter.java @@ -0,0 +1,81 @@ +/* + * 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.client.auth.hostbased; + +import java.security.KeyPair; +import java.util.List; + +import org.apache.sshd.client.session.ClientSession; + +/** + * Provides report about the client side host-based authentication progress + * + * @see <a href="https://tools.ietf.org/html/rfc4252#section-9">RFC-4252 section 9</a> + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +public interface HostBasedAuthenticationReporter { + /** + * Sending the initial request to use host based authentication + * + * @param session The {@link ClientSession} + * @param service The requesting service name + * @param identity The {@link KeyPair} identity being attempted + * @param hostname The host name value sent to the server + * @param username The username value sent to the server + * @param signature The signature data that is being sent to the server + * @throws Exception If failed to handle the callback - <B>Note:</B> may cause session close + */ + default void signalAuthenticationAttempt( + ClientSession session, String service, KeyPair identity, String hostname, String username, byte[] signature) + throws Exception { + // ignored + } + + /** + * @param session The {@link ClientSession} + * @param service The requesting service name + * @param identity The {@link KeyPair} identity being attempted + * @param hostname The host name value sent to the server + * @param username The username value sent to the server + * @throws Exception If failed to handle the callback - <B>Note:</B> may cause session close + */ + default void signalAuthenticationSuccess( + ClientSession session, String service, KeyPair identity, String hostname, String username) + throws Exception { + // ignored + } + + /** + * @param session The {@link ClientSession} + * @param service The requesting service name + * @param identity The {@link KeyPair} identity being attempted + * @param hostname The host name value sent to the server + * @param username The username value sent to the server + * @param partial {@code true} if some partial authentication success so far + * @param serverMethods The {@link List} of authentication methods that can continue + * @throws Exception If failed to handle the callback - <B>Note:</B> may cause session close + */ + default void signalAuthenticationFailure( + ClientSession session, String service, KeyPair identity, + String hostname, String username, boolean partial, List<String> serverMethods) + throws Exception { + // ignored + } +} diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/hostbased/UserAuthHostBased.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/hostbased/UserAuthHostBased.java index 861549a..5c18b5f 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/auth/hostbased/UserAuthHostBased.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/hostbased/UserAuthHostBased.java @@ -48,8 +48,9 @@ import org.apache.sshd.common.util.net.SshdSocketAddress; public class UserAuthHostBased extends AbstractUserAuth implements SignatureFactoriesManager { public static final String NAME = UserAuthHostBasedFactory.NAME; - private Iterator<? extends Map.Entry<KeyPair, ? extends Collection<X509Certificate>>> keys; - private final HostKeyIdentityProvider clientHostKeys; + protected Iterator<? extends Map.Entry<KeyPair, ? extends Collection<X509Certificate>>> keys; + protected Map.Entry<KeyPair, ? extends Collection<X509Certificate>> keyInfo; + protected final HostKeyIdentityProvider clientHostKeys; private List<NamedFactory<Signature>> factories; private String clientUsername; private String clientHostname; @@ -103,7 +104,7 @@ public class UserAuthHostBased extends AbstractUserAuth implements SignatureFact return false; } - Map.Entry<KeyPair, ? extends Collection<X509Certificate>> keyInfo = keys.next(); + keyInfo = keys.next(); KeyPair kp = keyInfo.getKey(); PublicKey pub = kp.getPublic(); String keyType = KeyUtils.getKeyType(pub); @@ -159,13 +160,22 @@ public class UserAuthHostBased extends AbstractUserAuth implements SignatureFact buffer.putBytes(keyBytes); buffer.putString(clientHostname); buffer.putString(clientUsername); - appendSignature(session, service, keyType, pub, keyBytes, clientHostname, clientUsername, verifier, buffer); + + byte[] signature = appendSignature( + session, service, keyType, pub, keyBytes, + clientHostname, clientUsername, verifier, buffer); + HostBasedAuthenticationReporter reporter = session.getHostBasedAuthenticationReporter(); + if (reporter != null) { + reporter.signalAuthenticationAttempt( + session, service, kp, clientHostname, clientUsername, signature); + } + session.writePacket(buffer); return true; } @SuppressWarnings("checkstyle:ParameterNumber") - protected void appendSignature( + protected byte[] appendSignature( ClientSession session, String service, String keyType, PublicKey key, byte[] keyBytes, String clientHostname, String clientUsername, @@ -203,6 +213,7 @@ public class UserAuthHostBased extends AbstractUserAuth implements SignatureFact bs.putString(keyType); bs.putBytes(signature); buffer.putBytes(bs.array(), bs.rpos(), bs.available()); + return signature; } @Override @@ -215,6 +226,28 @@ public class UserAuthHostBased extends AbstractUserAuth implements SignatureFact + " received unknown packet: cmd=" + SshConstants.getCommandMessageName(cmd)); } + @Override + public void signalAuthMethodSuccess(ClientSession session, String service, Buffer buffer) throws Exception { + HostBasedAuthenticationReporter reporter = session.getHostBasedAuthenticationReporter(); + if (reporter != null) { + reporter.signalAuthenticationSuccess( + session, service, (keyInfo == null) ? null : keyInfo.getKey(), resolveClientHostname(), + resolveClientUsername()); + } + } + + @Override + public void signalAuthMethodFailure( + ClientSession session, String service, boolean partial, List<String> serverMethods, Buffer buffer) + throws Exception { + HostBasedAuthenticationReporter reporter = session.getHostBasedAuthenticationReporter(); + if (reporter != null) { + reporter.signalAuthenticationFailure( + session, service, (keyInfo == null) ? null : keyInfo.getKey(), + resolveClientHostname(), resolveClientUsername(), partial, serverMethods); + } + } + protected String resolveClientUsername() { String value = getClientUsername(); return GenericUtils.isEmpty(value) ? OsUtils.getCurrentUser() : value; diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java index 490bbfb..6b1faff 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java @@ -32,6 +32,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import org.apache.sshd.client.ClientFactoryManager; import org.apache.sshd.client.auth.AuthenticationIdentitiesProvider; import org.apache.sshd.client.auth.UserAuthFactory; +import org.apache.sshd.client.auth.hostbased.HostBasedAuthenticationReporter; import org.apache.sshd.client.auth.keyboard.UserInteraction; import org.apache.sshd.client.auth.password.PasswordAuthenticationReporter; import org.apache.sshd.client.auth.password.PasswordIdentityProvider; @@ -95,6 +96,7 @@ public abstract class AbstractClientSession extends AbstractSession implements C private PasswordAuthenticationReporter passwordAuthenticationReporter; private KeyIdentityProvider keyIdentityProvider; private PublicKeyAuthenticationReporter publicKeyAuthenticationReporter; + private HostBasedAuthenticationReporter hostBasedAuthenticationReporter; private List<UserAuthFactory> userAuthFactories; private SocketAddress connectAddress; private ClientProxyConnector proxyConnector; @@ -229,6 +231,18 @@ public abstract class AbstractClientSession extends AbstractSession implements C } @Override + public HostBasedAuthenticationReporter getHostBasedAuthenticationReporter() { + ClientFactoryManager manager = getFactoryManager(); + return resolveEffectiveProvider(HostBasedAuthenticationReporter.class, hostBasedAuthenticationReporter, + manager.getHostBasedAuthenticationReporter()); + } + + @Override + public void setHostBasedAuthenticationReporter(HostBasedAuthenticationReporter reporter) { + this.hostBasedAuthenticationReporter = reporter; + } + + @Override public ClientProxyConnector getClientProxyConnector() { ClientFactoryManager manager = getFactoryManager(); return resolveEffectiveProvider(ClientProxyConnector.class, proxyConnector, manager.getClientProxyConnector()); diff --git a/sshd-core/src/test/java/org/apache/sshd/client/ClientAuthenticationManagerTest.java b/sshd-core/src/test/java/org/apache/sshd/client/ClientAuthenticationManagerTest.java index c1062e6..981f811 100644 --- a/sshd-core/src/test/java/org/apache/sshd/client/ClientAuthenticationManagerTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/client/ClientAuthenticationManagerTest.java @@ -30,6 +30,7 @@ import java.util.concurrent.atomic.AtomicReference; import org.apache.sshd.client.auth.AuthenticationIdentitiesProvider; import org.apache.sshd.client.auth.BuiltinUserAuthFactories; import org.apache.sshd.client.auth.UserAuthFactory; +import org.apache.sshd.client.auth.hostbased.HostBasedAuthenticationReporter; import org.apache.sshd.client.auth.keyboard.UserInteraction; import org.apache.sshd.client.auth.password.PasswordAuthenticationReporter; import org.apache.sshd.client.auth.password.PasswordIdentityProvider; @@ -102,6 +103,16 @@ public class ClientAuthenticationManagerTest extends BaseTestSupport { } @Override + public HostBasedAuthenticationReporter getHostBasedAuthenticationReporter() { + return null; + } + + @Override + public void setHostBasedAuthenticationReporter(HostBasedAuthenticationReporter reporter) { + throw new UnsupportedOperationException("setHostBasedAuthenticationReporter(" + reporter + ")"); + } + + @Override public UserInteraction getUserInteraction() { return null; } 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 c75d07d..beb3141 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 @@ -36,8 +36,10 @@ 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; @@ -72,6 +74,7 @@ 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; @@ -773,17 +776,72 @@ public class AuthenticationTest extends BaseTestSupport { @Test // see SSHD-620 public void testHostBasedAuthentication() throws Exception { - String hostClienUser = getClass().getSimpleName(); + 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); - AtomicInteger invocationCount = new AtomicInteger(0); sshd.setHostBasedAuthenticator((session, username, clientHostKey, clientHostName, clientUsername, certificates) -> { - invocationCount.incrementAndGet(); - return hostClienUser.equals(clientUsername) + return hostClientUser.equals(clientUsername) && hostClientName.equals(clientHostName) - && KeyUtils.compareKeys(hostClientKey.getPublic(), clientHostKey); + && KeyUtils.compareKeys(hostClientKey.getPublic(), clientHostKey) + && delegate.authenticate(session, username, clientHostKey, clientHostName, clientUsername, certificates); }); sshd.setPasswordAuthenticator(RejectAllPasswordAuthenticator.INSTANCE); sshd.setKeyboardInteractiveAuthenticator(KeyboardInteractiveAuthenticator.NONE); @@ -796,16 +854,16 @@ public class AuthenticationTest extends BaseTestSupport { org.apache.sshd.client.auth.hostbased.UserAuthHostBasedFactory factory = new org.apache.sshd.client.auth.hostbased.UserAuthHostBasedFactory(); // TODO factory.setClientHostname(CLIENT_HOSTNAME); - factory.setClientUsername(hostClienUser); + factory.setClientUsername(hostClientUser); factory.setClientHostKeys(HostKeyIdentityProvider.wrap(hostClientKey)); client.setUserAuthFactories(Collections.singletonList(factory)); client.start(); - try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port) + try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port) .verify(CONNECT_TIMEOUT) .getSession()) { - s.auth().verify(AUTH_TIMEOUT); - assertEquals("Mismatched authenticator invocation count", 1, invocationCount.get()); + preAuthInitializer.accept(session); + session.auth().verify(AUTH_TIMEOUT); } finally { client.stop(); }