freeeyes 发表于 2010-12-15 18:35:31

OpenSSL的实用思考

最近由于在做苹果的一个Push服务器,需要用到SSL的库,决定选择网上比较热炒的Openssl,在此之前,也听说了这个库比较难处理,因为资料并不是很完整,所以开发会有困难,不过本着挑战一下,既然有人说好,那么就实际检验一下吧。在查了很多资料后,写了一个openssl的上层封装类,给大家拍砖。
首先到openssl去下载一个最新的openssl。地址是http://www.openssl.org/
这里要说明一下,这时候最新的是1.0.0版本的。不过因为1.0.0支持VS2008,而本人的开发环境是VS2005,所以我使用了较为早一些的版本,0.9.8b。
linux下的安装很简单,按照install文件里面一步步来就可以了。
windows下比较复杂,需要下载一个工具。
安装步骤如下:
1.下载ActivePerl
2.打开Vs2005命令行提示工具。
3.进入你的openssl解压目录,并之行perl Configure VC-WIN32
4.运行ms\do_ms进行编译
5.运行vcvars32.bat
6.在openssl目录下运行“nmake -f ms\ntdll.mak ”
如果一切顺利,会在你的openssl解压目录下多了一个out*dll的目录(如果是32位系统,会是out32dll,不过在我的64位win7下生成的也是out32dll,不过根据官方文档说可以生成64位的)
行了,openssl已经可以使用了。
建立一个新的工程,你可以把你的库目录指定到out32dll,把C++的附加包含目录指定到include目录下,不过我个人建议把include目录考到你的工程路径下,然后建立一个lib目录,把OpenSSL下生成的libeay32.lib和ssleay32.lib考进去就行了,建立一个bin目录,把工程的工作路径指定在这里,并把libeay32.dll和ssleay32.dll拷贝进去,这样你的程序就可以随时随地带着走了。不需要到处编译OpenSSL。
呵呵,好了,所有的材料都齐备了。
看看怎么写一个支持SSL通讯的客户端程序吧。网上的一些OpenSSL写的比较乱,其实OpenSSL和CUrl一样,会用的话,非常好使,而且代码简单,完全不用带着恐惧的心情。(一开始总结网上那些OpenSSL可借鉴的时候,看着长篇累牍的代码确实头晕,但是后来一点点的分析,发现大部分写的都是没用的。)
其实OpenSSL并不是只能连接SSL连接,其实也可以连接非SSL的网址。并获得网站返回数据。
要用OpenSSL,需要像CUrl一样,初始化一些动作。

OpenSSL_add_all_algorithms();
ERR_load_BIO_strings();
SSL_load_error_strings();
SSL_library_init();


这是固定的代码,加载一些必要的库和函数,没啥好解释的。
SSL通讯需要几个关键对象,在这里解释一下。
SSL*      m_pSSL;
SSL的对象指针,主要用于记录一些SSL中用到的算法和对象,以及最关键的SSL_CTX。
SSL_CTX*m_pSSLCtx;
SSL_CTX实际上就是一个SSL的上下文。这里可以记录和设置你的SSL一些状态,比如你采用的SSL协议格式(SSL2,SSL3或者TLS)
BIO*      m_pSockBIO;
BIO我的理解是就是一个标准的Socket对象封装,之所以要用它把socket包装起来,因为加解密算法在里面完成,并不暴露在外面,极大的减轻了开发人员的开发量。
先从简单的来,先看看OpenSSL如何建立一个非安全的套接字。

//创建一个非安全连接
bool CSSLConnect::ConnectNonSSL(const char* pUrl, int nPort, const char* pCmd)
{
char szURL   = {'\0'};
char szData = {'\0'};
sprint_safe(szURL, SSL_BUFF_200, "%s:%d", pUrl, nPort);
Close();
m_pSockBIO = BIO_new_connect(szURL);
if(m_pSockBIO == NULL)
{
ERR_error_string(ERR_get_error(), (char* )m_szError);
printf(" BIO_new_connect ERROR: %s.\n", m_szError);
return false;
}
if(BIO_do_connect(m_pSockBIO) <= 0)
{
ERR_error_string(ERR_get_error(), (char* )m_szError);
printf(" BIO_do_connect ERROR: %s.\n", m_szError);
return false;
}
//链接建立后,发送命令,可以是http命令,如果是http命令,最好解析第一个包获得数据长度。
int nPos    = 0;
int nCmdLen = (int)strlen(pCmd);
while(true)
{
if(nPos == nCmdLen || nCmdLen == 0)
{
   break;
}
int nLen = BIO_write(m_pSockBIO, &pCmd, nCmdLen);
if(nLen <= 0)
{
   break;
}
else
{
   nPos    += nLen;
   nCmdLen -= nPos;
}
}
m_strBuff = "";
while(true)
{
int nLen = BIO_read(m_pSockBIO, szData, SSL_BUFF_1024);
if(nLen <= 0)
{
   break;
}
else
{
   m_strBuff += szData;
}
}
return true;
}

pUrl nPort是对应的你的URL和Port,比如你的URL是"www.baidu.com",端口是80。pCmd是你发送的命令,比如给它一个标准的http头。
"GET / HTTP/1.1\r\nHost: www.baidu.com\r\nConnection: Close\r\n\r\n"
在这里,m_pSockBIO完全就是一个socket的用法。
你可以发送数据,并获得返回,对应的BIO_write()就是send,BIO_read对应的是Recv。
这里可以有一个小技巧,你可以在recv的时候,回调一个指定的函数指针,呵呵,想到这里,如果你看过CUrl的话,你就会恍然大悟。其实CURL完全可以用openssl做一个底层,用法也就是上述我说的方法。当然在这里,我简化了,只持续的接受数据。
好了,来点正题吧。
如果我要连接一个https网站怎么办,这里要区分一下,有的htpps需要一个自己的证书,有的需要一个默认的根证书。
先说简单的,比如我访问google的https怎么处理?
来看看以下代码:

//创建一个安全连接,默认的安全证书
bool CSSLConnect::ConnectSSL(const char* pUrl, int nPort, const char* pCmd)
{
char szURL = {'\0'};

sprint_safe(szURL, SSL_BUFF_200, "%s:%d", pUrl, nPort);

Close();

m_pSSLCtx = SSL_CTX_new(SSLv23_client_method());

m_pSockBIO = BIO_new_ssl_connect(m_pSSLCtx);

BIO_get_ssl(m_pSockBIO, &m_pSSL);

SSL_set_mode(m_pSSL, SSL_MODE_AUTO_RETRY);

BIO_set_conn_hostname(m_pSockBIO, szURL);

if(BIO_do_connect(m_pSockBIO) <= 0)
{
ERR_error_string(ERR_get_error(), (char* )m_szError);
printf(" SSL_CTX_use_certificate ERROR: %s.\n", m_szError);
return false;
}

int nPos = 0;
int nCmdLen = (int)strlen(pCmd);

while(true)
{
if(nPos == nCmdLen || nCmdLen == 0)
{
break;
}

int nLen = BIO_write(m_pSockBIO, &pCmd, nCmdLen);
if(nLen <= 0)
{
break;
}
else
{
nPos += nLen;
nCmdLen -= nPos;
}
}

char szData = {'\0'};
m_strBuff = "";
while(true)
{
int nLen = BIO_read(m_pSockBIO, szData, SSL_BUFF_1024);
if(nLen <= 0)
{
break;
}
else
{
m_strBuff += szData;
}
}

return true;
}

代码就这么多。是不是很简单?
你完全可以对比上面的代码,你会发现其实只是多了几句话而已。
m_pSSLCtx = SSL_CTX_new(SSLv23_client_method());
这句话是生成一个SSL上下文,这里支持SSLv2_client_method(),SSLv3_client_method(),TLSv1_client_method()。这里应对的是SSL的协议标准,SSL2,SSL3亦或TLS1。具体可以根据自己的需要进行替换。
m_pSockBIO = BIO_new_ssl_connect(m_pSSLCtx);
生成一个BIO*对象,并绑定SSLCtx的上下文。
BIO_get_ssl(m_pSockBIO, &m_pSSL);
根据这个BIO对象返回一个SSL对象。
SSL_set_mode(m_pSSL, SSL_MODE_AUTO_RETRY);
设置SSL对象需要自动重试请求。
if(BIO_do_connect(m_pSockBIO) <= 0)
这个代码是,根据已有的SSL,向远程SSL提交请求。进行一次握手,如果对方服务器拒绝,会在这里返回错误。抓住这里的错误,你可以进行分析。
此后的代码,和非安全套接字一样。
BIO_write会在内部将你给出的字符加密,这里不用你担心。
BIO_read会在内部解密服务器的数据返回,这里也不用担心,这里得到的都是解密后的字符。
看看,简单吧。
让我们测试一下,这时候拿一个google的测试一下。
测试地址是www.google.com 端口是443(一般SSL都是这个端口)发送命令么。。就是这个吧"GET / HTTP/1.1\x0D\x0AHost: www.google.com\x0D\x0A\x43onnection: Close\x0D\x0A\x0D\x0A"
数据回来了吧。
呵呵,接下来,说一下最复杂的证书加载。
有些网站需要加载证书,证书类型不同,比如P12,pem,DER等等。有的证书还需要key。
写到这里,先写一个处理证书读取的类,在OpenSSL中,只要你把不同的证书加载成一种中间格式(X509)就可以了。
先写一个函数吧。

X509* CSSLConnect::LoadBaseCret(BIO* pKeyBuff, int nType, const char* pPassword)
{
X509* pX509 = NULL;
EVP_PKEY* pPKey = NULL;
switch(nType)
{
case DER:
{
//如果是DER证书
pX509 = d2i_X509_bio(pKeyBuff, NULL);
break;
}
case PEM:
{
pX509 = PEM_read_bio_X509(pKeyBuff, NULL, NULL, NULL);
break;
}
case P12:
{
PKCS12 *pP12 = d2i_PKCS12_bio(pKeyBuff, NULL);
if(1 != PKCS12_parse(pP12, pPassword, &pPKey, &pX509, NULL))
{
ERR_error_string(ERR_get_error(), (char* )m_szError);
printf(" ERROR: %s.\n", m_szError);
}

PKCS12_free(pP12);
break;
}
}
return pX509;
}

这里只写出常用的几种证书的加载,其他的你可以往里添加。
把证书转换成X509格式,然后加载到相应的SSLCtx就大功告成了。

//创建一个安全的链接,需要证书
bool CSSLConnect::ConnectSSL(const char* pUrl, int nPort, int nCreType, const char* pCerFile, const char* pPassword, const char* pCmd)
{
char szURL   = {'\0'};
sprint_safe(szURL, SSL_BUFF_200, "%s:%d", pUrl, nPort);

Close();
//初始化SSLCtx
//m_pSSLCtx = SSL_CTX_new(SSLv23_client_method());
m_pSSLCtx = SSL_CTX_new(TLSv1_client_method());
if(m_pSSLCtx == NULL)
{
printf("SSL CTX new Fail.\n");
return false;
}
SSL_CTX_set_options(m_pSSLCtx, SSL_OP_ALL);
//解析证书文件
BIO* pKeyBuff = BIO_new_file(pCerFile, "r");
if(NULL == pKeyBuff)
{
printf("Load key file Fail.\n");
return false;
}
X509*   pX509 = NULL;
EVP_PKEY* pPKey = NULL;
pX509 = LoadBaseCret(pKeyBuff, nCreType, pPassword);
if(NULL == pX509)
{
printf("LoadBaseCret error.\n");
BIO_free_all(pKeyBuff);
return false;
}
//加载证书
//if(1 != SSL_CTX_use_certificate(m_pSSLCtx, pX509))
if(1 != SSL_CTX_add_client_CA(m_pSSLCtx, pX509))
{
ERR_error_string(ERR_get_error(), (char* )m_szError);
printf(" SSL_CTX_use_certificate ERROR: %s.\n", m_szError);
return false;
}
/*
//测试代码
if(! SSL_CTX_load_verify_locations(m_pSSLCtx, "dev.pem", NULL))
{
//没有正确的获得pem
ERR_error_string(ERR_get_error(), (char* )m_szError);
printf(" pSockBIO ERROR: %s.\n", m_szError);
return false;
}

if(1 != SSL_CTX_use_certificate_file(m_pSSLCtx, "dev.pem", SSL_FILETYPE_PEM))
{
//没有正确的获得pem
ERR_error_string(ERR_get_error(), (char* )m_szError);
printf(" pSockBIO ERROR: %s.\n", m_szError);
return false;
}
*/

//检查证书是否有效
m_pSockBIO = BIO_new_ssl_connect(m_pSSLCtx);
if(m_pSockBIO == NULL)
{
//没有正确的获得BIO
ERR_error_string(ERR_get_error(), (char* )m_szError);
printf(" pSockBIO ERROR: %s.\n", m_szError);
return false;
}
BIO_get_ssl(m_pSockBIO, &m_pSSL);
if(m_pSSL == NULL)
{
//没有正确的获得BIO
ERR_error_string(ERR_get_error(), (char* )m_szError);
printf(" m_pSSL ERROR: %s.\n", m_szError);
return false;
}
if(SSL_get_verify_result(m_pSSL) != X509_V_OK)
{
//X509证书无效
ERR_error_string(ERR_get_error(), (char* )m_szError);
printf(" SSL_get_verify_result ERROR: %s.\n", m_szError);
return false;
}
SSL_set_mode(m_pSSL, SSL_MODE_AUTO_RETRY);

BIO_set_conn_hostname(m_pSockBIO, szURL);
int nRet = BIO_do_connect(m_pSockBIO);
if(nRet <= 0)
{
//没有正确连接
ERR_error_string(ERR_get_error(), (char* )m_szError);
printf(" pSockBIO Connect ERROR: %s.\n", m_szError);
return false;
}
int nPos    = 0;
int nCmdLen = (int)strlen(pCmd);
while(true)
{
if(nPos == nCmdLen || nCmdLen == 0)
{
   break;
}
int nLen = BIO_write(m_pSockBIO, &pCmd, nCmdLen);
if(nLen <= 0)
{
   break;
}
else
{
   nPos    += nLen;
   nCmdLen -= nPos;
}
}
char szData = {'\0'};
m_strBuff = "";
while(true)
{
int nLen = BIO_read(m_pSockBIO, szData, SSL_BUFF_1024);
if(nLen <= 0)
{
   break;
}
else
{
   m_strBuff += szData;
}
}
BIO_free_all(pKeyBuff);
X509_free(pX509);
return true;
}

这个函数就是加载有证书的SSL连接代码了。
其他部分的代码很简单,关键在这里
if(1 != SSL_CTX_add_client_CA(m_pSSLCtx, pX509))
加载你的证书到m_pSSLCtx就行了。
呵呵,不复杂吧。
好了,代码完成了,拿一个有证书的链接测试一下。这里我拿苹果的Push服务器测试的。
如果你有苹果开发者账号,你可以参考一下。
http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingWIthAPS/CommunicatingWIthAPS.html

呵呵,有兴趣的哈,给自己的IPhone Push一条属于自己的消息吧。
在这里抛砖引玉了。
把工程代码都贴上来,大家可以测试。不对的地方多多指正。

iq50 发表于 2010-12-15 22:29:25

眼总,我发现,你的文章对我超级实用!以前想用CURL下载HTTPS的东西,但是搞不定SSL。最近在看你写的LUA!

iq50 发表于 2010-12-29 11:17:56

如果证书是存放在USB KEY中将如何?USB KEY的证书不能导出到磁盘上,所有加密运算需要在KEY中完成

evilswords 发表于 2010-12-31 17:35:17

VS2005 里面选 x64而不是win32才能出 64版本,和64位系统无关吧
页: [1]
查看完整版本: OpenSSL的实用思考