ForcePilot/backend/package/yuxi/channels/plugins/qqbot/signature.py
Kris 063b28ddb2 feat(qqbot): add complete QQ Bot channel plugin implementation
实现完整的QQ Bot渠道插件,包含所有必要适配器、客户端、生命周期处理器与配置清单:
1. 新增15个功能适配器覆盖入站、出站、会话、身份解析等全流程
2. 实现QQ Bot API客户端与错误翻译逻辑
3. 完善插件生命周期管理与资源池配置
4. 附带完整的manifest.json声明配置
2026-07-08 22:56:20 +08:00

158 lines
5.2 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.

"""QQ Bot Webhook 签名校验模块v2 占位)。
v1 采用 WebSocket 长连接模式(决策 3Webhook HTTP 回调模式 v2 迭代。
本模块提供 Ed25519 签名校验占位实现,供 v2 启用 Webhook 模式时完善。
提供以下能力:
- ``verify_webhook_signature``: 校验 Webhook Ed25519 签名v2 占位)。
- ``is_timestamp_valid``: 校验时间戳是否在允许的时钟偏移窗口内(防重放)。
- ``is_callback_verification``: 识别 QQ Bot 回调地址验证请求。
依赖方向:仅依赖标准库与 ``yuxi.channels.contract`` 错误类型,不污染框架层。
Ed25519 校验依赖 ``cryptography``,缺失时返回 ``False`` 显式暴露缺失依赖。
"""
from __future__ import annotations
import time
from typing import Any
try:
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
_HAS_CRYPTOGRAPHY = True
except ImportError:
_HAS_CRYPTOGRAPHY = False
# 时间戳允许的最大时钟偏移(秒),防重放攻击
_MAX_TIMESTAMP_SKEW_SECONDS = 300
def verify_webhook_signature(
timestamp: str,
body: str,
signature: str,
public_key_pem: str,
) -> bool:
"""校验 QQ Bot Webhook Ed25519 签名v2 占位)。
签名算法Ed25519(``timestamp + body``),公钥从 QQ 开放平台获取。
使用恒定时间比对以防止时序攻击。
v1 场景下 WebSocket 事件无需 Webhook 签名校验,本方法供 v2 启用
Webhook 模式时调用。
Args:
timestamp: ``X-Signature-Timestamp`` 头值(秒级时间戳字符串)。
body: 原始请求体字符串。
signature: ``X-Signature-Ed25519`` 头值(十六进制编码)。
public_key_pem: QQ 开放平台公钥 PEM 字符串。
Returns:
签名校验结果,``True`` 表示通过。**不抛异常**:输入类型异常、
依赖缺失或签名不匹配时返回 ``False``,由调用方决定是否记录
日志或抛出 ``ValidationError``。
"""
if not _HAS_CRYPTOGRAPHY:
return False
try:
public_key = serialization.load_pem_public_key(
public_key_pem.encode("utf-8"),
)
if not isinstance(public_key, Ed25519PublicKey):
return False
message = f"{timestamp}{body}".encode()
sig_bytes = bytes.fromhex(signature)
public_key.verify(sig_bytes, message)
return True
except (ValueError, TypeError, InvalidSignature, UnicodeEncodeError):
return False
def is_timestamp_valid(timestamp: str, max_skew_seconds: int = _MAX_TIMESTAMP_SKEW_SECONDS) -> bool:
"""校验时间戳是否在允许的时钟偏移窗口内(防重放)。
Args:
timestamp: 时间戳字符串(秒级)。
max_skew_seconds: 允许的最大时间偏差(秒),默认 3005 分钟)。
Returns:
``True`` 表示时间戳有效。解析失败或超出窗口返回 ``False``
**不抛异常**。
"""
try:
ts = int(timestamp)
except (TypeError, ValueError):
return False
skew = abs(int(time.time()) - ts)
return skew <= max_skew_seconds
def is_callback_verification(payload: dict[str, Any]) -> bool:
"""识别 QQ Bot 回调地址验证请求。
QQ Bot 注册 Webhook 时发送 ``{"plain_token": "xxx", "event_ts": "xxx"}``
请求用于验证回调地址,适配器需用 Ed25519 私钥对
``plain_token + event_ts`` 签名并返回 ``{"plain_token": "xxx",
"signature": "xxx"}`` 响应v2 迭代)。
Args:
payload: Webhook 请求体解析后的 dict。
Returns:
``True`` 表示该请求为回调地址验证请求;``False`` 表示正常事件。
"""
return "plain_token" in payload and "event_ts" in payload
def build_callback_verification_response(
plain_token: str,
event_ts: str,
private_key_pem: str,
) -> dict[str, str] | None:
"""构造回调地址验证响应v2 占位)。
使用 Ed25519 私钥对 ``plain_token + event_ts`` 签名,返回
``{"plain_token": "...", "signature": "..."}`` 响应体。
Args:
plain_token: QQ Bot 发送的 plain_token。
event_ts: QQ Bot 发送的 event_ts。
private_key_pem: QQ 开放平台私钥 PEM 字符串。
Returns:
响应体 dict依赖缺失或签名失败返回 ``None``。
"""
if not _HAS_CRYPTOGRAPHY:
return None
try:
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
private_key = serialization.load_pem_private_key(
private_key_pem.encode("utf-8"),
password=None,
)
if not isinstance(private_key, Ed25519PrivateKey):
return None
message = f"{plain_token}{event_ts}".encode()
sig_bytes = private_key.sign(message)
return {
"plain_token": plain_token,
"signature": sig_bytes.hex(),
}
except (ValueError, TypeError, UnicodeEncodeError):
return None
__all__ = [
"verify_webhook_signature",
"is_timestamp_valid",
"is_callback_verification",
"build_callback_verification_response",
]