找回密码
 用户注册

QQ登录

只需一步,快速开始

查看: 5914|回复: 3

OpenSSL的实用思考

[复制链接]
发表于 2010-12-15 18:35:31 | 显示全部楼层 |阅读模式
最近由于在做苹果的一个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一样,初始化一些动作。
  1. OpenSSL_add_all_algorithms();
  2. ERR_load_BIO_strings();
  3. SSL_load_error_strings();
  4. 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如何建立一个非安全的套接字。
  1. //创建一个非安全连接
  2. bool CSSLConnect::ConnectNonSSL(const char* pUrl, int nPort, const char* pCmd)
  3. {
  4. char szURL[SSL_BUFF_200]   = {'\0'};
  5. char szData[SSL_BUFF_1024] = {'\0'};
  6. sprint_safe(szURL, SSL_BUFF_200, "%s:%d", pUrl, nPort);
  7. Close();
  8. m_pSockBIO = BIO_new_connect(szURL);
  9. if(m_pSockBIO == NULL)
  10. {
  11.   ERR_error_string(ERR_get_error(), (char* )m_szError);
  12.   printf("[CSSLConnect::ConnectNonSSL] BIO_new_connect ERROR: %s.\n", m_szError);
  13.   return false;  
  14. }
  15. if(BIO_do_connect(m_pSockBIO) <= 0)
  16. {
  17.   ERR_error_string(ERR_get_error(), (char* )m_szError);
  18.   printf("[CSSLConnect::ConnectNonSSL] BIO_do_connect ERROR: %s.\n", m_szError);
  19.   return false;
  20. }
  21. //链接建立后,发送命令,可以是http命令,如果是http命令,最好解析第一个包获得数据长度。
  22. int nPos    = 0;
  23. int nCmdLen = (int)strlen(pCmd);
  24. while(true)
  25. {
  26.   if(nPos == nCmdLen || nCmdLen == 0)
  27.   {
  28.    break;
  29.   }
  30.   int nLen = BIO_write(m_pSockBIO, &pCmd[nPos], nCmdLen);
  31.   if(nLen <= 0)
  32.   {
  33.    break;
  34.   }
  35.   else
  36.   {
  37.    nPos    += nLen;
  38.    nCmdLen -= nPos;
  39.   }
  40. }
  41. m_strBuff = "";
  42. while(true)
  43. {
  44.   int nLen = BIO_read(m_pSockBIO, szData, SSL_BUFF_1024);
  45.   if(nLen <= 0)
  46.   {
  47.    break;
  48.   }
  49.   else
  50.   {
  51.    m_strBuff += szData;
  52.   }
  53. }
  54. return true;
  55. }
复制代码
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怎么处理?
来看看以下代码:
  1. //创建一个安全连接,默认的安全证书
  2. bool CSSLConnect::ConnectSSL(const char* pUrl, int nPort, const char* pCmd)
  3. {
  4. char szURL[SSL_BUFF_200] = {'\0'};
  5. sprint_safe(szURL, SSL_BUFF_200, "%s:%d", pUrl, nPort);
  6. Close();
  7. m_pSSLCtx = SSL_CTX_new(SSLv23_client_method());
  8. m_pSockBIO = BIO_new_ssl_connect(m_pSSLCtx);
  9. BIO_get_ssl(m_pSockBIO, &m_pSSL);
  10. SSL_set_mode(m_pSSL, SSL_MODE_AUTO_RETRY);
  11. BIO_set_conn_hostname(m_pSockBIO, szURL);
  12. if(BIO_do_connect(m_pSockBIO) <= 0)
  13. {
  14. ERR_error_string(ERR_get_error(), (char* )m_szError);
  15. printf("[CSSLConnect::Connect] SSL_CTX_use_certificate ERROR: %s.\n", m_szError);
  16. return false;
  17. }
  18. int nPos = 0;
  19. int nCmdLen = (int)strlen(pCmd);
  20. while(true)
  21. {
  22. if(nPos == nCmdLen || nCmdLen == 0)
  23. {
  24. break;
  25. }
  26. int nLen = BIO_write(m_pSockBIO, &pCmd[nPos], nCmdLen);
  27. if(nLen <= 0)
  28. {
  29. break;
  30. }
  31. else
  32. {
  33. nPos += nLen;
  34. nCmdLen -= nPos;
  35. }
  36. }
  37. char szData[SSL_BUFF_1024] = {'\0'};
  38. m_strBuff = "";
  39. while(true)
  40. {
  41. int nLen = BIO_read(m_pSockBIO, szData, SSL_BUFF_1024);
  42. if(nLen <= 0)
  43. {
  44. break;
  45. }
  46. else
  47. {
  48. m_strBuff += szData;
  49. }
  50. }
  51. return true;
  52. }
复制代码
代码就这么多。是不是很简单?
你完全可以对比上面的代码,你会发现其实只是多了几句话而已。
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)就可以了。
先写一个函数吧。
  1. X509* CSSLConnect::LoadBaseCret(BIO* pKeyBuff, int nType, const char* pPassword)
  2. {
  3. X509* pX509 = NULL;
  4. EVP_PKEY* pPKey = NULL;
  5. switch(nType)
  6. {
  7. case DER:
  8. {
  9. //如果是DER证书
  10. pX509 = d2i_X509_bio(pKeyBuff, NULL);
  11. break;
  12. }
  13. case PEM:
  14. {
  15. pX509 = PEM_read_bio_X509(pKeyBuff, NULL, NULL, NULL);
  16. break;
  17. }
  18. case P12:
  19. {
  20. PKCS12 *pP12 = d2i_PKCS12_bio(pKeyBuff, NULL);
  21. if(1 != PKCS12_parse(pP12, pPassword, &pPKey, &pX509, NULL))
  22. {
  23. ERR_error_string(ERR_get_error(), (char* )m_szError);
  24. printf("[CSSLConnect::LoadBaseCret] ERROR: %s.\n", m_szError);
  25. }
  26. PKCS12_free(pP12);
  27. break;
  28. }
  29. }
  30. return pX509;
  31. }
复制代码
这里只写出常用的几种证书的加载,其他的你可以往里添加。
把证书转换成X509格式,然后加载到相应的SSLCtx就大功告成了。
  1. //创建一个安全的链接,需要证书
  2. bool CSSLConnect::ConnectSSL(const char* pUrl, int nPort, int nCreType, const char* pCerFile, const char* pPassword, const char* pCmd)
  3. {
  4. char szURL[SSL_BUFF_200]   = {'\0'};
  5. sprint_safe(szURL, SSL_BUFF_200, "%s:%d", pUrl, nPort);
  6. Close();
  7. //初始化SSLCtx
  8. //m_pSSLCtx = SSL_CTX_new(SSLv23_client_method());
  9. m_pSSLCtx = SSL_CTX_new(TLSv1_client_method());
  10. if(m_pSSLCtx == NULL)
  11. {
  12.   printf("SSL CTX new Fail.\n");
  13.   return false;
  14. }
  15. SSL_CTX_set_options(m_pSSLCtx, SSL_OP_ALL);
  16. //解析证书文件
  17. BIO* pKeyBuff = BIO_new_file(pCerFile, "r");
  18. if(NULL == pKeyBuff)
  19. {
  20.   printf("[CSSLConnect::Connect]Load key file Fail.\n");
  21.   return false;
  22. }
  23. X509*     pX509 = NULL;
  24. EVP_PKEY* pPKey = NULL;
  25. pX509 = LoadBaseCret(pKeyBuff, nCreType, pPassword);
  26. if(NULL == pX509)
  27. {
  28.   printf("[CSSLConnect::Connect]LoadBaseCret error.\n");
  29.   BIO_free_all(pKeyBuff);
  30.   return false;
  31. }
  32. //加载证书
  33. //if(1 != SSL_CTX_use_certificate(m_pSSLCtx, pX509))
  34. if(1 != SSL_CTX_add_client_CA(m_pSSLCtx, pX509))
  35. {
  36.   ERR_error_string(ERR_get_error(), (char* )m_szError);
  37.   printf("[CSSLConnect::Connect] SSL_CTX_use_certificate ERROR: %s.\n", m_szError);
  38.   return false;
  39. }
  40. /*
  41. //测试代码
  42. if(! SSL_CTX_load_verify_locations(m_pSSLCtx, "dev.pem", NULL))
  43. {
  44.   //没有正确的获得pem
  45.   ERR_error_string(ERR_get_error(), (char* )m_szError);
  46.   printf("[CSSLConnect::Connect] pSockBIO ERROR: %s.\n", m_szError);
  47.   return false;
  48. }
  49. if(1 != SSL_CTX_use_certificate_file(m_pSSLCtx, "dev.pem", SSL_FILETYPE_PEM))
  50. {
  51.   //没有正确的获得pem
  52.   ERR_error_string(ERR_get_error(), (char* )m_szError);
  53.   printf("[CSSLConnect::Connect] pSockBIO ERROR: %s.\n", m_szError);
  54.   return false;
  55. }
  56. */
  57. //检查证书是否有效
  58. m_pSockBIO = BIO_new_ssl_connect(m_pSSLCtx);
  59. if(m_pSockBIO == NULL)
  60. {
  61.   //没有正确的获得BIO
  62.   ERR_error_string(ERR_get_error(), (char* )m_szError);
  63.   printf("[CSSLConnect::Connect] pSockBIO ERROR: %s.\n", m_szError);
  64.   return false;
  65. }
  66. BIO_get_ssl(m_pSockBIO, &m_pSSL);
  67. if(m_pSSL == NULL)
  68. {
  69.   //没有正确的获得BIO
  70.   ERR_error_string(ERR_get_error(), (char* )m_szError);
  71.   printf("[CSSLConnect::Connect] m_pSSL ERROR: %s.\n", m_szError);
  72.   return false;
  73. }
  74. if(SSL_get_verify_result(m_pSSL) != X509_V_OK)
  75. {
  76.   //X509证书无效
  77.   ERR_error_string(ERR_get_error(), (char* )m_szError);
  78.   printf("[CSSLConnect::Connect] SSL_get_verify_result ERROR: %s.\n", m_szError);
  79.   return false;
  80. }
  81. SSL_set_mode(m_pSSL, SSL_MODE_AUTO_RETRY);
  82. BIO_set_conn_hostname(m_pSockBIO, szURL);
  83. int nRet = BIO_do_connect(m_pSockBIO);
  84. if(nRet <= 0)
  85. {
  86.   //没有正确连接
  87.   ERR_error_string(ERR_get_error(), (char* )m_szError);
  88.   printf("[CSSLConnect::Connect] pSockBIO Connect ERROR: %s.\n", m_szError);
  89.   return false;
  90. }
  91. int nPos    = 0;
  92. int nCmdLen = (int)strlen(pCmd);
  93. while(true)
  94. {
  95.   if(nPos == nCmdLen || nCmdLen == 0)
  96.   {
  97.    break;
  98.   }
  99.   int nLen = BIO_write(m_pSockBIO, &pCmd[nPos], nCmdLen);
  100.   if(nLen <= 0)
  101.   {
  102.    break;
  103.   }
  104.   else
  105.   {
  106.    nPos    += nLen;
  107.    nCmdLen -= nPos;
  108.   }
  109. }
  110. char szData[SSL_BUFF_1024] = {'\0'};
  111. m_strBuff = "";
  112. while(true)
  113. {
  114.   int nLen = BIO_read(m_pSockBIO, szData, SSL_BUFF_1024);
  115.   if(nLen <= 0)
  116.   {
  117.    break;
  118.   }
  119.   else
  120.   {
  121.    m_strBuff += szData;
  122.   }
  123. }
  124. BIO_free_all(pKeyBuff);
  125. X509_free(pX509);
  126. return true;
  127. }
复制代码
这个函数就是加载有证书的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一条属于自己的消息吧。
在这里抛砖引玉了。
把工程代码都贴上来,大家可以测试。不对的地方多多指正。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?用户注册

×
发表于 2010-12-15 22:29:25 | 显示全部楼层
眼总,我发现,你的文章对我超级实用!以前想用CURL下载HTTPS的东西,但是搞不定SSL。最近在看你写的LUA!
发表于 2010-12-29 11:17:56 | 显示全部楼层
如果证书是存放在USB KEY中将如何?USB KEY的证书不能导出到磁盘上,所有加密运算需要在KEY中完成
发表于 2010-12-31 17:35:17 | 显示全部楼层
VS2005 里面选 x64而不是win32才能出 64版本,和64位系统无关吧
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

Archiver|手机版|小黑屋|ACE Developer ( 京ICP备06055248号 )

GMT+8, 2024-5-5 05:22 , Processed in 0.035597 second(s), 7 queries , Redis On.

Powered by Discuz! X3.5

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表