PAM Authentication in MySQL

On Habré it was already written that in MySQL there was an opportunity to replace the built-in authentication procedure by loading the corresponding plug-in. In this plugin, you can implement a completely arbitrary user authentication policy, completely moving away from the traditional username / password scheme in MySQL in the mysql.user table .

Recently, Oracle released a PAM authentication plugin . When using it, the server does not look for passwords in mysql.user , but transfers the authentication task to PAM, a subsystem specially designed for solving authentication problems in various applications and contexts, with flexibly customizable rules and plug-in modules on the fly.

Unfortunately, this plugin has several drawbacks. Firstly, it is distributed only with the commercial version of MySQL and its sources are closed. Secondly, it does not support communication between the user and the pam-module, and password authentication remains the only possible one. Which, as it were, kills the whole idea.

“But why ...” I thought. “I will write my pam-plugin, with blackjack and whores!”

I’m more used to working with MySQL sources, so I download 5.5 and unpack it. Although this is not necessary for such a plugin, only the mysql-devel package is enough .

Now I'm preparing myself a sandbox:
mysql-5.5.17 $ mkdir plugin / pam_auth
mysql-5.5.17 $ cd plugin / pam_auth

As true Jedi, we are not writing anything from scratch - so I took auth_socket.c and removed everything superfluous at first. It turned out somewhere like this:

#include 
static int pam_auth(MYSQL_PLUGIN_VIO *vio, MYSQL_SERVER_AUTH_INFO *info)
{
}
static struct st_mysql_auth pam_auth_handler =
{
  MYSQL_AUTHENTICATION_INTERFACE_VERSION,       /* auth API version     */
  "dialog",                                     /* client plugin name   */
  pam_auth                                      /* main auth function   */
};
mysql_declare_plugin(pam_auth)
{
  MYSQL_AUTHENTICATION_PLUGIN,                  /* plugin type          */
  &pam_auth_handler,                            /* auth plugin handler  */
  "pam_auth",                                   /* plugin name          */
  "Sergei Golubchik",                           /* author               */
  "PAM based authentication",                   /* description          */
  PLUGIN_LICENSE_GPL,                           /* license              */
  NULL,                                         /* init function        */
  NULL,                                         /* deinit function      */
  0x0100,                                       /* version 1.0          */
  NULL,                                         /* for SHOW STATUS      */
  NULL,                                         /* for SHOW VARIABLES   */
  NULL,                                         /* unused               */
  0,                                            /* flags                */
}
mysql_declare_plugin_end;


At the very bottom is the plugin descriptor, it has the same structure for all plugins. A little higher is the descriptor of the authentication plugin, and even higher is the empty function, from which I will call pam.

Since the main idea of ​​this pam-plugin is to conduct a dialogue with the user, you need to somehow teach the client how to receive questions from the server, and send the answers entered by the user. For this, MySQL has client plugins - plugins that are loaded into the client (more precisely, they are loaded by libmysqlclient , according to the instructions of the server). I do not require any exotics from the client plugin - just repeat the question / answer until the server is completely satisfied. Such a plugin already exists - is called “dialog” and is, oddly enough, located in the dialog.c file .

This plugin must be specified in the second field of the st_mysql_auth structure , then the server will tell the client that it needs to load dialog.so and send it everything that my plugin wants to send.

Verification I create CMakeLists.txt (to be honest - I copy from another plugin and slightly correct it), there is only one line:
MYSQL_ADD_PLUGIN(pam_auth pam_auth.c LINK_LIBRARIES pam)

and compile
mysql-5.5.17 $ cmake. && make

It works, now it's time to smoke man pam . In MySQL, authentication is pretty simple. The plugin receives the name of the user to be authenticated and the vio handler. In vio has methods write_packet and read_packet , with which you can communicate with the client (in this case - a «dialog» plug). In pam, everything is a little more complicated, you need to use the callback function, from which I will call write_packet and read_packet . In general, working with pam looks like this:
  1. initialization - pam_start (here we say which function to call as callback)
  2. authentication - pam_authentificate (somewhere inside and our callback can be called)
  3. pam_acct_mgmt account verification
  4. checking a new username (if pam changed it) - pam_get_item (PAM_USER)
  5. pam_end completion

With any error at any stage, you must immediately proceed to the last step - pam_end . I got such a function here:
#include 
#include 
#include 
static int conv(int n, const struct pam_message **msg,
                struct pam_response **resp, void *data)
{
}
#define DO_PAM(X)                       \
  do {                                  \
    status = (X);                       \
    if (status != PAM_SUCCESS)          \
      goto ret;                         \
  } while(0)
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, ¶m };
  /* 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(pam_start(service, info->user_name, &c, &pamh));
  DO_PAM(pam_authenticate (pamh, 0));
  DO_PAM(pam_acct_mgmt(pamh, 0));
  DO_PAM(pam_get_item(pamh, PAM_USER, (const void**)&new_username));
  if (new_username)
    strncpy(info->authenticated_as, new_username, sizeof(info->authenticated_as));
ret:
  pam_end(pamh, status);
  return status == PAM_SUCCESS ? CR_OK : CR_ERROR;
}

It remains to write a conversation function - the function that pam will call when it wants to ask something. Pam will send a list of questions to this function, and it will give it a list of answers. In addition, a pointer is passed to it, as is always the case with callbacks, to store additional parameters and status. Since there is only one pointer and there are many parameters, I create a structure:
struct param {
  unsigned char buf[10240], *ptr;
  MYSQL_PLUGIN_VIO *vio;
};

The problem is that the “dialog” plug-in understands only commands in the form “display this text as a hint, read the line entered by the user, and send it to the server”. But pam has as many as four types of messages, two of which are purely informational and have the semantics of "output this, do not enter anything." Therefore, in my plugin, I accumulate them in the buffer without sending them until I need to enter something. It turns out like this:
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, in fact, is all. I collect, install - and it does not work. It turns out that due to bug 60745, clients cannot download the “dialog” plugin. Well, the solution is obvious
mv auth.so dialog.so

and you can authenticate in MySQL, for example, using S / Key:
$ mysql
challenge otp-md5 99 th91334
password:
(turning echo on)
pasword: OMEN US HORN OMIT BACK AHOY
mysql>

Also popular now: