Signature Verification
Merchants can follow the steps below to verify the signature of the response or callback.
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.
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.
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.
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.
Response timestamp\n
Response random string\n
Response message body\n
If the HTTP message of a response is (the specific content of ciphertext is omitted):
HTTP/1.1 200 OK
Server: nginx
Date: Tue, 02 Apr 2019 12:59:40 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 2204
Connection: keep-alive
Keep-Alive: timeout=8
Content-Language: zh-CN
Request-ID: e2762b10-b6b9-5108-a42c-16fe2422fc8a
Wechatpay-Nonce: c5ac7061fccab6bf3e254dcf98995b8c
Wechatpay-Signature: CtcbzwtQjN8rnOXItEBJ5aQFSnIXESeV28Pr2YEmf9wsDQ8Nx25ytW6FXBCAFdrr0mgqngX3AD9gNzjnNHzSGTPBSsaEkIfhPF4b8YRRTpny88tNLyprXA0GU5ID3DkZHpjFkX1hAp/D0fva2GKjGRLtvYbtUk/OLYqFuzbjt3yOBzJSKQqJsvbXILffgAmX4pKql+Ln+6UPvSCeKwznvtPaEx+9nMBmKu7Wpbqm/+2ksc0XwjD+xlvlECkCxfD/OJ4gN3IurE0fpjxIkvHDiinQmk51BI7zQD8k1znU7r/spPqB+vZjc5ep6DC5wZUpFu5vJ8MoNKjCu8wnzyCFdA==
Wechatpay-Timestamp: 1554209980
Wechatpay-Serial: 5157F09EFDC096DE15EBE81A47057A7232F1B8E1
Cache-Control: no-cache, must-revalidate
{"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":"..."}}]}
The signature verification string should be:
1554209980
c5ac7061fccab6bf3e254dcf98995b8c
{"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":"..."}}]}
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.)
Wechatpay-Signature: CtcbzwtQjN8rnOXItEBJ5aQFSnIXESeV28Pr2YEmf9wsDQ8Nx25ytW6FXBCAFdrr0mgqngX3AD9gNzjnNHzSGTPBSsaEkIfhPF4b8YRRTpny88tNLyprXA0GU5ID3DkZHpjFkX1hAp/D0fva2GKjGRLtvYbtUk/OLYqFuzbjt3yOBzJSKQqJsvbXILffgAmX4pKql+Ln+6UPvSCeKwznvtPaEx+9nMBmKu7Wpbqm/+2ksc0XwjD+xlvlECkCxfD/OJ4gN3IurE0fpjxIkvHDiinQmk51BI7zQD8k1znU7r/spPqB+vZjc5ep6DC5wZUpFu5vJ8MoNKjCu8wnzyCFdA==
Use Base64 to decode the field value of Wechatpay-Signature
to obtain the response 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.
$ openssl x509 -in 1900009191_wxp_cert.pem -pubkey -noout > 1900009191_wxp_pub.pem
$ cat 1900009191_wxp_pub.pem
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4zej1cqugGQtVSY2Ah8R
MCKcr2UpZ8Npo+5Ja9xpFPYkWHaF1Gjrn3d5kcwAFuHHcfdc3yxDYx6+9grvJnCA
2zQzWjzVRa3BJ5LTMj6yqvhEmtvjO9D1xbFTA2m3kyjxlaIar/RYHZSslT4VmjIa
tW9KJCDKkwpM6x/RIWL8wwfFwgz2q3Zcrff1y72nB8p8P12ndH7GSLoY6d2Tv0OB
2+We2Kyy2+QzfGXOmLp7UK/pFQjJjzhSf9jxaWJXYKIBxpGlddbRZj9PqvFPTiep
8rvfKGNZF9Q6QaMYTpTp/uKQ3YvpDlyeQlYe4rRFauH3mOE6j56QlYQWivknDX9V
rwIDAQAB
-----END PUBLIC KEY-----
Then, decode the signature base64 and save it as signature.txt
$ openssl base64 -d -A <<< \ 'CtcbzwtQjN8rnOXItEBJ5aQFSnIXESeV28Pr2YEmf9wsDQ8Nx25ytW6FXBCAFdrr0mgqngX3AD9gNzjnNHzSGTPBSsaEkIfhPF4b8YRRTpny88tNLyprXA0GU5ID3DkZHpjFkX1hAp/D0fva2GKjGRLtvYbtUk/OLYqFuzbjt3yOBzJSKQqJsvbXILffgAmX4pKql+Ln+6UPvSCeKwznvtPaEx+9nMBmKu7Wpbqm/+2ksc0XwjD+xlvlECkCxfD/OJ4gN3IurE0fpjxIkvHDiinQmk51BI7zQD8k1znU7r/spPqB+vZjc5ep6DC5wZUpFu5vJ8MoNKjCu8wnzyCFdA==' > signature.txt
Finally, verify the signature.
$ openssl dgst -sha256 -verify 1900009191_wxp_pub.pem -signature signature.txt << EOF
1554209980
c5ac7061fccab6bf3e254dcf98995b8c
{"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":"..."}}]}
EOF
Verified OK
package com.wechat.v3;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.util.EntityUtils;
import org.junit.Test;
import javax.security.auth.login.CredentialException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.*;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import java.util.Base64;
import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;
public class Verifier {
private static int RESPONSE_EXPIRED_MINUTES = 5;
private static final String certificate = "-----BEGIN CERTIFICATE-----" +
"-----END CERTIFICATE-----";
protected static IllegalArgumentException parameterError(String message, Object... args) {
message = String.format(message, args);
return new IllegalArgumentException("parameter error: " + message);
}
//
public final boolean validate(CloseableHttpResponse response) throws IOException {
validateParameters(response);
String message = buildMessage(response);
String serial = response.getFirstHeader(WECHAT_PAY_SERIAL).getValue(); //Should be used to find which cert should be used to verify
String signature = response.getFirstHeader(WECHAT_PAY_SIGNATURE).getValue();
return verify(loadCertificate(certificate), message.getBytes(StandardCharsets.UTF_8), signature);
}
public X509Certificate loadCertificate(String certificate) {
InputStream certStream = new ByteArrayInputStream(certificate.getBytes());
X509Certificate cert = null;
try{
CertificateFactory cf = CertificateFactory.getInstance("X509");
cert = (X509Certificate) cf.generateCertificate(certStream);
cert.checkValidity();
} catch (CertificateException e) {
throw new RuntimeException("Fail to load and vailid the certificate", e);
}
return cert;
}
protected boolean verify(X509Certificate certificate, byte[] message, String signature) {
try {
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initVerify(certificate);
sign.update(message);
return sign.verify(Base64.getDecoder().decode(signature));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("当前Java环境不支持SHA256withRSA", e);
} catch (SignatureException e) {
throw new RuntimeException("签名验证过程发生了错误", e);
} catch (InvalidKeyException e) {
throw new RuntimeException("无效的证书", e);
}
}
protected final void validateParameters(CloseableHttpResponse response) {
Header firstHeader = response.getFirstHeader(REQUEST_ID);
if (firstHeader == null) {
throw parameterError("empty " + REQUEST_ID);
}
String requestId = firstHeader.getValue();
// NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last
String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
Header header = null;
for (String headerName : headers) {
header = response.getFirstHeader(headerName);
if (header == null) {
throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
}
}
String timestampStr = header.getValue();
try {
Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr));
// 拒绝过期应答
if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) {
throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId);
}
} catch (DateTimeException | NumberFormatException e) {
throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId);
}
}
protected final String buildMessage(CloseableHttpResponse response) throws IOException {
String timestamp = response.getFirstHeader(WECHAT_PAY_TIMESTAMP).getValue();
String nonce = response.getFirstHeader(WECHAT_PAY_NONCE).getValue();
String body = getResponseBody(response);
return timestamp + "\n"
+ nonce + "\n"
+ body + "\n";
}
protected final String getResponseBody(CloseableHttpResponse response) throws IOException {
HttpEntity entity = response.getEntity();
return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";
}
}
Customer Service Tel
Business Development
9:00-18:00
Monday-Friday GMT+8
Technical Support
WeChat Pay Global
ICP证