Login expired. Please log in again.

Feedback

0/300

Feedback

Submitted successfully

ok

Feedback

Network exception, please try again later

ok

签名验证

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

如果验证商户的请求签名正确,微信支付会在应答的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换行符。

应答时间戳\n
应答随机串\n
应答报文主体\n

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

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":"..."}}]}

则验签名串为

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":"..."}}]}

4. 获取应答签名

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

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

具体组成为:

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

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

5. 验证签名

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

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

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

$ 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-----
说明
查Java支持使用证书初始化签名对象,详见 initVerify(Certificate),并不需要先导出公钥。

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

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

最后,验证签名

$ 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) : "";
    }
}
<?php
require_once('vendor/autoload.php');

use WeChatPay\Crypto\Rsa;
use WeChatPay\Crypto\AesGcm;
use WeChatPay\Formatter;

$inWechatpaySignature = '';// Get this value from the header in response
$inWechatpayTimestamp = '';// Get this value from the header in response
$inWechatpaySerial = '';// Get this value from the header in response
$inWechatpayNonce = '';// Get this value from the header in response
$inBody = '';// Get this value from the body in response
$apiv3Key = '';
$platformPublicKeyInstance = Rsa::from('file:///path/to/wechatpay/inWechatpaySerial.pem', Rsa::KEY_TYPE_PUBLIC);

$timeOffsetStatus = 300 >= abs(Formatter::timestamp() - (int)$inWechatpayTimestamp);
$verifiedStatus = Rsa::verify(
    Formatter::joinedByLineFeed($inWechatpayTimestamp, $inWechatpayNonce, $inBody),
    $inWechatpaySignature,
    $platformPublicKeyInstance
);
package validators

import (
	"bytes"
	"context"
	"crypto"
	"crypto/rsa"
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"
	"fmt"
	"github.com/wechatpay-apiv3/wechatpay-go/core"
	"github.com/wechatpay-apiv3/wechatpay-go/core/consts"
	"io/ioutil"
	"math"
	"net/http"
	"strconv"
	"strings"
	"time"
)

type SHA256WithRSAVerifier struct {
	certGetter core.CertificateGetter
}

type wechatPayHeader struct {
	RequestID string
	Serial    string
	Signature string
	Nonce     string
	Timestamp int64
}

var verifyCert *x509.Certificate // the platform certificate need to download from Certificate API and store locally.

func ValidateR(ctx context.Context, response *http.Response) error {
	body, err := ioutil.ReadAll(response.Body)
	if err != nil {
		return fmt.Errorf("read response body err:[%s]", err.Error())
	}
	response.Body = ioutil.NopCloser(bytes.NewBuffer(body))

	return validateHTTPMessage(ctx, response.Header, body)
}

func validateHTTPMessage(ctx context.Context, header http.Header, body []byte) error {

	headerArgs, err := getWechatPayHeader(ctx, header)
	if err != nil {
		return err
	}

	if err := checkWechatPayHeader(ctx, headerArgs); err != nil {
		return err
	}

	message := buildMessage(ctx, headerArgs, body)

	if err := verify(ctx, headerArgs.Serial, message, headerArgs.Signature); err != nil {
		return fmt.Errorf(
			"validate verify fail serial=[%s] request-id=[%s] err=%w",
			headerArgs.Serial, headerArgs.RequestID, err,
		)
	}
	return nil
}

func verify(ctx context.Context, serialNumber, message, signature string) error {
	err := checkParameter(ctx, serialNumber, message, signature)
	if err != nil {
		return err
	}
	sigBytes, err := base64.StdEncoding.DecodeString(signature)
	if err != nil {
		return fmt.Errorf("verify failed: signature not base64 encoded")
	}
	hashed := sha256.Sum256([]byte(message))
	err = rsa.VerifyPKCS1v15(verifyCert.PublicKey.(*rsa.PublicKey), crypto.SHA256, hashed[:], sigBytes)
	if err != nil {
		return fmt.Errorf("verifty signature with public key err:%s", err.Error())
	}
	return nil
}

func getWechatPayHeader(ctx context.Context, header http.Header) (wechatPayHeader, error) {
	_ = ctx // Suppressing warnings

	requestID := strings.TrimSpace(header.Get(consts.RequestID))

	getHeaderString := func(key string) (string, error) {
		val := strings.TrimSpace(header.Get(key))
		if val == "" {
			return "", fmt.Errorf("key `%s` is empty in header, request-id=[%s]", key, requestID)
		}
		return val, nil
	}

	getHeaderInt64 := func(key string) (int64, error) {
		val, err := getHeaderString(key)
		if err != nil {
			return 0, nil
		}
		ret, err := strconv.ParseInt(val, 10, 64)
		if err != nil {
			return 0, fmt.Errorf("invalid `%s` in header, request-id=[%s], err:%w", key, requestID, err)
		}
		return ret, nil
	}

	ret := wechatPayHeader{
		RequestID: requestID,
	}
	var err error

	if ret.Serial, err = getHeaderString(consts.WechatPaySerial); err != nil {
		return ret, err
	}

	if ret.Signature, err = getHeaderString(consts.WechatPaySignature); err != nil {
		return ret, err
	}

	if ret.Timestamp, err = getHeaderInt64(consts.WechatPayTimestamp); err != nil {
		return ret, err
	}

	if ret.Nonce, err = getHeaderString(consts.WechatPayNonce); err != nil {
		return ret, err
	}

	return ret, nil
}

func checkWechatPayHeader(ctx context.Context, args wechatPayHeader) error {
	// Suppressing warnings
	_ = ctx

	if math.Abs(float64(time.Now().Unix()-args.Timestamp)) >= consts.FiveMinute {
		return fmt.Errorf("timestamp=[%d] expires, request-id=[%s]", args.Timestamp, args.RequestID)
	}
	return nil
}

func buildMessage(ctx context.Context, headerArgs wechatPayHeader, body []byte) string {
	// Suppressing warnings
	_ = ctx

	return fmt.Sprintf("%d\n%s\n%s\n", headerArgs.Timestamp, headerArgs.Nonce, string(body))
}

func checkParameter(ctx context.Context, serialNumber, message, signature string) error {
	if ctx == nil {
		return fmt.Errorf("context is nil, verifier need input context.Context")
	}
	if strings.TrimSpace(serialNumber) == "" {
		return fmt.Errorf("serialNumber is empty, verifier need input serialNumber")
	}
	if strings.TrimSpace(message) == "" {
		return fmt.Errorf("message is empty, verifier need input message")
	}
	if strings.TrimSpace(signature) == "" {
		return fmt.Errorf("signature is empty, verifier need input signature")
	}
	return nil
}
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import base64

def decrypt(nonce, ciphertext, associated_data):
    key = "Your32Apiv3Key"

    key_bytes = str.encode(key)
    nonce_bytes = str.encode(nonce)
    ad_bytes = str.encode(associated_data)
    data = base64.b64decode(ciphertext)

    aesgcm = AESGCM(key_bytes)
    return aesgcm.decrypt(nonce_bytes, data, ad_bytes)
    页面导航

About  WeChat  Pay

Powered By Tencent & Tenpay Copyright©

2005-2024 Tenpay All Rights Reserved.

Contact Us
Wechat Pay Global

WeChat Pay Global

置顶