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 106778423eb506584393631a27ada6e5a4ec36f1 Author: Lyor Goldstein <lgoldst...@apache.org> AuthorDate: Fri Jan 1 07:59:08 2021 +0200 [SSHD-1114] Added capability for interactive key based authentication participation via UserInteraction --- CHANGES.md | 1 + docs/client-setup.md | 14 +++++ .../sshd/client/auth/keyboard/UserInteraction.java | 13 ++++- .../pubkey/PublicKeyAuthenticationReporter.java | 14 +++++ .../sshd/client/auth/pubkey/UserAuthPublicKey.java | 42 +++++++++++--- .../common/auth/PublicKeyAuthenticationTest.java | 65 ++++++++++++++++++++++ 6 files changed, 139 insertions(+), 10 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e0598d0..69e03b3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -30,3 +30,4 @@ * [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 * [SSHD-1114](https://issues.apache.org/jira/browse/SSHD-1114) Added capability for interactive password authentication participation via UserInteraction +* [SSHD-1114](https://issues.apache.org/jira/browse/SSHD-1114) Added capability for interactive key based authentication participation via UserInteraction diff --git a/docs/client-setup.md b/docs/client-setup.md index 55b64d8..7f71922 100644 --- a/docs/client-setup.md +++ b/docs/client-setup.md @@ -124,6 +124,20 @@ as described in [RFC-4252 section 8](https://tools.ietf.org/html/rfc4252#section String resolveAuthPasswordAttempt(ClientSession session) throws Exception; ``` +The interface can also be used to implement interactive key based authentication as described in [RFC-4252 section 7](https://tools.ietf.org/html/rfc4252#section-7) +via the `resolveAuthPublicKeyIdentityAttempt` method. + +```java +/** + * Invoked during public key authentication when no more pre-registered keys are available + * + * @param session The {@link ClientSession} through which the request was received + * @return The {@link KeyPair} to use - {@code null} signals no more keys available + * @throws Exception if failed to handle the request - <B>Note:</B> may cause session termination + */ +KeyPair resolveAuthPublicKeyIdentityAttempt(ClientSession session) throws Exception; +``` + ## Using the `SshClient` to connect to a server Once the `SshClient` instance is properly configured it needs to be `start()`-ed in order to connect to a server. diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserInteraction.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserInteraction.java index f5101f8..53304c4 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserInteraction.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/keyboard/UserInteraction.java @@ -18,6 +18,7 @@ */ package org.apache.sshd.client.auth.keyboard; +import java.security.KeyPair; import java.util.List; import org.apache.sshd.client.session.ClientSession; @@ -88,7 +89,6 @@ public interface UserInteraction { }; /** - * * @param session The {@link ClientSession} * @return {@code true} if user interaction allowed for this session (default) */ @@ -160,6 +160,17 @@ public interface UserInteraction { } /** + * Invoked during public key authentication when no more pre-registered keys are available + * + * @param session The {@link ClientSession} through which the request was received + * @return The {@link KeyPair} to use - {@code null} signals no more keys available + * @throws Exception if failed to handle the request - <B>Note:</B> may cause session termination + */ + default KeyPair resolveAuthPublicKeyIdentityAttempt(ClientSession session) throws Exception { + return null; + } + + /** * @param prompt The user interaction prompt * @param tokensList A comma-separated list of tokens whose <U>last</U> index is prompt is sought. * @return The position of any token in the prompt - negative if not found diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/PublicKeyAuthenticationReporter.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/PublicKeyAuthenticationReporter.java index b5900b8..7719721 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/PublicKeyAuthenticationReporter.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/PublicKeyAuthenticationReporter.java @@ -48,6 +48,20 @@ public interface PublicKeyAuthenticationReporter { } /** + * Signals end of public key attempts and optionally switching to other authentication methods. <B>Note:</B> neither + * {@link #signalAuthenticationSuccess(ClientSession, String, KeyPair) signalAuthenticationSuccess} nor + * {@link #signalAuthenticationFailure(ClientSession, String, KeyPair, boolean, List) signalAuthenticationFailure} + * are invoked. + * + * @param session The {@link ClientSession} + * @param service The requesting service name + * @throws Exception If failed to handle the callback - <B>Note:</B> may cause session close + */ + default void signalAuthenticationExhausted(ClientSession session, String service) throws Exception { + // ignored + } + + /** * Sending the signed response to the server's challenge * * @param session The {@link ClientSession} diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/UserAuthPublicKey.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/UserAuthPublicKey.java index 16fab44..26ee56d 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/UserAuthPublicKey.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/UserAuthPublicKey.java @@ -29,6 +29,7 @@ import java.util.List; import java.util.Map; import org.apache.sshd.client.auth.AbstractUserAuth; +import org.apache.sshd.client.auth.keyboard.UserInteraction; import org.apache.sshd.client.session.ClientSession; import org.apache.sshd.common.NamedFactory; import org.apache.sshd.common.RuntimeSshException; @@ -93,21 +94,26 @@ public class UserAuthPublicKey extends AbstractUserAuth implements SignatureFact protected boolean sendAuthDataRequest(ClientSession session, String service) throws Exception { boolean debugEnabled = log.isDebugEnabled(); try { - if ((keys == null) || (!keys.hasNext())) { - if (debugEnabled) { - log.debug("sendAuthDataRequest({})[{}] no more keys to send", session, service); - } - - return false; - } - - current = keys.next(); + current = resolveAttemptedPublicKeyIdentity(session, service); } catch (Error e) { warn("sendAuthDataRequest({})[{}] failed ({}) to get next key: {}", session, service, e.getClass().getSimpleName(), e.getMessage(), e); throw new RuntimeSshException(e); } + if (current == null) { + if (debugEnabled) { + log.debug("resolveAttemptedPublicKeyIdentity({})[{}] no more keys to send", session, service); + } + + PublicKeyAuthenticationReporter reporter = session.getPublicKeyAuthenticationReporter(); + if (reporter != null) { + reporter.signalAuthenticationExhausted(session, service); + } + + return false; + } + if (log.isTraceEnabled()) { log.trace("sendAuthDataRequest({})[{}] current key details: {}", session, service, current); } @@ -155,6 +161,24 @@ public class UserAuthPublicKey extends AbstractUserAuth implements SignatureFact return true; } + protected PublicKeyIdentity resolveAttemptedPublicKeyIdentity(ClientSession session, String service) throws Exception { + if ((keys != null) && keys.hasNext()) { + return keys.next(); + } + + UserInteraction ui = session.getUserInteraction(); + if ((ui == null) || (!ui.isInteractionAllowed(session))) { + return null; + } + + KeyPair kp = ui.resolveAuthPublicKeyIdentityAttempt(session); + if (kp == null) { + return null; + } + + return new KeyPairIdentity(this, session, kp); + } + @Override protected boolean processAuthDataRequest(ClientSession session, String service, Buffer buffer) throws Exception { String name = getName(); 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 index 300b77e..3b86eb5 100644 --- 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 @@ -33,7 +33,9 @@ import java.util.List; 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.pubkey.PublicKeyAuthenticationReporter; +import org.apache.sshd.client.future.AuthFuture; import org.apache.sshd.client.session.ClientSession; import org.apache.sshd.common.NamedFactory; import org.apache.sshd.common.NamedResource; @@ -53,6 +55,7 @@ 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.auth.pubkey.RejectAllPublickeyAuthenticator; import org.apache.sshd.server.session.ServerSession; import org.apache.sshd.util.test.CommonTestSupportUtils; import org.apache.sshd.util.test.CoreTestSupportUtils; @@ -324,4 +327,66 @@ public class PublicKeyAuthenticationTest extends AuthenticationTestSupport { // The signing is attempted only if the initial public key is accepted assertKeyListEquals("Signed", Collections.singletonList(goodIdentity.getPublic()), signed); } + + @Test // see SSHD-1114 + public void testAuthenticationAttemptsExhausted() throws Exception { + sshd.setPasswordAuthenticator(RejectAllPasswordAuthenticator.INSTANCE); + sshd.setPublickeyAuthenticator(RejectAllPublickeyAuthenticator.INSTANCE); + sshd.setKeyboardInteractiveAuthenticator(KeyboardInteractiveAuthenticator.NONE); + + AtomicInteger exhaustedCount = new AtomicInteger(); + PublicKeyAuthenticationReporter reporter = new PublicKeyAuthenticationReporter() { + @Override + public void signalAuthenticationExhausted(ClientSession session, String service) throws Exception { + exhaustedCount.incrementAndGet(); + } + }; + + KeyPair kp = CommonTestSupportUtils.generateKeyPair(KeyUtils.EC_ALGORITHM, 256); + AtomicInteger attemptsCount = new AtomicInteger(); + UserInteraction ui = new UserInteraction() { + @Override + public String[] interactive( + ClientSession session, String name, String instruction, String lang, String[] prompt, boolean[] echo) { + throw new UnsupportedOperationException("Unexpected interactive invocation"); + } + + @Override + public String getUpdatedPassword(ClientSession session, String prompt, String lang) { + throw new UnsupportedOperationException("Unexpected updated password request"); + } + + @Override + public KeyPair resolveAuthPublicKeyIdentityAttempt(ClientSession session) throws Exception { + int count = attemptsCount.incrementAndGet(); + if (count <= 3) { + return kp; + } else { + return UserInteraction.super.resolveAuthPublicKeyIdentityAttempt(session); + } + } + }; + + 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.setPublicKeyAuthenticationReporter(reporter); + session.setUserInteraction(ui); + for (int index = 1; index <= 5; index++) { + session.addPublicKeyIdentity(kp); + } + AuthFuture auth = session.auth(); + assertAuthenticationResult("Authenticating", auth, false); + } finally { + client.stop(); + } + } + + assertEquals("Mismatched invocation count", 1, exhaustedCount.getAndSet(0)); + assertEquals("Mismatched retries count", 4 /* 3 attempts + null */, attemptsCount.getAndSet(0)); + } }