LambdaのPythonでGitHubのWebhookから送られてくるHAMC値をチェックする
LambdaのPythonでGitHubのWebhookから送られてくるHAMC値をチェックするコード
# -*- coding: utf-8 -*- from __future__ import print_function import json, hmac, hashlib, base64, urllib def checkHmac(event) -> bool: """GitHubのWebhookから送られてくるHAMC値をチェックする""" # 送信者と受信者で秘密情報であるSecretの値 secret = 'GitHubのWebhookで設定したSecretの値(直書きしないで環境変数とかSecrets Managerから取得すべし)' # GitHubから送られてきた情報にあったHMAC値 sent_hmac = event['headers']['x-hub-signature-256'] print('SHA-256ハッシュ関数用のHMAC値:' + sent_hmac) # Secretの値をbytes型に変換する secret_bytes = bytes(secret, 'utf-8') if (event['isBase64Encoded']): # bodyをbase64でデコードしてbytes型にする body_bytes = base64.b64decode(event['body']) else: # bodyをbytes型に変換する body_bytes = bytes(event['body'], 'utf-8') # 「Secret + メッセージ + ハッシュ関数」でHMAC値を作る created_hmac = 'sha256=' + hmac.new(secret_bytes, body_bytes, hashlib.sha256).hexdigest() print('自分で作ったHMAC値:' + created_hmac) # 2つのHMAC値が同じであったら改竄されていないと判断する return hmac.compare_digest(created_hmac, sent_hmac) def lambda_handler(event, context): if checkHmac(event): print('HMAC値で認証できた!') # base64デコードした上で文字列にする b64decd = base64.b64decode(event['body']).decode() # URLデコードして「payload=」を削除する urldecd = urllib.parse.unquote(b64decd).lstrip('payload=') # 辞書型にする body = json.loads(urldecd) # そして処理で使う print(body) else: print('へんなHMAC値がGitHubから送られてきた!')
ことの発端は試験勉強
- じみに続けている情報処理試験の勉強でメッセージ認証を勉強していた。
- そういえば以前Backlogの課題にGitHubのコミットを連携するをやった時にGitHubのWebhookでHMAC値を使ったのを思い出して、今一度やってみようと思った。
- 手頃にGitHubでWebhookを設定
- AWSでAPI GatewayとLambdaをノリで作ってみた。
- WebhookからきたHMAC値と自作したHMAC値が一致しない!
GitHubのWebhookが送ってくるbodyがへんだ
print(event)
してCloudWatch Logsをみたら・・・bodyが・・・肉眼で読めない・・・。
{ "version": "2.0", ... "body": "cGF5bG9hZD0lN0IlMjJ...=", "isBase64Encoded": true }
アルファベットと数字と最後にある=
からBase64エンコードされてるっぽい・・・前にやった時は普通に読めたのに・・・・。
ペイロードがBase 64でエンコードされている
じっと見つめると "isBase64Encoded": true
と最後に書いてある・・・なにこれ?
Lambda プロキシ統合の場合、API Gateway は、次のようにクライアントリクエスト全体をバックエンド Lambda 関数の入力 event パラメータにマッピングします。
...省略...
"isBase64Encoded": "A boolean flag to indicate if the applicable request payload is Base64-encoded"
訳すと「適用可能なリクエスト・ペイロードがBase 64でエンコードされているかどうかを示す真偽フラグ」。
HMAC値を作るにはbodyをBase64デコードします。
# 「cGF5bG9hZD0lN0IlMjJ...=」なbodyをBase64デコードしてbytes型にしてあげる body_bytes = base64.b64decode(event['body']) # bytes型にしたbodyを使ってHMAC値を作ろう created_hmac = 'sha256=' + hmac.new(secret_bytes, b64decd, hashlib.sha256).hexdigest()
WebhookからきたHMAC値は X-Hub-Signature
ではなくX-Hub-Signature-256
を使うのがお勧めです。
{ ...省略... "headers": { ...省略... "x-hub-signature": "sha1=sha1のHMAC値", "x-hub-signature-256": "sha256=sha256のHMAC値" }, ...省略... }
ヘッダ | 説明 |
---|---|
X-Hub-Signature | このヘッダは、webhook が secret で設定されている場合に送信されます。 これはリクエスト本文の HMAC hex digest であり、SHA-1 ハッシュ関数と HMAC key としての secret を使用して生成されます。 X-Hub-Signature は、既存の統合との互換性のために提供されているため、より安全な X-Hub-Signature-256 を代わりに使用することをお勧めします。 |
X-Hub-Signature-256 | このヘッダは、webhook が secret で設定されている場合に送信されます。 これはリクエスト本文の HMAC hex digest であり、SHA-256 ハッシュ関数と HMAC key としての secret を使用して生成されます。 |
表の出典 : webhook イベントとペイロード - GitHub Docs
isBase64EncodedがFalseの時も考えておきます。
isBase64Encoded
がFalse
だとBase64エンコードはされなさそうなので処理の分岐を作っておきます。
if (event['isBase64Encoded']): # bodyをbase64でデコードしてbytes型にする body_bytes = base64.b64decode(event['body']) else: # bodyをbytes型に変換する body_bytes = bytes(event['body'], 'utf-8')
処理でBodyの内容を使う時はさらに加工して使うといいでしょう。
# base64デコードした上で文字列にする b64decd = base64.b64decode(event['body']).decode() # こんな感じになる >>> payload=%7B%22ref%22%3A%22refs%2Fheads%2Fmaster%22%2C%22before... print(b64decd) # URLデコードして「payload=」を削除する urldecd = urllib.parse.unquote(b64decd).lstrip('payload=') # こんな感じになる >>> {'ref': 'refs/heads/master', 'before... print(urldecd) # 辞書型にすると使いやすい(と思う) body = json.loads(urldecd)