一.简介
工作遇到一个问题,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才发现问题,也锻炼了排查问题能力。术业有专攻,涉猎广而不精是否好呢?