新增微信客服、微信公众号、微信支付通知三个渠道扩展。 微信客服渠道扩展功能模块: - account: 账户管理 - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - crypto: 加解密处理 - dedupe: 消息去重 - customer: 客户管理 - servicer: 客服管理 - session: 会话管理 - status: 会话状态管理 - media: 媒体资源处理 - statistics: 统计功能 - sync: 数据同步 - upgrade: 升级处理 微信公众号渠道扩展功能模块: - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - crypto: 加解密处理 - dedupe: 消息去重 - passive_reply: 被动回复 - message: 消息处理 - broadcast: 群发消息 - template: 模板消息 - menu: 菜单管理 - qrcode: 二维码管理 - user: 用户管理 - media: 媒体资源处理 - status: 会话状态管理 微信支付通知渠道扩展功能模块: - config: 渠道配置管理 - webhook: Webhook 事件处理 - crypto: 加解密与签名校验 - cert_manager: 证书管理 - event_router: 事件路由 - dedupe: 消息去重 - pay_repo: 支付数据仓库 - query_client: 查询客户端 - arq_tasks: 异步任务 - callback_compensator: 回调补偿
136 lines
4.2 KiB
Python
136 lines
4.2 KiB
Python
from __future__ import annotations
|
|
|
|
import base64
|
|
import logging
|
|
|
|
from cryptography import x509
|
|
from cryptography.hazmat.backends import default_backend
|
|
from cryptography.hazmat.primitives import hashes
|
|
from cryptography.hazmat.primitives.asymmetric import padding
|
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
MAX_TIMESTAMP_DRIFT_SECONDS = 300
|
|
|
|
|
|
class SignatureVerificationError(Exception):
|
|
pass
|
|
|
|
|
|
class DecryptionError(Exception):
|
|
pass
|
|
|
|
|
|
class WechatPayCrypto:
|
|
def __init__(self, api_v3_key: str):
|
|
if len(api_v3_key) != 64:
|
|
raise ValueError("APIv3 key must be 32 bytes (64 hex characters)")
|
|
self._api_v3_key = api_v3_key
|
|
|
|
@staticmethod
|
|
def build_authorization_signature(
|
|
method: str,
|
|
url: str,
|
|
body: str,
|
|
timestamp: str,
|
|
nonce_str: str,
|
|
private_key_pem: str,
|
|
) -> str:
|
|
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
|
|
|
private_key = load_pem_private_key(private_key_pem.encode("utf-8"), password=None, backend=default_backend())
|
|
sign_message = f"{method}\n{url}\n{timestamp}\n{nonce_str}\n{body}\n"
|
|
signature = private_key.sign(sign_message.encode("utf-8"), padding.PKCS1v15(), hashes.SHA256())
|
|
return base64.b64encode(signature).decode("utf-8")
|
|
|
|
def verify_signature(
|
|
self,
|
|
body: bytes,
|
|
signature: str,
|
|
timestamp: str,
|
|
nonce: str,
|
|
serial_no: str,
|
|
platform_cert_pem: str,
|
|
) -> bool:
|
|
try:
|
|
dr = _check_timestamp_drift(timestamp)
|
|
if dr:
|
|
logger.warning("WechatPay signature timestamp drift: %s", dr)
|
|
return False
|
|
except (ValueError, TypeError):
|
|
return False
|
|
|
|
sign_message = f"{timestamp}\n{nonce}\n{body.decode('utf-8')}\n"
|
|
|
|
try:
|
|
cert = x509.load_pem_x509_certificate(platform_cert_pem.encode("utf-8"), default_backend())
|
|
public_key = cert.public_key()
|
|
public_key.verify(
|
|
base64.b64decode(signature),
|
|
sign_message.encode("utf-8"),
|
|
padding.PKCS1v15(),
|
|
hashes.SHA256(),
|
|
)
|
|
return True
|
|
except Exception:
|
|
logger.warning("WechatPay signature verification failed for serial=%s", serial_no)
|
|
return False
|
|
|
|
def decrypt_resource(
|
|
self,
|
|
ciphertext: str,
|
|
nonce: str,
|
|
associated_data: str,
|
|
) -> str:
|
|
try:
|
|
aesgcm = AESGCM(bytes.fromhex(self._api_v3_key))
|
|
plaintext = aesgcm.decrypt(
|
|
nonce.encode("utf-8"),
|
|
base64.b64decode(ciphertext),
|
|
associated_data.encode("utf-8") if associated_data else None,
|
|
)
|
|
return plaintext.decode("utf-8")
|
|
except Exception as e:
|
|
raise DecryptionError(f"AES-GCM decryption failed: {e}") from e
|
|
|
|
def verify_signature_with_raw_key(
|
|
self,
|
|
body: bytes,
|
|
signature: str,
|
|
timestamp: str,
|
|
nonce: str,
|
|
public_key_pem: str,
|
|
) -> bool:
|
|
from cryptography.hazmat.primitives.serialization import load_pem_public_key
|
|
|
|
dr = _check_timestamp_drift(timestamp)
|
|
if dr:
|
|
logger.warning("WechatPay signature timestamp drift: %s", dr)
|
|
return False
|
|
|
|
sign_message = f"{timestamp}\n{nonce}\n{body.decode('utf-8')}\n"
|
|
try:
|
|
public_key = load_pem_public_key(public_key_pem.encode("utf-8"), default_backend())
|
|
public_key.verify(
|
|
base64.b64decode(signature),
|
|
sign_message.encode("utf-8"),
|
|
padding.PKCS1v15(),
|
|
hashes.SHA256(),
|
|
)
|
|
return True
|
|
except Exception:
|
|
logger.warning("WechatPay signature verification failed with raw public key")
|
|
return False
|
|
|
|
|
|
def _check_timestamp_drift(timestamp: str) -> str | None:
|
|
import time
|
|
|
|
ts = int(timestamp)
|
|
now = int(time.time())
|
|
diff = abs(now - ts)
|
|
if diff > MAX_TIMESTAMP_DRIFT_SECONDS:
|
|
return f"drift={diff}s > {MAX_TIMESTAMP_DRIFT_SECONDS}s"
|
|
return None
|