Skip to main content

Webhook 配信を検証する

ペイロードを受信するようにサーバーが設定されると、設定したエンドポイントに送信された配信すべてがリッスンされます。 サーバーが によって送信された Webhook 配信のみを処理し、配信が改ざんされていないことを確認するには、配信をさらに処理する前に webhook 署名を検証する必要があります。 これにより、 からの配信の処理にサーバー時間を費やすのを回避し、中間者攻撃を回避するのに役立ちます。

そのためには、次の手順を実行する必要があります。

  1. Webhook のシークレット トークンを作成します。
  2. トークンをサーバーに安全に格納します。
  3. 着信した Webhook ペイロードをトークンに対して検証し、それらが から送信されたもので、改ざんされていないことを確認します。

シークレット トークンを使用して新しい Webhook を作成することも、既存の Webhook にシークレット トークンを追加することもできます。 シークレット トークンを作成する際は、エントロピーの高いランダムな文字列を選択してください。

  • "シークレット トークンを使用して新しい Webhook を作成する" には、「webhookの作成」を参照してください。__
  • _既存の Webhook にシークレット トークンを追加する_には、Webhook の設定を編集します。 [シークレット] に、secret キーとして使用する文字列を入力します。 詳しくは、「webhookの編集」をご覧ください。

シークレット トークンを作成したら、サーバーがアクセスできる安全な場所に格納する必要があります。 トークンをアプリケーションにハードコーディングしたり、トークンをリポジトリにプッシュしたりしないでください。 コードで認証資格情報を安全に使用する方法の詳細については、「API 資格情報をセキュリティで保護する」を参照してください。

は、ユーザーのシークレット トークンを使って、各ペイロードでユーザーに送信されるハッシュ署名を作成します。 ハッシュ署名は、各配信の X-Hub-Signature-256 ヘッダーの値として表示されます。 詳しくは、「Webhook のイベントとペイロード」をご覧ください。

Webhook 配信を処理するコードでは、シークレット トークンを使用してハッシュを計算する必要があります。 次に、 が送信したハッシュを、計算したハッシュの想定値と比較し、それらが一致していることを確認します。 さまざまなプログラミング言語でハッシュを検証する方法を示す例については、「」を参照してください。

Webhook ペイロードを検証する際には、いくつかの重要な点に留意する必要があります。

  • は、HMAC 16 進ダイジェストを使ってハッシュを計算します。
  • ハッシュ署名は常に、sha256= から始まります。
  • ハッシュ署名は、Webhook のシークレット トークンとペイロードの内容を使用して生成されます。
  • 言語とサーバーの実装で文字エンコーディングが指定されている場合は、ペイロードをUTF-8として扱うようにしてください。 Webhook ペイロードには Unicode 文字を含めることができます。
  • プレーン == 演算子は使用しないでください。 代わりに、「一定時間」の文字列比較を行う secure_comparecrypto.timingSafeEqual などのメソッドを使用して、通常の等価演算子に対する特定のタイミングでの攻撃や、JIT 最適化言語における通常のループを緩和することを検討してください。

次の secretpayload 値を使用して、実装が正しいことを確認できます。

  • secret: It's a Secret to Everybody
  • payload: Hello, World!

実装が正しければ、生成するシグネチャは次のシグネチャ値と一致しています。

  • signature: 757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17
  • X-Hub-Signature-256: sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17

選択したプログラミング言語を使用して、コードに HMAC 検証を実装できます。 次に、実装がさまざまなプログラミング言語でどのように表示されるかを示す例をいくつか示します。

たとえば、次のような 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

たとえば、次のような verify_signature 関数を定義し、Webhook ペイロードを受信したらそれを呼び出すことができます。

import hashlib
import hmac
def verify_signature(payload_body, secret_token, signature_header):
    """Verify that the payload was sent from  by validating SHA256.

    Raise and return 403 if not authorized.

    Args:
        payload_body: original request body to verify (request.body())
        secret_token:  app webhook token (WEBHOOK_SECRET)
        signature_header: header received from  (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!")

たとえば、次のような 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;
}

たとえば、次のような verify_signature 関数を定義し、Webhook ペイロードを受信したらそれを呼び出すことができます。

JavaScript
import { Webhooks } from "@octokit/webhooks";

const webhooks = new Webhooks({
  secret: process.env.WEBHOOK_SECRET,
});

const handleWebhook = async (req, res) => {
  const signature = req.headers["x-hub-signature-256"];
  const body = await req.text();

  if (!(await webhooks.verify(body, signature))) {
    res.status(401).send("Unauthorized");
    return;
  }

  // The rest of your logic here
};

ペイロードが から確実に取得されているが、署名の検証が失敗する場合:

  • Webhook のシークレットが構成されていることを確認します。 Webhook のシークレットを構成していない場合、X-Hub-Signature-256 ヘッダーは存在しません。 Webhook シークレットの設定の詳細については、「webhookの編集」を参照してください。
  • 正しいヘッダーを使用していることを確認します。 では、HMAC-SHA256 アルゴリズムを使用する X-Hub-Signature-256 ヘッダーを使用することをお勧めします。 X-Hub-Signature ヘッダーはHMAC-SHA1 アルゴリズムを使用し、従来の目的でのみ含まれています。
  • 正しいアルゴリズムを使用していることを確認します。 X-Hub-Signature-256 ヘッダーを使用している場合は、HMAC-SHA256 アルゴリズムを使用する必要があります。
  • 正しい webhook シークレットを使用していることを確認します。 Webhook シークレットの値がわからない場合は、Webhook のシークレットを更新できます。 詳しくは、「webhookの編集」をご覧ください。
  • 検証の前にペイロードとヘッダーが変更されていないことを確認します。 たとえば、プロキシまたは負荷バランサーを使用する場合は、プロキシまたは負荷バランサーがペイロードまたはヘッダーを変更していないことを確認します。
  • 言語とサーバーの実装で文字エンコーディングが指定されている場合は、ペイロードをUTF-8として扱うようにしてください。 Webhook ペイロードには Unicode 文字を含めることができます。