签名验证

更新时间:2025.01.02

商户可以按照下述步骤验证应答或者回调的签名。


 

如果验证商户的请求签名正确,微信支付会在应答的HTTP头部中包括应答签名。我们建议商户验证应答签名。
同样的,微信支付会在回调的HTTP头部中包括回调报文的签名。商户必须 验证回调的签名,以确保回调是由微信支付发送。

1. 获取平台证书

微信支付API V3使用微信支付 的平台私钥(不是商户私钥 )进行应答签名。相应的,商户的技术人员应使用微信支付平台证书中的公钥验签。目前平台证书只提供API进行下载,请参考 获取平台证书列表

注意

再次提醒,应答和回调的签名验证使用的是 微信支付平台证书,不是商户API证书。使用商户API证书是验证不过的。

2. 检查平台证书序列号

微信支付的平台证书序列号位于HTTP头Wechatpay-Serial。验证签名前,请商户先检查序列号是否跟商户当前所持有的 微信支付平台证书的序列号一致。如果不一致,请重新获取证书。否则,签名的私钥和证书不匹配,将无法成功验证签名。

3. 构造验签名串

首先,商户先从应答中获取以下信息。

  • HTTP头Wechatpay-Timestamp 中的应答时间戳。

  • HTTP头Wechatpay-Nonce 中的应答随机串。

  • 应答主体(response Body),需要按照接口返回的顺序进行验签,错误的顺序将导致验签失败。

然后,请按照以下规则构造应答的验签名串。签名串共有三行,行尾以\n 结束,包括最后一行。\n为换行符(ASCII编码值为0x0A)。若应答报文主体为空(如HTTP状态码为204 No Content),最后一行仅为一个\n换行符。

1应答时间戳\n
2应答随机串\n
3应答报文主体\n
4

如某个应答的HTTP报文为(省略了ciphertext的具体内容):

1HTTP/1.1 200 OK
2Server: nginx
3Date: Tue, 02 Apr 2019 12:59:40 GMT
4Content-Type: application/json; charset=utf-8
5Content-Length: 2204
6Connection: keep-alive
7Keep-Alive: timeout=8
8Content-Language: zh-CN
9Request-ID: e2762b10-b6b9-5108-a42c-16fe2422fc8a
10Wechatpay-Nonce: c5ac7061fccab6bf3e254dcf98995b8c
11Wechatpay-Signature: CtcbzwtQjN8rnOXItEBJ5aQFSnIXESeV28Pr2YEmf9wsDQ8Nx25ytW6FXBCAFdrr0mgqngX3AD9gNzjnNHzSGTPBSsaEkIfhPF4b8YRRTpny88tNLyprXA0GU5ID3DkZHpjFkX1hAp/D0fva2GKjGRLtvYbtUk/OLYqFuzbjt3yOBzJSKQqJsvbXILffgAmX4pKql+Ln+6UPvSCeKwznvtPaEx+9nMBmKu7Wpbqm/+2ksc0XwjD+xlvlECkCxfD/OJ4gN3IurE0fpjxIkvHDiinQmk51BI7zQD8k1znU7r/spPqB+vZjc5ep6DC5wZUpFu5vJ8MoNKjCu8wnzyCFdA==
12Wechatpay-Timestamp: 1554209980
13Wechatpay-Serial: 5157F09EFDC096DE15EBE81A47057A7232F1B8E1
14Cache-Control: no-cache, must-revalidate
15
16{"data":[{"serial_no":"5157F09EFDC096DE15EBE81A47057A7232F1B8E1","effective_time":"2018-03-26T11:39:50+08:00","expire_time":"2023-03-25T11:39:50+08:00","encrypt_certificate":{"algorithm":"AEAD_AES_256_GCM","nonce":"4de73afd28b6","associated_data":"certificate","ciphertext":"..."}}]}
17

则验签名串为

11554209980
2c5ac7061fccab6bf3e254dcf98995b8c
3{"data":[{"serial_no":"5157F09EFDC096DE15EBE81A47057A7232F1B8E1","effective_time":"2018-03-26T11:39:50+08:00","expire_time":"2023-03-25T11:39:50+08:00","encrypt_certificate":{"algorithm":"AEAD_AES_256_GCM","nonce":"4de73afd28b6","associated_data":"certificate","ciphertext":"..."}}]}
4

4. 获取应答签名

微信支付的应答签名通过HTTP头Wechatpay-Signature传递。(注意,示例因为排版可能存在换行,实际数据应在一行)

1Wechatpay-Signature: CtcbzwtQjN8rnOXItEBJ5aQFSnIXESeV28Pr2YEmf9wsDQ8Nx25ytW6FXBCAFdrr0mgqngX3AD9gNzjnNHzSGTPBSsaEkIfhPF4b8YRRTpny88tNLyprXA0GU5ID3DkZHpjFkX1hAp/D0fva2GKjGRLtvYbtUk/OLYqFuzbjt3yOBzJSKQqJsvbXILffgAmX4pKql+Ln+6UPvSCeKwznvtPaEx+9nMBmKu7Wpbqm/+2ksc0XwjD+xlvlECkCxfD/OJ4gN3IurE0fpjxIkvHDiinQmk51BI7zQD8k1znU7r/spPqB+vZjc5ep6DC5wZUpFu5vJ8MoNKjCu8wnzyCFdA==
2

具体组成为:

Wechatpay-Signature的字段值使用Base64进行解码,得到应答签名。

注意

某些代理服务器或CDN服务提供商,转发时会“过滤”微信支付扩展的HTTP头,导致应用层无法取到微信支付的签名信息。商户遇到这种情况时,我们建议尝试调整代理服务器配置,或者通过直连的方式访问微信支付的服务器和接收通知。

5. 验证签名

很多编程语言的签名验证函数支持对验签名串和签名 进行签名验证。强烈建议商户调用该类函数,使用微信支付平台公钥对验签名串和签名进行SHA256 with RSA签名验证。

下面展示使用命令行演示如何进行验签。假设我们已经获取了平台证书并保存为1900009191_wxp_cert.pem

首先,从微信支付平台证书导出微信支付平台公钥

1$ openssl x509 -in 1900009191_wxp_cert.pem -pubkey -noout > 1900009191_wxp_pub.pem
2$ cat 1900009191_wxp_pub.pem
3-----BEGIN PUBLIC KEY-----
4MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4zej1cqugGQtVSY2Ah8R
5MCKcr2UpZ8Npo+5Ja9xpFPYkWHaF1Gjrn3d5kcwAFuHHcfdc3yxDYx6+9grvJnCA
62zQzWjzVRa3BJ5LTMj6yqvhEmtvjO9D1xbFTA2m3kyjxlaIar/RYHZSslT4VmjIa
7tW9KJCDKkwpM6x/RIWL8wwfFwgz2q3Zcrff1y72nB8p8P12ndH7GSLoY6d2Tv0OB
82+We2Kyy2+QzfGXOmLp7UK/pFQjJjzhSf9jxaWJXYKIBxpGlddbRZj9PqvFPTiep
98rvfKGNZF9Q6QaMYTpTp/uKQ3YvpDlyeQlYe4rRFauH3mOE6j56QlYQWivknDX9V
10rwIDAQAB
11-----END PUBLIC KEY-----
12

说明

查Java支持使用证书初始化签名对象,详见 initVerify(Certificate),并不需要先导出公钥。

然后,把签名base64解码后保存为文件signature.txt

1$ openssl base64 -d -A <<< \ 'CtcbzwtQjN8rnOXItEBJ5aQFSnIXESeV28Pr2YEmf9wsDQ8Nx25ytW6FXBCAFdrr0mgqngX3AD9gNzjnNHzSGTPBSsaEkIfhPF4b8YRRTpny88tNLyprXA0GU5ID3DkZHpjFkX1hAp/D0fva2GKjGRLtvYbtUk/OLYqFuzbjt3yOBzJSKQqJsvbXILffgAmX4pKql+Ln+6UPvSCeKwznvtPaEx+9nMBmKu7Wpbqm/+2ksc0XwjD+xlvlECkCxfD/OJ4gN3IurE0fpjxIkvHDiinQmk51BI7zQD8k1znU7r/spPqB+vZjc5ep6DC5wZUpFu5vJ8MoNKjCu8wnzyCFdA==' > signature.txt
2

最后,验证签名

1$ openssl dgst -sha256 -verify 1900009191_wxp_pub.pem -signature signature.txt << EOF
21554209980
3c5ac7061fccab6bf3e254dcf98995b8c
4{"data":[{"serial_no":"5157F09EFDC096DE15EBE81A47057A7232F1B8E1","effective_time":"2018-03-26T11:39:50+08:00","expire_time":"2023-03-25T11:39:50+08:00","encrypt_certificate":{"algorithm":"AEAD_AES_256_GCM","nonce":"d215b0511e9c","associated_data":"certificate","ciphertext":"..."}}]}
5EOF
6Verified OK
7

JAVA

1package com.wechat.v3;
2
3import org.apache.http.Header;
4import org.apache.http.HttpEntity;
5import org.apache.http.client.methods.CloseableHttpResponse;
6import org.apache.http.util.EntityUtils;
7import org.junit.Test;
8
9import javax.security.auth.login.CredentialException;
10import java.io.ByteArrayInputStream;
11import java.io.IOException;
12import java.io.InputStream;
13import java.nio.charset.StandardCharsets;
14import java.security.InvalidKeyException;
15import java.security.NoSuchAlgorithmException;
16import java.security.Signature;
17import java.security.SignatureException;
18import java.security.cert.*;
19import java.time.DateTimeException;
20import java.time.Duration;
21import java.time.Instant;
22import java.util.Base64;
23
24import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;
25
26public class Verifier {
27
28    private static int RESPONSE_EXPIRED_MINUTES = 5;
29    private static final String certificate = "-----BEGIN CERTIFICATE-----" +
30            "-----END CERTIFICATE-----";
31
32    protected static IllegalArgumentException parameterError(String message, Object... args) {
33        message = String.format(message, args);
34        return new IllegalArgumentException("parameter error: " + message);
35    }
36
37    //
38    public final boolean validate(CloseableHttpResponse response) throws IOException {
39        validateParameters(response);
40        String message = buildMessage(response);
41        String serial = response.getFirstHeader(WECHAT_PAY_SERIAL).getValue(); //Should be used to find which cert should be used to verify
42        String signature = response.getFirstHeader(WECHAT_PAY_SIGNATURE).getValue();
43        return verify(loadCertificate(certificate), message.getBytes(StandardCharsets.UTF_8), signature);
44    }
45
46    public X509Certificate loadCertificate(String certificate) {
47        InputStream certStream = new ByteArrayInputStream(certificate.getBytes());
48        X509Certificate cert = null;
49        try{
50            CertificateFactory cf = CertificateFactory.getInstance("X509");
51            cert = (X509Certificate) cf.generateCertificate(certStream);
52            cert.checkValidity();
53        } catch (CertificateException e) {
54            throw new RuntimeException("Fail to load and vailid the certificate", e);
55        }
56        return cert;
57    }
58
59    protected boolean verify(X509Certificate certificate, byte[] message, String signature) {
60        try {
61            Signature sign = Signature.getInstance("SHA256withRSA");
62            sign.initVerify(certificate);
63            sign.update(message);
64            return sign.verify(Base64.getDecoder().decode(signature));
65
66        } catch (NoSuchAlgorithmException e) {
67            throw new RuntimeException("当前Java环境不支持SHA256withRSA", e);
68        } catch (SignatureException e) {
69            throw new RuntimeException("签名验证过程发生了错误", e);
70        } catch (InvalidKeyException e) {
71            throw new RuntimeException("无效的证书", e);
72        }
73    }
74
75    protected final void validateParameters(CloseableHttpResponse response) {
76        Header firstHeader = response.getFirstHeader(REQUEST_ID);
77        if (firstHeader == null) {
78            throw parameterError("empty " + REQUEST_ID);
79        }
80        String requestId = firstHeader.getValue();
81
82        // NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
83        String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
84
85        Header header = null;
86        for (String headerName : headers) {
87            header = response.getFirstHeader(headerName);
88            if (header == null) {
89                throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
90            }
91        }
92
93        String timestampStr = header.getValue();
94        try {
95            Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
96            // 拒绝过期应答
97            if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {
98                throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
99            }
100        } catch (DateTimeException | NumberFormatException e) {
101            throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
102        }
103    }
104
105    protected final String buildMessage(CloseableHttpResponse response) throws IOException {
106        String timestamp = response.getFirstHeader(WECHAT_PAY_TIMESTAMP).getValue();
107        String nonce = response.getFirstHeader(WECHAT_PAY_NONCE).getValue();
108        String body = getResponseBody(response);
109        return timestamp + "\n"
110                + nonce + "\n"
111                + body + "\n";
112    }
113
114    protected final String getResponseBody(CloseableHttpResponse response) throws IOException {
115        HttpEntity entity = response.getEntity();
116        return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";
117    }
118}

PHP

1<?php
2require_once('vendor/autoload.php');
3
4use WeChatPay\Crypto\Rsa;
5use WeChatPay\Crypto\AesGcm;
6use WeChatPay\Formatter;
7
8$inWechatpaySignature = '';// Get this value from the header in response
9$inWechatpayTimestamp = '';// Get this value from the header in response
10$inWechatpaySerial = '';// Get this value from the header in response
11$inWechatpayNonce = '';// Get this value from the header in response
12$inBody = '';// Get this value from the body in response
13$apiv3Key = '';
14$platformPublicKeyInstance = Rsa::from('file:///path/to/wechatpay/inWechatpaySerial.pem', Rsa::KEY_TYPE_PUBLIC);
15
16$timeOffsetStatus = 300 >= abs(Formatter::timestamp() - (int)$inWechatpayTimestamp);
17$verifiedStatus = Rsa::verify(
18    Formatter::joinedByLineFeed($inWechatpayTimestamp, $inWechatpayNonce, $inBody),
19    $inWechatpaySignature,
20    $platformPublicKeyInstance
21);

GO

1package validators
2
3import (
4	"bytes"
5	"context"
6	"crypto"
7	"crypto/rsa"
8	"crypto/sha256"
9	"crypto/x509"
10	"encoding/base64"
11	"fmt"
12	"github.com/wechatpay-apiv3/wechatpay-go/core"
13	"github.com/wechatpay-apiv3/wechatpay-go/core/consts"
14	"io/ioutil"
15	"math"
16	"net/http"
17	"strconv"
18	"strings"
19	"time"
20)
21
22type SHA256WithRSAVerifier struct {
23	certGetter core.CertificateGetter
24}
25
26type wechatPayHeader struct {
27	RequestID string
28	Serial    string
29	Signature string
30	Nonce     string
31	Timestamp int64
32}
33
34var verifyCert *x509.Certificate // the platform certificate need to download from Certificate API and store locally.
35
36func ValidateR(ctx context.Context, response *http.Response) error {
37	body, err := ioutil.ReadAll(response.Body)
38	if err != nil {
39		return fmt.Errorf("read response body err:[%s]", err.Error())
40	}
41	response.Body = ioutil.NopCloser(bytes.NewBuffer(body))
42
43	return validateHTTPMessage(ctx, response.Header, body)
44}
45
46func validateHTTPMessage(ctx context.Context, header http.Header, body []byte) error {
47
48	headerArgs, err := getWechatPayHeader(ctx, header)
49	if err != nil {
50		return err
51	}
52
53	if err := checkWechatPayHeader(ctx, headerArgs); err != nil {
54		return err
55	}
56
57	message := buildMessage(ctx, headerArgs, body)
58
59	if err := verify(ctx, headerArgs.Serial, message, headerArgs.Signature); err != nil {
60		return fmt.Errorf(
61			"validate verify fail serial=[%s] request-id=[%s] err=%w",
62			headerArgs.Serial, headerArgs.RequestID, err,
63		)
64	}
65	return nil
66}
67
68func verify(ctx context.Context, serialNumber, message, signature string) error {
69	err := checkParameter(ctx, serialNumber, message, signature)
70	if err != nil {
71		return err
72	}
73	sigBytes, err := base64.StdEncoding.DecodeString(signature)
74	if err != nil {
75		return fmt.Errorf("verify failed: signature not base64 encoded")
76	}
77	hashed := sha256.Sum256([]byte(message))
78	err = rsa.VerifyPKCS1v15(verifyCert.PublicKey.(*rsa.PublicKey), crypto.SHA256, hashed[:], sigBytes)
79	if err != nil {
80		return fmt.Errorf("verifty signature with public key err:%s", err.Error())
81	}
82	return nil
83}
84
85func getWechatPayHeader(ctx context.Context, header http.Header) (wechatPayHeader, error) {
86	_ = ctx // Suppressing warnings
87
88	requestID := strings.TrimSpace(header.Get(consts.RequestID))
89
90	getHeaderString := func(key string) (string, error) {
91		val := strings.TrimSpace(header.Get(key))
92		if val == "" {
93			return "", fmt.Errorf("key `%s` is empty in header, request-id=[%s]", key, requestID)
94		}
95		return val, nil
96	}
97
98	getHeaderInt64 := func(key string) (int64, error) {
99		val, err := getHeaderString(key)
100		if err != nil {
101			return 0, nil
102		}
103		ret, err := strconv.ParseInt(val, 10, 64)
104		if err != nil {
105			return 0, fmt.Errorf("invalid `%s` in header, request-id=[%s], err:%w", key, requestID, err)
106		}
107		return ret, nil
108	}
109
110	ret := wechatPayHeader{
111		RequestID: requestID,
112	}
113	var err error
114
115	if ret.Serial, err = getHeaderString(consts.WechatPaySerial); err != nil {
116		return ret, err
117	}
118
119	if ret.Signature, err = getHeaderString(consts.WechatPaySignature); err != nil {
120		return ret, err
121	}
122
123	if ret.Timestamp, err = getHeaderInt64(consts.WechatPayTimestamp); err != nil {
124		return ret, err
125	}
126
127	if ret.Nonce, err = getHeaderString(consts.WechatPayNonce); err != nil {
128		return ret, err
129	}
130
131	return ret, nil
132}
133
134func checkWechatPayHeader(ctx context.Context, args wechatPayHeader) error {
135	// Suppressing warnings
136	_ = ctx
137
138	if math.Abs(float64(time.Now().Unix()-args.Timestamp)) >= consts.FiveMinute {
139		return fmt.Errorf("timestamp=[%d] expires, request-id=[%s]", args.Timestamp, args.RequestID)
140	}
141	return nil
142}
143
144func buildMessage(ctx context.Context, headerArgs wechatPayHeader, body []byte) string {
145	// Suppressing warnings
146	_ = ctx
147
148	return fmt.Sprintf("%d\n%s\n%s\n", headerArgs.Timestamp, headerArgs.Nonce, string(body))
149}
150
151func checkParameter(ctx context.Context, serialNumber, message, signature string) error {
152	if ctx == nil {
153		return fmt.Errorf("context is nil, verifier need input context.Context")
154	}
155	if strings.TrimSpace(serialNumber) == "" {
156		return fmt.Errorf("serialNumber is empty, verifier need input serialNumber")
157	}
158	if strings.TrimSpace(message) == "" {
159		return fmt.Errorf("message is empty, verifier need input message")
160	}
161	if strings.TrimSpace(signature) == "" {
162		return fmt.Errorf("signature is empty, verifier need input signature")
163	}
164	return nil
165}

 

About  WeChat  Pay

Powered By Tencent & Tenpay Copyright©

2005-2025 Tenpay All Rights Reserved.

Contact Us
Wechat Pay Global

WeChat Pay Global

Contact Us

Customer Service Tel

+86 571 95017

9:00-18:00 Monday-Friday GMT+8

Business Development

wxpayglobal@tencent.com

Developer Support

wepayTS@tencent.com

Wechat Pay Global

About Tenpay
Powered By Tencent & Tenpay Copyright© 2005-2025 Tenpay All Rights Reserved.