158 lines
4.6 KiB
Go
158 lines
4.6 KiB
Go
// 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 SubjectPublicKeyInfo(PKIX,奥塞斯公钥)与
|
||
// 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
|
||
}
|