openssl
2020-09-10 13:44:52 5 举报
AI智能生成
openssl ssl/tls 源码分析
作者其他创作
大纲/内容
编译
安装 perl
vs2017环境编译
MD 动态库
perl configure VC-WIN32 --prefix="D:\openssl\2017_md" enable-crypto-mdebug threads no-asm
nmake
nmake install
nmake clean
MT 动态库
perl configure VC-WIN32 --prefix="D:\openssl\2017_mt" enable-crypto-mdebug threads no-asm
修改生成的makefile md改为mt
CNF_CFLAGS=/Gs0 /GF /Gy /MT
nmake
nmake install
nmake clean
静态库生成
perl configure VC-WIN32 --prefix="D:\openssl\2017_static" no-shared no-asm
linux编译
gmssl
编译命令
./config --prefix="/usr/local/gmssl"
make
make test
sudo make install
设置库变量
临时设置
export LD_LIBRARY_PATH=/usr/local/gmssl/lib:$LD_LIBRARY_PATH
export PATH=/usr/local/gmssl/bin:$PATH
修改 ~/.bashrc 或 ~/.bash_profile或系统级别的/etc/profile
export LD_LIBRARY_PATH=/usr/local/gmssl/lib:$LD_LIBRARY_PATH<br>export PATH=/usr/local/gmssl/bin:$PATH<br>
android 交叉编译
下载 android-ndk-r20b
export ANDROID_NDK_HOME=/home/.../android-ndk-r20b
export PATH=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin:$ANDROID_NDK_HOME/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin:$PATH
./Configure android-arm --prefix=/usr/src/openssl_android
android-arm<br> android-arm64<br>
make
make install
参数
安装路径
--prefix=DESTDIR
编译配置
debug-VC-WIN32
debug
VC-WIN32
release
zlib使用
zlib
zlib_dynamic
no-zlib
线程
threads
no-threads
动态库
shared
no-shared
使用汇编代码
asm
no-asm
SSE2指令加速
enable-sse2
no-sse2
GMP
gmp
no-gmp
启用/禁用实现X509v3证书的IP地址扩展
rfc3779
no-rfc3779
启用/禁用 Kerberos 5 支持
krb5
no-krb5
ssl/tsl
ssl<br>no-ssl<br>ssl2<br>ssl3<br>no-ssl2<br>no-ssl3<br>tls<br>no-tls
启用/禁用调用其它动态链接库的功能。[提示]no-dso仅在no-shared的前提下可用
dso
no-dso
禁用选项
摘要算法
no-md2,no-md4,no-mdc2,no-ripemd
对称算法
no-des,no-rc2,no-rc4,no-rc5,no-idea,no-bf,no-cast,no-camellia
非对称算法
no-ec,no-dsa,no-ecdsa,no-dh,no-ecdh
数据压缩算法
no-comp
对象存储功能
no-store
SSL/TLS
源码解析
初始化
// 注册可用的SSL/TLS密码和摘要<br>SSL_library_init()<br>
//加载所有可用的算法<br>OpenSSL_add_all_algorithms()<br>
OPENSSL_init_crypto
//可读错误信息<br>SSL_load_error_strings()<br>
上下文结构 SSL_CTX
含有结构SSL的主要默认值,包含SSL会话结构
SSL_SESSION
含有链接的当前TLS/SSL会话细节
SSL_CIPHER
含有制定的加密算法信息
SSL_METHOD
功能函数接口,统一了各种SSL协议版本(SSLv1, SSLv2, SSLv3, TLSv1); 通过宏实现
# define IMPLEMENT_tls_meth_func(version, flags, mask, func_name, s_accept, s_connect, enc_data) :<br><br>IMPLEMENT_tls_meth_func(TLS1_2_VERSION, 0, SSL_OP_NO_TLSv1_2,<br> tlsv1_2_server_method,<br> ossl_statem_accept,<br> ssl_undefined_function, TLSv1_2_enc_data)<br>
version
用于指定 methos的版本
s_accept
指定状态机
ossl_statem_accept
state_machine(s, 1)
s_connect
指定连接的函数
目前没有定义 <br>ssl_undefined_function<br>
enc_data
指定加密结构数据
如:TLSv1_2_enc_data
SSL协议分析
ClientHello过程分析
握手过程图
客户端发送ClientHello
手第一步是客户端向服务端发送 Client Hello 消息,这个消息里包含了一个客户端生成的随机数 Random1、客户端支持的加密套件(Support Ciphers)和 SSL Version 等信息。通过 Wireshark 抓包,我们可以看到如下信息<br>
子主题
个消息会从 Client Hello 传过来的 Support Ciphers 里确定一份加密套件,这个套件决定了后续加密和生成摘要时具体使用哪些算法,另外还会生成一份随机数 Random2。注意,至此客户端和服务端都拥有了两个随机数(Random1+ Random2),这两个随机数会在后续生成对称秘钥时用到。<br>
服务端向客户端发送ServerHello
服务端将自己的证书下发给客户端
Server Key Exchange
如果是DH算法,这里发送服务器使用的DH参数。RSA算法不需要这一步<br>
Certificate Request
Certificate Request 是服务端要求客户端上报证书,这一步是可选的,对于安全性要求高的场景会用到。
Server Hello Done
Server Hello Done 通知客户端 Server Hello 过程结束
Certificate Verify
客户端收到服务端传来的证书后,先从 CA 验证该证书的合法性,验证通过后取出证书中的服务端公钥,再生成一个随机数 Random3,再用服务端公钥非对称加密 Random3 生成 PreMaster Key。<br>
Client Key Exchange
上面客户端根据服务器传来的公钥生成了 PreMaster Key,Client Key Exchange 就是将这个 key 传给服务端,服务端再用自己的私钥解出这个 PreMaster Key 得到客户端生成的 Random3。至此,客户端和服务端都拥有 Random1 + Random2 + Random3,两边再根据同样的算法就可以生成一份秘钥,握手结束后的应用层数据都是使用这个秘钥进行对称加密。为什么要使用三个随机数呢?这是因为 SSL/TLS 握手过程的数据都是明文传输的,并且多个随机数种子来生成秘钥不容易被暴力破解出来。客户端将 PreMaster Key 传给服务端的过程如下图所示:<br>
Change Cipher Spec(Client)
这一步是客户端通知服务端后面再发送的消息都会使用前面协商出来的秘钥加密了,是一条事件消息。
Encrypted Handshake Message(Client)
这一步对应的是 Client Finish 消息,客户端将前面的握手消息生成摘要再用协商好的秘钥加密,这是客户端发出的第一条加密消息。服务端接收后会用秘钥解密,能解出来说明前面协商出来的秘钥是一致的。
Change Cipher Spec(Server)
这一步是服务端通知客户端后面再发送的消息都会使用加密,也是一条事件消息
Application Data
到这里,双方已安全地协商出了同一份秘钥,所有的应用层数据都会用这个秘钥加密后再通过 TCP 进行可靠传输。
Encrypted Handshake Message(Server)
这一步对应的是 Server Finish 消息,服务端也会将握手过程的消息生成摘要再用秘钥加密,这是服务端发出的第一条加密消息。客户端接收后会用秘钥解密,能解出来说明协商的秘钥是一致的。
双向验证
握手过程优化
如果每次重连都要重新握手还是比较耗时的,所以可以对握手过程进行优化。从下图里我们看到 Client Hello 消息里还附带了上一次的 Session ID,服务端接收到这个 Session ID 后如果能复用就不再进行后续的握手过程。<br>
SSL_connect 对服务器发送ClientHello
SSL_connect
简单的调用 SSL_do_handshake
SSL_do_handshake
使用不同版本的TLS协议,所以 SSL_do_handshake 最终调用 s->handshake_func(s)<br> s->handshake_func 的赋值语句,发现了赋值为 ossl_statem_connect<br>
ossl_statem_connect
简单的对 state_machine(s, 0); 函数进行调用
state_mashine
协议状态机函数<br>根据 st->state 以及 st->hand_state 等协议状态,进行不同的操作<br>
write_state_machine
初始化一些函数指针,根据当前握手状态(ClientHello),调用 tls_construct_client_hello 函数
tls_construct_client_hello
这个函数就是最底层的组包函数了。(在 statem_clnt.c 文件中)
wireShark 抓ClientHello
协议状态机
客户端状态机
源码
write_state_machine
write_state_machine() 是SSL_connect 的第一个状态
分析
进入“写状态机”前,要进行一些简单的 状态标志 初始化。有: state , write_state , hand_state ,
//在 statem.c 的 state_machine 函数中初始化 <br>st->hand_state = TLS_ST_BEFORE;<br>st->request_state = TLS_ST_BEFORE;<br>st->state = MSG_FLOW_WRITING;<br><br>//在 statem.c 的 init_write_state_machine 函数中初始化<br>st->write_state = WRITE_START_TRANSITION;
根据当前执行的是客户端代码还是服务端代码,对一些关键 函数指针 初始化
static SUB_STATE_RETURN write_state_machine(SSL *s)<br>{<br> OSSL_STATEM *st = &s->statem;<br> int ret;<br> WRITE_TRAN(*transition) (SSL *s);<br> WORK_STATE(*pre_work) (SSL *s, WORK_STATE wst);<br> WORK_STATE(*post_work) (SSL *s, WORK_STATE wst);<br> int (*get_construct_message_f) (SSL *s, WPACKET *pkt,<br> int (**confunc) (SSL *s, WPACKET *pkt),<br> int *mt);<br> void (*cb) (const SSL *ssl, int type, int val) = NULL;<br> int (*confunc) (SSL *s, WPACKET *pkt);<br> int mt;<br> WPACKET pkt;<br><br> cb = get_callback(s);<br><br> if (s->server) {<br> transition = ossl_statem_server_write_transition;<br> pre_work = ossl_statem_server_pre_work;<br> post_work = ossl_statem_server_post_work;<br> get_construct_message_f = ossl_statem_server_construct_message;<br> } else {<br> transition = ossl_statem_client_write_transition;<br> pre_work = ossl_statem_client_pre_work;<br> post_work = ossl_statem_client_post_work;<br> get_construct_message_f = ossl_statem_client_construct_message;<br> }
写状态机的循环
根据 write_state ,执行不同基本块。根据前面的初始化,我们知道先执行的是<br>WRITE_STATE_TRANSITION 基本块。
先判断是否注册了回调函数,有则调用回调函数;接着调用 transition() 指针函数。<br>根据前面初始化 transition 函数指针的代码,分析 ossl_statem_client_write_transition()<br>函数。<br>
根据官方注释可以知道,这个函数判断握手状态应该迁移到哪个状态。函数根据 hand_state<br>执行不同基本块。前面 hand_state 被赋值为 TLS_ST_BEFORE 。让我们看看这个基本块做了啥。 <br>
<br>
返回 执行 WRITE_STATE_PRE_WORK 基本块
调用 pre_wrok() 指针函数,并根据返回值做别的操作……分析 pre_work() 函数
这是客户端发送服务端消息前的一些必要的预处理。根据 hand_state 执行
前面 hand_state 被赋值为 TLS_ST_CW_CLNT_HELLO
完成后就直接返回 WORK_FINISHED_CONTINUE 。返回到 write_state_machine() 继续分析:
write_state 为 WRITE_STATE_SEND ,接着调用 get_construct_message_f() 指针函数,即 ossl_statem_client_construct_message() 函数
获取 消息构造函数 以及 消息的类型。 我已经高亮了根据前面的分析,应该执行的代码块。OK返回到 write_state_machine() 继续分析:
调用 confunc() 函数指针,即 tls_construct_client_hello() 函数
执行完 ssl_close_construct_packet() 后,就返回到循环语句,执行 switch(st->write_state)
调用 statem_do_write() 函数,即 发送预构造的消息给伙伴(服务器)
因为 hand_state 的值为 TLS_ST_CW_CLNT_SEND ,所以调用 ssl_do_write() 函数
这个函数最后到底调用哪个函数是根据客户端初始的 TLS版本决定的 ,暂时就不分析了
statem_do_write() 函数返回后执行的代码,修改了 write_state 和 write_state_work。<br>进入 WRITE_STATE_POST_WORK 基本块,调用 post_work() 指针函数
post_work() 即 ossl_statem_client_post_work() 函数
执行一些发送消息的善后函数。这里我就不展开说明了。函数返回 WORK_FINISHED_CONTINUE
write_state 又回到了 WRITE_STATE_TRANSITION 状态?这是个死循环?不,因为 hand_state<br>不同了,所以 transition() 指针函数 将返回 WRITE_TRAN_FINISHED ,结束
返回值是 SUB_STATE_FINISHED ,所以将调用 init_read_state_machine() 函数,<br>并修改 state 为 MSG_FLOW_READING 进入“读取状态机”。
读状态机
简图
写状态机循环
read_state初始化为 READ_STATE_HEADER<br>
判断是否为DTLS,我用的是TLS ,所以分析 tls_get_message_header() 函数
所以分析 tls_get_message_header() 函数
读取一字节的消息类型: SSL3_RT_HANDSHAKE 或者 SSL3_RT_CHANGE_CIPHER_SPEC;<br>其他的类型都是错误的 握手包。 可以在 WireShark 抓包中看到,只要是握手包协议,开头第一字节<br>都是 0x16 ,即 SSL3_RT_HANDSHAKE 的值
初始化指针
循环退出后,根据接收的数据包协议版本,做不同的指针初始化
返回到read状态机,调用 transition()<br>即 ossl_statem_client_read_transition()<br>
这个函数根据从服务器读取的信息,修改握手状态,接收的数据包类型在 mt 参数中,<br>当前 握手状态 在 s->statem.hand_state 中
子主题
根据 当前握手状态,我们分析 TLS_ST_CW_CLNT_HELLO 基本块
判断消息类型是否为 SSL3_MT_SERVER_HELLO ,是则修改 握手状态为<br>TLS_ST_CR_SRVR_HELLO
正常情况下,客户端发送 ClientHello ,服务器都会先响应一个 SSL3_MT_SERVER_HELLO 类型的握手数据包。所以在这里我就返回<br>分析了。
接着就是修改 read_state ,进入 READ_STATE_BODY 基本块分析
进入 tls_get_message_body() 函数,开始分析
如果消息类型是 修改密码套件 ,则直接返回
否则不断的读取数据到缓存中。
如果数据包结束了,就计算消息的 MAC 值,并返回。好了,返回分析到“读状态机”
调用 process_message() 指针函数,即 ossl_statem_client_process_message()函数。
进入 ossl_statem_client_process_message() 函数分析:
根据 hand_state 调用不同的处理过程,即不同的握手状态(Hello,证书验证,密钥交换,Finish)
调用不同的函数。当前的握手状态是 ServerHello,ok,进入 tls_process_server_hello() 函数分析。
读取两个字节的版本信息,读取服务端随机数,读取会话ID,读取加密方式,压缩方式,<br>以及扩展信息。可以从 WireShark 解析的协议中,清楚的看见以上信息:
有了服务器发送的加密方式,就设置客户端的加密方式:
扩展信息
子主题
最后根据TLS版本调用一些函数,然后返回:
返回值有多个,根据不同返回值进行不同处理:
在 READ_STATE_POST_PROCESS 块中,发送客户端的证书:<br><br>
openssl ssl/tsl编写
服务端编写步骤
客户端编写步骤
实例
建立非安全连接
1. 必要的头文件
2. BIO *bio
3. 创建并打开连接
4. 从连接中读取
BIO_read 将尝试从服务器读取一定数目的字节。它返回读取的字节数、 0 或者 -1。在受阻塞的连接中,该函数返回 0,表示连接已经关闭,而 -1 则表示连接出现错误。在非阻塞连接的情况下,返回 0 表示没有可以获得的数据,返回 -1 表示连接出错。可以调用 BIO_should_retry来确定是否可能重复出现该错误。
5. 从连接中写入
BIO_write 会试着将字节写入套接字。它将返回实际写入的 字节数、0 或者 -1。同 BIO_read ,0 或 -1 不一定表示错误。BIO_should_retry 是找出问题的途径。如果需要重试写操作,它必须 使用和前一次完全相同的参数。
6. 关闭连接
BIO_reset
关闭连接并重新设置 BIO 对象的内部状态,以便可以重新使用连接。如果要在整个应用程序中使用同一对象,比如使用一台安全的聊天 客户机,那么这样做是有益的。该函数没有返回值。
BIO_free_all
所做正如其所言:它释放内部结构体,并释放 所有相关联的内存,其中包括关闭相关联的套接字。如果将 BIO 嵌入于一个类中,那么应该在类的 析构函数中使用这个调用。
建立安全连接
7. 设置SSL 指针
8. 加载信任证书
在创建上下文结构之后,必须加载一个可信任证书库。这是成功验证每个证书所必需的。如果 不能确认证书是可信任的,那么 OpenSSL 会将证书标记为无效(但连接仍可以继续)。
OpenSSL 附带了一组可信任证书。它们位于源文件树的 certs 目录中。 不过,每个证书都是一个独立的文件 —— 也就是说,需要单独加载每一个证书。在 certs 目录下,还有一个存放过期证书的子目录。试图加载这些证书将会出错
如果您愿意,可以分别加载每一个文件,但为了简便起见,最新的 OpenSSL 发行版本的可信任证书 通常存放在源代码档案文件中,这些档案文件位于名为“TrustStore.pem”的单个文件中。如果已经有了一个可信任证书库, 并打算将它用于特定的项目中,那么只需使用您的文件替换清单 8 中的“TrustStore.pem”(或者使用 单独的函数调用将它们全部加载)即可。
可以调用 SSL_CTX_load_verify_locations 来加载可信任证书库文件。这里要用到 三个参数:上下文指针、可信任库文件的路径 和文件名,以及证书所在目录的路径。必须指定可信任库文件或证书的目录。 如果指定成功,则返回 1,如果遇到问题,则返回 0。
9. 配置证书文件夹并使用它
如果打算使用目录存储可信任库,那么必须要以特定的方式命名文件。OpenSSL 文档清楚 地说明了应该如何去做,不过,OpenSSL 附带了一个名为 c_rehash 的工具, 它可以将文件夹配置为可用于 SSL_CTX_load_verify_locations 的 路径参数
为了指定所有需要的验证证书,您可以根据需要命名任意数量的单独文件或文件夹。您还可以同时指定 文件和文件夹
10. 设置BIO对象
将指向 SSL 上下文的指针作为惟一参数,使用 BIO_new_ssl_connect 创建 BIO 对象。还需要获得指向 SSL 结构的指针。在本文中,只将该指针用于 SSL_set_mode 函数。而这个函数是用来设置 SSL_MODE_AUTO_RETRY 标记的。使用这个选项进行设置,如果服务器突然希望进行 一次新的握手,那么 OpenSSL 可以在后台处理它。如果没有这个选项,当服务器希望进行一次新的握手时, 进行读或写操作都将返回一个错误,同时还会在该过程中设置 retry 标记。
11.打开安全连接
设置 SSL 上下文结构之后,就可以创建连接了。主机名是使用 BIO_set_conn_hostname 函数 设置的。主机名和端口的指定格式与前面的相同。该函数还可以打开到主机的连接。为了确认已经成功打开连接,必须 执行对 BIO_do_connect 的调用。该调用还将执行握手来建立安全连接。
12.检查证书是否有效
连接建立后,必须检查证书,以确定它是否有效。实际上,OpenSSL 为我们完成了这项任务。如果证书有致命的 问题(例如,哈希值无效),那么将无法建立连接。但是,如果证书的问题并不是致命的(当它已经过期 或者尚不合法时),那么仍可以继续使用连接
可以将 SSL 结构作为惟一参数,调用 SSL_get_verify_result 来查 明证书是否通过了 OpenSSL 的检验。如果证书通过了包括信任检查在内的 OpenSSL 的内部检查,则返回 X509_V_OK。如果有地方出了问题,则返回一个错误代码,该代码被记录在命令行工具的 verify 选项下。
应该注意的是,验证失败并不意味着连接不能使用。是否应该使用连接取决于验证结果和安全方面的考虑。例如, 失败的信任验证可能只是意味着没有可信任的证书。连接仍然可用,只是需要从思想上提高安全意识。
13. 清除SSL 上下文
错误检查
SSL_load_error_strings<br>ERR_load_BIO_strings<br>
初始化错误,即加载一个永久错误的字符串到内存中
int ERR_get_error()
获取错误代码
const char *ERR_reason_error_string(ERR_get_error())
错误信息
ERR_lib_error_string
错误发生在那个库中
ERR_func_error_string
导致错误的函数
ERR_error_string(ERR_get_error(), NULL)
预先格式化了的错误字符串<br>如果参数 为 NULL,则 OpenSSL 会将字符串写入到一个长度为 256 字节的静态缓冲区中<br>
ERR_print_errors_fp(FILE *);<br>ERR_print_errors(BIO *);
转储错误队列
SSL/TLS编程
主要函数
int SSL_Library_init()<br>int SSLeasy_add_ssl_algorithms()
功能:初始化SSL算法库函数,调用SSL系列函数之前必须调用此函数
返回值:1成功,0失败
SSL_CTX* SSL_CTX_new(SSL_METHOD* meth)
SSL_METHOD *meth
SSLV2.0协议
用于客户端
SSLv2_client_method(void)
用于服务端
SSLv2_server_method(void)
SSLV3.0协议
用于客户端
SSLv3_client_method(void)
用于服务端
SSLv3_server_method(void)
SSL3.0协议支持2.0
用于客户端
SSLv23_client_method(void)
用于服务端
SSLv23_server_method(void)
TSL1.1协议
用于客户端
TLSv1_client_method(void)
用于服务端
TLSv1_server_method(void)
TSL1.2协议
TSL1.3协议
功能:初始化SSL_CTX结构体,设置SSL协议算法
返回值:调用成功返回SSL_CTX结构体指针,否则返回NULL
Void SSL_CTX_free(SSL_CTX* ctx)
释放SSL上下文环境变量函数
SSL_CTX_set_min_proto_version
设置协议的最小版本
SSL_CTX_set_max_proto_version
设置协议的最大版本
SSL_CTX_set_quiet_shutdown(ctx, 1)
设置“安静关机”标志SSL是模式。该设置将保持有效,直到通过SSL_free(3)删除ssl或再次调用SSL_set_quiet_shutdown()为止,调用SSL_clear(3)时不会更改。模式可以是0或1。<br>将“ quiet shutdown”标志设置为1时,SSL_shutdown(3)将内部标志设置为SSL_SENT_SHUTDOWN | SSL_RECEIVED_SHUTDOWN。(然后,SSL_shutdown(3)的行为类似于通过SSL_SENT_SHUTDOWN | SSL_RECEIVED_SHUTDOWN调用的SSL_set_shutdown(3)。)因此,该会话被视为已关闭<br>
SSL_CTX_sess_set_cache_size(ctx, 128)
默认的为1024*20=20000,这个也就是可以存多少个session_id,一般都不需要更改的。假如为0的话将是无限
SSL_CTX_set_mode(ctx, SSL_MODE_ASYNC)
设置异步
SSL_CTX_set_max_send_fragment
设置最大发送的缓存
SSL_CTX_set_split_send_fragment
设置分割
SSL_CTX_set_max_pipelines
SSL_CTX_set_default_read_buffer_len
设置读缓存长度
SSL_CTX_set_dh_auto(ctx, 1)
设置默认的dh算法
Int SSL_CTX_use_certificate_file(SSL_CTX* ctx,constchar* file,int type)
功能:以文件的形式设置SSL证书。对于服务器,用来设置服务器证书。对于客户端,用来设置客户端证书。
参数:file:[IN]证书文件路径。Type:[IN]证书的编码类型。支持SSL_FILETYPE_PEM(PEM格式,即Base64编码格式的文件),SSL_FILETYPE_ASN1(ASN1格式,即DER编码的文件)
返回值:1成功。0失败。
Int SSL_CTX_use_PrivateKey_file(SSL_CTX *ctx,constchar *file,int type)
函数功能:以文件的形式设置SSL私钥
Int SSL_CTX_use_certificate(SSL_CTX *ctx,X509 *x)
功能:设置证书,功能和SSL_CTX_use_certificate_file函数一样
Int SSL_CTX_use_PrivateKey(SSL_CTX *ctx,EVP_PKEY *pkey)
设置SSL私钥函数。功能和SSL_CTX_use_PrivateKey_file函数一样
Int SSL_CTX_check_private_key(constSSL_CTX *ctx)
功能:检查私钥,检查私钥是否和证书匹配。此函数必须在设置了证书和私钥后才能调用
SSL* SSL_new(SSL_CTX *ctx)
功能:为一个新SSL链接建立SSL*结构体变量
Void SSL_free(SSL *ssl)
释放SSL句柄函数
Int SSL_set_fd(SSL *s,int fd)
功能:为SSL结构体设置socket句柄
参数:fd:[IN]socket链接句柄
Int SSL_connect(SSL *ssl)
功能:建立SSL链接,用于客户端
Int SSL_accept(SSL *ssl)
接受SSL链接
X509* SSL_get_peer_certificate(constSSL *s)
获得SSL链接使用的证书
Int SSL_write(SSL *ssl,constvoid *buf,int num)
Int SSL_read(SSL *ssl,void *buf,intnum)
关闭SSL连接 SSL_shutdown(ssl)
当我们完成响应的传送,我们需要发送一个close_notify。如前所述,这是通过SSL_shutdown()完成的。不幸的是,当server先关闭时事情会有一些麻烦,我们第一次调用SSL_shutdown()时发送了close_notify但是另一端却没有处理它。因此,它会立刻返回一个0,表明关闭动作并没有完成。这样的话,程序就必须再次调用SSL_shutdown()。
1.我们已经得到了我们关心的所有HTTP请求。我们不在关心其他的任何事情。因此,我们不关心client是否发送了close_notify。
2. 我们严格遵守协议,并且希望其他人也如此。所以,我们需要一个close_notify。<br>如果我们拥有第一种态度,那么就简单多了:我们调用SSL_shutdown()发送close_notify然后立刻退出,不管client是否发送了close_notify。如果我们抱着第二种态度,那么事情就会变得复杂得多,因为client经常表现的不正常。
我们面对的第一个问题是通常client不发送close_notify。事实上,一些client在他们读取了整个HTTP回应后立刻关闭连接(一些版本的IE正是如此)。当我们发送close_notify,另一端会发送一个TCP_RST字段,这时程序会捕捉到SIGPIPE。我们在initialize_ctx()中安装一个假的SIGPIPE句柄来避免这种情况的发生。
我们面对的第二个问题是client可能不会立刻发送close_notify以回应我们发出的close_notify。一些版本的Netscape要求你先发送一个TCP FIN。因此,我们在第二次调用SSL_shutdown()之前调用shutdown(s,1)。当把“how”这个参数设置为1后,shutdown()会发送FIN并且不关闭socket。server端的shutdown如下:
openssl之SSL/TLS处理流程
客户端发出请求(client hello)
客户端支持的SSL/TLS协议版本,生成随机数 用于对称加密算法的对话密钥,提供客户端支持的加密算法,以及压缩算法
服务端回应(server hello)
服务端确认使用的加密通信的协议版本,如果不能支持客户端协议版本,服务端关闭加密通信;确认使用的加密算法,并且携带服务器证书,发送给客户端。服务端发送以下三种信息
生成随机数,用于生成对称加密算法的对话密钥
确认使用的加密算法
服务器证书
客户端回应
客户端取得服务端回应之后,首先验证服务器证书。证书存在问题,需要客户端进行处理,这也是为什么浏览器访问https地址有时会出现警告或者错误的原因;确认证书无误,客户端会从证书中取得服务器的非对称加密算法的公钥。同时会向服务端发送以下三种信息。
一个随机数,使用随机数对服务端发送的公钥进行非对称加密,防止窃听
加密改变通知,表示随后的信息使用双发协定的加密方法和密钥进行发送
客户端握手结束通知,表示客户端的握手阶段已经结束,并提供前面发送内容的hash值,用于服务端校验
服务端的回应
服务端取得客户端的生成的第三个 pre-master-key随机数,计算生成本次会话使用的对称加密算法的密钥,并向客户端发送以下两种信息
加密改变通知,表示随后的信息都将使用双方协定的对称加密算法和密钥进行加密
服务端握手结束通知,表示服务端握手阶段结束。同时提供前面内容的hash值,供客户端进行校验
WireShark 实验
Client Hello
32字节的随机数random
Session ID
客户端支持的密码套件Cipher Suites
以及压缩算法Compression Methods
Server Hello
3~6 Send certificate and Server Hello Done
7~13 key exchange and change cipher spec
openssl中相关函数
源码位于ssl目录下
实现了sslv2,sslv3,TLS以及DTLS
源码格式
客户端实现(XXX_clnt.c)
服务端实现(XXX_srvr.c)
加密实现(XXX_enc.c)
记录协议实现(XXX_pkt.c)
METHOD方法(XXX_meth.c)
客户端服务端都用到的握手方法实现(XXX_both.c)
以及对外提供的函数实现(XXX_lib.c)
openssl编写SSL,TLS
简介
证书文件生成
生成服务端私钥
openssl genrsa -des3 -out server.key 1024
生成服务端证书
openssl req -new -key server.key -out server.csr
ca发证
生成客户端证书
openssl genrsa -des3 -out client.key 1024
ca发证
常用的函数
int SSL_CTX_set_cipher_list(SSL_CTX *,const char *str);
根据SSL/TLS规范,在ClientHello中,客户端会提交一份自己能够支持的加密方法的列表,<br>由服务端选择一种方法后在ServerHello中通知服务端, 从而完成加密算法的协商.<br>
这些算法按一定优先级排列,如果不作任何指定,将选用DES-CBC3-SHA.用SSL_CTX_set_cipher_list可以指定自己希望用的算法(实际上只是 提高其优先级,是否能使用还要看对方是否支持).
SSL_CTX_set_cipher_list(ctx,”RC4-MD5”);
我们在程序中选用了RC4做加密,MD5做消息摘要(先进行MD5运算,后进行RC4加密).
在消息传输过程中采用对称加密(比公钥加密在速度上有极大的提高),其所用秘钥(shared secret)在握手过程中中协商(每次对话过程均不同, 在一次对话中都有可能有几次改变),并通过公钥加密的手段由客户端提交服务端.
void SSL_CTX_set_verify(SSL_CTX ctx,int mode,int (*callback)(int, X509_STORE_CTX ));
缺省mode是SSL_VERIFY_NONE,如果想要验证对方的话,便要将此项变成SSL_VERIFY_PEER.SSL/TLS中缺省只验证server,如果没有设置 SSL_VERIFY_PEER的话,客户端连证书都不会发过来.
int SSL_CTX_load_verify_locations(SSL_CTX *ctx, const char *CAfile,const char *CApath);
要验证对方的话,当然装要有CA的证书了,此函数用来便是加载CA的证书文件的.
int SSL_CTX_use_certificate_file(SSL_CTX *ctx, const char *file, int type);
加载自己的证书文件.
int SSL_CTX_use_PrivateKey_file(SSL_CTX *ctx, const char *file, int type);
加载自己的私钥,以用于签名.
int SSL_CTX_check_private_key(SSL_CTX *ctx);
调用了以上两个函数后,自己检验一下证书与私钥是否配对.
void RAND_seed(const void *buf,int num);
在win32 的环境中client程序运行时出错(SSL_connect返回-1)的一个主要机制便是与UNIX平台下的随机数生成机制不同(握手的时候用的到). 具体描述可见mod_ssl的FAQ.解决办法就是调用此函数,其中buf应该为一随机的字符串,作为”seed”.
void RAND_screen(void);
RAND_screen()以屏幕内容作为”seed”产生随机数,RAND_event可以捕获windows中的事件(event),以此为基础 产生随机数.如果一直有 用户干预的话,用这种办法产生的随机数能够”更加随机”,但如果机器一直没人理(如总停在登录画面),则每次都将产生同样的数字.
int RAND_event(UINT, WPARAM, LPARAM);
OpenSSL_add_ssl_algorithms()或SSLeay_add_ssl_algorithms()
其实都是调用int SSL_library_init(void)
进行一些必要的初始化工作,用openssl编写SSL/TLS程序的话第一句便应是它.
void SSL_load_error_strings(void );
如果想打印出一些方便阅读的调试信息的话,便要在一开始调用此函数.
void ERR_print_errors_fp(FILE *fp);
如果调用了SSL_load_error_strings()后,便可以随时用ERR_print_errors_fp()来打印错误信息了.
X509 *SSL_get_peer_certificate(SSL *s);
握手完成后,便可以用此函数从SSL结构中提取出对方的证书(此时证书得到且已经验证过了)整理成X509结构.
X509_NAME *X509_get_subject_name(X509 *a);
得到证书所有者的名字,参数可用通过SSL_get_peer_certificate()得到的X509对象.
X509_NAME *X509_get_issuer_name(X509 *a)
得到证书签署者(往往是CA)的名字,参数可用通过SSL_get_peer_certificate()得到的X509对象.
char *X509_NAME_oneline(X509_NAME *a,char *buf,int size);
将以上两个函数得到的对象变成字符型,以便打印出来.
SSL_METHOD的构造函数
流程图
程序源代码
改造的openssl自带的demos目录下的cli.cpp,serv.cpp文件,做了一些修改,并增加了一些功能.
0 条评论
下一页