支付宝token获取

This commit is contained in:
李子铭 2024-08-30 11:29:25 +08:00
parent d9c3e7ad88
commit d15e6d43be
14 changed files with 599 additions and 15 deletions

74
alipay/client.go Normal file
View File

@ -0,0 +1,74 @@
package alipay
import (
"fmt"
"gitea.cdlsxd.cn/BaseSystem/plugin/alipay/vo"
"github.com/go-playground/validator/v10"
)
const BaseUri = "https://openapi.alipay.com/gateway.do"
const (
Version = "1.0"
Format = "json"
SignType = "RSA2"
Charset = "UTF-8"
)
type Client struct {
AppId string // 应用ID
Prk string // 私钥
PukPath string // 验签公钥
MchCertPath string // 商户证书路径
RootCertPath string // 根证书路径
NotifyUrl string // 回调地址
}
type Req interface {
Validate() error
}
var _ Req = (*Param)(nil)
type Param struct {
AlipayRootCertSn string `json:"alipay_root_cert_sn"`
AppCertSn string `json:"app_cert_sn"`
AppId string `json:"app_id" validate:"required"`
Method string `json:"method" validate:"required"`
Format string `json:"format" validate:"required"`
Charset string `json:"charset" validate:"required"`
SignType string `json:"sign_type" validate:"required"`
Timestamp string `json:"timestamp" validate:"required"`
Version string `json:"version" validate:"required"`
BizContent string `json:"biz_content"`
Sign string `json:"sign"`
}
type ErrorResponse struct {
Code vo.Code `json:"code"`
Msg string `json:"msg"`
SubCode string `json:"sub_code"`
SubMsg string `json:"sub_msg"`
}
type Notify struct {
Charset string `json:"charset"`
UtcTimestamp string `json:"utc_timestamp"`
AppId string `json:"app_id"`
Version string `json:"version"`
SignType string `json:"sign_type"`
NotifyId string `json:"notify_id"`
MsgMethod string `json:"msg_method"`
Sign string `json:"sign"`
BizContent string `json:"biz_content"`
}
func (req *Param) Validate() error {
err := validator.New().Struct(req)
if err != nil {
for _, err = range err.(validator.ValidationErrors) {
return fmt.Errorf("参数有误:" + err.Error())
}
}
return nil
}

43
alipay/token/client.go Normal file
View File

@ -0,0 +1,43 @@
package token
import (
"context"
"fmt"
"gitea.cdlsxd.cn/BaseSystem/plugin/alipay"
"gitea.cdlsxd.cn/BaseSystem/plugin/alipay/utils"
"github.com/carlmjohnson/requests"
"time"
)
type Token alipay.Client
func (t *Token) Client(ctx context.Context, code string) (*AlipayTokenResponse, error) {
cert, err := utils.GetCert(t.MchCertPath, t.RootCertPath, t.PukPath, t.AppId)
if err != nil {
return nil, err
}
param := &Param{
AlipayRootCertSn: cert.RootCertSN,
AppCertSn: cert.MchCertSN,
AppId: t.AppId,
Method: "alipay.system.oauth.token",
Format: alipay.Format,
Charset: alipay.Charset,
SignType: alipay.SignType,
Timestamp: time.Now().Format(time.DateTime),
Version: alipay.Version,
Sign: "",
GrantType: "authorization_code",
Code: code,
}
uv, err := utils.UrlValues(t.Prk, param)
if err != nil {
return nil, err
}
var response AlipayTokenResponse
err = requests.URL(alipay.BaseUri).Post().Params(uv).ToJSON(&response).Fetch(ctx)
if err != nil {
return nil, fmt.Errorf("请求异常msg:" + err.Error())
}
return &response, nil
}

View File

@ -0,0 +1,29 @@
package token
import (
"context"
"testing"
)
func TestToken_Client(t *testing.T) {
d := Token{
AppId: "2021004100663111",
Prk: "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDbA+YuMp4JUVj6rjzgwGKNXWkEMGX/rinqkfyBZ6B6p8EKz8zgA+ypiJLOixD3GyKnUnAzx4waNRZHfdEu+l57kJFtd/ipfwtJ28aTi7TqtqEpqD+UPY4ourt2CuyCFxWsonS6dczqtTvfAVArTdbGJYY+kNNVR3WiXgGUhkUu8N7vEowU00RUQGNdSVMUs4FX+HlU3RnEoRc/xUhPiaLf0Bm/g9wG96kwyg/TZvkNU7PpMVRdXeLrVORn0qThs3VA4dqondF+O12iC1TK4TKYGzFYczGAUsfuurtDyCc2GMoE+hH2FR8U7amQOVuYZFkutTdqaqukWpFQOr8wLeMzAgMBAAECggEAD715/3v3y6ejA3EeQvDQpGRANeLckcGMlaUkRpCSAf6oawSALuFZUt3T3zAzae7zUJ8mHTKMKR4DmeO68utfevXq3bkvj87nmslGvjfeKrgxYPMMjrTV0KuK6XLjiH3sOtn6FaR9s6iOwvovLs2LT/ZGbZyu84QNOjwTVP9JXZQkBgMItdKf+U3H2Cjp7U/qXBt8/9yzVFklp1g1883DAty0lzmT27dJimGVGaPQ8vNxo81+ZUEJAn6GUTk0K/GwJfhPTU8hh8G90n2LTyskoMjGxQe9lXfCcS9DmWawEQL4WTctPrDYlnS/cjCVMS0KXIFuxRNf6qaMYDeywC8BgQKBgQDyf40dvmw34Rrb46+NLayQ9W5CJI/dYeRajpCjoOomq5QYhbXzUCpVfbtByeGMsg3zN58NNsZGhl5SU0GdEdoOlxCk+2Hey2yQYF4ugQm/dTd68Jgqi3yujigAbNYa1ZhL9t3FqouPY1dGiaxl+DFYdSMIrsVFXh2NbrPyqTk5IQKBgQDnNaH7LCcIUqg9H8Tsls+8GLzP2HwF3hdll8asEsF3K3HX6/Zlp4VnnEcIkAxLRL/L0o5akXrmA18ZwfoSguTPXV9va3G2GgIiJHgcytmGtQVvbpFKuPnCXKz+avxnfO0flJqyYEuHr/40jsGbMkk0Kr52/n3ivXZbBUlT+tkt0wKBgB0qLgSnxEgsMJjFl3V5SsncWrhlwU+02Evz3X1wevjPpe4VFr7+ozjI+F5/MztCpt7bj6t9LPeKbYmlLb0ASqN6k6vj9+9ds97hWDJrnoqCRHvqt8JWKFauDi2O6WksyzZHqIB/dG14WyTGpg9VfEnRPLdsnZksKo26BLZol9NBAoGBAMz7oMNljqlzVtLyMo2q2zuhFuySusoc79NTL4FpE3rK2qCbA5V2YvDL/bIau7uTlRNodmrXZgU84fidIE9/Gsq5tp26vVK8Vj3c5Vxpf1dNcCct+MQtoMjvjzP0uBgsCrKf9lLEytHed1ozYnRsrbgBWWF4GTWH0cG6uxsoX5mfAoGAfYXKZdyRU+Su4y4EnDLMXd320ar7PaeuY8aZU7V6UQEsaOj6H3O8JMEOuBrOVEhAP2EkAC8ayargSXTSOkN97pg88agKDwA6jh4N6TKAK8XMft81YPPliVwZMsAUqihSKQBnKZ7ssHHLGWWWp5vfkyb7Y7dIkZcPzB0X3q/jL58=",
PukPath: "/Users/lsxd/code/php/yxxt/market/config/alipaycash/alipayCertPublicKey_RSA2.crt",
MchCertPath: "/Users/lsxd/code/php/yxxt/market/config/alipaycash/appCertPublicKey_2021004100663111.crt",
RootCertPath: "/Users/lsxd/code/php/yxxt/market/config/alipaycash/alipayRootCert.crt",
}
resp, err := d.Client(context.Background(), "349feb5fb58e455da1b00a7748ceMD68")
if err != nil {
t.Error(err)
} else {
if resp.IsSuccess() {
t.Logf("Response: %+v", resp.Response)
} else {
t.Errorf("ErrorResponse: %+v", resp.ErrorResponse)
t.Errorf("ErrorResponse Msg: %+v", resp.ErrorResponse.Msg)
t.Errorf("ErrorResponse SubCode: %+v", resp.ErrorResponse.SubCode)
t.Errorf("ErrorResponse SubMsg: %+v", resp.ErrorResponse.SubMsg)
}
}
}

53
alipay/token/model.go Normal file
View File

@ -0,0 +1,53 @@
package token
import (
"fmt"
"gitea.cdlsxd.cn/BaseSystem/plugin/alipay"
"github.com/go-playground/validator/v10"
)
type Param struct {
AlipayRootCertSn string `json:"alipay_root_cert_sn"`
AppCertSn string `json:"app_cert_sn"`
AppId string `json:"app_id" validate:"required"`
Method string `json:"method" validate:"required"`
Format string `json:"format" validate:"required"`
Charset string `json:"charset" validate:"required"`
SignType string `json:"sign_type" validate:"required"`
Timestamp string `json:"timestamp" validate:"required"`
Version string `json:"version" validate:"required"`
BizContent string `json:"biz_content"`
Sign string `json:"sign"`
GrantType string `json:"grant_type"`
Code string `json:"code"`
}
type AlipayTokenSuccessResponse struct {
UserId string `json:"user_id"`
OpenId string `json:"open_id"`
AccessToken string `json:"access_token"`
ExpiresIn string `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
ReExpiresIn string `json:"re_expires_in"`
AuthStart string `json:"auth_start"`
}
type AlipayTokenResponse struct {
ErrorResponse *alipay.ErrorResponse `json:"error_response"`
Response *AlipayTokenSuccessResponse `json:"alipay_system_oauth_token_response"`
Sign string `json:"sign"`
}
func (req *Param) Validate() error {
err := validator.New().Struct(req)
if err != nil {
for _, err = range err.(validator.ValidationErrors) {
return fmt.Errorf("参数有误:" + err.Error())
}
}
return nil
}
func (r *AlipayTokenResponse) IsSuccess() bool {
return r.ErrorResponse == nil
}

67
alipay/utils/cert.go Normal file
View File

@ -0,0 +1,67 @@
package utils
import (
"fmt"
"sync"
)
type CertConfig struct {
MchCertSN string
RootCertSN string
PublicKey string
}
type manager struct {
once sync.Once
mutex sync.RWMutex
CertConfigs map[string]*CertConfig
}
var instance manager
func getCertConfig(appId string) *CertConfig {
c, ok := instance.CertConfigs[appId]
if !ok {
return nil
}
return c
}
func setCertConfig(appId string, certConfig *CertConfig) {
instance.mutex.Lock()
defer instance.mutex.Unlock()
instance.CertConfigs[appId] = certConfig
}
func init() {
instance.CertConfigs = make(map[string]*CertConfig)
}
func GetCert(mchCertPath, rootCertPath, PublicKeyPath, appId string) (*CertConfig, error) {
if mchCertPath == "" || rootCertPath == "" || appId == "" {
return nil, fmt.Errorf("mchCertPath or rootCertPath or appId is empty")
}
c := getCertConfig(appId)
if c != nil {
return nil, fmt.Errorf("appId %s already exists", appId)
}
mchCertSN, err := getMchCertSN(mchCertPath)
if err != nil {
return nil, fmt.Errorf("get mchCertSN error: %v", err)
}
rootCertSN, err := getRootCertSN(rootCertPath)
if err != nil {
return nil, fmt.Errorf("get rootCertSN error: %v", err)
}
publicKey, err := getPublicKey(PublicKeyPath)
if err != nil {
return nil, fmt.Errorf("get publicKey error: %v", err)
}
c = &CertConfig{
MchCertSN: mchCertSN,
RootCertSN: rootCertSN,
PublicKey: publicKey,
}
setCertConfig(appId, c)
return c, nil
}

53
alipay/utils/cert_test.go Normal file
View File

@ -0,0 +1,53 @@
package utils
import (
"testing"
)
func TestCertSN(t *testing.T) {
ms, err := getMchCertSN("/Users/lsxd/code/php/yxxt/market/config/alipaycash/appCertPublicKey_2021004100663111.crt")
if err != nil {
t.Error(err)
} else {
//78a57140055b8e7853b1576bcf763361
//78a57140055b8e7853b1576bcf763361
t.Logf("merchantCertSN%s", ms)
}
rs, err := getRootCertSN("/Users/lsxd/code/php/yxxt/market/config/alipaycash/alipayRootCert.crt")
if err != nil {
t.Error(err)
} else {
//687b59193f3f462dd5336e5abf83c5d8_02941eef3187dddf3d3b83462e1dfcf6
//687b59193f3f462dd5336e5abf83c5d8_02941eef3187dddf3d3b83462e1dfcf6
t.Logf("alipayRootCertSN%s", rs)
}
}
func TestMchCertSN(t *testing.T) {
s, err := getMchCertSN("/Users/lsxd/code/php/yxxt/market/config/alipaycash/appCertPublicKey_2021004100663111.crt")
if err != nil {
t.Error(err)
} else {
//78a57140055b8e7853b1576bcf763361
t.Log(s)
}
}
func TestRootCertSN(t *testing.T) {
s, err := getRootCertSN("/Users/lsxd/code/php/yxxt/market/config/alipaycash/alipayRootCert.crt")
if err != nil {
t.Error(err)
} else {
t.Log(s)
}
}
func TestPublicKey(t *testing.T) {
s, err := getPublicKey("/Users/lsxd/code/php/yxxt/market/config/alipaycash/alipayCertPublicKey_RSA2.crt")
if err != nil {
t.Error(err)
} else {
//78a57140055b8e7853b1576bcf763361
t.Log(s)
}
}

38
alipay/utils/common.go Normal file
View File

@ -0,0 +1,38 @@
package utils
import (
"crypto/md5"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"fmt"
"log"
)
// md5Hash 计算 MD5 哈希值
func md5Hash(s string) string {
h := md5.New()
h.Write([]byte(s))
return hex.EncodeToString(h.Sum(nil))
}
// hex2dec 将字节数组转换为十六进制字符串
func hex2dec(hex string) string {
var dec int
fmt.Sscanf(hex, "%x", &dec)
return fmt.Sprintf("%d", dec)
}
func getCert(certData []byte) (*x509.Certificate, error) {
block, _ := pem.Decode(certData)
if block == nil {
log.Fatal("Failed to parse PEM block containing the cert")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate: %v", err)
}
return cert, nil
}

22
alipay/utils/mch_cert.go Normal file
View File

@ -0,0 +1,22 @@
package utils
import (
"fmt"
"io/ioutil"
"log"
)
// getMchCertSN 提取证书序列号
func getMchCertSN(certPath string) (string, error) {
certData, err := ioutil.ReadFile(certPath)
if err != nil {
log.Fatalf("Failed to read the cert file: %s", err)
}
cert, err := getCert(certData)
if err != nil {
return "", fmt.Errorf("failed to parse certificate: %v", err)
}
return md5Hash(cert.Issuer.ToRDNSequence().String() + cert.SerialNumber.String()), nil
}

View File

@ -0,0 +1,51 @@
package utils
import (
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"strings"
)
// 从证书文件中提取公钥
func getPublicKey(certPath string) (string, error) {
// 读取证书文件内容
certData, err := ioutil.ReadFile(certPath)
if err != nil {
return "", fmt.Errorf("failed to read certificate file: %w", err)
}
// 解码 PEM 编码的证书
block, _ := pem.Decode(certData)
if block == nil {
return "", fmt.Errorf("failed to decode PEM block")
}
// 解析证书
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return "", fmt.Errorf("failed to parse certificate: %w", err)
}
// 获取公钥
pubKey := cert.PublicKey
// 转换公钥为 PEM 格式
pubKeyBytes, err := x509.MarshalPKIXPublicKey(pubKey)
if err != nil {
return "", fmt.Errorf("failed to marshal public key: %w", err)
}
// 创建 PEM 块
pemBlock := &pem.Block{
Type: "PUBLIC KEY",
Bytes: pubKeyBytes,
}
// 编码为 PEM 格式字符串
pemBytes := pem.EncodeToMemory(pemBlock)
publicKey := strings.TrimSpace(string(pemBytes))
return publicKey, nil
}

38
alipay/utils/root_cert.go Normal file
View File

@ -0,0 +1,38 @@
package utils
import (
"crypto/x509"
"fmt"
"io/ioutil"
"strings"
)
// getRootCertSN 计算证书的序列号并返回最终字符串
func getRootCertSN(certPath string) (string, error) {
certData, err := ioutil.ReadFile(certPath)
if err != nil {
return "", fmt.Errorf("failed to read certificate file: %v", err)
}
var sn string
blocks := strings.Split(string(certData), "-----END CERTIFICATE-----")
for _, blockStr := range blocks[:len(blocks)-1] {
cert, err := getCert([]byte(strings.TrimSpace(blockStr) + "\n-----END CERTIFICATE-----"))
if err != nil {
continue
}
serialNumber := cert.SerialNumber.String()
if strings.HasPrefix(serialNumber, "0x") {
serialNumber = hex2dec(serialNumber[2:])
}
if cert.SignatureAlgorithm == x509.SHA1WithRSA || cert.SignatureAlgorithm == x509.SHA256WithRSA {
hash := md5Hash(cert.Issuer.ToRDNSequence().String() + serialNumber)
if sn == "" {
sn = hash
} else {
sn += "_" + hash
}
}
}
return sn, nil
}

107
alipay/utils/sign.go Normal file
View File

@ -0,0 +1,107 @@
package utils
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"gitea.cdlsxd.cn/BaseSystem/plugin/alipay"
"gitea.cdlsxd.cn/BaseSystem/plugin/utils"
"net/url"
"strings"
)
func UrlValues(prk string, req alipay.Req) (url.Values, error) {
if err := req.Validate(); err != nil {
return nil, err
}
var strToBeSigned strings.Builder
uv := url.Values{}
kvRows := utils.SortStructJsonTag(req)
for _, kv := range kvRows {
if kv.Key == "sign" || kv.Value == "" {
continue
}
uv.Set(kv.Key, kv.Value)
strToBeSigned.WriteString(fmt.Sprintf("%s=%s&", kv.Key, kv.Value))
}
s := strings.TrimRight(strToBeSigned.String(), "&")
sign, err := Sign(s, []byte(utils.NewPrivate().Build(prk)))
if err != nil {
return nil, err
}
uv.Set("sign", sign)
return uv, nil
}
func Sign(data string, privateKeyPEM []byte) (string, error) {
block, _ := pem.Decode(privateKeyPEM)
if block == nil {
return "", errors.New("failed to parse PEM block containing the private key")
}
privyKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return "", fmt.Errorf("failed to parse DER encoded private key: %v", err)
}
hashed := sha256.Sum256([]byte(data))
signature, err := rsa.SignPKCS1v15(rand.Reader, privyKey.(*rsa.PrivateKey), crypto.SHA256, hashed[:])
if err != nil {
return "", fmt.Errorf("failed to sign: %v", err)
}
return base64.StdEncoding.EncodeToString(signature), nil
}
func Verify(n *alipay.Notify, publicKeyPEM string) (bool, error) {
var strToBeSigned strings.Builder
uv := url.Values{}
kvRows := utils.SortStructJsonTag(n)
for _, kv := range kvRows {
if kv.Key == "sign" || kv.Key == "sign_type" || kv.Value == "" {
continue
}
uv.Set(kv.Key, kv.Value)
strToBeSigned.WriteString(fmt.Sprintf("%s=%s&", kv.Key, kv.Value))
}
s := strings.TrimRight(strToBeSigned.String(), "&")
return check(s, n.Sign, []byte(publicKeyPEM))
}
func check(data, signature string, publicKeyPEM []byte) (bool, error) {
block, _ := pem.Decode(publicKeyPEM)
if block == nil {
return false, fmt.Errorf("failed to parse DER encoded public key")
}
pubKey, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return false, fmt.Errorf("failed to parse DER encoded public key: %v", err)
}
rsaPubKey, ok := pubKey.(*rsa.PublicKey)
if !ok {
return false, fmt.Errorf("failed to parse DER encoded public key: %v", err)
}
hashed := sha256.Sum256([]byte(data))
sig, err := base64.StdEncoding.DecodeString(signature)
if err != nil {
return false, fmt.Errorf("failed to decode signature: %v", err)
}
err = rsa.VerifyPKCS1v15(rsaPubKey, crypto.SHA256, hashed[:], sig)
if err != nil {
fmt.Println("signature verification error:", err)
return false, nil
}
return true, nil
}

9
alipay/vo/alipay.go Normal file
View File

@ -0,0 +1,9 @@
package vo
type Code string
const CodeSuccess Code = "10000"
func (c Code) IsSuccess() bool {
return c == CodeSuccess
}

10
go.mod
View File

@ -3,7 +3,7 @@ module gitea.cdlsxd.cn/BaseSystem/plugin
go 1.22.2
require (
github.com/envoyproxy/protoc-gen-validate v1.0.4
github.com/carlmjohnson/requests v0.24.2
github.com/go-playground/validator/v10 v10.22.0
github.com/hashicorp/go-plugin v1.6.1
github.com/pkg/errors v0.9.1
@ -24,9 +24,9 @@ require (
github.com/mattn/go-isatty v0.0.10 // indirect
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 // indirect
github.com/oklog/run v1.0.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect
)

20
go.sum
View File

@ -1,9 +1,9 @@
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
github.com/carlmjohnson/requests v0.24.2 h1:JDakhAmTIKL/qL/1P7Kkc2INGBJIkIFP6xUeUmPzLso=
github.com/carlmjohnson/requests v0.24.2/go.mod h1:duYA/jDnyZ6f3xbcF5PpZ9N8clgopubP2nK5i6MVMhU=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A=
github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
@ -46,16 +46,16 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=