Writing a MariaDB PAM Authentication Plugin

As you may know, since version 5.2.0 (released in April 2010) we support Pluggable Authentication. Using this feature one can implement an arbitrary user authentication and account management policy, completely replacing built-in MariaDB authentication with its username/password combination and mysql.user table.

Also, as you might have heard, Oracle has recently released a PAM authentication plugin for MySQL. Alas, this plugin will not run on MariaDB — although the MySQL implementation of pluggable authentication is based on ours, the API is incompatible. And, being closed source, this plugin cannot be fixed to run in MariaDB. And — I’m not making it up — this plugin does not support communication between the client and the server, so even with this plugin and all the power of PAM the only possible authentication method remains a simple username/password combination.

But writing authentication plugins is easy, I said to myself. I will do my own authentication plugin! With blackjack and hookers.

I started by installing the development headers:

sudo rpm -ivh MariaDB-devel-5.2.9-102.el5.x86_64.rpm

On Debian or Ubuntu you would’ve needed to install libmariadbclient-dev. By the way, a disclaimer — I’m doing it for MariaDB-5.2, but with minimal changes this plugin can work with MySQL-5.5 too.

Now I create a working directory and, being a lazy guy, copy the auth_socket plugin  sources — one of the authentication plugins that come with MariaDB — from Launchpad (only auth_socket.c). Stripped down, with the old code removed, it becomes my pam.c:

#define MYSQL_DYNAMIC_PLUGIN
#include <mysql/plugin_auth.h>

static int pam_auth(MYSQL_PLUGIN_VIO *vio, MYSQL_SERVER_AUTH_INFO *info)
{
}

static struct st_mysql_auth pam_info =
{
  MYSQL_AUTHENTICATION_INTERFACE_VERSION,
  "dialog",
  pam_auth
};

mysql_declare_plugin(pam)
{
  MYSQL_AUTHENTICATION_PLUGIN,
  &pam_info,
  "pam",
  "Sergei Golubchik",
  "PAM based authentication",
  PLUGIN_LICENSE_GPL,
  NULL,
  NULL,
  0x0100,
  NULL,
  NULL,
  NULL
}
mysql_declare_plugin_end;

At the end of the file we have the plugin descriptor — it always has the same structure for all plugin types. Above it — authentication plugin descriptor, it tells MariaDB which function performs the actual authentication, and what plugin the client should use.

Let me repeat — what plugin the client should use. Indeed, an authentication process is always a dialog. The server asks questions (“username?”, “password?”), the client answers them. Because a loadable plugin may cause the server to ask the most unexpected questions (“the fingerprint of the left index finger?”), the client should support plugins too — which know how to answer them. And it does support them — or, more precisely, libmysqlclient does, automatically and transparently for the client applications.

In this particular case, though, the questions aren’t very exotic. PAM may only ask the end user to enter some text, so the client plugin needs to be able to print prompt text, read the user’s input, and send it back to the server. And repeat until the server is satisfied. Luckily, MariaDB already has a plugin to perform such a dialog with the user. The plugin is called, not surprisingly, dialog, and in my plugin descriptor, I specify that the pam server plugin needs the client to load the dialog plugin to be able to continue the authentication.

Now, let’s see if this plugin skeleton works:

gcc -o pam.so pam.c `mysql_config --cflags` -shared -fPIC -lpam

It compiles and even loads into the server. Looks good so far, and I open man pam.

According to the man pages, to perform a PAM authentication one needs to do the following:

  1. initialize the PAM subsystem with the pam_start() function.
  2. invoke pam_authenticate() which performs the actual authentication
  3. verify the user’s account with the pam_acct_mgmt()
  4. in the process of authentication, PAM can change the user name. Retrieve the new name with pam_get_item(PAM_USER)
  5. at the end one should always call pam_end()

To talk to the client, PAM allows one to specify a conversation function — the function that PAM will invoke as necessary.

I then put the above logic into the main pam_auth() function:

#include <string.h>
#include <security/pam_modules.h>
#include <security/pam_appl.h>

static int conv(int n, const struct pam_message **msg,
                struct pam_response **resp, void *data)
{
}

#define DO(X) if ((status = (X)) != PAM_SUCCESS) goto end

static int pam_auth(MYSQL_PLUGIN_VIO *vio, MYSQL_SERVER_AUTH_INFO *info)
{
  pam_handle_t *pamh = NULL;
  int status;
  const char *new_username;
  struct param param;
  struct pam_conv c = { &conv, &param };

  /* get the service name, as specified in

     CREATE USER ... IDENTIFIED WITH pam_auth AS  "service"

  */
  const char *service = info->auth_string ? info->auth_string : "mysql";

  param.ptr = param.buf + 1;
  param.vio = vio;

  DO( pam_start(service, info->user_name, &c, &pamh) );
  DO( pam_authenticate (pamh, 0) );
  DO( pam_acct_mgmt(pamh, 0) );
  DO( pam_get_item(pamh, PAM_USER, (const void**)&new_username) );

  if (new_username)
    strncpy(info->authenticated_as, new_username,
            sizeof(info->authenticated_as));

end:
  pam_end(pamh, status);
  return status == PAM_SUCCESS ? CR_OK : CR_ERROR;
}

The plugin is almost done. The only missing bit is the conversation function conv(). According to the PAM documentation, it will be invoked with an array of “questions”, which should be shown to the user, and it must return the user’s answers. Additionally, it will get one opaque pointer argument — callback functions almost always have it in almost all the APIs in the world. From this function I will send the “questions” to the client, and receive the answers. The dialog plugin on the client side will do the actual communication with the user.

Sending and receiving is easy in the Pluggable Authentication API. One of the arguments of the main authentication function — pam_auth() in our case — is a so called vio handle. This handle provides read_packet() and write_packet() functions, which the client and server plugins can use to communicate with each other. The server will take care of everything else — delivering packets, splitting and reassembling them, encrypting (if SSL is used), using unix sockets, tcp/ip, named pipes, shared memory, making sure that the server plugin talks to the right client plugin, maintaining backward compatible protocol on the wire, and so on. That’s, by the way, where the name vio comes from — it means Virtual I/O.

There is one last difficulty to overcome. PAM can send four different types of messages, two of them being purely informational, with the meaning “print this to the user”, and two being input messages, with the meaning “print this and read the reply”. The dialog plugin, however, supports only “print this and read the reply” kinds of actions. To solve this API mismatch, our conversation function will accumulate PAM informational messages until it sees the first input message. Then it’ll send all accumulated and concatenated messages to the dialog plugin as one big prompt string, in one packet. This is what I mean:

struct param {
  unsigned char buf[10240], *ptr;
  MYSQL_PLUGIN_VIO *vio;
};

static int conv(int n, const struct pam_message **msg,
                struct pam_response **resp, void *data)
{
  struct param *param = (struct param *)data;
  unsigned char *end = param->buf + sizeof(param->buf) - 1;
  int i;

  for (i = 0; i < n; i++) {
    /* if there's a message - append it to the buffer */
    if (msg[i]->msg) {
      int len = strlen(msg[i]->msg);
      if (len > end - param->ptr)
        len = end - param->ptr;
      memcpy(param->ptr, msg[i]->msg, len);
      param->ptr+= len;
      *(param->ptr)++ = 'n';
    }
    /* if the message style is *_PROMPT_*, meaning PAM asks a question,
       send the accumulated text to the client, read the reply */
    if (msg[i]->msg_style == PAM_PROMPT_ECHO_OFF ||
        msg[i]->msg_style == PAM_PROMPT_ECHO_ON) {
      int pkt_len;
      unsigned char *pkt;

      /* allocate the response array.
         freeing it is the responsibility of the caller */
      if (*resp == 0) {
        *resp = calloc(sizeof(struct pam_response), n);
        if (*resp == 0)
          return PAM_BUF_ERR;
      }

      /* dialog plugin interprets the first byte of the packet
         as the magic number.
           2 means "read the input with the echo enabled"
           4 means "password-like input, echo disabled"
         C'est la vie. */
      param->buf[0] = msg[i]->msg_style == PAM_PROMPT_ECHO_ON ? 2 : 4;
      if (param->vio->write_packet(param->vio, param->buf, param->ptr - param->buf - 1))
        return PAM_CONV_ERR;

      pkt_len = param->vio->read_packet(param->vio, &pkt);
      if (pkt_len < 0)
        return PAM_CONV_ERR;
      /* allocate and copy the reply to the response array */
      (*resp)[i].resp = strndup((char*)pkt, pkt_len);
      param->ptr = param->buf + 1;
    }
  }
  return PAM_SUCCESS;
}

That’s all. Now I can compile it as above (repeating the process twice, because I forgot -lpam the first time), load it, configure PAM to use pam_skey for the “mysql” service, create the user and, finally, login:

$ mysql -u root

Welcome to the MariaDB monitor. Commands end with ; or g.
Your MariaDB connection id is 1
Server version: 5.2.9-MariaDB-debug Source distribution

MariaDB [(none)]> CREATE USER serg IDENTIFIED VIA pam USING 'mysql';
Query OK, 0 rows affected (0.00 sec)

MariaDB [(none)]> ^DBye

$ mysql -u serg
challenge otp-md5 99 th91334
password: <enter>
(turning echo on)
pasword: OMEN US HORN OMIT BACK AHOY

Welcome to the MariaDB monitor. Commands end with ; or g.
Your MariaDB connection id is 2
Server version: 5.2.9-MariaDB-debug Source distribution

MariaDB [(none)]> SELECT "Hey-ho! It works!!!";