什么是SSL?
SSL(Secure Sockets Layer 安全套接字协议),及其继任者传输层安全(Transport Layer Security,TLS)是为网络通信提供安全及数据完整性的一种安全协议。TLS与SSL在传输层与应用层之间对网络连接进行加密,它最早为Netscape所研发,用以保障在Internet上数据传输的安全,利用数据加密(Encryption)技术,可确保数据在网络上的传输过程中不会被截取及窃听。
SSL协议位于TCP/IP协议与各种应用层协议之间,为数据通讯提供安全支持。
SSL协议可分为两层:
SSL记录协议(SSL Record Protocol)—— 它建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。
SSL握手协议(SSL Handshake Protocol)—— 它建立在SSL记录协议之上,用于在实际的数据传输开始前,通讯双方进行身份认证、协商加密算法、交换加密密钥等。
SSL提供的服务:
1)认证用户和服务器,确保数据发送到正确的客户机和服务器;
2)加密数据以防止数据中途被窃取;
3)维护数据的完整性,确保数据在传输过程中不被改变。
开发流程:
我从网上摘了几个图,分别介绍如下。
图1:SSL的通用开发流程示意:
无论是服务端还是客户端,进行一次完整的SSL通讯,大致可以抽像为以下几个步骤:
1)初使化SSL环境。
2)创建SSL上下文。
3)配置SSL上下文证书及公钥信息。
4)创建SSL上下文。
5)创建TCP通讯端口。
6)建立SSL和TCP通讯端口的关联。
7)执行SSL握手。
8)执行SSL数据读写交互。
9)关闭SSL连接。
10)关于TCP通讯端口。
11)释放SSL上下文。
图2:SSL服务端接口调用流程示意:
图3:SSL客户端接口调用流程示意:
图2、图3表达得非常好,非常具体地说明了程序员应该调用哪些函数,以及执行的步骤。
主要接口:
SSL接口的主要头文件在ssl中。我们根据SSL通讯的流程摘录如下:
1)初使化SSL环境。
通常用得比较多的是这三个函数:
SSL_library_init()
OpenSSL_add_ssl_algorithms()
SSL_load_error_strings()
在1.1.1版本的定义中是这样的:
# if OPENSSL_API_COMPAT < 0x10100000L
# define SSL_library_init() OPENSSL_init_ssl(0, NULL)
# endif
# if OPENSSL_API_COMPAT < 0x10100000L
# define SSL_load_error_strings() \
OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS \
| OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL)
# endif
# if OPENSSL_API_COMPAT < 0x10100000L
# define OpenSSL_add_ssl_algorithms() SSL_library_init()
# define SSLeay_add_ssl_algorithms() SSL_library_init()
# endif
int OPENSSL_init_ssl(uint64_t opts, const OPENSSL_INIT_SETTINGS *settings);
可以看到,上面图示中的OpenSSL_add_ssl_algorithms()这一步是多余的,通常初使化只需要调用这两个就可以了:
SSL_library_init()
SSL_load_error_strings()
在初使化过程中,SSL_library_init()注册了所有在SSL APIs中的加密算法和哈希算法,SSL_load_error_strings()则加载了所有的错误描述字符串。
2)创建SSL上下文。
SSL_CTX *SSL_CTX_new(const SSL_METHOD *meth);
这个函数用于创建SSL上下文,其参数SSL_METHOD用于传入SSL的抽象方法集合。
对于每个SSL/TSL来说,有三种APIs可以用来创建一个SSL_METHOD:
一个可以用于服务端和客户端,一个只能用于服务端,另外一个只能由于客户端。定义如下:
const SSL_METHOD *TLS_method(void);
const SSL_METHOD *TLS_server_method(void);
const SSL_METHOD *TLS_client_method(void);
#define SSLv23_method TLS_method
#define SSLv23_server_method TLS_server_method
#define SSLv23_client_method TLS_client_method
int SSL_CTX_set_cipher_list(SSL_CTX *, const char *str);
这个函数用于设置SSL上下文的算法套件信息。
成功返回1,失败返回0。
可用的算法如:
EDH-RSA-DES-CBC3-SHA
EDH-DSS-DES-CBC3-SHA
DES-CBC3-SHA
DHE-DSS-RC4-SHA
IDEA-CBC-SHA
RC4-SHA
RC4-MD5
EXP1024-DHE-DSS-RC4-SHA
EXP1024-RC4-SHA
EXP1024-DHE-DSS-DES-CBC-SHA
EXP1024-DES-CBC-SHA
EXP1024-RC2-CBC-MD5
EXP1024-RC4-MD5
EDH-RSA-DES-CBC-SHA
EDH-DSS-DES-CBC-SHA
DES-CBC-SHA
EXP-EDH-RSA-DES-CBC-SHA
EXP-EDH-DSS-DES-CBC-SHA
EXP-DES-CBC-SHA
EXP-RC2-CBC-MD5
EXP-RC4-MD5
这些算法按一定优先级排列,如果不作任何指定,将选用DES-CBC3-SHA.用SSL_CTX_set_cipher_list可以指定自己希望用的算法(实际上只是 提高其优先级,是否能使用还要看对方是否支持)。
3)配置SSL上下文证书及公钥信息。
int SSL_CTX_use_PrivateKey_file(SSL_CTX *ctx, const char *file, int type);
int SSL_CTX_use_certificate_file(SSL_CTX *ctx, const char *file, int type);
这两个函数用于加载私钥和证书文件。
成功返回1,失败返回0。
其中,type的取值:
# define SSL_FILETYPE_ASN1 X509_FILETYPE_ASN1
# define SSL_FILETYPE_PEM X509_FILETYPE_PEM
即
# define X509_FILETYPE_PEM 1
# define X509_FILETYPE_ASN1 2
也可以直接使用二进制结构和ASN1序列化的内存数据:
int SSL_CTX_use_RSAPrivateKey(SSL_CTX *ctx, RSA *rsa);
int SSL_CTX_use_RSAPrivateKey_ASN1(SSL_CTX *ctx, const unsigned char *d, long len);
int SSL_CTX_use_certificate(SSL_CTX *ctx, X509 *x);
int SSL_CTX_use_certificate_ASN1(SSL_CTX *ctx, int len, const unsigned char *d);
int SSL_CTX_check_private_key(const SSL_CTX *ctx);
在完成私钥和证书加载后,这个函数由于检查二者是否匹配。
成功返回1,失败返回0。
4)创建SSL上下文。
SSL *SSL_new(SSL_CTX *ctx);
创建SSL结构,SSL的连接信息都保存在SSL结构中。
新的SSL结构会从SSL_CTX结构中继承包括,连接类型、选项、验证方式以及超时。
5)创建TCP通讯端口。
本步省略,不在本文的介绍范围。
6)建立SSL和TCP通讯端口的关联。
int SSL_set_fd(SSL *s, int fd);
成功返回1,失败返回0。
也可以直接使用BIO代替。
BIO* bio = BIO_new_socket(socket, BIO_NOCLOSE);
SSL_set_bio(ssl, bio, bio);
7)执行SSL握手。
SSL握手过程是一个复杂过程,涉及到重要的加密秘钥交换。
但是握手过程可以通过服务端调用SSL_accept()和客户端调用SSL_connect()完成。
int SSL_accept(SSL *ssl);
int SSL_connect(SSL *ssl);
成功返回1,失败返回<=0。
这两个函数是可以重复调用的,这个特性在非阻塞模式下尤为明显。
此外,在握手完成后,可通过调用SSL_get_peer_certificate来获取对端的证书。
X509 *SSL_get_peer_certificate(const SSL *s);
如果对方存在证书,就可以调用X509的相关函数提取证书的身份信息,比如:
X509_NAME *X509_get_subject_name(X509 *a);
8)执行SSL数据读写交互。
在SSL握手完成后,数据就可以通过已经建立好的连接安全的发送了。
不要再使用send、recv函数,而是要使用SSL_write和SSL_read。
int SSL_read(SSL *ssl, void *buf, int num);
int SSL_write(SSL *ssl, const void *buf, int num);
与send、recv用法相似。
成功返回发送或接收的字节数,失败返回<=0。
当返回值<0(通常为-1)时,应检查错误码,尤其是在非阻塞模式时。
错误码可使用SSL_get_error()函数获取,其定义为:
int SSL_get_error(const SSL *s, int ret_code);
9)关闭SSL连接。
当关闭SSL连接时,SSL客户端和服务端需要发送close_notify消息,通知对端SSL将要关闭了,调用SSL_shutdown函数来发送close_notify消息。
int SSL_shutdown(SSL *s);
成功返回1,失败返回<=0。
关闭过包含以下两个步骤:
1)发送一个close_notify关闭告警。
2)从对端接收一个close_notify的关闭消息。
发起关闭的客户端或者服务端可以调用SSL_shutdown一次或者两次。
如果调用了两次,一次调用用于发送close_notify消息,另外一次用于响应对端的。
如果只调用一次,发起关闭一端将不会等待对端的响应(发起关闭的一端不需要等待对端的关闭响应,一旦收到对端关闭消息就要马上发送关闭响应。
10)关于TCP通讯端口。
本步省略,不在本文的介绍范围。
11)释放SSL上下文。
void SSL_free(SSL *ssl);
void SSL_CTX_free(SSL_CTX *);
使用举例:
阻塞模式用法:
基本上只要按照前面介绍的开发流程,组织好整个程序,通常就能正常工作了。下面是示例代码。
服务端代码:
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define SSL_print_error_and_freectx() \
ERR_print_errors_fp(stdout); \
SSL_CTX_free(ctx);
int listenLocal(unsigned short uPort)
{
int sockS = socket(AF_INET, SOCK_STREAM, 0);
if (sockS < 0)
{
printf("socket() error! \n");
return -1;
}
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = INADDR_ANY;
sin.sin_port = htons(uPort);
int ret = bind(sockS, (struct sockaddr*)&sin, sizeof(sin));
if (ret < 0)
{
printf("bind() error! \n");
close(sockS);
return -1;
}
listen(sockS, 50);
return sockS;
}
int main(int argc, char* argv[])
{
SSL_library_init();
SSL_load_error_strings();
SSL_CTX* ctx = SSL_CTX_new( SSLv23_server_method() );
if (ctx == NULL)
{
ERR_print_errors_fp(stdout);
return -1;
}
int ret = SSL_CTX_use_certificate_file(ctx, "test.crt", SSL_FILETYPE_PEM);
if (ret != 1)
{
SSL_print_error_and_freectx();
return -1;
}
ret = SSL_CTX_use_PrivateKey_file(ctx, "test.key", SSL_FILETYPE_PEM);
if (ret != 1)
{
SSL_print_error_and_freectx();
return -1;
}
ret = SSL_CTX_check_private_key(ctx);
if (ret != 1)
{
SSL_print_error_and_freectx();
return -1;
}
int sockS = listenLocal(9999);
if (sockS < 0)
{
SSL_CTX_free(ctx);
return -1;
}
while (true)
{
struct sockaddr_in sinfrom;
socklen_t sinfromlen = sizeof(sinfrom);
int sockC = accept(sockS, (struct sockaddr*)&sinfrom, &sinfromlen);
if (sockC < 0)
{
printf("accept() error! \n");
break;
}
printf("accpet connect:[%d - %s:%d] \n", sockC, inet_ntoa(sinfrom.sin_addr), ntohs(sinfrom.sin_port));
SSL* ssl = SSL_new(ctx);
SSL_set_fd(ssl, sockC);
do
{
ret = SSL_accept(ssl);
if (ret != 1)
{
printf("SSL handshake failed! \n");
break;
}
printf("SSL handshake success! \n");
SSL_write(ssl, "hello", 5);
while (true)
{
char sBuf[1024] = {0};
int bytesin = SSL_read(ssl, sBuf, sizeof(sBuf)-1);
if (bytesin <= 0)
{
printf("error or disconnect! \n");
break;
}
printf("read:[%s] \n", sBuf);
}
SSL_shutdown(ssl);
} while(0);
SSL_free(ssl);
close(sockC);
}
close(sockS);
SSL_CTX_free(ctx);
return 0;
}
客户端代码:
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int connectPeer(const char* sIp, unsigned short uPort)
{
int sockC = socket(AF_INET, SOCK_STREAM, 0);
if (sockC < 0)
{
printf("socket() errror! \n");
return -1;
}
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = inet_addr(sIp);
sin.sin_port = htons(uPort);
int ret = connect(sockC, (struct sockaddr*)&sin, sizeof(sin));
if (ret < 0)
{
printf("connect() failed! \n");
close(sockC);
return -1;
}
return sockC;
}
void showCert(SSL* ssl)
{
X509* x509 = SSL_get_peer_certificate(ssl);
if (x509)
{
const char* subjectname = X509_NAME_oneline(X509_get_subject_name(x509), 0, 0);
printf("subject name:[%s] \n", subjectname);
const char* issuername = X509_NAME_oneline(X509_get_issuer_name(x509), 0, 0);
printf("issuer name:[%s] \n", issuername);
X509_free(x509);
}
}
int main(int argc, char* argv[])
{
SSL_library_init();
SSL_load_error_strings();
SSL_CTX* ctx = SSL_CTX_new( SSLv23_client_method() );
if (ctx == NULL)
{
ERR_print_errors_fp(stdout);
SSL_CTX_free(ctx);
return -1;
}
int sockC = connectPeer("127.0.0.1", 9999);
if (sockC < 0)
{
SSL_CTX_free(ctx);
return -1;
}
SSL* ssl = SSL_new(ctx);
SSL_set_fd(ssl, sockC);
do
{
int ret = SSL_connect(ssl);
if (ret != 1)
{
printf("SSL handshake failed! \n");
break;
}
printf("SSL handshake success! \n");
showCert(ssl);
char sBuf[1024] = {0};
int bytesin = SSL_read(ssl, sBuf, sizeof(sBuf)-1);
if (bytesin <= 0)
{
printf("disconnect! \n");
SSL_shutdown(ssl);
break;
}
printf("read:[%s] \n", sBuf);
for (int i = 0; i < 3; ++i)
{
SSL_write(ssl, "hello", 5);
sleep(1);
}
SSL_shutdown(ssl);
} while (0);
SSL_free(ssl);
close(sockC);
SSL_CTX_free(ctx);
return 0;
}
非阻塞模式用法:
对非阻塞套接字来说,SSL_read()和SSL_write()的调用通常会返回-1,这并不是真正表示收发失败了,大多可能的原因是套接字的缓冲不可用,我们需要稍后进行尝试。只不过,在非阻塞模式下,采用主动尝试的方法通常都不可取,正式的场合应该使用异步事件模型。
下面的代码出于简单性,暂时采用稍后尝试的方法。代码示例如下:
服务端代码:
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define SSL_print_error_and_freectx() \
ERR_print_errors_fp(stdout); \
SSL_CTX_free(ctx);
int listenLocal(unsigned short uPort)
{
int sockS = socket(AF_INET, SOCK_STREAM, 0);
if (sockS < 0)
{
printf("socket() error! \n");
return -1;
}
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = INADDR_ANY;
sin.sin_port = htons(uPort);
int ret = bind(sockS, (struct sockaddr*)&sin, sizeof(sin));
if (ret < 0)
{
printf("bind() error! \n");
close(sockS);
return -1;
}
listen(sockS, 50);
return sockS;
}
int main(int argc, char* argv[])
{
SSL_library_init();
SSL_load_error_strings();
SSL_CTX* ctx = SSL_CTX_new( SSLv23_server_method() );
if (ctx == NULL)
{
ERR_print_errors_fp(stdout);
return -1;
}
int ret = SSL_CTX_use_certificate_file(ctx, "test.crt", SSL_FILETYPE_PEM);
if (ret != 1)
{
SSL_print_error_and_freectx();
return -1;
}
ret = SSL_CTX_use_PrivateKey_file(ctx, "test.key", SSL_FILETYPE_PEM);
if (ret != 1)
{
SSL_print_error_and_freectx();
return -1;
}
ret = SSL_CTX_check_private_key(ctx);
if (ret != 1)
{
SSL_print_error_and_freectx();
return -1;
}
int sockS = listenLocal(9999);
if (sockS < 0)
{
SSL_CTX_free(ctx);
return -1;
}
while (true)
{
struct sockaddr_in sinfrom;
socklen_t sinfromlen = sizeof(sinfrom);
int sockC = accept(sockS, (struct sockaddr*)&sinfrom, &sinfromlen);
if (sockC < 0)
{
printf("accept() error! \n");
break;
}
printf("accpet connect:[%d - %s:%d] \n", sockC, inet_ntoa(sinfrom.sin_addr), ntohs(sinfrom.sin_port));
fcntl(sockC, F_SETFL, fcntl(sockC, F_GETFL) | O_NONBLOCK);
SSL* ssl = SSL_new(ctx);
SSL_set_fd(ssl, sockC);
do
{
bool bHandShake = false;
while (true)
{
ret = SSL_accept(ssl);
printf("SSL_accept() ret:[%d] \n", ret);
if (ret != 1)
{
int err = SSL_get_error(ssl, ret);
if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE)
{
printf("want read or write... \n");
usleep(1000);
continue;
}
break;
}
bHandShake = true;
break;
}
if (!bHandShake)
{
printf("SSL handshake failed! \n");
break;
}
printf("SSL handshake success! \n");
SSL_write(ssl, "hello", 5);
while (true)
{
char sBuf[1024] = {0};
int bytesin = SSL_read(ssl, sBuf, sizeof(sBuf)-1);
if (bytesin < 0)
{
int err = SSL_get_error(ssl, -1);
if (err == SSL_ERROR_WANT_READ)
{
printf("want read... \n");
usleep(100000);
continue;
}
printf("read error! \n");
break;
}
if (bytesin == 0)
{
printf("disconnect! \n");
break;
}
printf("read:[%s] \n", sBuf);
}
SSL_shutdown(ssl);
} while(0);
SSL_free(ssl);
close(sockC);
}
close(sockS);
SSL_CTX_free(ctx);
return 0;
}
客户端代码:
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int connectPeer(const char* sIp, unsigned short uPort)
{
int sockC = socket(AF_INET, SOCK_STREAM, 0);
if (sockC < 0)
{
printf("socket() errror! \n");
return -1;
}
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = inet_addr(sIp);
sin.sin_port = htons(uPort);
int ret = connect(sockC, (struct sockaddr*)&sin, sizeof(sin));
if (ret < 0)
{
printf("connect() failed! \n");
close(sockC);
return -1;
}
return sockC;
}
void showCert(SSL* ssl)
{
X509* x509 = SSL_get_peer_certificate(ssl);
if (x509)
{
const char* subjectname = X509_NAME_oneline(X509_get_subject_name(x509), 0, 0);
printf("subject name:[%s] \n", subjectname);
const char* issuername = X509_NAME_oneline(X509_get_issuer_name(x509), 0, 0);
printf("issuer name:[%s] \n", issuername);
X509_free(x509);
}
}
int main(int argc, char* argv[])
{
SSL_library_init();
SSL_load_error_strings();
SSL_CTX* ctx = SSL_CTX_new( SSLv23_client_method() );
if (ctx == NULL)
{
ERR_print_errors_fp(stdout);
SSL_CTX_free(ctx);
return -1;
}
int sockC = connectPeer("127.0.0.1", 9999);
if (sockC < 0)
{
SSL_CTX_free(ctx);
return -1;
}
fcntl(sockC, F_SETFL, fcntl(sockC, F_GETFL) | O_NONBLOCK);
SSL* ssl = SSL_new(ctx);
SSL_set_fd(ssl, sockC);
do
{
bool bHandShake = false;
while (true)
{
int ret = SSL_connect(ssl);
printf("SSL_connect() ret:[%d] \n", ret);
if (ret != 1)
{
int err = SSL_get_error(ssl, -1);
if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE)
{
printf("want read or write... \n");
usleep(1000);
continue;
}
break;
}
bHandShake = true;
break;
}
if (!bHandShake)
{
printf("SSL handshake failed! \n");
break;
}
printf("SSL handshake success! \n");
showCert(ssl);
bool bShutDown = false;
while (true)
{
char sBuf[1024] = {0};
int bytesin = SSL_read(ssl, sBuf, sizeof(sBuf)-1);
if (bytesin < 0)
{
int err = SSL_get_error(ssl, -1);
if (err == SSL_ERROR_WANT_READ)
{
printf("want read... \n");
usleep(100000);
continue;
}
printf("read error! \n");
SSL_shutdown(ssl);
bShutDown = true;
break;
}
if (bytesin == 0)
{
printf("disconnect! \n");
SSL_shutdown(ssl);
bShutDown = true;
break;
}
printf("read:[%s] \n", sBuf);
break;
}
if (bShutDown)
break;
for (int i = 0; i < 3; ++i)
{
SSL_write(ssl, "hello", 5);
sleep(1);
}
SSL_shutdown(ssl);
} while (0);
SSL_free(ssl);
close(sockC);
SSL_CTX_free(ctx);
return 0;
}