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.
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.)
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.
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
1packagecom.wechat.v3;23importorg.apache.http.Header;4importorg.apache.http.HttpEntity;5importorg.apache.http.client.methods.CloseableHttpResponse;6importorg.apache.http.util.EntityUtils;7importorg.junit.Test;89importjavax.security.auth.login.CredentialException;10importjava.io.ByteArrayInputStream;11importjava.io.IOException;12importjava.io.InputStream;13importjava.nio.charset.StandardCharsets;14importjava.security.InvalidKeyException;15importjava.security.NoSuchAlgorithmException;16importjava.security.Signature;17importjava.security.SignatureException;18importjava.security.cert.*;19importjava.time.DateTimeException;20importjava.time.Duration;21importjava.time.Instant;22importjava.util.Base64;2324importstaticcom.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;2526publicclassVerifier{2728privatestaticintRESPONSE_EXPIRED_MINUTES=5;29privatestaticfinalStringcertificate="-----BEGIN CERTIFICATE-----"+30"-----END CERTIFICATE-----";3132protectedstaticIllegalArgumentExceptionparameterError(Stringmessage,Object...args){33message=String.format(message,args);34returnnewIllegalArgumentException("parameter error: "+message);35}3637//38publicfinalbooleanvalidate(CloseableHttpResponseresponse)throwsIOException{39validateParameters(response);40Stringmessage=buildMessage(response);41Stringserial=response.getFirstHeader(WECHAT_PAY_SERIAL).getValue();//Should be used to find which cert should be used to verify42Stringsignature=response.getFirstHeader(WECHAT_PAY_SIGNATURE).getValue();43returnverify(loadCertificate(certificate),message.getBytes(StandardCharsets.UTF_8),signature);44}4546publicX509CertificateloadCertificate(Stringcertificate){47InputStreamcertStream=newByteArrayInputStream(certificate.getBytes());48X509Certificatecert=null;49try{50CertificateFactorycf=CertificateFactory.getInstance("X509");51cert=(X509Certificate)cf.generateCertificate(certStream);52cert.checkValidity();53}catch(CertificateExceptione){54thrownewRuntimeException("Fail to load and vailid the certificate",e);55}56returncert;57}5859protectedbooleanverify(X509Certificatecertificate,byte[]message,Stringsignature){60try{61Signaturesign=Signature.getInstance("SHA256withRSA");62sign.initVerify(certificate);63sign.update(message);64returnsign.verify(Base64.getDecoder().decode(signature));6566}catch(NoSuchAlgorithmExceptione){67thrownewRuntimeException("当前Java环境不支持SHA256withRSA",e);68}catch(SignatureExceptione){69thrownewRuntimeException("签名验证过程发生了错误",e);70}catch(InvalidKeyExceptione){71thrownewRuntimeException("无效的证书",e);72}73}7475protectedfinalvoidvalidateParameters(CloseableHttpResponseresponse){76HeaderfirstHeader=response.getFirstHeader(REQUEST_ID);77if(firstHeader==null){78throwparameterError("empty "+REQUEST_ID);79}80StringrequestId=firstHeader.getValue();8182// NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last83String[]headers={WECHAT_PAY_SERIAL,WECHAT_PAY_SIGNATURE,WECHAT_PAY_NONCE,WECHAT_PAY_TIMESTAMP};8485Headerheader=null;86for(StringheaderName:headers){87header=response.getFirstHeader(headerName);88if(header==null){89throwparameterError("empty [%s], request-id=[%s]",headerName,requestId);90}91}9293StringtimestampStr=header.getValue();94try{95InstantresponseTime=Instant.ofEpochSecond(Long.parseLong(timestampStr));96// 拒绝过期应答97if(Duration.between(responseTime,Instant.now()).abs().toMinutes()>=RESPONSE_EXPIRED_MINUTES){98throwparameterError("timestamp=[%s] expires, request-id=[%s]",timestampStr,requestId);99}100}catch(DateTimeException|NumberFormatExceptione){101throwparameterError("invalid timestamp=[%s], request-id=[%s]",timestampStr,requestId);102}103}104105protectedfinalStringbuildMessage(CloseableHttpResponseresponse)throwsIOException{106Stringtimestamp=response.getFirstHeader(WECHAT_PAY_TIMESTAMP).getValue();107Stringnonce=response.getFirstHeader(WECHAT_PAY_NONCE).getValue();108Stringbody=getResponseBody(response);109returntimestamp+"\n"110+nonce+"\n"111+body+"\n";112}113114protectedfinalStringgetResponseBody(CloseableHttpResponseresponse)throwsIOException{115HttpEntityentity=response.getEntity();116return(entity!=null&&entity.isRepeatable())?EntityUtils.toString(entity):"";117}118}
PHP
1<?php2require_once('vendor/autoload.php');34useWeChatPay\Crypto\Rsa;5useWeChatPay\Crypto\AesGcm;6useWeChatPay\Formatter;78$inWechatpaySignature='';// Get this value from the header in response9$inWechatpayTimestamp='';// Get this value from the header in response10$inWechatpaySerial='';// Get this value from the header in response11$inWechatpayNonce='';// Get this value from the header in response12$inBody='';// Get this value from the body in response13$apiv3Key='';14$platformPublicKeyInstance=Rsa::from('file:///path/to/wechatpay/inWechatpaySerial.pem',Rsa::KEY_TYPE_PUBLIC);1516$timeOffsetStatus=300>=abs(Formatter::timestamp()-(int)$inWechatpayTimestamp);17$verifiedStatus=Rsa::verify(18Formatter::joinedByLineFeed($inWechatpayTimestamp,$inWechatpayNonce,$inBody),19$inWechatpaySignature,20$platformPublicKeyInstance21);
GO
1packagevalidators23import(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)2122typeSHA256WithRSAVerifierstruct{23certGettercore.CertificateGetter24}2526typewechatPayHeaderstruct{27RequestIDstring28Serialstring29Signaturestring30Noncestring31Timestampint6432}3334varverifyCert*x509.Certificate// the platform certificate need to download from Certificate API and store locally.3536funcValidateR(ctxcontext.Context,response*http.Response)error{37body,err:=ioutil.ReadAll(response.Body)38iferr!=nil{39returnfmt.Errorf("read response body err:[%s]",err.Error())40}41response.Body=ioutil.NopCloser(bytes.NewBuffer(body))4243returnvalidateHTTPMessage(ctx,response.Header,body)44}4546funcvalidateHTTPMessage(ctxcontext.Context,headerhttp.Header,body[]byte)error{4748headerArgs,err:=getWechatPayHeader(ctx,header)49iferr!=nil{50returnerr51}5253iferr:=checkWechatPayHeader(ctx,headerArgs);err!=nil{54returnerr55}5657message:=buildMessage(ctx,headerArgs,body)5859iferr:=verify(ctx,headerArgs.Serial,message,headerArgs.Signature);err!=nil{60returnfmt.Errorf(61"validate verify fail serial=[%s] request-id=[%s] err=%w",62headerArgs.Serial,headerArgs.RequestID,err,63)64}65returnnil66}6768funcverify(ctxcontext.Context,serialNumber,message,signaturestring)error{69err:=checkParameter(ctx,serialNumber,message,signature)70iferr!=nil{71returnerr72}73sigBytes,err:=base64.StdEncoding.DecodeString(signature)74iferr!=nil{75returnfmt.Errorf("verify failed: signature not base64 encoded")76}77hashed:=sha256.Sum256([]byte(message))78err=rsa.VerifyPKCS1v15(verifyCert.PublicKey.(*rsa.PublicKey),crypto.SHA256,hashed[:],sigBytes)79iferr!=nil{80returnfmt.Errorf("verifty signature with public key err:%s",err.Error())81}82returnnil83}8485funcgetWechatPayHeader(ctxcontext.Context,headerhttp.Header)(wechatPayHeader,error){86_=ctx// Suppressing warnings8788requestID:=strings.TrimSpace(header.Get(consts.RequestID))8990getHeaderString:=func(keystring)(string,error){91val:=strings.TrimSpace(header.Get(key))92ifval==""{93return"",fmt.Errorf("key `%s` is empty in header, request-id=[%s]",key,requestID)94}95returnval,nil96}9798getHeaderInt64:=func(keystring)(int64,error){99val,err:=getHeaderString(key)100iferr!=nil{101return0,nil102}103ret,err:=strconv.ParseInt(val,10,64)104iferr!=nil{105return0,fmt.Errorf("invalid `%s` in header, request-id=[%s], err:%w",key,requestID,err)106}107returnret,nil108}109110ret:=wechatPayHeader{111RequestID:requestID,112}113varerrerror114115ifret.Serial,err=getHeaderString(consts.WechatPaySerial);err!=nil{116returnret,err117}118119ifret.Signature,err=getHeaderString(consts.WechatPaySignature);err!=nil{120returnret,err121}122123ifret.Timestamp,err=getHeaderInt64(consts.WechatPayTimestamp);err!=nil{124returnret,err125}126127ifret.Nonce,err=getHeaderString(consts.WechatPayNonce);err!=nil{128returnret,err129}130131returnret,nil132}133134funccheckWechatPayHeader(ctxcontext.Context,argswechatPayHeader)error{135// Suppressing warnings136_=ctx137138ifmath.Abs(float64(time.Now().Unix()-args.Timestamp))>=consts.FiveMinute{139returnfmt.Errorf("timestamp=[%d] expires, request-id=[%s]",args.Timestamp,args.RequestID)140}141returnnil142}143144funcbuildMessage(ctxcontext.Context,headerArgswechatPayHeader,body[]byte)string{145// Suppressing warnings146_=ctx147148returnfmt.Sprintf("%d\n%s\n%s\n",headerArgs.Timestamp,headerArgs.Nonce,string(body))149}150151funccheckParameter(ctxcontext.Context,serialNumber,message,signaturestring)error{152ifctx==nil{153returnfmt.Errorf("context is nil, verifier need input context.Context")154}155ifstrings.TrimSpace(serialNumber)==""{156returnfmt.Errorf("serialNumber is empty, verifier need input serialNumber")157}158ifstrings.TrimSpace(message)==""{159returnfmt.Errorf("message is empty, verifier need input message")160}161ifstrings.TrimSpace(signature)==""{162returnfmt.Errorf("signature is empty, verifier need input signature")163}164returnnil165}