libpam-mruby を試してみた

mrubyアドベントカレンダー、21日目です！！！ 前回は、[harasouさんによる超便利ツールでした](https://harasou.github.io/2015/12/20/gfmarkdown-release/) 。

ぼくも難しい記事は無理なのですが、今日は、あんちぽさんが以前スパイクで作っていた [libpam-mruby](https://github.com/pepabo/libpam-mruby) をいじってみて、OS認証のmruby化について実験してみることにします。

## PAMについてのおさらい

PAMとは、 `Pluggable Authentication Module` の略で、各種の認証処理を実行するモジュール群と、それを利用するための標準的なAPIを備えたライブラリによる認証システムの呼称です。

代表的には、LDAP、SMBなどでのLinuxのユーザ認証に利用されているため、Linuxのユーザ認証のためのプラガブルな認証システムのことという文脈で使われることが多いようです。

[Sambaユーザーのパスワード管理 PAMを利用して認証を行う](http://www.atmarkit.co.jp/flinux/samba/sambatips02/sambatips02.html) より、図を引用させていただきます。

![図です](http://www.atmarkit.co.jp/flinux/samba/sambatips02/Samba3.gif)

libpam-mrubyは、そのような認証の中身のロジックをmrubyを用いて記述することができるPAMミドルウェアです。

## まずはビルドを通す

環境はCentOS 7.1です。

```
git clone https://github.com/pepabo/libpam-mruby.git
cd libpam-mruby/
sudo yum groupinstall "Development Tools"
sudo yum install rake pam-devel
rake
sudo install build/mruby.so /usr/lib64/security/pam_mruby.so
```

ここまでで一応ビルドはできるのですが、実は記事執筆直前の段階 [`0bd52ea`](https://github.com/pepabo/libpam-mruby/commit/0bd52ea3ff1f5589d35a743dc66eb23cec3e843d) では、このままではロードしてくれませんでした。syslog（`/var/log/secure`）を見ると、以下のようなエラーが出ています。

```
PAM unable to dlopen(/usr/lib64/security/pam_mruby.so): /usr/lib64/security/pam_mruby.so: undefined symbol: mrb_str_new_cstr
PAM adding faulty module: /usr/lib64/security/pam_mruby.so
```

どうもmrubyのライブラリなどのリンクに失敗しているようです。

## ビルドスクリプトを変更する

リンクを上手く行かせるために、以下のように最後の `pam_mruby.so` を生成するコマンドを変更します（成果物の名前もpamの規約に沿って変えておきます）。

```diff
--- a/Rakefile
+++ b/Rakefile
@@ -21,7 +21,7 @@ end
 
 desc 'build'
 task :build => [:build_mruby_c, :build_pam_c] do
-  sh 'ld -x --shared -o build/mruby.so build/mruby.o build/pam.o'
+  sh 'gcc -shared -Wl,-soname=pam_mruby.so.1 -lm -lpam -o build/pam_mruby.so build/mruby.o build/pam.o mruby/build/host/lib/libmruby.a'
 end
 
 desc 'install'
```

また、 `build_config.rb` も以下のように変えます。

```diff
--- a/build_config.rb
+++ b/build_config.rb
@@ -1,4 +1,8 @@
 MRuby::Build.new do |conf|
   toolchain :gcc
   conf.gembox 'default'
+
+  conf.cc do |cc|
+    cc.flags << '-fPIC'
+  end
 end
```

この上でビルドをやり直すと、リンクがいい感じになり、mrubyも `pam_mruby.so` の中にちゃんと組み込まれるようになります。

```
[vagrant@log libpam-mruby]$ ldd build/pam_mruby.so 
        linux-vdso.so.1 =>  (0x00007fffa8973000)
        libm.so.6 => /lib64/libm.so.6 (0x00007fe657b65000)
        libpam.so.0 => /lib64/libpam.so.0 (0x00007fe657956000)
        libc.so.6 => /lib64/libc.so.6 (0x00007fe657594000)
        libaudit.so.1 => /lib64/libaudit.so.1 (0x00007fe65736e000)
        libdl.so.2 => /lib64/libdl.so.2 (0x00007fe65716a000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fe65810b000)
```

## パスワードをmrubyで認識できるようにする

ここまでで、mrubyで認証が可能になったのですが、実はこの段階ではmrubyのスクリプトではユーザ名しか認証に利用できないので、パスワードを認識できるようにコードを変更してみます。

以下のようにmruby周りの定義を変更し、 `check` メソッドの引数を1つから2つにします。

```diff
diff --git a/src/mruby.c b/src/mruby.c
index 9606465..60c1a52 100644
--- a/src/mruby.c
+++ b/src/mruby.c
@@ -1,12 +1,14 @@
 #include "mruby.h"
 
-int pam_mruby_check(FILE *rbfile, const char *name)
+int pam_mruby_check(FILE *rbfile, const char *name, const char *passwd)
 {
   mrb_value ret;
 
   mrb_state *mrb = mrb_open();
   mrb_load_file(mrb, rbfile);
-  ret = mrb_funcall(mrb, mrb_top_self(mrb), "check", 1, mrb_str_new_cstr(mrb, name));
+  ret = mrb_funcall(mrb, mrb_top_self(mrb), "check", 2,
+                    mrb_str_new_cstr(mrb, name),
+                    mrb_str_new_cstr(mrb, passwd));
   mrb_close(mrb);
 
   if (mrb_type(ret) == MRB_TT_TRUE)
diff --git a/src/mruby.h b/src/mruby.h
index 8075ed5..6a1828e 100644
--- a/src/mruby.h
+++ b/src/mruby.h
@@ -7,6 +7,6 @@
 
 #include <stdio.h>
 
-int pam_mruby_check(FILE *rbfile, const char *name);
+int pam_mruby_check(FILE *rbfile, const char *name, const char *passwd);
 
 #endif
```

そうしたら、PAM認証の肝である `pam_sm_authenticate` 内部で、パスワードを取得するコードを追加します。 `pam_get_authtok` という関数を利用できるようです。

```diff
--- a/src/pam.c
+++ b/src/pam.c
@@ -46,7 +46,12 @@ PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags,int argc, const
   }
   if (debug) pam_syslog(pamh, LOG_DEBUG, "username is %s", username);
 
-  ret = pam_mruby_check(rbfile, username);
+  if (pam_get_authtok(pamh, PAM_AUTHTOK, &password, NULL) != PAM_SUCCESS) {
+    pam_syslog(pamh, LOG_ERR, "couldn't get password from PAM stack");
+    return PAM_AUTH_ERR;
+  }
+
+  ret = pam_mruby_check(rbfile, username, password);
   fclose(rbfile);
 
   if (ret > 0)
```

```conf:/etc/pam.d/system-auth-ac
auth required pam_env.so
# この行を追加
auth sufficient pam_mruby.so rbfile=/etc/pam-mruby.d/auth.rb debug try_first_pass
auth sufficient pam_unix.so nullok try_first_pass
auth requisite pam_succeed_if.so uid >= 500 quiet
auth required pam_deny.so
```

また、このままだと `PAM unable to resolve symbol: pam_sm_setcred` というログが（実害はないようですが）出続けてしまうので、何もしない `pam_sm_setcred` を定義する必要があります。

```diff
diff --git a/src/pam.c b/src/pam.c
index 5d2abe2..ff7a9f5 100644
--- a/src/pam.c
+++ b/src/pam.c
@@ -59,3 +59,8 @@ PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags,int argc, const
   else
     return PAM_AUTH_ERR;
 }
+
+PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv)
+{
+  return PAM_SUCCESS;
+}
```

これで、一通りの変更が終わりましたので、ビルドとインストールをやり直しておきます。

## mrubyをつかってユーザ認証をする

あとは、以下のエントリを `/etc/pam.d/system-auth-ac` に追加し、

```conf:/etc/pam.d/system-auth-ac
auth required pam_env.so
# ここを追加
auth sufficient pam_mruby.so rbfile=/etc/pam-mruby.d/auth.rb debug try_first_pass
auth sufficient pam_unix.so nullok try_first_pass
auth requisite pam_succeed_if.so uid >= 500 quiet
auth required pam_deny.so
#...
```

以下のような認証のためのmrubyスクリプトを用意して確認してみましょう。
（追記: 最新のバージョンでは `authenticate` というメソッドを定義する必要があります）

```rb:/etc/pam-mruby.d/auth.rb
def check(username, password)
  if username == 'udzura'
    password == 'p@ssw0rd'
  else
    true
  end
end
```

```console
$ sudo adduser udzura
## 雑にUNIXパスワードを潰す
$ echo "udzura:$(uuidgen)" | sudo chpasswd
$ su - udzura
パスワード: # <- p@ssw0rd を入力！
最終ログイン: 2015/12/22 (火) 15:23:48 JST日時 pts/0
[udzura@mruby ~]$
```

見事にmrubyでユーザログインができました！

認証手段にmrubyを利用できることで、例えば外部APIとの連携など、様々な応用が考えられると思います。

今回のコードの最終的なdiffは [こちらのPR](https://github.com/pepabo/libpam-mruby/pull/1) にあります。

## PAMの拡張について

PAMの拡張の書き方は、[The Linux-PAM Module Writers' Guide](http://uw714doc.sco.com/en/SEC_pam/pam_modules-3.html) の3章によると、以下に大別されるそうです。

* 3.2 Authentication management
* 3.3 Account management
* 3.4 Session management
* 3.5 Password management

これは、 pam.d 配下のファイルの `auth/account/session/password` の項目に対応しています。LDAPでの認証のように、自動でLDAPのデータベースからアカウントを作成するような処理は `Account management` で行われるようですが、今回は `Authentication management` のみを行うので、既存のユーザの認証方法を操作できるということでした。

その他の項目についても実装すれば、より便利になるかもしれません。

----

なお、このサンプルは、あくまでもサンプルとして、セキュリティなどに関しては不十分な箇所もある（認証用のmrubyファイルが丸見え、とか...）ことをご留意ください。

明日（今日...）は、 [yasuyuki](http://qiita.com/yasuyuki@github/items/9b23182ae5bf93d01019) さんです。