You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2021/01/02 07:14:53 UTC
[mina-sshd] 14/15: [SSHD-1114] Added capability for interactive key
based authentication participation via UserInteraction
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 <lg...@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));
+ }
}