加密通信的基础 - 全面解读SSL/TLS协议
SSL/TLS是世界上应用最广泛的密码通信方法。比如,在网上商城输入信用卡卡号时,Web浏览器就会使用SSL/TLS进行密码通信。使用SSL/TLS可以对通信对象进行认证,还可以确保通信内容的机密性。TLS相当于SSL的后续版本。
概述
- SSL/TLS是世界上应用最广泛的密码通信方法。比如,在网上商城输入信用卡卡号时,Web浏览器就会使用SSL/TLS进行密码通信。使用SSL/TLS可以对通信对象进行认证,还可以确保通信内容的机密性。TLS相当于SSL的后续版本。
- SSL (Secure Sockets Layer)安全套接层协议 :由Netscape公司开发。协议的第一个版本从未发布过,第二个版于1994年11月发布,但是存在严重漏洞,最终退出历史舞台。1995年年底,第三个版本SSL 3.0发布,此版本是完全重新设计的协议,因此该设计一直沿用到今天。
- TLS(Transport Layer Security)安全传输层协议:1996年5月,TLS工作组成立,开始将SSL从Netscape迁移至IETF。最终,TLS 1.0于1999年1月问世,与SSL 3.0版本相比修改并不大,但还是改名为TLS。2006年4月,TLS 1.1发布,修复了一些关键的安全性问题。2008年8月,TLS 1.2发布,该版本添加了对已验证加密的支持,并且基本上删除了协议说明中所有硬编码的安全基元,使协议完全弹性化。
SSL/TLS、HTTP、HTTPS关系
- HTTPS是安全版本的HTTP,简单理解 HTTPS = HTTP + SSL/TLS,即HTTPS就是使用SSL/TLS协议对HTTP报文进行了加密处理。
SSL/TLS单向认证和双向认证
- 单向认证指的是通信双方只校验一方发送过来的数字证书,另一方不校验。通常是客户端根据服务器传过来的证书信息校验服务器的合法性。
- 双向认证指的是在通信双方都要校验对方发送过来的数字证书,一般应用于对安全性要求更高的领域。
SSL/TLS握手过程
- 这里先重点介绍下SSL/TLS双向认证的握手过程,SSL/TLS单向认证后面也会提到,并进行比对。
- TLS握手涉及到加密算法和数字证书的知识。不了解的可参考我的另外两篇文章。加密算法简介 数字证书简介
- 握手图示 图中虚线部分是双向认证时需要额外发送的消息。
- 备注:不同的密钥协商算法计算预备主密钥的方法不一样,这里展示的是RSA密钥协商算法计算预备主密钥的方法。
- 握手详解
- [1] Client Hello
- 客户端发送TLS版本,支持的加密套件列表给服务端,并生成客户端随机数发送给服务端。
- Version : 表示客户端支持的SSL/TLS协议版本
- Random : 客户端随机数
- Session ID : 和会话恢复有关
- Cipher Suites : 客户端支持的密码套件列表
- Compression Methods : 客户端支持的压缩方法
- Extension : 扩展项
- [2] Server Hello
- 服务端确认TLS版本,选择使用的加密套件,生成服务端随机数发送给客户端。
- Version : 服务端根据客户端传递的版本号,选择一个双方都支持的版本
- Random : 服务端随机数
- Session ID : 和会话恢复有关
- Cipher Suite : 根据客户端传递过来的密码套件列表,选择一个双方都支持的密码套件
- Compression Method : 压缩算法
- Extension : 扩展项
- [3] Server Certificate
- 该消息是可选的。根据协商出来的密码套件,服务端选择是否发送证书消息。在HTTPS网站中一般服务器会发送证书,如果协商出的密码套件是DH_anon或者ECDH_anon,则服务器不发送该消息,可能会遇到中间人攻击。
- 服务器发送证书一般有两个目的:一是进行身份验证,二是证书中包含服务器的公钥,该公钥结合密码套件的密钥协商算法协商出预备主密钥。
- [4] Server Key Exchange
- 该消息是有条件才发送的。如果证书包含的信息不足以进行密钥交换,那么必须发送该消息。
- 下列的密码套件,服务端会发送Server Key Exchange消息
- DHE DSS、DHE RSA 、ECDHE ECDSA 、ECDHE RSA
- 上述密码套件都是使用临时DH/ECDH密码协商算法,客户端每次连接服务器的时候,服务器会发送动态DH信息,这些信息不存在服务端证书中,需要通过Server Key Exchange消息传递,传递的DH信息需要使用服务器的私钥进行签名,该私钥和证书中包含的服务器公钥是一对。
- 下列的密钥套件,服务端也会发送Server Key Exchange消息
- DH_anon、ECDH_anon
- 使用的是静态DH/ECDH协商算法,但由于没有证书,所以需要Server Key Exchange消息传递相关DH信息,传递的DH消息需要使用服务器的私钥进行签名
- 下列的密码套件不允许服务器发送Server Key Exchange消息
- RSA、DH_DSS、DH_RSA
- 对于RSA密码套件,客户端计算出预备主密钥,然后使用服务器RSA公钥加密发送给服务端,服务端反解出预备主密钥即可,没有Server Key Exchange子消息也能完成密钥协商。
- 对于DH_DSS/DH_RSA密钥套件,证书中已经包含静态DH信息,无须服务端额外发送Server Key Exchange子消息,客户端和服务端各协商出预备主密钥的一半密钥,结合起来就是预备主密钥。目前已经很少看到这样的密码套件,CA机构也不会在签发证书时包含静态DH信息。
- [5] Certificate Request
- 该消息表示想要收到客户端的证书,一般是双向认证时有该消息。
- [6] Server Hello Done
- 表示服务端发送了足够的消息,接下来等待和客户端协商出预备主密钥。
- [7] Client Certificate
- 发送客户端证书,一般是双向认证时会发送。
- [8] Client Key Exchange
- 消息结构
-
struct { select(KeyExchangeAlgorithm){ case rsa: EncryptedPreMasterSecret; case dhe_dss: case dhe_rsa: case df_dss: case dh_rsa: case dh_anon: ClientDiffieHellmanPublic; case ec_diffie)hellman: ClientECHiffieHellmanPublic; }exchange_keys; } ClientKeyExchange;
- 针对不同的密码套件,该消息一般有三种处理逻辑
- EncryptedPreMasterSecret
- 如果 RSA算法用于身份验证和密钥交换,客户端会生成一个48字节的预备主密钥,然后用服务器证书中的公钥加密并发送给服务器端。最终发送的消息就是Encrypted PreMaster。
- ClientDiffieHellmanPublic
- 如果密码套件中密钥协商算法是 DH算法,客户端必须发送 DH公钥给服务器端。
- ClientECDiffieHellmanPublic
- 如果协商出的密码套件密钥协商算法是 ECDHE,客户端需要发送 ECDH公钥。
- [9] 计算主密钥和密钥块
- 计算预备主密钥,不同的密钥协商算法,计算方法不同。
- RSA密钥协商算法
- RSA密钥协商算法,生成预备主密钥比较简单,直接生成预备主密钥。
- 然后使用服务端证书中的公钥加密预备主密钥,将加密后的预备主密钥发送给服务端。
- 服务端接收到加密后的预备主密钥后,使用私钥解密,得到预备主密钥。
- DH密钥协商算法
- 服务器端生成DH参数和服务器DH密钥对,用RSA私钥签名DH参数和服务器DH公钥,最后将签名值、DH参数、服务器DH公钥发送给客户端。
- 客户端通过服务器发送的证书中的RSA的公钥验证签名,获取到DH参数和服务器DH公钥。
- 客户端通过DH参数生成客户端的DH密钥对,并将客户端DH公钥发送给服务器端。
- 客户端通过客户端DH私钥和服务器端DH公钥计算出预备主密钥。
- 服务器端接收到客户端的DH公钥,结合服务器的DH私钥计算出预备主密钥。
- 最终客户端和服务器端计算出的预备主密钥能够保持一致。
- 双方都拿到预备主密钥后,就可以根据以下公式计算出主密钥
- master_secret = PRF(pre_master_secret, “master secret”, ClientHello.random + ServerHello.random)
- 主密钥的长度固定是48字节。而预备主密钥的长度取决于密码套件算法,如果 RSA算法用来协商密钥,预备主密钥的长度是 48字节;如果 DH/ECDH算法用来协商密钥,长度取决于 DH /ECDH算法的公钥。
- 计算出主密钥后,还需要根据主密钥计算出密钥块。密钥块主要有六个
- Client MAC Key
- Server MAC Key
- Client Key
- Server Key
- Client IV
- Server IV
- MAC Key主要用于数据的完整性校验,Key用于加密数据。IV作为加密算法的初始化向量。
- [10] Certificate verify
- 证书验证。双向认证时有该消息,这个时候客户端已经把证书发送给服务端了,但客户端还要向服务器证明证书中对应的私钥的正确和自己是会话持有者。
- [11] Change Cipher Spec
- 通知对方,可以用协商好的密钥进行通信了。
- [12] Finished [Encrypted Handshake Message]
- 确认所有握手消息没有被篡改。
- [1] Client Hello
Nginx搭建HTTPS网站
-
接下来用Nginx搭建一个HTTPS网站,实现SSL/TLS的双向认证。
-
打开Nginx的配置文件/usr/local/nginx/conf/nginx.conf,HTTPS服务默认是注释的,去掉注释,并且需要再添加两个字段ssl_verify_client 和 ssl_client_certificate
-
# HTTPS server server { listen 10088 ssl; server_name localhost; # 服务端证书 ssl_certificate server.crt; # 服务端证书密钥 ssl_certificate_key server.key; ssl_session_cache shared:SSL:1m; ssl_session_timeout 5m; # 表示开启客户端证书校验 ssl_verify_client on; # 放置CA证书 ssl_client_certificate ca.crt; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; location / { root html; index index.html index.htm; } }
-
这里需要用到三张证书,CA证书,服务端证书和客户端证书,可以通过OpenSSL命令行工具来生成。
-
CA证书
- 生成RSA私钥
- openssl genrsa -out ca.key 2048
- 生成CA证书
- openssl req -new -x509 -key ca.key -out ca.crt -days 365
-
服务端证书
- 生成服务端密钥
- openssl genrsa -out server.key 2048
- 生成证书请求文件
- openssl req -new -key server.key -out server.csr
- 使用CA证书签发服务端证书
- openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt -days 365
-
客户端证书
- 生成客户端密钥
- openssl genrsa -out client.key 2048
- 生成证书请求文件
- openssl req -new -key client.key -out client.csr
- 使用CA证书签发客户端证书
- openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client.crt -days 365
- 将客户端证书和私钥合并成pfx证书
- openssl pkcs12 -export -in client.crt -inkey client.key -out client.pfx
-
服务端证书以及私钥和CA证书生成后,放到Nginx配置文件所在目录下,然后将客户端的pfx证书拷贝到windows平台下,打开Edge浏览器的设置界面,在隐私、搜索和服务栏找到管理证书,然后把刚才的client.pfx证书导入即可。这里注意下,导入后要关掉浏览器重新打开。
-
然后去访问对应https网站,浏览器会弹出对话框,让我们选择对应的客户端证书。只有选择证书并确认后,才可以成功访问到对应的https网站。没有客户端证书或者未成功选择证书,都是无法访问的。如果是单向认证的话,就不需要客户端证书了。
WireShark抓包分析SSL/TLS握手过程
-
接下来就用WireShark抓包工具详细分析SSL/TLS双向认证的握手过程。
-
先看下整体流程,抓包可以看到,在进行SSL/TLS握手前,先要进行TCP的三次握手,建立连接。关于TCP的握手过程,这里就不介绍了,可参考我的这一篇文章 抓包分析TCP协议
-
接下来对SSL/TLS握手的每条信息,详细看一下
-
Client Hello
- Client Hello消息中有TLS版本号,客户端生成的随机数,以及客户端支持的密码套件等信息。
- Client Hello消息中有TLS版本号,客户端生成的随机数,以及客户端支持的密码套件等信息。
-
Server Hello
- Client Hello消息中有TLS版本号,服务端生成的随机数,以及服务端最终确认要使用的密码套件等信息。
- Client Hello消息中有TLS版本号,服务端生成的随机数,以及服务端最终确认要使用的密码套件等信息。
-
Server Certificate
- 服务端发送自己的证书
- 服务端发送自己的证书
-
Server Key Exchange
-
Certificate Request
- 表示服务端想要收到客户端的证书
- 表示服务端想要收到客户端的证书
-
Server Hello Done
- 服务端请求结束
- 服务端请求结束
-
Client Certificate
- 客户端发送自己的证书
- 客户端发送自己的证书
-
Client Key ExChange
- 上面选择的加密套件是 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,密钥协商算法是ECDHE,因此这里发送的消息是ClientECDiffieHellmanPublic
- 如果选择的加密套件密钥协商算法为RSA,比如 TLS_RSA_WITH_AES_128_GCM_SHA256 加密套件,则对应发送的消息就为 EncryptedPreMasterSecret。
- 上面选择的加密套件是 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,密钥协商算法是ECDHE,因此这里发送的消息是ClientECDiffieHellmanPublic
-
Certificate Verify
- 客户端证书验证
- 客户端证书验证
-
Change Cipher Spec
- 通知对方,可以用协商好的密钥进行通信了。
- 通知对方,可以用协商好的密钥进行通信了。
-
Finished [Encrypted Handshake Message]
- 密钥协商完成
- 密钥协商完成
-
Application Data
- 用协商好的密钥进行加密通信
- 用协商好的密钥进行加密通信
-
这就是整个SSL/TLS协议握手的过程。
-
上面展示的是SSL/TLS双向认证的过程,可以关掉Nginx对客户端的证书校验,实现单向认证。
-
这是单向认证时抓的包,可以与上面的双向认证比对下,主要少了服务端请求客户端发送证书的消息Certificate Request,客户端发送证书消息Client Certificate,客户端证书校验消息Certificate verify。其他流程和双向认证是一样的。
C/C++代码实现SSL/TLS通信
- 我们还可以使用C/C++代码来实现SSL/TLS的通信
- 服务端代码
-
#include <stdio.h> #include <stdlib.h> #include <memory.h> #include <errno.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <openssl/rsa.h> #include <openssl/crypto.h> #include <openssl/x509.h> #include <openssl/pem.h> #include <openssl/ssl.h> #include <openssl/err.h> #include <openssl/rand.h> #define CERTF "server.crt" /*服务端的证书(需经CA签名)*/ #define KEYF "server.key" /*服务端的私钥(建议加密存储)*/ #define CACERT "ca.crt" /*CA 的证书*/ #define PORT 10088 /*准备绑定的端口*/ int main (){ SSL_load_error_strings(); /*为打印调试信息作准备*/ OpenSSL_add_ssl_algorithms(); /*初始化*/ SSL_CTX* ctx = SSL_CTX_new(TLSv1_server_method()); if(ctx == NULL) { printf("SSL_CTX_new failed.\n"); return -1; } SSL_CTX_set_verify(ctx,SSL_VERIFY_PEER,NULL); /*是否验证客户端证书,双向认证时开启*/ SSL_CTX_load_verify_locations(ctx,CACERT,NULL); /*若验证,则放置CA证书*/ if (SSL_CTX_use_certificate_file(ctx, CERTF, SSL_FILETYPE_PEM) <= 0) { ERR_print_errors_fp(stderr); return -1; } if (SSL_CTX_use_PrivateKey_file(ctx, KEYF, SSL_FILETYPE_PEM) <= 0) { ERR_print_errors_fp(stderr); return -1; } if (!SSL_CTX_check_private_key(ctx)) { printf("Private key does not match the certificate public key\n"); return -1; } SSL_CTX_set_cipher_list(ctx,"RC4-MD5"); /*开始正常的TCP socket过程.*/ printf("Begin TCP socket...\n"); int listenSock = socket(AF_INET, SOCK_STREAM, 0); if(listenSock == -1){ perror("socket"); return -1; } struct sockaddr_in sa_serv; memset (&sa_serv, 0, sizeof(sa_serv)); sa_serv.sin_family = AF_INET; sa_serv.sin_addr.s_addr = INADDR_ANY; sa_serv.sin_port = htons(PORT); if(bind(listenSock, (struct sockaddr*) &sa_serv, sizeof (sa_serv)) == -1){ perror("bind"); return -1; } /*接受TCP链接*/ if(listen (listenSock, 5) == -1){ perror("listen"); return -1; } struct sockaddr_in sa_cli; socklen_t client_len = sizeof(sa_cli); int connfd = accept (listenSock, (struct sockaddr*) &sa_cli, &client_len); if(connfd == -1){ perror("accept"); close (listenSock); return -1; } printf ("[%s:%d] connected...\n", inet_ntoa(sa_cli.sin_addr), sa_cli.sin_port); /*TCP连接已建立,进行服务端的SSL过程. */ printf("Begin server side SSL\n"); SSL* ssl = SSL_new (ctx); if(ssl == NULL){ printf("SSL_new failed.\n"); return -1; } SSL_set_fd (ssl, connfd); int sslSock = SSL_accept (ssl); if(sslSock == -1){ ERR_print_errors_fp(stderr); return -1; } printf("SSL_accept finished\n"); /*打印所有加密算法的信息(可选)*/ printf ("SSL connection using %s\n", SSL_get_cipher(ssl)); /*得到客户端的证书并打印些信息(可选) */ X509* client_cert = SSL_get_peer_certificate (ssl); if (client_cert != NULL) { printf ("Client certificate:\n"); char* subStr = X509_NAME_oneline(X509_get_subject_name (client_cert), 0, 0); if(subStr == NULL){ printf("X509_NAME_oneline subject failed.\n"); return -1; } printf ("subject: %s\n", subStr); //Free (subStr); char* issStr = X509_NAME_oneline(X509_get_issuer_name (client_cert), 0, 0); if(issStr == NULL){ printf("X509_NAME_oneline subject failed.\n"); return -1; } printf ("issuer: %s\n", issStr); //Free (issStr); X509_free (client_cert);/*如不再需要,需将证书释放 */ }else{ printf ("Client does not have certificate\n"); } char buf[4096] = {0}; /* 数据交换开始,用SSL_write,SSL_read代替write,read */ int readSize = SSL_read(ssl, buf, sizeof(buf) - 1); if(readSize == -1){ ERR_print_errors_fp(stderr); return -1; } printf ("SSL_read buf[%d] = {%s}\n", readSize, buf); if(SSL_write (ssl, "Welcome to Connect to Server!", strlen("Welcome to Connect to Server!")) == -1){ ERR_print_errors_fp(stderr); return -1; } shutdown (connfd, 2); SSL_free (ssl); SSL_CTX_free (ctx); return 0; }
-
- 客户端代码
-
#include <stdio.h> #include <stdlib.h> #include <memory.h> #include <errno.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <openssl/rsa.h> #include <openssl/crypto.h> #include <openssl/x509.h> #include <openssl/pem.h> #include <openssl/ssl.h> #include <openssl/err.h> #include <openssl/rand.h> /*所有需要的参数信息都在此处以#define的形式提供*/ #define CERTF "client.crt" /*客户端的证书(需经CA签名)*/ #define KEYF "client.key" /*客户端的私钥(建议加密存储)*/ #define CACERT "ca.crt" /*CA 的证书*/ #define PORT 10088 /*服务端的端口*/ #define SERVER_ADDR "127.0.0.1" /*服务段的IP地址*/ int main () { /*初始化*/ OpenSSL_add_ssl_algorithms(); //载入所有SSL错误消息 SSL_load_error_strings(); /*采用什么协议(SSLv2/SSLv3/TLSv1)在此指定*/ /*申请SSL会话环境*/ SSL_CTX *ctx = SSL_CTX_new(TLSv1_client_method()); if(ctx == NULL){ printf("SSL_CTX_new failed!\n"); return -1; } /*是否验证服务端证书*/ SSL_CTX_set_verify(ctx,SSL_VERIFY_PEER,NULL); /*若验证对方,则放置CA证书*/ SSL_CTX_load_verify_locations(ctx,CACERT,NULL); /*加载自己的证书*/ if (SSL_CTX_use_certificate_file(ctx, CERTF, SSL_FILETYPE_PEM) <= 0) { ERR_print_errors_fp(stderr); return -1; } /*加载自己的私钥,以用于签名*/ if (SSL_CTX_use_PrivateKey_file(ctx, KEYF, SSL_FILETYPE_PEM) <= 0) { ERR_print_errors_fp(stderr); return -1; } /*调用了以上两个函数后,检验一下自己的证书与私钥是否配对*/ if (!SSL_CTX_check_private_key(ctx)) { printf("Private key does not match the certificate public key\n"); return -1; } /*以下是正常的TCP socket建立过程*/ printf("Begin tcp socket...\n"); int sock = socket(AF_INET, SOCK_STREAM, 0); if(sock == -1){ perror("socket"); return -1; } struct sockaddr_in sa; memset (&sa, 0, sizeof(sa)); sa.sin_family = AF_INET; sa.sin_addr.s_addr = inet_addr(SERVER_ADDR); /* Server IP */ sa.sin_port = htons(PORT); /* Server Port number */ if(connect(sock, (struct sockaddr*) &sa, sizeof(sa)) == -1){ perror("connect"); return -1; } /* TCP 链接已建立.开始 SSL 握手过程 */ printf("Begin SSL negotiation \n"); /*申请一个SSL套接字*/ SSL* ssl = SSL_new (ctx); if(ssl == NULL){ printf("SSL_new failed.\n"); return -1; } /*绑定读写套接字*/ SSL_set_fd(ssl, sock); if(SSL_connect(ssl) == -1){ ERR_print_errors_fp(stderr); return -1; } /*打印所有加密算法的信息(可选)*/ printf ("SSL connection using %s\n", SSL_get_cipher(ssl)); /*得到服务端的证书并打印些信息(可选) */ X509* server_cert = SSL_get_peer_certificate (ssl); if(server_cert == NULL){ printf("SSL_get_peer_certificate failed.\n"); return -1; } printf ("Server certificate:\n"); char* subStr = X509_NAME_oneline(X509_get_subject_name (server_cert),0,0); if(subStr == NULL){ printf("X509_NAME_oneline subject failed.\n"); return -1; } printf("subject: %s\n", subStr); // Free(subStr); char* issStr = X509_NAME_oneline (X509_get_issuer_name(server_cert),0,0); if(issStr == NULL){ printf("X509_NAME_oneline issuer failed.\n"); return -1; } printf ("issuer: %s\n", issStr); // Free (issStr); X509_free(server_cert); /*如不再需要,需将证书释放 */ /* 数据交换开始,用SSL_write,SSL_read代替write,read */ printf("Begin SSL data exchange\n"); if(SSL_write(ssl, "Hello, I am client!", strlen("Hello, I am client!")) == -1){ ERR_print_errors_fp(stderr); return -1; } char buf[4096] = {0}; int readSize = SSL_read(ssl, buf, sizeof(buf) - 1); if(readSize == -1){ ERR_print_errors_fp(stderr); return -1; } printf ("SSL_read buf[%d] = {%s}\n", readSize, buf); SSL_shutdown (ssl); /* send SSL/TLS close_notify */ shutdown (sock, 2); SSL_free (ssl); SSL_CTX_free (ctx); return 0; }
-
- Makefile
-
all: server client server: server.cpp g++ -o server server.cpp -lssl -lcrypto client: client.cpp g++ -o client client.cpp -lssl -lcrypto clean: rm server client
-
参考资料
- 《HTTPS权威指南》
- 《深入浅出HTTPS从原理到实战》
- 《图解密码技术》
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)