现在的位置: 首页 > 综合 > 正文

openssl框架闲谈–SSL实现

2013年10月28日 ⁄ 综合 ⁄ 共 4177字 ⁄ 字号 评论关闭

BIO 和EVP的一个应用就是SSL,没有SSL这个应用,BIO或者EVP只不过是一些底层的支撑接口,没有任何的现实意义,正是SSL使用了BIO和EVP 的机制提供了一个已经成型的安全套接字的实现策略。其实想象一下,安全套接字有两层含义,一层就是安全,这个由EVP接口实现了,另外一层含义就是套接 字,也就是说它必须是一个套接字,必须在操作的网络协议栈上进行IO,这一层含义是在BIO接口体现的,这个意义上,SSL正是通过组合BIO和EVP来 实现安全套接字的,BIO除了提供底层的抽象接口之外并不和SSL存在别的方面的耦合,因此BIO可以单独被使用,同样的,EVP也是可以单独被使用的。
不过,继续我们美妙的旅程之前首先要说的一点是,SSL本身就是一个BIO类型,并且是属于过滤类型的,在它的下层必须有一个socket类型的源/目的类型的BIO,在openssl中自带的sconnect实例中体现了这一点,创建过程如下:
SSL_load_error_strings();
OpenSSL_add_ssl_algorithms();
ssl_ctx=SSL_CTX_new(SSLv23_client_method());
ssl=SSL_new(ssl_ctx);
SSL_set_connect_state(ssl);
ssl_bio=BIO_new(BIO_f_ssl());
BIO_set_ssl(ssl_bio,ssl,BIO_CLOSE);
out=BIO_new(BIO_s_connect());
BIO_set_conn_hostname(out,host);
BIO_set_nbio(out,1);
out=BIO_push(ssl_bio,out);   //将socket类型的BIO置于SSL类型的BIO之下,注意,BIO_push里面会调用BIO_ctrl
对于ssl_bio的BIO_ctrl,它的BIO_CTRL_PUSH命令实际上就是执行:
static long ssl_ctrl(BIO *b, int cmd, long num, void *ptr)
{
...
    case BIO_CTRL_PUSH:
        if ((b->next_bio != NULL) && (b->next_bio != ssl->rbio))
        {
            SSL_set_bio(ssl,b->next_bio,b->next_bio);  //b->next_bio其实就是上面的out,它是一个
socket类型的BIO
            CRYPTO_add(&b->next_bio->references,1,CRYPTO_LOCK_BIO);
        }
        break;
...
}
可 以看出,ssl类型的BIO并没有使用一般的过滤BIO的模式,在其write回调函数中继续调用 SSL_write(b->next_bio,...)的模式,而是直接在SSL_push中使用回调函数将这个socket类型的BIO设置为 SSL的wbio,这样的话就可以使用统一的SSL_write来发送数据了。因为openssl设计的正交化,它就很灵活,各种实现方式对于 openssl来说都是可能的。
     openssl可以使用SSL和BIO两种方式实现SSL,如果使用BIO方式,那么就是上面说的这一种,最终的IO是要调用BIO_write和 BIO_read来进行的,用BIO实现的ssl关键在于在io之前必须设置好套接字BIO和ssl类型的BIO以及SSL结构体之间的联系,这是通过 BIO_set_ssl和BIO_push来实现的。
另外一种实现ssl的方式更加简单,大致流程如下:ssl = SSL_new (ctx)之后使用SSL_set_fd (ssl, sd),其中sd是使用socket函数创建的套接字文件描述符,之后就可以使用SSL_read和SSL_write进行io操作了。这种方式显然很简 单,这二种方式其实效果是一样的,一种是构建于BIO之上,另一种是构建于一个新的特殊的SSL之上,殊途同归。openssl对于ssl的实现很好的一 点就是将复杂的ssl协议的握手操作隐藏到了io的过程当中,不管哪种方式实现的ssl,最终都要调用sslX_write,比如ssl2_write, 这个函数中会判断握手是否已经完成,如果没有,那么就会调用该SSL结构的handshake_func回调函数,这个回调函数也可以手工调用,在 SSL_write之前首先调用SSL_accept或者SSL_connect来手工调用handshake_func函数,这个 handshake_func是在什么时候赋值给SSL结构的呢?可以在SSL_accept/connect中进行懒惰的赋值,也可以使用 BIO_ctrl进行手动赋值。
     ssl的握手操作怎么实现的呢?其实和操作系统内核的TCP三次握手是一样的,通过一个状态机实现,其实你完全可以将ssl协议理解成协议栈的一部分,这 样的话,ssl的握手仅仅是比tcp更高层次的协议握手罢了,状态机的实现将握手过程和协议的其它部分隔离开来,握手的状态机转换过程被封装在了一个无限 循环之中。
     openssl代码中最有意思的就是加密和解密算法了,相信大部分人都会被吸引的。可惜本系列文章不能大谈加密算法,只能简单说一下算法怎么结合到程序的 流程里面。简单的说,有两种方式在ssl中使用加密算法,第一种就是使用BIO接口,使用一个加密解密类型的BIO摞到一个套接字的BIO之上,在加密解 密的BIO之上再摞一个buffer类型的BIO就可以了,加密解密的BIO的作用就是数据加密解密,buffer类型的BIO的作用是添加协议头部和校 验,最下面的socket类型的BIO的作用就是将数据发往传输层。这种方式十分的和谐,并且和已有的协议栈相处得十分融洽,安全套接字层的意义也正是如 此。另外一种加密解密的使用方式就是隐藏这一切,直接使用SSL_write和SSL_read进行通信,每一个SSL结构体都有一个 EVP_CIPHER_CTX类型的字段,在SSL_write中实现加密,加密数据的缓冲区也在SSL结构中被分配,一个SSL结构体定义如下:
struct ssl_st
{
    int version;           //版本号
    int type;         //是服务器还是客户端
    SSL_METHOD *method;     //ssl的回调函数
#ifndef OPENSSL_NO_BIO        //如果ssl使用BIO机制
    BIO *rbio;             //包含一个套接字,下同
    BIO *wbio;
    BIO *bbio;
#else                //反之就提供一个缓冲区
    char *rbio;
    char *wbio;
    char *bbio;
#endif
    int rwstate;
    int in_handshake;    //是否正在握手
    int (*handshake_func)();//握手函数,它其实在SSL_METHOD中被提供
...
    STACK_OF(SSL_CIPHER) *cipher_list;    //算法列表
    STACK_OF(SSL_CIPHER) *cipher_list_by_id;
    EVP_CIPHER_CTX *enc_read_ctx;   //解密使用的上下文环境
...
    struct ssl2_state_st *s2;  //版本2的特定结构,缓冲区在内部被包含
    struct ssl3_state_st *s3;  //版本3的特定结构,缓冲区在内部被包含
...
    EVP_CIPHER_CTX *enc_write_ctx;  //加密使用的上下文环境
...
    SSL_CTX *ctx;        //此ssl的上下文环境
...
};
可 以看出这个SSL结构有点自成体系的味道,其实它本应该自成体系的,否则它就真成了BIO和EVP的demo了,所有的相关的东西都在SSL中,包括加 密,解密使用的数据结构,加密,解密使用的缓冲区,最底层的socket类型的BIO以及其他ssl协议相关联的概念,这套代码之所以叫做openssl 而不是openbio或者openevp的原因吧。按照这种方式使用加密和解密虽然可能没有BIO的方式直观,但是却很直接,SSL指针中随时可以取出加 密解密结构体然后操作同样位于SSL结构中的加密解密缓冲区,最后发送给同样位于SSL中的wbio或者从rbio接收数据。在SSL_write最终要 调用的do_ssl3_write函数中会有以下逻辑:
s->method->ssl3_enc->enc(s,1);
而这个>ssl3_enc是另一套回调函数集合,是关于加密和解密的,其中的enc回调函数对于ssl3就是ssl3_enc:
int ssl3_enc(SSL *s, int send)
{
    SSL3_RECORD *rec;
    EVP_CIPHER_CTX *ds;
...
    const EVP_CIPHER *enc;
    if (send)
    {
        ds=s->enc_write_ctx;       //得到ssl的加密解密上下文环境
        rec= &(s->s3->wrec);    //得到ssl加密解密的缓冲区
        enc=EVP_CIPHER_CTX_cipher(s->enc_write_ctx);
    }
...
        EVP_Cipher(ds,rec->data,rec->input,l); //加密,ds为上下文,后面的参数为输入和输出缓冲区
...
}
可 能是由于效率的原因,openssl采用了自成体系的SSL的方式而没有采用串联BIO的方式,但是我个人还是很看好串联BIO的方式的,串联的BIO实 际上串联了不同的过滤策略,你当然可以理解成封包规则,网络的分层协议不就是靠这种方式实现的吗?每一层的封装其实就是一个过滤器,就是一个过滤策略,用 BIO来理解网络封包再好不过了。

抱歉!评论已关闭.