Java Secure Socket Extension (JSSE) -类和接口篇
- SocketFactory and ServerSocketFactory Classes
- SSLSocket and SSLServerSocket Classes
- SSLEngine Class
- SSLSession and ExtendedSSLSession
- HttpsURLConnection Class
- Support Classes and Interfaces
为了 通信安全,两侧的连接必须是SSL-enabled。JSSE API里,连接的endpoint类是SSLSocket和SSLEngine。
SSLSocketFactory或者SSLServerSocket生成的SSLSocket,接收入站连接。
SSLServerSocket由SSLServerSocketFactory增加。SSLSocketFactory和SSLServerSocketFactory对象由SSLContext生成。
SSLEngine由SSLContext直接生成,依靠这些处理全部I/O。
注意:
使用原生的SSLSocket和SSLEngine类的时候,在发送数据之前,你总是应该检查对方的证书。从JDK 7开始,endpoint识别/验证过程可以在SSL/TLS握手期间处理。见方法SSLParameters.setEndpointIdentificationAlgorithm。
例如,URL里的主机名应该和对方证书里的主机名匹配。如果不校验主机名,程序可以进行URL欺骗攻击。
SocketFactory and ServerSocketFactory Classes
抽象的javax.net.SocketFactory类用来增加sockets。它的子类是增加特定sockets的工厂,这样提供了一个添加公共socket级功能的通用框架。
抽象的javax.net.ServerSocketFactory类类似SocketFactory类,但是,只能用来增加服务器sockets。
Socket工厂是捕获与正在构造的sockets相关的各种策略简单的办法,这样生成sockets,不需要特定的配置代码:
- 通过工厂和sockets的多态性,不同类型的sockets可以使用相同的程序代码,仅仅传给不同类型的工厂
- 工厂使用构造socket时的参数,定制自己。比如,工厂返回的sockets可以有不同的网络超时或者其他安全参数
- 程序返回的sockets可以是java.net.Socket(或者javax.net.ssl.SSLSocket)的子类,这样可以直接暴露新的API,比如压缩、安全、打标签、统计或者防火墙隧道。
javax.net.ssl.SSLSocketFactory是增加安全sockets的工厂,是抽象类javax.net.SocketFactory的子类。安全socket工厂封装了增加和初始化配置安全sockets的细节。包括认证keys,对端证书骄傲眼、确定密码套件等。
javax.net.ssl.SSLServerSocketFactory类类似SSLSocketFactory类,但是用来增加服务器sockets。
Obtaining an SSLSocketFactory
使用下列办法获得SSLSocketFactory:
- 使用SSLSocketFactory.getDefault()静态方法获得默认的工厂
- 把工厂作为一个API参数。这样做,生成sockets的代码,不用关心配置sockets的细节,可以包括一个带SSLSocketFactory参数的方法。可以由客户端在增加sockets的时候调用(比如 javax.net.ssl.HttpsURLConnection)。
- 使用特定的配置行为构造一个新工厂
默认工厂支持服务器认证的典型配置,所以,默认工厂增加的sockets不会比普通的TCP socket泄漏更多的客户端信息。
增加和使用sockets的很多类不需要知道socket生成行为的细节。
你可以或者实现自己的socket工厂子类,或者使用另一个类作为socket工厂的工厂来增加新的socket工厂实例。比如SSLContext就是这样一个类。
SSLSocket and SSLServerSocket Classes
javax.net.ssl.SSLSocket类是标准的java.net.Socket类的子类。它支持全部的标准socket的方法,增加了安全sockets的特定方法。该类的实例封装了SSLContext。它有为socket实例增加安全的sessions的API,但是trust和key管理没有被直接暴露。
javax.net.ssl.SSLServerSocket用来增加服务器sockets。
为了防止对方欺骗,应该总是验证SSLSocket的证书。
注意:由于SSL和TLS协议的复杂性,很难预测从连接接收的信息是握手信息还是程序数据,数据的不同可能影响当前的连接状态(甚至导致过程的结束)。Oracle的JSSE实现里,通过SSLSocket.getInputStream()获得的对象的available()方法返回从SSL连接解密的还没有读取的程序数据的字节数。
Obtaining an SSLSocket
有两种办法获得SSLSocket实例:
- SSLSocketFactory实例的几个createSocket方法
- SSLServerSocket类的accept方法
Cipher Suite Choice and Remote Entity Verification
SSL/TLS协议定义一系列步骤确保受保护的连接。cipher suite的选择直接影响连接的安全类型。例如,如果选择了匿名的cipher suite,程序就没办法校验对方的身份。如果选择了不加密的suite,就不能保护数据的隐私。另外,SSL/TLS协议没规定接收的证书必须匹配对方可能期望发送的。如果连接不知何故重定向到一个流氓端,而流氓的证书基于当前的trust资料被接受了,会认为连接是有效的。
当使用原生的SSLSocket和SSLEngine类的时候,你应该总是在发送数据之前检查对方的证书。SSLSocket和SSLEngine类不自动校验URL里的主机名是否和对方证书里的主机名匹配。如果不校验主机名,程序可能被URL欺骗所利用。从JDK 7开始,endpoint识别/验证过程能在SSL/TLS期间被处理。
比如HTTPS协议不需要主机名校验。从JDK 7开始,默认地,HTTPS endpoint识别在握手期间由HttpsURLConnection强制执行。见SSLParameters.getEndpointIdentificationAlgorithm方法。另外,程序可以使用HostnameVerifier接口覆盖默认的HTTPS主机名规则。见HostnameVerifier接口和HttpsURLConnection类。
SSLEngine Class
TLS/DTLS越来越受欢迎。用于很多计算平台和设备。这要求TLS/DTLS使用不同的I/O和线程模型,以满足性能、扩展性、跟踪(footprint)和其他需求。这要求TLS/DTLS既能使用阻塞channels,也能使用非阻塞channels、支持异步、任意的输入/输出流和byte buffers。要求有高的可扩展性,能满足性能需求,能管理数以千计的网络连接。在Java SE中,使用SSLEngine类抽象I/O传输机制,允许程序以与传输无关的方式使用TLS/DTLS协议,开发者可以自由选择最符合需要的传输和计算模型。这些抽象使程序既可以使用非阻塞I/O channels也可以使用其他I/O模型,还包括不同的线程模型。因为这些灵活性,开发者必须管理I/O和线程,以及对TLS / DTLS协议的一些了解。初学者应该使用SSLSocket。
使用其他API,比如Java Generic Security Services(Java GSS-API)和Java Simple Authentication Security Layer (Java SASL)的开发者要注意相似之处在于应用程序还负责传输数据。
核心类是javax.net.ssl.SSLEngine。它包含TLS/DTLS状态机,由SSLEngine的用户操作入站出站byte buffers。
下图表明了程序的数据流向。
程序,显示在左边,提供数据,放入buffer,传给SSLEngine。SSLEngine对象处理buffer里的数据(包括握手数据),产生TLS/DTLS编码的数据,放到程序的网络buffer。程序然后使用恰当的传输(显示在右边),把内容发送给对端的网络buffer。从对端接收到TLS/DTLS编码的数据,程序把数据放到网络buffer,传给SSLEngine。SSLEngine对象处理网络buffer的内容,生成握手数据或者程序数据。
SSLEngine的实例可以有下列状态:
- Creation:SSLEngine已经生成,且已经初始化完成,但是还没有使用。期间,程序可以配置SSLEngine(启用的密码套件,SSLEngine工作在客户端方式还是服务器方式等)一旦开始握手,任何新的设置(除了客户端/服务器方式)将在下次握手时生效
- Initial handshaking:初始握手是一个过程,双方交换通信参数,直到SSLSession建立。此阶段,不能发送程序数据
- Application data:确定了通信参数,握手完成以后,可以传输程序数据了。出站消息是加密的,收到了完整性保护,入站数据做相反的处理
- Rehandshaking:在Application Data阶段的任何时间,session的任何一侧都可以请求重新谈判。新的握手数据可以和程序数据混合。在重新握手阶段开始前,程序可以复位TLS/DTLS通信参数,比如可以启用的加密套件列表,比如是否需要客户端认证,但是不能改变客户端/服务器方式。握手开始以后,新的SSLEngine配置就得等下次握手再生效了
- Closure:当不再需要连接了,程序应该关闭SSLEngine,在关闭底层传输机制之前,应该发送/接收剩余消息。一旦引擎被关闭了,将不再可用,必须增加新的SSLEngine
Understanding SSLEngine Operation Statuses
SSLEngine的状态由SSLEngineResult.Status表示。
想知道引擎的状态,想知道程序应该做什么操作,可以使用SSLEngine.wrap()和SSLEngine.unwrap()方法返回一个SSLEngineResult实例。
SSLEngineResult对象包含两条状态信息:引擎的整体状态和握手状态。
SSLEngineResult.Status枚举的值如下:
- OK:没有错误
- CLOSED:操作关闭了SSLEngine,或者操作完成不了,因为引擎已经关闭
- BUFFER_UNDERFLOW:输入buffer数据不足,程序应该获取更多对端数据(比如从网络读数据)
- BUFFER_OVERFLOW:输出buffer没有足够的空间保存结果,程序应该清理或者扩充buffer
SSLEngineResult res = engine.unwrap(peerNetData, peerAppData);
switch (res.getStatus()) {
case BUFFER_OVERFLOW:
// 可能需要放大peer application data buffer
if (engine.getSession().getApplicationBufferSize() > peerAppData.capacity()) {
// 放大buffer
} else {
// compact or clear the buffer
}
// retry
break;
case BUFFER_UNDERFLOW:
// 可能需要放大peer network packet buffer
if (engine.getSession().getPacketBufferSize() > peerNetData.capacity()) {
// 放大
} else {
// compact or clear the buffer
}
// 获取更多入站数据,然后重试操作
break;
// 处理其他状态: CLOSED, OK
// ...
}
握手状态由SSLEngineResult.HandshakeStatus表示:
- FINISHED:SSLEngine已经完成了握手
- NEED_TASK:握手能继续之前,SSLEngine需要一个或者更多的授权任务结果
- NEED_UNWRAP:握手能继续之前,SSLEngine需要从对端接收数据
- NEED_UNWRAP_AGAIN:握手能继续之前,SSLEngine需要unwrap。表示先前接收的数据还未解释,不用重新接收。数据进入JSSE框架,但是还没处理
- NEED_WRAP:握手能继续之前,SSLEngine需要发送数据,所以应该调用SSLEngine.wrap()了
- NOT_HANDSHAKING:SSLEngine当前不在握手进行中
void doHandshake(SocketChannel socketChannel, SSLEngine engine,
ByteBuffer myNetData, ByteBuffer peerNetData) throws Exception {
// 增加byte buffers,用来保存程序数据
int appBufferSize = engine.getSession().getApplicationBufferSize();
ByteBuffer myAppData = ByteBuffer.allocate(appBufferSize);
ByteBuffer peerAppData = ByteBuffer.allocate(appBufferSize);
// 开始握手
engine.beginHandshake();
SSLEngineResult.HandshakeStatus hs = engine.getHandshakeStatus();
// 处理握手信息
while (hs != SSLEngineResult.HandshakeStatus.FINISHED &&
hs != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) {
switch (hs) {
case NEED_UNWRAP:
// 从对端接收握手数据
if (socketChannel.read(peerNetData) < 0) {
// channel到达流的结尾
}
// 处理入站握手数据
peerNetData.flip();
SSLEngineResult res = engine.unwrap(peerNetData, peerAppData);
peerNetData.compact();
hs = res.getHandshakeStatus();
// 检查状态
switch (res.getStatus()) {
case OK :
// 处理 OK 状态
break;
// 处理其他状态: BUFFER_UNDERFLOW, BUFFER_OVERFLOW, CLOSED
// ...
}
break;
case NEED_WRAP :
// 清空 local network packet buffer.
myNetData.clear();
// 生成握手数据
res = engine.wrap(myAppData, myNetData);
hs = res.getHandshakeStatus();
// 检查状态
switch (res.getStatus()) {
case OK :
myNetData.flip();
// 发送握手数据
while (myNetData.hasRemaining()) {
socketChannel.write(myNetData);
}
break;
// 处理其他状态: BUFFER_OVERFLOW, BUFFER_UNDERFLOW, CLOSED
// ...
}
break;
case NEED_TASK :
// 处理阻塞的任务
break;
// 处理其他状态: // FINISHED or NOT_HANDSHAKING
// ...
}
}
// 处理其他握手
// ...
}
每个结果有两种状态,表明程序必须执行两个动作。比如,调用SSLEngine.unwrap()返回SSLEngineResult.Status.OK表明输入数据处理完毕,
SSLEngineResult.HandshakeStatus.NEED_UNWRAP表明程序应该获取更多的TLS/DTLS编码的数据,提供给SSLEngine.unwrap(),这样可以继续握手。
SSLEngine for TLS Protocols
下来看看如何增加一个SSLEngine对象,如何用它生成和处理TLS数据。
Creating an SSLEngine Object
使用SSLContext.createSSLEngine() 方法增加javax.net.ssl.SSLEngine对象。
增加SSLEngine对象之前,你必须配置引擎,充当客户端还是服务器,设置其他参数,比如加密套件和是否需要客户端认证。
import javax.net.ssl.*;
import java.security.*;
// 使用key,增加和初始化SSLContext
char[] passphrase = "passphrase".toCharArray();
// 首先初始化key和trust
KeyStore ksKeys = KeyStore.getInstance("JKS");
ksKeys.load(new FileInputStream("testKeys"), passphrase);
KeyStore ksTrust = KeyStore.getInstance("JKS");
ksTrust.load(new FileInputStream("testTrust"), passphrase);
// KeyManagers决定使用什么key
KeyManagerFactory kmf = KeyManagerFactory.getInstance("PKIX");
kmf.init(ksKeys, passphrase);
// TrustManagers决定是否允许连接
TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
tmf.init(ksTrust);
// 获取TLS协议的SSLContext实例
sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
// 增加引擎
SSLEngine engine = sslContext.createSSLengine(hostname, port);
// 当作客户端
engine.setUseClientMode(true);
Generating and Processing TLS Data
SSLEngine的两个主要方法是wrap()和unwrap()。他们分别负责生成和使用网络数据。依赖SSLEngine对象的状态,数据可能是握手数据,也可能是程序数据。
每个SSLEngine对象,在生存期内可分为几个阶段。在程序数据能被发送或者接收之前,TLS协议需要通过握手建立加密参数。握手需要几次往返步骤。
在初始化握手期间,wrap()和unwrap()方法生成或者消费握手数据。wrap()和unwrap()方法序列重复多次,直到握手完成。每个SSLEngine操作生成一个SSLEngineResult实例,其中的SSLEngineResult.HandshakeStatus属性被用来决定接下来做什么操作。
握手完成以后,再调用wrap()方法将尝试消费数据数据,为传输打包数据。unwrap()跟它相反。
要把数据送给对端,程序首先提供想要发送的数据,数据由SSLEngine.wrap()生成TLS编码数据。程序然后使用选择的传输机制发给对端。
当程序接收到对端发来的TLS编码数据,由SSLEngine.unwrap()生成纯文本数据。
发送“hello”
// 增加非阻塞channel
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress(hostname, port));
// 完成连接
while (!socketChannel.finishedConnect()) {
// 做一些事情,直到连接建立
}
//增加byte buffers,保存程序数据和编码后的数据
SSLSession session = engine.getSession();
ByteBuffer myAppData = ByteBuffer.allocate(session.getApplicationBufferSize());
ByteBuffer myNetData = ByteBuffer.allocate(session.getPacketBufferSize());
ByteBuffer peerAppData = ByteBuffer.allocate(session.getApplicationBufferSize());
ByteBuffer peerNetData = ByteBuffer.allocate(session.getPacketBufferSize());
// 握手
doHandshake(socketChannel, engine, myNetData, peerNetData);
myAppData.put("hello".getBytes());
myAppData.flip();
while (myAppData.hasRemaining()) {
// 生成TLS/DTLS编码数据(握手数据或者程序数据)
SSLEngineResult res = engine.wrap(myAppData, myNetData);
// 处理状态
if (res.getStatus() == SSLEngineResult.Status.OK) {
myAppData.compact();
// 发送TLS/DTLS编码数据
while(myNetData.hasRemaining()) {
int num = socketChannel.write(myNetData);
if (num == 0) {
// 没有要写的数据了,以后再试
}
}
}
// 处理其他状态: BUFFER_OVERFLOW, CLOSED
...
}
接收数据
// 读TLS/DTLS编码数据
int num = socketChannel.read(peerNetData);
if (num == -1) {
// 没数据了
} else if (num == 0) {
// 没有数据,再试一次
} else {
// 处理入站数据
peerNetData.flip();
res = engine.unwrap(peerNetData, peerAppData);
if (res.getStatus() == SSLEngineResult.Status.OK) {
peerNetData.compact();
if (peerAppData.hasRemaining()) {
// Use peerAppData
}
}
// 处理其他状态: BUFFER_OVERFLOW, BUFFER_UNDERFLOW, CLOSED
...
}
Dealing With Blocking Tasks
握手期间,SSLEngine可能遭遇阻塞的任务或者任务执行了很长时间。
比如,TrustManager需要连接到远方的证书校验服务,或者KeyManager需要提醒用户决定客户端认证使用的证书。
SSLEngine是非阻塞的,碰到这样的任务,将返回SSLEngineResult.HandshakeStatus.NEED_TASK。
收到这个状态,程序应该调用SSLEngine.getDelegatedTask()获取这个任务,然后,使用适当的线程模型,处理任务。
当主线程处理其他I/O的时候,程序可以从线程池获取线程处理该任务。
下面的代码使用新增加的线程处理每个任务:
if (res.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_TASK) {
Runnable task;
while ((task = engine.getDelegatedTask()) != null) {
new Thread(task).start();
}
}
SSLEngine将阻塞接下来的wrap()和unwrap()调用,直到完成了这些任务。
Shutting Down a TLS/DTLS Connection
为了有序关闭TLS/DTLS连接, TLS/DTLS协议需要传递关闭消息。因此,当一个程序完成了TLS/DTLS连接,它需要首先从SSLEngine获得关闭消息,然后传给对端,最后关闭传输机制。
除了明确关闭SSLEngine的程序之外,SSLEngine可能被对端关闭(当处理握手数据时,收到关闭消息),或者处理程序数据或者握手数据时,发生错误(SSLException)。此时,程序应该调用SSLEngine.wrap()获取关闭消息,送给对端,直到SSLEngine.isOutboundDone()返回true,或者直到SSLEngineResult.getStatus()返回CLOSED。
除了有序关闭,当传输链路在交换关闭消息前被切断,也可能意外关闭。程序可能得到-1或者在从非阻塞的SocketChannel读数据时抛IOException异常。
当你读完输入数据,应该调用engine.closeInbound(),它会校验对端是否已关闭。然后,程序仍然应该尝试上面的关闭过程。
明显地,不像SSLSocket,程序使用SSLEngine必须处理更多的状态。
// Indicate that application is done with engine
engine.closeOutbound();
while (!engine.isOutboundDone()) {
// 获取关闭消息
SSLEngineResult res = engine.wrap(empty, myNetData);
// 检查状态
// 发送关闭消息
while(myNetData.hasRemaining()) {
int num = socketChannel.write(myNetData);
if (num == 0) {
// 没有要写的数据了,等一会重试
}
myNetData().compact();
}
}
// 关闭
socketChannel.close();
SSLSession and ExtendedSSLSession
javax.net.ssl.SSLSession接口代表协商好的安全上下文。session建立以后,被连接两端的未来的SSLSocket或者SSLEngine对象共享。
在某些情况下,握手期间协商的参数将在握手后期进行,以便做出有关信任的决策。比如,有效的签名算法可能限制用来做认证的证书类型。在握手期间,通过调用SSLSocket或者SSLEngine对象的getHandshakeSession()方法可以获取SSLSession。TrustManager和KeyManager的实现可以使用getHandshakeSession()获取session参数的相关信息,帮助他们做决定。
完整的SSLSession初始化包括通信使用的加密套件,和增加时间、最后使用时间等管理信息。session也包含共享的协商秘密,用来增加加密key和保护通信的完整性。
ExtendedSSLSession扩展了SSLSession接口,支持附加属性。ExtendedSSLSession增加的方法描述双方支持的签名算法。在请求的Server Name Indication (SNI) Extension中,getRequestedServerNames()用来获取SNIServerName对象列表。服务器应该使用请求的服务器名称,指导自己选择恰当的签名证书,以及安全策略的其他信息。客户端应该使用请求的服务器名称指导它的端点识别,以及安全策略的其他信息。
调用SSLSession的getPacketBufferSize()和getApplicationBufferSize()方法,SSLEngine可以决定buffer容量。
注意:TLS协议规定,packets最多包含16KB的纯文本。但是,很多实现违反了规定,生成超过32KB的大记录。如果SSLEngine.unwrap()检测到入站大包,SSLSession返回的buffer容量就变成动态的。程序应该总是检查BUFFER_OVERFLOW和BUFFER_UNDERFLOW状态,必要的时候扩大buffer的容量。SunJSSE总是发送标准的16KB记录,允许入站的32KB记录。
HttpsURLConnection Class
javax.net.ssl.HttpsURLConnection类扩展了java.net.HttpURLConnection,支持HTTPS相关特性。
HTTPS类似HTTP,但是,HTTPS首先通过TLS sockets建立安全channel,然后在请求和接收数据前验证对端身份。
要获取HttpsURLConnection实例,你可以实际上通过URLConnection.connect()方法初始化网络连接之前,配置HTTP和HTTPS参数。
Setting the Assigned SSLSocketFactory
有时候,要定制HttpsURLConnection实例使用的SSLSocketFactory。
比如,你可能想要默认实现不支持的代理隧道(tunnel)。新的SSLSocketFactory可以返回已经执行了所有必要隧道操作的的sockets,这样HttpsURLConnection可以使用代理。
HttpsURLConnection类有默认的SSLSocketFactory,类被加载的时候就分配好了(通过SSLSocketFactory.getDefault()方法返回)。
后来的HttpsURLConnection实例将继承默认的SSLSocketFactory,直到新的默认SSLSocketFactory通过静态方法HttpsURLConnection.setDefaultSSLSocketFactory()分配给类。
一旦生成了HttpsURLConnection的实例,实例的SSLSocketFactory可以通过调用setSSLSocketFactory()覆盖。
Setting the Assigned HostnameVerifier
如果URL的主机名不匹配TLS握手时接收到的证书里的主机名,就可能是发生了URL攻击。如果实现不能确定主机匹配的合理性,TLS实现将回调分配给实现的HostnameVerifier。主机名验证将执行必要的步骤,作出决定,比如看主机名是否匹配,或者打开交互式的对话框。验证失败会关闭连接。
Support Classes and Interfaces
类 | 算法或协议 |
---|---|
KeyStore | PKCS12 |
KeyManagerFactory | PKIX, SunX509 |
TrustManagerFactory | PKIX (X509 or SunPKIX), SunX509 |
SSLContext | SSLv3, TLSv1, TLSv1.1, TLSv1.2, TLSv1.3, DTLSv1.0, DTLSv1.2 |
SSLContext Class
javax.net.ssl.SSLContext类是安全socket协议的引擎类。该类的实例实际上是SSLSocket、SSLServerSocket和SSLEngine的工厂类。
SSLContext对象保存了全部状态信息,共享给由它生成的全部对象。比如,SSLContext关联的session状态。这些缓存的session能重用,共享给其他sockets。
每个实例通过它的init方法配置keys、证书链、需要认证的信任的根CA证书。配置以key和trust管理器的方式提供。这些管理器提供认证支持和密码套件的关键约定。
目前,只支持基于X.509的管理器。
Obtaining and Initializing the SSLContext Class
使用SSLSocketFactory或者SSLServerSocketFactory生成SSLContext。
有两种办法生成SSLContext:
- 最简单的是SSLSocketFactory或者SSLServerSocketFactory静态方法SSLContext.getDefault。该方法生成默认的SSLContext,包括默认的KeyManager、TrustManager和SecureRandom(安全随机数生成器)。默认的KeyManagerFactory和TrustManagerFactory可以生成KeyManager和TrustManager。使用的密钥材料位于默认密钥库(keystore)和信任库(truststore)中
- 这种办法给调用者更大的控制权,使用SSLContext类的SSLContext.getDefault方法,然后通过调用实例的init()方法初始化上下文。init()方法的一个变种需要三个参数:KeyManager数组、TrustManager数组和SecureRandom。KeyManager对象和TrustManager对象或者通过实现接口增加,或者使用KeyManagerFactory和TrustManagerFactory增加。KeyManagerFactory和TrustManagerFactory的init()方法使用的参数是保存在KeyStore里的密钥材料。
一旦建立了TLS连接,就生成SSLSession,包含诸如身份和密码套件之类的信息。SSLSession被用来描述两个实体间的关系和状态信息。
每个TLS连接一次只涉及一个session,但是,session可以在这些实体之间,同时或者顺序地被很多连接使用。
Creating an SSLContext Object
SSLContext类的getInstance()可以生成SSLContext对象。
这些静态方法每一个都可以返回一个实例,包含至少一个安全socket协议。返回的实例也可以实现其他协议。例如,getInstance(“TLSv1”)返回的实例实现了TLSv1, TLSv1.1和TLSv1.2。增加SSLSocket、SSLServerSocket或者SSLEngine的时候,getSupportedProtocols()方法返回支持的协议列表。你能使用setEnabledProtocols(String[] protocols) 方法,控制使用哪个协议建立SSL连接。
public static SSLContext getInstance(String protocol);
public static SSLContext getInstance(String protocol, String provider);
public static SSLContext getInstance(String protocol, Provider provider);
如果只传协议名,系统将决定请求的协议的实现是否有效。如果有多个实现,将使用最合适的一个。
如果传了协议名和供应商,系统将决定是否有协议的实现,如果找不到,抛出异常。
可以这样获取SSLContext:
SSLContext sc = SSLContext.getInstance("TLS");
也可以使用这个方法:
public void init(KeyManager[] km, TrustManager[] tm, SecureRandom random);
如果KeyManager[]是null,该上下文将有一个空的KeyManager。如果TrustManager[]是null,将搜索安装的安全供应商,获取一个恰当的TrustManager。如果SecureRandom是null,将使用默认实现。
TrustManager Interface
TrustManager的主要任务是决定接收的认证证书是否应该被信任。如果证书不被信任,连接被断开。
要认证对方的身份,你必须使用一个或者多个TrustManager对象初始化SSLContext。你必须给支持的每个认证机制传递一个TrustManager。如果SSLContext初始化时,传的是null,将为你增加一个TrustManager。通常是支持基于X.509的公钥证书做认证的TrustManager(比如X509TrustManager)一些安全socket实现也支持基于共享密钥、Kerberos或者其他机制的认证。
可以使用TrustManagerFactory增加TrustManager,也可以直接实现该接口。
TrustManagerFactory Class
可以自己实现和配置工厂,以提供额外的TrustManager,以提供更复杂的服务或实现特定的身份验证策略。
Creating a TrustManagerFactory
可以这样生成
TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX", "SunJSSE");
如果你使用X509TrustManager接口实现TrustManager,就不需要使用TrustManagerFactory。
也可以这样增加工厂
public void init(KeyStore ks);
public void init(ManagerFactoryParameters spec);
很多工厂,比如SunJSSE的SunX509 TrustManagerFactory,只需要一个参数就可以初始化TrustManagerFactory。
有时候,供应商需要除KeyStore之外的初始化参数。供应商要求开发者传入供应商定义的ManagerFactoryParameters,供应商可以调用ManagerFactoryParameters的其他方法,获取需要的信息。比如,假设TrustManagerFactory供应商需要初始化参数B、R和S。供应商需要程序提供实现ManagerFactoryParameters接口的类的实例。本例中,假设供应商需要实现MyTrustManagerFactoryParams,传给它的第二个init方法,MyTrustManagerFactoryParams看起来可能是这样的:
public interface MyTrustManagerFactoryParams extends ManagerFactoryParameters {
public boolean getBValue();
public float getRValue();
public String getSValue();
}
一些TrustManager作出信任决定,不需要明确地初始化KeyStore或者其他参数。比如,他们能通过LDAP访问信任材料,使用远方的在线证书状态检查服务,或者在本地访问默认的信任材料。
PKIX TrustManager Support
默认的信任管理器算法是PKIX。编辑java.security文件的ssl.TrustManagerFactory.algorithm属性,可以修改。
PKIX信任管理器用的是CertPath PKIX实现。
下面的代码让信任管理器使用LDAP证书存储和打开废止检查(revocation checking)。
import javax.net.ssl.*;
import java.security.cert.*;
import java.security.KeyStore;
import java.io.FileInputStream;
...
// 获取 Keystore 密码
char[] pass = System.console().readPassword("Password: ");
// 增加 PKIX 参数
KeyStore anchors = KeyStore.getInstance("JKS");
anchors.load(new FileInputStream(anchorsFile, pass));
PKIXBuilderParameters pkixParams = new PKIXBuilderParameters(anchors, new X509CertSelector());
// 使用 LDAP 证书存储
LDAPCertStoreParameters lcsp = new LDAPCertStoreParameters("ldap.imc.org", 389);
pkixParams.addCertStore(CertStore.getInstance("LDAP", lcsp));
// 废止检查
pkixParams.setRevocationEnabled(true);
// 给信任管理器PKIX 参数
ManagerFactoryParameters trustParams = new CertPathTrustManagerParameters(pkixParams);
// 增加与PKIX兼容的TrustManagerFactory
TrustManagerFactory factory = TrustManagerFactory.getInstance("PKIX");
// 传给工厂的是CertPath 实现
factory.init(trustParams);
// 使用工厂
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, factory.getTrustManagers(), null);
X509TrustManager Interface
javax.net.ssl.X509TrustManager扩展了TrustManager接口。如果使用基于X.509的认证,必须实现该接口。
想让对端socket通过JSSE支持X.509认证,该接口的实例必须传给一个SSLContext对象。
Creating an X509TrustManager
可以直接实现该接口,也可以使用供应商的TrustManagerFactory。你也可以实现自己的工厂类。
如果不给SunJSSE PKIX或者SunX509 TrustManagerFactory传KeyStore参数,工厂使用下列步骤寻找信任材料:
- 如果定义了javax.net.ssl.trustStore属性,TrustManagerFactory就寻找该文件,该文件就是KeyStore参数。如果还定义了javax.net.ssl.trustStorePassword属性,将在打开文件前,检查数据完整性。如果找不到文件,使用空的keystore增加默认的TrustManager
- 如果没定义javax.net.ssl.trustStore属性,然后
- 如果有java-home/lib/security/jssecacerts,就使用它
- 如果有java-home/lib/security/cacerts,就使用它
- 如果这些文件都不存在,TLS密码套件就是匿名的,你执行认证,不需要truststore。
Creating Your Own X509TrustManager
假设MyX509TrustManager提高了默认的SunJSSE X509TrustManager 行为,在默认的X509TrustManager失败后,使用其他认证逻辑。
可以这样写代码,让SSLContext生成的SocketFactories使用你自己的TrustManager:
TrustManager[] myTMs = new TrustManager[] { new MyX509TrustManager() };
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, myTMs, null);
MyX509TrustManager的代码是这样的:
class MyX509TrustManager implements X509TrustManager {
X509TrustManager pkixTrustManager;
MyX509TrustManager() throws Exception {
// 增加默认的JSSE X509TrustManager.
KeyStore ks = KeyStore.getInstance("JKS");
ks.load(new FileInputStream("trustedCerts"), "passphrase".toCharArray());
TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
tmf.init(ks);
TrustManager tms [] = tmf.getTrustManagers();
/*
* 遍历返回的信任管理器,查找X509TrustManager实例
* 找到以后,把它作为默认的信任管理器
*/
for (int i = 0; i < tms.length; i++) {
if (tms[i] instanceof X509TrustManager) {
pkixTrustManager = (X509TrustManager) tms[i];
return;
}
}
/*
* 找不到就失败
*/
throw new Exception("Couldn't initialize");
}
/*
* 默认的信任管理器
*/
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
try {
pkixTrustManager.checkClientTrusted(chain, authType);
} catch (CertificateException excep) {
// 其他处理,或者重新抛异常
}
}
/*
* 默认的信任管理器
*/
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
try {
pkixTrustManager.checkServerTrusted(chain, authType);
} catch (CertificateException excep) {
/*
* 也许弹出对话框,询问是否信任该证书链
*/
}
}
public X509Certificate[] getAcceptedIssuers() {
return pkixTrustManager.getAcceptedIssuers();
}
}
Updating the Keystore Dynamically
你可以增强MyX509TrustManager,处理动态keystore更新。当checkClientTrusted或者checkServerTrusted测试失败时,不会建立受信任的证书链,
你可以给keystore增加一个需要的受信任的证书。你必须从TrustManagerFactory使用更新的keystore增加新的pkixTrustManager。
当建立新连接(使用以前初始化的SSLContext),将使用新证书做信任决定。
X509ExtendedTrustManager Class
X509ExtendedTrustManager是抽象实现。它增加了连接敏感(connection-sensitive)的信任管理方法。它能在TLS层做端点验证。
TLS 1.2以及后来的版本,客户端和服务器都能指定他们能接受的hash和签名算法。为了做对端认证,认证决定必须基于X509证书和本地接受的hash和签名算法。可以使用ExtendedSSLSession.getLocalSupportedSignatureAlgorithms()获取本地接受的hash和签名算法。
调用SSLSocket.getHandshakeSession()方法或者SSLEngine.getHandshakeSession()方法可以获取ExtendedSSLSession对象。
X509TrustManager接口不是连接敏感的。它不能访问SSLSocket和SSLEngine的session属性。
除了TLS 1.2和以后版本的支持,X509ExtendedTrustManager也支持算法限制和SSL层主机名验证。
Creating an X509ExtendedTrustManager
可以增加自己的X509ExtendedTrustManager子类,或者从供应商的TrustManagerFactory获取一个。
Creating Your Own X509ExtendedTrustManager
import java.io.*;
import java.net.*;
import java.security.*;
import java.security.cert.*;
import javax.net.ssl.*;
public class MyX509ExtendedTrustManager extends X509ExtendedTrustManager {
/*
* 默认的PKIX X509ExtendedTrustManager.
* 如果默认的X509ExtendedTrustManager不信任,就执行回调
*/
X509ExtendedTrustManager pkixTrustManager;
MyX509ExtendedTrustManager() throws Exception {
// 增加默认的JSSE X509ExtendedTrustManager.
KeyStore ks = KeyStore.getInstance("JKS");
ks.load(new FileInputStream("trustedCerts"), "passphrase".toCharArray());
TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
tmf.init(ks);
TrustManager tms [] = tmf.getTrustManagers();
/*
* 遍历返回的信任管理器,查找X509TrustManager实例
* 找到以后,把它作为默认的信任管理器
*/
for (int i = 0; i < tms.length; i++) {
if (tms[i] instanceof X509ExtendedTrustManager) {
pkixTrustManager = (X509ExtendedTrustManager) tms[i];
return;
}
}
/*
* 想办法初始化,或者返回失败
*/
throw new Exception("Couldn't initialize");
}
/*
* 默认的信任管理器
*/
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
try {
pkixTrustManager.checkClientTrusted(chain, authType);
} catch (CertificateException excep) {
// 其他处理,或者重新抛异常
}
}
/*
* 默认的信任管理器
*/
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
try {
pkixTrustManager.checkServerTrusted(chain, authType);
} catch (CertificateException excep) {
/*
* 也许弹出对话框,询问是否信任该证书链
*/
}
}
/*
* 连接敏感的验证.
*/
public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket)
throws CertificateException {
try {
pkixTrustManager.checkClientTrusted(chain, authType, socket);
} catch (CertificateException excep) {
// 其他处理,或者重新抛异常
}
}
public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine)
throws CertificateException {
try {
pkixTrustManager.checkClientTrusted(chain, authType, engine);
} catch (CertificateException excep) {
// 其他处理,或者重新抛异常
}
}
public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket)
throws CertificateException {
try {
pkixTrustManager.checkServerTrusted(chain, authType, socket);
} catch (CertificateException excep) {
// 其他处理,或者重新抛异常
}
}
public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine)
throws CertificateException {
try {
pkixTrustManager.checkServerTrusted(chain, authType, engine);
} catch (CertificateException excep) {
// 其他处理,或者重新抛异常
}
}
public X509Certificate[] getAcceptedIssuers() {
return pkixTrustManager.getAcceptedIssuers();
}
}
KeyManager Interface
KeyManager的主要责任是选择将要送给对方的认证证书。
应该使用一个或者更多的KeyManager初始化SSLContext对象。要为每个认证机制传递一个KeyManager。
如果初始化SSLContext的时候,没有传KeyManager,将生成一个空的KeyManager。
KeyManagerFactory Class
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509", "SunJSSE");
X509KeyManager Interface
如果服务器侧支持 Server Name Indication (SNI) Extension,就可以检查服务器名称并选择适当的密钥。
比如,假设keystore里有三个key条目的证书:
cn=www.example.com
cn=www.example.org
cn=www.example.net
如果ClientHello请求连接www.example.net,服务器应该能选择www.example.net相应的证书。
Relationship Between a TrustManager and a KeyManager
历史上,TrustManager和KeyManager的功能有混淆。
TrustManager决定远方的认证证书应该被信任。
KeyManager决定哪个认证证书应该被送到远方主机。