Java安全之SSL/TLS


在前面所讲到的一些安全技术手段如:消息摘要、加解密算法、数字签名和数据证书等,一般都不会由开发者直接地去使用,而是经过了一定的封装,甚至形成了某些安全协议,再暴露出一定的接口来供开发者使用。因为直接使用这些安全手段,对开发者的学习成本太高,需要深入了解底层实现才行,而直接使用封装后暴露出来的接口就容易多了。

在这些封装与协议的背后,很多都使用到了SSL/TSL协议,其中最常见的HTTPS就是在HTTP协议的基础上加入了SSL/TLS协议形成的,来保障Web访问安全性。SSL/TLS协议包含两个协议:SSL(Secure Socket Layer,安全套接字层)和TLS(Transport Layer Security,传输层安全)协议。SSL由Netscape公司研发,位于TCP/IP参考模型中的网络传输层,作为网络通讯提供安全及数据完完整性的一种安全协议。TLS是基于SSL协议之上的通用化协议,它同样位于TCP/IP参考模型中的网络传输层,作为SSL协议的继承者,成为下一代网络安全性与数据完整性安全协议。

SSL/TLS协议的具体实现与细节肯定是很复杂的,可以百度一下慢慢了解,下面主要列举一下Java中经常遇见的与SSL/TLS有关的情况:

一、Tomcat中HTTPS协议的配置

"8443" protocol="org.apache.coyote.http11.Http11NioProtocol" SSLEnabled="true"
maxThreads="150" scheme="https" secure="true"
clientAuth="true" sslProtocol="TLS"
keystoreFile="conf/serverKeyStore.jks"
keystorePass="gitblit"
truststoreFile="conf/caKeyStore.p12"
truststorePass="gitblit"
truststoreType="pkcs12"/>

属性解释: port:协议监听端口号 protocol: 协议实现类 maxThreads: 最大线程数 SSLEnabledschemesecuresslProtocol:基本上是固定配置 keystoreFile:服务器KeyStore文件路径 keystorePass:服务器KeyStore密码 clientAuth:是否验证客户端 truststoreFile:服务器信任KeyStore文件路径 truststorePass:服务器信任KeyStore密码 truststoreType:服务器信任KeyStore类型,如不指定,默认为jks

在服务器KeyStore文件中最好只有一个条目,当然条目为KeyEntry类型,因为在该配置中无法配置被用于安全通信的条目别名,如果有多个条目的话,服务器会任意选行一个条目,就会造成所使用条目不确定的情况。服务器信任KeyStore中存储的条目是CertificateEntry类型,正确情况下里面只存储了服务器信任的证书。一般说来这些证书都是有一根证书颁发给客户端使用的,而这个根证书肯定是服务器所有的,所以服务器信任KeyStore中最好只存储一个为客户端颁发证书的根证书,这样只要信任了该根证书,也就会信任该根证书颁发的所有证书,利于添加新的客户端。当然还有一种更笨的做法就是将根证书颁发给客户端使用的证书全部添加到服务器信任KeyStore文件中。如果clientAuth配置为false,即单向认证,只认证服务端而不,而不认证客户端,那么,truststoreFile, truststorePass, truststoreType这几个属性是用不上的,可不配置。

二、SSLSocket中使用

package com.xtayfjpk.security.jsse;

import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.KeyStore;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;

import org.junit.Test;

public class SSLSocketTest {

@Test
public void testRunServer() throws Exception {
//获取SSL上下文
SSLContext context = SSLContext.getInstance("SSL");
String keyStorePassword = "password";
//获取服务端KeyStore
KeyStore serverKeys = getKeyStore("serverKeys", "jks", keyStorePassword);
//获取KeyManagerFactory
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
String privateKeyPassword = "password";
//初始化KeyManagerFactory
keyManagerFactory.init(serverKeys, privateKeyPassword.toCharArray());

//获取TrustManagerFactory
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
String trustStorePassword = "password";
//获取服务端信任KeyStore
KeyStore serverTrustKeys = getKeyStore("serverTrust", "jks", trustStorePassword);
//初始化TrustManagerFactory
trustManagerFactory.init(serverTrustKeys);
//初始化SSL上下文
context.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);

//使用SSL上下文获取SSLServerSocketFactory
SSLServerSocketFactory ssf = (SSLServerSocketFactory) context.getServerSocketFactory();
//使用SSLServerSocketFactory创建出SSLServerSocket,并监听指定端口
SSLServerSocket serverSocket = (SSLServerSocket) ssf.createServerSocket(9999);
//设置需要对客户端进行认证
serverSocket.setNeedClientAuth(true);

while(true) {
try {
//等待客户端连接
SSLSocket socket = (SSLSocket) serverSocket.accept();
InputStream in = socket.getInputStream();

byte[] buf = new byte[1024];
int len = in.read(buf);
System.out.println(new String(buf, 0, len));
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}

}

@Test
public void testRunClient() throws Exception {
SSLContext context = SSLContext.getInstance("SSL");
String keyStorePassword = "password";
KeyStore clientKeys = SimpleSSLServer.getKeyStore("clientKeys", "jks", keyStorePassword);
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
String privateKeyPassword = "xtayfjpk";
keyManagerFactory.init(clientKeys, privateKeyPassword.toCharArray());

TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
String trustStorePassword = "password";
KeyStore serverTrustKeys = getKeyStore("clientTrust", "jks", trustStorePassword);
trustManagerFactory.init(serverTrustKeys);
context.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);

//使用SSL上下文创建SSLSocket
SSLSocketFactory factory = (SSLSocketFactory) context.getSocketFactory();

String host = "127.0.0.1";
//创建SSLSocket
SSLSocket socket = (SSLSocket) factory.createSocket(host, 9999);
//与服务端进行通信
OutputStream outputStream = socket.getOutputStream();
outputStream.write("xtayfjpk".getBytes());
outputStream.flush();
outputStream.close();
socket.close();

}

public static KeyStore getKeyStore(String keyStorePath, String type, String keyStorePassword) throws Exception {
KeyStore keyStore = KeyStore.getInstance(type);
FileInputStream in = new FileInputStream(keyStorePath);
keyStore.load(in, keyStorePassword.toCharArray());
in.close();
return keyStore;
}
}

能过上面的例子能够发现,这和Tomcat的https协议的配置是一致的,因为Tomcat底层肯定也是使用SSLSocket来实现https协议的。SSLServerSocketFactory还有个getDefault方法直接返回一个SSLServerSocketFactory实例,如果使用此方法不需要创建与初始化SSLContext,有关SSL相关的配置设置在系统属性中,设置系统属性的方式有两种:一是在虚拟机启动的时候设置,如下所示:

-Djavax.net.ssl.keyStore=clientKeys
-Djavax.net.ssl.keyStorePassword=password
-Djavax.net.ssl.trustStore=clientTrust
-Djavax.net.ssl.trustStorePassword=password

二是通过System.setProperty设置。

三、HttpsURLConnection中使用

package com.xtayfjpk.security.jsse;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.KeyStore;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;

import org.junit.Test;

public class HttpsUrlConnectionTest {

@Test
public void test() throws Exception {
URL url = new URL("https://localhost:8443/");
HttpsURLConnection connection = HttpsURLConnection.class.cast(url.openConnection());


SSLContext context = SSLContext.getInstance("SSL");
String keyStorePassword = "gitblit";
KeyStore clientKeys = SimpleSSLServer.getKeyStore("D:\\java-app\\apache-tomcat-6.0.35\\conf\\clientKeyStore.p12", "pkcs12", keyStorePassword);
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
String privateKeyPassword = "gitblit";
keyManagerFactory.init(clientKeys, privateKeyPassword.toCharArray());

TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
String trustStorePassword = "gitblit";
KeyStore serverTrustKeys = getKeyStore("D:\\java-app\\apache-tomcat-6.0.35\\conf\\clientTrustStore.jks", "jks", trustStorePassword);
trustManagerFactory.init(serverTrustKeys);
context.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);

//使用SSL上下文创建SSLSocket
SSLSocketFactory factory = (SSLSocketFactory) context.getSocketFactory();
//如果服务端没设置需要认证客户端的话,可以不用设置SSLSocketFactory
connection.setSSLSocketFactory(factory);

connection.setDoInput(true);
connection.setDoOutput(true);
InputStream in = connection.getInputStream();
String line = null;
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
while((line=reader.readLine())!=null) {
System.out.println(line);
}
}

public static KeyStore getKeyStore(String keyStorePath, String type, String keyStorePassword) throws Exception {
KeyStore keyStore = KeyStore.getInstance(type);
FileInputStream in = new FileInputStream(keyStorePath);
keyStore.load(in, keyStorePassword.toCharArray());
in.close();
return keyStore;
}
}

四、使服务端KeyStore支持多条目,并可指定被使用条目别名

在Tomcat中HTTPS协议的配置时说到,服务器KeyStore最好只有一个条目,否则会造成所使用条目不确定的情况。但有时候你可能会想,该KeyStore中存储多个条目,在启动时通过配置条目别名来指定具体的条目,因为Tomcat中没有提供别名配置支持,所以KeyStore中最好还是只有一个条目。但如果是自己写SSLSocket程序,可能通过扩展来支持,如下:

package com.xtayfjpk.security;

import java.net.Socket;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Arrays;

import javax.net.ssl.SSLEngine;
import javax.net.ssl.X509ExtendedKeyManager;
import javax.net.ssl.X509KeyManager;

public class MyAliasedX509ExtendKeyManager extends X509ExtendedKeyManager {
private String keyAlias;
private X509KeyManager keyManager;

public MyAliasedX509ExtendKeyManager(String keyAlias, X509KeyManager keyManager) {
this.keyAlias = keyAlias;
this.keyManager = keyManager;
}


//提供给客户端使用,用于选择客户端Keystore中的一个别名
@Override
public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) {
String alias = keyAlias==null ? keyManager.chooseClientAlias(keyTypes, issuers, socket) : keyAlias;
return alias;
}

//提供给服务端使用,用于选择服务端Keystore中的一个别名
@Override
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
String alias = keyAlias==null ? keyManager.chooseServerAlias(keyType, issuers, socket) : keyAlias;
return alias;
}

@Override
public X509Certificate[] getCertificateChain(String alias) {
return keyManager.getCertificateChain(alias);
}

@Override
public String[] getClientAliases(String keyType, Principal[] issuers) {
return keyManager.getClientAliases(keyType, issuers);
}

@Override
public PrivateKey getPrivateKey(String alias) {
return keyManager.getPrivateKey(alias);
}

@Override
public String[] getServerAliases(String keyType, Principal[] issuers) {
return keyManager.getServerAliases(keyType, issuers);
}

@Override
public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) {
String alias = keyAlias==null ? super.chooseEngineClientAlias(keyType, issuers, engine) : keyAlias;
return alias;
}

@Override
public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) {
String alias = keyAlias==null ? super.chooseEngineServerAlias(keyType, issuers, engine) : keyAlias;
return alias;
}
}

通过继承X509ExtendedKeyManager,自己实现一个KeyManager,别名通过构造方法传入,然后使用自己的KeyManager实现类包装KeyManagerFactory创建的KeyManager即可通过别名达到指定KeyStore中被使用条目的目的。

------------------ END ---------------------
及时获取更多精彩文章,请扫码关注如下公众号《云原生之家》: