package payment import ( "crypto" "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/pem" "errors" "fmt" "os" "strings" "backend/internal/model" "gorm.io/gorm" ) // LoadWaffoWebhookVerifyKey 读取 webhook 验签材料:环境变量优先,其次网关 WebhookSecret // Logan.Li func LoadWaffoWebhookVerifyKey(db *gorm.DB) string { if v := strings.TrimSpace(os.Getenv("WAFFO_WEBHOOK_PUBLIC_KEY")); v != "" { return v } if v := strings.TrimSpace(os.Getenv("WAFFO_WEBHOOK_SECRET")); v != "" { return v } if db == nil { return "" } var gw model.PaymentGatewayConfig if err := db.Where("gateway = ?", "waffo").First(&gw).Error; err != nil { return "" } return strings.TrimSpace(gw.WebhookSecret) } // VerifyWaffoWebhookSignature 校验 Waffo webhook:RSA PKCS1v15(SHA256(body)) 或 HMAC 共享密钥 // Logan.Li func VerifyWaffoWebhookSignature(verifyKey string, body []byte, sigHeader string) error { verifyKey = strings.TrimSpace(verifyKey) sigHeader = strings.TrimSpace(sigHeader) if verifyKey == "" { return errors.New("未配置 webhook 验签公钥/密钥") } if sigHeader == "" { return errors.New("缺少 X-Waffo-Signature") } // 共享密钥模式:签名头即为密钥(或 Bearer 同值) // Logan.Li if !looksLikePEMOrDER(verifyKey) { secret := strings.TrimPrefix(sigHeader, "Bearer ") if subtleConstantTimeEq(secret, verifyKey) || subtleConstantTimeEq(sigHeader, verifyKey) { return nil } return errors.New("webhook 共享密钥不匹配") } pub, err := parseRSAPublicKey(verifyKey) if err != nil { return fmt.Errorf("webhook 验签公钥无效: %w", err) } sig, err := base64.StdEncoding.DecodeString(sigHeader) if err != nil { // 兼容 URL-safe base64 // Logan.Li sig, err = base64.RawURLEncoding.DecodeString(sigHeader) if err != nil { return errors.New("签名格式错误") } } sum := sha256.Sum256(body) if err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, sum[:], sig); err != nil { return errors.New("webhook 签名校验失败") } return nil } func looksLikePEMOrDER(s string) bool { return strings.Contains(s, "BEGIN") || len(s) > 80 } func subtleConstantTimeEq(a, b string) bool { if len(a) != len(b) { return false } var v byte for i := 0; i < len(a); i++ { v |= a[i] ^ b[i] } return v == 0 } func parseRSAPublicKey(raw string) (*rsa.PublicKey, error) { raw = strings.TrimSpace(raw) raw = strings.ReplaceAll(raw, `\n`, "\n") if block, _ := pem.Decode([]byte(raw)); block != nil { return parseRSAPublicKeyBytes(block.Bytes) } compact := strings.Map(func(r rune) rune { if r == '\n' || r == '\r' || r == ' ' || r == '\t' { return -1 } return r }, raw) if decoded, err := base64.StdEncoding.DecodeString(compact); err == nil { if pub, err2 := parseRSAPublicKeyBytes(decoded); err2 == nil { return pub, nil } } wrapped := "-----BEGIN PUBLIC KEY-----\n" + chunkBase64(compact, 64) + "\n-----END PUBLIC KEY-----\n" if block, _ := pem.Decode([]byte(wrapped)); block != nil { return parseRSAPublicKeyBytes(block.Bytes) } return nil, errors.New("无法解析 RSA 公钥") } func parseRSAPublicKeyBytes(der []byte) (*rsa.PublicKey, error) { if pub, err := x509.ParsePKIXPublicKey(der); err == nil { if rsaPub, ok := pub.(*rsa.PublicKey); ok { return rsaPub, nil } return nil, errors.New("公钥不是 RSA") } if pub, err := x509.ParsePKCS1PublicKey(der); err == nil { return pub, nil } return nil, errors.New("不支持的公钥格式") }