0
votes

I'm trying to set up a simple SSH client that verifies server's public key via known hosts file, using Apache Mina SSHD. I've pieced together below pesudo-code that works - well, it would work, if I knew how to make the client wait for server hostkey verification to complete.

import java.io.IOException;
import java.net.SocketAddress;
import java.nio.file.*;
import java.security.PublicKey;
import java.util.concurrent.TimeUnit;
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.config.hosts.KnownHostEntry;
import org.apache.sshd.client.future.*;
import org.apache.sshd.client.keyverifier.*;
import org.apache.sshd.client.session.ClientSession;

public class SshdTest {
    public static void main(String[] args) throws IOException {
        Path knownHostsPath = Paths.get(System.getProperty("user.home"), ".ssh", "known_hosts");
        SshClient ssh = SshClient.setUpDefaultClient();
        
        KnownHostsServerKeyVerifier verifier = new DefaultKnownHostsServerKeyVerifier(new ServerKeyVerifier() {
            @Override
            public boolean verifyServerKey(ClientSession cs, SocketAddress sa, PublicKey pk) {
                return true; // ask user to verify unknown public key
            }
        }, true, knownHostsPath);
        verifier.setModifiedServerKeyAcceptor(new ModifiedServerKeyAcceptor() {
            @Override
            public boolean acceptModifiedServerKey(ClientSession cs, SocketAddress sa, KnownHostEntry khe, PublicKey expected, PublicKey actual) throws Exception {
                return true; // ask user to verify public key change
            }
        });
        ssh.setServerKeyVerifier(verifier);
        
        ssh.start();
        
        String host = "localhost";
        String username = "user";
        int port = 22;        
        String password = "password";
        
        ConnectFuture cf = ssh.connect(username, host, port).verify(5L, TimeUnit.SECONDS);
        cf.await();
        
        // wait for server hostkey verification to complete and check ok?

        try ( 
            ClientSession session = cf.getSession()) {
            session.addPasswordIdentity(password);
            AuthFuture auth = session.auth();
            auth.await(5L, TimeUnit.SECONDS);
            
            // check ok and do the work
        }
        
        ssh.stop();
    }
}

I expected cf.await() line to encompass both the connection and hostkey verification process, but it doesn't, which means user is asked for both password and hostkey verification at the same time or in some undefined order if the code stays as is.

How do I make the client wait for server hostkey verification to complete before attempting anything else, such as user authentication? Also, how do I check if the verification was successful?

1

1 Answers

0
votes

Ended up implementing synchronization for this on my own, but not sure if that is the right way to go. I doubt that an API like Apache SSHD would not provide a way to achieve the same, but I have not been able to find it.

FWIW, pseudo-code below.

import java.io.IOException;
import java.net.SocketAddress;
import java.nio.file.*;
import java.security.PublicKey;
import java.util.Objects;
import java.util.concurrent.*;
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.config.hosts.KnownHostEntry;
import org.apache.sshd.client.future.*;
import org.apache.sshd.client.keyverifier.*;
import org.apache.sshd.client.session.ClientSession;

public class SshdTest {
    public static void main(String[] args) throws IOException {
        Path knownHostsPath = Paths.get(System.getProperty("user.home"), ".ssh", "known_hosts");
        SshClient ssh = SshClient.setUpDefaultClient();
        
        final BlockingQueue<Boolean> hostKeyVerification = new ArrayBlockingQueue<>(1);
        
        KnownHostsServerKeyVerifier verifier = new DefaultKnownHostsServerKeyVerifier(new ServerKeyVerifier() {
            @Override
            public boolean verifyServerKey(ClientSession cs, SocketAddress sa, PublicKey pk) {
                boolean ret = true;
                hostKeyVerification.offer(ret);
                return ret; // ask user to verify unknown public key
            }
        }, true, knownHostsPath) {
            @Override
            protected boolean acceptKnownHostEntry(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey, KnownHostEntry entry) {
                boolean ret = super.acceptKnownHostEntry(clientSession, remoteAddress, serverKey, entry);
                hostKeyVerification.offer(ret);
                return ret;
            }};
        verifier.setModifiedServerKeyAcceptor(new ModifiedServerKeyAcceptor() {
            @Override
            public boolean acceptModifiedServerKey(ClientSession cs, SocketAddress sa, KnownHostEntry khe, PublicKey expected, PublicKey actual) throws Exception {
                boolean ret = true;
                hostKeyVerification.offer(ret);
                return ret; // ask user to verify public key change
            }
        });
        ssh.setServerKeyVerifier(verifier);
        
        ssh.start();
        
        String host = "localhost";
        String username = "user";
        int port = 22;        
        String password = "password";
        
        ConnectFuture cf = ssh.connect(username, host, port).verify(5L, TimeUnit.SECONDS);
        cf.await();
        
        // wait for server hostkey verification to complete and check ok
        try {
            Boolean hostKeyVerified = hostKeyVerification.take();
            if (Objects.equals(hostKeyVerified, Boolean.FALSE)) {
                throw new IOException("failed to verify server host key");
            }
        } catch (InterruptedException ex) {
            throw new IOException("interrupted while waiting for hostkey verification process to complete");
        }

        try (ClientSession session = cf.getSession()) {
            session.addPasswordIdentity(password);
            AuthFuture auth = session.auth();
            auth.await(5L, TimeUnit.SECONDS);
            
            // check ok and do the work
        }
        
        ssh.stop();
    }
}