From df9670c9af87f7623d0fbaf5fd65fe30d5512f87 Mon Sep 17 00:00:00 2001 From: Thomas Wolf Date: Wed, 17 Jul 2024 23:17:15 +0200 Subject: [PATCH] GH-533: Fix ClientUserAuthService iteration through methods The ClientUserAuthService iterates through the configured authentication methods and tries them each in order, skipping those that the server did not list as acceptable. However, it only set the "server acceptable" on a partial success, or on the initial failure for the "none" request. This was wrong: if a pubkey auth fails and the server replies with SSH_MSG_USERAUTH_FAILURE and an acceptable methods list not containing "pubkey", then pubkey should not continue even if it had more keys available (or more signatures to try for an RSA key). This case actually occurs, for instance with an XFB.Gateway SSH server and RSA keys, if an rsa-sha2* signature is used. That server apparently knows only ssh-rsa signatures and consequently may reply with SSH_MSG_USERAUTH_FAILURE, partialSuccess=false, allowed=password. (Presumably if the key was right but the signature was unknown, and the user has only a single authorized key, and password auth is enabled server-side.) So reset the "server acceptable" list on any SSH_MSG_USERAUTH_FAILURE, even if partialSuccess = false. Check whether the current method is still allowed, and if not start over instead of continuing with the current method. A second problem was with the handling of multiple required authentication methods. A server may require multiple authentications, for instance first a pubkey auth, then a password auth, and then again a pubkey auth with another key. Such continuations are indicated by SSH_MSG_USERAUTH_FAILURE with partialSuccess = true and a list of methods to try next. For pubkey,password,pubkey the client would get a message SSH_MSG_USERAUTH_FAILURE partialSuccess=true allowed=password after the successful first pubkey authentication, and then a message SSH_MSG_USERAUTH_FAILURE partialSuccess=true allowed=pubkey after the successful password log-in. On that second use of pubkey auth, the previously successful key must not be re-tried, and there's also no point in re-trying previously rejected keys. Thus our code cannot simply create a new UserAuthPublicKey instance again as this would re-load the key iterator from scratch. Instead we must remember the pubkey auth instance once created and re-use that same instance on the second use, so that we keep on with any remaining keys that might be available. See also https://issues.apache.org/jira/browse/SSHD-1229, which may be related. Bug: https://github.com/apache/mina-sshd/issues/533 --- CHANGES.md | 1 + .../client/auth/pubkey/UserAuthPublicKey.java | 28 +- .../client/session/ClientUserAuthService.java | 153 +++++++-- .../client/auth/pubkey/MultiAuthTest.java | 319 ++++++++++++++++++ 4 files changed, 462 insertions(+), 39 deletions(-) create mode 100644 sshd-core/src/test/java/org/apache/sshd/client/auth/pubkey/MultiAuthTest.java diff --git a/CHANGES.md b/CHANGES.md index bfdb282b9..d2e2315d7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -39,6 +39,7 @@ ## Bug Fixes * [GH-524](https://github.com/apache/mina-sshd/issues/524) Performance improvements +* [GH-533](https://github.com/apache/mina-sshd/issues/533) Fix multi-step authentication ## New Features 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 376dfc048..6bd9424a5 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 @@ -473,9 +473,12 @@ protected byte[] appendSignature( @Override public void signalAuthMethodSuccess(ClientSession session, String service, Buffer buffer) throws Exception { + PublicKeyIdentity successfulKey = current; + KeyPair identity = (successfulKey == null) ? null : successfulKey.getKeyIdentity(); + current = null; PublicKeyAuthenticationReporter reporter = session.getPublicKeyAuthenticationReporter(); if (reporter != null) { - reporter.signalAuthenticationSuccess(session, service, (current == null) ? null : current.getKeyIdentity()); + reporter.signalAuthenticationSuccess(session, service, identity); } } @@ -483,9 +486,30 @@ public void signalAuthMethodSuccess(ClientSession session, String service, Buffe public void signalAuthMethodFailure( ClientSession session, String service, boolean partial, List serverMethods, Buffer buffer) throws Exception { + PublicKeyIdentity keyUsed = current; + if (partial) { + // Actually a pubkey success, but we must continue with either this or another authentication method. + // + // Prevent re-use of this key if this instance of UserAuthPublicKey is used again. See OpenBSD sshd_config, + // AuthenticationMethods: "If the publickey method is listed more than once, sshd(8) verifies that keys + // that have been used successfully are not reused for subsequent authentications. For example, + // "publickey,publickey" requires successful authentication using two different public keys." + // + // https://man.openbsd.org/sshd_config#AuthenticationMethods + // + // If the successful key was an RSA key, and we succeeded with an rsa-sha2-512 signature, we might otherwise + // re-try that same key with an rsa-sha2-256 and ssh-rsa signature, which would be wrong. We have to + // continue with the next available key from the iterator. + // + // Note that if a server imposes an order on the keys used in such a case (say, it requires successful + // pubkey authentication first with key A, then with key B), it is the user's responsibility to ensure that + // the iterator has the keys in that order, for instance by specifying them in that order in "IdentityFile" + // directives in the host entry in the client-side ~/.ssh/config. + current = null; + } PublicKeyAuthenticationReporter reporter = session.getPublicKeyAuthenticationReporter(); if (reporter != null) { - KeyPair identity = (current == null) ? null : current.getKeyIdentity(); + KeyPair identity = (keyUsed == null) ? null : keyUsed.getKeyIdentity(); reporter.signalAuthenticationFailure(session, service, identity, partial, serverMethods); } } diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientUserAuthService.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientUserAuthService.java index 8ba615803..ffcfe5b30 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientUserAuthService.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientUserAuthService.java @@ -30,6 +30,7 @@ import org.apache.sshd.client.auth.UserAuth; import org.apache.sshd.client.auth.UserAuthFactory; import org.apache.sshd.client.auth.keyboard.UserInteraction; +import org.apache.sshd.client.auth.pubkey.UserAuthPublicKey; import org.apache.sshd.client.future.AuthFuture; import org.apache.sshd.client.future.DefaultAuthFuture; import org.apache.sshd.common.NamedResource; @@ -66,8 +67,9 @@ public class ClientUserAuthService extends AbstractCloseable implements Service, private final Map properties = new ConcurrentHashMap<>(); private String service; - private UserAuth userAuth; + private UserAuth currentUserAuth; private int currentMethod; + private UserAuth pubkeyAuth; private final Object initLock = new Object(); private boolean started; @@ -150,6 +152,7 @@ public AuthFuture auth(String service) throws IOException { // start from scratch serverMethods = null; + pubkeyAuth = null; currentMethod = 0; clearUserAuth(); @@ -284,15 +287,17 @@ protected void processUserAuth(int cmd, Buffer buffer, AuthFuture authFuture) th if (cmd == SshConstants.SSH_MSG_USERAUTH_SUCCESS) { if (log.isDebugEnabled()) { log.debug("processUserAuth({}) SSH_MSG_USERAUTH_SUCCESS Succeeded with {}", - session, (userAuth == null) ? "" : userAuth.getName()); + session, (currentUserAuth == null) ? "" : currentUserAuth.getName()); } - if (userAuth != null) { + if (currentUserAuth != null) { try { - userAuth.signalAuthMethodSuccess(session, service, buffer); + currentUserAuth.signalAuthMethodSuccess(session, service, buffer); } finally { clearUserAuth(); } + } else { + destroyPubkeyAuth(); } session.setAuthenticated(); ((ClientSessionImpl) session).switchToNextService(); @@ -309,43 +314,82 @@ protected void processUserAuth(int cmd, Buffer buffer, AuthFuture authFuture) th return; } if (cmd == SshConstants.SSH_MSG_USERAUTH_FAILURE) { - String mths = buffer.getString(); + String methods = buffer.getString(); boolean partial = buffer.getBoolean(); if (log.isDebugEnabled()) { log.debug("processUserAuth({}) Received SSH_MSG_USERAUTH_FAILURE - partial={}, methods={}", - session, partial, mths); + session, partial, methods); } - if (partial || (serverMethods == null)) { - serverMethods = Arrays.asList(GenericUtils.split(mths, ',')); - currentMethod = 0; - if (userAuth != null) { - try { - userAuth.signalAuthMethodFailure(session, service, partial, Collections.unmodifiableList(serverMethods), - buffer); - } finally { - clearUserAuth(); + List allowedMethods; + if (GenericUtils.isEmpty(methods)) { + if (serverMethods == null) { + // RFC 4252 section 5.2 says that in the SSH_MSG_USERAUTH_FAILURE response + // to a 'none' request a server MAY return a list of methods. Here it didn't, + // so we just assume all methods that the client knows are fine. + // + // https://datatracker.ietf.org/doc/html/rfc4252#section-5.2 + allowedMethods = new ArrayList<>(clientMethods); + } else if (partial) { + // Don't reset to an empty list; keep going with the previous methods. Sending + // a partial success without methods that may continue makes no sense and would + // be a server bug. + // + // currentUserAuth should always be set here! + if (log.isDebugEnabled()) { + log.debug( + "processUserAuth({}) : potential bug in {} server: SSH_MSG_USERAUTH_FAILURE with partial success after {} authentication, but without continuation methods", + session, session.getServerVersion(), + currentUserAuth != null ? currentUserAuth.getName() : "UNKNOWN"); } + allowedMethods = serverMethods; + } else { + allowedMethods = new ArrayList<>(); } + } else { + allowedMethods = Arrays.asList(GenericUtils.split(methods, ',')); } + if (currentUserAuth != null) { + try { + currentUserAuth.signalAuthMethodFailure(session, service, partial, + Collections.unmodifiableList(allowedMethods), buffer); + } catch (Exception e) { + clearUserAuth(); + throw e; + } + + // Check if the current method is still allowed. + if (allowedMethods.indexOf(currentUserAuth.getName()) < 0) { + if (currentUserAuth == pubkeyAuth) { + // Don't destroy it yet, we might still need it later on + currentUserAuth = null; + } else { + destroyUserAuth(); + } + } + } + if (partial || (serverMethods == null)) { + currentMethod = 0; + } + serverMethods = allowedMethods; tryNext(cmd, authFuture); return; } - if (userAuth == null) { + if (currentUserAuth == null) { throw new IllegalStateException("Received unknown packet: " + SshConstants.getCommandMessageName(cmd)); } if (log.isDebugEnabled()) { log.debug("processUserAuth({}) delegate processing of {} to {}", - session, SshConstants.getCommandMessageName(cmd), userAuth.getName()); + session, SshConstants.getCommandMessageName(cmd), currentUserAuth.getName()); } buffer.rpos(buffer.rpos() - 1); - if (!userAuth.process(buffer)) { + if (!currentUserAuth.process(buffer)) { tryNext(cmd, authFuture); } else { - authFuture.setCancellable(userAuth.isCancellable()); + authFuture.setCancellable(currentUserAuth.isCancellable()); } } @@ -353,21 +397,28 @@ protected void tryNext(int cmd, AuthFuture authFuture) throws Exception { ClientSession session = getClientSession(); // Loop until we find something to try for (boolean debugEnabled = log.isDebugEnabled();; debugEnabled = log.isDebugEnabled()) { - if (userAuth == null) { + if (currentUserAuth == null) { if (debugEnabled) { - log.debug("tryNext({}) starting authentication mechanisms: client={}, server={}", - session, clientMethods, serverMethods); + log.debug("tryNext({}) starting authentication mechanisms: client={}, client index={}, server={}", session, + clientMethods, currentMethod, serverMethods); } - } else if (!userAuth.process(null)) { + } else if (!currentUserAuth.process(null)) { if (debugEnabled) { - log.debug("tryNext({}) no initial request sent by method={}", session, userAuth.getName()); + log.debug("tryNext({}) no initial request sent by method={}", session, currentUserAuth.getName()); + } + if (currentUserAuth == pubkeyAuth) { + // Don't destroy it yet. It might re-appear later if the server requires multiple methods. + // It doesn't have any more keys, but we don't want to re-create it from scratch and re-try + // all the keys already tried again. + currentUserAuth = null; + } else { + destroyUserAuth(); } - clearUserAuth(); currentMethod++; } else { if (debugEnabled) { log.debug("tryNext({}) successfully processed initial buffer by method={}", - session, userAuth.getName()); + session, currentUserAuth.getName()); } return; } @@ -385,7 +436,7 @@ protected void tryNext(int cmd, AuthFuture authFuture) throws Exception { log.debug("tryNext({}) exhausted all methods - client={}, server={}", session, clientMethods, serverMethods); } - + clearUserAuth(); // also wake up anyone sitting in waitFor authFuture.setException(new SshException(SshConstants.SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE, "No more authentication methods available")); @@ -398,30 +449,58 @@ protected void tryNext(int cmd, AuthFuture authFuture) throws Exception { clearUserAuth(); return; } - userAuth = UserAuthMethodFactory.createUserAuth(session, authFactories, method); - if (userAuth == null) { - throw new UnsupportedOperationException("Failed to find a user-auth factory for method=" + method); + if (UserAuthPublicKey.NAME.equals(method) && pubkeyAuth != null) { + currentUserAuth = pubkeyAuth; + } else { + currentUserAuth = UserAuthMethodFactory.createUserAuth(session, authFactories, method); + if (currentUserAuth == null) { + throw new UnsupportedOperationException("Failed to find a user-auth factory for method=" + method); + } } - if (debugEnabled) { log.debug("tryNext({}) attempting method={}", session, method); } - - userAuth.init(session, service); - authFuture.setCancellable(userAuth.isCancellable()); + if (currentUserAuth != pubkeyAuth) { + currentUserAuth.init(session, service); + } + if (UserAuthPublicKey.NAME.equals(currentUserAuth.getName())) { + pubkeyAuth = currentUserAuth; + } + authFuture.setCancellable(currentUserAuth.isCancellable()); if (authFuture.isCanceled()) { authFuture.getCancellation().setCanceled(); clearUserAuth(); + return; } } } private void clearUserAuth() { - if (userAuth != null) { + if (currentUserAuth == pubkeyAuth) { + pubkeyAuth = null; + destroyUserAuth(); + } else { + destroyUserAuth(); + destroyPubkeyAuth(); + } + } + + private void destroyUserAuth() { + if (currentUserAuth != null) { + try { + currentUserAuth.destroy(); + } finally { + currentUserAuth = null; + } + } + } + + private void destroyPubkeyAuth() { + if (pubkeyAuth != null) { try { - userAuth.destroy(); + pubkeyAuth.destroy(); } finally { - userAuth = null; + pubkeyAuth = null; } } } diff --git a/sshd-core/src/test/java/org/apache/sshd/client/auth/pubkey/MultiAuthTest.java b/sshd-core/src/test/java/org/apache/sshd/client/auth/pubkey/MultiAuthTest.java new file mode 100644 index 000000000..aa62348fb --- /dev/null +++ b/sshd-core/src/test/java/org/apache/sshd/client/auth/pubkey/MultiAuthTest.java @@ -0,0 +1,319 @@ +/* + * 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.security.KeyPairGenerator; +import java.security.PublicKey; +import java.util.ArrayList; +import java.util.List; + +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.auth.password.PasswordAuthenticationReporter; +import org.apache.sshd.client.auth.password.PasswordIdentityProvider; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.AttributeRepository.AttributeKey; +import org.apache.sshd.common.auth.UserAuthMethodFactory; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.core.CoreModuleProperties; +import org.apache.sshd.server.SshServer; +import org.apache.sshd.server.auth.AsyncAuthException; +import org.apache.sshd.server.auth.hostbased.RejectAllHostBasedAuthenticator; +import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator; +import org.apache.sshd.server.session.ServerSession; +import org.apache.sshd.util.test.BaseTestSupport; +import org.apache.sshd.util.test.CoreTestSupportUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class MultiAuthTest extends BaseTestSupport { + + private static final String USER_NAME = "foo"; + private static final String PASSWORD = "pass"; + + private SshServer sshd; + private SshClient client; + private int port; + + private KeyPair ecKeyUser; + private KeyPair rsaKeyUser; + + public MultiAuthTest() { + super(); + } + + private static class PubkeyAuth implements PublickeyAuthenticator { + + private static final AttributeKey SUCCESSFUL_AUTH_COUNT = new AttributeKey<>(); + + private final List knownKeys; + + PubkeyAuth(PublicKey... keys) { + knownKeys = GenericUtils.asList(keys); + } + + @Override + public boolean authenticate(String username, PublicKey key, ServerSession session) throws AsyncAuthException { + if (!USER_NAME.equals(username)) { + return false; + } + Integer count = session.getAttribute(SUCCESSFUL_AUTH_COUNT); + int successfulAuths = count == null ? 0 : count.intValue(); + // Server-side interfaces are poor. We should get the "hasSignature" flag. + // We know our client will send two auth requests per key (pre-auth without signature, then auth with + // signature). + int index = successfulAuths / 2; + if (index < knownKeys.size()) { + if (KeyUtils.compareKeys(key, knownKeys.get(index))) { + session.setAttribute(SUCCESSFUL_AUTH_COUNT, Integer.valueOf(successfulAuths + 1)); + return true; + } + } + return false; + } + } + + private static KeyPair getKeyPair(String algorithm, int size) throws Exception { + KeyPairGenerator generator = KeyPairGenerator.getInstance(algorithm); + generator.initialize(size); + return generator.generateKeyPair(); + } + + @Before + public void setupClientAndServer() throws Exception { + sshd = CoreTestSupportUtils.setupTestServer(MultiAuthTest.class); + sshd.setHostBasedAuthenticator(RejectAllHostBasedAuthenticator.INSTANCE); + // Generate two user keys + rsaKeyUser = getKeyPair(KeyUtils.RSA_ALGORITHM, 2048); + ecKeyUser = getKeyPair(KeyUtils.EC_ALGORITHM, 256); + sshd.setPublickeyAuthenticator(new PubkeyAuth(rsaKeyUser.getPublic(), ecKeyUser.getPublic())); + sshd.setPasswordAuthenticator((username, password, session) -> { + return USER_NAME.equals(username) && PASSWORD.equals(password); + }); + sshd.start(); + port = sshd.getPort(); + client = CoreTestSupportUtils.setupTestClient(MultiAuthTest.class); + client.setUserAuthFactoriesNames(UserAuthMethodFactory.PUBLIC_KEY, UserAuthMethodFactory.PASSWORD); + client.start(); + } + + @After + public void teardownClientAndServer() throws Exception { + if (sshd != null) { + try { + sshd.stop(true); + } finally { + sshd = null; + } + } + if (client != null) { + try { + client.stop(); + } finally { + client = null; + } + } + } + + @Test + public void testConnect() throws Exception { + CoreModuleProperties.AUTH_METHODS.set(sshd, "publickey,password,publickey"); + StringBuilder sb = new StringBuilder(); + try (ClientSession session = createClientSession(USER_NAME, client, port)) { + session.setKeyIdentityProvider(ctx -> { + List result = new ArrayList<>(); + result.add(rsaKeyUser); + result.add(ecKeyUser); + return result; + }); + session.setPasswordIdentityProvider(PasswordIdentityProvider.wrapPasswords(PASSWORD)); + session.setPublicKeyAuthenticationReporter(new PubkeyReporter(sb)); + session.setPasswordAuthenticationReporter(new PasswordReporter(sb)); + session.auth().verify(AUTH_TIMEOUT); + } catch (Exception e) { + throw new RuntimeException(e.getMessage() + '\n' + sb.toString(), e); + } + String expected = "publickey TRY RSA rsa-sha2-512\n" // + + "publickey PARTIAL RSA\n" // + + "password TRY pass\n" // + + "password PARTIAL pass\n" // + + "publickey TRY EC ecdsa-sha2-nistp256\n" // + + "publickey SUCCESS EC\n"; + assertEquals(expected, sb.toString()); + } + + @Test + public void testConnect2() throws Exception { + CoreModuleProperties.AUTH_METHODS.set(sshd, "publickey,publickey"); + StringBuilder sb = new StringBuilder(); + try (ClientSession session = createClientSession(USER_NAME, client, port)) { + session.setKeyIdentityProvider(ctx -> { + List result = new ArrayList<>(); + result.add(rsaKeyUser); + result.add(ecKeyUser); + return result; + }); + session.setPublicKeyAuthenticationReporter(new PubkeyReporter(sb)); + session.setPasswordAuthenticationReporter(new PasswordReporter(sb)); + session.auth().verify(AUTH_TIMEOUT); + } catch (Exception e) { + throw new RuntimeException(e.getMessage() + '\n' + sb.toString(), e); + } + String expected = "publickey TRY RSA rsa-sha2-512\n" // + + "publickey PARTIAL RSA\n" // + + "publickey TRY EC ecdsa-sha2-nistp256\n" // + + "publickey SUCCESS EC\n"; + assertEquals(expected, sb.toString()); + } + + @Test + public void testConnect3() throws Exception { + CoreModuleProperties.AUTH_METHODS.set(sshd, "publickey password"); + StringBuilder sb = new StringBuilder(); + try (ClientSession session = createClientSession(USER_NAME, client, port)) { + session.setKeyIdentityProvider(ctx -> { + List result = new ArrayList<>(); + result.add(rsaKeyUser); + result.add(ecKeyUser); + return result; + }); + session.setPublicKeyAuthenticationReporter(new PubkeyReporter(sb)); + session.setPasswordAuthenticationReporter(new PasswordReporter(sb)); + session.auth().verify(AUTH_TIMEOUT); + } catch (Exception e) { + throw new RuntimeException(e.getMessage() + '\n' + sb.toString(), e); + } + String expected = "publickey TRY RSA rsa-sha2-512\n" // + + "publickey SUCCESS RSA\n"; + assertEquals(expected, sb.toString()); + } + + @Test + public void testConnect4() throws Exception { + CoreModuleProperties.AUTH_METHODS.set(sshd, "password,publickey"); + StringBuilder sb = new StringBuilder(); + try (ClientSession session = createClientSession(USER_NAME, client, port)) { + session.setKeyIdentityProvider(ctx -> { + List result = new ArrayList<>(); + result.add(rsaKeyUser); + result.add(ecKeyUser); + return result; + }); + session.setPasswordIdentityProvider(PasswordIdentityProvider.wrapPasswords(PASSWORD)); + session.setPublicKeyAuthenticationReporter(new PubkeyReporter(sb)); + session.setPasswordAuthenticationReporter(new PasswordReporter(sb)); + session.auth().verify(AUTH_TIMEOUT); + } catch (Exception e) { + throw new RuntimeException(e.getMessage() + '\n' + sb.toString(), e); + } + String expected = "password TRY pass\n" // + + "password PARTIAL pass\n" // + + "publickey TRY RSA rsa-sha2-512\n" // + + "publickey SUCCESS RSA\n"; + assertEquals(expected, sb.toString()); + } + + @Test + public void testConnect5() throws Exception { + CoreModuleProperties.AUTH_METHODS.set(sshd, "password,publickey,publickey"); + StringBuilder sb = new StringBuilder(); + try (ClientSession session = createClientSession(USER_NAME, client, port)) { + session.setKeyIdentityProvider(ctx -> { + List result = new ArrayList<>(); + result.add(rsaKeyUser); + result.add(ecKeyUser); + return result; + }); + session.setPasswordIdentityProvider(PasswordIdentityProvider.wrapPasswords(PASSWORD)); + session.setPublicKeyAuthenticationReporter(new PubkeyReporter(sb)); + session.setPasswordAuthenticationReporter(new PasswordReporter(sb)); + session.auth().verify(AUTH_TIMEOUT); + } catch (Exception e) { + throw new RuntimeException(e.getMessage() + '\n' + sb.toString(), e); + } + String expected = "password TRY pass\n" // + + "password PARTIAL pass\n" // + + "publickey TRY RSA rsa-sha2-512\n" // + + "publickey PARTIAL RSA\n" // + + "publickey TRY EC ecdsa-sha2-nistp256\n" // + + "publickey SUCCESS EC\n"; + assertEquals(expected, sb.toString()); + } + + private static class PubkeyReporter implements PublicKeyAuthenticationReporter { + + private final StringBuilder out; + + PubkeyReporter(StringBuilder sink) { + out = sink; + } + + @Override + public void signalAuthenticationAttempt(ClientSession session, String service, KeyPair identity, String signature) + throws Exception { + out.append("publickey TRY ").append(identity == null ? "null" : identity.getPublic().getAlgorithm()).append(' ') + .append(signature == null ? "null" : signature).append('\n'); + } + + @Override + public void signalAuthenticationSuccess(ClientSession session, String service, KeyPair identity) throws Exception { + out.append("publickey SUCCESS ").append(identity == null ? "null" : identity.getPublic().getAlgorithm()) + .append('\n'); + } + + @Override + public void signalAuthenticationFailure( + ClientSession session, String service, KeyPair identity, boolean partial, + List serverMethods) throws Exception { + out.append("publickey ").append(partial ? "PARTIAL " : "FAILURE ") + .append(identity == null ? "null" : identity.getPublic().getAlgorithm()).append('\n'); + } + } + + private static class PasswordReporter implements PasswordAuthenticationReporter { + + private final StringBuilder out; + + PasswordReporter(StringBuilder sink) { + out = sink; + } + + @Override + public void signalAuthenticationAttempt( + ClientSession session, String service, String oldPassword, boolean modified, + String newPassword) throws Exception { + out.append("password TRY " + oldPassword).append('\n'); + } + + @Override + public void signalAuthenticationSuccess(ClientSession session, String service, String password) throws Exception { + out.append("password SUCCESS " + password).append('\n'); + } + + @Override + public void signalAuthenticationFailure( + ClientSession session, String service, String password, boolean partial, + List serverMethods) throws Exception { + out.append("password ").append(partial ? "PARTIAL " : "FAILURE ").append(password == null ? "null" : password) + .append('\n'); + } + } +}