OpenSSL代码阅读

来源:互联网 发布:spark时间序列算法 编辑:程序博客网 时间:2024/06/08 16:58

毫无疑问,OpenSSL是当今使用最广泛的SSL和加密套件的实现。但是由于其代码的低质量,OpenBSD重新实现了LibreSSL,谷歌更加激进的实现了BoringSSLBoringSSL专门服务于TLSHTTPS,但是OpenSSL还有很多其他的功能,OpenSSL是一个大而全的系统。BoringSSL已经在逐步应用到谷歌的所有项目中,主体由C++写成。

本书重点介绍OpenSSL,同时带有对其他两种实现的对比和介绍性分析。OpenSSL由两个库组成,一个是加密库libcrypto.so,另外一个是SSLlibssl.so。一般在发行版中会以共享库的形式安装,但是在编译其他使用到OpenSSL的应用的时候,一般以静态库的形式组装。比如手动编译nginx,使用nginxconfigure选项指定OpenSSL的源代码目录,就是静态编译的。

libcrypto.so可以单独使用,里面定义的是各种各样的哈希和加密算法。对外提供的是叫做EVP的统一接口。像libssl.so库在使用libcrypto.so的时候大部分情况都是直接使用EVP接口的函数。EVP中一个重要的封装是一些分层次的密码学上下文结构体,其最外层是EVP_CIPHER_CTX结构体,这个结构体是libssl.so中要使用libcrypto.so中的数学功能的时候的通用接口,无论下层究竟是数学运算,MD5、加密等等,都是通过统一的这个上下文结构体配合通用的函数调用实现的。在下层,每一种算法都会对应的转换为私有的上下文结构体,例如EVP_AES_GCM_CTX就是GCM模式的AES上下文结构体。EVP_AES_GCM_CTX *gctx = (EVP_AES_GCM_CTX *)ctx->cipher_data。其中ctxEVP_AES_GCM_CTX结构体对象。

我们知道有很多加密和哈希算法,都是重度耗费CPU的数学运算。也存在很多的硬件来加速这个过程,甚至内核内部都实现了一套类似的数学系统。OpenSSL起初也是都自己在应用层编写了数学运算的逻辑,但是很快OpenSSL就发现了这么做在效率上的不足。为了适配可能的多种底层加密的具体实现,OpenSSL实现了CryptoEngine。无论是OpenSSL的用户端实现,还是内核的软件实现,还是cryptodev的硬件实现,还是更加激进的Intel QAT硬件,都是CryptoEngine的一种。CryptoEnginelibcrypto.so的其他逻辑提供了一个统一的接口封装。也正是因此,人们在看OpenSSL的代码的时候,经常会气愤于OpenSSL代码到处存在的方发表。因为方法表是C语言最常用的对上层提供统一接口,而下层实现不同驱动的编程手法。libcrypto.so还有一个重要的封装,是对I/O的封装,叫做BIOBIO处理一切的读写,常见的是Socket的读写和文件的读写。SSL_readlibssl.so中读取数据的常用方法,它内部直接调用了Socketrecv函数,因此在调用SSL_read之前首先要绑定Socket对应的fdSSL结构体(SSL_set_fd)。而SSL_set_fd函数的内部是将Socketfd生成了两个不同的BIO,一个负责读,一个负责写。也因此,还存在SSL_set_rfdSSL_set_wfd两个分别设置读写的函数。

 

libssl.so是一个功能性的专用库,用于实现SSLTLS各个版本的协议逻辑。SSL目前已经基本退出使用,TLS常用的是1.2版本,但是目前的1.3版本由于提高了性能,也会逐渐的普及。TLS协议本身并不复杂,但是多个版本并存,每个版本还支持很多种不同的密钥交换算法,数据传输的对称加密算法和用于验数据是否篡改的哈希算法。这些支持都要在libssl.so中实现,因此libssl.so中的实现也相当复杂。但是谷歌的BoringSSL库也实现了类似的功能,代码就简单很多。

由于OpenSSL最早是支持的SSL协议起步的,所以在libssl.so中仍然有很多以ssl命名的通用逻辑。例如在对称密钥生成的过程中最重要的ssl->s3结构体(确切的说是ssl->s3->tmp),其命名s3就已经失去了字面的意思,TLS也用的这个结构体生成密钥。在libssl.so中最表层的是一个状态机系统,在ssl/statem/目录下。

 * MSG_FLOW_UNINITED     MSG_FLOW_RENEGOTIATE

 *        |                       |

 *        +-----------------------+

 *        v

 * MSG_FLOW_WRITING <---> MSG_FLOW_READING

 *        |

 *        V

 * MSG_FLOW_FINISHED

 *        |

 *        V

 *    [SUCCESS]

这个状态机主要就是控制在读过程和写过程中切换,而读过程和写过程又分别对应自己的子状态机。

 * READ_STATE_HEADER <--+<-------------+

 *        |             |              |

 *        v             |              |

 * READ_STATE_BODY -----+-->READ_STATE_POST_PROCESS

 *        |                            |

 *        +----------------------------+

 *        v

 * [SUB_STATE_FINISHED]

 

 * +-> WRITE_STATE_TRANSITION ------> [SUB_STATE_FINISHED]

 * |             |

 * |             v

 * |      WRITE_STATE_PRE_WORK -----> [SUB_STATE_END_HANDSHAKE]

 * |             |

 * |             v

 * |       WRITE_STATE_SEND

 * |             |

 * |             v

 * |     WRITE_STATE_POST_WORK

 * |             |

 * +-------------+

 

读写状态机的设计也都不是产品层面的,而是实现层面的。这么说的意思是这些状态的设计是服务于OpenSSL内部的功能逻辑的,因为这部分的状态机也是一段时间之后因为代码质量实在太差而重构过的。看代码的时候明白一个机制的来由是很重要的。否则你就很可能会对照着RFC中对整个流程的解释来理解最上层的状态机的状态。这个状态机是实现层面的,与功能层面的关系不大。

整个libssl.so建立在SSL_CTX结构体和SSL结构体的基础之上。SSL_CTXSSL上下文的意思,SSL表示的是一条SSL连接。上下文在一个程序中只需要存在一个实力,一般用于存放设置SSL结构体生成时候的末人参数,还有诸如证书等不只属于某一条连接的信息。通过SSL_CTX生成SSL结构体(SSL_new(SSL_CTX)),SSL结构体里的很大一部分参数是直接从SSL_CTX中拷贝过来的。我们必须要时刻的对协议和密码学算法做出区分。协议依赖于密码学算法,甚至SSL协议本身的定义到处都是密码学的内容,但是我们仍然应该知道密码学相关实现是位于libcrypto.so中。

当我们拿到任何一个结构体的时候,几乎都会去想这个结构体的意义,以及为何这个结构体中要有这么多的成员。在仔细越多代码之后方能逐渐的体会这些成员设计的原因。因此阅读代码的时候一定要有主线,而不是逐个去分析每一个结构体域的意义。尤其是像OpenSSL这种缺少注释和文章的代码,阅读OpenSSL的握手过程,最好的方法是使用gdb命令的tui参数,然后单步跟踪,边执行边看代码,事半功倍。

 

 

 

 

原创粉丝点击