a6b068b070
照 alipay adapter 的 RSA + redirect/qr + 表单请求模式实现哪吒(nzzf.org)聚合支付: SHA256WithRSA + Base64 签名(商户私钥出站签名/平台公钥验响应及回调),Create 走 POST /api/pay/create,VerifyCallback 处理 GET 异步通知(handler 层新增 Query 透传 + nezha 专属纯文本 success/fail 回复),Query 走 POST /api/pay/query。BuildRegistry 按凭证齐备与否决策注册,缺凭证只 log 跳过不 fatal。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
137 lines
4.2 KiB
Go
137 lines
4.2 KiB
Go
package nezha
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// buildSignSource 实现哪吒 sign_note 的签名串规则:收集所有*非空*参数,排除
|
|
// sign/sign_type/数组参数(调用方在构造 params 时就不应放入非标量值),按参数名
|
|
// ASCII 升序排序,拼成 "key=value&key2=value2"(不拼商户密钥、不做任何 URL 编码)。
|
|
// sort.Strings 是逐字节比较,对本协议里全 ASCII 的参数名等价于 ASCII 排序。
|
|
func buildSignSource(params map[string]string) string {
|
|
keys := make([]string, 0, len(params))
|
|
for k, v := range params {
|
|
if k == "sign" || k == "sign_type" || v == "" {
|
|
continue
|
|
}
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
parts := make([]string, 0, len(keys))
|
|
for _, k := range keys {
|
|
parts = append(parts, k+"="+params[k])
|
|
}
|
|
return strings.Join(parts, "&")
|
|
}
|
|
|
|
// signRSA 对签名串做 SHA256WithRSA + Base64(sign_note 规定算法)。
|
|
func signRSA(priv *rsa.PrivateKey, source string) (string, error) {
|
|
h := sha256.Sum256([]byte(source))
|
|
sig, err := rsa.SignPKCS1v15(rand.Reader, priv, crypto.SHA256, h[:])
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return base64.StdEncoding.EncodeToString(sig), nil
|
|
}
|
|
|
|
// verifyRSA 验 SHA256WithRSA + Base64 签名。签名不合法/验签失败均返回 error。
|
|
func verifyRSA(pub *rsa.PublicKey, source, sigB64 string) error {
|
|
sig, err := base64.StdEncoding.DecodeString(sigB64)
|
|
if err != nil {
|
|
return fmt.Errorf("nezha: sign 不是合法 base64: %w", err)
|
|
}
|
|
h := sha256.Sum256([]byte(source))
|
|
return rsa.VerifyPKCS1v15(pub, crypto.SHA256, h[:], sig)
|
|
}
|
|
|
|
// parsePrivateKey 解析商户 RSA 私钥,兼容 PKCS1/PKCS8、PEM 头或裸 base64 DER
|
|
// (与 alipay 账户凭证"无 PEM 头亦可"的既有惯例对齐,详见 config.AlipaySandboxConfig
|
|
// 字段注释)。
|
|
func parsePrivateKey(s string) (*rsa.PrivateKey, error) {
|
|
der, err := pemOrBase64ToDER(s)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if key, perr := x509.ParsePKCS8PrivateKey(der); perr == nil {
|
|
rsaKey, ok := key.(*rsa.PrivateKey)
|
|
if !ok {
|
|
return nil, fmt.Errorf("nezha: 私钥不是 RSA 类型(PKCS8)")
|
|
}
|
|
return rsaKey, nil
|
|
}
|
|
if key, perr := x509.ParsePKCS1PrivateKey(der); perr == nil {
|
|
return key, nil
|
|
}
|
|
return nil, fmt.Errorf("nezha: 无法解析商户私钥(需 PKCS1/PKCS8,PEM 或裸 base64 DER)")
|
|
}
|
|
|
|
// parsePublicKey 解析平台 RSA 公钥,兼容 PKIX/PKCS1、PEM 头或裸 base64 DER。
|
|
func parsePublicKey(s string) (*rsa.PublicKey, error) {
|
|
der, err := pemOrBase64ToDER(s)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if key, perr := x509.ParsePKIXPublicKey(der); perr == nil {
|
|
rsaKey, ok := key.(*rsa.PublicKey)
|
|
if !ok {
|
|
return nil, fmt.Errorf("nezha: 公钥不是 RSA 类型(PKIX)")
|
|
}
|
|
return rsaKey, nil
|
|
}
|
|
if key, perr := x509.ParsePKCS1PublicKey(der); perr == nil {
|
|
return key, nil
|
|
}
|
|
return nil, fmt.Errorf("nezha: 无法解析平台公钥(需 PKIX/PKCS1,PEM 或裸 base64 DER)")
|
|
}
|
|
|
|
func pemOrBase64ToDER(s string) ([]byte, error) {
|
|
s = strings.TrimSpace(s)
|
|
if block, _ := pem.Decode([]byte(s)); block != nil {
|
|
return block.Bytes, nil
|
|
}
|
|
clean := strings.Join(strings.Fields(s), "")
|
|
der, err := base64.StdEncoding.DecodeString(clean)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("nezha: 密钥既非 PEM 也非合法 base64: %w", err)
|
|
}
|
|
return der, nil
|
|
}
|
|
|
|
// stringifyParams 把渠道 JSON 响应(map[string]any,数字统一是 float64)拍平成
|
|
// map[string]string 供 buildSignSource 用;数组/对象/nil 一律丢弃(sign_note 排除
|
|
// 数组参数,响应里也不应出现需要参与签名的复合结构)。
|
|
func stringifyParams(raw map[string]any) map[string]string {
|
|
out := make(map[string]string, len(raw))
|
|
for k, v := range raw {
|
|
switch t := v.(type) {
|
|
case string:
|
|
out[k] = t
|
|
case float64:
|
|
out[k] = strconv.FormatFloat(t, 'f', -1, 64)
|
|
case bool:
|
|
out[k] = strconv.FormatBool(t)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// verifyResponseSign 用平台公钥验渠道 JSON 响应(下单/查单)自带的 sign。
|
|
func verifyResponseSign(pub *rsa.PublicKey, raw map[string]any) error {
|
|
sig, _ := raw["sign"].(string)
|
|
if sig == "" {
|
|
return fmt.Errorf("响应缺少 sign 字段")
|
|
}
|
|
source := buildSignSource(stringifyParams(raw))
|
|
return verifyRSA(pub, source, sig)
|
|
}
|