0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Zoho WidgetとLambdaの連携 第2回:HMAC署名とDelugeのJSON運搬

0
Last updated at Posted at 2026-07-15

HMACが通らない原因は、鍵でも計算式でもなく、Delugeが組み立てたJSON文字列でした。手元で同じsecret・timestamp・想定本文から再計算した署名は一致するのに、入口Lambdaだけが拒否します。暗号実装ではなく、署名対象の文字列が途中で変わっていました。

シリーズ「Zoho WidgetとLambdaの連携」の第2回です。単独でも読めるように、本稿で使う構成要素を先に示します。

前回: Zoho WidgetとLambdaの連携 第1回:Lambdaの3分割と公開URLの整理

次回: Zoho WidgetとLambdaの連携 第3回:PDFプレビューと直送保存の分離

この記事の内容

  1. HMACの定義と、署名対象がraw bodyである理由
  2. HTTP 401が出たときの切り分け手順
  3. payloadBase64ラッパーを誰が作るか、replayとidempotencyの違い

構成要素

要素 役割
Widget CRM上のUI。通常操作の起点。共有鍵は持たない
Deluge Zoho側の中継。共有鍵で署名し、入口へPOSTする
入口Lambda 着弾本文と署名を検証する公開入口
Secrets Manager(相当) 共有鍵のサーバ側保管場所

画面はZoho CRMのWidget(Svelte)です。通常操作はDelugeがHMACして入口LambdaへPOSTします。

HMACとは何か

HMACは、共有秘密鍵とメッセージから認証符号を作り、改ざんやなりすましを検出する方式です。送り手が署名を付け、受け手が同じ計算結果と比較します。本稿ではHMAC-SHA256を使い、出力をhexで運びます。

署名対象は「意図したJSONオブジェクト」ではなく、HTTPで運ばれた本文のバイト列(raw body)です。1バイトでも異なれば、計算式が正しくても検証は失敗します。

最初に疑った場所

最初に疑ったのは鍵とHMACの計算です。Delugeが出した署名と、同じsecret・timestamp・「送ったつもり」の文字列から手元で再計算した署名は一致しました。それでも入口だけHTTP 401になります。

HTTP 401 Unauthorizedは、有効な認証情報が無いときに返すステータスです。この経路では、多くの場合HMAC検証失敗として扱います。

ヘッダ欠落、hexとBase64、時刻単位、許容幅、鍵の改行やBOMも確認しました。決め手は、入口Lambdaが受け取ったraw bodyをログ出力したことです。送った想定と着弾文字列が構造レベルで異なっていました。空白差だけではありませんでした。

Delugeで文字列が変わる

WidgetからZoho関数へ渡すところは次のとおりです。

ZOHO.CRM.FUNCTIONS.execute(fnName, {
  arguments: JSON.stringify(payload),
});

ここまではブラウザの JSON.stringify です。その先でDelugeがargumentsをMap経由で扱ったり toString() したりすると、Lambdaへ送る最終文字列が変わります。中間層が再シリアライズすると、意味が近くても署名対象のoctet列は一致しません。

日本語、改行入りメモ、+09:00 付き時刻、入れ子構造、キー順や空白は特に壊れやすいです。

実ログの一字一句は載せません。失敗モードから組み立てた例です。

意図(イメージ):
{"action":"save_form","memo":"一行目\n二行目","at":"2026-07-15T10:00:00+09:00"}
壊れ方の例:
- "\n" が消える/別表現になる
- "+09:00" 周りが別文字列になる
- Map文字列化で引用符や構造が崩れる

結果:
署名に使った文字列 ≠ 着弾raw body
→ 計算式が正しくても401

切り分け手順は次のとおりです。

  1. secret/timestamp/signatureヘッダ欠落を潰す
  2. 手元で署名を再計算し、Delugeの署名と一致するか確認する
  3. 入口の着弾raw bodyと、Delugeが署名に使ったrelay文字列をdiffする
  4. diffがあれば暗号実装の疑いをやめる

本件は手順3で原因が確定しました。Deluge側でも、署名直前の文字列を残さないと比較できません。

payloadBase64ラッパー

Base64は、任意のバイト列をASCII文字へ可逆変換する符号化です。ここではaction JSONをBase64化する担当はDelugeです。Widgetは従来どおりaction JSONを渡すだけです。

送るのはaction本体ではなく、UTF-8のBase64と操作者IDを含む最小ラッパーです。HMAC対象はこのラッパーのraw bodyのみです。Lambdaは検証後にデコードします。

{
  "payloadBase64": "<action JSONのBase64>",
  "actorUserId": "<Zoho側で解決した操作者ID>"
}

中継の処理順は次です。arguments検証、ログインユーザーからCRMユーザID解決、action JSONのBase64化、ラッパーJSONの手組み立て、timestamp + "." + relay_body のHMAC-SHA256(hex)、bodyはrelay_bodyのままPOST。操作者を解決できない通常actionは拒否し、画面には認証・リクエスト失敗系の公開文言を返します。

payload_base64 = zoho.encryption.base64Encode(raw_body, "UTF-8");
relay_body = "{\"payloadBase64\":\"" + payload_base64 + "\",\"actorUserId\":\"" + actor_user_id + "\"}";

now_text = zoho.currenttime.toString("yyyy-MM-dd HH:mm:ss", "UTC");
epoch_ms = now_text.unixEpoch("UTC");
ts = (epoch_ms / 1000).toLong().toString();

sign_base = ts + "." + relay_body;
signature = zoho.encryption.hmacsha256(secret, sign_base, "hex");

headers = Map();
headers.put("Content-Type", "application/json; charset=utf-8");
headers.put("X-App-Timestamp", ts);
headers.put("X-App-Signature", signature);

res = invokeurl
[
    url : endpoint
    type : POST
    body : relay_body
    headers : headers
    detailed : true
];

ラッパー自体を再びMapの toString() に載せると、同じ問題に戻ります。手組み立てが必要です。操作者IDはフロント申告を使わず、Delugeが解決した値のみを業務に使います。

HMACの仕様と公開面

  • 署名対象: timestamp + "." + raw_body
  • アルゴリズム: HMAC-SHA256、出力hex
  • 時刻許容: 300秒
  • 検証入力: パース後JSONではなく着弾バイト列
msg = ts.encode("utf-8") + b"." + body
expected = hmac.new(secret.encode("utf-8"), msg, hashlib.sha256).hexdigest()
ok = hmac.compare_digest(sig, expected)

Function URLの認可が NONE でも、通常APIはraw body署名なしでは通りません。ただしエンドポイントが公開HTTPである事実は変わりません。「第三者に何もできない」ではなく、「署名なしでは業務APIが通らない」です。攻撃面の有無と、認可成功の可否は分けて考えます。

replayとidempotency

用語 意味
replay 有効な署名付き要求を、許容時間内にそのまま再利用すること
idempotency 同一の業務要求を複数回送っても、副作用を1回に近づけること

nonce台帳はありません。300秒以内の厳密なreplay防止は未実装です。更新系はクライアント発行のidempotencyKey(HMACのreplay台帳ではない)、更新時刻競合、履歴の状態ガードで副作用の重複を抑えます。

入口から副作用Lambdaへのサーバ間呼び出しにもHMACがあります。鍵の配置は環境依存ですが、ブラウザには出しません。確認用Lambdaはこの境界に入りません。大きいPDFもこの経路には載せません(第3回)。

よくある失敗

症状 確認箇所
401/認証失敗 raw body一致、鍵、timestamp、ヘッダ
署名は合うのにLambdaだけ失敗 Delugeの文字列化(本稿)
他処理により更新済み 楽観的ロック/HTTP 412
大きいPDFだけ失敗 直送経路(第3回
既知の制限 扱い
300秒以内のreplay台帳なし 受容。更新系は別手段で抑止
片方だけの鍵更新 運用事故になる。同時更新が必要
raw bodyデバッグログ 秘密が混ざりやすい。取り扱いに注意

類似症状では、署名式の前に着弾raw bodyと署名対象文字列を一字一句比較してください。本件ではそこで、暗号ではなくJSON運搬の問題だと確定しました。

次回: Zoho WidgetとLambdaの連携 第3回:PDFプレビューと直送保存の分離

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?