Plan 9とGo言語のブログ

主にPlan 9やGo言語の日々気づいたことを書きます。

Gitの認証情報管理にFactotumを使う

Gitは認証が必要なリポジトリにアクセスするとき、credential helperと呼ばれるコマンドを実行します。credential helperはただの標準入出力を扱うプログラムで、一般的には git-credential-xxx という命名になっています。おそらく以下のうちどれかをよく使うのではないかと思います。

macOSの場合は、デフォルトでgit-credential-osxkeychainが使われるので、自分で設定することは少ないかもしれませんね。helperコマンドはgit configで設定します。

git config --global credential.helper cache

Gitは認証が必要だとわかった時点で、git configで設定したhelperコマンドを実行します。このときコマンドには、以下のような値が標準入力経由で渡されることになります。入力はname=value形式で1行1つ記述されていて、最後に空行で終わります。これらの値は状況によって省略されるものもあります。

protocol=https
host=github.com
path=path/to
username=lufia
password=xxx

helperは入力を空行まで読み込んで、条件に対応する値を標準出力でGitに返す必要があります。基本的にはpasswordだけを返せば動くと思いますが、それ以外の値も返せば、入力に優先して返した値が使われます。protocolなどを個別に返してもいいし、urlとしてまとめても同じです。

protocol=https
host=github.com
path=path/to
username=lufia
password=xxx
url=https://lufia:xxx@github.com/path/to
quit=0

credential helperの詳細はPro Gitに書かれています。

また、呼び出す側の実装はこのファイルです。

標準で持っているhelperはgit-coreディレクトリに入っています。

% git --exec-path
/Library/Developer/CommandLineTools/usr/libexec/git-core

% 9 ls -p /Library/Developer/CommandLineTools/usr/libexec/git-core/git-credential-*
git-credential-cache
git-credential-cache--daemon
git-credential-osxkeychain
git-credential-store

Factotumを使う

上でみたように、credential helperはただの標準入出力を扱うプログラムなので、echoで固定の文字列を返してもいいし、ネットワークからパスワードを持ってくるコマンドを自作することもできます。ところでPlan 9にはfactotumという認証エージェントが存在していて、これはPlan 9ツールをUnixに移植したplan9portでも使えるので、factotumからパスワードを取り出すcredential helperを実装してみました。完全なソースコードはこちらです。

github.com

Factotum

FactotumはSSH秘密鍵やログインパスワードなどを一括して扱うプログラムです。key=valueのリストを管理します。!で始まる属性は秘密情報扱いとなり、表示されません。

% factotum
% echo 'key proto=pass role=client service=git dom=github.com user=lufia !password=xxx' |
> 9p write factotum/ctl
% 9p read factotum/ctl
key proto=pass role=client service=git dom=github.com user=lufia !password?

上記の例ではpassプロトコル(生のパスワードを扱うプロトコル)を使いましたが、これ以外にもp9sk1rsaなどいくつか用意されています。protoの値によって必須となる属性は変わります。passプロトコルではuserpassword属性が必須です。任意の属性はいくつ追加しても構いません。ここでは、Git用のパスワードと識別するためにserviceキーと、対象サービスのドメインdom属性を追加しています。

登録された情報を使う場合、proto=pass role=client user=lufiaのように絞り込むための属性を使ってfactotumへアクセスします。ところで、Plan 9libauthにはproto=passのための関数があるので、それを使えば十分です。

#include <u.h>
#include <libc.h>
#include <thread.h>
#include <auth.h>

...

UserPasswd *up;

up = auth_getuserpasswd(nil, "proto=pass role=client user=lufia");
if(up == nil)
    sysfatal("auth_getuserpasswd: %r");
print("username=%s\n", up->username);
print("password=%s\n", up->passwd);
free(up);

Gitはcredential helperからパスワードを取り出せなかった場合に入力を促して、その内容をcredential helperに保存しようとします。factotumにエントリを登録する場合はfactotum/ctlファイルに書き込めば終わりです。ただし、Plan 9ネイティブの場合は/mntfactotumが提供するファイルをマウントしているので/mnt/factotum/ctlopenすればいいんですが、plan9portの場合は直接参照することができないため、lib9pclientを使う必要があります。

#include <u.h>
#include <libc.h>
#include <thread.h>
#include <9pclient.h>

...

CFsys *fs;
CFid *fid;

fs = nsamount("factotum", nil);
if(fs == nil)
    sysfatal("nsamount: %r");
fid = fsopen(fs, "ctl", OWRITE);
if(fid == nil)
    sysfatal("fsopen: %r");
if(fswrite(fid, "key proto=pass ...", n) < 0)
    sysfatal("fswrite: %r");
fsclose(fid);
fsunmount(fs);

上記ではfsopenを使いましたが、代わりにfsopenfdを使うと、CFidの代わりにファイルディスクリプタを受け取ることができて、以降普通のファイルとして扱えるのでこちらの方が便利かもしれません。

Plan 9ネイティブなfactotumはパスワードを扱うだけあって、swapしないようになっていたり、デバッガの接続を禁止していたりと安全な作りになっていますが、plan9portのfactotumにはそういった仕様は盛り込まれていなさそうです。また、factotum単体ではプロセスが死ぬと記憶していたパスワードは消えてしまうので、永続化したい場合はsecstoredと一緒に使いましょう。

macOSの場合

launchd設定例です。

com.bell-labs.plan9.factotum.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>EnvironmentVariables</key>
    <dict>
        <key>PLAN9</key>
        <string>/usr/local/plan9</string>
        <key>PATH</key>
        <string>/usr/local/plan9/bin</string>
    </dict>
    <key>Label</key>
    <string>com.bell-labs.plan9.factotum</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/plan9/bin/factotum</string>
    </array>
    <key>RunAtLoad</key>
    <false/>
    <key>KeepAlive</key>
    <dict>
        <key>OtherJobEnabled</key>
        <dict>
            <key>com.bell-labs.plan9.secstored</key>
            <true/>
        </dict>
        <key>SuccessfulExit</key>
        <false/>
    </dict>
</dict>
</plist>

com.bell-labs.plan9.secstored.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>EnvironmentVariables</key>
    <dict>
        <key>PLAN9</key>
        <string>/usr/local/plan9</string>
    </dict>
    <key>Label</key>
    <string>com.bell-labs.plan9.secstored</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/plan9/bin/secstored</string>
        <string>-v</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
</dict>
</plist>