Implementing a Challenge-Response Authenticator

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 configuring a password-based authenticator. In practice, this means that when you connect to your SSH server, it advertises both “password” and “keyboard-interactive” as supported authentications. In this default mode, it is performing password authentication over the keyboard-interactive mechanism. This default behaviour 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, 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 three fields in our KeyboardInteractiveProvider implementation.

public class MyKeyboardInteractiveAuthenticator implements KeyboardInteractiveProvider {
   List<Integer> numbers; 
   String answer; 
   boolean done;
   ...
}

The authentication session will start with creating 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 following 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 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); 
}

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; the echo flag 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 implementation 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 on the screen for each authentication attempt.

@Override 
public String getName() {
   return "Higher or Lower"; 
}

We also provide some instructions, which are also printed to the screen:

@Override 
public String getInstruction() { 
   return "Answer the question to show me you are human."; 
}

We also need a way to indicate to the server if the session is authenticated. We added a ‘done’ flag to our class earlier, so we will 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 the setResponse method. We are provided with an array of answers; if you had more than one prompt, these answers would be in the same order as you returned the prompts. 

The method requires that we return a boolean success value; this should indicate the success of this round of prompts, whether 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 rounds 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 complete, returning true to indicate 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.

If the answer was incorrect, we added the prompt back to additionalPrompts and returned false.

Configuring the Server 

As a final step, you must configure the server to use your new authenticator. Add this to the authenticator list, 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