S/Key

二段階認証

最近ipadに二段階認証がやってきた。流行りなんだな。

手軽な二段階認証として、Cメール(今は国際的に通用するSMSって言葉に置き換えられたけどね)がある。でも、 SMS認証の代行業者の取締まり強化を全国の警察に指示 こんな話も有るので、うかうかしていられない。で、SMSに変わる方法が推奨される。

オイラーが使ってるさくらさんの所でも、ワンタイムパスワードによる二段階認証がサポートされた。前回の最後を参照

OTP

一口にワンタイムパスワードと言っても、色々有るとな。

ワンタイムパスワード

ワンタイムパスワードとは?仕組みをわかりやすく解説

タイムベース・ワンタイムパスワードトークン(C200)

お手軽にタイムベースのワンタイムパスワードが勧められている。

ぐるるの奴が筆頭かな。でも、互換機を確認しておかないと、知らず知らずのうちにロックインされちゃうぞ。そんな訳で、 あなたのアカウントを守る、2段階認証アプリ5選 こんな紹介も有ったぞ。

さくらの導入手順書に有ったんだけど、これらの装置が機能しなかった場合の非常手段として、 キーのバックアップを強く勧めるとな。そうだよな、市中の合鍵屋で作って貰う事って出来ないからね。

でも、この文言を良く読むと、バックアップしたキーも一度しか使えないとな。これ、どういう仕組み? 散歩しながら、過去に読んだ記憶を辿ってみたよ。

OPIE in FreeBSD

そしたら、 OPIE - One-time Passwords In Everything なんてのを思い出した(覚えているオイラーは偉い、誉めて遣わそう)。昔の事なんで、何でこんな事を考えるんだとスルーしてたんだ(恥じる事だね)。でも、今の時代に必要になるとは、慧眼だな。

早速試してみたいんだけど、よそ様はどうなってるか、市場調査が先だな。

at linux

ワンタイムパスワードでggじゃなくて、apt-cache search 'one time password' して、目ぼしいものを列挙してみた。

donkey - One Time Password calculator
golang-github-pquerna-otp-dev - Google Authenticator compatible one time passwords for Go
python3-pyotp - Python One Time Password Library (Python 3)
ruby-rotp - Ruby library for generating and verifying one time passwords

donkeyと言うのをやってみる。debianに移植した日本人は偉いぞ。

AUTHOR
       This      program     is     developed     by     Kazuhiko     Yamamoto
       <kazu@is.aist-nara.ac.jp>.  This manual page was written  by  Fumitoshi
       UKAI  <ukai@debian.or.jp>,  based on the documentation of this program,
       for the Debian GNU/Linux system (but may be used by others).

作ったはいいけど、リナでは使い道が無いと言う、悲しい現実が。あっ、archLinuxでは、ちょっと手間をかけると S/KEY 認証 出来るようになってた。

debian:~$ donkey -f md5 -i
Enter login name [default sakae]:
Enter sequence 1 to 999 [default 99]:
Enter new seed [default de05302]:
Please choose passphrase between 8 and 256 characters.
Enter passphrase :
Re-enter passphrase :
sakae 0099 de05302          8843f0a7ba1fa0f5  Apr 25,2021 08:01:56
GAIN JAN MUD DISK WENT EGAN

このコマンドでmd4かmd5を選択(デフォはmd4なので注意)。自分のログイン情報にSOLTとしてseedを加えるんだな。

debian:~$ donkey -f md5 -n 5 99 de05302
Enter passphrase :
95: THEE TAB OX RAP GUT GOOF
96: OWL GLUE FRED ATE SAL BIAS
97: MUTT WALE NAB WINE SAC ADEN
98: GUS BAIL FEET SIT TEST FEUD
99: GAIN JAN MUD DISK WENT EGAN

同じsoltを使って、5つのパスワードを生成してみた。これを印刷して、財布にしまっておくのさ。

S/Key

at OpenBSD

OpenBSDで試してみたいんだけど、自分のアカウントでは気が引ける。新しいユーザーを作ろう。えと、adduserだかuseraddだか。。。軽くmanしたら、adduserの方でbatchモードのオプションが有った。ひねくれもののオイラーは、オプション指定しなければ対話モードだろうと予想。たまたまCDでzardを聴いていたので、アカウントはzardにした。

new user

ob# adduser
  :
Enter password again []: sakai2007

Name:        zard
Password:    ****
Fullname:    zard
Uid:         1001
Gid:         1001 (zard)
Groups:      zard
Login Class: default
HOME:        /home/zard
Shell:       /bin/ksh
OK? (y/n) [y]: y
Added user ``zard''
Copy files from /etc/skel to /home/zard
Add another user? (y/n) [y]: n
Goodbye!

/etc/adduser.confなんて言うのが、初めて場合作られるのね。知らんかった。以前から度々問題になってたshellに何を使うかなんてのも、ここにデフォで指定しておけるのね。

try S/Key

まず初めにrootで、skeyをするための準備が必要とな。

ob# skeyinit -E
ob# ls -l /etc/skey/

空のdirが出来るんだな。

ob$ skeyinit
Password:sakai2007
[Updating zard with md5]
Old seed: [md5] obl45746
Enter new secret passphrase:hello
ERROR: Your passphrase must contain more than just lower case letters.
Whitespace, numbers, and punctuation are suggested.
Enter new secret passphrase:HelloHello
Again secret passphrase:HelloHello

ID zard skey is otp-md5 100 obl45747
Next login password: CAFE ROOF WATS KENT GAFF CHAR

zard用のskeyを準備。まずは、普通のパスワードで同人か確認。それから、skey用のパスワードを入力。手抜きのやつだと叱られる。

ob# cat /etc/skey/zard
zard
md5
0100
obl45747
621a3be0d2f87eca

こんな記録が残ったぞ。

ob$ skeyinfo -v
otp-md5 99 obl45747
ob$ otp-md5 99 obl45747
Enter secret passphrase:HelloHello
SIN IOWA CRY HAND HAY SARA

skeyinfoで、skey/zardの情報を取り出す。それを使ってotp-md5で、ワンタイムパスワードを表示するって寸法なんだな。

ob$ otp-md5 -n 5 99 obl45747
Enter secret passphrase:
95: DUD RUNT GENE GOOF VALE ONE
96: SONG SAC ANEW MEET RUIN GULL
97: WEAN WORM LICK NOEL SULK KENO
98: BAND SURE BITS JOIN POW GRIT
99: SIN IOWA CRY HAND HAY SARA

これを財布の中に仕舞っておけとな。

ob$ ssh -l zard localhost
zard@localhost's password:sakai2007
Last login: Sun Apr 25 15:27:19 2021 from 127.0.0.1
OpenBSD 6.8 (GENERIC.MP) #5: Mon Feb 22 04:36:10 MST 2021
  :

これが普通のssh login

ob$ ssh -l zard:skey localhost
otp-md5 99 obl45747
S/Key Password:SIN IOWA CRY HAND HAY SARA
Last login: Sun Apr 25 15:31:25 2021 from 127.0.0.1
OpenBSD 6.8 (GENERIC.MP) #5: Mon Feb 22 04:36:10 MST 2021
 :

skeyオプションを付けると、計算しておいたパスワードを入力する必要が出てくる。

zard
md5
99
obl45747
3c13e030c9a199ad

シーケンス番号が一つ減ってるし、次のHex値も変わっている。

ob$ ssh -l zard:skey localhost
otp-md5 98 obl45747
S/Key Password:BAND SURE BITS JOIN POW GRIT
  :

もう一度loginすると、違うシーケンス番号をパスワードを要求された。悪い人が99のシーケンスに対するパスワードを盗聴しておいて、それを使おうとしても失敗する。すなわち一度しか使えないパスワードって事だ。

ftp with OTP

今はもうブラウザーにも見放されてしまったftpだけど、ワンタイムパスワードが有れば、鬼に金棒だからね。

OpenBSDは古式なUNIXなので、ftpサーバーを内蔵してる。ってな事で、例に倣って試してみる。

ob# /etc/rc.d/ftpd -f start
ftpd(ok)
ob$ ftp localhost
Trying 127.0.0.1...
Connected to localhost.
220 ob.localhost.jp FTP server ready.
Name (localhost:zard):
331 Password required for zard.
Password:sakai2007
230- OpenBSD 6.8 (GENERIC.MP) #5: Mon Feb 22 04:36:10 MST 2021
230-
230- Welcome to OpenBSD: The proactively secure Unix-like operating system.
230-
230 User zard logged in.

普通のftp。パスワードただ漏れ

Connected to localhost.
220 ob.localhost.jp FTP server ready.
Name (localhost:zard): zard:skey
331 Password required for zard.
Password:
530 Login incorrect.
ftp: Login zard:skey failed.

仰せに従ってやってみると、駄目じゃん。ユーザー名の一部と見做しているよ。6.5の時代までなのがな? 世間一般の情勢を鑑み、機能削除したのかな?

ob$ skey -md5 -n 5 95 obl45747
Enter secret passphrase:HelloHello
91: MOLT SNAG ANNE NUDE AT BOIL
92: BOP HUNK MAID LINK TACT WARN
93: FIVE LOAM TOUT TOUT MEET JURY
94: WHY VEAL WILD KIRK TEET MOTH
95: DUD RUNT GENE GOOF VALE ONE

ちょいと悔しいので、汎用的なコマンドにも当たっておく。

skey reading

恒例のソース読み。目標は、どんな原理で動いているかを探る事。今までの取り調べで、心理状態を把握しつつあるので、軽めに行きます。

skey enable/disable

まずは大本のメインスイッチだな。使い始める前にrootになって skeyinit -E しろとなってた。じゃ使用を中止する指令も有るはずで、ご想像通りなってる。skeyinit -D 大本なんで、大文字だ。

スイッチはskeyinit.cの中

void
enable_db(int op)
{
        if (op == 1) {
                /* enable */
                   :
                if (chmod(_PATH_SKEYDIR, 01730) != 0)
                        err(1, "can't chmod %s", _PATH_SKEYDIR);
        } else {
                /* disable */
                if (chmod(_PATH_SKEYDIR, 0) != 0 && errno != ENOENT)
                        err(1, "can't chmod %s", _PATH_SKEYDIR);
        }
}

/etc/skey ってdirを読み書き可能にするか、権限を一切剥奪するか(無いに等しい)で制御してる。呆れるぐらい潔い方法だ。

key or seed

donkeyをやった時、シーケンス番号とキーを設定した。シーケンス番号はパスワードを消費する度に減った値をセット。じゃキーは何者? 予想ではSOLTだ。塩で本当のパスワードに味付けして、本当のパスワードを隠すんだろう。どうやって生成してる?

/* Build up a default seed based on the hostname and some randomness */
if (gethostname(hostname, sizeof(hostname)) == -1)
        err(1, "gethostname");
for (i = 0, p = seed; hostname[i] && i < SKEY_NAMELEN; i++) {
        if (isalnum((unsigned char)hostname[i]))
                *p++ = tolower((unsigned char)hostname[i]);
}
for (i = 0; i < 5; i++)
        *p++ = arc4random_uniform(10) + '0';
*p = '\0';

ホスト名にランダムは数字を加えたものになってる。

read donkey

折角なので、リナで提供されてたdonkeyで、コードを追ってみる。

(gdb) b main
Breakpoint 1 at 0x58bb: file donkey.c, line 271.
(gdb) r -f md5 -n 5 99 obl45747
Starting program: /tmp/donkey/donkey -f md5 -n 5 99 obl45747

Breakpoint 1, main (argc=7, argv=0x7f7ffffe3b18) at donkey.c:271
271     {
(gdb) b keycrunch
Breakpoint 2 at 0x8e8a1e14f7: file skey.c, line 22.
(gdb) c
Continuing.

Breakpoint 2, keycrunch (result=0x7f7ffffe3a90 "", seed=0x7f7ffffe3cb2 "obl4574\
7", passwd=0x7f7ffffe3990 "HelloHello") at skey.c:22
22      {

seedとパスワードを受け取って処理する部分。

(gdb) p buf
$1 = 0x9156ccf620 "obl45747HelloHello"

合体した。

(*mdsw[MD].md_init)(&md);
(*mdsw[MD].md_update)(&md, (POINTER)buf, buflen);
(*mdsw[MD].md_final)((POINTER)results, &md);
free(buf);

results[0] ^= results[2];
results[1] ^= results[3];

memcpy(result,(char *)results,8);

そいつのMD5を取っている。MD5は128Bitになるので、それを畳み込んでいる。結果は

(gdb) p result
$2 = 0x7f7ffffe3a90 "\265F&/\256\276\257?\353\061\264/\351\211H\277"

後は

=>              for (i = 0; i < (seq - n + 1); i++) secure_hash(key);
                for (; i < seq + 1; i++) {
                        printf("%zu: %-29s\n", i, btoe(key));
                        secure_hash(key);

最初のforで、必要な回数になるまで、MD5を取り続ける。続いて次のforで、要求されたシーケンス番号から、MD5の結果を表示する。

(gdb) p i
$4 = 95
(gdb) p seq
$5 = 99
(gdb) p key
$6 = "\017\372\216*Go2_"
Enter passphrase :
95: DUD RUNT GENE GOOF VALE ONE
96: SONG SAC ANEW MEET RUIN GULL

すなわち、初期値の文字列(password+seed)に何回もハッシュを取る。ハッシュの結果はバイナリーなんで、btoeで人間が読める6個のワードに変換してるんだ。 このワードをetobで元に戻せるようになっている。

例えば、100回の結果を残しておく。/etc/skeyに中にね。使う時は、99に相当する内容を財布から取り出す。そしてシステムはloginの際中にそれを1回MD5する。100回目の値を求めるわけだ。それと保存してる値が一致すればOK。

次は、99の番号と値を保存。login時には98番目の値を提示して貰うって寸法。動的に提示すべきパスワードが変わっていく。一度しか使えないパスワードシステムの完成だ。

若し、/etc/skeyの中のファイルが盗まれても、それからの本当のパスワードを計算するのは不可能。ああ、ビットコインみたいに、無駄な電気を消費すれば、可能かも。MD5は弱いのでsha1とかrmd160とかの強力なやつで対抗出来るようになってる。

リベンジ ftp login by S/Key

散歩してて、ふと思い付いた。ftpってサーバーが有って成り立つサービスだよな。サーバーがskeyでのログインを許可してない限り使えないはず。だったら、さっさと man ftpd してみろ。

ftpd authenticates users by using the service and type of ftp, as defined
in the /etc/login.conf file (see login.conf(5)).  An authentication style
may be specified by appending with a colon (‘:’) following the
authentication style, i.e. “joe:skey”.  The allowed authentication styles
for ftpd may be explicitly specified by the “auth-ftp” entry in
/etc/login.conf.

login.confで許可しておけとな。

# Default allowed authentication styles for authentication type ftp
auth-ftp-defaults:auth-ftp=passwd,skey:

passwdの後ろにskeyを追加したよ。そうしておいて再度チャレンジ。

vbox$ ftp localhost
Trying 127.0.0.1...
Connected to localhost.
220 vbox.local.jp FTP server ready.
Name (localhost:sakae): sakae:skey
331- otp-md5 98 vbox08230
331 S/Key Password:
Password:
230- OpenBSD 6.8 (GENERIC) #5: Mon Feb 22 03:42:30 MST 2021
230-
230- Welcome to OpenBSD: The proactively secure Unix-like operating system.
230 User sakae logged in.
Remote system type is UNIX.
Using binary mode to transfer files.

今度はloginの途中で、親切にもS/Keyの調べ方を指南してくれたぞ。別端末で、skey用のパスワードを計算して、それを張り付けたら、無事にログイン出来た。もっと早く気付けよ。

vbox$ otp-md5 98 vbox08230
Enter secret passphrase:
SWUM MAY HUNK FEAR MART SURE

debug ftpd

OpenBSDのftpdで観光。-g付きでコンパイル。make installしちゃうとstripされちゃうので手動でコピーする。そして/etc/rc.dから起動。

box# ps awx | grep ftpd
64738 ??  I        0:00.00 /usr/libexec/ftpd -D

-Dでデーモンモードになってる。gdbでこのプロセスにアタッチして、b user , b pass で、それらしい所にBPを置く。ftpでアクセスして来ると、forkされるので、子供を追いかけるようにgdbを設定。

(gdb) set follow-fork-mode child

アクセス中のプロセスは、下記のようになってた。

box# ps awx | grep ftpd
64738 ??  I        0:00.00 /usr/libexec/ftpd -D
95908 ??  S        0:00.01 ftpd: localhost: [priv post-auth] (ftpd)
62022 ??  S        0:00.00 ftpd: localhost: sakae (ftpd)

post-authのプロセスは、 monitor.c:197/handle_cmds() で待ってた。もう一方は、yyparse()の方だった(多分実働部隊か。なんか、カースト制だな)。

user()の最後の方にこんなのが有った。

B         if (as != NULL && (cp = auth_challenge(as)) != NULL)
  =>              reply(331, "%s", cp);
          else
B                 reply(331, "Password required for %s.", name);

そして、指南のメッセージが見えた。

805                     reply(331, "%s", cp);
(gdb) p cp
$1 = 0x5d356000 "otp-md5 96 vbox08230\nS/Key Password:"

後はpass()関数に飛び込む。

(gdb) bt
#0  _libc_auth_userresponse (as=0x5d353000, response=0x60eed280 "SIN... VETO", more=0) at /usr/src/lib/libc/gen/authenticate.c:456
#1  0x1b91a004 in pass (passwd=0x60eed280 "SIN... VETO") at ftpd.c:890
#2  0x1b9291f3 in handle_cmds () at monitor.c:286
#3  0x1b928f38 in monitor_init () at monitor.c:197
#4  0x1b918660 in main (argc=2, argv=0xcf7edc84) at ftpd.c:603

そして、この auth_userresponce の中で、認証が行われている。

実際に認証に使われるのは、

box# ls /usr/libexec/auth/
login_activ     login_lchpass   login_radius    login_snk
login_chpass    login_ldap      login_reject    login_token
login_crypto    login_passwd    login_skey      login_yubikey

/etc/login.confに列挙されてるのに対応しているんだな。

NAME
     login_skey – provide S/Key authentication type

SYNOPSIS
     login_skey [-s service] [-v fd=number] user [class]

DESCRIPTION
     The login_skey utility is called by login(1), su(1), ftpd(8), and others
     to authenticate the user with S/Key authentication.
      :

ssh by S/Key

sshの場合は、どうなるんだろう? 下記のようにして、どのあたりで動いているかを調べて、それからかな。

ob$ ssh -vvv -l sakae:skey localhost
   :
debug1: Next authentication method: keyboard-interactive
debug2: userauth_kbdint
debug3: send packet: type 50
debug2: we sent a keyboard-interactive packet, wait for reply
debug3: receive packet: type 60
debug2: input_userauth_info_req
debug2: input_userauth_info_req: num_prompts 1
otp-md5 97 vbox08230
S/Key Password:
   :

RFC 2289, 6238