ペイロードを受信するようにサーバーが設定されると、設定したエンドポイントに送信されたペイロードがリッスンされます。 セキュリティ上の理由から、GitHub からのリクエストに制限することをお勧めします。 これを行うにはいくつかの方法があります。たとえば、GitHub の IP アドレスからのリクエストを許可することですが、はるかに簡単な方法は、シークレットトークンを設定して情報を検証することです。
REST API を利用し、リポジトリ、組織、アプリ Webhook を管理できます。 Webhook の Webhook 配信を一覧表示したり、Webhook の個別の配信を取得して再配信したりできます。Webhook は、外部のアプリまたはサービスに統合できます。 REST API を使用して、Webhook の構成を変更することもできます。 たとえば、ペイロードURL、コンテントタイプ、SSLの検証、シークレットを変更できます。 詳細については、次を参照してください。
シークレットトークンを設定する
シークレットトークンは、GitHub とサーバーの 2 か所に設定する必要があります。
GitHub にトークンを設定するには:
- Webhook を設定しているリポジトリに移動します。
- リポジトリ名の下にある [設定] をクリックします。 [設定] タブが表示されない場合は、 [] ドロップダウン メニューを選び、 [設定] をクリックします。
- 左側のサイドバーで、 [ Webhooks] をクリックします。
- Webhook の横にある [編集] をクリックします。
- "シークレット" フィールドに、エントロピが高いランダムな文字列を入力します。 たとえば、ターミナルで
ruby -rsecurerandom -e 'puts SecureRandom.hex(20)'
を含む文字列を生成できます。 - [webhook の更新] をクリックします。
次に、このトークンを保存する環境変数をサーバーに設定します。 通常、これは実行と同じくらい簡単です。
export SECRET_TOKEN=YOUR-TOKEN
トークンをアプリにハードコーディングしないでください。
GitHub からのペイロードを検証する
シークレットトークンが設定されると、GitHub AE はそれを使用して各ペイロードでハッシュ署名を作成します。 このハッシュ署名は、x-hub-signature-256
として各要求のヘッダーに含まれています。
SECRET_TOKEN
を使ってハッシュを計算し、結果が GitHub AE のハッシュと一致することを確認する必要があります。 GitHub AE は、HMAC 16 進ダイジェストを使ってハッシュを計算します。
注: Webhook ペイロードには Unicode 文字を含めることができます。 言語とサーバーの実装で文字エンコーディングが指定されている場合は、ペイロードをUTF-8として扱うようにしてください。
言語とサーバーの実装は、以下の例とは異なる場合があります。 ただし、次のようないくつかの非常に重要な事項があります。
-
どの実装を使用する場合でも、ハッシュ署名は
sha256=
で始まり、シークレット トークンのキーとペイロード本文を使用します。 -
プレーン
==
演算子の使用はお勧めしません。secure_compare
やcrypto.timingSafeEqual
のようなメソッドでは "一定時間" の文字列比較を実行され、JIT 最適化言語での通常の等式演算子 または通常のループに対する特定のタイミング攻撃を軽減するのに役立ちます。
テスト値
コードに HMAC 検証を実装するのに使用するプログラミング言語に関係なく、次 secret
値および payload
値を使用して実装が正しいことを確認できます。
- secret: "It's a Secret to Everybody"
- payload: "Hello, World!"
実装が正しく、SHA-256 アルゴリズムを使用している場合、生成するシグネチャは次のシグネチャ値と一致する必要があります。
- signature: 757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17
- x-hub-signature: sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17
実装が正しく、SHA-1 アルゴリズムを使用している場合、生成するシグネチャは次のシグネチャ値と一致する必要があります。
- signature: 01dc10d0c83e72ed246219cdd91669667fe2ca59
- x-hub-signature: sha1=01dc10d0c83e72ed246219cdd91669667fe2ca59
Ruby の例
たとえば、次のような verify_signature
関数を定義できます。
def verify_signature(payload_body)
signature = 'sha256=' + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), ENV['SECRET_TOKEN'], payload_body)
return halt 500, "Signatures didn't match!" unless Rack::Utils.secure_compare(signature, request.env['HTTP_X_HUB_SIGNATURE_256'])
end
その後、Webhook ペイロードを受信したらそれを呼び出すことができます。
post '/payload' do
request.body.rewind
payload_body = request.body.read
verify_signature(payload_body)
push = JSON.parse(payload_body)
"I got some JSON: #{push.inspect}"
end
Python の例
たとえば、次のような verify_signature
関数を定義し、Webhook ペイロードを受信したらそれを呼び出すことができます。
import hashlib
import hmac
def verify_signature(payload_body, secret_token, signature_header):
"""Verify that the payload was sent from GitHub by validating SHA256.
Raise and return 403 if not authorized.
Args:
payload_body: original request body to verify (request.body())
secret_token: GitHub app webhook token (WEBHOOK_SECRET)
signature_header: header received from GitHub (x-hub-signature-256)
"""
if not signature_header:
raise HTTPException(status_code=403, detail="x-hub-signature-256 header is missing!")
hash_object = hmac.new(secret_token.encode('utf-8'), msg=payload_body, digestmod=hashlib.sha256)
expected_signature = "sha256=" + hash_object.hexdigest()
if not hmac.compare_digest(expected_signature, signature_header):
raise HTTPException(status_code=403, detail="Request signatures didn't match!")
JavaScript の例
たとえば、次のような verifySignature
関数を定義し、Webhook ペイロード受信時に呼び出すことができます。
let encoder = new TextEncoder();
async function verifySignature(secret, header, payload) {
let parts = header.split("=");
let sigHex = parts[1];
let algorithm = { name: "HMAC", hash: { name: 'SHA-256' } };
let keyBytes = encoder.encode(secret);
let extractable = false;
let key = await crypto.subtle.importKey(
"raw",
keyBytes,
algorithm,
extractable,
[ "sign", "verify" ],
);
let sigBytes = hexToBytes(sigHex);
let dataBytes = encoder.encode(payload);
let equal = await crypto.subtle.verify(
algorithm.name,
key,
sigBytes,
dataBytes,
);
return equal;
}
function hexToBytes(hex) {
let len = hex.length / 2;
let bytes = new Uint8Array(len);
let index = 0;
for (let i = 0; i < hex.length; i += 2) {
let c = hex.slice(i, i + 2);
let b = parseInt(c, 16);
bytes[index] = b;
index += 1;
}
return bytes;
}
TypeScript の例
たとえば、次のような verify_signature
関数を定義し、Webhook ペイロードを受信したらそれを呼び出すことができます。
import * as crypto from "crypto"; const WEBHOOK_SECRET: string = process.env.WEBHOOK_SECRET; const verify_signature = (req: Request) => { const signature = crypto .createHmac("sha256", WEBHOOK_SECRET) .update(JSON.stringify(req.body)) .digest("hex"); let trusted = Buffer.from(`sha256=${signature}`, 'ascii'); let untrusted = Buffer.from(req.headers.get("x-hub-signature-256"), 'ascii'); return crypto.timingSafeEqual(trusted, untrusted); }; const handleWebhook = (req: Request, res: Response) => { if (!verify_signature(req)) { res.status(401).send("Unauthorized"); return; } // The rest of your logic here };
import * as crypto from "crypto";
const WEBHOOK_SECRET: string = process.env.WEBHOOK_SECRET;
const verify_signature = (req: Request) => {
const signature = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(JSON.stringify(req.body))
.digest("hex");
let trusted = Buffer.from(`sha256=${signature}`, 'ascii');
let untrusted = Buffer.from(req.headers.get("x-hub-signature-256"), 'ascii');
return crypto.timingSafeEqual(trusted, untrusted);
};
const handleWebhook = (req: Request, res: Response) => {
if (!verify_signature(req)) {
res.status(401).send("Unauthorized");
return;
}
// The rest of your logic here
};