Introduction
The SSH protocol supports a generic challenge-response authentication mechanism called keyboard-interactive. When you build your server using the Maverick Synergy Java SSH API this mechanism is automatically supported when you configure a password-based authenticator. This actually means in practice that when you connect to your SSH server it advertises both "password" and "keyboard-interactive" as supported authentications. In this default mode, it is in fact performing password authentication over the keyboard-interactive mechanism. This default behavior helps to ensure you are compatible with the greatest number of clients.
If you wanted to support a different type of challenge-response authentication other than password then you would need to implement your own KeyboardInteractiveProvider. The interface for which is defined as:
public interface KeyboardInteractiveProvider {
public interface KeyboardInteractiveProvider {
KBIPrompt[] init(SshConnection con);
boolean setResponse(String[] answers, Collection<KBIPrompt> additionalPrompts) throws IOException;
String getName();
String getInstruction();
boolean hasAuthenticated();
}
An instance of this object is created for each authentication session so you can build a relatively compact implementation that maintains its state within itself.
In this article, we will implement a custom challenge-response authentication that will ask the user to select the higher value of a set of numbers.
The Implementation
When a user logs in we will generate some numbers, present these to the user and ask them to select the highest.
So we're going to store 3 fields in our KeyboardInteractiveProvider implementation.
public class MyKeyboardInteractiveAuthenticator implements KeyboardInteractiveProvider {
List<Integer> numbers;
String answer;
boolean done;
...
}
The authentication session will start with the creation of a new instance of your KeyboardInteractiveProvider. The system will call the init method and expect you to return the first set of prompts that will be shown to the user.
This means we need to generate the numbers:
private Set<Integer> generateRandomNumbers(int count) {
if(count <= 1) {
throw new IllegalArgumentException();
}
Set<Integer> results = new HashSet<>();
SecureRandom rnd = new SecureRandom();
while(results.size() < count) {
results.add(rnd.nextInt(99));
}
return results;
}
This code generates random numbers between 0 and 99 until the number requested "count" is satisfied.
We also need a method to select the highest number, i.e. the correct answer that the user should provide:
private String getHighestNumber() {
Integer i = -1;
for(Integer n : numbers) {
if(i < n) {
i = n;
}
}
return String.valueOf(i);
}
And then we should create a way to generate the prompt:
private KBIPrompt generatePrompt() {
StringBuffer buf = new StringBuffer("What is the highest number: ");
for(int i=0;i<numbers.size();i++) {
if(i > 0) {
if(i == numbers.size()-1) {
buf.append(" or ");
} else {
buf.append(", ");
}
}
buf.append(numbers.get(i));
}
buf.append("? ");
return new KBIPrompt(buf.toString(), true);
}
Note the boolean parameter to KBIPrompt, this is the echo flag and it tells the client that it can echo the answer to the terminal. If this was a sensitive secret like a password you should use false to ensure the client does not echo the value to the terminal.
When we put this all together in our init method it looks like this:
@Override
public KBIPrompt[] init(SshConnection con) {
this.numbers = new ArrayList<>(generateRandomNumbers(2));
this.answer = getHighestNumber();
return new KBIPrompt[] { generatePrompt() };
}
Here we have decided only to show two numbers, hence the parameter to generateRandomNumbers.
This is most of the implemention completed. We now only have a few remaining methods to implement from KeyboardInteractiveProvider. Let's get the simple ones out of the way.
We must return a name for the mechanism. This is generally printed to screen for each authentication attempt.
@Override
public String getName() {
return "Higher or Lower";
}
We also provide some instructions which are also printed to screen:
@Override
public String getInstruction() {
return "Answer the question to show me you are human.";
}
We also need a way to indicate back to the server if the session is authenticated. We added a 'done' flag to our class earlier so we will simply return that:
@Override
public boolean hasAuthenticated() {
return done;
}
So finally we have to implement the verification of the user's answer. We do this in setResponse method. We are provided with an array of answers, if you had more than one prompt then these answers would be in the same order that you returned the prompts.
The method requires that we return a boolean success value, this should indicate the success of this round of prompts, were they answered correctly or not, it does not indicate the success of the entire authentication attempt. For example, if you had more prompts to return to the user, you would still return true in setResponse if those round of responses were correct. Use the hasAuthenticated method of your implementation to return the state of the actual authentication attempt.
@Override
public boolean setResponse(String[] answers, Collection<KBIPrompt> additionalPrompts) throws IOException {
if(answers.length==1) {
if(answers[0].equals(answer)) {
done = true;
return true;
}
}
additionalPrompts.add(generatePrompt());
return false;
}
We check the result and set the done flag if we are complete, returning true to indicate back to the server that this round of prompts was successfully entered. The system will check hasAuthenticated next and if true, will allow the user to continue to the next authentication or into their session if all authentications are complete.
In the case that the answer was not correct, we added the prompt back to additionalPrompts and return false.
Configuring the Server
As a final step, you must configure the server to use your new authenticator. Just add this to the authenticator list, just pass your class to KeyboardInteractiveAuthenticator and it will handle creation for you.
server.addAuthenticator(new KeyboardInteractiveAuthenticator(
MyKeyboardInteractiveAuthenticator.class));
You can now run the server with your keyboard-interactive challenge-response authentication mechanism
ssh -p 2222 admin@localhost
Higher or Lower
Answer the question to show me you are human.
What is the highest number: 88 or 26? 88