plugins/utils/osais/sign.go

158 lines
4.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package osais 实现奥塞斯开放平台(微信企业付/企业码RSA(SHA256WithRSA) 双向签名,
// 逻辑对齐《开放平台接口文档v1.2》第 2.4 节签名规范与第 8 节加签示例代码。
package osais
import (
"bytes"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"sort"
)
// BuildContent 将所有参数按参数名 ASCII 码升序(字典序)拼接为 key1=value1&key2=value2 字符串。
// 规则(文档 2.4.3
// - 参数值为 null 不参与排序,空字符串参与排序
// - 参数名区分大小写
// - sign 不参与签名
// - 接口可能增加字段,验签时必须支持增加的字段
func BuildContent(params map[string]any) (string, error) {
if len(params) == 0 {
return "", nil
}
keys := make([]string, 0, len(params))
for k, v := range params {
if v == nil {
continue // null 不参与排序
}
keys = append(keys, k)
}
sort.Strings(keys)
content := ""
for i, k := range keys {
if i > 0 {
content += "&"
}
content += k + "=" + fmt.Sprintf("%v", params[k])
}
return content, nil
}
// SortJSONKeys 将 JSON 文本的键按 ASCII 字典序递归重排,用于响应验签的 data 归一化:
// 网关对 data 内部字段也按字典序排列后参与签名(实测验证),且值为 null 的字段
// 不参与(规范 2.4.3 "参数的值为 null 不参与排序" 递归应用到 data 内部,实测验证)。
// 非对象(字符串/数字/null/数组元素)原样保留;对象键排序并剔除 null 字段;数组元素逐个递归排序。
func SortJSONKeys(raw []byte) []byte {
trimmed := bytes.TrimSpace(raw)
if len(trimmed) == 0 {
return trimmed
}
switch trimmed[0] {
case '{':
var obj map[string]json.RawMessage
if err := json.Unmarshal(trimmed, &obj); err != nil {
return trimmed // 解析失败按原样返回
}
keys := make([]string, 0, len(obj))
for k, v := range obj {
if bytes.Equal(bytes.TrimSpace(v), []byte("null")) {
continue // 值为 null 的字段不参与拼接
}
keys = append(keys, k)
}
sort.Strings(keys)
var buf bytes.Buffer
buf.WriteByte('{')
for i, k := range keys {
if i > 0 {
buf.WriteByte(',')
}
kb, _ := json.Marshal(k)
buf.Write(kb)
buf.WriteByte(':')
buf.Write(SortJSONKeys(obj[k])) // 递归处理嵌套值
}
buf.WriteByte('}')
return buf.Bytes()
case '[':
var arr []json.RawMessage
if err := json.Unmarshal(trimmed, &arr); err != nil {
return trimmed
}
var buf bytes.Buffer
buf.WriteByte('[')
for i, e := range arr {
if i > 0 {
buf.WriteByte(',')
}
buf.Write(SortJSONKeys(e))
}
buf.WriteByte(']')
return buf.Bytes()
default:
return trimmed // 字符串/数字/null 等原样返回
}
}
// Sign 使用私钥PKCS8 Base64对 data 做 SHA256withRSA 签名,返回 Base64 签名值。
func Sign(data []byte, privateKeyBase64 string) (string, error) {
keyBytes, err := base64.StdEncoding.DecodeString(privateKeyBase64)
if err != nil {
return "", fmt.Errorf("解码私钥失败: %w", err)
}
priKey, err := x509.ParsePKCS8PrivateKey(keyBytes)
if err != nil {
return "", fmt.Errorf("解析 PKCS8 私钥失败: %w", err)
}
rsaKey, ok := priKey.(*rsa.PrivateKey)
if !ok {
return "", errors.New("私钥不是 RSA 私钥")
}
digest := sha256.Sum256(data)
sig, err := rsa.SignPKCS1v15(rand.Reader, rsaKey, crypto.SHA256, digest[:])
if err != nil {
return "", fmt.Errorf("签名失败: %w", err)
}
return base64.StdEncoding.EncodeToString(sig), nil
}
// Verify 使用公钥验证 SHA256withRSA 签名。
// 公钥支持两种编码X.509 SubjectPublicKeyInfoPKIX奥塞斯公钥
// PKCS#1 RSAPublicKey对接方公钥mock 场景),自动识别。
func Verify(data []byte, signBase64, publicKeyBase64 string) (bool, error) {
sig, err := base64.StdEncoding.DecodeString(signBase64)
if err != nil {
return false, fmt.Errorf("解码签名值失败: %w", err)
}
keyBytes, err := base64.StdEncoding.DecodeString(publicKeyBase64)
if err != nil {
return false, fmt.Errorf("解码公钥失败: %w", err)
}
var pubKey any
if pk, err := x509.ParsePKIXPublicKey(keyBytes); err == nil {
pubKey = pk
} else if pk1, err1 := x509.ParsePKCS1PublicKey(keyBytes); err1 == nil {
pubKey = pk1
} else {
return false, fmt.Errorf("解析公钥失败(非 PKIX/PKCS1 格式): %w", err)
}
rsaKey, ok := pubKey.(*rsa.PublicKey)
if !ok {
return false, errors.New("公钥不是 RSA 公钥")
}
digest := sha256.Sum256(data)
if err := rsa.VerifyPKCS1v15(rsaKey, crypto.SHA256, digest[:], sig); err != nil {
return false, nil
}
return true, nil
}