This commit is contained in:
macbook_max
2026-10-15 20:36:59 +08:00
commit 6bf949949c
7923 changed files with 1369582 additions and 0 deletions
+216
View File
@@ -0,0 +1,216 @@
"""支付渠道加密能力测试:RSA2 签名/验签与微信 V3 AES-GCM 解密均为离线可验证。
真实商户密钥接入前,用自签密钥对完成「本地签名 → 验签」往返,确保:
- 支付宝 order_string 可被支付宝公钥验签通过(签名串构造正确)
- 回调验签能正确拒绝篡改报文
- 微信 V3 回调解密可还原明文
"""
import base64
import json
import uuid
from urllib.parse import parse_qs
import pytest
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from app.config import settings
from app.payment import alipay, base, wechat
@pytest.fixture(scope="module")
def keypair():
"""生成一对 RSA 密钥(模拟商户私钥 / 平台公钥)"""
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
priv = key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption(),
).decode()
pub = key.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
).decode()
return priv, pub
def _pem_body(pem: str, label: str) -> str:
"""去掉 PEM 头尾与换行,得到裸 base64(支付宝密钥常见下发形式)"""
body = "\n".join(
line for line in pem.splitlines() if not line.startswith("-----")
)
return body.replace("\n", "")
# ---------- base:密钥加载 / 签名原语 ----------
def test_load_private_key_accepts_pem_and_bare_base64(keypair):
priv, _ = keypair
assert base.load_private_key(priv) is not None
# 裸 base64(PKCS8)也应可加载
assert base.load_private_key(_pem_body(priv, "PRIVATE KEY")) is not None
def test_load_public_key_accepts_pem(keypair):
_, pub = keypair
assert base.load_public_key(pub) is not None
def test_load_private_key_empty_raises():
with pytest.raises(base.PaymentConfigError):
base.load_private_key("")
def test_rsa2_sign_verify_roundtrip(keypair):
priv, pub = keypair
content = "app_id=2021000000&method=alipay.trade.app.pay&version=1.0"
signature = base.rsa2_sign(content, priv)
assert base.rsa2_verify(content, signature, pub) is True
# 篡改内容必须验签失败
assert base.rsa2_verify(content + "&x=1", signature, pub) is False
# 篡改签名必须验签失败(非法 base64 也不得抛异常)
assert base.rsa2_verify(content, "not-a-signature", pub) is False
def test_build_sign_content_sorts_and_filters():
params = {"b": "2", "a": "1", "sign": "xxx", "sign_type": "RSA2", "empty": ""}
assert base.build_sign_content(params) == "a=1&b=2"
# ---------- 支付宝 ----------
def test_alipay_dev_mock_when_not_configured(monkeypatch):
monkeypatch.setattr(settings, "alipay_app_id", "")
result = alipay.create_app_pay_params("R123", 100)
assert result["dev_mock"] is True
def test_alipay_order_string_signable_and_verifiable(monkeypatch, keypair):
priv, pub = keypair
monkeypatch.setattr(settings, "alipay_app_id", "2021000000000000")
monkeypatch.setattr(settings, "alipay_private_key", priv)
monkeypatch.setattr(settings, "alipay_public_key", pub)
result = alipay.create_app_pay_params("R20261015001", 1234)
assert "dev_mock" not in result
params = {k: v[0] for k, v in parse_qs(result["order_string"]).items()}
# 关键参数齐备
assert params["app_id"] == "2021000000000000"
assert params["method"] == alipay.METHOD_APP_PAY
assert params["sign_type"] == "RSA2"
# 金额按分转元、两位小数
biz = json.loads(params["biz_content"])
assert biz["total_amount"] == "12.34"
assert biz["out_trade_no"] == "R20261015001"
# order_string 中的参数可被公钥验签通过(即签名串构造正确)
assert base.rsa2_verify(base.build_sign_content(params), params["sign"], pub) is True
def test_alipay_verify_callback_accepts_valid_and_rejects_tampered(monkeypatch, keypair):
priv, pub = keypair
monkeypatch.setattr(settings, "alipay_public_key", pub)
form = {
"out_trade_no": "R20261015001",
"trade_no": "2026101522001400000001",
"trade_status": "TRADE_SUCCESS",
"total_amount": "12.34",
}
form["sign"] = base.rsa2_sign(base.build_sign_content(form), priv)
form["sign_type"] = "RSA2"
assert alipay.verify_callback(form) is True
# 篡改金额 → 验签失败
assert alipay.verify_callback({**form, "total_amount": "0.01"}) is False
# 缺少 sign → 拒绝
form.pop("sign")
assert alipay.verify_callback(form) is False
# ---------- 微信 V3 ----------
def test_wechat_dev_mock_when_not_configured(monkeypatch):
monkeypatch.setattr(settings, "wechat_mchid", "")
import asyncio
result = asyncio.run(wechat.create_app_pay_params("R123", 100))
assert result["dev_mock"] is True
def test_wechat_app_params_second_sign_verifiable(monkeypatch, keypair):
"""未接入网络前,直接验证 App 调起参数的二次签名逻辑"""
import asyncio
priv, pub = keypair
monkeypatch.setattr(settings, "wechat_mchid", "1900000001")
monkeypatch.setattr(settings, "wechat_app_id", "wx1234567890")
monkeypatch.setattr(settings, "wechat_api_v3_key", "k" * 32)
monkeypatch.setattr(settings, "wechat_private_key", priv)
async def fake_prepay(order_no, amount_fen):
return "prepay_test_123"
monkeypatch.setattr(wechat, "_create_prepay_id", fake_prepay)
params = asyncio.run(wechat.create_app_pay_params("R20261015001", 1234))
assert params["appid"] == "wx1234567890"
assert params["partnerid"] == "1900000001"
assert params["prepayid"] == "prepay_test_123"
assert params["package"] == "Sign=WXPay"
sign_str = (
f"{params['appid']}\n{params['partnerid']}\n{params['prepayid']}\n"
f"{params['package']}\n{params['noncestr']}\n{params['timestamp']}\n"
)
assert base.rsa2_verify(sign_str, params["sign"], pub) is True
def test_wechat_verify_callback_roundtrip(monkeypatch, keypair):
"""构造一次完整回调:平台私钥签名 + AES-GCM 加密 → 验签解密还原明文"""
priv, pub = keypair
api_v3_key = "0123456789abcdef0123456789abcdef"
monkeypatch.setattr(settings, "wechat_mchid", "1900000001")
monkeypatch.setattr(settings, "wechat_app_id", "wx1234567890")
monkeypatch.setattr(settings, "wechat_api_v3_key", api_v3_key)
monkeypatch.setattr(settings, "wechat_private_key", priv)
monkeypatch.setattr(settings, "wechat_platform_cert", pub)
plain = {
"out_trade_no": "R20261015001",
"transaction_id": "4200001234202610150001",
"trade_state": "SUCCESS",
"amount": {"total": 1234, "currency": "CNY"},
}
nonce = uuid.uuid4().hex[:12]
ciphertext = AESGCM(api_v3_key.encode()).encrypt(
nonce.encode(), json.dumps(plain).encode(), b"transaction"
)
body = json.dumps({
"resource": {
"algorithm": "AEAD_AES_256_GCM",
"ciphertext": base64.b64encode(ciphertext).decode(),
"nonce": nonce,
"associated_data": "transaction",
}
}).encode()
timestamp, header_nonce = "1792000000", uuid.uuid4().hex
message = f"{timestamp}\n{header_nonce}\n{body.decode()}\n"
headers = {
"wechatpay-timestamp": timestamp,
"wechatpay-nonce": header_nonce,
"wechatpay-signature": base.rsa2_sign(message, priv),
}
result = wechat.verify_callback(headers, body)
assert result is not None
assert result["out_trade_no"] == "R20261015001"
assert result["amount"]["total"] == 1234
# 篡改 timestamp → 验签失败
assert wechat.verify_callback({**headers, "wechatpay-timestamp": "1"}, body) is None
def test_wechat_decrypt_rejects_bad_ciphertext():
assert base.wechat_decrypt_resource({"ciphertext": "!!", "nonce": "abc"}, "k" * 32) is None