Files
lone-services/pkg/wechat/pay/certificate.go
T
2026-09-11 16:08:17 +08:00

150 lines
3.7 KiB
Go

package pay
import (
"context"
"crypto/rsa"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"net/http"
"os"
"strings"
)
const certificatesPath = "/v3/certificates"
type certificatesResponse struct {
Data []platformCertificateData `json:"data"`
}
type platformCertificateData struct {
SerialNo string `json:"serial_no"`
EffectiveTime string `json:"effective_time"`
ExpireTime string `json:"expire_time"`
EncryptCertificate encryptCertificate `json:"encrypt_certificate"`
}
type encryptCertificate struct {
Algorithm string `json:"algorithm"`
Nonce string `json:"nonce"`
AssociatedData string `json:"associated_data"`
Ciphertext string `json:"ciphertext"`
}
func certSerialNumber(cert *x509.Certificate) string {
return strings.ToUpper(cert.SerialNumber.Text(16))
}
func loadPlatformCertificate(path string) (string, *rsa.PublicKey, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", nil, fmt.Errorf("read platform certificate %s: %w", path, err)
}
block, _ := pem.Decode(data)
if block == nil {
return "", nil, fmt.Errorf("invalid platform certificate pem: %s", path)
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return "", nil, fmt.Errorf("parse platform certificate %s: %w", path, err)
}
publicKey, ok := cert.PublicKey.(*rsa.PublicKey)
if !ok {
return "", nil, fmt.Errorf("platform certificate public key is not rsa: %s", path)
}
return certSerialNumber(cert), publicKey, nil
}
func (c *Client) addPlatformCert(serial string, publicKey *rsa.PublicKey) {
if c.platformCerts == nil {
c.platformCerts = make(map[string]*rsa.PublicKey)
}
c.platformCerts[strings.ToUpper(serial)] = publicKey
if c.platformSerial == "" {
c.platformSerial = strings.ToUpper(serial)
}
}
func (c *Client) refreshPlatformCertificates(ctx context.Context) error {
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, c.Host+certificatesPath, nil)
if err != nil {
return err
}
httpReq.Header.Set("Accept", "application/json")
httpReq.Header.Set("User-Agent", "lone-services-wechatpay")
authorization, err := c.buildAuthorization(http.MethodGet, certificatesPath, nil)
if err != nil {
return err
}
httpReq.Header.Set("Authorization", authorization)
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if !isSuccessStatus(resp.StatusCode) {
return newAPIError(resp.StatusCode, respBody)
}
if len(c.platformCerts) > 0 {
if err := c.validateResponse(resp.Header, respBody); err != nil {
return fmt.Errorf("validate certificates response: %w", err)
}
}
var result certificatesResponse
if err := json.Unmarshal(respBody, &result); err != nil {
return err
}
for _, item := range result.Data {
plaintext, err := decryptAES256GCM(
c.APIv3Key,
item.EncryptCertificate.AssociatedData,
item.EncryptCertificate.Nonce,
item.EncryptCertificate.Ciphertext,
)
if err != nil {
return fmt.Errorf("decrypt platform certificate %s: %w", item.SerialNo, err)
}
block, _ := pem.Decode(plaintext)
if block == nil {
return fmt.Errorf("invalid decrypted platform certificate pem: %s", item.SerialNo)
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return fmt.Errorf("parse platform certificate %s: %w", item.SerialNo, err)
}
publicKey, ok := cert.PublicKey.(*rsa.PublicKey)
if !ok {
return fmt.Errorf("platform certificate public key is not rsa: %s", item.SerialNo)
}
c.addPlatformCert(item.SerialNo, publicKey)
}
if len(c.platformCerts) == 0 {
return fmt.Errorf("no platform certificates available")
}
return nil
}