yeskery

SSLEngine 结合 NIO 实现异步的 Socket

不存在 SSLServerSocketChannel

对于 BIO 通道的程序来讲,建立起 SSLServerSocket 之后,后续的工作就和普通的 ServerSocket 没有什么区别了,这是因为 JDK 中通过 JSSE 的 API,封装了 SSL 通道的实现逻辑,否则,类似于 C 程序员如果想要编写一个 https 的加密程序,那他基本得累个半死,所以,我们应该感谢 JAVA。

对于 NIO 通道来讲,我们一贯的思维也是存在一个 SSLServerSocketChannel,然后注册到 selector 中,后续的操作也和普通的 ServerSocketChannel 没什么区别了,但是,并不存在一个 SSLServerSocketChannel,关于这一点,JSSE 的官方的 Reference Guide 中:

nio_ssl_server_socket_channel

翻译出来的原因主要有一下两点:

  1. 因为 JDK 类继承限制,无法搞出一个 SSLServerSocketChannel,如果需要搞出这样的一个类,需要在源码和 API 中大动干戈。
  2. 其次,JSSE 的框架设计不想因为封装出一个 SSLServerSocketChannel,就将 NIO 的灵活多变,并且高性能的优势给抹掉。

因此,基于上述的原因,最终 JSSE 在 NIO 通道中并没有 SSLServerSocketChannel,而是采用 NIO + SSLEngine 的方式进行实现。

NIO + SSLEngine

NIO 的 selector 体系这里就不再说了,也即是 ServerSocketChannel 注册到 Selector 中,然后进行轮询,如果事件发生了,SelectionKey 就被遍历出来,然后判断监听的是 register 事件,还是 read/write 事件,事件触发后,将 socket 的数据读取到缓冲区中。这是 NIO 的思路,而如果在 NIO 中使用 SSL,也是延续这样的一个套路,不用做任何的改变.
当 read/write 事件发生之后,普通的 socket 直接就开始读取数据了,这个时候 SSLEngine 就开始工作了,首先 SSLEngine 会调用 beginHandShake 开始准备握手,然后执行握手,握手的工作是需要通过编程,调用 SSLEngine 的出栈和入栈来完成的,看下面的一个图:

ssl_engin

你可以认为 SSLEngine 相当于一个黑盒子,它的内部封装了 SSL 握手,会话等各种协议实现。而我们看到上面的四个蓝色的 buffer,左面的两个是应用程序中定义的 buffer,供应用程序使用的,右面的两个是需要从 socket 缓冲区或者 socketchannel 接收数据并存放的 buffer,用于缓存使用的;SSLEngine 的一共两个操作,一个是 wrap,叫做出栈,发出的出,从应用的 buffer 中,到网络缓冲区的 buffer 中,SSLEngine 起到的是转接的作用,相反的是 unwrap,数据从网络缓冲区流入到应用程序中。

我们为什么要了解 SSLEngine 这样的一个架构呢?

因为 NIO 方式的 SSL 交互,必须要通过 SSLEngine 来实现,甚至是握手也需要通过这种方法来实现,我们需要编程调用 wrap 和 unwrap,并根据出栈和入栈是否成功,和下一步的需要做的动作,再执行什么动作。

判断的依据是两个状态:

第一个是 SSLEngineResult.HandShakeStatus:

hand_shake_status

这个状态指示,当每一次 SSLEngine 的 wrap 或者 unwrap 之后下一步应该干什么。

例如 NEED_WRAP 状态,相当于下一步我们需要执行 wrap;如果状态是 NEED_UNWRAP 状态,那么下一步应该执行 unwrap; NOT_HANDSHAKING 说明握手没有成功,应该抛出异常;如果是是 NEED_TASK 状态,SSL 协议中定义了一些耗时比较长的操作,需要执行 delegate 操作,这里应该开启一个线程,将这个耗时比较长的操作给执行了,例如 CRL 的远程校验。

这里可以看到,我们编程的时候,应该会有一个 while 循环,因为 SSL 的握手协议不是一下子就完成的,需要客户端和服务器端反复的进行交互,这里面会不断的通过 SSLEngine 进行 wrap 和 unwrap 等其它的操作;

其次,需要对上面的这几种状态进行条件判断,可以使用 case 语句。

还有一个状态标识是 SSLEngineResult.Status

sslengine_result_status

这个属性是指示 wrap 或者 unwrap 方法是否执行成功和失败的,可以看到上面,如果 buffer 字节数没有足够的空间,就会报 overflow,因此,对于这个状态的判断,通常出现在异常处理和边界检验中。

SSLEngine 处理流程

SSL 通讯过程,主要包括握手,对话,关闭对话,三个步骤。其中握手部分的主要内容有协商协议,相互验证,生成并交换对称密钥,其中相互验证和对称密钥的交换是由非对称加密来完成的。在对话过程中,实际的明文是由之前生成的对称密钥来加密的。当对话结束后,互相发送结束信号结束通讯。

在上述过程中不仅仅是通讯双方简单的交换数据,更重要的是要根据 SSL 协议的要求,在特定的状态下发送或接受特定的数据,并且这些数据是经过处理的数据,也就是在 tcp 头和通讯正文之间还要包括一些 ssl 的信 息,并且正文是由特定形式加密的。SSLEngine 正是完成了管理状态,封装应用程序数据发往网络,解析网络数据并传递给应用程序的角色。

先看看 SSLEngine 有哪些状态,有哪些工作要做,假设从一个 ssl server 看。

首先在握手阶段,需要和 client 程序多次握手,进行身份验证,对称密钥生成等工作,在这段时间里,并没有实际的应用层数据交换,而只有 SSL 协议数据的交换。且不看实际传输的内容和意义,在握手过程中,Server 的 SSLEngine 初始化后总是等待 client 的请求(等待接收数据),此时它的状态是 NEED_UNWRAP,unwrap 是解包的意思,这意味着,SSLEngine 等待解析一个 SSL 的数据包。

当 server 收到数据包后,在 nio 中数据包总是放在一个 buffer 里而不再是读 stream,我们把这个 buffer 交给 SSLEngine,调用它的 unwrap 方法,SSLEngine 会解析这个数据包,把其中关于 SSL 握手的信息提取出来,并改变自己的状态,此处它将变成 NEED_WRAP 状态,意味着打包,它需要把对应的 SSL 回复内容写到数据包中返回到客户端。

以此类推,SSLEngine多数时间总是在解包和打包两个状态间切换,尤其是在实际通讯时,注意到在 unwrap 和 wrap 函数中都有一个源 buffer 和一个目的 buffer,因为 SSLEngine 不仅提取 SSL 协议相关的内容还要解密网络数据并把明文传递给应用程序,这其实才是这两个函数名字的来源,只不过在握手过程中,并没有实际的数据,而只有 SSL 协议信息,所以那个目的 buffer 总是没有东西。可以把SSL通讯看做交换礼物,SSLEngine把包裹拆了把礼物给你,或者他把礼物包起来送走,只是在SSL握手时,那个包裹里没有礼物,SSLEngine 只是拆了个空包裹或是寄了个空包裹。

那么还有没有其它状态,有一个 FINISHED 状态,表示这次handshake完成了;而当进入实际交换数据的时候,这个状态是 NOT_HANDSHAKE,表示当前不在握手,一般这个时候只需要在 socket 可读时,调用 unwrap 函数解密来自网络的 SSL 数据包,在 socket 可写的时候调用 wrap 函数把明文数据加密发送出去。

还有一个状态 NEED_TASK,首先要知道一点 SSLEngine 是异步的,wrap 和 unwrap 函数调用都会立刻返回,比如在 server 收到 client 第一次请求后,会调用 unwrap,但实际上 SSLEngine 还会做很多工作,比如访问 Keystore 文件,这些操作是费时的,但是实际上函数却立刻返回了,这时候 SSLEngine 会进入 NEED_TASK 状态,而不是立刻进入 NEED_WRAP 状态,所以必须让 SSLEngine 完成手头的工作,才能进入下一步工作,这时可以调用 SSLEngine的 getDelegatedTask() 方法获得那个尚未完成的工作,它是一个 Runnable 的对象,可以调用它的 run 方法等待他完成,如果你是个高并发的 server,也可以在这个时候做其他事情,等待这个工作完成,再接下去做wrap工作。

另外还有一个非常容易出错的地方,一个 NEED_UNWRAP 状态的下一个状态然有可能是 NEED_UNWRAP,并且一次调用 unwrap 方法并不一定把 buffer 中的所有内容都解包出来,可能还有内容需要在一次 unwrap 才能把所有东西都解析完,我遇到的这种情况发生在用 nio 的 server 和老的 SSLSocket 通讯时,client 只向 server 一次性发送了这些数据,而 server 端需要连续两次 unwrap 才能把 client 的数据完整处理掉。

除了上述 4 个状态描述了 SSLEngine 的状态,还有 4 个状态用于描述每次调用 wrap 和 unwrap 后的结果状态。它们分别是:

  • BUFFER_OVERFLOW 表示目标 buffer 没有足够的空间来存放解包的内容,这往往是因为你的目的buffer太小,或者在 buffer 在写入前没有 clear;
  • BUFFER_UNDERFLOW 表示源 buffer 没足够内容让 SSLEngine 来解包,这往往是因为,可能还有数据尚未到达,或者在 buffer 读取前没有 flip;
  • CLOSED 表示通讯的某一段正试图结束这个SSL通讯。
  • OK 表示 SSLEngin 完成了操作。

首先 NIO 的 socket 基本都通过 Selector 来实现,把 socket 的 accept,read,write 事件都注册到 selector 上,不断的循环 select()就可以,只是对于一个 SSL Server Socket 而言,它只是个普通的 ServerSocket,首先只关心 accept 事件,所以首先这在 selector 上注册一个事件。

当 serversocket 接收到一个 SSL client 的请求后,就要开始进行握手,这个过程是同步的,所以先不要把 read 和 write 事件也注册到 selector 上,当完成握手后,才注册这两个事件,并把 socket 设置成非阻塞。当 select 到 socket 可读时先调用 unwrap 方法,可写时先调用 wrap 方法。

每个 socket 都有两组 buffer,分别是 appIn,netIn 和 appOut,netOut,其中 netXX 都代表从 socket 中读取或写入的东西,他们都是加了密的,而 appXX 代表应用程序可理解的数据内容,它们都通过 SSLEngine 的 wrap 和 unwrap 方法才能与 netXX 相互转换。

示例

服务端示例

  1. public class SSLHandshakeServer {
  2. private static Logger logger = Logger.getLogger(SSLHandshakeServer.class.getName());
  3. private SocketChannel sc;//channel
  4. private SSLEngine sslEngine;//SSLEngine引擎
  5. private Selector selector;//NIO通道
  6. private ByteBuffer myNetData;
  7. private ByteBuffer myAppData;
  8. private ByteBuffer peerNetData;
  9. private ByteBuffer peerAppData;//四个buffer缓冲区
  10. private ByteBuffer dummy = ByteBuffer.allocate(0);
  11. private HandshakeStatus hsStatus;//SSLEngineResult.HandShakeStatus
  12. private Status status;//SSLEngineResult.Status
  13. public void run() throws Exception {
  14. char[] password = "123456".toCharArray();
  15. KeyStore keyStore = KeyStore.getInstance("JKS");
  16. InputStream in = this.getClass().getResourceAsStream("serverkeystore");
  17. keyStore.load(in, password);
  18. KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
  19. kmf.init(keyStore, password);
  20. SSLContext sslContext = SSLContext.getInstance("SSL");
  21. sslContext.init(kmf.getKeyManagers(), null, null);
  22. sslEngine = sslContext.createSSLEngine();
  23. sslEngine.setUseClientMode(false);
  24. SSLSession session = sslEngine.getSession();//初始化SSLEngine
  25. myAppData = ByteBuffer.allocate(session.getApplicationBufferSize());
  26. myNetData = ByteBuffer.allocate(session.getPacketBufferSize());
  27. peerAppData = ByteBuffer.allocate(session.getApplicationBufferSize());
  28. peerNetData = ByteBuffer.allocate(session.getPacketBufferSize());
  29. peerNetData.clear();//定义四个缓冲区
  30. //NIO的流程
  31. ServerSocketChannel serverChannel = ServerSocketChannel.open();
  32. serverChannel.configureBlocking(false);
  33. selector = Selector.open();
  34. ServerSocket serverSocket = serverChannel.socket();
  35. serverSocket.bind(new InetSocketAddress(443));
  36. serverChannel.register(selector, SelectionKey.OP_ACCEPT);
  37. logger.info("Server listens on port 443... ...");
  38. while (true) {
  39. selector.select();
  40. Iterator<SelectionKey> it = selector.selectedKeys().iterator();
  41. while (it.hasNext()) {
  42. SelectionKey selectionKey = it.next();
  43. it.remove();
  44. handleRequest(selectionKey);//当SelectionKey有事件进来后,进行NIO的处理
  45. }
  46. }
  47. }
  48. private void handleRequest(SelectionKey key) throws Exception {
  49. if (key.isAcceptable()) {
  50. ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
  51. SocketChannel channel = ssc.accept();
  52. channel.configureBlocking(false);
  53. channel.register(selector, SelectionKey.OP_READ);//当rigister事件发生后,下一步就是读了
  54. } else if (key.isReadable()) {
  55. sc = (SocketChannel) key.channel();
  56. logger.info("Server handshake begins... ...");
  57. //从这里,SSL的交互就开始了
  58. sslEngine.beginHandshake();//开始begin握手
  59. hsStatus = sslEngine.getHandshakeStatus();
  60. doHandshake();//开始进行正式的SSL握手
  61. if (hsStatus == HandshakeStatus.FINISHED) {//当握手阶段告一段落,握手完毕
  62. key.cancel();
  63. sc.close();
  64. }
  65. logger.info("Server handshake completes... ...");
  66. }
  67. }
  68. //这个方法就是服务器端的握手
  69. private void doHandshake() throws IOException {
  70. SSLEngineResult result;
  71. while (hsStatus != HandshakeStatus.FINISHED) {//一个大的while循环,
  72. logger.info("handshake status: " + hsStatus);
  73. switch (hsStatus) {//判断handshakestatus,下一步的动作是什么?
  74. case NEED_TASK://指定delegate任务
  75. Runnable runnable;
  76. while ((runnable = sslEngine.getDelegatedTask()) != null) {
  77. runnable.run();//因为耗时比较长,所以需要另起一个线程
  78. }
  79. hsStatus = sslEngine.getHandshakeStatus();
  80. break;
  81. case NEED_UNWRAP://需要进行入站了,说明socket缓冲区中有数据包进来了
  82. int count = sc.read(peerNetData);//从socket中进行读取
  83. if (count < 0) {
  84. logger.info("no data is read for unwrap.");
  85. break;
  86. } else {
  87. logger.info("data read: " + count);
  88. }
  89. peerNetData.flip();
  90. peerAppData.clear();
  91. do {
  92. result = sslEngine.unwrap(peerNetData, peerAppData);//调用SSLEngine进行unwrap操作
  93. logger.info("Unwrapping:\n" + result);
  94. // During an handshake renegotiation we might need to
  95. // perform
  96. // several unwraps to consume the handshake data.
  97. } while (result.getStatus() == SSLEngineResult.Status.OK//判断状态
  98. && result.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_UNWRAP
  99. && result.bytesProduced() == 0);
  100. if (peerAppData.position() == 0 && result.getStatus() == SSLEngineResult.Status.OK
  101. && peerNetData.hasRemaining()) {
  102. result = sslEngine.unwrap(peerNetData, peerAppData);
  103. logger.info("Unwrapping:\n" + result);
  104. }
  105. hsStatus = result.getHandshakeStatus();
  106. status = result.getStatus();
  107. assert status != status.BUFFER_OVERFLOW : "buffer not overflow." + status.toString();
  108. // Prepare the buffer to be written again.
  109. peerNetData.compact();
  110. // And the app buffer to be read.
  111. peerAppData.flip();
  112. break;
  113. case NEED_WRAP://需要出栈
  114. myNetData.clear();
  115. result = sslEngine.wrap(dummy, myNetData);//意味着从应用程序中发送数据到socket缓冲区中,先wrap
  116. hsStatus = result.getHandshakeStatus();
  117. status = result.getStatus();
  118. while (status != Status.OK) {
  119. logger.info("status: " + status);
  120. switch (status) {
  121. case BUFFER_OVERFLOW:
  122. break;
  123. case BUFFER_UNDERFLOW:
  124. break;
  125. }
  126. }
  127. myNetData.flip();
  128. sc.write(myNetData);//最后再发送socketchannel
  129. break;
  130. }
  131. }
  132. }
  133. public static void main(String[] args) throws Exception {
  134. new SSLHandshakeServer().run();
  135. }
  136. }

客户端示例

  1. public class SSLHandshakeClient {
  2. private static Logger logger = Logger.getLogger(SSLHandshakeClient.class.getName());
  3. private SocketChannel sc;
  4. private SSLEngine sslEngine;
  5. private Selector selector;
  6. private HandshakeStatus hsStatus;
  7. private Status status;
  8. private ByteBuffer myNetData;
  9. private ByteBuffer myAppData;
  10. private ByteBuffer peerNetData;
  11. private ByteBuffer peerAppData;
  12. private ByteBuffer dummy = ByteBuffer.allocate(0);
  13. public void run() throws Exception {
  14. char[] password = "123456".toCharArray();
  15. KeyStore trustStore = KeyStore.getInstance("JKS");
  16. InputStream in = this.getClass().getResourceAsStream("clienttruststore.jks");
  17. trustStore.load(in, password);
  18. TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
  19. tmf.init(trustStore);
  20. SSLContext sslContext = SSLContext.getInstance("SSL");
  21. sslContext.init(null, tmf.getTrustManagers(), null);
  22. sslEngine = sslContext.createSSLEngine();
  23. sslEngine.setUseClientMode(true);
  24. SSLSession session = sslEngine.getSession();
  25. myAppData = ByteBuffer.allocate(session.getApplicationBufferSize());
  26. myNetData = ByteBuffer.allocate(session.getPacketBufferSize());
  27. peerAppData = ByteBuffer.allocate(session.getApplicationBufferSize());
  28. peerNetData = ByteBuffer.allocate(session.getPacketBufferSize());
  29. peerNetData.clear();
  30. SocketChannel channel = SocketChannel.open();
  31. channel.configureBlocking(false);
  32. selector = Selector.open();
  33. channel.register(selector, SelectionKey.OP_CONNECT);
  34. channel.connect(new InetSocketAddress("localhost", 443));
  35. sslEngine.beginHandshake();
  36. hsStatus = sslEngine.getHandshakeStatus();
  37. while (true) {
  38. selector.select();
  39. Iterator<SelectionKey> it = selector.selectedKeys().iterator();
  40. while (it.hasNext()) {
  41. SelectionKey selectionKey = it.next();
  42. it.remove();
  43. handleSocketEvent(selectionKey);
  44. }
  45. }
  46. }
  47. private void handleSocketEvent(SelectionKey key) throws IOException {
  48. if (key.isConnectable()) {
  49. sc = (SocketChannel) key.channel();
  50. if (sc.isConnectionPending()) {
  51. sc.finishConnect();
  52. }
  53. doHandshake();
  54. sc.register(selector, SelectionKey.OP_READ);
  55. }
  56. if (key.isReadable()) {
  57. sc = (SocketChannel) key.channel();
  58. doHandshake();
  59. if (hsStatus == HandshakeStatus.FINISHED) {
  60. logger.info("Client handshake completes... ...");
  61. key.cancel();
  62. sc.close();
  63. }
  64. }
  65. }
  66. private void doHandshake() throws IOException {
  67. SSLEngineResult result;
  68. int count = 0;
  69. while (hsStatus != HandshakeStatus.FINISHED) {
  70. logger.info("handshake status: " + hsStatus);
  71. switch (hsStatus) {
  72. case NEED_TASK:
  73. Runnable runnable;
  74. while ((runnable = sslEngine.getDelegatedTask()) != null) {
  75. runnable.run();
  76. }
  77. hsStatus = sslEngine.getHandshakeStatus();
  78. break;
  79. case NEED_UNWRAP:
  80. count = sc.read(peerNetData);
  81. if (count < 0) {
  82. logger.info("no data is read for unwrap.");
  83. break;
  84. } else {
  85. logger.info("data read: " + count);
  86. }
  87. peerNetData.flip();
  88. peerAppData.clear();
  89. do {
  90. result = sslEngine.unwrap(peerNetData, peerAppData);
  91. logger.info("Unwrapping:\n" + result);
  92. // During an handshake renegotiation we might need to
  93. // perform
  94. // several unwraps to consume the handshake data.
  95. } while (result.getStatus() == SSLEngineResult.Status.OK
  96. && result.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_UNWRAP
  97. && result.bytesProduced() == 0);
  98. if (peerAppData.position() == 0 && result.getStatus() == SSLEngineResult.Status.OK
  99. && peerNetData.hasRemaining()) {
  100. result = sslEngine.unwrap(peerNetData, peerAppData);
  101. logger.info("Unwrapping:\n" + result);
  102. }
  103. hsStatus = result.getHandshakeStatus();
  104. status = result.getStatus();
  105. assert status != status.BUFFER_OVERFLOW : "buffer not overflow." + status.toString();
  106. // Prepare the buffer to be written again.
  107. peerNetData.compact();
  108. // And the app buffer to be read.
  109. peerAppData.flip();
  110. break;
  111. case NEED_WRAP:
  112. myNetData.clear();
  113. result = sslEngine.wrap(dummy, myNetData);
  114. hsStatus = result.getHandshakeStatus();
  115. status = result.getStatus();
  116. while (status != Status.OK) {
  117. logger.info("status: " + status);
  118. switch (status) {
  119. case BUFFER_OVERFLOW:
  120. break;
  121. case BUFFER_UNDERFLOW:
  122. break;
  123. }
  124. }
  125. myNetData.flip();
  126. count = sc.write(myNetData);
  127. if (count <= 0) {
  128. logger.info("No data is written.");
  129. } else {
  130. logger.info("Written data: " + count);
  131. }
  132. break;
  133. }
  134. }
  135. }
  136. public static void main(String[] args) throws Exception {
  137. new SSLHandshakeClient().run();
  138. }
  139. }

总结一下,NIO+SSLEngine 的这种方式,可以说是高度的开放,利用 SSLEngine 这个工具,来完成握手这个过程,而握手的过程需要通过编程和控制状态来实现,实际上,这也是 JDK 的 API 设计的缺陷之一,没有封装的好,让我们程序员写这些莫名其妙的代码,而 JDK 说加不了 SSLServerSocketChannel 其实应该就是一个说辞而已,不过,既然如此,那我们只需按照他的要求进行编程,也能达到 NIO 中 SSL 通道的效果。

SSLEngine 也可以用在 BIO 中,也就是在 BIO 你也可以这么搞,但是因为 BIO 中有 SSLServerSocket,这个类,一个类就可以搞定了,握手的内部实现和流程控制都在这个JDK的内部来实现,作为程序员完全不用掌握那么多。

本文内容来自:

  1. http://www.bubuko.com/infodetail-1500001.html
  2. https://blog.csdn.net/chyroger/article/details/7217383

评论

发表评论 点击刷新验证码

提示

该功能暂未开放