Authenticating Users with Public Keys

Lee Painter

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. If you want to support public key authentication in your server implementation then you should either implement this interface or choose one of the built-in methods of public key authentication.

Built-in Authenticators

The following authenticators are available that implement public key authentication.

Authorized Keys

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

In order to support this type of authenticator, you will need to have a valid file system installed and each user that you want to login 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 into the constructor. It is assumed that the path is relative to the users home folder.

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


Gateway Authenticator

The JADAPTIVE Authentication Gateway provides a mechanism for users to store their private keys on their mobile phones and authenticate using these with any SSH client. You can add support for this gateway to your own server implementations using the GatewayKeyAuthenticationProvider.

To configure your server with this authenticator use:

server.addAuthenticator(new GatewayKeyAuthenticationProvider());

By default, this will use our default cloud Authentication Gateway at gateway.sshtools.com. If you have your own installation of the Authentication Gateway you can pass the host/port through in the constructor.

server.addAuthenticator(new GatewayKeyAuthenticationProvider("keyserver.example.com", 443));


OpenSSH Certificate Authenticator

OpenSSH provides a certificate authentication mechanism using specially formatted public keys. Rather than trusting each individual key you can instead trust a single Certificate Authority key that signs users keys. This prevents you from having to manage authentication for each individual account and instead have 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 that is signed by this CA certificate they will be allowed access. 

 

Building your own Authenticator

If you require a different source for keys you can build your own public key authenticator. 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 an acceptable key 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 these keys and load them into SshPublicKey instances then you can use the equals method to compare and return a value.

Consider the InMemoryPublicKeyAuthenticator implementation. It is holding a map of usernames and public keys and simply checks the key provided as an argument to isAuthorizedKey is equal to 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 first 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 its signs a request with the users private key. The validation of the signature is performed by the server automatically 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 if the call is for a real authentication attempt or not, you can override checkKey and return from that method if the key is acceptable for the user. That way you know that any call to isAuthorizedKey is a real authentication attempt. 

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