ForcePilot/backend/package/yuxi/channels/plugins/dingtalk/signature.py

109 lines
3.3 KiB
Python
Raw Normal View History

"""钉钉 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",
]