Telegram Webhookのsecret_tokenとHMAC:受信側で何を検証するか
Telegram Bot APIのsecret_tokenは、HMAC署名ではありません。setWebhookで設定すると、Telegramは同じ値をX-Telegram-Bot-Api-Secret-Tokenヘッダーに入れて送信します。受信側は設定済みのトークンと照合します。一方、署名を有効にしたUnifyPortのwebhookでは、タイムスタンプとリクエスト本文の生バイト列からHMAC-SHA256を計算し、X-Device-Signatureを検証します。この2つの手順は交換できません。
最初に押さえる違い
- Telegramのヘッダーは共有認証情報そのものです。JSON本文から計算したダイジェストではありません。
- UnifyPortの署名は、エンドポイントの
signing_secretを使い、生の本文とX-Device-Timestampを結び付けます。 - どちらにもHTTPS、秘密情報の保護、重複処理を防ぐ設計が必要です。
- 検証方式は信頼できるルート設定で決めます。未検証のJSON内の
providerや、たまたま付いているヘッダーから選んではいけません。
この記事の対象はリクエスト認証です。ボットと既存のメッセージングアカウントのどちらを使うかについては、Telegram Bot API webhookと統一受信webhookの比較を参照してください。
Telegramのsecret_tokenが検証するもの
Telegram公式Bot APIリファレンスでは、secret_tokenはsetWebhookの任意パラメーターです。長さは1~256文字で、使用できる文字はA-Z、a-z、0-9、_、-です。設定すると、各webhookリクエストのX-Telegram-Bot-Api-Secret-Tokenにその値が含まれます。
この保護を必須にする受信側では、ヘッダーが欠落している、または一致しないリクエストを信頼済みの処理キューに入れません。一致から分かるのは、設定した値をリクエストの送信者が持っていることです。その値と本文との間に、独立した暗号学的な結び付きができるわけではありません。
この違いは、プロキシや転送サービスで重要になります。トークンを読めるコンポーネントは、同じトークンを別の本文に付けて送信できます。HTTPSは通信接続を保護しますが、固定値のヘッダー自体はTLS終端後の本文変更を検出しません。これは信頼境界の違いであり、公式Bot APIを使わない理由ではありません。
webhookには専用の認証情報を用意し、bot tokenを流用しないことを推奨します。公開のリクエスト収集サービスに貼り付けず、アクセスログ、トレースのエクスポート、サポート用の画面キャプチャにも残さないようにします。
固定トークンと本文署名の比較
HMACは、共有秘密鍵を使ったメッセージ認証方式です。UnifyPortの契約では、ヘッダーと秘密鍵を直接比較するのではなく、指定された入力からダイジェストを再計算します。
| 項目 | Telegram Bot API webhook | 署名付きUnifyPort webhook |
|---|---|---|
| 設定 | setWebhook.secret_token | エンドポイントのsigning_secret |
| 検証するヘッダー | X-Telegram-Bot-Api-Secret-Token | X-Device-Signature |
| 受け取る値 | 設定したトークン自体 | 16進数のHMAC-SHA256ダイジェスト |
| 本文を検証対象に含むか | 含まない | 生のバイト列を含む |
| タイムスタンプを含むか | 含まない | X-Device-Timestampを含む |
| 受信側の処理 | ヘッダーとトークンを照合 | ダイジェストを再計算して照合 |
| 一度だけの処理を保証するか | しない | しない |
UnifyPortの署名対象は厳密に次の形です。
<X-Device-Timestamp>.<raw request body>
タイムスタンプはRFC 3339 UTC形式で、区切りは文字としてのピリオドです。JSONの整形、空白の変更、解析後の再シリアライズは、署名対象のバイト列を変える可能性があります。signing_secretはHMACの鍵であって、署名ヘッダーにそのまま入る値ではありません。
signing_secretが空の場合、UnifyPortの署名は無効になり、X-Device-Signatureは送られません。署名を必須にする受信側は、認証方式を自動で切り替えずに拒否する必要があります。
受信ルートを分離する
ネイティブのTelegram更新とUnifyPortイベントには、別々のアプリケーションルートを用意する設計を推奨します。これはアプリケーション側の推奨設計であり、新しいプロバイダーAPIではありません。LINEとTelegramを同じサポートキューに集約する場合も、認証の境界は先に分けておきます。
- ルートと送信元・認証情報を結び付ける。 デプロイ設定で検証方式を固定します。
- 配送処理の前に認証する。 Telegramはトークンヘッダー、署名付きUnifyPortは生の本文のHMACとタイムスタンプの鮮度を確認します。
- それぞれの形式を検証する。 Telegramの
Updateを、UnifyPortのmessage.receivedを期待するハンドラーに渡しません。 - 受け付けた仕事を永続化する。 認証、冪等性、業務上の権限確認は別々に扱います。
- 秘密ではなく失敗の種類を記録する。 どの検証に失敗したかだけをログに残します。
共通ルートで「どちらかのヘッダーが通れば受け付ける」ミドルウェアは避けてください。弱い分岐や誤って有効にした分岐が、本来の検証ポリシーに代わる入口になります。特にUnifyPortルートで必要なHMACを、Telegramのトークンで代用してはいけません。
実装の詳細はHMACのリプレイ対策と再試行処理を参照してください。メッセージJSON内の時刻は、配信署名で保護されたタイムスタンプの代わりにはなりません。
本番投入前の受け入れテスト
以下は推奨テストであり、実行済みの結果ではありません。管理下の認証情報を使い、隔離環境で確認してください。
| テスト | 期待する挙動 |
|---|---|
| Telegramヘッダーが欠落または不一致 | 信頼済み処理の前に拒否 |
| Telegramトークンは正しいが本文が変更された | ヘッダー検証だけでは検出できない。形式検証と信頼できる通信境界が必要 |
| UnifyPort本文が署名後に変更された | ダイジェスト不一致で拒否 |
| UnifyPort署名は正しいが時刻が許容範囲外 | 鮮度ポリシーに従って拒否 |
| UnifyPortルートにTelegramトークンを送る | 検証方式を変えずに拒否 |
| 正当な通常イベントが再配信される | 業務処理を繰り返さず、冪等に受け付ける |
時刻の許容範囲は、時計の精度と配信条件に合わせて設定します。別サービスの値をそのまま使わないでください。また、認証が成功しても、メッセージ内のすべての命令を実行してよいとは限りません。
UnifyPortの役割と限界
UnifyPortの非公式インターフェースは、接続済みのメッセージングアカウントから正規化イベントを提供します。HMACが保護するのは、UnifyPortから受信サーバーへの受け渡しです。Telegramネイティブの署名でも、Telegramユーザーによる作成をエンドツーエンドで証明するものでもありません。
Telegramボットとして動く製品なら、その入口で公式の保護を実装すれば十分です。認証方式を変えるためだけに移行する必要はありません。UnifyPortでアカウント単位または複数チャネルのキューを構築する場合は、独自の契約に従い、webhookエンドポイントの作成時に署名を有効にしてください。
よくある質問
Telegramのsecret_tokenでHMACを計算しますか?
公式Bot APIの秘密ヘッダーを検証するためには計算しません。ヘッダーを設定済みのトークンと比較します。トークン自体を運ぶヘッダーに、独自の本文署名アルゴリズムを想定しないでください。
X-Device-Signatureとsigning_secretを直接比較できますか?
できません。文書で指定されたタイムスタンプと生の本文からHMAC-SHA256を再計算し、受け取った16進数の署名と比較します。
どちらかを使えば重複処理を防げますか?
いいえ。認証と重複排除は別です。受け付けた仕事を永続化し、後続の操作も冪等にしてください。HMACだけで一度きりの配信にはなりません。
次のステップと出典
UnifyPort受信側の実装契約には、webhook配信と署名検証リファレンスを使用してください。
出典確認日:2026-09-17。
メッセージ連携を安定したプロダクトパイプラインへ。
まずは 1 つの API で送信を始め、標準イベントですべての inbound メッセージを業務システムへ戻しましょう。