Saturday, January 18, 2014

Public key authentication with libssh

Continuing my previous post about libssh, the next question was how to handle authentication.
The library (version 0.6.0) allows for several methods, including passwords, public keys, keyboard interactive and GSSAPI. For my use case, I knew passwords were right out because once the shared key was passed around, it would be worthless with no way to control access. I didn't like the sound of keyboard interactive and don't know a thing about GSSAPI. I've been using public key authentication with ssh for many years, so that seemed like a good choice.

Once a session has been established and keys exchanged, you are ready to accept the authentication.
Set your public key authorization callback in the server callbacks struct.
   struct ssh_server_callbacks_struct cb = {   
      .auth_pubkey_function = auth_pubkey
   };
Tell it that you only want public key authentication. It will automatically deny all attempts to use passwords and other methods for you.
  ssh_set_auth_methods(session,SSH_AUTH_METHOD_PUBLICKEY);
Create the event loop that will take care of all the I/O for you and simply invoke your authentication callback at the appropriate time to let you decide what the answer will be.
    mainloop = ssh_event_new();
    ssh_event_add_session(mainloop, session);

    while (!(authenticated && chan != NULL)){
        if(error)
            break;
        r = ssh_event_dopoll(mainloop, -1);
    }
The public key authentication callback looks like this. You are given a pointer to the session, any username that was used, a pointer to the public key that was offered, the key's signature state and your own application user data (if any).
   int auth_pubkey( ssh_session session,
                               const char *user,
                               struct ssh_key_struct *pubkey,
                               char signature_state,
                               void *userdata );
So what do we do with all this? I had a brief moment of initial confusion about being handed a public key instead of a private key. Well duh, you don't ever send the private key anywhere or it would not remain private for long, would it? Referring to RFC 4252 - The Secure Shell (SSH) Authentication Protocol, section 7, we see what's going on. Back on the client side, the SSH client uses your private key to sign a copy of your public key. Then the public key and signature get sent to the server and show up in this callback. Actually, the library has already evaluated the validity of the signature and simply gives you the signature_state enumerated value. You need the whole public key, however, because you're likely going to compare that to one or more public keys already installed on the server side to see if this one is a match. Using OpenSSH, for example, you would have your public key installed on the server side in the $HOME/.ssh/authorized_keys2 file. The server sees you attempting a login as user 'fred' and compares the public key offered from the client side to all the keys in user fred's authorized_keys2 file. If the signature is valid and there is a match, you're in. If not, then you are rejected.

In my use case, there is no relevant username because we're not logging into a system. So I just ignore that value. It can even be omitted on the client side. I chose to simply have a directory within my application's data area that is the designated location for authorized public keys. This is equivalent to the authorized_keys2 file. There is little difference between keeping them all in one file or having separate files in one directory. So my auth_pubkey function logic looks like this (error-checking elided for clarity).

   if ( signature_state == SSH_PUBLICKEY_STATE_NONE )
      return SSH_AUTH_PARTIAL;

   if ( signature_state != SSH_PUBLICKEY_STATE_VALID )
      return SSH_AUTH_DENIED;

   // valid signature received
   // loop through the keys directory, for each key file 'f'
   ssh_key k;
   ssh_pki_import_pubkey_file( f, &k );
   int result = ssh_key_cmp( k, pubkey, SSH_KEY_CMP_PUBLIC );
   ssh_key_free(k);
   if ( result == 0 )
      authenticated = true; // to exit the event polling loop
      return SSH_AUTH_SUCCESS;

  // if no matches
  return SSH_AUTH_DENIED;

This works but leaves me with one open question. Why does it get called twice? Once with a signature state of NONE and then again with VALID after the client has given the private key passphrase. In fact, if you don't return PARTIAL in response to NONE, then the client side never prompts for the passphrase at all. With this logic, the client side (at least OpenSSH) shows an extra line of verbose output stating "Authenticated with partial success". That's harmless but I will continue to investigate and see if I can clean that up.

Note that there are two mechanisms at work here to denote that authentication is complete. You return SUCCESS/PARTIAL/DENIED for the sake of the library logic that is taking care of so many things for you. You also set the 'authenticated' boolean to true to break out of the application-managed event loop. Are these two controls redundant? Not at all. Suppose you wanted to be extra secure and use multiple authentication mechanisms. They all get processed off this one loop with all the respective method callbacks firing. Each one of them is telling the library if that particular method is successful or not. Then you still need something like the authenticated boolean to reflect your application's overall success state, i.e. once all the methods have succeeded.


It does not get much easier than this. Many thanks to the authors of libssh for encapsulating so much of the work.

This post maps to CompTIA SY0-301 exam objective 1.4.

1 comment: