署名検証とセキュリティ
Webhook
署名検証
HMAC
X-RR-Signature
セキュリティ
Webhookの受信URLは公開されているため、第三者が偽のリクエストを送ることができます。レシートローラーはすべての配信にWebhookのシークレットで計算した署名を付けているので、受信側は処理の前に必ず署名を検証してください。この記事では署名の計算方法、言語別の検証コード、シークレットの入れ替え方を説明します。
署名の仕組み
- 署名はヘッダー
X-RR-Signatureに入っています - 値は
sha256=に続けて、リクエストボディのバイト列そのもの(UTF-8)を、Webhookのシークレット(UTF-8)をキーにしてHMAC-SHA256で計算した結果を、小文字の16進文字列(64文字)にしたものです - 署名の対象はボディだけです。タイムスタンプのヘッダーはなく、ヘッダーの値は署名に含まれません
- 同じイベントの再送では、ボディも署名も最初の送信と同じです
X-RR-Signature: sha256=5d41402abc4b2a76b9719d911017c592...(64文字)
配信に付くヘッダーは X-RR-Signature、X-RR-Event(イベント名)、X-RR-Delivery(イベントID)の3つです。
受信側の検証手順
- JSONにパースする前の、受信したボディのバイト列を取り出します。パースしてから文字列に戻すと空白やキーの順序が変わり、署名が一致しません
- 保管しているシークレットをキーにしてHMAC-SHA256を計算し、小文字の16進文字列にして先頭に
sha256=を付けます X-RR-Signatureの値と、定数時間で比較する関数で照合します- 一致しなければ
401などを返して処理しません(2xx以外を返すと再送されますが、署名が変わらない限り結果は同じです) - 一致したら
X-RR-Deliveryで重複を確認してから処理します
サンプルコード
Node.js(Express)
import crypto from "node:crypto";
import express from "express";
const SECRET = process.env.RR_WEBHOOK_SECRET;
const app = express();
function hasValidSignature(rawBody, header) {
if (!header) return false;
const expected = "sha256=" +
crypto.createHmac("sha256", SECRET).update(rawBody).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(header);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// express.raw でパース前のボディ(Buffer)を受け取る
app.post("/rr-webhook", express.raw({ type: "application/json" }), (req, res) => {
if (!hasValidSignature(req.body, req.get("X-RR-Signature"))) {
return res.status(401).end();
}
const event = JSON.parse(req.body.toString("utf8"));
// X-RR-Delivery で重複を確認してから処理する
res.status(200).end();
});
Python(Flask)
import hashlib, hmac, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["RR_WEBHOOK_SECRET"].encode("utf-8")
@app.post("/rr-webhook")
def rr_webhook():
body = request.get_data() # パース前のバイト列
expected = "sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-RR-Signature", "")):
abort(401)
event = request.get_json()
# X-RR-Delivery で重複を確認してから処理する
return "", 200
C#(ASP.NET Core)
[HttpPost("/rr-webhook")]
public async Task<IActionResult> Receive()
{
using var buffer = new MemoryStream();
await Request.Body.CopyToAsync(buffer);
var body = buffer.ToArray(); // パース前のバイト列
var secret = Encoding.UTF8.GetBytes(_config["RR_WEBHOOK_SECRET"]!);
var expected = "sha256=" + Convert.ToHexStringLower(HMACSHA256.HashData(secret, body));
var received = Request.Headers["X-RR-Signature"].ToString();
if (!CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(received)))
return Unauthorized();
// X-RR-Delivery で重複を確認してから処理する
return Ok();
}
リプレイへの備え
署名はボディだけに対するものなので、一度届いた正しいリクエストを第三者がそのまま送り直すと、署名の検証は通ります。次の方法で防ぎます。
X-RR-Delivery(=ボディのid)を処理済みとして記録し、同じ値は処理しない。これは再送による重複を除くのと同じ仕組みです- ボディの
timestampは署名の対象に含まれるため、古すぎるリクエストを拒否する目安に使えます。ただし再送は最初の送信と同じtimestampのまま約71分後まで届くので、許容幅は2時間程度より短くしないでください
シークレットの発行と入れ替え
- シークレットはWebhookを登録したときに決まります。APIでは16〜64文字を指定するか省略して32文字の英数字を生成させ、開発者ポータルの画面では自動で生成されます
- シークレットは登録時に一度だけ表示されます。あとから表示したり、作り直したりする機能はありません
- シークレットを入れ替えるときは、新しいシークレットで同じURLのWebhookを登録し、受信側でしばらく新旧どちらのシークレットでも検証が通るようにしてから、古いWebhookを削除します。並行している間は同じイベントが両方のWebhookから届きますが、
X-RR-Deliveryは同じなので、重複の除去で1回だけ処理されます - 漏れた疑いがあるときは、すぐに新しいWebhookを登録して古いものを削除してください
以前に登録したWebhookの署名
2026年10月23日の修正より前に登録したWebhookは、シークレットではなくアプリのクライアントIDで署名されています。シークレットで検証して一致しない場合は、Webhookを登録し直してください。新しく登録したWebhookはシークレットで署名されます。
よくある実装ミス
| ミス | 起きること | 対処 |
|---|---|---|
| パースしたJSONを文字列に戻して計算する | 毎回一致しない | 受信したバイト列をそのまま使う |
sha256= を付けずに比較する | 毎回一致しない | 計算結果の先頭に sha256= を付けるか、ヘッダーから取り除いて比較する |
| 大文字の16進やBase64で比較する | 毎回一致しない | 小文字の16進にする |
| クライアントシークレットで計算する | 毎回一致しない | Webhook登録時のシークレットを使う |
== で比較する | タイミング攻撃で署名を推測される可能性 | 定数時間で比較する関数を使う |
検証に失敗しても 2xx を返す | 偽のリクエストを受け入れたように見える | 401 を返す |
シークレットの保管
- コードやリポジトリに書かず、環境変数やシークレット管理サービスに置きます
- 本番・ステージング・開発で別のWebhookを登録し、別のシークレットを使います
- ログにシークレットや署名の値を出力しません
関連ガイド
公開日: 2026-04-27
更新日: 2026-10-26
カテゴリ
タグ
API (26)
OAuth (21)
Android (13)
iOS (12)
Webhook (9)
api (6)
oauth (5)
クイックスタート (5)
トラブル (5)
Node.js (4)
関連記事
-
Webhookの登録方法開発者ポータルの画面とAPIでWebhookを登録する手順、購読イベントの選び方、テスト送信、変更・一時停止・削除の方法を説明します。
-
SNS Webhookバイパス(LINEなど外部SNSのWebhook転送)レシートローラーがLINE等のSNSプラットフォームから受け取ったWebhookを、店舗側の同意のもとで開発者アプリへ転送する「SNS Webhookバイパス」機能の仕組み、設定方法、署名の取り扱い、注意点を解説します。
-
Webhookが届かないWebhookが届かないときに、テスト送信と配信ログで原因を切り分け、無効化・購読イベント・接続・署名検証を順に確認する手順です。
-
監視と失敗時の対応Webhookの有効状態・最終送信日時・配信ログの見方と、自動で無効になったことに気づいて復旧し、抜けた期間をAPIで補う方法を説明します。
-
開発者向けヘルプ目次レシートローラー開発者向けヘルプ目次です。開発者申請、アプリケーション登録、OAuth認証とスコープ、実装ガイド(ウォレットアプリ・店舗向けWebhook・Survey API)、データ領域別ガイド、運用とセキュリティ、コミュニティ、トラブルシューティングまでをまとめています。