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));
+    }
 }