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个纳税人的社保号被攻击者在系统中完全删除了。
任何网络攻击都能够给企业造成破坏,但是如何将这些破坏具体量化成金融数据呢?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 -filesChat.cer -storepass cNetty -keystore cChat.jks
|
上述工作完成之后,我们就开始编写SSL服务端和客户端的代码,下面我们对核心代码进行讲解。
首先看服务端的代码,在TCP链路初始化的时候,创建SSLContext并对其进行正确的初始化,下面我们对SSLContext的创建进行讲解:
因为是客户端认证服务端,因此服务端需要正确的设置和加载私钥仓库KeyStore,相关代码如下:
初始化KeyManagerFactory之后,创建SSLContext并初始化,代码如下:
由于是单向认证,服务端不需要验证客户端的合法性,因此,TrustManager为空,安全随机数不需要设置,使用JDK默认创建的即可。
服务端的SSLContext创建完成之后,利用SSLContext创建SSL引擎SSLEngine,设置SSLEngine为服务端模式,由于不需要对客户端进行认证,因此NeedClientAuth不需要额外设置,使用默认值False。相关代码如下:
engine.setuseclientMode(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 |
|