11.3平台证书
微信支付会在HTTP请求应答的头部中包括应答签名,商户必须 验证应答的签名,以确保应答是由微信支付平台发送的。同样也要对微信支付回调的HTTP头部的签名进行验证。
应答和回调的签名验证使用的是微信支付的平台证书,注意不是商户证书。平台证书是指由微信支付 负责申请的,包含微信支付平台标识、公钥信息的证书。商户可以使用平台证书中的公钥进行验签。微信支付的平台证书的可通过调用“获取平台证书接口”来获取。获取平台证书的接口地址为:
https://api.mch.weixin.qq.com/v3/certificates
调用微信支付平台证书获取接口的处理逻辑如下:
- 首先获取商户证书以及商户私钥,使用商户证书以及商户私钥构造HTTP请求签名,并设置请求头规定的字段
- 发送HTTP的GET请求
- 解析请求返回数据,并使用AEAD_AES_256_GCM 算法解密证书数据
以下是微信支付平台证书的接口调用的相关代码:
type WxCertRet struct {
Data []WxCertRetItem `json:"data"`
}
type WxCertRetItem struct {
EffectiveTime string `json:"effective_time"`
Certificate WxCertificate `json:"encrypt_certificate"`
ExpireTime string `json:"expire_time"`
SerialNo string `json:"serial_no"`
}
type WxCertificate struct {
Algorithm string `json:"algorithm"`
AssociatedData string `json:"associated_data"`
Ciphertext string `json:"ciphertext"`
Nonce string `json:"nonce"`
}
//获取微信支付平台证书
func downloadPlatCertificate(ctx *MchParam) error {
const publicKeyUrl = "https://api.mch.weixin.qq.com/v3/certificates"
log.Println(publicKeyUrl)
token, err := GenerateWxPayReqHeader(ctx, http.MethodGet, publicKeyUrl, "")
if err != nil {
log.Println(err)
return err
}
request, err := http.NewRequest(http.MethodGet, publicKeyUrl, nil)
if err != nil {
log.Println(err)
return err
}
request.Header.Add("Authorization", token)
request.Header.Add("User-Agent", "go pay sdk")
request.Header.Add("Content-type", "application/json;charset='utf-8'")
request.Header.Add("Accept", "application/json")
client := http.DefaultClient
response, err := client.Do(request)
if err != nil {
log.Println(err)
return err
}
defer response.Body.Close()
bodyBytes, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Println(err)
return err
}
var tokenResponse WxCertRet
if err = json.Unmarshal(bodyBytes, &tokenResponse); err != nil {
log.Println(err)
return err
}
//使用 AEAD_AES_256_GCM 算法进行解密
for _, encrypt_cert := range tokenResponse.Data {
decryptBytes, err := DecryptAES256GCM(
ctx.MchAPIKey,
encrypt_cert.Certificate.AssociatedData,
encrypt_cert.Certificate.Nonce,
encrypt_cert.Certificate.Ciphertext)
if err != nil {
log.Println(err)
return err
}
cert_ret, _ := LoadCertificate(decryptBytes)
if cert_ret != nil {
serial_no := encrypt_cert.SerialNo
map_plat_cert[serial_no] = cert_ret
}
}
return nil
}
说明:
- 平台证书设置了有效期,商户需在证书过期前调用接口, 并更新操证书。
- 微信平台会在旧证书过期前10天生成新证书,建议商户在旧证书过期前5-10天下载并部署新证书。
- 旧证书过期前的5天内,微信支付允许同时使用新旧证书,因此建议商户系统能够支持多平台证书。
以下代码是多平台证书管理的示例:
var map_plat_cert = make(map[string]*x509.Certificate)
var lock_plat_cert sync.RWMutex
func GetPlatCertificate(ctx *MchParam, serial_no string) *x509.Certificate {
lock_plat_cert.Lock()
defer lock_plat_cert.Unlock()
cert, ok := map_plat_cert[serial_no]
if !ok {
//调用证书获取接口
downloadPlatCertificate(ctx)
cert = map_plat_cert[serial_no]
if cert == nil {
log.Println("GetPlatCertificate error !!!")
}
}
return cert
}
11.4应答签名
如果验证商户的请求签名正确,微信支付会在应答的HTTP头部中包括应答签名。我们建议商户验证应答签名。同样的,微信支付会在回调的HTTP头部中包括回调报文的签名。商户必须 验证回调的签名,以确保回调是由微信支付平台发送的。
微信支付平台使用平台私钥(注意不是商户私钥 )进行应答签名。微信支付的平台证书序列号位于HTTP的请求头Wechatpay-Serial中。验证签名前,请商户先检查序列号是否跟商户当前所持有的微信支付平台证书的序列号一致。如果不一致,请重新获取证书。否则签名的私钥和证书不匹配,将无法成功验证签名。
首先,商户先从应答中获取以下信息。
- HTTP头Wechatpay-Timestamp 中的应答时间戳。
- HTTP头Wechatpay-Nonce 中的应答随机串
- 应答主体(response Body)
然后,请按照以下规则构造应答的签名串。签名串共有三行,行尾以\n 结束,包括最后一行。
应答时间戳\n应答随机串\n应答报文主体\n
微信支付的应答签名通过HTTP头Wechatpay-Signature传递。对 Wechatpay-Signature的字段值使用Base64进行解码,得到应答签名。以下是签名验证的相关代码:
//对微信支付应答报文进行验证
//sign_param:微信支付应答报文数据
//certificate:微信支付平台证书中,使用微信支付平台证书中的公钥验签
func ResponseValidate(sign_param *WxSignParam, certificate *x509.Certificate) error {
//构造签名串
message := sign_param.BuildResponseMessage()
signature, err := base64.StdEncoding.DecodeString(sign_param.Signature)
if err != nil {
return fmt.Errorf("base64 decode err:%s", err.Error())
}
hashed := sha256.Sum256([]byte(message))
err = rsa.VerifyPKCS1v15(certificate.PublicKey.(*rsa.PublicKey), crypto.SHA256, hashed[:], []byte(signature))
if err != nil {
return fmt.Errorf("verifty signature err:%s", err.Error())
}
return nil
}
WxSignParam是应答签名的相关数据,WxSignParam的定义如下:
type WxSignParam struct {
Timestamp string //微信支付回包时间戳
Nonce string //微信支付回包随机字符串
Signature string //微信支付回包签名信息
CertSerial string //微信支付回包平台序列号
RequestId string //微信支付回包请求ID
Body string
}
WxSignParam的数据通过函数:InitFromResponse来获取,InitFromResponse从HTTP的应答头获取校验签名所需的参数,包括:答时间戳、应答随机串、应答签名以及平台证书序列号:
func (ent *WxSignParam)InitFromResponse(response *http.Response, body string) error {
ent.RequestId = strings.TrimSpace(response.Header.Get("Request-Id"))
ent.CertSerial = response.Header.Get("Wechatpay-Serial")
ent.Signature = response.Header.Get("Wechatpay-Signature")
ent.Timestamp = response.Header.Get("Wechatpay-Timestamp")
ent.Nonce = response.Header.Get("Wechatpay-Nonce")
ent.Body = body
return nil
}
通过WxSignParam数据构造签名串的代码为:
unc (ent *WxSignParam)BuildResponseMessage() string {
message := fmt.Sprintf("%s\n%s\n%s\n",
ent.Timestamp, ent.Nonce, ent.Body)
return message
}
有了签名验证函数,我们就可以对支付请求的应答进行签名验证了,以下代码是增加了签名验证逻辑的支付接口调用函数:
func WxPayPostV3(ctx *MchParam, url string, data []byte) (string, error) {
token, err := GenerateWxPayReqHeader(ctx, http.MethodPost, url, string(data))
if err != nil {
log.Println(err)
return "", err
}
request, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
if err != nil {
return "", err
}
request.Header.Add("Authorization", token)
request.Header.Add("User-Agent", "go pay sdk")
request.Header.Add("Content-type", "application/json;charset='utf-8'")
request.Header.Add("Accept", "application/json")
client := &http.Client{
Timeout: 5 * time.Second}
resp, err := client.Do(request)
if err != nil {
log.Println(err)
return "", err
}
defer resp.Body.Close()
result, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != 200 && resp.StatusCode != 204 {
err := fmt.Errorf("status:%d;msg=%s", resp.StatusCode, string(result))
log.Println(err)
return string(result), err
}
var sign_param WxSignParam
err = sign_param.InitFromResponse(resp, string(result))
if err != nil {
log.Println(err)
return string(result), err
}
//Validate WechatPay Signature
plat_certificate := GetPlatCertificate(ctx, sign_param.CertSerial)
err = ResponseValidate(&sign_param, plat_certificate);
if err != nil {
log.Println(err)
return string(result), err
}
log.Println(string(result))
return string(result), nil
}
11.5报文解密
为了保证安全性,微信支付在回调通知和平台证书下载接口中,对关键信息进行了AES-256-GCM加密。AES-GCM是一种NIST标准的认证加密算法,是一种能够同时保证数据的保密性、完整性和真实性的一种加密模式。它最广泛的应用是在TLS中。证书和回调报文使用的加密密钥为是一种NIST标准的APIv3密钥。商户需先在【商户平台】->【API安全】的页面设置APIv3密钥,密钥的长度为32个字节。下面是报文解密的函数实现:
func DecryptAES256GCM(aesKey, associatedData, nonce, ciphertext string) (string, error) {
decodedCiphertext, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return "", err
}
c, err := aes.NewCipher([]byte(aesKey))
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(c)
if err != nil {
return "", err
}
dataBytes, err := gcm.Open(nil, []byte(nonce), decodedCiphertext, []byte(associatedData))
if err != nil {
return "", err
}
return string(dataBytes), nil
}