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