署名検証とセキュリティ
Webhook
署名検証
HMAC
セキュリティ
リプレイ攻撃
この記事の対象
Webhookエンドポイントを実装する開発者向けです。届いたリクエストが本当にレシートローラーから送られたものかを確認する署名検証の方法と、セキュリティ上の注意点を説明します。
Webhookエンドポイントを実装する開発者向けです。届いたリクエストが本当にレシートローラーから送られたものかを確認する署名検証の方法と、セキュリティ上の注意点を説明します。
Webhookエンドポイントは公開URLなので、第三者が偽のPOSTを送り込むことが原理的に可能です。これを防ぐため、レシートローラーは全てのWebhookリクエストに HMAC-SHA256 ベースの署名を付与しています。受信側は必ず署名を検証してから処理してください。検証していないエンドポイントは攻撃に対して無防備です。
署名の仕組み
レシートローラーは送信前に次の手順で署名を計算します。
- 署名対象文字列を作る:
{timestamp}.{request_body} - Webhook Secret をキーとして HMAC-SHA256 を計算
- 結果を hex エンコード
- HTTPヘッダーに付与して送信
送信される関連ヘッダー
| ヘッダー名 | 内容 |
|---|---|
X-RR-Signature |
HMAC-SHA256 の hex 文字列(64文字) |
X-RR-Timestamp |
配信時刻(Unix秒) |
X-RR-Event-Id |
イベントID(冪等性キー) |
X-RR-Signature-Version |
署名バージョン(現行は v1) |
受信側の検証手順
X-RR-Timestampと生のリクエストボディを取り出す(パース後のオブジェクトではなく、受信したバイト列そのもの)- 署名対象文字列
{timestamp}.{body}を組み立てる - 保管している Webhook Secret で HMAC-SHA256 を計算し hex エンコード
- 計算結果と
X-RR-Signatureを定数時間比較で照合(タイミング攻撃対策) - タイムスタンプが現在時刻から ±5分以内であることを確認(リプレイ攻撃対策)
サンプルコード
Node.js(Express)
const crypto = require("crypto");
const express = require("express");
const app = express();
const SECRET = process.env.RR_WEBHOOK_SECRET;
const TOLERANCE_SEC = 300; // 5分
// 重要:生ボディを保持するため express.raw を使う
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.header("X-RR-Signature");
const ts = req.header("X-RR-Timestamp");
const body = req.body; // Buffer
// タイムスタンプ検証
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(ts, 10)) > TOLERANCE_SEC) {
return res.status(401).send("timestamp out of tolerance");
}
// 署名検証
const expected = crypto
.createHmac("sha256", SECRET)
.update(`${ts}.${body.toString("utf8")}`)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).send("invalid signature");
}
// 検証OK:イベントを処理
const event = JSON.parse(body.toString("utf8"));
// ... 冪等性チェック、業務処理 ...
res.status(200).send("ok");
});
Python(Flask)
import hmac, hashlib, time, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["RR_WEBHOOK_SECRET"].encode()
TOLERANCE_SEC = 300
@app.route("/webhook", methods=["POST"])
def webhook():
sig = request.headers.get("X-RR-Signature", "")
ts = request.headers.get("X-RR-Timestamp", "")
body = request.get_data() # 生バイト列
# タイムスタンプ検証
if abs(int(time.time()) - int(ts)) > TOLERANCE_SEC:
abort(401)
# 署名検証
payload = f"{ts}.".encode() + body
expected = hmac.new(SECRET, payload, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
abort(401)
# 検証OK:イベントを処理
event = request.get_json()
# ... 冪等性チェック、業務処理 ...
return "ok", 200
C#(ASP.NET Core)
[HttpPost("/webhook")]
public async Task<IActionResult> Receive()
{
Request.EnableBuffering();
using var reader = new StreamReader(Request.Body);
var body = await reader.ReadToEndAsync();
var sig = Request.Headers["X-RR-Signature"].ToString();
var ts = Request.Headers["X-RR-Timestamp"].ToString();
// タイムスタンプ検証
var nowSec = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (Math.Abs(nowSec - long.Parse(ts)) > 300) return Unauthorized();
// 署名検証
var secret = Encoding.UTF8.GetBytes(_config["RR_WEBHOOK_SECRET"]);
using var h = new HMACSHA256(secret);
var expected = Convert.ToHexString(
h.ComputeHash(Encoding.UTF8.GetBytes($"{ts}.{body}"))
).ToLowerInvariant();
if (!CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(sig),
Encoding.UTF8.GetBytes(expected)))
return Unauthorized();
// 検証OK:イベントを処理
// ... 冪等性チェック、業務処理 ...
return Ok();
}
よくある実装ミス
| ミス | 影響 | 対処 |
|---|---|---|
| JSONパース後の文字列で署名計算 | 空白・改行が変わり毎回失敗 | 必ず生バイト列を使う |
通常の == で比較 |
タイミング攻撃で署名を推測される可能性 | 定数時間比較関数を使う |
| タイムスタンプを検証しない | 過去の有効リクエストを再送される(リプレイ攻撃) | ±5分の許容範囲を実装 |
| シークレットをコードに直書き | リポジトリ流出時に漏えい | 環境変数またはシークレットマネージャーへ |
検証失敗時に 2xx を返す |
攻撃者に「受信成功」と誤認させる | 401 を返す |
シークレットの安全な管理
- 保管場所:環境変数、AWS Secrets Manager、Azure Key Vault、GCP Secret Manager などのシークレット管理サービス
- アクセス制御:本番シークレットは本番環境のサービスアカウントのみが読める状態に
- ローテーション:少なくとも年1回、漏えい疑い時は即時。詳細は「シークレットのローテーションと安全な保管」を参照
- 環境分離:本番/ステージング/開発で別シークレットを使う
- ログ出力禁止:エラーログや監査ログにシークレットや署名値を出力しない
追加のネットワーク対策(任意)
署名検証で十分ですが、より厳格に運用したい場合は以下も検討できます。
- IP許可リスト:レシートローラーの送信元IPレンジを許可リストに追加(IPレンジは予告なく変更される可能性があるため、署名検証の代替ではなく追加策として)
- WAF:エンドポイントの前段にWAFを置き、不審なリクエストを遮断
- レート制限:単一IPからの過剰リクエストを制限
関連ガイド
公開日: 2026-04-27
更新日: 2026-04-27
カテゴリ
タグ
API (8)
Webhook (8)
api (6)
oauth (5)
トラブル (5)
OAuth (4)
getting-started (4)
アプリ登録 (4)
app-registration (3)
webhook (3)
関連記事
-
Webhookの登録方法レシートローラーの開発者ポータルでWebhookエンドポイントを登録する手順、購読イベントの選び方、テスト配信、複数エンドポイントの使い分け、削除と一時停止の方法を解説します。
-
SNS Webhookバイパス(LINEなど外部SNSのWebhook転送)レシートローラーがLINE等のSNSプラットフォームから受け取ったWebhookを、店舗側の同意のもとで開発者アプリへ転送する「SNS Webhookバイパス」機能の仕組み、設定方法、署名の取り扱い、注意点を解説します。
-
監視と失敗時の対応レシートローラーWebhookの配信履歴の見方、監視すべき指標とアラート設計、デッドレターの再配信、よくある障害パターンと復旧手順を解説します。
-
Webhookが届かないWebhookが受信エンドポイントに届かないときの原因切り分け。エンドポイント設定・購読イベント・到達性・署名検証失敗・ファイアウォールなどを順に確認する手順を解説します。
-
開発者向けヘルプ目次レシートローラー開発者向けヘルプ目次です。開発者申請、アプリケーション登録、OAuth認証とスコープ、実装ガイド(ウォレットアプリ・店舗向けWebhook・Survey API)、データ領域別ガイド、運用とセキュリティ、コミュニティ、トラブルシューティングまでをまとめています。