微信支付 APIv3 上送敏感字段:Wechatpay-Serial error 与平台私钥解密失败的排查顺序
商家转账收款人姓名、银行卡号、实名字段这类敏感信息,上送前都要用 RSA 公钥加密。跑不通的时候,报错一般落在这两个上:请求直接被拒(PARAM_ERROR: Platform certificate serial number Wechatpay-Serial error),或者微信侧报平台私钥解密失败。绝大多数是密钥方向和 Wechatpay-Serial 声明对不上,不是代码写错了。
官方文档
方向:上送和下行用的是两把不同的密钥
这是最容易弄反的地方。
上送(商户 → 微信支付)敏感信息:用微信支付平台证书公钥或微信支付公钥加密。只有持有对应私钥的微信支付能解开。
下行(微信支付 → 商户)敏感信息:微信支付用商户证书公钥加密,商户用商户私钥解密。
所以别拿商户证书去加密上送内容,也不要用任何私钥加密上送。方向反了,接口不报加密格式错,而是直接在你说的那两个报错上翻车。
加密算法与填充
算法是 RSA 公钥加密,填充方案 RSAES-OAEP。
各语言里 OAEP 的写法:
- OpenSSL:
padding = RSA_PKCS1_OAEP_PADDING - Java:
Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding") - PHP:
padding = OPENSSL_PKCS1_OAEP_PADDING - .NET:
fOAEP = true - Node.js:
padding = crypto.constants.RSA_PKCS1_OAEP_PADDING - Go:
EncryptOAEP
官方推荐使用 SHA-256 作为哈希算法,但 Java 示例给的是 OAEPWithSHA-1AndMGF1Padding。这里存在不一致,以你实际用的 SDK 和文档为准,核心约束是:你加密的方式,微信侧要能解开。
密文输出统一 Base64。
Wechatpay-Serial 声明的是本次用了哪把公钥
请求头 Wechatpay-Serial 的作用是告诉微信支付:这份密文是用哪把公钥加密的。两种模式传的值不一样。
平台证书模式:传平台证书序列号,例如 Wechatpay-Serial: 4A4EF961F12345D888A902944179F1F270FC68B3。这是示例值,每个商户号不同,必须传自己商户号对应的那本平台证书序列号。
微信支付公钥模式:传微信支付公钥 ID,例如 Wechatpay-Serial: PUB_KEY_ID_0116571234562024052000123400000000。同样是示例值。
微信支付会更新平台证书,更新期间可能同时有多本平台证书可用于加密,所以才要靠 Wechatpay-Serial 声明本次用的是哪一本,微信侧才能挑出对应私钥解出密文。
长度限制:RSA2048 + OAEP 约 214 字节
RSA2048 下,OAEP(SHA-1) 能加密的原文上限约 214 字节,Java 示例里对应的报错文案就是「加密原串的长度不能超过 214 字节」。
姓名、身份证号、银行卡号这类单字段都很短,没问题。要留意的是把多个字段拼成一个 JSON 一起加密的场景,拼完可能就超了。改用 SHA-256 时可用长度还会更短。
报错归因
PARAM_ERROR: Platform certificate serial number Wechatpay-Serial error
出现在「上传参数需要加密敏感信息」的接口,含义是证书序列号错误,或者与请求商户号对应的平台证书不匹配。按顺序查:
- 加密用的证书是不是「微信支付平台证书」。用成商户证书就会报这个。
- 是不是「请求方商户号对应」的平台证书,有没有误用别的商户号的平台证书。
- 商户证书 / 平台证书是否过期,按指引检查更新后重新部署。
平台私钥解密失败
微信侧拿到你的密文,用对应私钥解不开。三类常见原因:
- 加密敏感数据用的公钥不对,不是微信支付平台证书公钥或微信支付公钥。
Wechatpay-Serial声明的序列号,和实际用来加密的那把公钥不匹配。- 加密方式不符合规范,需确保使用 RSAES-OAEP(推荐 SHA-256)。
关联:回调报 AEADBadTagException: Tag mismatch
这是另一条链路的问题——回调 / 平台证书下载报文用 AES-256-GCM,密钥是 APIv3 密钥。Tag mismatch 常见原因:APIv3 密钥用错(用了别的商户号的,或者误用 APIv2 的 APIKey);密文本身不对(报文里给的是 Base64);解密时漏传了 associated_data。
各语言加密示例要点
- Java:
Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding"),密文 Base64 编码。 - PHP:
openssl_public_encrypt($str, $encrypted, $public_key, OPENSSL_PKCS1_OAEP_PADDING)。 - Go:
EncryptOAEP(sha1.New(), rand.Reader, rsaPublicKey, msg, nil),再 Base64。
适用范围说明
上面讲的是商户直连 APIv3、上送敏感字段的场景。服务商模式下的证书归属,以及「平台证书模式」与「微信支付公钥模式」在下载、轮换流程上的差异,以官方文档为准,不要跨模式套用序列号。语言示例部分来自官方文档写法,未在全部语言环境逐一实测,接入时以你所用 SDK 的实现为准。