GitHub Webhookのpassword機構とPythonで戯れる

![secret.png](https://qiita-image-store.s3.amazonaws.com/0/14365/a0e54a26-06d8-09d1-af8a-7d282d238716.png)

な、なにこのSecretフィールドうれしい(ﾟ∀ﾟ)

これ、入ったのって最近な予感がします。
ざっくり試した過程を書いておきます。
間違いについては是非ご指摘ください。

# どういう機能

要はGitHubがペイロードの署名を送ってくれます。

送られるJSONのペイロードをこのkeyを使ってHMAC (鍵付きハッシュ) にかけた結果を
WebHookのHTTPヘッダにしれっとぶち込んでくれる機構です。
Secretは入力後GitHub画面上で見られず、以降HTTPのデータには生で出て来ませんので、それもちょっとだけ安心です。

＃ 再入力は出来ます

この機構は多分昔なかったと思います。
WebHookがリニューアルされた直後にも多分ありませんでした。入ったのいつでしょう誰か教えて


参考: http://blog.manaten.net/entry/573

上のページを見ると情報保全の意義を感じます。
パスワードフィールドはなく、自分でquery stringに入れてます。1年も前だけど。

# なにが嬉しい

簡単に安全度が上がります。

WebHook URLへは基本的にPOSTし放題で、CSRFトークンとか埋め込めません。
Djangoで```@csrf_exempt```書く度にSAN値が下がるんですが仕方ありません

一応WebHookの口はゆるくIP制限をかけられるらしいのでかけるのですが、
それでも悪意のあるGitHubプロジェクトから何か差し込まれるという懸念はあるんです。
送られる情報の全てか相当量は偽装できそうですし(調べてませんが)。

なので実際URL以外にやはり鍵が欲しかった。
でも自分でその実装はあんまり書きたくないのです (上のサイトの人は自分で書いてますねすごい)

# どういう挙動なの

GitHubがWebHookを送る際、そのJSONペイロードの文字列をHMAC + sha1 でkeyを使って40文字のハッシュに変換します。で、HTTP POSTを送信する際に```sha1=dd671d65f5aee8c8aba748fd8f0143c10c5ba875``` みたいな文字列がヘッダにぶちこみます。サーバ (自分のほう) はペイロードとその「署名」を受け取ることになります。

受け取る側つまり自分のサーバは、共通のkeyを用いて送られたJSONの文字列を、同じアルゴリズムでハッシュに変換すれば署名を検証できます。署名が違えばオカシイので警告を管理者に送れます。

多分OpenSSLのラッパがあるスクリプト言語であればそれほど困難はありませんし、HMACもsha1もアルゴリズム自体はふつうのものなのでshellでもきっと検証できるでしょう (```sha1sum```使わないでもって意味ですが試さないよ絶対試さないからね)。

GitHub本体が用いている(らしい)Ruby実装でもライブラリそのまま使ってるようです。

* https://github.com/github/github-services/blob/f3bb3dd780feb6318c42b2db064ed6d481b70a1f/lib/service/http_helper.rb#L77
* http://ruby-doc.org/stdlib-2.1.0/libdoc/openssl/rdoc/OpenSSL/HMAC.html

Pythonにもあります。わざとらしいPython味。

* http://docs.python.jp/2.7/library/hmac.html
* http://docs.python.jp/2.7/library/hashlib.html

# Python (+ Django) でちょっと使ってみる

JSONペイロードを受けるため、サーバ側でContent-Typeに合わせて二通りの受け皿を用意します。
これ自体は今回の話と関係なく実装するものです。

また、```request.META.get('HTTP_X_HUB_SIGNATURE')```などを経由して、送られてきた文字列を引っ張り出します。

空ならパスワード入ってません。空でない時にはペイロードの生文字列をhmac/hashlib.sha1にぶちこんで自分側で鍵付きハッシュを生成し、同じものになるのかを確認します。

```
import hashlib
import hmac

...

    if content_type == 'application/json':
        payload = request.body
    else:
        payload = request.POST.get('payload')
    signature = request.META.get('HTTP_X_HUB_SIGNATURE')
    if signature:
        hasher = hmac.new(secret, payload, hashlib.sha1)
        logger.debug('Signature : {}'.format(signature))
        logger.debug('Calculated: sha1={}'.format(hasher.hexdigest()))

```

手元では計算結果が合ったのでこれで良い気がしています。

ただ、毎度のごとくエンコーディングとか改行コードが常に一致するのかよくわかりません。詳しい方がいらっしゃれば是非コメントお願いします。

 * 参考
     * https://github.com/github/github-services/blob/f3bb3dd780feb6318c42b2db064ed6d481b70a1f/lib/service/http_helper.rb#L77

# おまけ

いっそサーバ側で生の鍵おぼえたくないんですが、楽でかつ良い方法を知っていたら是非教えてください。Djangoでシリアライズしたの取っておくの楽なのかな……

# 追記: django.test.Client でテストする場合

ペイロード自体が文字列として準備できているとすれば、やることは上の生成結果をheader相当の部分にねじ込むだけです。Django 1.6.5では以下のようにして組み込めます。

```
        hasher = hmac.new('TestSecret', payload_str, hashlib.sha1)
        response = self.client.post(
            url, data={'payload': payload_str},
            HTTP_X_HUB_SIGNATURE='sha1={}'.format(hasher.hexdigest()))
```

なおdjango 1.6.5なんですが、content_typeとしてGitHubで選べる"application/x-www-form-urlencoded"をClientに設定すると、response.POST.get('...')でPOSTデータを受け取れなくなるという問題があるようです。
content_typeについてサーバ側でmultipart/form-dataも受け取れるようにしときましょう。そのうちGitHubの方もだんまり変えるかも知れません。