fix: 按评审报告止血支付漏洞并加固授权链路

废除浏览器回跳自助结算,通用 webhook 强制密钥鉴权,Waffo 支持验签与未知事件忽略;重构订单状态机/激活占座/SQLite WAL,并补备份、readyz、限流与前端导出/登录页安全修复。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Logan.LiandCursor committed 2026-08-12 21:26:58 +08:00
1 parent 8cc2c83f9d
commit bac72e6945
24 files changed
+682 -391

No files matched your search

@@ -5,6 +5,7 @@ import (
"backend/internal/model"
"backend/utils"
"errors"
"strings"
"time"
"gorm.io/gorm"
@@ -134,7 +135,7 @@ func (s *EntitlementService) Renew(id, planID uint) (*dto.EntitlementResponse, e
return &resp, nil
}
// GetByEmail 按邮箱查询客户权益(客户端接口) // Logan.Li
// GetByEmail 按邮箱查询客户权益(客户端接口;不回传完整 License Key) // Logan.Li
func (s *EntitlementService) GetByEmail(email string) ([]dto.EntitlementResponse, error) {
var customer model.Customer
if err := s.db.Where("email = ?", email).First(&customer).Error; err != nil {
@@ -148,11 +149,25 @@ func (s *EntitlementService) GetByEmail(email string) ([]dto.EntitlementResponse
results := make([]dto.EntitlementResponse, 0, len(entitlements))
for _, e := range entitlements {
results = append(results, s.toResponse(e))
resp := s.toResponse(e)
resp.LicenseKey = maskLicenseKey(resp.LicenseKey)
results = append(results, resp)
}
return results, nil
}
// maskLicenseKey 脱敏展示,避免公开接口泄露完整密钥 // Logan.Li
func maskLicenseKey(key string) string {
key = strings.TrimSpace(key)
if len(key) <= 8 {
if key == "" {
return ""
}
return "****"
}
return key[:4] + "****" + key[len(key)-4:]
}
// toResponse 模型转响应 DTO // Logan.Li
func (s *EntitlementService) toResponse(e model.CustomerEntitlement) dto.EntitlementResponse {
resp := dto.EntitlementResponse{
@@ -6,6 +6,8 @@ import (
"backend/utils"
"errors"
"time"
"gorm.io/gorm"
)
func inferPlanType(lic model.License) string {
@@ -15,12 +17,13 @@ func inferPlanType(lic model.License) string {
if lic.Duration <= 0 || lic.EndDate.Equal(model.PermanentEndDate) || lic.EndDate.Year() >= 2099 {
return "perpetual"
}
if lic.Duration <= 180 {
return "trial"
}
// 先判月度,再判体验/包年,避免 monthly 成为死代码 // Logan.Li
if lic.Duration <= 31 {
return "monthly"
}
if lic.Duration <= 180 {
return "trial"
}
return "yearly"
}
@@ -75,75 +78,112 @@ func (s *LicenseService) Verify(req dto.LicenseVerifyRequest) (*dto.LicenseVerif
}, nil
}
// Activate 客户端激活 License(机器绑定) // Logan.Li
// Activate 客户端激活 License(事务 + 条件占座,消除 TOCTOU) // Logan.Li
func (s *LicenseService) Activate(req dto.LicenseActivateRequest, clientIP string) (map[string]interface{}, error) {
var lic model.License
if err := s.db.Where("license_key = ?", req.LicenseKey).First(&lic).Error; err != nil {
return nil, errors.New("无效的 License")
}
var payload map[string]interface{}
err := s.db.Transaction(func(tx *gorm.DB) error {
var lic model.License
if err := tx.Where("license_key = ?", req.LicenseKey).First(&lic).Error; err != nil {
return errors.New("无效的 License")
}
if lic.Status == model.LicenseStatusRevoked || lic.Status == model.LicenseStatusSuspended {
return errors.New("License 已被吊销或暂停")
}
if lic.Status == model.LicenseStatusExpired || utils.IsLicenseExpired(lic.EndDate) {
return errors.New("License 已过期")
}
if lic.Status == model.LicenseStatusRevoked {
return nil, errors.New("License 已被吊销")
}
if lic.Status == model.LicenseStatusExpired || utils.IsLicenseExpired(lic.EndDate) {
return nil, errors.New("License 已过期")
}
now := time.Now()
var existing model.Activation
if err := tx.Where("license_id = ? AND device_id = ?", lic.ID, req.DeviceID).
First(&existing).Error; err == nil {
updates := map[string]interface{}{
"last_seen_at": now,
"is_online": true,
"device_name": req.DeviceName,
"device_info": req.DeviceInfo,
"ip": clientIP,
}
if existing.Status != "active" {
updates["status"] = "active"
res := tx.Model(&model.License{}).
Where("id = ? AND current_activations < max_activations", lic.ID).
Update("current_activations", gorm.Expr("current_activations + 1"))
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 0 {
return errors.New("已达到最大激活数量限制")
}
}
if err := tx.Model(&existing).Updates(updates).Error; err != nil {
return err
}
if err := tx.First(&lic, lic.ID).Error; err != nil {
return err
}
payload = licenseClientPayload(lic)
return nil
}
var existing model.Activation
if err := s.db.Where("license_id = ? AND device_id = ?", lic.ID, req.DeviceID).First(&existing).Error; err == nil {
existing.LastSeenAt = time.Now()
existing.IsOnline = true
existing.DeviceName = req.DeviceName
existing.DeviceInfo = req.DeviceInfo
existing.IP = clientIP
s.db.Save(&existing)
return licenseClientPayload(lic), nil
}
res := tx.Model(&model.License{}).
Where("id = ? AND current_activations < max_activations", lic.ID).
Updates(map[string]interface{}{
"current_activations": gorm.Expr("current_activations + 1"),
"status": model.LicenseStatusActive,
})
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 0 {
return errors.New("已达到最大激活数量限制")
}
if lic.CurrentActivations >= lic.MaxActivations {
return nil, errors.New("已达到最大激活数量限制")
if err := tx.Create(&model.Activation{
LicenseID: lic.ID,
DeviceID: req.DeviceID,
DeviceName: req.DeviceName,
DeviceInfo: req.DeviceInfo,
IP: clientIP,
Status: "active",
IsOnline: true,
ActivatedAt: now,
LastSeenAt: now,
}).Error; err != nil {
return errors.New("激活失败")
}
if err := tx.First(&lic, lic.ID).Error; err != nil {
return err
}
payload = licenseClientPayload(lic)
return nil
})
if err != nil {
return nil, err
}
activation := model.Activation{
LicenseID: lic.ID,
DeviceID: req.DeviceID,
DeviceName: req.DeviceName,
DeviceInfo: req.DeviceInfo,
IP: clientIP,
Status: "active",
IsOnline: true,
ActivatedAt: time.Now(),
LastSeenAt: time.Now(),
}
if err := s.db.Create(&activation).Error; err != nil {
return nil, errors.New("激活失败")
}
lic.CurrentActivations++
if lic.Status == model.LicenseStatusInactive {
lic.Status = model.LicenseStatusActive
}
s.db.Save(&lic)
return licenseClientPayload(lic), nil
return payload, nil
}
// Deactivate 客户端停用 // Logan.Li
// Deactivate 客户端停用(仅 active→inactive 才减席位) // Logan.Li
func (s *LicenseService) Deactivate(req dto.LicenseDeactivateRequest) error {
var lic model.License
if err := s.db.Where("license_key = ?", req.LicenseKey).First(&lic).Error; err != nil {
return errors.New("无效的 License")
}
result := s.db.Model(&model.Activation{}).
Where("license_id = ? AND device_id = ?", lic.ID, req.DeviceID).
Update("status", "inactive")
if result.RowsAffected > 0 && lic.CurrentActivations > 0 {
lic.CurrentActivations--
s.db.Save(&lic)
}
return nil
return s.db.Transaction(func(tx *gorm.DB) error {
var lic model.License
if err := tx.Where("license_key = ?", req.LicenseKey).First(&lic).Error; err != nil {
return errors.New("无效的 License")
}
res := tx.Model(&model.Activation{}).
Where("license_id = ? AND device_id = ? AND status = ?", lic.ID, req.DeviceID, "active").
Updates(map[string]interface{}{"status": "inactive", "is_online": false})
if res.Error != nil {
return res.Error
}
if res.RowsAffected > 0 {
return tx.Model(&model.License{}).
Where("id = ? AND current_activations > 0", lic.ID).
Update("current_activations", gorm.Expr("current_activations - 1")).Error
}
return nil
})
}
// Heartbeat 客户端心跳 // Logan.Li
@@ -153,8 +193,8 @@ func (s *LicenseService) Heartbeat(req dto.LicenseHeartbeatRequest) (map[string]
return nil, errors.New("无效的 License")
}
if lic.Status == model.LicenseStatusRevoked {
return nil, errors.New("License 已被吊销")
if lic.Status == model.LicenseStatusRevoked || lic.Status == model.LicenseStatusSuspended {
return nil, errors.New("License 已被吊销或暂停")
}
if lic.Status == model.LicenseStatusExpired || utils.IsLicenseExpired(lic.EndDate) {
return nil, errors.New("License 已过期")
@@ -183,4 +223,3 @@ func (s *LicenseService) ListDevicesByKey(licenseKey string) ([]dto.ActivationRe
}
return results, lic.MaxActivations, nil
}
+130 -155
View File
@@ -7,6 +7,7 @@ import (
"backend/utils"
"errors"
"fmt"
"math"
"strings"
"time"
@@ -193,125 +194,72 @@ func (s *OrderService) Checkout(req dto.CheckoutRequest, clientIP, userAgent str
}, clientIP, userAgent)
}
// ClaimPurchase fetch license after payment by order_no or email+product. // Logan.Li
// ClaimPurchase 领取授权:必须同时提供 order_no + customer_email,防止仅凭邮箱取证 // Logan.Li
func (s *OrderService) ClaimPurchase(req dto.ClaimPurchaseRequest) (*dto.ClaimPurchaseResponse, error) {
orderNo := strings.TrimSpace(req.OrderNo)
email := strings.TrimSpace(req.CustomerEmail)
productCode := strings.TrimSpace(req.ProductCode)
if productCode == "" {
productCode = "opsxshell"
if orderNo == "" || email == "" {
return nil, errors.New("请同时提供 order_no 与 customer_email")
}
if orderNo != "" {
var order model.Order
if err := s.db.Where("order_no = ?", orderNo).First(&order).Error; err != nil {
return nil, errors.New("订单不存在")
}
switch order.Status {
case "pending":
return &dto.ClaimPurchaseResponse{
OrderNo: order.OrderNo,
Status: "pending",
Message: "订单尚未支付完成,请稍后再试",
}, nil
case "paid":
licenseKey := ""
if order.LicenseID > 0 {
var lic model.License
if err := s.db.First(&lic, order.LicenseID).Error; err == nil {
licenseKey = lic.LicenseKey
}
}
if licenseKey == "" {
var ent model.CustomerEntitlement
if err := s.db.Where("customer_id = ? AND product_id = ? AND status = ?", order.CustomerID, order.ProductID, "active").
First(&ent).Error; err == nil {
licenseKey = ent.LicenseKey
}
}
if licenseKey == "" {
return &dto.ClaimPurchaseResponse{
OrderNo: order.OrderNo,
Status: "paid",
Message: "订单已支付,但 License 尚未就绪",
}, nil
}
return &dto.ClaimPurchaseResponse{
LicenseKey: licenseKey,
OrderNo: order.OrderNo,
Status: "paid",
Message: "ok",
}, nil
default:
return &dto.ClaimPurchaseResponse{
OrderNo: order.OrderNo,
Status: order.Status,
Message: fmt.Sprintf("订单状态为 %s,无法领取授权", order.Status),
}, nil
var order model.Order
if err := s.db.Preload("Customer").Where("order_no = ?", orderNo).First(&order).Error; err != nil {
return nil, errors.New("订单不存在")
}
if order.Customer.ID == 0 {
if err := s.db.First(&order.Customer, order.CustomerID).Error; err != nil {
return nil, errors.New("订单客户不存在")
}
}
if email == "" {
return nil, errors.New("请提供 order_no 或 customer_email")
if !strings.EqualFold(strings.TrimSpace(order.Customer.Email), email) {
return nil, errors.New("订单与邮箱不匹配")
}
var product model.Product
if err := s.db.Where("code = ?", productCode).First(&product).Error; err != nil {
return nil, errors.New("产品不存在")
}
var customer model.Customer
if err := s.db.Where("email = ?", email).First(&customer).Error; err != nil {
switch order.Status {
case "pending":
return &dto.ClaimPurchaseResponse{
Status: "not_found",
Message: "未找到购买记录",
}, nil
}
var ent model.CustomerEntitlement
if err := s.db.Where("customer_id = ? AND product_id = ?", customer.ID, product.ID).
Order("updated_at DESC").First(&ent).Error; err != nil {
// 可能有未支付订单 // Logan.Li
var pending model.Order
if err2 := s.db.Where("customer_id = ? AND product_id = ? AND status = ?", customer.ID, product.ID, "pending").
Order("created_at DESC").First(&pending).Error; err2 == nil {
return &dto.ClaimPurchaseResponse{
OrderNo: pending.OrderNo,
Status: "pending",
Message: "订单尚未支付完成,请稍后再试",
}, nil
}
return &dto.ClaimPurchaseResponse{
Status: "not_found",
Message: "未找到购买记录",
}, nil
}
if ent.Status == "active" && ent.LicenseKey != "" {
return &dto.ClaimPurchaseResponse{
LicenseKey: ent.LicenseKey,
Status: "paid",
Message: "ok",
}, nil
}
var pending model.Order
if err := s.db.Where("customer_id = ? AND product_id = ? AND status = ?", customer.ID, product.ID, "pending").
Order("created_at DESC").First(&pending).Error; err == nil {
return &dto.ClaimPurchaseResponse{
OrderNo: pending.OrderNo,
OrderNo: order.OrderNo,
Status: "pending",
Message: "订单尚未支付完成,请稍后再试",
}, nil
case "paid":
licenseKey := ""
if order.LicenseID > 0 {
var lic model.License
if err := s.db.First(&lic, order.LicenseID).Error; err == nil {
licenseKey = lic.LicenseKey
}
}
if licenseKey == "" {
var ent model.CustomerEntitlement
if err := s.db.Where("customer_id = ? AND product_id = ? AND status = ?", order.CustomerID, order.ProductID, "active").
First(&ent).Error; err == nil {
licenseKey = ent.LicenseKey
}
}
if licenseKey == "" {
return &dto.ClaimPurchaseResponse{
OrderNo: order.OrderNo,
Status: "paid",
Message: "订单已支付,但 License 尚未就绪",
}, nil
}
return &dto.ClaimPurchaseResponse{
LicenseKey: licenseKey,
OrderNo: order.OrderNo,
Status: "paid",
Message: "ok",
}, nil
default:
return &dto.ClaimPurchaseResponse{
OrderNo: order.OrderNo,
Status: order.Status,
Message: fmt.Sprintf("订单状态为 %s,无法领取授权", order.Status),
}, nil
}
return &dto.ClaimPurchaseResponse{
Status: ent.Status,
Message: "暂无可用授权",
}, nil
}
// resolveWebhookOrder finds local order by merchant external id, Waffo order id, or buyer email. // Logan.Li
// resolveWebhookOrder 仅按 order_no / gateway_order_no 匹配,禁止邮箱兜底劫持 // Logan.Li
func (s *OrderService) resolveWebhookOrder(req dto.PaymentWebhookRequest) (model.Order, error) {
var order model.Order
if no := strings.TrimSpace(req.OrderNo); no != "" {
@@ -328,46 +276,72 @@ func (s *OrderService) resolveWebhookOrder(req dto.PaymentWebhookRequest) (model
return order, nil
}
}
if email := strings.TrimSpace(req.CustomerEmail); email != "" {
var customer model.Customer
if err := s.db.Where("email = ?", email).First(&customer).Error; err == nil {
if err := s.db.Where("customer_id = ? AND status = ?", customer.ID, "pending").
Order("created_at DESC").First(&order).Error; err == nil {
return order, nil
}
}
}
return order, errors.New("订单不存在")
}
// PaymentWebhook 支付回调(幂等 + 事务) // Logan.Li
// PaymentWebhook 支付回调:原子状态流转 + 金额校验 + 退款可达 // Logan.Li
func (s *OrderService) PaymentWebhook(req dto.PaymentWebhookRequest, clientIP string) error {
status := strings.ToLower(strings.TrimSpace(req.Status))
if status == "completed" {
status = "paid"
}
switch status {
case "paid", "failed", "cancelled", "refunded":
default:
return nil // 未识别状态直接忽略 // Logan.Li
}
order, err := s.resolveWebhookOrder(req)
if err != nil {
return err
}
if order.Status == "paid" || order.Status == "refunded" {
return nil // 已处理,幂等返回
// 金额/货币校验(有金额时) // Logan.Li
if req.Amount > 0 && (math.Abs(req.Amount-order.Amount) > 0.005 ||
(req.Currency != "" && !strings.EqualFold(req.Currency, order.Currency))) {
utils.LogSystemAction(s.db, "payment_amount_mismatch", "order", order.ID, order.OrderNo,
fmt.Sprintf("webhook %.2f%s 与订单 %.2f%s 不符", req.Amount, req.Currency, order.Amount, order.Currency), clientIP)
return errors.New("支付金额与订单不符")
}
// 事务处理 // Logan.Li
return s.db.Transaction(func(tx *gorm.DB) error {
now := time.Now()
switch req.Status {
case "paid", "completed":
// 更新订单 // Logan.Li
order.Status = "paid"
order.GatewayOrderNo = req.GatewayOrderNo
order.PaymentGateway = req.Gateway
order.PaymentMethod = req.Gateway
order.PaidAt = &now
if err := tx.Save(&order).Error; err != nil {
return err
}
now := time.Now()
updates := map[string]interface{}{
"payment_gateway": req.Gateway,
"payment_method": req.Gateway,
}
if g := strings.TrimSpace(req.GatewayOrderNo); g != "" {
updates["gateway_order_no"] = g
}
switch status {
case "paid":
updates["status"] = "paid"
updates["paid_at"] = now
case "refunded":
updates["status"] = "refunded"
updates["refund_at"] = now
default:
updates["status"] = status
}
// 发放 License // Logan.Li
// 原子状态流转:refund 只允许 paid→refunded;paid/failed/cancelled 只允许非终态 // Logan.Li
res := s.db.Model(&model.Order{}).Where("id = ?", order.ID).
Where("(status NOT IN ('paid','refunded') AND ? IN ('paid','failed','cancelled')) OR (status = 'paid' AND ? = 'refunded')",
status, status).
Updates(updates)
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 0 {
return nil // 幂等:已处理或非法流转 // Logan.Li
}
return s.db.Transaction(func(tx *gorm.DB) error {
switch status {
case "paid":
var plan model.Plan
tx.First(&plan, order.PlanID)
if err := tx.First(&plan, order.PlanID).Error; err != nil {
return fmt.Errorf("plan 不存在: %w", err)
}
var validUntil *time.Time
var endDate time.Time
@@ -391,14 +365,14 @@ func (s *OrderService) PaymentWebhook(req dto.PaymentWebhookRequest, clientIP st
if err := tx.Create(&lic).Error; err != nil {
return err
}
if err := tx.Model(&model.Order{}).Where("id = ?", order.ID).
Update("license_id", lic.ID).Error; err != nil {
return err
}
order.LicenseID = lic.ID
tx.Save(&order)
// 更新权益记录(防重复购买核心) // Logan.Li
renewable := plan.PlanType != "perpetual" && plan.PlanType != "lifetime"
var ent model.CustomerEntitlement
entResult := tx.Where("customer_id = ? AND product_id = ?", order.CustomerID, order.ProductID).First(&ent)
renewable := plan.PlanType != "perpetual" && plan.PlanType != "lifetime"
if entResult.Error != nil {
ent = model.CustomerEntitlement{
CustomerID: order.CustomerID,
@@ -412,7 +386,9 @@ func (s *OrderService) PaymentWebhook(req dto.PaymentWebhookRequest, clientIP st
ValidUntil: validUntil,
Renewable: renewable,
}
tx.Create(&ent)
if err := tx.Create(&ent).Error; err != nil {
return err
}
} else {
ent.PlanID = order.PlanID
ent.LicenseID = lic.ID
@@ -422,31 +398,30 @@ func (s *OrderService) PaymentWebhook(req dto.PaymentWebhookRequest, clientIP st
ent.ValidFrom = now
ent.ValidUntil = validUntil
ent.Renewable = renewable
tx.Save(&ent)
if err := tx.Save(&ent).Error; err != nil {
return err
}
}
utils.LogSystemAction(tx, "payment_completed", "order", order.ID, order.OrderNo,
fmt.Sprintf("订单支付成功,已发放 License: %s", lic.LicenseKey), clientIP)
case "failed", "cancelled":
order.Status = req.Status
if err := tx.Save(&order).Error; err != nil {
return err
}
case "refunded":
order.Status = "refunded"
order.RefundAt = &now
if err := tx.Save(&order).Error; err != nil {
return err
}
// 吊销关联 License // Logan.Li
if order.LicenseID > 0 {
tx.Model(&model.License{}).Where("id = ?", order.LicenseID).
Updates(map[string]interface{}{"status": model.LicenseStatusRevoked, "revoke_reason": "订单退款"})
tx.Model(&model.CustomerEntitlement{}).Where("license_id = ?", order.LicenseID).
Update("status", "revoked")
if err := tx.Model(&model.License{}).Where("id = ?", order.LicenseID).
Updates(map[string]interface{}{
"status": model.LicenseStatusRevoked,
"revoke_reason": "订单退款",
}).Error; err != nil {
return err
}
if err := tx.Model(&model.CustomerEntitlement{}).Where("license_id = ?", order.LicenseID).
Update("status", "revoked").Error; err != nil {
return err
}
}
utils.LogSystemAction(tx, "payment_refunded", "order", order.ID, order.OrderNo,
"订单退款,已吊销关联授权", clientIP)
}
return nil
})
@@ -630,7 +605,7 @@ func (s *OrderService) CheckPurchaseStatus(customerEmail string, productID uint)
return &dto.CheckPurchaseStatusResponse{
Purchased: true,
HasActiveLicense: isActive,
LicenseKey: ent.LicenseKey,
LicenseKey: "", // 公开接口不回传完整密钥,请走 claim(order+email) // Logan.Li
LicenseStatus: licenseStatus,
PlanType: ent.PlanType,
ValidUntil: validUntilStr,
@@ -0,0 +1,127 @@
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("不支持的公钥格式")
}