// 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 }