ForcePilot/backend/package/yuxi/channels/plugins/dingtalk/signature.py
Kris c88afb79ce feat(dingtalk): 新增钉钉渠道插件完整实现
实现钉钉企业机器人全渠道能力,包含会话解析、消息收发、富消息卡片、命令适配、身份解析、状态回传、健康探测等功能,提供完整的Stream/HTTP双模式事件接入支持,遵循单真相源设计原则,以manifest.json作为配置唯一依据。
2026-07-08 22:55:00 +08:00

109 lines
3.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""钉钉 HTTP 模式签名校验。
实现钉钉 HTTP 模式 Webhook 回调的签名验签(设计方案 §3.3)。钉钉 HTTP
模式签名规则:
1. 拼接 ``timestamp + "\\n" + sign_secret``
2. 计算 HMAC-SHA256 拼接字符串Base64 编码
3. 与回调中的 ``sign`` 字段比对(恒定时间比较防时序攻击)
4. ``timestamp`` 与当前时间差超过 60 分钟拒绝(防重放)
Stream 模式由 SDK 内置 TLS 加密,无需签名校验(``verifySignature`` 直接
返回 ``true``)。
依赖方向:仅标准库 + ``yuxi.channels.contract.errors.*``,不污染框架层。
"""
from __future__ import annotations
import base64
import hashlib
import hmac
import time
from typing import Any
from yuxi.channels.contract.errors import ValidationError
from ._constants import HTTP_SIGN_TIMESTAMP_TOLERANCE_SEC
def compute_signature(timestamp: int, sign_secret: str) -> str:
"""计算钉钉 HTTP 模式签名。
Args:
timestamp: 回调中的 ``timestamp`` 字段(毫秒级时间戳)。
sign_secret: HTTP 模式签名密钥(``sign_secret`` 配置项)。
Returns:
Base64 编码的 HMAC-SHA256 签名字符串。
"""
string_to_sign = f"{timestamp}\n{sign_secret}"
hmac_code = hmac.new(
string_to_sign.encode("utf-8"),
digestmod=hashlib.sha256,
).digest()
return base64.b64encode(hmac_code).decode("utf-8")
def verify_signature(
timestamp: int,
sign: str,
sign_secret: str,
*,
now_ts: int | None = None,
) -> bool:
"""校验钉钉 HTTP 模式签名。
Args:
timestamp: 回调中的 ``timestamp`` 字段(毫秒级时间戳)。
sign: 回调中的 ``sign`` 字段(待校验签名)。
sign_secret: HTTP 模式签名密钥。
now_ts: 当前时间戳(毫秒),用于测试注入;为 ``None`` 时取
``int(time.time() * 1000)``。
Returns:
``True`` 表示签名通过;``False`` 表示签名失败或时间戳过期。
本函数 **不抛异常**,调用方据返回值决定是否终止入站管道。
"""
if not sign or not sign_secret or not timestamp:
return False
now = now_ts if now_ts is not None else int(time.time() * 1000)
if abs(now - timestamp) > HTTP_SIGN_TIMESTAMP_TOLERANCE_SEC * 1000:
return False
expected = compute_signature(timestamp, sign_secret)
return hmac.compare_digest(expected, sign)
def extract_timestamp_sign(payload: dict[str, Any]) -> tuple[int, str]:
"""从钉钉 HTTP 模式回调 payload 提取 timestamp 与 sign。
Args:
payload: 钉钉 HTTP 模式回调的完整 payload。
Returns:
``(timestamp, sign)`` 元组。字段缺失时返回 ``(0, "")``。
Raises:
ValidationError: timestamp 字段存在但非整数。
"""
timestamp = payload.get("timestamp", 0)
if timestamp and not isinstance(timestamp, int):
try:
timestamp = int(timestamp)
except (TypeError, ValueError) as exc:
raise ValidationError(
field="timestamp",
message=f"timestamp must be integer: {timestamp}",
) from exc
sign = payload.get("sign", "") or ""
return int(timestamp), sign
__all__ = [
"compute_signature",
"verify_signature",
"extract_timestamp_sign",
]