rsa

使用方式

签名算法

RSA

待签名参数

获取所有请求参数(防止新增参数导致签名失败),除去>防止新增参数导致签名失败),除去 sign,其他请求参数(包含空值的参数,即 key="" )都要参加验签。

详细步骤

STEP1:获取验签公钥
STEP2:参数拼接,获取 $source 字符串
  1. 对所有待签名参数按照字段名的ASCII码从小到大排序;
  2. 使用键值对的格式( key1=value1&key2=value2 )连接参数,拼接成字符串;
  3. 对拼接完参数的字符串进行 urlencode 编码,获得 $source 字符串;

其中,value 的定义如下:

类型 定义
string 直接拼接字符串内容即可,没有引号,如:abc
number 需要转成字符串后拼接,如:1231.03
bool 需要转成 true/false 后拼接
空(null) 需要转成空字符串("")后拼接
list 一般会转为 string 的方式来使用,不会直接使用 list 结构本身
object 一般会转为 string 的方式来使用,不会直接使用 object 结构本身
STEP3:利用密钥校验请求签名参数
  1. 将请求传递过去的sign值进行base64解码后,进行openssl校验 (参考:RFC 8017 - PKCS #1: RSA Cryptography Specifications Version 2.2
  2. 哈希算法使用 SHA1,签名算法使用 PKCS#1 v1.5

使用举例

假设

验签公钥-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCniSL6NpV9INgXlsV3GVGXmXq9
blKezBBPWGbhQuuDF5VK8iduKmXE8uInF1JwOu/wG2rCWCVxuvB5JDQFZQkigZFo
WR4/zIrTMz0qMXJeqdb3ITXzRrF9Vrt6cGMZUB3rTYSsCWSgDoiCGXkqtescPb0W
eP2Ja76KxWKVt7fUPQIDAQAB
-----END PUBLIC KEY-----

signNGfJ7gJdiLuEbq6FInSh+7PoXVbAV7YG8MoFZY74viojPtlPKQMII0LFKZDL41fotp5OyKpI5A7lfCf51z2J0TFbaRl0nppwARpzv4t7Zdyt54B5jfst7LZvxAHFN+svej00ggAyxt3l+GCdE/ZQj+9gvHtWoneZqWzghcah+5A=
请求参数为:

  • b = 1
  • a = "飞鱼"
  • d = 0.1
  • c = null
  • x = true
  • y = false

实际计算步骤如下:

  1. 通过步骤2的排序和连接,可以得到:a=飞鱼&b=1&c=&d=0.1&x=true&y=false
  2. 通过步骤2的 urlencode 编码,可以得到 a%3D%E9%A3%9E%E9%B1%BC%26b%3D1%26c%3D%26d%3D0.1%26x%3Dtrue%26y%3Dfalse
  3. 通过步骤3进行签名校验

签名样例

Golang
func verifySign(params map[string]interface{}, pubKey, sign string) (bool, error) {
    keys := make([]string, 0, len(params))
    for k := range params {
        keys = append(keys, k)
    }

    // 对key进行排序
    sort.Strings(keys)

    var kvPairs []string
    for _, k := range keys {
        if k == "sign" {
            continue
        }
        v := params[k]
        strValue := ""
        if v != nil {
            switch v := v.(type) {
            case float32, float64:
                // 浮点类型需要特殊处理,不然转换后的字符串会变成科学计数法的形式导致签名url不正确
                strValue = strconv.FormatFloat(v.(float64), 'f', -1, 64)
            default:
                strValue = fmt.Sprintf("%v", v)
            }
        }
        kvPairs = append(kvPairs, k+"="+strValue)
    }

    // 拼接参数
    preStr := strings.Join(kvPairs, "&")
    // 进行urlencode编码
    preStr = strings.Replace(url.QueryEscape(preStr), "+", "%20", -1)

    // 计算hash
    signature, err := base64.StdEncoding.DecodeString(sign)
    if err != nil {
        return false, err
    }
    h := crypto.SHA1.New()
    h.Write([]byte(preStr))
    hashed := h.Sum(nil)

    // 验证签名
    pk, err := getRSAPubKey([]byte(pubKey))
    if err != nil {
        return false, err
    }
    err = rsa.VerifyPKCS1v15(pk, crypto.SHA1, hashed, signature)
    if err != nil {
        return false, err
    }
    return true, nil
}

func getRSAPubKey(pkey []byte) (*rsa.PublicKey, error) {
    block, _ := pem.Decode(pkey)
    if block == nil {
        return nil, errors.New("no PEM data is found")
    }

    public, err := x509.ParsePKIXPublicKey(block.Bytes)
    if err != nil {
        return nil, err
    }
    return public.(*rsa.PublicKey), nil
}
PHP
function verifySign($params, $rsapublickey, $sign = '')
{
    ksort($params);//1 按照健值从小到大排序
    $signParams = array();//2 键值对按照'='拼接,排除sign参数
    foreach ($params as $key => $val) {
        if ($key == 'sign'){
            continue;
        }
        switch (gettype($val)) {
            case 'NULL':
                $val = '';
                break;
            case 'boolean':
                $val = $val === true ? 'true' : 'false';
                break;
            default:
                $val = (string) $val;
                break;
        }
        array_push($signParams, $key . '=' . $val);
    }
    $signStr = join('&', $signParams);// 将上述项目按照&拼接
    $source = rawurlencode($signStr);// 将拼接结果用rawurlencode编码
    $res = openssl_get_publickey($rsapublickey);
    if (!$res) {
        return false;
    }
    $result = (bool)openssl_verify($source, base64_decode($sign), $res);//使用base64_decode进行解码验证签名
    openssl_free_key($res);//释放资源
    return  $result;
}