实现钉钉企业机器人全渠道能力,包含会话解析、消息收发、富消息卡片、命令适配、身份解析、状态回传、健康探测等功能,提供完整的Stream/HTTP双模式事件接入支持,遵循单真相源设计原则,以manifest.json作为配置唯一依据。
109 lines
3.3 KiB
Python
109 lines
3.3 KiB
Python
"""钉钉 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",
|
||
]
|