mirror of
https://github.com/wechatpay-apiv3/wechatpay-go.git
synced 2026-03-13 08:42:37 +08:00
支持公钥验签 (#226)
* feat: 支持使用微信支付公钥验签 * feat: 回调验签同时支持公钥和证书 * Update README.md
This commit is contained in:
72
README.md
72
README.md
@@ -1,6 +1,6 @@
|
||||
# 微信支付 API v3 Go SDK
|
||||
|
||||
[](https://pkg.go.dev/github.com/wechatpay-apiv3/wechatpay-go)
|
||||
[](https://huntr.dev)
|
||||
[](https://github.com/wechatpay-apiv3/wechatpay-go/blob/main/LICENSE)
|
||||
|
||||
[微信支付 APIv3](https://wechatpay-api.gitbook.io/wechatpay-api-v3/) 官方Go语言客户端代码库。
|
||||
@@ -28,10 +28,12 @@
|
||||
go mod init
|
||||
```
|
||||
|
||||
#### 2、无需 clone 仓库中的代码,直接在项目目录中执行:
|
||||
#### 2、无需 clone 仓库中的代码,直接在项目目录中执行
|
||||
|
||||
```shell
|
||||
go get -u github.com/wechatpay-apiv3/wechatpay-go
|
||||
```
|
||||
|
||||
来添加依赖,完成 `go.mod` 修改与 SDK 下载。
|
||||
|
||||
### 发送请求
|
||||
@@ -89,8 +91,11 @@ func main() {
|
||||
#### 名词解释
|
||||
|
||||
+ **商户 API 证书**,是用来证实商户身份的。证书中包含商户号、证书序列号、证书有效期等信息,由证书授权机构(Certificate Authority ,简称 CA)签发,以防证书被伪造或篡改。如何获取请见 [商户 API 证书](https://wechatpay-api.gitbook.io/wechatpay-api-v3/ren-zheng/zheng-shu#shang-hu-api-zheng-shu) 。
|
||||
|
||||
+ **商户 API 私钥**。商户申请商户 API 证书时,会生成商户私钥,并保存在本地证书文件夹的文件 apiclient_key.pem 中。
|
||||
|
||||
> :warning: 不要把私钥文件暴露在公共场合,如上传到 Github,写在客户端代码等。
|
||||
|
||||
+ **微信支付平台证书**。微信支付平台证书是指由微信支付负责申请的,包含微信支付平台标识、公钥信息的证书。商户使用微信支付平台证书中的公钥验证应答签名。获取微信支付平台证书需通过 [获取平台证书列表](https://wechatpay-api.gitbook.io/wechatpay-api-v3/ren-zheng/zheng-shu#ping-tai-zheng-shu) 接口下载。
|
||||
+ **证书序列号**。每个证书都有一个由 CA 颁发的唯一编号,即证书序列号。扩展阅读 [如何查看证书序列号](https://wechatpay-api.gitbook.io/wechatpay-api-v3/chang-jian-wen-ti/zheng-shu-xiang-guan#ru-he-cha-kan-zheng-shu-xu-lie-hao) 。
|
||||
+ **微信支付 APIv3 密钥**,是在回调通知和微信支付平台证书下载接口中,为加强数据安全,对关键信息 `AES-256-GCM` 加密时使用的对称加密密钥。
|
||||
@@ -156,7 +161,8 @@ if err == nil {
|
||||
|
||||
```
|
||||
|
||||
### 以 [图片上传API](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter2_1_1.shtml) 为例:
|
||||
### 以 [图片上传API](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter2_1_1.shtml) 为例
|
||||
|
||||
```go
|
||||
import (
|
||||
"os"
|
||||
@@ -199,10 +205,10 @@ result, err := client.Get(ctx, "https://api.mch.weixin.qq.com/v3/certificates")
|
||||
|
||||
以下情况,SDK 发送请求会返回 `error`:
|
||||
|
||||
- HTTP 网络错误,如应答接收超时或网络连接失败
|
||||
- 客户端失败,如生成签名失败
|
||||
- 服务器端返回了**非** `2xx` HTTP 状态码
|
||||
- 应答签名验证失败
|
||||
+ HTTP 网络错误,如应答接收超时或网络连接失败
|
||||
+ 客户端失败,如生成签名失败
|
||||
+ 服务器端返回了**非** `2xx` HTTP 状态码
|
||||
+ 应答签名验证失败
|
||||
|
||||
为了方便使用,SDK 将服务器返回的 `4xx` 和 `5xx` 错误,转换成了 `APIError`。
|
||||
|
||||
@@ -223,6 +229,7 @@ if err != nil {
|
||||
2. 调用 `handler.ParseNotifyRequest` 验签,并解密报文。
|
||||
|
||||
### 初始化
|
||||
|
||||
+ 方法一(大多数场景):先手动注册下载器,再获取微信平台证书访问器。
|
||||
|
||||
适用场景: 仅需要对回调通知验证签名并解密的场景。例如,基础支付的回调通知。
|
||||
@@ -291,7 +298,6 @@ fmt.Println(transaction.TransactionId)
|
||||
|
||||
将 SDK 未支持的回调消息体,解析至 `map[string]interface{}`。
|
||||
|
||||
|
||||
```go
|
||||
content := make(map[string]interface{})
|
||||
notifyReq, err := handler.ParseNotifyRequest(context.Background(), request, &content)
|
||||
@@ -453,6 +459,47 @@ func NewCustomClient(ctx context.Context, mchID string) (*core.Client, error) {
|
||||
}
|
||||
```
|
||||
|
||||
### 使用公钥验证微信支付签名
|
||||
|
||||
如果你的商户是全新入驻,且仅可使用微信支付的公钥验证应答和回调的签名,请使用微信支付公钥和公钥 ID 初始化。
|
||||
|
||||
```go
|
||||
var (
|
||||
wechatpayPublicKeyID string = "00000000000000000000000000000000" // 微信支付公钥ID
|
||||
)
|
||||
|
||||
wechatpayPublicKey, err = utils.LoadPublicKeyWithPath("/path/to/wechatpay/pub_key.pem")
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("load wechatpay public key err:%s", err.Error()))
|
||||
}
|
||||
|
||||
// 初始化 Client
|
||||
opts := []core.ClientOption{
|
||||
option.WithWechatPayPublicKeyAuthCipher(
|
||||
mchID,
|
||||
mchCertificateSerialNumber, mchPrivateKey,
|
||||
wechatpayPublicKeyID, wechatpayPublicKey),
|
||||
}
|
||||
client, err := core.NewClient(ctx, opts...)
|
||||
|
||||
// 初始化 notify.Handler
|
||||
handler := notify.NewNotifyHandler(
|
||||
mchAPIv3Key,
|
||||
verifiers.NewSHA256WithRSAPubkeyVerifier(wechatpayPublicKeyID, *wechatPayPublicKey))
|
||||
```
|
||||
|
||||
如果你既有微信支付平台证书,又有公钥。那么,你可以在商户平台自助地从微信支付平台证书切换到公私钥,或者反过来。
|
||||
在切换期间,回调要同时支持使用平台证书和公钥的验签。
|
||||
|
||||
请参考下文,使用微信平台证书访问器和公钥一起初始化 `NotifyHandler`。
|
||||
|
||||
```go
|
||||
// 初始化 notify.Handler
|
||||
handler := notify.NewNotifyHandler(
|
||||
mchAPIv3Key,
|
||||
verifiers.NewSHA256WithRSACombinedVerifier(certificateVisitor, wechatpayPublicKeyID, *wechatPayPublicKey))
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
常见问题请见 [FAQ.md](FAQ.md)。
|
||||
@@ -461,10 +508,10 @@ func NewCustomClient(ctx context.Context, mchID string) (*core.Client, error) {
|
||||
|
||||
微信支付欢迎来自社区的开发者贡献你们的想法和代码。请你在提交 PR 之前,先提一个对应的 issue 说明以下内容:
|
||||
|
||||
- 背景(如,遇到的问题)和目的
|
||||
- **着重**说明你的想法
|
||||
- 通过代码或者其他方式,简要的说明是如何实现的,或者它会是如何使用
|
||||
- 是否影响现有的接口
|
||||
+ 背景(如,遇到的问题)和目的
|
||||
+ **着重**说明你的想法
|
||||
+ 通过代码或者其他方式,简要的说明是如何实现的,或者它会是如何使用
|
||||
+ 是否影响现有的接口
|
||||
|
||||
[#35](https://github.com/wechatpay-apiv3/wechatpay-go/issues/35) 是一个很好的参考。
|
||||
|
||||
@@ -485,6 +532,7 @@ go test -gcflags=all=-l ./...
|
||||
```
|
||||
|
||||
## 联系微信支付
|
||||
|
||||
如果你发现了 BUG,或者需要的功能还未支持,或者有任何疑问、建议,欢迎通过 [issue](https://github.com/wechatpay-apiv3/wechatpay-go/issues) 反馈。
|
||||
|
||||
也欢迎访问微信支付的 [开发者社区](https://developers.weixin.qq.com/community/pay)。
|
||||
|
||||
@@ -10,5 +10,6 @@ import (
|
||||
|
||||
// Validator 应答报文验证器
|
||||
type Validator interface {
|
||||
Validate(ctx context.Context, response *http.Response) error // 对 HTTP 应答报文进行验证
|
||||
Validate(ctx context.Context, response *http.Response) error // 对 HTTP 应答报文进行验证
|
||||
GetAcceptSerial(ctx context.Context) (serial string, err error) // 客户端可以处理的证书或者公钥序列号
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package validators
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@@ -14,6 +15,10 @@ type NullValidator struct {
|
||||
}
|
||||
|
||||
// Validate 跳过报文签名验证
|
||||
func (validator *NullValidator) Validate(context.Context, *http.Response) error {
|
||||
func (v *NullValidator) Validate(context.Context, *http.Response) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *NullValidator) GetAcceptSerial(ctx context.Context) (serial string, err error) {
|
||||
return "", fmt.Errorf("NullValidator has no serial")
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@ import (
|
||||
type mockVerifier struct {
|
||||
}
|
||||
|
||||
func (v *mockVerifier) GetSerial(ctx context.Context) (serial string, err error) {
|
||||
return "SERIAL1234567890", nil
|
||||
}
|
||||
|
||||
func (v *mockVerifier) Verify(ctx context.Context, serialNumber string, message string, signature string) error {
|
||||
if "["+serialNumber+"-"+message+"]" == signature {
|
||||
return nil
|
||||
|
||||
@@ -29,6 +29,11 @@ func (v *WechatPayResponseValidator) Validate(ctx context.Context, response *htt
|
||||
return v.validateHTTPMessage(ctx, response.Header, body)
|
||||
}
|
||||
|
||||
// GetAcceptSerial 客户端可以处理的证书或者公钥序列号
|
||||
func (v *WechatPayResponseValidator) GetAcceptSerial(ctx context.Context) (string, error) {
|
||||
return v.getAcceptSerial(ctx)
|
||||
}
|
||||
|
||||
// NewWechatPayResponseValidator 使用 auth.Verifier 初始化一个 WechatPayResponseValidator
|
||||
func NewWechatPayResponseValidator(verifier auth.Verifier) *WechatPayResponseValidator {
|
||||
return &WechatPayResponseValidator{
|
||||
|
||||
@@ -52,6 +52,10 @@ func (v *wechatPayValidator) validateHTTPMessage(ctx context.Context, header htt
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *wechatPayValidator) getAcceptSerial(ctx context.Context) (string, error) {
|
||||
return v.verifier.GetSerial(ctx)
|
||||
}
|
||||
|
||||
// getWechatPayHeader 从 http.Header 中获取 wechatPayHeader 信息
|
||||
func getWechatPayHeader(ctx context.Context, header http.Header) (wechatPayHeader, error) {
|
||||
_ = ctx // Suppressing warnings
|
||||
@@ -105,7 +109,7 @@ func getWechatPayHeader(ctx context.Context, header http.Header) (wechatPayHeade
|
||||
// checkWechatPayHeader 对 wechatPayHeader 内容进行检查,看是否符合要求
|
||||
//
|
||||
// 检查项:
|
||||
// - Timestamp 与当前时间之差不得超过 FiveMinute;
|
||||
// - Timestamp 与当前时间之差不得超过 FiveMinute;
|
||||
func checkWechatPayHeader(ctx context.Context, args wechatPayHeader) error {
|
||||
// Suppressing warnings
|
||||
_ = ctx
|
||||
|
||||
@@ -8,4 +8,5 @@ import "context"
|
||||
// Verifier 数字签名验证器
|
||||
type Verifier interface {
|
||||
Verify(ctx context.Context, serial, message, signature string) error // 对签名信息进行验证
|
||||
GetSerial(ctx context.Context) (serial string, err error) // 获取可验签的平台证书或公钥序列号
|
||||
}
|
||||
|
||||
37
core/auth/verifiers/sha256withrsa_combined_verifier.go
Normal file
37
core/auth/verifiers/sha256withrsa_combined_verifier.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package verifiers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core"
|
||||
)
|
||||
|
||||
// SHA256WithRSACombinedVerifier 数字签名验证器,组合了公钥和平台证书
|
||||
type SHA256WithRSACombinedVerifier struct {
|
||||
publicKeyVerifier SHA256WithRSAPubkeyVerifier
|
||||
certVerifier SHA256WithRSAVerifier
|
||||
}
|
||||
|
||||
// Verify 验证签名,如果序列号和公钥一致则使用公钥验签,否则使用平台证书验签
|
||||
func (v *SHA256WithRSACombinedVerifier) Verify(ctx context.Context, serialNumber, message, signature string) error {
|
||||
if serialNumber == v.publicKeyVerifier.keyID {
|
||||
return v.publicKeyVerifier.Verify(ctx, serialNumber, message, signature)
|
||||
}
|
||||
return v.certVerifier.Verify(ctx, serialNumber, message, signature)
|
||||
}
|
||||
|
||||
// GetSerial 获取可验签的公钥序列号。该验签器只用在回调,所以获取序列号时返回错误
|
||||
func (v *SHA256WithRSACombinedVerifier) GetSerial(ctx context.Context) (string, error) {
|
||||
return v.publicKeyVerifier.keyID, nil
|
||||
}
|
||||
|
||||
// NewSHA256WithRSACombinedVerifier 用公钥和平台证书初始化验证器
|
||||
func NewSHA256WithRSACombinedVerifier(
|
||||
getter core.CertificateGetter,
|
||||
keyID string,
|
||||
publicKey rsa.PublicKey) *SHA256WithRSACombinedVerifier {
|
||||
return &SHA256WithRSACombinedVerifier{
|
||||
*NewSHA256WithRSAPubkeyVerifier(keyID, publicKey),
|
||||
*NewSHA256WithRSAVerifier(getter),
|
||||
}
|
||||
}
|
||||
50
core/auth/verifiers/sha256withrsa_pubkey_verifier.go
Normal file
50
core/auth/verifiers/sha256withrsa_pubkey_verifier.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright 2024 Tencent Inc. All rights reserved.
|
||||
|
||||
// Package verifiers 微信支付 API v3 Go SDK 数字签名验证器
|
||||
package verifiers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// SHA256WithRSAPubkeyVerifier 数字签名验证器,使用微信支付提供的公钥验证签名
|
||||
type SHA256WithRSAPubkeyVerifier struct {
|
||||
keyID string
|
||||
publicKey rsa.PublicKey
|
||||
}
|
||||
|
||||
// Verify 使用微信支付提供的公钥验证签名
|
||||
func (v *SHA256WithRSAPubkeyVerifier) Verify(ctx context.Context, serialNumber, message, signature string) error {
|
||||
if ctx == nil {
|
||||
return fmt.Errorf("verify failed: context is nil")
|
||||
}
|
||||
if v.keyID != serialNumber {
|
||||
return fmt.Errorf("verify failed: key-id[%s] does not match serial number[%s]", v.keyID, serialNumber)
|
||||
}
|
||||
|
||||
sigBytes, err := base64.StdEncoding.DecodeString(signature)
|
||||
if err != nil {
|
||||
return fmt.Errorf("verify failed: signature is not base64 encoded")
|
||||
}
|
||||
hashed := sha256.Sum256([]byte(message))
|
||||
err = rsa.VerifyPKCS1v15(&v.publicKey, crypto.SHA256, hashed[:], sigBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("verify signature with public key error:%s", err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSerial 获取可验签的公钥序列号
|
||||
func (v *SHA256WithRSAPubkeyVerifier) GetSerial(ctx context.Context) (string, error) {
|
||||
return v.keyID, nil
|
||||
}
|
||||
|
||||
// NewSHA256WithRSAPubkeyVerifier 使用 rsa.PublicKey 初始化验签器
|
||||
func NewSHA256WithRSAPubkeyVerifier(keyID string, publicKey rsa.PublicKey) *SHA256WithRSAPubkeyVerifier {
|
||||
return &SHA256WithRSAPubkeyVerifier{keyID: keyID, publicKey: publicKey}
|
||||
}
|
||||
154
core/auth/verifiers/sha256withrsa_pubkey_verifier_test.go
Normal file
154
core/auth/verifiers/sha256withrsa_pubkey_verifier_test.go
Normal file
@@ -0,0 +1,154 @@
|
||||
// Copyright 2021 Tencent Inc. All rights reserved.
|
||||
|
||||
package verifiers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"testing"
|
||||
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
testPubKeyID = "F5765756002FDD77"
|
||||
testPubKey = `-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2VCTd91fnUn73Xy9DLvt
|
||||
/V62TVxRTEEstVdeRaZ3B3leO0pldE806mXO4RwdHXagHQ4vGeZN0yqm++rDsGK+
|
||||
U3AH7kejyD2pXshNP9Cq5YwbptiLGtjcquw4HNxJQUOmDeJf2vg6byms9RUipiq4
|
||||
SzbJKqJFlUpbuIPDpSpWz10PYmyCNeDGUUK65E5h2B834uxl1zNLYQCrkdBzb8oU
|
||||
xwYeP5a2DNxmjL5lsJML7DGr5znsevnoqGRwTm9fxCGfy8wus7hwKz6clt3Whmmd
|
||||
a7UAdb1c08hEQFVRbF14AR73xbnd8N0obCWJPCbzMCtkaSef4FdEEgEXJiw0VAJT
|
||||
8wIDAQAB
|
||||
-----END PUBLIC KEY-----`
|
||||
// testExpectedSignature = "BKyAfU4iMCuvXMXS0Wzam3V/cnxZ+JaqigPM5OhljS2iOT95OO6Fsuml2JkFANJU9" +
|
||||
// "K6q9bLlDhPXuoVz+pp4hAm6pHU4ld815U4jsKu1RkyaII+1CYBUYC8TK0XtJ8FwUXXz8vZHh58rrAVN1XwNyv" +
|
||||
// "D1vfpxrMT4SL536GLwvpUHlCqIMzoZUguLli/K8V29QiOhuH6IEqLNJn8e9b3nwNcQ7be3CzYGpDAKBfDGPCq" +
|
||||
// "Cv8Rw5zndhlffk2FEA70G4hvMwe51qMN/RAJbknXG23bSlObuTCN7Ndj1aJGH6/L+hdwfLpUtJm4QYVazzW7D" +
|
||||
// "FD27EpSQEqA8bX9+8m1rLg=="
|
||||
)
|
||||
|
||||
var (
|
||||
pubKey *rsa.PublicKey
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
pubKey, err = utils.LoadPublicKey(testPubKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWechatPayPubKeyVerifier(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
serialNumber string
|
||||
message string
|
||||
signature string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields *rsa.PublicKey
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "verify success",
|
||||
fields: pubKey,
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
serialNumber: testPubKeyID,
|
||||
signature: testExpectedSignature,
|
||||
message: "source",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "verify failed",
|
||||
fields: pubKey,
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
serialNumber: testPubKeyID,
|
||||
signature: testExpectedSignature,
|
||||
message: "wrong source",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "verify failed with null context",
|
||||
fields: pubKey,
|
||||
args: args{
|
||||
ctx: nil,
|
||||
serialNumber: testWechatPayVerifierPlatformSerialNumber,
|
||||
signature: testExpectedSignature,
|
||||
message: "source",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "verify failed with empty keyId",
|
||||
fields: pubKey,
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
serialNumber: "",
|
||||
signature: testExpectedSignature,
|
||||
message: "source",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "verify failed with empty message",
|
||||
fields: pubKey,
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
serialNumber: testPubKeyID,
|
||||
signature: testExpectedSignature,
|
||||
message: "",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "verify failed with empty signature",
|
||||
fields: pubKey,
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
serialNumber: testPubKeyID,
|
||||
signature: "",
|
||||
message: "source",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "verify failed with non-base64 signature",
|
||||
fields: pubKey,
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
serialNumber: testPubKeyID,
|
||||
signature: "invalid base64 signature",
|
||||
message: "source",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "verify failed with no corresponding pubkey",
|
||||
fields: pubKey,
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
serialNumber: "invalid serial number",
|
||||
signature: testExpectedSignature,
|
||||
message: "source",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var verifier = NewSHA256WithRSAPubkeyVerifier(testPubKeyID, *tt.fields)
|
||||
if err := verifier.Verify(tt.args.ctx, tt.args.serialNumber, tt.args.message,
|
||||
tt.args.signature); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Verify() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,11 @@ func (verifier *SHA256WithRSAVerifier) Verify(ctx context.Context, serialNumber,
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSerial 获取可验签的平台证书序列号
|
||||
func (verifier *SHA256WithRSAVerifier) GetSerial(ctx context.Context) (string, error) {
|
||||
return verifier.certGetter.GetNewestSerial(ctx), nil
|
||||
}
|
||||
|
||||
func checkParameter(ctx context.Context, serialNumber, message, signature string) error {
|
||||
if ctx == nil {
|
||||
return fmt.Errorf("context is nil, verifier need input context.Context")
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/utils"
|
||||
)
|
||||
|
||||
// WechatPayEncryptor 微信支付字符串加密器
|
||||
// WechatPayEncryptor 微信支付字符串加密器,使用微信支付平台证书
|
||||
type WechatPayEncryptor struct {
|
||||
// 微信支付平台证书提供器
|
||||
certGetter core.CertificateGetter
|
||||
@@ -34,7 +34,8 @@ func (e *WechatPayEncryptor) SelectCertificate(ctx context.Context) (serial stri
|
||||
}
|
||||
|
||||
// Encrypt 对字符串加密
|
||||
func (e *WechatPayEncryptor) Encrypt(ctx context.Context, serial, plaintext string) (ciphertext string, err error) {
|
||||
func (e *WechatPayEncryptor) Encrypt(
|
||||
ctx context.Context, serial, plaintext string) (ciphertext string, err error) {
|
||||
cert, ok := e.certGetter.Get(ctx, serial)
|
||||
|
||||
if !ok {
|
||||
|
||||
@@ -88,6 +88,16 @@ jWNRBmvvftZhY59PILHO2X5vO4FXh7suEjy6VIh0gsnK36mmRboYIBGsNuDHjXLe
|
||||
BDa+8mDLkWu5nHEhOxy2JJZl
|
||||
-----END TESTING KEY-----`
|
||||
|
||||
const publicKeyStr = `-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2VCTd91fnUn73Xy9DLvt
|
||||
/V62TVxRTEEstVdeRaZ3B3leO0pldE806mXO4RwdHXagHQ4vGeZN0yqm++rDsGK+
|
||||
U3AH7kejyD2pXshNP9Cq5YwbptiLGtjcquw4HNxJQUOmDeJf2vg6byms9RUipiq4
|
||||
SzbJKqJFlUpbuIPDpSpWz10PYmyCNeDGUUK65E5h2B834uxl1zNLYQCrkdBzb8oU
|
||||
xwYeP5a2DNxmjL5lsJML7DGr5znsevnoqGRwTm9fxCGfy8wus7hwKz6clt3Whmmd
|
||||
a7UAdb1c08hEQFVRbF14AR73xbnd8N0obCWJPCbzMCtkaSef4FdEEgEXJiw0VAJT
|
||||
8wIDAQAB
|
||||
-----END PUBLIC KEY-----`
|
||||
|
||||
func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") }
|
||||
|
||||
func initWechatPayEncryptor() (*WechatPayEncryptor, error) {
|
||||
@@ -103,7 +113,7 @@ func initWechatPayEncryptor() (*WechatPayEncryptor, error) {
|
||||
return NewWechatPayEncryptor(core.NewCertificateMapWithList(l)), nil
|
||||
}
|
||||
|
||||
func TestWechatPayEncryptor_SelectCertificate(t *testing.T) {
|
||||
func TestWechatPayEncryptorSelectCertificate(t *testing.T) {
|
||||
e, err := initWechatPayEncryptor()
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -112,7 +122,7 @@ func TestWechatPayEncryptor_SelectCertificate(t *testing.T) {
|
||||
assert.Equal(t, "D7CE59D1F522D701", serial)
|
||||
}
|
||||
|
||||
func TestWechatPayEncryptor_Encrypt(t *testing.T) {
|
||||
func TestWechatPayEncryptorEncrypt(t *testing.T) {
|
||||
e, err := initWechatPayEncryptor()
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -130,7 +140,7 @@ func TestWechatPayEncryptor_Encrypt(t *testing.T) {
|
||||
assert.Equal(t, newPlainText, plaintext)
|
||||
}
|
||||
|
||||
func TestWechatPayEncryptor_EncryptEmpty(t *testing.T) {
|
||||
func TestWechatPayEncryptorEncryptEmpty(t *testing.T) {
|
||||
e, err := initWechatPayEncryptor()
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -142,7 +152,7 @@ func TestWechatPayEncryptor_EncryptEmpty(t *testing.T) {
|
||||
assert.Equal(t, "", ciphertext)
|
||||
}
|
||||
|
||||
func TestWechatPayEncryptor_EncryptWithWrongSerial(t *testing.T) {
|
||||
func TestWechatPayEncryptorEncryptWithWrongSerial(t *testing.T) {
|
||||
e, err := initWechatPayEncryptor()
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -153,7 +163,7 @@ func TestWechatPayEncryptor_EncryptWithWrongSerial(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMockEncryptor_Encrypt(t *testing.T) {
|
||||
func TestMockEncryptorEncrypt(t *testing.T) {
|
||||
e := MockEncryptor{Serial: "F5765756002FDD77"}
|
||||
|
||||
cipertext, err := e.Encrypt(context.Background(), "F5765756002FDD77", "hehe")
|
||||
@@ -161,9 +171,23 @@ func TestMockEncryptor_Encrypt(t *testing.T) {
|
||||
assert.Equal(t, "Encryptedhehe", cipertext)
|
||||
}
|
||||
|
||||
func TestMockEncryptor_EncryptWithWrontSerial(t *testing.T) {
|
||||
func TestMockEncryptorEncryptWithWrontSerial(t *testing.T) {
|
||||
e := MockEncryptor{Serial: "F5765756002FDD77"}
|
||||
|
||||
_, err := e.Encrypt(context.Background(), "wrong serial", "hehe")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestPublicEncryptorEncrypt(t *testing.T) {
|
||||
publicKey, _ := utils.LoadPublicKey(publicKeyStr)
|
||||
e := NewWechatPayPubKeyEncryptor("F5765756002FDD77", *publicKey)
|
||||
|
||||
plaintext := "hehe"
|
||||
ciphertext, err := e.Encrypt(context.Background(), "F5765756002FDD77", plaintext)
|
||||
require.NoError(t, err)
|
||||
|
||||
privateKey, _ := utils.LoadPrivateKey(testingKey(privateKeyStr))
|
||||
newPlainText, err := utils.DecryptOAEP(ciphertext, privateKey)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, newPlainText, plaintext)
|
||||
}
|
||||
|
||||
44
core/cipher/encryptors/wechat_pay_pubkey_encryptor.go
Normal file
44
core/cipher/encryptors/wechat_pay_pubkey_encryptor.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright 2024 Tencent Inc. All rights reserved.
|
||||
|
||||
package encryptors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/utils"
|
||||
)
|
||||
|
||||
// WechatPayPubKeyEncryptor 微信支付字符串加密器,使用微信支付公钥
|
||||
type WechatPayPubKeyEncryptor struct {
|
||||
// 微信支付公钥
|
||||
publicKey rsa.PublicKey
|
||||
// 公钥 ID
|
||||
keyID string
|
||||
}
|
||||
|
||||
// NewWechatPayPubKeyEncryptor 新建一个 WechatPayPubKeyEncryptor
|
||||
func NewWechatPayPubKeyEncryptor(keyID string, publicKey rsa.PublicKey) *WechatPayPubKeyEncryptor {
|
||||
return &WechatPayPubKeyEncryptor{publicKey: publicKey, keyID: keyID}
|
||||
}
|
||||
|
||||
// SelectCertificate 选择合适的微信支付平台证书用于加密
|
||||
// 返回公钥对应的 KeyId 作为证书序列号
|
||||
func (e *WechatPayPubKeyEncryptor) SelectCertificate(ctx context.Context) (serial string, err error) {
|
||||
return e.keyID, nil
|
||||
}
|
||||
|
||||
// Encrypt 对字符串加密
|
||||
func (e *WechatPayPubKeyEncryptor) Encrypt(ctx context.Context, serial, plaintext string) (ciphertext string, err error) {
|
||||
if serial != e.keyID {
|
||||
return "", fmt.Errorf("serial %v not match key-id %v", serial, e.keyID)
|
||||
}
|
||||
|
||||
// 不需要对空串进行加密
|
||||
if plaintext == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return utils.EncryptOAEPWithPublicKey(plaintext, &e.publicKey)
|
||||
}
|
||||
@@ -3,8 +3,8 @@
|
||||
// Package core 微信支付 API v3 Go SDK HTTPClient 基础库,你可以使用它来创建一个 Client,并向微信支付发送 HTTP 请求
|
||||
//
|
||||
// 初始化 Client 时,你需要指定以下参数:
|
||||
// - Credential 用于生成 HTTP Header 中的 Authorization 信息,微信支付 API v3依赖该值来保证请求的真实性和数据的完整性
|
||||
// - Validator 用于对微信支付的应答进行校验,避免被恶意攻击
|
||||
// - Credential 用于生成 HTTP Header 中的 Authorization 信息,微信支付 API v3依赖该值来保证请求的真实性和数据的完整性
|
||||
// - Validator 用于对微信支付的应答进行校验,避免被恶意攻击
|
||||
package core
|
||||
|
||||
import (
|
||||
@@ -227,6 +227,11 @@ func (client *Client) doRequest(
|
||||
}
|
||||
request.Header.Set(consts.Authorization, authorization)
|
||||
|
||||
// indicate Wechatpay-Serial that client can verify
|
||||
if serial, err := client.validator.GetAcceptSerial(ctx); err == nil {
|
||||
request.Header.Set(consts.WechatPaySerial, serial)
|
||||
}
|
||||
|
||||
// Send HTTP Request
|
||||
result, err := client.doHTTP(request)
|
||||
if err != nil {
|
||||
@@ -369,12 +374,14 @@ func UnMarshalResponse(httpResp *http.Response, resp interface{}) error {
|
||||
// CreateFormField 设置form-data 中的普通属性
|
||||
//
|
||||
// 示例内容
|
||||
//
|
||||
// Content-Disposition: form-data; name="meta";
|
||||
// Content-Type: application/json
|
||||
//
|
||||
// { "filename": "file_test.mp4", "sha256": " hjkahkjsjkfsjk78687dhjahdajhk " }
|
||||
//
|
||||
// 如果要设置上述内容
|
||||
//
|
||||
// CreateFormField(w, "meta", "application/json", meta)
|
||||
func CreateFormField(w *multipart.Writer, fieldName, contentType string, fieldValue []byte) error {
|
||||
h := make(textproto.MIMEHeader)
|
||||
@@ -391,6 +398,7 @@ func CreateFormField(w *multipart.Writer, fieldName, contentType string, fieldVa
|
||||
// CreateFormFile 设置form-data中的文件
|
||||
//
|
||||
// 示例内容:
|
||||
//
|
||||
// Content-Disposition: form-data; name="file"; filename="file_test.mp4";
|
||||
// Content-Type: video/mp4
|
||||
//
|
||||
@@ -409,8 +417,9 @@ func CreateFormFile(w *multipart.Writer, filename, contentType string, file []by
|
||||
return err
|
||||
}
|
||||
|
||||
//revive:disable-next-line:cyclomatic 本函数实现需要考虑多种情况,但理解起来并不复杂,进行圈复杂度豁免
|
||||
// setBody Set Request body from an interface
|
||||
//
|
||||
//revive:disable-next-line:cyclomatic 本函数实现需要考虑多种情况,但理解起来并不复杂,进行圈复杂度豁免
|
||||
func setBody(body interface{}, contentType string) (bodyBuf *bytes.Buffer, err error) {
|
||||
bodyBuf = &bytes.Buffer{}
|
||||
|
||||
|
||||
@@ -273,6 +273,7 @@ func TestRequest(t *testing.T) {
|
||||
schema, params := parseAuthorization(t, r.Header.Get("Authorization"))
|
||||
body, _ := ioutil.ReadAll(r.Body)
|
||||
assertAuthorization(t, schema, r.Method, r.RequestURI, params, body)
|
||||
assert.Equal(t, "9F2A649600414C1485E8A643CB103593", r.Header.Get("Wechatpay-Serial"))
|
||||
|
||||
if test.body != nil {
|
||||
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||
|
||||
@@ -28,6 +28,7 @@ func (w withAuthCipherOption) Apply(o *core.DialSettings) error {
|
||||
}
|
||||
|
||||
// WithWechatPayAuthCipher 一键初始化 Client,使其具备「签名/验签/敏感字段加解密」能力
|
||||
// Deprecated: 使用 WithWechatPayAutoAuthCipher 或 WithWechatPayPublicKeyAuthCipher 代替
|
||||
func WithWechatPayAuthCipher(
|
||||
mchID string, certificateSerialNo string, privateKey *rsa.PrivateKey, certificateList []*x509.Certificate,
|
||||
) core.ClientOption {
|
||||
@@ -91,3 +92,28 @@ func WithWechatPayAutoAuthCipherUsingDownloaderMgr(
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// WithWechatPayPublicKeyAuthCipher 一键初始化 Client,使其具备「签名/验签/敏感字段加解密」能力。
|
||||
// 使用微信支付提供的公钥验签
|
||||
func WithWechatPayPublicKeyAuthCipher(
|
||||
mchID, certificateSerialNo string, privateKey *rsa.PrivateKey, publicKeyID string, publicKey *rsa.PublicKey,
|
||||
) core.ClientOption {
|
||||
return withAuthCipherOption{
|
||||
settings: core.DialSettings{
|
||||
Signer: &signers.SHA256WithRSASigner{
|
||||
MchID: mchID,
|
||||
CertificateSerialNo: certificateSerialNo,
|
||||
PrivateKey: privateKey,
|
||||
},
|
||||
Validator: validators.NewWechatPayResponseValidator(
|
||||
verifiers.NewSHA256WithRSAPubkeyVerifier(
|
||||
publicKeyID,
|
||||
*publicKey,
|
||||
)),
|
||||
Cipher: ciphers.NewWechatPayCipher(
|
||||
encryptors.NewWechatPayPubKeyEncryptor(publicKeyID, *publicKey),
|
||||
decryptors.NewWechatPayDecryptor(privateKey),
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user