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 <lgoldst...@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:d...@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:d...@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 {