写点什么

Netty 系列之 Netty 安全性

  • 2014-08-08
  • 本文字数:13590 字

    阅读完需:约 45 分钟

1. 背景

1.1. 严峻的安全形势

1.1.1. OpenSSL Heart bleed 漏洞

2014 年上半年对网络安全影响最大的问题就是 OpenSSL Heart bleed 漏洞,来自 Codenomicon 和谷歌安全部门的研究人员发现 OpenSSL 的源代码中存在一个漏洞,可以让攻击者获得服务器上 64K 内存中的数据内容。该漏洞在国内被译为” OpenSSL 心脏出血漏洞”,因其破坏性之大和影响的范围之广,堪称网络安全里程碑事件。

OpenSSL 是为网络通信提供安全及数据完整性的一种安全协议,囊括了主要的密码算法、常用的密钥和证书封装管理功能以及 SSL 协议.多数 SSL 加密网站是用名为 OpenSSL 的开源软件包,由于这也是互联网应用最广泛的安全传输方法,被网银、在线支付、电商网站、门户网站、电子邮件等重要网站广泛使用,所以漏洞影响范围广大。

全球第一个被攻击通告的案例是加拿大税务局确认 OpenSSL Heart bleed 漏洞导致了 900 个纳税人的社会保障号被盗,这 900 个纳税人的社保号被攻击者在系统中完全删除了。

1.1.2. 安全漏洞的高成本

任何网络攻击都能够给企业造成破坏,但是如何将这些破坏具体量化成金融数据呢?2013 年,B2B International 联合卡巴斯基实验室基于对全球企业的调查结果,计算出网络攻击平均造成的损失。

根据调查报告得出的结论,当企业遭遇网络攻击后平均损失为 649,000 美元。损失主要包括两方面:

  1. 安全事件本身造成的损失,即由重要数据泄漏、业务连续性以及安全修复专家费用相关成本;
  2. 为列入计划的”响应”成本,用于阻止未来发生类似的攻击事件,包括雇佣、培训员工成本以及硬件、软件和其它基础设施安全升级成本。

1.2. Netty 面临的安全风险

作为一个高性能的 NIO 通信框架,基于 Netty 的行业应用非常广泛,不同的行业、不同的应用场景,面临的安全挑战也不同,下面我们根据 Netty 的典型应用场景,分析下 Netty 面临的安全挑战。

1.2.1. 仅限内部使用的 RPC 通信框架

随着业务的发展,网站规模的扩大,传统基于 MVC 的垂直架构已经无法应对业务的快速发展。需要对数据和业务进行水平拆分,基于 RPC 的分布式服务框架成为最佳选择。

业务水平拆分之后,内部的各个模块需要进行高性能的通信,传统基于 RMI 和 Hession 的同步阻塞式通信已经无法满足性能和可靠性要求。因此,高性能的 NIO 框架成为构建分布式服务框架的基石。

网站的架构演进过程如下:

图 1-1 网站的架构演进

高性能的 RPC 框架,各模块之间往往采用长连接通信,通过心跳检测保证链路的可靠性。由于 RPC 框架通常是在内部各模块之间使用,运行在授信的内部安全域中,不直接对外开放接口。因此,不需要做握手、黑白名单、SSL/TLS 等,正所谓是“防君子不防小人”。

在这种应用场景下,Netty 的安全性是依托企业的防火墙、安全加固操作系统等系统级安全来保障的,它自身并不需要再做额外的安全性保护工作。

1.2.2. 对第三方开放的通信框架

如果使用 Netty 做 RPC 框架或者私有协议栈,RPC 框架面向非授信的第三方开放,例如将内部的一些能力通过服务对外开放出去,此时就需要进行安全认证,如果开放的是公网 IP,对于安全性要求非常高的一些服务,例如在线支付、订购等,需要通过 SSL/TLS 进行通信。

它的原理图如下:

图 1-2 对第三方开放的通信框架

对第三方开放的通信框架的接口调用存在三种场景:

  • 在企业内网,开放给内部其它模块调用的服务,通常不需要进行安全认证和 SSL/TLS 传输;
  • 在企业内网,被外部其它模块调用的服务,往往需要利用 IP 黑白名单、握手登陆等方式进行安全认证,认证通过之后双方使用普通的 Socket 进行通信,如果认证失败,则拒绝客户端连接;
  • 开放给企业外部第三方应用访问的服务,往往需要监听公网 IP(通常是防火墙的 IP 地址),由于对第三方服务调用者的监管存在诸多困难,或者无法有效监管,这些第三方应用实际是非授信的。为了有效应对安全风险,对于敏感的服务往往需要通过 SSL/TLS 进行安全传输。

1.2.3. 应用层协议的安全性

作为高性能、异步事件驱动的 NIO 框架,Netty 非常适合构建上层的应用层协议,相关原理,如下图所示:

图 1-3 基于 Netty 构建应用层协议

由于绝大多数应用层协议都是公有的,这意味着底层的 Netty 需要向上层提供通信层的安全传输,也就是需要支持 SSL/TLS。

JDK 的安全类库提供了 javax.net.ssl.SSLSocket 和 javax.net.ssl.SSLServerSocket 类库用于支持 SSL/TLS 安全传输,对于 NIO 非阻塞 Socket 通信,JDK 并没有提供现成可用的类库简化用户开发。

Netty 通过 JDK 的 SSLEngine,以 SslHandler 的方式提供对 SSL/TLS 安全传输的支持,极大的简化了用户的开发工作量,降低开发难度。

对于 Netty 默认提供的 HTTP 协议,Netty 利用 SslHandler,同样支持 HTTPS 协议。

2. Netty SSL 开发

2.1. SSL 单向认证

单向认证,即客户端只验证服务端的合法性,服务端不验证客户端。下面我们通过 Netty 的 SSL 单向认证代码开发来掌握基于 Netty 的 SSL 单向认证。

2.1.1. SSL 单向认证开发

首先,利用 JDK 的 keytool 工具,Netty 服务端依次生成服务端的密钥对和证书仓库、服务端自签名证书。

生成 Netty 服务端私钥和证书仓库命令:

复制代码
keytool -genkey -alias securechat -keysize 2048 -validity
365 -keyalg RSA -dname "CN=localhost" -keypass sNetty
-storepass sNetty -keystore sChat.jks

生成 Netty 服务端自签名证书:

复制代码
keytool -export -alias securechat -keystore sChat.jks -storepass sNetty -file sChat.cer

生成客户端的密钥对和证书仓库,用于将服务端的证书保存到客户端的授信证书仓库中,命令如下:

复制代码
keytool -genkey -alias smcc -keysize 2048 -validity 365
-keyalg RSA -dname "CN=localhost" -keypass cNetty
-storepass cNetty -keystore cChat.jks

随后,将 Netty 服务端的证书导入到客户端的证书仓库中,命令如下:

keytool -import -trustcacerts -alias securechat -file sChat.cer -storepass cNetty -keystore cChat.jks

上述工作完成之后,我们就开始编写 SSL 服务端和客户端的代码,下面我们对核心代码进行讲解。

首先看服务端的代码,在 TCP 链路初始化的时候,创建 SSLContext 并对其进行正确的初始化,下面我们对 SSLContext 的创建进行讲解:

因为是客户端认证服务端,因此服务端需要正确的设置和加载私钥仓库 KeyStore,相关代码如下:

初始化 KeyManagerFactory 之后,创建 SSLContext 并初始化,代码如下:

由于是单向认证,服务端不需要验证客户端的合法性,因此,TrustManager 为空,安全随机数不需要设置,使用 JDK 默认创建的即可。

服务端的 SSLContext 创建完成之后,利用 SSLContext 创建 SSL 引擎 SSLEngine,设置 SSLEngine 为服务端模式,由于不需要对客户端进行认证,因此 NeedClientAuth 不需要额外设置,使用默认值 False。相关代码如下:

SSL 服务端创建完成之后,下面继续看客户端的创建,它的原理同服务端类似,也是在初始化 TCP 链路的时候创建并设置 SSLEngine,代码如下:

由于是客户端认证服务端,因此,客户端只需要加载存放服务端 CA 的证书仓库即可。

加载证书仓库完成之后,初始化 SSLContext,代码如下:对于客户端只需要设置信任证书 TrustManager。

客户端 SSLContext 初始化完成之后,创建 SSLEngine 并将其设置为客户端工作模式,代码如下:

将 SslHandler 添加到 pipeline 中,利用 SslHandler 实现 Socket 安全传输,代码如下:

客户端和服务端创建完成之后,测试下 SSL 单向认证功能是否 OK,为了查看 SSL 握手过程,我们打开 SSL 握手的调测日志,Eclipse 设置如下:

图 2-1 打开 SSL 调测日志

分别运行服务端和客户端,运行结果如下:

图 2-2 客户端 SSL 握手日志

图 2-3 服务端 SSL 握手日志

在客户端输入信息,服务端原样返回,测试结果如下:

到此,Netty SSL 单向认证已经开发完成,下个小节我们将结合 SSL 握手日志,详细解读下 SSL 单向认证的原理。

2.1.2. SSL 单向认证原理分析

SSL 单向认证的过程总结如下:

  1. SSL 客户端向服务端传送客户端 SSL 协议的版本号、支持的加密算法种类、产生的随机数,以及其它可选信息;
  2. 服务端返回握手应答,向客户端传送确认 SSL 协议的版本号、加密算法的种类、随机数以及其它相关信息;
  3. 服务端向客户端发送自己的公钥;
  4. 客户端对服务端的证书进行认证,服务端的合法性校验包括:证书是否过期、发行服务器证书的 CA 是否可靠、发行者证书的公钥能否正确解开服务器证书的“发行者的数字签名”、服务器证书上的域名是否和服务器的实际域名相匹配等;
  5. 客户端随机产生一个用于后面通讯的“对称密码”,然后用服务端的公钥对其加密,将加密后的“预主密码”传给服务端;
  6. 服务端将用自己的私钥解开加密的“预主密码”,然后执行一系列步骤来产生主密码;
  7. 客户端向服务端发出信息,指明后面的数据通讯将使用主密码为对称密钥,同时通知服务器客户端的握手过程结束;
  8. 服务端向客户端发出信息,指明后面的数据通讯将使用主密码为对称密钥,同时通知客户端服务器端的握手过程结束;
  9. SSL 的握手部分结束,SSL 安全通道建立,客户端和服务端开始使用相同的对称密钥对数据进行加密,然后通过 Socket 进行传输;

下面,我们结合 JDK 的 SSL 工作原理对 Netty 的 SSL 单向认证过程进行讲解,首先,我们看下 JDK SSL 单向认证的流程图:

图 2-4 SSL 单向认证流程图

下面结合 JDK SSL 引擎的调测日志信息我们对 SSL 单向认证的流程进行详细讲解,对于比较简单的流程会进行步骤合并。

步骤 1:客户端使用 TLS 协议版本发送一个 ClientHello 消息,这个消息包含一个随机数、建议的加密算法套件和压缩方法列表,如下所示:

复制代码
*** ClientHello, TLSv1
RandomCookie: GMT: 1389796107 bytes = { 125, 107, 138, 150, 226, 182, 238, 75, 38,
150, 222, 147, 127, 35, 36, 149, 172, 128, 152, 34, 110, 104, 176, 34, 180, 118, 185, 55 }
Session ID: {}
Cipher Suites: [TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
TLS_RSA_WITH_AES_128_CBC_SHA, TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA, TLS_ECDH_RSA_WITH_AES_128_CBC_SHA,
TLS_DHE_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_DSS_WITH_AES_128_CBC_SHA, TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
TLS_ECDHE_RSA_WITH_RC4_128_SHA, SSL_RSA_WITH_RC4_128_SHA, TLS_ECDH_ECDSA_WITH_RC4_128_SHA,
TLS_ECDH_RSA_WITH_RC4_128_SHA, TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA, TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
SSL_RSA_WITH_3DES_EDE_CBC_SHA, TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA, TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA,
SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA, SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA, SSL_RSA_WITH_RC4_128_MD5,
TLS_EMPTY_RENEGOTIATION_INFO_SCSV]
Compression Methods: { 0 }
Extension elliptic_curves, curve names: {secp256r1, sect163k1, sect163r2, secp192r1, secp224r1,
sect233k1, sect233r1, sect283k1, sect283r1, secp384r1, sect409k1, sect409r1, secp521r1, sect571k1,
sect571r1, secp160k1, secp160r1, secp160r2, sect163r1, secp192k1, sect193r1, sect193r2, secp224k1,
sect239k1, secp256k1}
Extension ec_point_formats, formats: [uncompressed]

步骤 2:服务端使用 ServerHello 消息来响应,这个消息包含由客户提供的信息基础上的另一个随机数和一个可选的会话 ID,以及服务端选择的加密套件算法,响应消息如下:

复制代码
*** ServerHello, TLSv1
RandomCookie: GMT: 1389796108 bytes = { 27, 170, 76, 238, 56, 58, 172, 146,
41, 159, 249, 213, 16, 214, 53, 167, 50, 74, 39, 107, 121, 63, 80, 26, 210, 149, 249, 194 }
Session ID: {83, 215, 155, 12, 122, 5, 231, 3, 13, 11, 17, 204, 56, 73, 119,
49, 85, 229, 220, 92, 55, 40, 25, 194, 198, 244, 200, 6, 55, 209, 23, 245}
Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
Compression Method: 0
Extension renegotiation_info, renegotiated_connection: <empty>
***

步骤 3:服务端发送自签名的证书消息,包含完整的证书链:

复制代码
*** Certificate chain
chain [0] = [
[
Version: V3
Subject: CN=localhost
Signature Algorithm: SHA1withRSA, OID = 1.2.840.113549.1.1.5
Key: Sun RSA public key, 2048 bits
modulus: 180074092335949740506599932729136061127910704822256890304785299212
89120399567693292155689698972062026646025485683710348109589875614228688670418
26997320367322716218554750309434289655244757299259864742384047112657948157239
74656070231306588457907121768485493115189644689102055777319298694358710534010
07782509767857568645054682957874162480829502504137753701941108204165639642395
91445925708790136700350526512926021140926345621403182628994210668730957728483
67874786322927437079881769937503767679525485790533062220506746478912515940552
94347989837561879359652740344329755331698082706888032724267649830488014296906
294110074041
public exponent: 65537
Validity: [From: Sun Jul 27 08:49:30 CST 2014,
To: Mon Jul 27 08:49:30 CST 2015]
Issuer: CN=localhost
SerialNumber: [ 53d44c9a]
]
Algorithm: [SHA1withRSA]
Signature:
0000: 10 05 5E D4 EE A8 1C 8E 82 F1 3F 6B 0A 34 9B 96 ..^.......?k.4..
0010: 97 BE 62 13 F7 2E 94 74 A5 46 CC AB C5 0B FC 67 ..b....t.F.....g
0020: 3C E1 1B 43 B8 A4 3B C9 F9 44 9F F2 D2 90 35 3C <..C..;..D....5<
0030: F6 47 78 3A AC 6B 87 E5 43 EA C8 C5 8C 4C 6E AB .Gx:.k..C....Ln.
0040: 46 F8 C8 C4 BA 86 97 1E C5 75 2F 85 15 CB A1 93 F........u/.....
0050: 0E 23 06 57 93 47 DF 8D 04 0F 21 AC FC E0 7D 14 .#.W.G....!.....
0060: 07 BE 0F 62 F4 75 A9 CE F9 B3 11 0B 75 B4 87 22 ...b.u......u.."
0070: D5 8E E2 0A A9 1F C2 15 3A 64 B2 23 8F 1A 84 6C ........:d.#...l
0080: EE 2C 3A C3 24 65 F5 BC 5C AF BD F8 B9 C4 45 83 .,:.$e..\.....E.
0090: 5B FF BD 36 E8 5D BE 98 03 2E AB 3F FE EC 9A 7B [..6.].....?....
00A0: 31 35 7D EF 53 81 8B 7A 8B 37 7D BD EB 17 F0 36 15..S..z.7.....6
00B0: 93 CF 74 28 A3 C1 8B E1 B1 12 9F 44 20 CA 48 64 ..t(.......D .Hd
00C0: D6 F5 B0 B1 D9 18 AA F6 88 02 26 93 C8 B8 91 1A ..........&.....
00D0: F8 B0 8B E6 7D C6 56 39 B2 6A AF 73 D2 78 76 1A ......V9.j.s.xv.
00E0: 10 F0 C5 98 4F 90 39 2F 84 BC A0 78 81 8B ED 04 ....O.9/...x....
00F0: B8 60 49 84 C3 BD CC D2 CA 52 0A 03 E0 6C 21 B3 .`I......R...l!.
]
***

步骤 4:服务端向客户端发送自己的公钥信息,最后发送 ServerHelloDone:

复制代码
*** ECDH ServerKeyExchange
Server key: Sun EC public key, 256 bits
public x coord: 11246390291863077910794590233832192297756589204670697
905888685651118114908704
public y coord: 14161558430218398366136024174925258002831938156653157
074058492642854053163673
parameters: secp256r1 [NIST P-256, X9.62 prime256v1] (1.2.840.10045.3.1.7)
*** ServerHelloDone

步骤 5:客户端对服务端自签名的证书进行认证,如果客户端的信任证书列表中包含了服务端发送的证书,对证书进行合法性认证,相关信息如下:

复制代码
***
Found trusted certificate:
[
[
Version: V3
Subject: CN=localhost
Signature Algorithm: SHA1withRSA, OID = 1.2.840.113549.1.1.5
Key: Sun RSA public key, 2048 bits
modulus: 18007409233594974050659993272913606112791070482225689030478529921
2891203995676932921556896989720620266460254856837103481095898756142286886704
1826997320367322716218554750309434289655244757299259864742384047112657948157
2397465607023130658845790712176848549311518964468910205577731929869435871053
4010077825097678575686450546829578741624808295025041377537019411082041656396
4239591445925708790136700350526512926021140926345621403182628994210668730957
7284836787478632292743707988176993750376767952548579053306222050674647891251
5940552943479898375618793596527403443297553316980827068880327242676498304880
14296906294110074041
public exponent: 65537
Validity: [From: Sun Jul 27 08:49:30 CST 2014,
To: Mon Jul 27 08:49:30 CST 2015]
Issuer: CN=localhost
SerialNumber: [ 53d44c9a]
]
Algorithm: [SHA1withRSA]
Signature:
0000: 10 05 5E D4 EE A8 1C 8E 82 F1 3F 6B 0A 34 9B 96 ..^.......?k.4..
0010: 97 BE 62 13 F7 2E 94 74 A5 46 CC AB C5 0B FC 67 ..b....t.F.....g
0020: 3C E1 1B 43 B8 A4 3B C9 F9 44 9F F2 D2 90 35 3C <..C..;..D....5<
0030: F6 47 78 3A AC 6B 87 E5 43 EA C8 C5 8C 4C 6E AB .Gx:.k..C....Ln.
0040: 46 F8 C8 C4 BA 86 97 1E C5 75 2F 85 15 CB A1 93 F........u/.....
0050: 0E 23 06 57 93 47 DF 8D 04 0F 21 AC FC E0 7D 14 .#.W.G....!.....
0060: 07 BE 0F 62 F4 75 A9 CE F9 B3 11 0B 75 B4 87 22 ...b.u......u.."
0070: D5 8E E2 0A A9 1F C2 15 3A 64 B2 23 8F 1A 84 6C ........:d.#...l
0080: EE 2C 3A C3 24 65 F5 BC 5C AF BD F8 B9 C4 45 83 .,:.$e..\.....E.
0090: 5B FF BD 36 E8 5D BE 98 03 2E AB 3F FE EC 9A 7B [..6.].....?....
00A0: 31 35 7D EF 53 81 8B 7A 8B 37 7D BD EB 17 F0 36 15..S..z.7.....6
00B0: 93 CF 74 28 A3 C1 8B E1 B1 12 9F 44 20 CA 48 64 ..t(.......D .Hd
00C0: D6 F5 B0 B1 D9 18 AA F6 88 02 26 93 C8 B8 91 1A ..........&.....
00D0: F8 B0 8B E6 7D C6 56 39 B2 6A AF 73 D2 78 76 1A ......V9.j.s.xv.
00E0: 10 F0 C5 98 4F 90 39 2F 84 BC A0 78 81 8B ED 04 ....O.9/...x....
00F0: B8 60 49 84 C3 BD CC D2 CA 52 0A 03 E0 6C 21 B3 .`I......R...l!.
]

步骤 6:客户端通知服务器改变加密算法,通过 Change Cipher Spec 消息发给服务端,随后发送 Finished 消息,告知服务器请检查加密算法的变更请求:

复制代码
nioEventLoopGroup-2-1, WRITE: TLSv1 Change Cipher Spec, length = 1
*** Finished

步骤 7:服务端读取到 Change Cipher Spec 变更请求消息,向客户端返回确认密钥变更消息,最后通过发送 Finished 消息表示 SSL/TLS 握手结束。

复制代码
nioEventLoopGroup-3-1, READ: TLSv1 Change Cipher Spec, length = 1
nioEventLoopGroup-3-1, READ: TLSv1 Handshake, length = 48
*** Finished
verify_data: { 157, 255, 187, 52, 139, 16, 20, 190, 11, 35, 79, 0 }
***
nioEventLoopGroup-3-1, WRITE: TLSv1 Change Cipher Spec, length = 1
*** Finished

2.2. SSL 双向认证

2.2.1. SSL 双向认证开发

我们在 2.1 章节的基础上进行开发,与单向认证不同的是服务端也需要对客户端进行安全认证。这就意味着客户端的自签名证书也需要导入到服务端的数字证书仓库中。

首先,生成客户端的自签名证书:

复制代码
keytool -export -alias smcc -keystore cChat.jks -storepass cNetty
-file cChat.cer

最后,将客户端的自签名证书导入到服务端的信任证书仓库中:

复制代码
keytool -import -trustcacerts -alias smcc -file cChat.cer -storepass
sNetty -keystore sChat.jks

证书导入之后,需要对 SSL 客户端和服务端的代码同时进行修改,首先我们看下服务端如何修改。

由于服务端需要对客户端进行验证,因此在初始化服务端 SSLContext 的时候需要加载证书仓库。首先需要对 TrustManagerFactory 进行初始化,代码如下:

初始化 SSLContext 的时候根据 TrustManagerFactory 获取 TrustManager 数组,代码如下:

最后,创建 SSLEngine 之后,设置需要进行客户端认证,代码如下:

完成服务端修改之后,再回头看下客户端的修改,由于服务端需要认证客户端的证书,因此,需要初始化和加载私钥仓库,向服务端发送公钥,初始化 KeyStore 的代码如下:

初始化 SSLContext 的时候需要传入 KeyManager 数组,代码如下:

客户端开发完成之后,测试下程序是否能够正常工作,运行结果如下所示。

客户端运行结果:

图 2-5 Netty SSL 双向认证客户端运行结果

服务端运行结果:

图 2-6 Netty SSL 双向认证服务端运行结果

在客户端控制台进行输入,看 SSL 传输是否正常:

图 2-7 Netty SSL 安全传输测试

2.2.2. SSL 双向认证原理分析

SSL 双向认证相比单向认证,多了一步服务端发送认证请求消息给客户端,客户端发送自签名证书给服务端进行安全认证的过程。下面,我们结合 Netty SSL 调测日志,对双向认证的差异点进行分析。

相比于客户端,服务端在发送 ServerHello 时携带了要求客户端认证的请求信息,如下所示:

复制代码
*** CertificateRequest
Cert Types: RSA, DSS, ECDSA
Cert Authorities:
<CN=localhost>
<CN=localhost>
*** ServerHelloDone

客户端接收到服务端要求客户端认证的请求消息之后,发送自己的证书信息给服务端,信息如下:

复制代码
matching alias: smcc
*** Certificate chain
chain [0] = [
[
Version: V3
Subject: CN=localhost
Signature Algorithm: SHA1withRSA, OID = 1.2.840.113549.1.1.5
Key: Sun RSA public key, 2048 bits
modulus: 212639695562264078962258083015763969567082142460170954053624074
53705267323050920051941696590911289892005894127848317880153980200067657563
15944918691324084822137929027919841383304228071408660098765703368443353862
47349919704780645114810932016343908989985053434023995248208445566727867691
73042913746571760169661698040844437316556983406538131853892449014877947773
16977794500345715634646402492099542466990685058179767825995777860790787074
72339147926907851214779520246763960901175126351376922481444497141021631392
59603124160944922844840171133151822882039207352509182052426500279100525773
147139994269292585983679425433429361
public exponent: 65537
Validity: [From: Sun Jul 27 08:50:35 CST 2014,
To: Mon Jul 27 08:50:35 CST 2015]
Issuer: CN=localhost
SerialNumber: [ 53d44cdb]

服务端对客户端的自签名证书进行认证,信息如下:

复制代码
***
Found trusted certificate:
[
[
Version: V3
Subject: CN=localhost
Signature Algorithm: SHA1withRSA, OID = 1.2.840.113549.1.1.5
Key: Sun RSA public key, 2048 bits
modulus: 21263969556226407896225808301576396956708214246017095405362407
4537052673230509200519416965909112898920058941278483178801539802000676575
6315944918691324084822137929027919841383304228071408660098765703368443353
8624734991970478064511481093201634390898998505343402399524820844556672786
7691730429137465717601696616980408444373165569834065381318538924490148779
4777316977794500345715634646402492099542466990685058179767825995777860790
7870747233914792690785121477952024676396090117512635137692248144449714102
1631392596031241609449228448401711331518228820392073525091820524265002791
00525773147139994269292585983679425433429361
public exponent: 65537
Validity: [From: Sun Jul 27 08:50:35 CST 2014,
To: Mon Jul 27 08:50:35 CST 2015]
Issuer: CN=localhost
SerialNumber: [ 53d44cdb]

2.3. 第三方 CA 认证

使用 jdk keytool 生成的数字证书是自签名的。自签名就是指证书只能保证自己是完整且没有经过非法修改,但是无法保证这个证书是属于谁的。为了对自签名证书进行认证,需要每个客户端和服务端都交换自己自签名的私有证书,对于一个大型网站或者应用服务器,这种工作量是非常大的。

基于自签名的 SSL 双向认证,只要客户端或者服务端修改了密钥和证书,就需要重新进行签名和证书交换,这种调试和维护工作量是非常大的。因此,在实际的商用系统中往往会使用第三方 CA 证书颁发机构进行签名和验证。我们的浏览器就保存了几个常用的 CA_ROOT。每次连接到网站时只要这个网站的证书是经过这些 CA_ROOT 签名过的。就可以通过验证了。

CA 数字证书认证服务往往是收费的,国内有很多数字认证中心都提供相关的服务,如下所示:

图 2-8 商业的数字认证中心

作为示例,我们自己生成一个 CA_ROOT 的密钥对,部署应用时,把这个 CA_ROOT 的私钥部署在所有需要 SSL 传输的节点就可以完成安全认证。作为示例,如果要生成 CA_ROOT,我们使用开源的 OpenSSL。

在 Windows 上安装和使用 OpenSSL 网上有很多教程,也不是本文的重点,因此,OpenSSL 的安装和使用本文不详细介绍。

下面我们对基于第三方 CA 认证的步骤进行详细介绍。

2.3.1. 服务端证书制作

步骤 1:利用 OpenSSL 生成 CA 证书:

复制代码
openssl req -new -x509 -keyout ca.key -out ca.crt -days 365

步骤 2:生成服务端密钥对:

复制代码
keytool -genkey -alias securechat -keysize 2048 -validity 365
-keyalg RSA -dname "CN=localhost" -keypass sNetty -storepass sNetty
-keystore sChat.jks

步骤 3:生成证书签名请求:

复制代码
keytool -certreq -alias securechat -sigalg MD5withRSA -file sChat.csr
-keypass sNetty -storepass sNetty -keystore sChat.jks

步骤 4:用 CA 私钥进行签名:

复制代码
openssl ca -in sChat.csr -out sChat.crt -cert ca.crt -keyfile ca.key -notext

步骤 5:导入信任的 CA 根证书到 keystore:

复制代码
keytool -import -v -trustcacerts -alias ca_root -file ca.crt -storepass
sNetty -keystore sChat.jks

步骤 6:将 CA 签名后的 server 端证书导入 keystore:

复制代码
keytool -import -v -alias securechat -file server.crt -keypass sNetty
-storepass sNetty -keystore sChat.jks

2.3.2. 客户端证书制作

步骤 1:生成客户端密钥对:

复制代码
keytool -genkey -alias smcc -keysize 2048 -validity 365 -keyalg
RSA -dname "CN=localhost" -keypass cNetty -storepass cNetty -keystore cChat.jks

步骤 2:生成证书签名请求:

复制代码
keytool -certreq -alias smcc -sigalg MD5withRSA -file cChat.csr
-keypass cNetty -storepass cNetty -keystore cChat.jks

步骤 3:用 CA 私钥进行签名:

复制代码
openssl ca -in cChat.csr -out cNetty.crt -cert ca.crt -keyfile ca.key -notext

步骤 4:导入信任的 CA 根证书到 keystore:

复制代码
keytool -import -v -trustcacerts -alias ca_root -file ca.crt
-storepass cNetty -keystore cChat.jks

步骤 5:将 CA 签名后的 client 端证书导入 keystore:

复制代码
keytool -import -v -alias smcc -file cNetty.crt -keypass cNetty -storepass
cNetty -keystore cChat.jks

2.3.3. 开发和测试

基于 CA 认证的开发和测试与 SSL 双向和单向认证代码相同,此处不再赘述。

3. Netty SSL 源码分析

3.1. SSL 客户端

当客户端和服务端的 TCP 链路建立成功之后,SslHandler 的 channelActive 被触发,SSL 客户端通过 SSL 引擎发起握手请求消息,代码如下:

发起握手请求之后,需要将 SSLEngine 创建的握手请求消息进行 SSL 编码,发送给服务端,因此,握手之后立即调用 wrapNonAppData 方法,下面具体对该方法进行分析:

因为只需要发送握手请求消息,因此 Source ByteBuf 为空,下面看下 wrap 方法的具体实现:

将 SSL 引擎中创建的握手请求消息编码到目标 ByteBuffer 中,然后对写索引进行更新。判断写入操作是否越界,如果越界说明 out 容量不足,需要调用 ensureWritable 对 ByteBuf 进行动态扩展,扩展之后继续尝试编码操作。如果编码成功,返回 SSL 引擎操作结果。

对编码结果进行判断,如果编码字节数大于 0,则将编码后的结果发送给服务端,然后释放临时变量 out。

判断 SSL 引擎的操作结果,SSL 引擎的操作结果定义如下:

  1. FINISHED:SSLEngine 已经完成握手;
  2. NEED_TASK:SSLEngine 在继续进行握手前需要一个(或多个)代理任务的结果;
  3. NEED_UNWRAP:在继续进行握手前,SSLEngine 需要从远端接收数据,所以应带调用 SSLEngine.unwrap();
  4. NEED_WRAP: 在继续进行握手前,SSLEngine 必须向远端发送数据,所以应该调用 SSLEngine.wrap();
  5. NOT_HANDSHAKING:SSLEngine 当前没有进行握手。

下面我们分别对 5 种操作的代码进行分析:

如果握手成功,则设置 handshakePromise 的操作结果为成功,同时发送 SslHandshakeCompletionEvent.SUCCES 给 SSL 监听器,代码如下:

如果是 NEED_TASK,说明异步执行 SSL Task,完成后续可能耗时的操作或者任务,Netty 封装了一个任务立即执行线程池专门处理 SSL 的代理任务,代码如下:

如果是 NEED_UNWRAP,则判断是否由 UNWRAP 发起,如果不是则执行 UNWRAP 操作。

如果是 NOT_HANDSHAKING,则调用 unwrap,继续接收服务端的消息。

服务端应答消息的接收跟服务端接收客户端的代码类似,唯一不同之处在于 SSL 引擎的客户端模式设置不同,一个是服务端,一个是客户端。上层的代码处理是相同的,下面我们在 SSL 服务端章节分析握手消息的接收。

3.2. SSL 服务端

SSL 服务端接收客户端握手请求消息的入口方法是 decode 方法,下面对它进行详细分析。

首先获取接收缓冲区的读写索引,并对读取的偏移量指针进行备份:

对半包标识进行判断,如果上一个消息是半包消息,则判断当前可读的字节数是否小于整包消息的长度,如果小于整包长度,则说明本次读取操作仍然没有把 SSL 整包消息读取完整,需要返回 IO 线程继续读取,代码如下:

如果消息读取完整,则修改偏移量:同时置位半包长度标识。

下面在 for 循环中读取 SSL 消息,因为 TCP 存在拆包和粘包,因此一个 ByteBuf 可能包含多条完整的 SSL 消息。

首先判断可读的字节数是否小于协议消息头长度,如果是则退出循环继续由 IO 线程接收后续的报文:

获取 SSL 消息包的报文长度,具体算法不再介绍,可以参考 SSL 的规范文档进行解读,代码如下:

对长度进行判断,如果 SSL 报文长度大于可读的字节数,说明是个半包消息,将半包标识长度置位,返回 IO 线程继续读取后续的数据报,代码如下:

对消息进行解码,将 SSL 加密的消息解码为加密前的原始数据,unwrap 方法如下:

调用 SSLEngine 的 unwrap 方法对 SSL 原始消息进行解码,对解码结果进行判断,如果越界,说明 out 缓冲区不够,需要进行动态扩展。如果是首次越界,为了尽量节约内存,使用 SSL 最大缓冲区长度和 SSL 原始缓冲区可读的字节数中较小的。如果再次发生缓冲区越界,说明扩张后的缓冲区仍然不够用,直接使用 SSL 缓冲区的最大长度,保证下次解码成功。

解码成功之后,对 SSL 引擎的操作结果进行判断:如果需要继续接收数据,则继续执行解码操作;如果需要发送握手消息,则调用 wrapNonAppData 发送握手消息;如果需要异步执行 SSL 代理任务,则调用立即执行线程池执行代理任务;如果是握手成功,则设置 SSL 操作结果,发送 SSL 握手成功事件;如果是

应用层的业务数据,则继续执行解码操作,其它操作结果,抛出操作类型异常。

需要指出的是,SSL 客户端和服务端接收对方 SSL 握手消息的代码是相同的,那为什么 SSL 服务端和客户端发送的握手消息不同呢?这些是 SSL 引擎负责区分和处理的,我们在创建 SSL 引擎的时候设置了客户端模式,SSL 引擎就是根据这个来进行区分的,代码如下:

无论客户端还是服务端,只需要围绕 SSL 引擎的操作结果进行编程即可。

3.3. SSL 消息读取

SSL 的消息读取实际就是 ByteToMessageDecoder 将接收到的 SSL 加密后的报文解码为原始报文,然后将整包消息投递给后续的消息解码器,对消息做二次解码。基于 SSL 的消息解码模型如下:

SSL 消息读取的入口都是 decode,因为是非握手消息,它的处理非常简单,就是循环调用引擎的 unwrap 方法,将 SSL 报文解码为原始的报文,代码如下:

握手成功之后的所有消息都是应用数据,因此它的操作结果为 NOT_HANDSHAKING,遇到此标识之后继续读取消息,直到没有可读的字节,退出循环,代码如下:

如果读取到了可用的字节,则将读取到的缓冲区加到输出结果列表中,代码如下:

ByteToMessageDecoder 判断解码结果 List,如果非空,则循环调用后续的 Handler,由后续的解码器对解密后的报文进行二次解码。

3.4. SSL 消息发送

SSL 消息发送时,由 SslHandler 对消息进行编码,编码后的消息实际就是 SSL 加密后的消息,它的入口是 flush 方法,代码如下:

从待加密的消息队列中弹出消息,调用 SSL 引擎的 wrap 方法进行编码,代码如下:

wrap 方法很简单,就是调用 SSL 引擎的编码方法,然后对写索引进行修改,如果缓冲区越界,则动态扩展缓冲区:

对 SSL 操作结果进行判断,因为已经握手成功,因此返回的结果是 NOT_HANDSHAKING,执行 finishWrap 方法,调用 ChannelHandlerContext 的 write 方法,将消息写入发送缓冲区中,如果待发送的消息为空,则构造空的 ByteBuf 写入:

编码后,调用 ChannelHandlerContext 的 flush 方法消息发送给对方,代码如下:

复制代码
ctx.flush();

4. Netty 学习推荐书籍

目前市面上介绍 netty 的文章很多,如果读者希望系统性的学习 Netty,推荐两本书:

1) 《Netty in Action》,建议阅读英文原版。

2) 《Netty 权威指南》,建议通过理论联系实际方式学习。

5. 作者简介

李林锋,2007 年毕业于东北大学,2008 年进入华为公司从事高性能通信软件的设计和开发工作,有 6 年 NIO 设计和开发经验,精通 Netty、Mina 等 NIO 框架,Netty 中国社区创始人和 Netty 框架推广者。

联系方式:新浪微博 Nettying 微信:Nettying Netty 学习群:195820454


感谢郭蕾对本文的策划和审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。

2014-08-08 02:4015346
用户头像

发布了 25 篇内容, 共 69.7 次阅读, 收获喜欢 228 次。

关注

评论

发布
暂无评论
发现更多内容

四年Java开发,刚从美团、京东、阿里面试归来(已拿offer),分享个人面经

Java架构之路

Java 程序员 架构 面试 编程语言

Prometheus的使用:编写自己的exporter

Rayzh

Prometheus 监控告警

10万级ETL批量作业调度工具Taskctl-Web应用版0元授权永久使用

敏捷调度TASKCTL

大数据 kettle 运维自动化 海豚调度 批量任务

ECS实践案例丨逻辑卷的创建和扩容操作指导

华为云开发者联盟

数据库 数据 服务

架构师训练营第 1 期 第 13 周作业

李循律

极客大学架构师训练营

顶会两篇论文连发,华为云医疗AI低调中崭露头角

华为云开发者联盟

人工智能 医疗 华为云

用 Proxy 简单实现 Vue 3 的 Reactive

三钻

Java 大前端 Vue3 Proxy 七日更

鹅厂面经,希望对大家有用

Bryant-cx

大前端 面经

TARS RPC 通信框架|提供多种远程调用方式

TARS基金会

微服务 RPC 远程调用 TARS

有效破解行业难点 专家称区块链+医疗发展还需找准应用场景

CECBC

区块链 大数据 医疗

APP专项测试 —— 弱网测试

行者AI

测试

万字详解 阿里面试真题:请你说说索引的原理

Java架构师迁哥

程序员必看的Redis6.0多线程性能测试结果及分析 你看懂了嘛?

比伯

Java 编程 架构 面试

用 JavaScript 实现寻路算法 —— 编程训练

三钻

Java 算法 大前端 七日更 寻路算法

真香!阿里P8耗时半年著作660页Java高并发与网络编程实战总结

Java架构之路

Java 程序员 架构 面试 编程语言

使用LL算法构建AST —— 编程训练

三钻

Java 算法 大前端 七日更

摸透原理|一文带你了解 Redis 列表底层的实现方式

Java架构师迁哥

字典树 —— 字符串分析算法

三钻

算法 大前端 七日更 字典树

用 Range 实现 CSSOM 精准操作

三钻

大前端 Range 七日更 CSSOM

数字资产币种钱包APP系统开发方案

week5-homework-一致性哈希

J

极客大学架构师训练营

阿里P8整理出Nginx笔记:Nginx应用与运维实战核心篇

996小迁

Java nginx 架构 面试

用 JavaScript 实现一个 TicTacToe 游戏 —— 编程训练

三钻

Java 算法 大前端 游戏开发 七日更

滴滴Java后台3面题目:网络+内存溢出+各种锁+高性能+消息队列

Java架构之路

Java 程序员 架构 面试 编程语言

从中心到边缘,IoT正变为互联网之上的一张大网

华为云开发者联盟

物联网 IoT 网络

区块链钱包app系统开发源码案例

区块链和通证经济带来的,是一部分股份制公司的消亡与重生

CECBC

金融 通证经济

应对游戏业务的四大“崩溃”场景有妙招,安全畅玩不是梦!

华为云开发者联盟

游戏 场景 崩溃

Java并发编程:AQS的自旋锁

码农架构

Java java 并发

TARS 服务信息上报|全方位服务监控

TARS基金会

微服务 TARS 监控告警

Nice!有了这份Alibaba内部 “Java面试宝典”,跳槽面试稳了

Java架构之路

Java 程序员 架构 面试 编程语言

Netty系列之Netty安全性_安全_李林锋_InfoQ精选文章