Signature Verification

Update Time:2024.09.18

Merchants can follow the steps below to verify the signature of the response or callback.


If the merchant's request signature is correctly verified, WeChat Pay will include the response signature in the response HTTP header. We recommend that merchants verify the response signature.
Similarly, WeChat Pay will include the signature of the callback message in the HTTP header of the callback. Merchants must verify the signature of the callback to ensure that the callback is sent by WeChat Pay.

1. Obtaining A Platform Certificate

WeChat Pay API V3 uses the platform private key of WeChat Pay (instead of the merchant private key) to sign the response. Accordingly, the technical personnel of the merchant should use the public key in the WeChat Pay platform certificate to verify the signature. Currently, the platform certificate can only be downloaded through API. Please refer to Obtaining the Platform Certificate List.

Notice

We would like to remind you again that the signature verification of the response and the callback uses the WeChat Pay Platform Certificate instead of the merchant API certificate. If the merchant API certificate is used, the signature will not be verified.

2. Check Platform Certificate Serial Number

The platform certificate serial number of WeChat Pay is located in the HTTP header Wechatpay-Serial. Before verifying the signature, check whether the serial number matches the serial number of the WeChat Pay Platform Certificate currently held by the merchant. If not, obtain the certificate again. Otherwise, the private key of the signature does not match the certificate, and the signature cannot be verified.

3. Constructing A Signature String

First, the merchant needs to obtain the following information from the response.

  • Response timestamp in the HTTP header Wechatpay-Timestamp

  • Response random string in the HTTP header Wechatpay-Nonce

  • The response body should verify the signature in the order of API returned. If an order is wrong, the signature will not be verified.

Then, construct the response signature string according to the following rules. The signature string comprises three lines, each of which should end with \n. \n is a line break character (ASCII code value is 0x0A). If the body of the response message is empty (for example, the HTTP status code is 204 No Content), the last line will only be a \n line break character.

1Response timestamp\n
2Response random string\n
3Response message body\n
4

If the HTTP message of a response is (the specific content of ciphertext is omitted):

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

The signature verification string should be:

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. Obtaining the Response Signature

The response signature of WeChat Pay is passed through the HTTP header Wechatpay-Signature. (note that the example may have line breaks due to formatting. The actual data should be on a single line.)

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

Use Base64 to decode the field value of Wechatpay-Signature to obtain the response signature.

Notice

Some proxy servers or CDN service providers will "filter" the HTTP headers of WeChat Pay extensions when forwarding, causing the application layer to fail to obtain the signature information of WeChat Pay. In that case, we recommend adjusting the proxy server configuration, or accessing the WeChat Pay server and receiving notifications through direct connection.

5. Verifying Signature

The signature verification functions of many programming languages support signature verification on the verification signature string and signature. We strongly recommend that merchants call this type of function and use the WeChat Pay platform public key to verify the signature for verification signature string and signature using SHA256 with RSA.

The following shows how to perform verification using command lines. Suppose we have obtained the platform certificate and saved it as 1900009191_wxp_cert.pem

First, export the WeChat Pay platform public key from the WeChat Pay platform certificate.

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

Notice

Java supports using certificates to initialize the signature object. Refer to initVerify(Certificate) for details. There's no need to export the public key first.

Then, decode the signature base64 and save it as signature.txt

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

Finally, verify the signature.

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.