https://blog.csdn.net/www646288178/article/details/112218359
1、TLSv1.2 Handshake步骤:
在java8 JSSE中,TLSv1.2的handshake文档链接:https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/tls.html#the_tls_1.2_handshake
下面我们直接上一个握手过程图:

在TLSv1.2 handshake中,排除掉Optional的选项(这里注意:证书部分也是可选内容,也就是说HTTPS这些基于SSL/TLS协议的通信,其实是不需要构建证书也可以访问的,前提是密匙交换算法要支持无证书模式),只看Handshake的必经阶段,这里摘自oracle官方SSLEngine Demo的内容:

链接:https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/samples/sslengine/SSLEngineSimpleDemo.java
可以看到,整个handshake步骤如下:
- client端发送clientHello packet,其中包含了client端所支持的TLS协议版本列表、cipher suites列表等。(wrap阶段)
- server端获取到clientHello后,进行协议版本和密匙交换算法等内容的协商。(unwrap阶段)
- server将第2步的协商结果通过serverHello packet返回给client。(wrap阶段)
- client端获取serverHello的协商结果。(unwrap阶段)。
- client发送clientKeyExchange数据包,其中包含用于生成通信密匙的public key。(wrap阶段)
- client发送ChangeCipherSpec数据包,通知server端,我client已经准备好进行加密通信了.(wrap阶段)
- client发送Finished数据包。(wrap阶段)
- server端获取到用于生成通信密匙的client public key。(unwrap阶段)
- server端获取到client端准备好加密通信的通知。(unwrap阶段)
- server端获取到client的Finished packet。(unwrap阶段)
- server端发送ChangeCipherSpec,通知client端我server端也准备好进行加密通信了。(wrap阶段)
- server端发送Finished。(wrap阶段),到此Server端的TLS handshake完成。
- client端获取到server的ChangeCipherSpec和Finished。到此Client端的TLS handshake完成。
附上一段密匙交换算法的文章:https://www.zhihu.com/question/65464646/answer/231934108
2、TLSv1.2 shutdown步骤
TLSv1.2的shutdown过程是一个双向通信过程,server和client都需要向对方发送close_notify来通知对方自己已经关闭,具体的关闭分为主动关闭、被动关闭和peer意外终止三种,具体的内容我贴一下从oracle官方的原文
For an orderly shutdown of an SSL/TLS connection, the SSL/TLS protocols require transmission of close messages. Therefore, when an application is done with the SSL/TLS connection, it should first obtain the close messages from the SSLEngine
, then transmit them to the peer using its transport mechanism, and finally shut down the transport mechanism
In addition to an application explicitly closing the SSLEngine
, the SSLEngine
might be closed by the peer (via receipt of a close message while it is processing handshake data), or by the SSLEngine
encountering an error while processing application or handshake data, indicated by throwing an SSLException
. In such cases, the application should invoke SSLEngine.wrap()
to get the close message and send it to the peer until SSLEngine.isOutboundDone()
returns true
(as shown in Example 6), or until the SSLEngineResult.getStatus()
returns CLOSED
.
In addition to orderly shutdowns, there can also be unexpected shutdowns when the transport link is severed before close messages are exchanged. In the previous examples, the application might get -1
or IOException when trying to read from the nonblocking SocketChannel
, or get IOException when trying to write to the non-blocking SocketChannel. When you get to the end of your input data, you should call engine.closeInbound()
, which will verify with the SSLEngine
that the remote peer has closed cleanly from the SSL/TLS perspective. Then the application should still try to shut down cleanly by using the procedure in Example 6. Obviously, unlike SSLSocket
, the application using SSLEngine
must deal with more state transitions, statuses, and programming.
链接:https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html#SSLEngine
3、SSLEgnine工作原理

可以这样理解,SSLEngine只是一个TLS协议数据的解析器和加密算法处理器,可以独立于Socket单独使用;
Application将plain text的数据通过SSLEngine.wrap(myAppBuf,myNetBuf)方法进行加密,MyNetBuf就是对app数据加密后的ByteBuffer对象;之后Socket调用write方法将MyNetBuf发送给peer。
Socket read获取到的加密数据,通过SSLEngine.unwrap(peerNetBuf,peerAppBuf)进行解密,peerAppBuf就是解密后的数据,可以随意解析。(NOTE:这里依然存在半包、粘包的问题)
4、SSLEngine Handshake阶段的两个重要的状态标识
NOTE:建议看官方原文,这里表述的是我的理解。
SSLEngineResult.Status
OK:wrap(发送数据)或者unwrap(接受数据)成功,没有错误。
CLOSED:对于handshake NEED_WRAP操作来说,就是当前端主动关闭了TLS通信;对于NEED_UNWRAP来说,就是peer主动调用了TLS通信,当前端获取到了peer发送过来的close_notify message。
BUFFER_UNDERFLOW(buffer空闲):理论上来说,这个情况不会出现在handshake NEED_WRAP阶段;对于NEED_UNWRAP阶段来说,(1)peerNetBuf空间不足,需要扩容;(2)peerNetBuf读取的数据出现了半包问题,需要继续从socket中read。
BUFFER_OVERFLOW(buffer溢出):对于NEED_WRAP来说,myNetBuf空间不足,需要扩充或者清空;对于NEED_UNWRAP,peerAppBuf不足,需要扩容或者清空。
SSLEngineResult.handshakeStatus
FINISHED:握手完成。
NEED_TASK:需要等待一些task的完成,否则handshake无法继续,出现这个情况时,后续engine的wrap和unwrap方法都会阻塞直到task完成。
NEED_UNWRAP:需要从peer端读取新的数据,否则handshake无法继续。
NEED_UNWRAP_AGAIN:与NEED_UNWRAP类似,但表示从peer读取的数据已经存在于本地了,这个状态下,不需要再重新走一遍网络,只要解析已经接收到的数据就可以了。NOTE:在java8_u151中,并没有这个枚举类型。
NEED_WRAP:需要向peer端发送数据,否则handshake无法继续。
NOT_HANDSHAKING:当前没有处于handshake阶段。
5、上代码
NOTE:代码使用无证书模式。
server端
-
package com.jsse.sslengine.newio.bio;
-
-
import com.jsse.sslengine.HandshakeUtils;
-
-
import javax.net.ssl.SSLContext;
-
import javax.net.ssl.SSLEngine;
-
import javax.net.ssl.SSLEngineResult;
-
import javax.net.ssl.SSLException;
-
import java.io.IOException;
-
import java.net.InetSocketAddress;
-
import java.nio.ByteBuffer;
-
import java.nio.channels.ServerSocketChannel;
-
import java.nio.channels.SocketChannel;
-
import java.nio.charset.StandardCharsets;
-
import java.security.KeyManagementException;
-
import java.security.NoSuchAlgorithmException;
-
import java.security.Security;
-
-
-
-
-
-
public class BioServerEngine {
-
public static void main(String[] args) throws NoSuchAlgorithmException, KeyManagementException {
-
Security.setProperty("jdk.tls.disabledAlgorithms", "SSLv3, RC4, MD5withRSA, EC keySize < 224");
-
SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
-
sslContext.init(null,null,null);
-
try (ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {
-
serverSocketChannel.configureBlocking(true);
-
serverSocketChannel.bind(new InetSocketAddress("localhost", 8080));
-
-
SSLEngine sslEngine = null;
-
try (SocketChannel socketChannel = serverSocketChannel.accept()) {
-
-
sslEngine = sslContext.createSSLEngine();
-
sslEngine.setUseClientMode(false);
-
-
sslEngine.setEnabledCipherSuites(new String[]{"SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA"});
-
-
sslEngine.beginHandshake();
-
HandshakeUtils.SSLEngineResultDTO sslEngineResultDTO = new HandshakeUtils.SSLEngineResultDTO(sslEngine, socketChannel,
-
sslEngine.getSession().getApplicationBufferSize(), sslEngine.getSession().getPacketBufferSize());
-
if (!HandshakeUtils.handshakeBio(sslEngineResultDTO)) {
-
System.out.println("握手失败!!!!!!!");
-
-
System.out.println("握手成功!!!!!!!!!!!!!!!!!!!!!!!!!");
-
sslEngineResultDTO.clearAllBuffer();
-
-
-
-
} catch (IOException e) {
-
-
-
-
-
client端
-
package com.jsse.sslengine.newio.bio;
-
-
import com.jsse.sslengine.HandshakeUtils;
-
-
import javax.net.ssl.SSLContext;
-
import javax.net.ssl.SSLEngine;
-
import javax.net.ssl.SSLEngineResult;
-
import javax.net.ssl.SSLException;
-
import java.io.IOException;
-
import java.net.InetSocketAddress;
-
import java.nio.channels.SocketChannel;
-
import java.nio.charset.StandardCharsets;
-
import java.security.KeyManagementException;
-
import java.security.NoSuchAlgorithmException;
-
import java.security.Security;
-
-
-
-
-
-
public class BioClientEngine {
-
public static void main(String[] args) throws NoSuchAlgorithmException, KeyManagementException {
-
Security.setProperty("jdk.tls.disabledAlgorithms", "SSLv3, RC4, MD5withRSA, EC keySize < 224");
-
SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
-
sslContext.init(null,null,null);
-
SSLEngine sslEngine = sslContext.createSSLEngine();
-
sslEngine.setUseClientMode(true);
-
sslEngine.setEnabledCipherSuites(new String[]{"SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA"});
-
try (SocketChannel socketChannel = SocketChannel.open()) {
-
socketChannel.connect(new InetSocketAddress("localhost", 8080));
-
HandshakeUtils.SSLEngineResultDTO sslEngineResultDTO = new HandshakeUtils.SSLEngineResultDTO(sslEngine, socketChannel,
-
sslEngine.getSession().getApplicationBufferSize(), sslEngine.getSession().getPacketBufferSize());
-
sslEngine.beginHandshake();
-
if (HandshakeUtils.handshakeBio(sslEngineResultDTO)) {
-
System.out.println("握手成功!!!!");
-
sslEngineResultDTO.clearAllBuffer();
-
byte[] bytes = "hello,i am client!".getBytes(StandardCharsets.UTF_8);
-
byte[] quitBytes = "quit".getBytes(StandardCharsets.UTF_8);
-
-
System.out.println("握手失败!!!!");
-
-
-
} catch (IOException e) {
-
-
-
-
Handshake核心处理逻辑
-
package com.jsse.sslengine;
-
-
import javax.net.ssl.SSLEngine;
-
import javax.net.ssl.SSLEngineResult;
-
import javax.net.ssl.SSLException;
-
import java.io.IOException;
-
import java.nio.ByteBuffer;
-
import java.nio.channels.SocketChannel;
-
-
public class HandshakeUtils {
-
-
-
-
-
-
-
-
-
public static boolean handshakeBio(SSLEngineResultDTO sslEngineResultDTO) throws IOException {
-
SSLEngine sslEngine = sslEngineResultDTO.sslEngine;
-
-
SocketChannel socketChannel = sslEngineResultDTO.socketChannel;
-
while (sslEngine.getHandshakeStatus() != SSLEngineResult.HandshakeStatus.FINISHED
-
&& sslEngine.getHandshakeStatus() != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) {
-
switch (sslEngine.getHandshakeStatus()) {
-
-
-
-
-
-
System.out.println("NEED_WRAP");
-
-
-
-
if(sslEngine.isOutboundDone()) {
-
-
if(sslEngineResultDTO.myNetBuf.position()>0) {
-
sslEngineResultDTO.myNetBuf.flip();
-
while(sslEngineResultDTO.myNetBuf.hasRemaining()) {
-
n += sslEngineResultDTO.socketChannel.write(sslEngineResultDTO.myNetBuf);
-
-
-
System.out.println("NEED_WRAP[CLOSED1]发送了["+n+"]个字节");
-
-
-
-
-
SSLEngineResult sslEngineResult = sslEngine.wrap(sslEngineResultDTO.myAppBuf,sslEngineResultDTO.myNetBuf);
-
-
if(handSSLStatus(sslEngineResult,1,sslEngineResultDTO)) {
-
-
-
sslEngineResultDTO.myNetBuf.flip();
-
while(sslEngineResultDTO.myNetBuf.hasRemaining()) {
-
n += sslEngineResultDTO.socketChannel.write(sslEngineResultDTO.myNetBuf);
-
-
System.out.println("NEED_WRAP发送了["+n+"]个字节");
-
-
sslEngineResultDTO.myNetBuf.clear();
-
-
-
-
-
-
} catch (SSLException e) {
-
-
-
sslEngine.closeOutbound();
-
-
} catch (ClosedException e2) {
-
-
sslEngine.closeOutbound();
-
sslEngineResultDTO.myNetBuf.flip();
-
while(sslEngineResultDTO.myNetBuf.hasRemaining()) {
-
n += sslEngineResultDTO.socketChannel.write(sslEngineResultDTO.myNetBuf);
-
-
System.out.println("NEED_WRAP[CLOSED]发送了["+n+"]个字节");
-
-
-
-
-
System.out.println("NEED_UNWRAP");
-
-
boolean underflow = false;
-
-
if(sslEngine.isInboundDone()) {
-
-
if(sslEngine.isOutboundDone()) {
-
-
-
sslEngine.closeOutbound();
-
-
-
-
-
-
rn = socketChannel.read(sslEngineResultDTO.peerNetBuf);
-
System.out.println("NEED_UNWRAP READ["+rn+"]个字节");
-
-
-
if(!sslEngine.isInboundDone()) {
-
-
-
-
sslEngine.closeInbound();
-
}catch (SSLException e) {
-
-
-
-
-
sslEngine.closeOutbound();
-
-
-
-
-
-
SSLEngineResult sslEngineResult = null;
-
sslEngineResultDTO.peerNetBuf.flip();
-
-
int unwrapSize = sslEngineResultDTO.peerNetBuf.position();
-
sslEngineResult = sslEngine.unwrap(sslEngineResultDTO.peerNetBuf,sslEngineResultDTO.peerAppBuf);
-
if(handSSLStatus(sslEngineResult,2,sslEngineResultDTO)) {
-
-
unwrapSize = sslEngineResultDTO.peerNetBuf.position()-unwrapSize;
-
System.out.println("unwrap了["+unwrapSize+"]个字节");
-
-
-
-
-
-
-
-
sslEngineResultDTO.peerNetBuf.compact();
-
-
-
}while(sslEngineResultDTO.peerNetBuf.hasRemaining()||sslEngineResult.bytesProduced()>0);
-
-
-
-
-
-
-
sslEngineResultDTO.peerNetBuf.clear();
-
-
} catch (SSLException e) {
-
-
-
sslEngine.closeOutbound();
-
}catch (ClosedException e2) {
-
-
if(sslEngine.isOutboundDone()) {
-
-
-
-
sslEngine.closeOutbound();
-
-
-
-
System.out.println("NEED_TASK");
-
-
sslEngine.getDelegatedTask().run();
-
-
-
-
-
-
-
throw new IllegalStateException("invalid HandshakeStatus:" + sslEngine.getHandshakeStatus());
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
public static boolean handSSLStatus(SSLEngineResult sslEngineResult, int type, SSLEngineResultDTO sslEngineResultDTO) {
-
String typeStr = type == 1 ? "WRAP" : "UNWRAP";
-
switch (sslEngineResult.getStatus()) {
-
-
System.out.format("%s-OK%n", typeStr);
-
-
-
System.out.format("%s-CLOSED%n", typeStr);
-
throw new ClosedException();
-
-
System.out.format("%s-BUFFER_OVERFLOW%n", typeStr);
-
-
-
-
int netBufSize = sslEngineResultDTO.sslEngine.getSession().getPacketBufferSize();
-
ByteBuffer newBuffer = null;
-
if (netBufSize > sslEngineResultDTO.myNetBuf.capacity()) {
-
newBuffer = ByteBuffer.allocate(netBufSize);
-
-
newBuffer = ByteBuffer.allocate(sslEngineResultDTO.myNetBuf.capacity() * 2);
-
-
if (sslEngineResultDTO.myNetBuf.position() != 0) {
-
sslEngineResultDTO.myNetBuf.flip();
-
-
newBuffer.put(sslEngineResultDTO.myNetBuf);
-
sslEngineResultDTO.myNetBuf = newBuffer;
-
-
-
int appBufSize = sslEngineResultDTO.sslEngine.getSession().getApplicationBufferSize();
-
ByteBuffer newBuffer = null;
-
if (appBufSize > sslEngineResultDTO.peerAppBuf.capacity()) {
-
newBuffer = ByteBuffer.allocate(appBufSize);
-
-
newBuffer = ByteBuffer.allocate(sslEngineResultDTO.peerAppBuf.capacity() * 2);
-
-
if (sslEngineResultDTO.peerAppBuf.position() != 0) {
-
sslEngineResultDTO.peerAppBuf.flip();
-
-
newBuffer.put(sslEngineResultDTO.peerAppBuf);
-
sslEngineResultDTO.peerAppBuf = newBuffer;
-
-
-
-
-
System.out.format("%s-BUFFER_UNDERFLOW%n", typeStr);
-
-
-
-
-
if (sslEngineResultDTO.peerNetBuf.capacity() < sslEngineResultDTO.sslEngine.getSession().getPacketBufferSize()) {
-
-
ByteBuffer newBuffer = ByteBuffer.allocate(sslEngineResultDTO.sslEngine.getSession().getPacketBufferSize());
-
sslEngineResultDTO.peerNetBuf.flip();
-
newBuffer.put(sslEngineResultDTO.peerNetBuf);
-
sslEngineResultDTO.peerNetBuf = newBuffer;
-
-
-
-
-
if (sslEngineResultDTO.peerNetBuf.position() >= sslEngineResultDTO.peerNetBuf.capacity() * 0.75) {
-
ByteBuffer newBuffer = ByteBuffer.allocate(sslEngineResultDTO.peerNetBuf.capacity() * 2);
-
sslEngineResultDTO.peerNetBuf.flip();
-
newBuffer.put(sslEngineResultDTO.peerNetBuf);
-
sslEngineResultDTO.peerNetBuf = newBuffer;
-
-
-
-
-
System.out.println("WARN!!!!,在WRAP阶段出现了BUFFER_UNDERFLOW");
-
-
-
-
throw new IllegalStateException("invalid sslEngineResult:" + sslEngineResult.getStatus());
-
-
-
-
public static class ClosedException extends RuntimeException {
-
-
-
-
public static class SSLEngineResultDTO {
-
public SSLEngine sslEngine;
-
public SocketChannel socketChannel;
-
public ByteBuffer myAppBuf;
-
public ByteBuffer myNetBuf;
-
public ByteBuffer peerAppBuf;
-
public ByteBuffer peerNetBuf;
-
-
public SSLEngineResultDTO(SSLEngine sslEngine, SocketChannel socketChannel, int appBufSize, int netBufSize) {
-
this.sslEngine = sslEngine;
-
this.socketChannel = socketChannel;
-
this.myAppBuf = this.peerAppBuf = ByteBuffer.allocate(appBufSize);
-
this.myNetBuf = this.peerNetBuf = ByteBuffer.allocate(netBufSize);
-
-
-
public void clearAllBuffer() {
-
-
-
-
-
-
-
6、遇到的问题
问题1:使用Socket API时,出现如下错误

产生原因:
Socket API通信双方在read和write时,需要使用message split character来解决粘包问题(其实最主要的还是解决read方法什么时候结束的问题),在前期开发中,使用过\n和\0作为分隔符,然而都曾出现过TSL协议无法解析的问题,下面是我截取的client端和server端的一部分handshake阶段内容
client:

75字节是client keyExchange阶段发送的报文内容,需要再额外增加1字节的\0,总共76字节;
在wireshark中,可以明显看到有两个PSH+ACK报文,分别是75字节和1字节;
server:

server端按照\0进行read,当遇到\0时,就认为是一个完整的tls报文(其中\0不会作为报文内容),此时发现,76个字节只接收了74个,理论上应该接收75个字节,也就是说,TSL的报文中包含\0这个字符,通过wireshark验证,也确实如此。
结论:
在使用Socket+SSLengine时,报文切割字符的方式比较危险,建议通过设置SO_TIMEOUT来实现。
问题2:使用SocketChannel BIO模型时,server端在unwrap阶段阻塞
产生的原因:
SocketChannel BIO模型下,read方法是无限阻塞模式的,SO_TIMEOUT并没有作用;
client端的changeCipherSpec和Finished的报文内容是分两次进行的NEED_WRAP,但是server端在NEED_UNWRAP时却是一次性从socketChannel中read出来的(也不清楚是因为粘包问题还是SSLEngine底层自动组合封装后一次性发送的,如果有知道的大佬,请指点我一下,谢谢。);
server端在unwrap client端的changeCipherSpec时,从socketChannel read出来的是changeCipherSpec和Finished两个报文内容,但是在执行sslengine.unwrap(peerNetBuf,peerAppBuf)时,只处理了changeCipherSpec,然后修改handshakestatus=NEED_UNWRAP,进入第二次循环的NEED_UNWRAP处理逻辑中,也就造成了第二次的socketChannel.read方法的执行,因为此时Client端已经发送完Finished,进入了NEED_UNWRAP阶段,也就不会再发送任何数据给server,因此server就会阻塞在SocketChannel.read方法上。
具体的分析截图:
client端handshake

server端的handshake:

上图中可以很明显的发现,client端分两次发送了6个字节和45个字节,但server端在一次unwrap中就读取了51个字节
结论:
在demo的handshake核心处理逻辑代码中,已经做了处理,具体看代码。
问题3:handshake阶段,如何处理shutdown
这个问题需要参考SSLEngine的API文档,里面的描述很清楚。
链接:https://docs.oracle.com/javase/8/docs/api/javax/net/ssl/SSLEngine.html
sslengine
原文:https://www.cnblogs.com/Janly/p/14279095.html