使用安全套接字层(SSL)或IETF RFC 2246 "Transport Layer Security" (TLS)协议等协议实现安全通信但与传输无关的类。
安全通信模式包括:
诚信保护 。 SSL / TLS / DTLS可防止活动窃听器修改消息。
身份验证 在大多数模式下,SSL / TLS / DTLS提供对等身份验证。 服务器通常经过身份验证,客户端可以按服务器的请求进行身份验证。
保密(隐私保护) 。 在大多数模式下,SSL / TLS / DTLS会加密客户端和服务器之间发送的数据。 这样可以保护数据的机密性,使被动窃听器不会看到敏感数据,如财务信息或多种个人信息。
这些类型的保护由“密码套件”指定,“密码套件”是给定SSL连接使用的加密算法的组合。
在协商过程中,两个端点必须就两个环境中都可用的密码套件达成一致。
如果没有这样的套件,则不能建立SSL连接,也不能交换数据。
使用的密码套件由称为“握手”的协商过程建立。 此过程的目标是创建或重新加入“会话”,这可能会随着时间的推移保护许多连接。 握手完成后,您可以使用getSession()方法访问会话属性。
SSLSocket类提供了大部分相同的安全功能,但所有入站和出站数据都是使用基础Socket自动传输的,该设计使用了阻塞模型。 虽然这适用于许多应用程序,但此模型不提供大型服务器所需的可伸缩性。
SSLEngine的主要区别在于它对入站和出站字节流进行操作,与传输机制无关。 SSLEngine用户有责任为对等SSLEngine安排可靠的I / O传输。 通过分离SSL / TLS / DTLS从I / O传输机制抽象化,所述SSLEngine可用于各种各样的I / O类型的,例如non-blocking I/O (polling) , selectable non-blocking I/O , Socket和传统的输入/ OutputStreams,本地ByteBuffers或字节数组, future asynchronous I/O models等。
在较高的水平, SSLEngine因此出现:
app data
| ^
| | |
v | |
+----+-----|-----+----+
| | |
| SSL|Engine |
wrap() | | | unwrap()
| OUTBOUND | INBOUND |
| | |
+----+-----|-----+----+
| | ^
| | |
v |
net data
应用程序数据(也称为明文或明文)是由应用程序生成或使用的数据。
它的对应物是网络数据,它由握手和/或密文(加密)数据组成,并且通过I / O机制传输。
入站数据是从对等方接收的数据,出站数据以对等方为目的地。
(在SSLEngine的上下文中,术语“握手数据”用于表示为建立和控制安全连接而交换的任何数据。握手数据包括SSL / TLS / DTLS消息“alert”,“change_cipher_spec”和“握手” “。)
SSLEngine有五个不同的阶段。
创建 - SSLEngine已创建并初始化,但尚未使用。 在此阶段,应用程序可以设置任何SSLEngine特定设置(启用密码套件, SSLEngine是否应在客户端或服务器模式下握手,等等)。 但是,一旦握手开始,任何新设置(客户端/服务器模式除外,见下文)将用于下一次握手。
初始握手 - 初始握手是两个对等体交换通信参数直到建立SSLSession的过程。 在此阶段无法发送应用程序数据。
应用程序数据 - 一旦建立了通信参数并且握手完成,应用程序数据就可以流经SSLEngine 。 出站应用程序消息经过加密和完整性保护,入站消息可以反转该过程。
重新整理 - 任何一方都可以在应用程序数据阶段的任何时候请求重新协商会话。 新的握手数据可以在应用程序数据之间混合。 在开始重新握手阶段之前,应用程序可能会重置SSL / TLS / DTLS通信参数,例如启用的密码套件列表以及是否使用客户端身份验证,但无法在客户端/服务器模式之间进行更改。 和以前一样,一旦握手开始,任何新的SSLEngine配置设置将在下一次握手之前使用。
关闭 - 当不再需要连接时,客户端和服务器应用程序应各自关闭其各自连接的两端。 对于SSLEngine对象,应用程序应调用closeOutbound()并将任何剩余消息发送给对等方。 同样,应用程序应在调用closeInbound()之前从对等方接收任何剩余消息。 然后可以在关闭SSLEngine两侧后关闭基础传输机制。 如果连接未按顺序关闭(例如,在收到对等方的写入关闭通知之前调用closeInbound() ),则会引发异常以指示发生了错误。 一旦发动机关闭,它就不可重复使用:必须创建一个新的SSLEngine 。
一个SSLEngine通过调用创建SSLContext.createSSLEngine()从初始化SSLContext 。
任何配置参数应使得在第一次调用之前设置wrap() , unwrap() ,或beginHandshake() 。
这些方法都会触发初始握手。
通过分别在出站或入站数据上调用wrap()或unwrap() ,数据在引擎中移动。 根据SSLEngine的状态, wrap()调用可能会消耗源缓冲区中的应用程序数据,并可能在目标缓冲区中生成网络数据。 出站数据可以包含应用程序和/或握手数据。 对unwrap()的调用将检查源缓冲区,如果数据是握手信息,则可以提前握手;如果数据是应用程序,则可以将应用程序数据放在目标缓冲区中。 底层SSL / TLS / DTLS算法的状态将决定何时消耗和生成数据。
对wrap()和unwrap()调用返回SSLEngineResult ,其指示操作的状态,以及(可选)如何与引擎交互以进行操作。
SSLEngine生成/使用完整的SSL / TLS / DTLS数据包,并且在调用wrap()/unwrap()之间不会在内部存储应用程序数据。 因此,输入和输出ByteBuffer必须适当调整大小以保持可以产生的最大记录。 应使用对SSLSession.getPacketBufferSize()和SSLSession.getApplicationBufferSize()的调用来确定适当的缓冲区大小。 出站应用程序数据缓冲区的大小通常无关紧要。 如果缓冲条件不允许正确消耗/生成数据,则应用程序必须确定(通过SSLEngineResult )并更正问题,然后再次尝试呼叫。
例如,如果引擎确定没有足够的目标缓冲区空间可用,则unwrap()将返回SSLEngineResult.Status.BUFFER_OVERFLOW结果。 应用程序应调用SSLSession.getApplicationBufferSize()并将该值与目标缓冲区中的可用空间进行比较,并在必要时放大缓冲区。 同样,如果unwrap()要返回SSLEngineResult.Status.BUFFER_UNDERFLOW ,则应用程序应调用SSLSession.getPacketBufferSize()以确保源缓冲区有足够的空间来保存记录(必要时放大),然后获取更多的入站数据。
SSLEngineResult r = engine.unwrap(src, dst); switch (r.getStatus()) { BUFFER_OVERFLOW: // Could attempt to drain the dst buffer of any already obtained // data, but we'll just increase it to the size needed. int appSize = engine.getSession().getApplicationBufferSize(); ByteBuffer b = ByteBuffer.allocate(appSize + dst.position()); dst.flip(); b.put(dst); dst = b; // retry the operation. break; BUFFER_UNDERFLOW: int netSize = engine.getSession().getPacketBufferSize(); // Resize buffer if needed. if (netSize > dst.capacity()) { ByteBuffer b = ByteBuffer.allocate(netSize); src.flip(); b.put(src); src = b; } // Obtain more inbound network data for src, // then retry the operation. break; // other cases: CLOSED, OK. }
与SSLSocket不同,SSLEngine的所有方法都是非阻塞的。 SSLEngine实现可能需要可能需要较长时间才能完成的任务结果, SSLEngine可能需要阻止。 例如,TrustManager可能需要连接到远程证书验证服务,或者KeyManager可能需要提示用户确定将哪个证书用作客户端身份验证的一部分。 此外,创建加密签名并验证它们可能很慢,看似阻塞。
对于可能阻止的任何操作, SSLEngine将创建Runnable委派任务。 当SSLEngineResult指示需要委派的任务结果时,应用程序必须调用getDelegatedTask()以获取未完成的委派任务并调用其run()方法(可能使用不同的线程,具体取决于计算策略)。 应用程序应继续获取委派任务,直到不再存在,并再次尝试原始操作。
在通信会话结束时,应用程序应正确关闭SSL / TLS / DTLS链接。 SSL / TLS / DTLS协议具有闭包握手消息,在释放SSLEngine并关闭底层传输机制之前,应将这些消息传递给对等体。 可以通过以下之一启动关闭:SSLException,入站闭包握手消息或其中一种close方法。 在所有情况下,由发动机产生的关闭握手报文,并wrap()直至所得应被重复调用SSLEngineResult的状态返回‘关闭’,或isOutboundDone()返回true。 从wrap()方法获得的所有数据都应发送给对等方。
closeOutbound()用于向引擎发出信号,表明应用程序将不再发送任何数据。
对等体将通过发送其自己的闭包握手消息来发出关闭意图。 此消息已被接收和处理的地方后SSLEngine的unwrap()调用,应用程序可以检测关闭,通过调用unwrap()和寻找SSLEngineResult状态为‘关闭’,或者如果isInboundDone()返回true。 如果由于某种原因,对等方在没有发送正确的SSL / TLS / DTLS关闭消息的情况下关闭通信链路,则应用程序可以检测到流末端并且可以通过closeInbound()向引擎发出信号,表示将不再有要处理的入站消息。 某些应用程序可能会选择要求来自对等方的有序关闭消息,在这种情况下,它们可以检查闭包是由握手消息生成的,而不是由流末尾条件生成的。
在管理密码套件时,您需要了解两组密码套件:
支持的密码套件:SSL实现支持的所有套件。 此列表使用getSupportedCipherSuites()报告。
启用密码套件,可能少于全套支持套件。 使用setEnabledCipherSuites(String [])方法设置该组,并使用getEnabledCipherSuites()方法查询。 最初,将在代表最小建议配置的新引擎上启用一组默认密码套件。
实现默认值要求默认情况下仅启用对服务器进行身份验证并提供机密性的密码套件。
只有当双方明确同意未经认证和/或非私人(未加密)的通信时,才会选择这样的密码套件。
每个SSL / TLS / DTLS连接必须有一个客户端和一个服务器,因此每个端点必须决定要承担哪个角色。 该选择决定了谁开始握手过程以及每一方应该发送哪种类型的消息。 方法setUseClientMode(boolean)配置模式。 初始握手开始后,即使执行重新协商, SSLEngine也无法在客户端和服务器模式之间切换。
应用程序可能会选择在不同的线程中处理委派的任务。 当SSLEngine创建,目前AccessControlContext保存。 将使用此上下文处理所有将来的委派任务:即,将使用在引擎创建时捕获的上下文来做出所有访问控制决策。
并发注意事项 :有两个并发问题需要注意:
wrap()和unwrap()方法可以彼此同时执行。
SSL / TLS / DTLS协议使用有序数据包。 应用程序必须注意确保生成的数据包按顺序传递。 如果数据包无序到达,可能会发生意外或致命的结果。 例如:
synchronized (outboundLock) {
sslEngine.wrap(src, dst);
outboundQueue.put(dst);
} 作为推论,两个线程不能同时尝试调用相同的方法( wrap()或unwrap() ),因为无法保证最终的数据包排序。