Authenticating Users with Public Keys

Public key authentication provides many security benefits over password authentication and is a common requirement for authentication when users create their own SSH servers. 

The Maverick Synergy SSH API supports public key authentication through the PublicKeyAuthenticationProvider interface. Suppose you want to support public key authentication in your server implementation. In that case, you should either implement this interface or choose one of the built-in public key authentication methods.

Built-in Authenticators

The following authenticators are available that implement public key authentication.

Authorized Keys

This authenticator supports using OpenSSH style authorized_keys files in the user’s ~/.ssh folder. The authorized_keys file is a list of the public keys a user can use to log into the system using public key authentication. 

To support this authenticator, you will need to have a valid file system installed, and each user that you want to log in using public key authentication should have a file accessible via ~/.ssh/authorized_keys.

To configure your server with this authenticator, use:

server.addAuthenticator(new AuthorizedKeysPublicKeyAuthenticationProvider());

If you prefer to change the path and have an alternative path consulted, then pass this path to the constructor. The path is assumed to be relative to the user’s home folder.

server.addAuthenticator(new AuthorizedKeysPublicKeyAuthenticationProvider(".myapp/keys"));

OpenSSH Certificate Authenticator

OpenSSH provides a certificate authentication mechanism using specially formatted public keys. Rather than trusting each key, you can instead trust a single Certificate Authority key that signs users’ keys. This prevents you from managing authentication for each account and instead has your server trust a set of CA certificates, much like X509.

To configure your server with this authenticator, use:

server.addAuthenticator(new OpenSshCertificateAuthenticationProvider(SshKeyUtils.getPublicKey("certs/ca.pub"));

This loads the authenticator and trusts the public keys passed in the constructor as CA keys. If a user presents a valid key signed by this CA certificate, they will be allowed access. 

Building your own Authenticator

You can build your own public key authenticator if you require a different key source. In the first instance, you should consider extending AbstractPublicKeyAuthenticationProvider. This only requires that you implement a single method, isAuthorizedKey.

public class MyPublicKeyAuthenticator extends AbstractPublicKeyAuthenticationProvider {
      @Override 
      public boolean isAuthorizedKey(SshPublicKey key, SshConnection con) throws IOException { 
            return false; 
      }
}

The only responsibility of your implementation is to check if the public key provided is acceptable for the user of the current SshConnection. The SshConnection getUsername method will tell you the name of the current user. How and where you store the keys is up to you. If you can retrieve and load them into SshPublicKey instances, you can use the equals method to compare and return a value.

Consider the InMemoryPublicKeyAuthenticator implementation. It holds a map of usernames and public keys and checks the key provided as an argument to isAuthorizedKey equals the key we have in memory on the map.

public class InMemoryPublicKeyAuthenticator extends AbstractPublicKeyAuthenticationProvider { 
   Map<String,SshPublicKey> authorizedKeys = new HashMap<>(); 

   public InMemoryPublicKeyAuthenticator() { }

   public InMemoryPublicKeyAuthenticator addAuthorizedKey(String username, SshPublicKey key) { 
      authorizedKeys.put(username, key); 
      return this; 
   } 

   @Override 
   public boolean isAuthorizedKey(SshPublicKey key, SshConnection con) throws IOException { 
      return key.equals(authorizedKeys.get(con.getUsername())); 
   }
}

Additional Information

Generally, an SSH client will ask the server if a key is acceptable. If the server responds that it is, the client follows up with a real authentication attempt where it signs a request with the user’s private key. The server automatically performs the signature is validation; however, in this situation, you may notice that you receive two calls to isAuthorizedKey. This is because the abstract implementation of AbstractPublicKeyAuthenticationProvider implements the checkKey method by delegating the call to isAuthorizedKey. When the client asks the server if a key is acceptable, the API calls checkKey. When the real authentication attempt is being performed, it calls isAuthorizedKey. In most situations, this will be acceptable; however, if you need to distinguish whether the call is for a real authentication attempt, you can override checkKey and return from that method if the key is acceptable for the current user. That way, you know that any call to isAuthorizedKey is a real authentication attempt. 

Other methods on the base PublicKeyAuthenticationProvider interface are related to the listing, adding and removing keys. These are designed for use with the Public Key Subsystem, which allows users to manipulate their authorized keys, but this is unfortunately out of the scope of this document.