Java Secure Socket Extension (JSSE) -类和接口篇

12 篇文章 0 订阅

介绍篇

为了 通信安全,两侧的连接必须是SSL-enabled。JSSE API里,连接的endpoint类是SSLSocket和SSLEngine。

JSSE Classes Used to Create SSLSocket and 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。
下图表明了程序的数据流向。

Flow of Data Through SSLEngine

程序,显示在左边,提供数据,放入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属性被用来决定接下来做什么操作。

State Machine during TLS Handshake

握手完成以后,再调用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

算法或协议
KeyStorePKCS12
KeyManagerFactoryPKIX, SunX509
TrustManagerFactoryPKIX (X509 or SunPKIX), SunX509
SSLContextSSLv3, 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决定哪个认证证书应该被送到远方主机。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值