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:49 UTC

[mina-sshd] 10/15: [SSHD-1114] Added callbacks for client-side public key authentication progress

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 a94f45622ace7cad2d6dd78ef8980ff7e87858a3
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Thu Dec 31 23:18:04 2020 +0200

    [SSHD-1114] Added callbacks for client-side public key authentication progress
---
 CHANGES.md                                         |  1 +
 docs/event-listeners.md                            |  6 ++
 .../sshd/client/auth/pubkey/PublicKeyIdentity.java |  6 +-
 .../apache/sshd/util/test/JUnitTestSupport.java    | 13 +++
 .../main/java/org/apache/sshd/agent/SshAgent.java  | 10 +++
 .../sshd/agent/common/AbstractAgentProxy.java      |  1 +
 .../org/apache/sshd/agent/local/AgentImpl.java     |  6 ++
 .../sshd/client/ClientAuthenticationManager.java   |  5 ++
 .../java/org/apache/sshd/client/SshClient.java     | 14 +++-
 .../sshd/client/auth/pubkey/KeyAgentIdentity.java  | 20 +++--
 .../sshd/client/auth/pubkey/KeyPairIdentity.java   | 12 +--
 .../pubkey/PublicKeyAuthenticationReporter.java    | 92 ++++++++++++++++++++++
 .../sshd/client/auth/pubkey/UserAuthPublicKey.java | 63 +++++++++++----
 .../sshd/client/session/AbstractClientSession.java | 14 ++++
 .../client/ClientAuthenticationManagerTest.java    | 11 +++
 .../sshd/common/auth/AuthenticationTest.java       | 85 +++++++++++++++++++-
 16 files changed, 326 insertions(+), 33 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 75ae5f7..c3aebd2 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -27,3 +27,4 @@
 * [SSHD-1085](https://issues.apache.org/jira/browse/SSHD-1085) Added more notifications related to channel state change for detecting channel closing or closed earlier.
 * [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
diff --git a/docs/event-listeners.md b/docs/event-listeners.md
index 3f9dcf6..50869ff 100644
--- a/docs/event-listeners.md
+++ b/docs/event-listeners.md
@@ -199,3 +199,9 @@ in [RFC 4254 - section 6.7](https://tools.ietf.org/html/rfc4254#section-6.7)
 Used to inform about the progress of the client-side password based authentication as described in [RFC-4252 section 8](https://tools.ietf.org/html/rfc4252#section-8).
 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.
+
+### `PublicKeyAuthenticationReporter`
+
+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.
diff --git a/sshd-common/src/main/java/org/apache/sshd/client/auth/pubkey/PublicKeyIdentity.java b/sshd-common/src/main/java/org/apache/sshd/client/auth/pubkey/PublicKeyIdentity.java
index e794986..f0f3634 100644
--- a/sshd-common/src/main/java/org/apache/sshd/client/auth/pubkey/PublicKeyIdentity.java
+++ b/sshd-common/src/main/java/org/apache/sshd/client/auth/pubkey/PublicKeyIdentity.java
@@ -18,7 +18,7 @@
  */
 package org.apache.sshd.client.auth.pubkey;
 
-import java.security.PublicKey;
+import java.security.KeyPair;
 import java.util.Map;
 
 import org.apache.sshd.common.session.SessionContext;
@@ -30,9 +30,9 @@ import org.apache.sshd.common.session.SessionContext;
  */
 public interface PublicKeyIdentity {
     /**
-     * @return The {@link PublicKey} identity value
+     * @return The {@link KeyPair} identity value
      */
-    PublicKey getPublicKey();
+    KeyPair getKeyIdentity();
 
     /**
      * Proves the public key identity by signing the given data
diff --git a/sshd-common/src/test/java/org/apache/sshd/util/test/JUnitTestSupport.java b/sshd-common/src/test/java/org/apache/sshd/util/test/JUnitTestSupport.java
index 98d870f..ff3a329 100644
--- a/sshd-common/src/test/java/org/apache/sshd/util/test/JUnitTestSupport.java
+++ b/sshd-common/src/test/java/org/apache/sshd/util/test/JUnitTestSupport.java
@@ -455,6 +455,19 @@ public abstract class JUnitTestSupport extends Assert {
         assertArrayEquals(message + "[encoded-data]", expected.getEncoded(), actual.getEncoded());
     }
 
+    public static <T extends Key> void assertKeyListEquals(
+            String message, List<? extends T> expected, List<? extends T> actual) {
+        int numKeys = GenericUtils.size(expected);
+        assertEquals(message + "[size]", numKeys, GenericUtils.size(actual));
+        if (numKeys <= 0) {
+            return;
+        }
+
+        for (int index = 0; index < numKeys; index++) {
+            assertKeyEquals(message + "[#" + index + "]", expected.get(index), actual.get(index));
+        }
+    }
+
     public static <T extends Key> void assertKeyEquals(String message, T expected, T actual) {
         if (expected == actual) {
             return;
diff --git a/sshd-core/src/main/java/org/apache/sshd/agent/SshAgent.java b/sshd-core/src/main/java/org/apache/sshd/agent/SshAgent.java
index 1c25f3a..700c69c 100644
--- a/sshd-core/src/main/java/org/apache/sshd/agent/SshAgent.java
+++ b/sshd-core/src/main/java/org/apache/sshd/agent/SshAgent.java
@@ -47,6 +47,16 @@ public interface SshAgent extends java.nio.channels.Channel {
      */
     Map.Entry<String, byte[]> sign(SessionContext session, PublicKey key, String algo, byte[] data) throws IOException;
 
+    /**
+     * Used for reporting client-side public key authentication via agent
+     *
+     * @param  key The {@link PublicKey} that is going to be used
+     * @return     The {@link KeyPair} identity for it - if available - {@code null} otherwise
+     */
+    default KeyPair resolveLocalIdentity(PublicKey key) {
+        return null;
+    }
+
     void addIdentity(KeyPair key, String comment) throws IOException;
 
     void removeIdentity(PublicKey key) throws IOException;
diff --git a/sshd-core/src/main/java/org/apache/sshd/agent/common/AbstractAgentProxy.java b/sshd-core/src/main/java/org/apache/sshd/agent/common/AbstractAgentProxy.java
index 3add243..da38c72 100644
--- a/sshd-core/src/main/java/org/apache/sshd/agent/common/AbstractAgentProxy.java
+++ b/sshd-core/src/main/java/org/apache/sshd/agent/common/AbstractAgentProxy.java
@@ -83,6 +83,7 @@ public abstract class AbstractAgentProxy extends AbstractLoggingBean implements
         }
 
         int nbIdentities = buffer.getInt();
+        // TODO make the maximum a Property
         if ((nbIdentities < 0) || (nbIdentities > 1024)) {
             throw new SshException("Illogical identities count: " + nbIdentities);
         }
diff --git a/sshd-core/src/main/java/org/apache/sshd/agent/local/AgentImpl.java b/sshd-core/src/main/java/org/apache/sshd/agent/local/AgentImpl.java
index 2b80370..bb50ce6 100644
--- a/sshd-core/src/main/java/org/apache/sshd/agent/local/AgentImpl.java
+++ b/sshd-core/src/main/java/org/apache/sshd/agent/local/AgentImpl.java
@@ -105,6 +105,12 @@ public class AgentImpl implements SshAgent {
     }
 
     @Override
+    public KeyPair resolveLocalIdentity(PublicKey key) {
+        Map.Entry<KeyPair, String> pp = getKeyPair(keys, key);
+        return (pp == null) ? null : pp.getKey();
+    }
+
+    @Override
     public void removeIdentity(PublicKey key) throws IOException {
         if (!isOpen()) {
             throw new SshException("Agent closed");
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 14bca04..b6c6706 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
@@ -30,6 +30,7 @@ import org.apache.sshd.client.auth.UserAuthFactory;
 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.keyverifier.ServerKeyVerifier;
 import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.common.auth.UserAuthFactoriesManager;
@@ -111,6 +112,10 @@ public interface ClientAuthenticationManager
 
     void setPasswordAuthenticationReporter(PasswordAuthenticationReporter reporter);
 
+    PublicKeyAuthenticationReporter getPublicKeyAuthenticationReporter();
+
+    void setPublicKeyAuthenticationReporter(PublicKeyAuthenticationReporter 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 4182bf8..2d07cfa 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
@@ -49,6 +49,7 @@ 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.password.UserAuthPasswordFactory;
+import org.apache.sshd.client.auth.pubkey.PublicKeyAuthenticationReporter;
 import org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyFactory;
 import org.apache.sshd.client.config.hosts.HostConfigEntry;
 import org.apache.sshd.client.config.hosts.HostConfigEntryResolver;
@@ -178,10 +179,11 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
     private HostConfigEntryResolver hostConfigEntryResolver;
     private ClientIdentityLoader clientIdentityLoader;
     private KeyIdentityProvider keyIdentityProvider;
+    private PublicKeyAuthenticationReporter publicKeyAuthenticationReporter;
     private FilePasswordProvider filePasswordProvider;
     private PasswordIdentityProvider passwordIdentityProvider;
-    private UserInteraction userInteraction;
     private PasswordAuthenticationReporter passwordAuthenticationReporter;
+    private UserInteraction userInteraction;
 
     private final List<Object> identities = new CopyOnWriteArrayList<>();
     private final AuthenticationIdentitiesProvider identitiesProvider;
@@ -359,6 +361,16 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
     }
 
     @Override
+    public PublicKeyAuthenticationReporter getPublicKeyAuthenticationReporter() {
+        return publicKeyAuthenticationReporter;
+    }
+
+    @Override
+    public void setPublicKeyAuthenticationReporter(PublicKeyAuthenticationReporter reporter) {
+        this.publicKeyAuthenticationReporter = reporter;
+    }
+
+    @Override
     protected void checkConfig() {
         super.checkConfig();
 
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/KeyAgentIdentity.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/KeyAgentIdentity.java
index 53b37b2..2cec320 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/KeyAgentIdentity.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/KeyAgentIdentity.java
@@ -18,6 +18,7 @@
  */
 package org.apache.sshd.client.auth.pubkey;
 
+import java.security.KeyPair;
 import java.security.PublicKey;
 import java.util.Map;
 import java.util.Objects;
@@ -33,18 +34,23 @@ import org.apache.sshd.common.session.SessionContext;
  */
 public class KeyAgentIdentity implements PublicKeyIdentity {
     private final SshAgent agent;
-    private final PublicKey key;
+    private final KeyPair keyPair;
+    private KeyPair resolvedPair;
     private final String comment;
 
     public KeyAgentIdentity(SshAgent agent, PublicKey key, String comment) {
         this.agent = Objects.requireNonNull(agent, "No signing agent");
-        this.key = Objects.requireNonNull(key, "No public key");
+        this.keyPair = new KeyPair(Objects.requireNonNull(key, "No public key"), null);
         this.comment = comment;
     }
 
     @Override
-    public PublicKey getPublicKey() {
-        return key;
+    public KeyPair getKeyIdentity() {
+        if (resolvedPair == null) {
+            resolvedPair = agent.resolveLocalIdentity(keyPair.getPublic());
+        }
+
+        return (resolvedPair == null) ? keyPair : resolvedPair;
     }
 
     public String getComment() {
@@ -53,12 +59,14 @@ public class KeyAgentIdentity implements PublicKeyIdentity {
 
     @Override
     public Map.Entry<String, byte[]> sign(SessionContext session, String algo, byte[] data) throws Exception {
-        return agent.sign(session, getPublicKey(), algo, data);
+        KeyPair kp = getKeyIdentity();
+        return agent.sign(session, kp.getPublic(), algo, data);
     }
 
     @Override
     public String toString() {
-        PublicKey pubKey = getPublicKey();
+        KeyPair kp = getKeyIdentity();
+        PublicKey pubKey = kp.getPublic();
         return getClass().getSimpleName() + "[" + KeyUtils.getKeyType(pubKey) + "]"
                + " fingerprint=" + KeyUtils.getFingerPrint(pubKey)
                + ", comment=" + getComment();
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/KeyPairIdentity.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/KeyPairIdentity.java
index b90c369..6dfcf72 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/KeyPairIdentity.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/KeyPairIdentity.java
@@ -43,7 +43,7 @@ import org.apache.sshd.common.util.ValidateUtils;
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 public class KeyPairIdentity implements PublicKeyIdentity, SignatureFactoriesHolder {
-    protected final KeyPair pair;
+    private final KeyPair pair;
     private final List<NamedFactory<Signature>> signatureFactories;
 
     public KeyPairIdentity(SignatureFactoriesManager primary, SignatureFactoriesManager secondary, KeyPair pair) {
@@ -55,8 +55,8 @@ public class KeyPairIdentity implements PublicKeyIdentity, SignatureFactoriesHol
     }
 
     @Override
-    public PublicKey getPublicKey() {
-        return pair.getPublic();
+    public KeyPair getKeyIdentity() {
+        return pair;
     }
 
     @Override
@@ -68,7 +68,8 @@ public class KeyPairIdentity implements PublicKeyIdentity, SignatureFactoriesHol
     public Map.Entry<String, byte[]> sign(SessionContext session, String algo, byte[] data) throws Exception {
         NamedFactory<? extends Signature> factory;
         if (GenericUtils.isEmpty(algo)) {
-            algo = KeyUtils.getKeyType(getPublicKey());
+            KeyPair kp = getKeyIdentity();
+            algo = KeyUtils.getKeyType(kp.getPublic());
             // SSHD-1104 check if the key type is aliased
             factory = SignatureFactory.resolveSignatureFactory(algo, getSignatureFactories());
         } else {
@@ -86,7 +87,8 @@ public class KeyPairIdentity implements PublicKeyIdentity, SignatureFactoriesHol
 
     @Override
     public String toString() {
-        PublicKey pubKey = getPublicKey();
+        KeyPair kp = getKeyIdentity();
+        PublicKey pubKey = kp.getPublic();
         return getClass().getSimpleName()
                + " type=" + KeyUtils.getKeyType(pubKey)
                + ", factories=" + getSignatureFactoriesNameList()
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
new file mode 100644
index 0000000..b5900b8
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/pubkey/PublicKeyAuthenticationReporter.java
@@ -0,0 +1,92 @@
+/*
+ * 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.pubkey;
+
+import java.security.KeyPair;
+import java.util.List;
+
+import org.apache.sshd.client.session.ClientSession;
+
+/**
+ * Provides report about the client side public key authentication progress
+ *
+ * @see    <a href="https://tools.ietf.org/html/rfc4252#section-7">RFC-4252 section 7</a>
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface PublicKeyAuthenticationReporter {
+    /**
+     * Sending the initial request to use public key authentication
+     *
+     * @param  session   The {@link ClientSession}
+     * @param  service   The requesting service name
+     * @param  identity  The {@link KeyPair} identity being attempted - <B>Note:</B> for agent based authentications the
+     *                   private key may be {@code null}
+     * @param  signature The type of signature that is being used
+     * @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 signature)
+            throws Exception {
+        // ignored
+    }
+
+    /**
+     * Sending the signed response to the server's challenge
+     *
+     * @param  session   The {@link ClientSession}
+     * @param  service   The requesting service name
+     * @param  identity  The {@link KeyPair} identity being attempted - <B>Note:</B> for agent based authentications the
+     *                   private key may be {@code null}
+     * @param  signature The type of signature that is being used
+     * @param  signed    The generated signature data
+     * @throws Exception If failed to handle the callback - <B>Note:</B> may cause session close
+     */
+    default void signalSignatureAttempt(
+            ClientSession session, String service, KeyPair identity, String signature, byte[] signed)
+            throws Exception {
+        // ignored
+    }
+
+    /**
+     * @param  session   The {@link ClientSession}
+     * @param  service   The requesting service name
+     * @param  identity  The {@link KeyPair} identity being attempted - <B>Note:</B> for agent based authentications the
+     *                   private key may be {@code null}
+     * @throws Exception If failed to handle the callback - <B>Note:</B> may cause session close
+     */
+    default void signalAuthenticationSuccess(ClientSession session, String service, KeyPair identity) throws Exception {
+        // ignored
+    }
+
+    /**
+     * @param  session       The {@link ClientSession}
+     * @param  service       The requesting service name
+     * @param  identity      The {@link KeyPair} identity being attempted - <B>Note:</B> for agent based authentications
+     *                       the private key may be {@code null}
+     * @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, boolean partial, List<String> serverMethods)
+            throws Exception {
+        // ignored
+    }
+}
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 28e575e..16fab44 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
@@ -20,6 +20,7 @@ package org.apache.sshd.client.auth.pubkey;
 
 import java.io.Closeable;
 import java.io.IOException;
+import java.security.KeyPair;
 import java.security.PublicKey;
 import java.security.spec.InvalidKeySpecException;
 import java.util.Collection;
@@ -111,16 +112,17 @@ public class UserAuthPublicKey extends AbstractUserAuth implements SignatureFact
             log.trace("sendAuthDataRequest({})[{}] current key details: {}", session, service, current);
         }
 
-        PublicKey key;
+        KeyPair keyPair;
         try {
-            key = current.getPublicKey();
+            keyPair = current.getKeyIdentity();
         } catch (Error e) {
-            warn("sendAuthDataRequest({})[{}] failed ({}) to retrieve public key: {}",
+            warn("sendAuthDataRequest({})[{}] failed ({}) to retrieve key identity: {}",
                     session, service, e.getClass().getSimpleName(), e.getMessage(), e);
             throw new RuntimeSshException(e);
         }
 
-        String keyType = KeyUtils.getKeyType(key);
+        PublicKey pubKey = keyPair.getPublic();
+        String keyType = KeyUtils.getKeyType(pubKey);
         NamedFactory<? extends Signature> factory;
         // SSHD-1104 check if the key type is aliased
         if (current instanceof SignatureFactoriesHolder) {
@@ -134,7 +136,12 @@ public class UserAuthPublicKey extends AbstractUserAuth implements SignatureFact
         String name = getName();
         if (debugEnabled) {
             log.debug("sendAuthDataRequest({})[{}] send SSH_MSG_USERAUTH_REQUEST request {} type={} - fingerprint={}",
-                    session, service, name, algo, KeyUtils.getFingerPrint(key));
+                    session, service, name, algo, KeyUtils.getFingerPrint(pubKey));
+        }
+
+        PublicKeyAuthenticationReporter reporter = session.getPublicKeyAuthenticationReporter();
+        if (reporter != null) {
+            reporter.signalAuthenticationAttempt(session, service, keyPair, algo);
         }
 
         Buffer buffer = session.createBuffer(SshConstants.SSH_MSG_USERAUTH_REQUEST);
@@ -143,7 +150,7 @@ public class UserAuthPublicKey extends AbstractUserAuth implements SignatureFact
         buffer.putString(name);
         buffer.putBoolean(false);
         buffer.putString(algo);
-        buffer.putPublicKey(key);
+        buffer.putPublicKey(pubKey);
         session.writePacket(buffer);
         return true;
     }
@@ -161,17 +168,18 @@ public class UserAuthPublicKey extends AbstractUserAuth implements SignatureFact
         /*
          * Make sure the server echo-ed the same key we sent as sanctioned by RFC4252 section 7
          */
-        PublicKey key;
+        KeyPair keyPair;
         boolean debugEnabled = log.isDebugEnabled();
         try {
-            key = current.getPublicKey();
+            keyPair = current.getKeyIdentity();
         } catch (Error e) {
-            warn("processAuthDataRequest({})[{}][{}] failed ({}) to retrieve public key: {}",
+            warn("processAuthDataRequest({})[{}][{}] failed ({}) to retrieve key identity: {}",
                     session, service, name, e.getClass().getSimpleName(), e.getMessage(), e);
             throw new RuntimeSshException(e);
         }
 
-        String curKeyType = KeyUtils.getKeyType(key);
+        PublicKey pubKey = keyPair.getPublic();
+        String curKeyType = KeyUtils.getKeyType(pubKey);
         String rspKeyType = buffer.getString();
         Collection<String> aliases = KeyUtils.getAllEquivalentKeyTypes(curKeyType);
         String algo;
@@ -193,10 +201,10 @@ public class UserAuthPublicKey extends AbstractUserAuth implements SignatureFact
         }
 
         PublicKey rspKey = buffer.getPublicKey();
-        if (!KeyUtils.compareKeys(rspKey, key)) {
+        if (!KeyUtils.compareKeys(rspKey, pubKey)) {
             throw new InvalidKeySpecException(
                     "processAuthDataRequest(" + session + ")[" + service + "][" + name + "]"
-                                              + " mismatched " + algo + " keys: expected=" + KeyUtils.getFingerPrint(key)
+                                              + " mismatched " + algo + " keys: expected=" + KeyUtils.getFingerPrint(pubKey)
                                               + ", actual=" + KeyUtils.getFingerPrint(rspKey));
         }
 
@@ -216,14 +224,19 @@ public class UserAuthPublicKey extends AbstractUserAuth implements SignatureFact
         buffer.putString(name);
         buffer.putBoolean(true);
         buffer.putString(algo);
-        buffer.putPublicKey(key);
-        appendSignature(session, service, name, username, algo, key, buffer);
+        buffer.putPublicKey(pubKey);
+
+        byte[] sig = appendSignature(session, service, name, username, algo, pubKey, buffer);
+        PublicKeyAuthenticationReporter reporter = session.getPublicKeyAuthenticationReporter();
+        if (reporter != null) {
+            reporter.signalSignatureAttempt(session, service, keyPair, algo, sig);
+        }
 
         session.writePacket(buffer);
         return true;
     }
 
-    protected void appendSignature(
+    protected byte[] appendSignature(
             ClientSession session, String service, String name, String username, String algo, PublicKey key, Buffer buffer)
             throws Exception {
         byte[] id = session.getSessionId();
@@ -265,6 +278,26 @@ public class UserAuthPublicKey extends AbstractUserAuth implements SignatureFact
         bs.putString(algo);
         bs.putBytes(sig);
         buffer.putBytes(bs.array(), bs.rpos(), bs.available());
+        return sig;
+    }
+
+    @Override
+    public void signalAuthMethodSuccess(ClientSession session, String service, Buffer buffer) throws Exception {
+        PublicKeyAuthenticationReporter reporter = session.getPublicKeyAuthenticationReporter();
+        if (reporter != null) {
+            reporter.signalAuthenticationSuccess(session, service, (current == null) ? null : current.getKeyIdentity());
+        }
+    }
+
+    @Override
+    public void signalAuthMethodFailure(
+            ClientSession session, String service, boolean partial, List<String> serverMethods, Buffer buffer)
+            throws Exception {
+        PublicKeyAuthenticationReporter reporter = session.getPublicKeyAuthenticationReporter();
+        if (reporter != null) {
+            KeyPair identity = (current == null) ? null : current.getKeyIdentity();
+            reporter.signalAuthenticationFailure(session, service, identity, partial, serverMethods);
+        }
     }
 
     @Override
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 4c5eff4..490bbfb 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
@@ -35,6 +35,7 @@ import org.apache.sshd.client.auth.UserAuthFactory;
 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.channel.ChannelDirectTcpip;
 import org.apache.sshd.client.channel.ChannelExec;
 import org.apache.sshd.client.channel.ChannelShell;
@@ -93,6 +94,7 @@ public abstract class AbstractClientSession extends AbstractSession implements C
     private PasswordIdentityProvider passwordIdentityProvider;
     private PasswordAuthenticationReporter passwordAuthenticationReporter;
     private KeyIdentityProvider keyIdentityProvider;
+    private PublicKeyAuthenticationReporter publicKeyAuthenticationReporter;
     private List<UserAuthFactory> userAuthFactories;
     private SocketAddress connectAddress;
     private ClientProxyConnector proxyConnector;
@@ -215,6 +217,18 @@ public abstract class AbstractClientSession extends AbstractSession implements C
     }
 
     @Override
+    public PublicKeyAuthenticationReporter getPublicKeyAuthenticationReporter() {
+        ClientFactoryManager manager = getFactoryManager();
+        return resolveEffectiveProvider(PublicKeyAuthenticationReporter.class, publicKeyAuthenticationReporter,
+                manager.getPublicKeyAuthenticationReporter());
+    }
+
+    @Override
+    public void setPublicKeyAuthenticationReporter(PublicKeyAuthenticationReporter reporter) {
+        this.publicKeyAuthenticationReporter = 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 40df790..c1062e6 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
@@ -33,6 +33,7 @@ import org.apache.sshd.client.auth.UserAuthFactory;
 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.keyverifier.ServerKeyVerifier;
 import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.client.session.ClientSessionImpl;
@@ -91,6 +92,16 @@ public class ClientAuthenticationManagerTest extends BaseTestSupport {
             }
 
             @Override
+            public PublicKeyAuthenticationReporter getPublicKeyAuthenticationReporter() {
+                return null;
+            }
+
+            @Override
+            public void setPublicKeyAuthenticationReporter(PublicKeyAuthenticationReporter reporter) {
+                throw new UnsupportedOperationException("setPublicKeyAuthenticationReporter(" + 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 2dbe763..c75d07d 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
@@ -42,6 +42,7 @@ 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;
@@ -996,13 +997,21 @@ public class AuthenticationTest extends BaseTestSupport {
     public void testPasswordAuthenticationReporter() throws Exception {
         String goodPassword = getCurrentTestName();
         String badPassword = getClass().getSimpleName();
-        List<String> actual = new ArrayList<>();
+        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 {
-                actual.add(oldPassword);
+                reported.add(oldPassword);
             }
 
             @Override
@@ -1035,7 +1044,77 @@ public class AuthenticationTest extends BaseTestSupport {
             }
         }
 
-        assertListEquals("Attempted passwords", Arrays.asList(badPassword, goodPassword), actual);
+        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 {