From cd4d2631c715f19b1d065656f42a37efa4398e5c Mon Sep 17 00:00:00 2001 From: Xiaoyu PENG Date: Tue, 20 Aug 2024 16:47:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=85=AC=E9=92=A5=E9=AA=8C?= =?UTF-8?q?=E7=AD=BE=20(#226)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 支持使用微信支付公钥验签 * feat: 回调验签同时支持公钥和证书 * Update README.md --- README.md | 72 ++++++-- core/auth/validator.go | 3 +- core/auth/validators/null_validator.go | 7 +- core/auth/validators/validator_test.go | 4 + .../wechat_pay_response_validator.go | 5 + core/auth/validators/wechat_pay_validator.go | 6 +- core/auth/verifier.go | 1 + .../sha256withrsa_combined_verifier.go | 37 +++++ .../sha256withrsa_pubkey_verifier.go | 50 ++++++ .../sha256withrsa_pubkey_verifier_test.go | 154 ++++++++++++++++++ core/auth/verifiers/sha256withrsa_verifier.go | 5 + .../cipher/encryptors/wechat_pay_encryptor.go | 5 +- .../encryptors/wechat_pay_encryptor_test.go | 36 +++- .../encryptors/wechat_pay_pubkey_encryptor.go | 44 +++++ core/client.go | 15 +- core/client_test.go | 1 + core/option/auth_cipher_option.go | 26 +++ 17 files changed, 445 insertions(+), 26 deletions(-) create mode 100644 core/auth/verifiers/sha256withrsa_combined_verifier.go create mode 100644 core/auth/verifiers/sha256withrsa_pubkey_verifier.go create mode 100644 core/auth/verifiers/sha256withrsa_pubkey_verifier_test.go create mode 100644 core/cipher/encryptors/wechat_pay_pubkey_encryptor.go diff --git a/README.md b/README.md index 4d93df4..3e5556a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 微信支付 API v3 Go SDK + [![GoDoc](http://img.shields.io/badge/godoc-reference-blue.svg)](https://pkg.go.dev/github.com/wechatpay-apiv3/wechatpay-go) -[![huntr](https://cdn.huntr.dev/huntr_security_badge_mono.svg)](https://huntr.dev) [![licence](https://badgen.net/github/license/wechatpay-apiv3/wechatpay-go)](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)。 diff --git a/core/auth/validator.go b/core/auth/validator.go index ae6961b..9055018 100644 --- a/core/auth/validator.go +++ b/core/auth/validator.go @@ -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) // 客户端可以处理的证书或者公钥序列号 } diff --git a/core/auth/validators/null_validator.go b/core/auth/validators/null_validator.go index 0a2f2dd..09b752e 100644 --- a/core/auth/validators/null_validator.go +++ b/core/auth/validators/null_validator.go @@ -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") +} diff --git a/core/auth/validators/validator_test.go b/core/auth/validators/validator_test.go index 7d6b098..d43be9e 100644 --- a/core/auth/validators/validator_test.go +++ b/core/auth/validators/validator_test.go @@ -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 diff --git a/core/auth/validators/wechat_pay_response_validator.go b/core/auth/validators/wechat_pay_response_validator.go index 380a20f..d5fe728 100644 --- a/core/auth/validators/wechat_pay_response_validator.go +++ b/core/auth/validators/wechat_pay_response_validator.go @@ -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{ diff --git a/core/auth/validators/wechat_pay_validator.go b/core/auth/validators/wechat_pay_validator.go index b60f699..2cc76b2 100644 --- a/core/auth/validators/wechat_pay_validator.go +++ b/core/auth/validators/wechat_pay_validator.go @@ -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 diff --git a/core/auth/verifier.go b/core/auth/verifier.go index 2d1f9ed..694ab29 100644 --- a/core/auth/verifier.go +++ b/core/auth/verifier.go @@ -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) // 获取可验签的平台证书或公钥序列号 } diff --git a/core/auth/verifiers/sha256withrsa_combined_verifier.go b/core/auth/verifiers/sha256withrsa_combined_verifier.go new file mode 100644 index 0000000..67777dd --- /dev/null +++ b/core/auth/verifiers/sha256withrsa_combined_verifier.go @@ -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), + } +} diff --git a/core/auth/verifiers/sha256withrsa_pubkey_verifier.go b/core/auth/verifiers/sha256withrsa_pubkey_verifier.go new file mode 100644 index 0000000..f400825 --- /dev/null +++ b/core/auth/verifiers/sha256withrsa_pubkey_verifier.go @@ -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} +} diff --git a/core/auth/verifiers/sha256withrsa_pubkey_verifier_test.go b/core/auth/verifiers/sha256withrsa_pubkey_verifier_test.go new file mode 100644 index 0000000..64bac98 --- /dev/null +++ b/core/auth/verifiers/sha256withrsa_pubkey_verifier_test.go @@ -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) + } + }) + } +} diff --git a/core/auth/verifiers/sha256withrsa_verifier.go b/core/auth/verifiers/sha256withrsa_verifier.go index db473a5..9c3fe41 100644 --- a/core/auth/verifiers/sha256withrsa_verifier.go +++ b/core/auth/verifiers/sha256withrsa_verifier.go @@ -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") diff --git a/core/cipher/encryptors/wechat_pay_encryptor.go b/core/cipher/encryptors/wechat_pay_encryptor.go index d5984ea..6e7cbd9 100644 --- a/core/cipher/encryptors/wechat_pay_encryptor.go +++ b/core/cipher/encryptors/wechat_pay_encryptor.go @@ -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 { diff --git a/core/cipher/encryptors/wechat_pay_encryptor_test.go b/core/cipher/encryptors/wechat_pay_encryptor_test.go index 6fada36..19f1e77 100644 --- a/core/cipher/encryptors/wechat_pay_encryptor_test.go +++ b/core/cipher/encryptors/wechat_pay_encryptor_test.go @@ -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) +} diff --git a/core/cipher/encryptors/wechat_pay_pubkey_encryptor.go b/core/cipher/encryptors/wechat_pay_pubkey_encryptor.go new file mode 100644 index 0000000..f274c29 --- /dev/null +++ b/core/cipher/encryptors/wechat_pay_pubkey_encryptor.go @@ -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) +} diff --git a/core/client.go b/core/client.go index c673629..9a7cb3e 100644 --- a/core/client.go +++ b/core/client.go @@ -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{} diff --git a/core/client_test.go b/core/client_test.go index c4bc8aa..d2ae451 100644 --- a/core/client_test.go +++ b/core/client_test.go @@ -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")) diff --git a/core/option/auth_cipher_option.go b/core/option/auth_cipher_option.go index 8bab742..1e159f3 100644 --- a/core/option/auth_cipher_option.go +++ b/core/option/auth_cipher_option.go @@ -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), + ), + }, + } +}