Skip to content

Commit

Permalink
feat: 支持使用微信支付公钥验签
Browse files Browse the repository at this point in the history
  • Loading branch information
xy-peng committed Apr 20, 2024
1 parent d20bbf7 commit 5737c5a
Show file tree
Hide file tree
Showing 8 changed files with 355 additions and 25 deletions.
10 changes: 5 additions & 5 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ jobs:
strategy:
matrix:
go:
- "1.22"
- "1.21"
- "1.20"
- "1.19"
- "1.18"
- "1.17"
- "1.16"
- "1.15"
- "1.14"
- "1.13"
steps:
- uses: actions/checkout@v2

Expand All @@ -39,10 +39,10 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: "1.16"
go-version: "1.19"
- name: staticcheck
run: |
go get -u honnef.co/go/tools/cmd/staticcheck@v0.2.2 &&
go install honnef.co/go/tools/cmd/staticcheck@latest &&
$HOME/go/bin/staticcheck ./...
- name: Revive Action
uses: morphy2k/[email protected]
Expand Down
60 changes: 48 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -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语言客户端代码库。
Expand Down Expand Up @@ -28,10 +28,12 @@
go mod init
```

#### 2、无需 clone 仓库中的代码,直接在项目目录中执行:
#### 2、无需 clone 仓库中的代码,直接在项目目录中执行

```shell
go get -u github.com/wechatpay-apiv3/wechatpay-go
```

来添加依赖,完成 `go.mod` 修改与 SDK 下载。

### 发送请求
Expand Down Expand Up @@ -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` 加密时使用的对称加密密钥。
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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`

Expand All @@ -223,6 +229,7 @@ if err != nil {
2. 调用 `handler.ParseNotifyRequest` 验签,并解密报文。

### 初始化

+ 方法一(大多数场景):先手动注册下载器,再获取微信平台证书访问器。

适用场景: 仅需要对回调通知验证签名并解密的场景。例如,基础支付的回调通知。
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -453,6 +459,35 @@ 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))
```

## 常见问题

常见问题请见 [FAQ.md](FAQ.md)
Expand All @@ -461,10 +496,10 @@ func NewCustomClient(ctx context.Context, mchID string) (*core.Client, error) {

微信支付欢迎来自社区的开发者贡献你们的想法和代码。请你在提交 PR 之前,先提一个对应的 issue 说明以下内容:

- 背景(如,遇到的问题)和目的
- **着重**说明你的想法
- 通过代码或者其他方式,简要的说明是如何实现的,或者它会是如何使用
- 是否影响现有的接口
+ 背景(如,遇到的问题)和目的
+ **着重**说明你的想法
+ 通过代码或者其他方式,简要的说明是如何实现的,或者它会是如何使用
+ 是否影响现有的接口

[#35](https://github.com/wechatpay-apiv3/wechatpay-go/issues/35) 是一个很好的参考。

Expand All @@ -485,6 +520,7 @@ go test -gcflags=all=-l ./...
```

## 联系微信支付

如果你发现了 BUG,或者需要的功能还未支持,或者有任何疑问、建议,欢迎通过 [issue](https://github.com/wechatpay-apiv3/wechatpay-go/issues) 反馈。

也欢迎访问微信支付的 [开发者社区](https://developers.weixin.qq.com/community/pay)
Expand Down
45 changes: 45 additions & 0 deletions core/auth/verifiers/sha256withrsa_pubkey_verifier.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// 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
}

// NewSHA256WithRSAPubkeyVerifier 使用 rsa.PublicKey 初始化验签器
func NewSHA256WithRSAPubkeyVerifier(keyID string, publicKey rsa.PublicKey) *SHA256WithRSAPubkeyVerifier {
return &SHA256WithRSAPubkeyVerifier{keyID: keyID, publicKey: publicKey}
}
154 changes: 154 additions & 0 deletions core/auth/verifiers/sha256withrsa_pubkey_verifier_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
5 changes: 3 additions & 2 deletions core/cipher/encryptors/wechat_pay_encryptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"github.com/wechatpay-apiv3/wechatpay-go/utils"
)

// WechatPayEncryptor 微信支付字符串加密器
// WechatPayEncryptor 微信支付字符串加密器,使用微信支付平台证书
type WechatPayEncryptor struct {
// 微信支付平台证书提供器
certGetter core.CertificateGetter
Expand All @@ -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 {
Expand Down
Loading

0 comments on commit 5737c5a

Please sign in to comment.