[硬核]浅谈RSA加密、签名、验证

一.简介

工作遇到一个问题,c++服务用sha1withrsa签名,java服务验证,但是有些字符串能验证通过,有些不能,网上找了很多都没解决,只能从rsa原理看起,文章较长,可根据需要跳转到对应的小标题浏览。
先说结论rsa返回的签名中(假设64字节,512位),可能存在某个字节8个bit位全为0,按当字符串处理时为'\0'代表字符串结束,会导致后面的签名被漏掉,验证签名当然会失败。看了openssl的部分源码才发现是这儿的问题。。。也顺便学了点密码学,算好事。

二.问题排查

2.1代码如下

下面这个函数对输入字符串进行签名时,部分输入签名正常,部分异常。签名代码如下,引用代码时makefile要链接 ssl 和crypto库

头文件.h

/**
 * @brief: 
 *  openssl 库对数据加密、签名
 */
#ifndef __SS_SSL_H__
#define __SS_SSL_H__
#include <string>
#include <map>

enum E_ALGO
{
    E_SHA1 = 1,
    E_MD5,
    E_DE,

};

class COpenSslApi
{
public:
    /**
     * @brief: 通过key文件加签
     * @param: {sMsg} 待加签的消息
     * @param: {sPrivateKeyPath} 私钥文件保存路径
     * @param: {eAlgo} 加密算法
     * @return 返回加签后的值
     */
    static  std::string signedRSA(const  std::string& sMsg, const  std::string& sPirvateKeyPath, const E_ALGO& eAlgo );

    /**
     * @brief: 验签
     * @param {sMsg} 原始消息
     * @param {sPublicKeyPath} 公钥保存路径
     * @param {sign} 待验证的签值
     * @param {E_ALGO} 验证算法
     * @return  是否验证成功
     */
    static bool verifyRSA(const  std::string& sMsg,const  std::string& sPublicKeyPath, const  std::string& sign, const E_ALGO& eAlgo );
    

    /**
     * @brief: 生成私钥加密
     * @param {sMsg} 待加签的消息
     * @param {eAlgo} 加密算法
     * @param {*}
     * @return 返回私钥的值和加密后的sign值
     */
    static  std::string signedRSA(const  std::string& sMsg,const E_ALGO& eAlgo){return "";};
};
#endif

cpp文件

#include "servant/RemoteLogger.h"
#include "openssl/evp.h"
#include "openssl/x509.h"
#include "openssl/pem.h"
#include "ss_ssl.h"
#include <cstring>
using namespace taf;

string COpenSslApi::signedRSA(const string& sMsg, const string& sPirvateKeyPath, const E_ALGO& eAlgo )
{
    BIO                 *bufio  = NULL;         //密钥缓存buff
    RSA                 *rsa    = NULL;         //rsa结构变量      
    EVP_PKEY            *evpKey = NULL;		    //EVP KEY结构体变量
    const EVP_MD*       e_algo  = nullptr;      //摘要算法  支持sha1 md5等,具体参见枚举
    EVP_MD_CTX          *mdctx  = NULL;		    //摘要上下文变量
    unsigned char*      pSign   = nullptr;      //加密后的内容
    unsigned int        iSignLen= 0;            //sign长度
    string              sSignRet;               //返回值
    try
    {
        //入参判断
        if (sMsg.empty() || sPirvateKeyPath.empty())
        {
            LOG_ERROR << "empty msg or keypath" << endl;
            goto safe_exit;
       }
        //打开密钥文件buff
        bufio = BIO_new(BIO_s_file());
        BIO_read_filename(bufio, sPirvateKeyPath.c_str());
        if(bufio == NULL)
        {
            LOG_ERROR <<"BIO_read_filename error" <<endl;
            goto safe_exit;
        }
        //获取rsa
        rsa = PEM_read_bio_RSAPrivateKey(bufio, NULL, NULL, NULL);
        if(rsa == NULL)
        {
            LOG_ERROR << "PEM_read_bio_RSAPrivateKey error,path="<< sPirvateKeyPath <<endl;
            goto safe_exit;
        }       
        //evp_key结构变量初始化
        evpKey = EVP_PKEY_new();
        if(evpKey == NULL)
        {
            LOG_ERROR <<"EVP_PKEY_new error" <<endl;
            goto safe_exit;
        }
        //保存RSA结构体到EVP_PKEY结构体
        if(EVP_PKEY_set1_RSA(evpKey,rsa) != 1)	
        {
            LOG_ERROR <<"EVP_PKEY_set1_RSA error" <<endl;
            goto safe_exit;
        }
        //初始化摘要上下文
        mdctx = EVP_MD_CTX_new();
        if(mdctx == NULL)
        {
            LOG_ERROR <<"EVP_MD_CTX_new error" <<endl;
            goto safe_exit;
        }
        EVP_MD_CTX_init(mdctx);
        switch(eAlgo)
        {
            case E_SHA1:
                e_algo = EVP_sha1();
                break;
            case E_MD5:
                e_algo = EVP_md5();
                break;
            default:
                break;
        }
        //签名初始化,设置摘要算法
        if(!EVP_SignInit_ex(mdctx, e_algo, NULL)) 
        {
            LOG_ERROR <<"EVP_SignInit_ex error" <<endl;
            goto safe_exit;
        }
        //计算签名(摘要)Update
        if(!EVP_SignUpdate(mdctx, sMsg.c_str() , sMsg.length() ))
        {
            LOG_ERROR <<"EVP_SignUpdate error" <<endl;
            goto safe_exit;
        }
        //申请内存
        iSignLen = EVP_PKEY_size(evpKey);
        pSign = (unsigned char*)malloc(iSignLen+1);
        memset(pSign, 0, iSignLen+1);
        if( pSign == nullptr || iSignLen == 0)
        {
            LOG_ERROR <<"EVP_SignFinal error" <<endl;
            goto safe_exit;   
        }
        //签名输出
        if(!EVP_SignFinal(mdctx,pSign,&iSignLen,evpKey) )
        {
            LOG_ERROR <<"EVP_SignFinal error" <<endl;
            goto safe_exit;
        }
        //此处赋值不能转char* 构造
        //sSignRet = static_cast<char*>(pSign) //之前失败的用的这个赋值
        for(int i=0;i< iSignLen; ++i)
        {
            sSignRet += static_cast<char>(*(pSign+i) );
        }
        if(sSignRet.size() != iSignLen )
        {
            LOG_ERROR << "sign.size error,size=" << sSignRet.size() <<endl;
        }
    safe_exit:
        if (mdctx)
        {
            EVP_MD_CTX_reset(mdctx);
            EVP_MD_CTX_free(mdctx);  
            mdctx = NULL;          
        }
        if (bufio)
        {
            BIO_free_all(bufio);
            bufio = NULL;
        }
        if (rsa)
        {
            RSA_free(rsa);
            rsa = NULL;
        }
        if (evpKey)
        {
            EVP_PKEY_free(evpKey);
            evpKey = NULL;
        }
        if (pSign)
        {
            free(pSign);
            pSign = NULL;
        }
    }
    catch(const std::exception& e)
    {
        LOG_ERROR << e.what() << endl;
    }
    
    //有必要检查两遍,否则可能内存泄漏
    if (bufio)
        BIO_free_all(bufio);
    if (rsa)
        RSA_free(rsa);
    if(evpKey)
        EVP_PKEY_free(evpKey);
    if (pSign)
        free(pSign);
    if(mdctx)
    {
        EVP_MD_CTX_reset(mdctx);
        EVP_MD_CTX_free(mdctx);
    }
    return std::move(sSignRet);
}

2.2程序执行结果

输入:www.baidu.com22时,程序签名和工具签名长度一致,验证成功。
输入:www.baidu.com2时,程序签名54字节,与工具直接签名对比,签名完全一致少了几个字节,验证失败,看来是丢失字节导致的问题。
在这里插入图片描述

2.3原因分析

(1)了解了sha1摘要算法的工作原理,详细见后面原理描述。
把待加密字符串按摘要算法处理,最后和成一个20字节的hash玛,不足会区全20个字节,看来可能是代码中初始化ctx部分出了问题
EVP_SignInit_ex:初始化一个ctx环境,选了sha1摘要算法,应该没啥问题
EVP_SignUpdate: 原型是EVP_DigestInit_ex 更新ctx计算环境,部分源码,
在这里插入图片描述
意思大概是用type的摘要算法初始化ctx上下文,注意,ctx初始化会申请内存,不用时要调用EVP_MD_CTX_free()清理,会否则内存泄露。详细函数使用参考官方文档。

EVP_SignUpdate :源码部分
在这里插入图片描述把str部分数据流入ctx上下文中,参数带进去看看,没啥问题。

EVP_SignFinal :该函数里面调用了EVP_DigestFinal完成摘要最后一步,然后调用ctx中加密函数指针进行加密操作,返回摘要加密后的数据(数字签名)和签名长度,没啥问题。
在这里插入图片描述

2.4GDB调试

写了个测试程序,两边字符串加密,左边验证成功,右边验证失败,gdb调试输出签名长度不一致,结果如下图
在这里插入图片描述
检查内存x /10xg,10个内存块,每块8个字节,按16进制方式打印,共80字节,因为我的测试密钥是512位(64字节)的,结果显示验证失败的(右边)返回的第八块内存也是满的,也就是算法没有问题。
最后发现有一个16进制位为00的现象,在转换成字符串时,会被当作字符串的结尾处理,导致后面的数字签名内容丢失,验证失败。终于逮住了,真难查。。。

三、签名加密原理

3.1:sha1摘要算法

在这里插入图片描述
把SHA1摘要算法想成一个黑盒子,数据流入会形成一个固定长度的摘要值(hash码)。每当有原始数据流入这个黑盒子,他都会修改最后的hash值,该过程是不可逆的。当数据长度本身不足这个hash值的长度时,算法会补齐到该长度。

3.2 RSA算法

俩科学家和一个数学家搞出来的算法。原理要追溯到欧拉函数:详细见参考文章中的rsa原理部分。
简单来讲就是这样:(为啥?问就是公式)
(1)找到俩质数, z1 = 61 , z2= 53 ,然后他们的乘积 n = z1*z2 = 3233
(2)他们的欧拉函数φ(n) = (z1-1)(z2-1) = 3120
(3)选取一个随机质数 e =17
(4)计算e对于φ(n)的模反元素d,根据拓展欧几里得算法,解除一组解,d=2753
(5)n=3233,e=17,d=2753,所以公钥就是 (3233,17)私钥就是(3233, 2753)
到了加密环节
(1)加密要用公钥 (n,e),原始消息为m,则me ≡ c (mod n) ,大白话就是 m*n的结果余上n,得到的c=2790就是加密后的密文
(2)解密要用私钥(n,d),cd ≡ m (mod n) ,同样的运算,解出来m就是原始消息的明文

总结一下
加密算法毕竟有大量的数学运算,如果要在网上传输大文件,或者用户实时性要求比较高的场合,就不太适用了。
这种场合现在有这些处理办法:
(1)数据传输采用对称加密,但为了保证对称加密的密钥安全的传输到对方手中,用RSA来对密钥进行加密传输,后面用对称加密就快很多了。
(2)大量数据防止网络传输过程中被修改,收发件双方约定一种摘要算法和加密算法,例如SHA1WithRSA,对数据进行摘要,再对摘要进行加密生成数字签名,并放在数据包中和原始数据一并发过去,也叫数据证书。 收件方验证签名和原始数据即可

3.3参考文章:

rsa签名和加密区别:https://blog.csdn.net/cwg2552298/article/details/81638207
rsa原理:https://www.kancloud.cn/kancloud/rsa_algorithm/48484
openssl官方文档:https://www.openssl.org/docs/manmaster/man3/

四、总结

这次问题看了很多文章,资料,也问过挺多人,最后还靠自己,耐心,细心,还好从c学起的,对内存有一定认识,gdb才发现问题,也锻炼了排查问题能力。术业有专攻,涉猎广而不精是否好呢?

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值