OpenSSL之SSL用法

什么是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的通用开发流程示意:
image.png

无论是服务端还是客户端,进行一次完整的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服务端接口调用流程示意:
image.png
图3:SSL客户端接口调用流程示意:
image.png

图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 
#include 
#include 

#include 
#include 

#include 
#include 
#include 
#include 
#include 
#include 

#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 
#include 
#include 

#include 
#include 

#include 
#include 
#include 
#include 
#include 
#include 

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 
#include 
#include 

#include 
#include 

#include 
#include 
#include 
#include 
#include 
#include 

#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 
#include 
#include 

#include 
#include 

#include 
#include 
#include 
#include 
#include 
#include 

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;
}

你可能感兴趣的:(OpenSSL之SSL用法)