SSL + Socket NIO 原理 与 SSLEngine + Naga SSLSocket 的使用

  • 什么是SSL
    • SSL记录协议
      • 工作流程
    • SSL握手协议
      • 原理
  • SSLEngine的使用
    • SSLEngine的握手流程
    • SSLEngine的状态标志
  • Naga 库中 SSL 使用
    • 自签名认证证书的申请
    • Naga SSL 使用代码
  • 小结

什么是SSL

SSL(Secure Sockets Layer 安全套接层),及其继任者传输层安全(Transport Layer Security,TLS)是为网络通信提供安全及数据完整性的一种安全协议。TLS与SSL在传输层对网络连接进行加密。

SSL协议位于TCP/IP协议与各种应用层协议之间,为数据通讯提供安全支持。SSL协议可分为两层: SSL记录协议(SSL Record Protocol):它建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。 SSL握手协议(SSL Handshake Protocol):它建立在SSL记录协议之上,用于在实际的数据传输开始前,通讯双方进行身份认证、协商加密算法、交换加密密钥等。[百度百科]

SSL记录协议

SSL纪录协议(Record Protocol)为SSL连提供两种服务。

  1. 保密性:利用握手协议所定义的共享密钥对SSL净荷(Payload)加密。
  2. 完整性:利用握手协议所定义的共享的MAC密钥来生成报文的鉴别码(MAC)。

工作流程

  • 发送方

    1. 从应用层获取要发送的数据(包括各种消息和数据)
    2. 对信息进行分段,分成若干纪录
    3. 使用指定的压缩算法进行数据压缩(Optional)
    4. 使用指定的MAC算法生成MAC
    5. 使用指定的加密算法进行数据加密(RC4)
    6. 添加SSL纪录协议的头,发送数据
  • 接收方

    1. 接收数据并从SSL纪录协议的头中获取相关信息
    2. 使用指定的解密算法解密数据(RC4)
    3. 使用指定的MAC算法校验MAC
    4. 使用压缩算法对数据解压缩(如指定)
    5. 将纪录进行数据重组
    6. 将数据发送给应用层

SSL握手协议

SSL握手协议(Handshake Protocol)提供为SSL提供加密信道的建立服务。

  1. 身份认证:通过证书或者可信公钥文件进行身份认证
  2. 协商密钥:通过信息交换,相互协商一个流加密密钥

原理

SSL在信道加密之前,首先要进行握手通信,协商出流加密密钥,其握手过程如下图:
SSL + Socket NIO 原理 与 SSLEngine + Naga SSLSocket 的使用_第1张图片

在网页浏览过程中,网站服务器为Client端,浏览器为Server端。这是因为此时网站为不可信源,所以需要对不可信源进行验证。

在Android开发过程中,APP为不可信源,所以APP为Client端,服务器为Server端。

从上图可以看出,当建立连接时,客户端主动发起请求,服务器监听后应答,并寻求身份验证,身份验证可通过证书或者服务器所发送的公钥进行识别。客户端发送服务器签发的证书或者公钥信息、随机密码,服务器验证后也会发送给客户端一个随机密码,当双方都有一对随机密码后,通过协商的算法将一对随机密码合成为最终的流加密密钥,到此握手协商完成。

SSLEngine的使用

SSLEngine的握手流程

在实际开发过程中,一般使用 SSLEngine进行SSL信道建立。SSLEngine可以和阻塞、非阻塞的Socket一起工作。

阻塞的Socket一般为SSLServerSocket和SSLSocke,虽然使用方便,但是扩展性和并发性较差。没法满足大并发的业务要求,所以暂且不在继续分析。

非阻塞的Socket一般是与SocketChannel结合使用。SSLEngine在工作时,有以下几个阶段:

  1. 创建:在此阶段,程序可以设置SSLEngine,其中包括:证书或者公钥文件的指定、通过SSLContext生成SSLEngine、设置SSLEngine在握手过程中的角色身份。
  2. 握手协商: SSLEngine根据上一节的握手过程进行握手操作。
  3. 数据传输:握手完成后,SSLEngine通过协商的流密钥进行加解密数据并传输数据。
  4. 重新握手:程序可以在数据传输的任意时间,请求重新协商握手密钥。SSLEngine会重置通信参数,但不能更改客户端/服务器角色模式,设置完成后,会在下一次通信中开始重新握手。
  5. 关闭引擎: 当关闭SSLEngine时,SSLEngine会完成剩余数据的处理工作,并关闭连接。

SSLEngine的状态标志

SSLEngine在握手协商过程中会根据握手数据交互,设置自己的不同状态:NEED_UNWRAP、NEED_WRAP、NEED_TASK、FINISHED、NOT_HANDSHAKING。

  • NEED_UNWRAP: SSLEngine在等待握手数据时,状态为NEED_UNWRAP,表明为解包数据的状态。
  • NEED_TASK:由于SSLEngine为异步处理,所以当进行解包完毕后,采用回调方式触发,这个中间过程SSLEngine状态为NEED_TASK。
  • NEED_WRAP:当解包数据处理完毕后,SSLEngine的状态为NEED_WRAP,这时候是将回告数据打包,等待下一步操作。
  • FINISHED:当握手协商结束时,SSLEngine的状态为FINISHED,SSLEngine准备做随后的一些整理操作。
  • NOT_HANDSHAKING:此时的SSLEngine已经完成握手协商,准备对应用数据进行加解密处理。

Naga 库中 SSL 使用

Naga提供了对SSL的支持,并且通过封装NIOServerSocketSSL、SSLServerSocketChannelResponder、SSLSocketChannelResponder、SSLSocketObserver使SSL与channel完美结合,形成AIO Sokcet + SSL完整的通信加密方案。

自签名认证证书的申请

SSLEngine认证过程中有时需要使用自签名的认证证书及其认证文件,SSLContext可以引入自签名文件并由SSLContext.createSSLEngine()所产生SSLEngine,由于SSLContext自带的默认TrustManager无法验证自签名证书或文件,所以我们需要自定义TrustManager,导入我们的可信证书文件。

注:
1. SSL双向认证时需要生成两对认证证书,也就是执行两次证书生成过程,但是在Android客户端需要对服务器证书进行一下加工才能正常读取。
2. 在Android中公钥文件及证书信息可使用BKS文件存储,BKS文件可以由证书文件*.cer通过bcprov-jdk15on.jar包导出,其下载地址为:http://www.bouncycastle.org/latest_releases.html,创建过程中还需要使用JAVA SDK自带的keytool工具。
3. 对于Cert转JKS/BKS文件,也可以使用UI工具(portecle)进行转换,下载地址:https://sourceforge.net/projects/portecle/

生成证书对过程如下:

  1. 生成keystone文件:

    keytool -genkey -alias YourAliasName -keystore C:\path\keystonname.keystone -validity 365

    这行命令创建一个别名为YourAliasName 的密钥(key), 生成的文件名是 keystonname.keystore. 执行文件生成过程中会要求输入密钥(key)跟keystore的密码。这里需要注意下,当要求你录入Common name 的时候,要填你的主机名。

  2. 生成cer证书文件:

    keytool -export -alias YourAliasName -keystore C:\path\keystonname.keystone -file C:\path\certificationname.cer

    这样命令是将keystone文件导入生成X509证书文件,生成cer文件。

  3. 为Android客户端生成BKS文件(3/4互斥):

    keytool -import -alias YourAliasName -file C:\path\clientcertificationname.cer -keystore C:\path\yourbksfile.bks -storetype BKS -providerClass org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath C:\path\bcprov-jdk15on-XXX.jar

    此命令是将.cer文件转换为BKS文件,BKS是Android客户端引入SSL所必须的公钥文件格式,也就是说BKS文件是cer证书文件的一种变形,给Android客户端Trust keystone使用。

  4. 为Java服务端生成JKS文件(3/4互斥):

    keytool -exportcert -alias YourAliasName -file C:\path\servercertificationname.cer -keystore yourjksfile.jks

    此命令是将.cer文件转换为JKS文件,JKS是JAVA服务器端引入SSL的常用公钥文件格式,给Java服务端 Trust keystone使用。

Naga SSL 使用代码

在上一篇:Socket 与 Android Socket AIO 库 Naga 的介绍中,我已经介绍了Naga库的基本用法,在本节中,我会介绍一下,经过我的源码修改后的Naga库在SSL通信是的使用方法,修改后的源码,我使用方法已经发布在我的Github项目NagaForSSL中的example子目录中,链接为:https://github.com/fy2462/NagaForSSL

客户端使用代码如下:

new Thread() {
    public void run() {
        try {
            NIOService service = new NIOService();
            // 填写keystone密码
            char[] password = "keyston password".toCharArray();
            KeyStore keyStore = KeyStore.getInstance("BKS");
            KeyStore trustStore = KeyStore.getInstance("BKS");
            //加载客户端keystone文件,第一参数为文件路径, 读取client文件私钥client.keytone
            keyStore.load(getResources().openRawResource(R.raw.clientkeystone), password);
            //加载服务器BKS证书文件,第一参数为文件路径,读取server端公钥server.bks
            trustStore.load(getResources().openRawResource(R.raw.ssltrustserver), password);
            //定义证书实体,并加载BKS文件
            TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
            tmf.init(trustStore);
            //生成SSLEngine 
            SSLContext sslContext = SSLContext.getInstance("SSL");
            sslContext.init(null, tmf.getTrustManagers(), null);
            SSLEngine engine = sslContext.createSSLEngine();
            // 使用Naga
            if (null != engine) {
                SSLSocketChannelResponder socket = service.openSSLSocket(engine, SERVICR_HOST, SERVICR_PORT);
                socket.listen(new SSLSocketObserver() {

                    @Override   
                    public void packetSent(NIOSocket socket, Object tag) {
                        System.out.println("Packet sent");
                    }

                    @Override
                    public void connectionOpened(NIOSocket nioSocket) {
                        try {
                            //连接建立后开始发起握手
                            ((NIOSocketSSL) nioSocket).beginHandshake();
                        } catch (SSLException e) {
                            e.printStackTrace();
                        }
                        System.out.println("*Connection opened");
                    }

                    @Override
                    public void connectionBroken(NIOSocket nioSocket, Exception exception) {
                        System.out.println("*Connection broken");
                        if (exception != null)
                            exception.printStackTrace();
                    }

                    @Override
                    public void packetReceived(NIOSocket socket, byte[] packet) {
                        System.out.println("*Unencrypted Packet received " + packet.length);
                        System.out.println(new String(packet));
                    }

                    @Override
                    public void handleFinished(NIOSocket socket) {
                        // 发数据
                        socket.write("hello word -- 11111".getBytes());
                    }
                });
                // 轮询Task并处理
                while (true) {
                    service.selectBlocking();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    };
}.start();

服务端使用代码如下:

public static void main(String... args) {
    try {
        final NIOService service = new NIOService();
        NIOServerSocketSSL socket = service.openSSLServerSocket(getSSLContext(), SERVICR_PORT);
        System.out.println("Server listens on port " + SERVICR_PORT + "... ...");

        socket.listen(new ServerSocketObserverAdapter() {
            @Override
            public void newConnection(final NIOSocket nioSocket) {

                nioSocket.listen(new SSLSocketObserver() {
                    @Override
                    public void packetReceived(NIOSocket socket, byte[] packet) {
                        System.out.println("Received " + socket.getIp() + ": " +new String(packet));

                        socket.write("This is an SSL Server".getBytes());
                        System.out.println("Server sent: " + "This is an SSL Server");
                    }

                    @Override
                    public void connectionBroken(NIOSocket nioSocket, Exception exception) {
                        System.out.println("Client " + nioSocket.getIp()
                                + " disconnected. Exception: " + exception.getMessage());
                    }

                    @Override
                    public void connectionOpened(NIOSocket nioSocket) {
                        try {
                            ((NIOSocketSSL) nioSocket).beginHandshake();
                        } catch (SSLException e) {
                            e.printStackTrace();
                        }

                        System.out.println("Client " + nioSocket.getIp() + " connected.");
                    }

                    @Override
                    public void packetSent(NIOSocket socket, Object tag) {
                        System.out.println("packetSent");

                    }

                    @Override
                    public void handleFinished(NIOSocket socket) {
                        socket.write("This is an SSL Server".getBytes());
                        System.out.println("Server sent: " + "This is an SSL Server");

                    }
                });
            }
        });

        while (true) {
            service.selectBlocking();
        }
    } catch (Exception e) {
        e.printStackTrace(); 
    }
}

private static SSLContext getSSLContext() throws GeneralSecurityException,
        FileNotFoundException, IOException {
    KeyStore ks = KeyStore.getInstance("JKS");
    KeyStore ts = KeyStore.getInstance("JKS");

    char[] passphrase = "testssl".toCharArray();

    // 载入server端私钥
    ks.load(new FileInputStream("ssl/serverkey.keystone"), passphrase);
    // 载入client可信证书,获取客户端公钥
    ts.load(new FileInputStream("ssl/ssltrustclient.jks"), passphrase);

    KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
    kmf.init(ks, passphrase);

    TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
    tmf.init(ts);

    SSLContext sslCtx = SSLContext.getInstance("SSL");

    sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);

    return sslCtx;
}

小结

本篇文章,主要介绍了SSL的概念以及加密通信的原理,并且描述了在日常开发中SSLEngine的使用,以及Naga Socket库在SSL方面的使用方法,由于时间仓促,未免会出现错误,大家可以留言交流、指正。

你可能感兴趣的:(SSL + Socket NIO 原理 与 SSLEngine + Naga SSLSocket 的使用)