package payment import ( "backend/internal/dto" "crypto/ecdsa" "crypto/rsa" "crypto/sha1" "crypto/sha256" "crypto/x509" "encoding/hex" "encoding/pem" "errors" "fmt" "os" "path/filepath" "strings" "time" ) // 证书/密钥落地目录相关逻辑。 // 目录必须位于仓库之外(容器内为数据卷 /data/payment-certs),权限 0700,文件 0600, // 不通过任何静态路由对外暴露。 // Logan.Li // EnvPaymentCertDir 证书目录覆盖变量 // Logan.Li const EnvPaymentCertDir = "PAYMENT_CERT_DIR" // 允许上传的文件类型 // Logan.Li const ( UploadKindWechatCert = "wechat_cert" // apiclient_cert.pem UploadKindWechatKey = "wechat_key" // apiclient_key.pem UploadKindWechatPlatformCert = "wechat_platform_cert" // 微信支付平台证书 UploadKindAlipayPrivateKey = "alipay_private_key" // 支付宝应用私钥 UploadKindAlipayPublicKey = "alipay_public_key" // 支付宝公钥 ) // fixedFileNames 每种类型固定落地文件名,避免用户文件名注入 // Logan.Li var fixedFileNames = map[string]string{ UploadKindWechatCert: "wechat_apiclient_cert.pem", UploadKindWechatKey: "wechat_apiclient_key.pem", UploadKindWechatPlatformCert: "wechat_platform_cert.pem", UploadKindAlipayPrivateKey: "alipay_app_private_key.pem", UploadKindAlipayPublicKey: "alipay_public_key.pem", } // IsValidUploadKind 判断上传类型是否受支持 // Logan.Li func IsValidUploadKind(kind string) bool { _, ok := fixedFileNames[kind] return ok } // IsCertKind 该类文件应为 X.509 证书 // Logan.Li func IsCertKind(kind string) bool { return kind == UploadKindWechatCert || kind == UploadKindWechatPlatformCert } // CertDir 返回证书目录(默认容器数据卷内,仓库之外) // Logan.Li func CertDir() string { if d := strings.TrimSpace(os.Getenv(EnvPaymentCertDir)); d != "" { return d } return "/data/payment-certs" } // ensureCertDir 确保目录存在且权限为 0700,必要时回退到临时目录 // Logan.Li func ensureCertDir() (string, error) { dir := CertDir() if err := os.MkdirAll(dir, 0o700); err == nil { _ = os.Chmod(dir, 0o700) return dir, nil } fallback := filepath.Join(os.TempDir(), "opsx-licence-payment-certs") if err := os.MkdirAll(fallback, 0o700); err != nil { return "", errors.New("无法创建证书目录,请检查 PAYMENT_CERT_DIR 配置") } _ = os.Chmod(fallback, 0o700) return fallback, nil } // WriteSecretFile 以 0600 权限原子写入敏感文件 // Logan.Li func WriteSecretFile(kind string, content []byte) (string, error) { name, ok := fixedFileNames[kind] if !ok { return "", errors.New("不支持的上传类型") } dir, err := ensureCertDir() if err != nil { return "", err } target := filepath.Join(dir, name) tmp, err := os.CreateTemp(dir, ".upload-*") if err != nil { return "", errors.New("写入证书文件失败") } tmpName := tmp.Name() defer os.Remove(tmpName) if _, err := tmp.Write(content); err != nil { tmp.Close() return "", errors.New("写入证书文件失败") } if err := tmp.Close(); err != nil { return "", errors.New("写入证书文件失败") } if err := os.Chmod(tmpName, 0o600); err != nil { return "", errors.New("设置证书文件权限失败") } if err := os.Rename(tmpName, target); err != nil { return "", errors.New("保存证书文件失败") } _ = os.Chmod(target, 0o600) return target, nil } // ReadSecretFile 读取已配置的证书/密钥文件 // Logan.Li func ReadSecretFile(path string) ([]byte, error) { p := strings.TrimSpace(path) if p == "" { return nil, errors.New("未配置文件") } b, err := os.ReadFile(p) if err != nil { return nil, fmt.Errorf("读取文件失败(%s)", filepath.Base(p)) } return b, nil } // BuildFileInfo 生成可展示的文件元信息(不含内容) // Logan.Li func BuildFileInfo(path string, wantCert bool) dto.PaymentFileInfo { info := dto.PaymentFileInfo{} p := strings.TrimSpace(path) if p == "" { return info } info.Configured = true info.FileName = filepath.Base(p) b, err := os.ReadFile(p) if err != nil { info.ParseError = "文件不可读" return info } info.SizeBytes = int64(len(b)) sum := sha256.Sum256(b) info.Fingerprint = hex.EncodeToString(sum[:])[:16] if wantCert { cert, err := ParseCertificatePEM(b) if err != nil { info.ParseError = "证书解析失败:" + err.Error() return info } info.Subject = cert.Subject.CommonName info.NotBefore = cert.NotBefore.Format("2006-01-02 15:04:05") info.NotAfter = cert.NotAfter.Format("2006-01-02 15:04:05") return info } info.Subject = "私钥" return info } // ParseCertificatePEM 解析 X.509 证书 PEM // Logan.Li func ParseCertificatePEM(content []byte) (*x509.Certificate, error) { block, _ := pem.Decode(content) if block == nil { return nil, errors.New("不是合法的 PEM 证书") } cert, err := x509.ParseCertificate(block.Bytes) if err != nil { return nil, errors.New("证书内容不合法") } return cert, nil } // ParsePrivateKeyPEM 解析 RSA/EC 私钥(兼容 PKCS#1 / PKCS#8 / SEC1 / 裸 base64) // Logan.Li func ParsePrivateKeyPEM(content []byte) (any, error) { text := string(content) if block, _ := pem.Decode(content); block != nil { text = string(block.Bytes) } else if !strings.Contains(text, "-----BEGIN") { // 支付宝后台复制的是裸 base64,补 PEM 头再解析 // Logan.Li normalized := NormalizePEM(text, "-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----") if block2, _ := pem.Decode([]byte(normalized)); block2 != nil { text = string(block2.Bytes) } } else { return nil, errors.New("私钥 PEM 解析失败") } raw := []byte(text) if key, err := x509.ParsePKCS1PrivateKey(raw); err == nil { return key, nil } if keyAny, err := x509.ParsePKCS8PrivateKey(raw); err == nil { switch k := keyAny.(type) { case *rsa.PrivateKey: return k, nil case *ecdsa.PrivateKey: return k, nil default: return nil, errors.New("不支持的私钥类型(需要 RSA)") } } if key, err := x509.ParseECPrivateKey(raw); err == nil { return key, nil } return nil, errors.New("私钥格式无法识别(需 PEM 或裸 base64 的 RSA 私钥)") } // ParseRSAPrivateKey 强制解析为 RSA 私钥(微信/支付宝签名均需要 RSA) // Logan.Li func ParseRSAPrivateKey(content []byte) (*rsa.PrivateKey, error) { key, err := ParsePrivateKeyPEM(content) if err != nil { return nil, err } rsaKey, ok := key.(*rsa.PrivateKey) if !ok { return nil, errors.New("当前仅支持 RSA 私钥") } if err := rsaKey.Validate(); err != nil { return nil, errors.New("RSA 私钥校验失败") } return rsaKey, nil } // ParseRSAPublicKey 解析支付宝公钥(支持证书 / PKIX / PKCS#1 / 裸 base64) // Logan.Li func ParseRSAPublicKey(content []byte) (*rsa.PublicKey, error) { trimmed := strings.TrimSpace(string(content)) if strings.Contains(trimmed, "BEGIN CERTIFICATE") { if cert, err := ParseCertificatePEM(content); err == nil { if pub, ok := cert.PublicKey.(*rsa.PublicKey); ok { return pub, nil } return nil, errors.New("证书内公钥不是 RSA") } } var der []byte if block, _ := pem.Decode(content); block != nil { der = block.Bytes } else { normalized := NormalizePEM(trimmed, "-----BEGIN PUBLIC KEY-----", "-----END PUBLIC KEY-----") block, _ := pem.Decode([]byte(normalized)) if block == nil { return nil, errors.New("公钥格式无法识别") } der = block.Bytes } if pubAny, err := x509.ParsePKIXPublicKey(der); err == nil { if pub, ok := pubAny.(*rsa.PublicKey); ok { return pub, nil } return nil, errors.New("公钥不是 RSA") } if pub, err := x509.ParsePKCS1PublicKey(der); err == nil { return pub, nil } return nil, errors.New("公钥解析失败(请填写支付宝公钥,非应用公钥)") } // FileModTime 取文件修改时间(页面展示用) // Logan.Li func FileModTime(path string) string { if st, err := os.Stat(path); err == nil { return st.ModTime().Format("2006-01-02 15:04:05") } return "" } // CertSerialHex 取证书序列号(大写十六进制,用于与填写值比对) // Logan.Li func CertSerialHex(cert *x509.Certificate) string { if cert == nil { return "" } return strings.ToUpper(cert.SerialNumber.Text(16)) } // Sha1Fingerprint 证书 SHA-1 指纹(微信后台展示口径) // Logan.Li func Sha1Fingerprint(cert *x509.Certificate) string { if cert == nil { return "" } sum := sha1.Sum(cert.Raw) hexStr := strings.ToUpper(hex.EncodeToString(sum[:])) parts := make([]string, 0, len(hexStr)/2) for i := 0; i < len(hexStr); i += 2 { parts = append(parts, hexStr[i:i+2]) } return strings.Join(parts, ":") } // NowUnix 当前时间戳(测试用) // Logan.Li func NowUnix() int64 { return time.Now().Unix() }