java中的ssl连接

Aug 12, 2021


前言

在java中,我们经常需要使用SSL/TLS连接,这种连接是加密连接,https之类的基于TCP/IP协议的应用层协议加密传输,也经常基于这种加密连接,这种连接jdk底层已经提供了默认的实现,并且是基于jdk中的keystroe证书库实现的认证。

在常用的http连接组件(如okhttp,httpcomponent)中,底层连接一般也是通过SocketFactroy建立的连接,通常不需要我们专门设置SocketFactory,使用的是jdk内置的默认实现:

SocketFactory sf = SocketFactory.getDefault();

默认的SocketFactory实现ssl连接时,是基于{JAVA_HOME}/jre/lib/security/cacerts这个默认的证书库的实现,因此当遇到证书库中不存在的证书时,就会出现ssl连接异常的问题。

此时我们通常会通过创建一个忽略认证校验的SocketFactory,并设置为默认实现:

SocketFactory.setDefault(sf);

或者将不信任的证书安装到{JAVA_HOME}/jre/lib/security/cacerts中解决问题。

但是实际上,设置默认值,影响了整个系统的安全性,因为此时意味着所有证书都信任,容易遭到攻击,而将证书安装到证书库中,又改变了标准的jdk,此时相当于应用和jdk绑定了,不便于迁移或者jdk更新。

因此,为了解决上述两个问题,我们应该针对特定的服务使用特定的SocketFactory,这样就可以将忽略证书校验的影响最小化。

SSLContext的体系

java中所有基于TCP/IP协议的SSL/TLS网络编程本质都是基于SSLSocket这个对象,因此这个问题的核心在于如何让组件的代码使用我们定制化的方式创建SSLSocket对象。

常见的网络编程组件都是通过SSLSocketFactory这个接口来创建SSLSocket的,因此要定制网络编程组件创建的SSLSocket,我们只需要给组件提供定制的SSLSocketFactory即可。

以http组件为例,常见的http组件都支持我们设置定制化的SSLSocketFactory对象来代替默认实现。

现在的问题在于,我们如何创建一个定制化的SSLSocketFactory对象?

我们先来看看在jdk中的SSL体系结构:

graph TD SSLSocket-->SSLSocketFactory SSLSocketFactory-->SSLContext SSLContext-->TrustManager SSLContext-->KeyManager TrustManager-->KeyStore KeyManager-->KeyStore

从上图我们可以看出,要创建一个SSLSocketFactory我们需要通过SSLContext这个对象来创建,而SSLContext这个对象又依赖TrustManager和KeyManager这两个对象,这两个对象都依赖KeyStore这个对象。

因此我们需要从KeyStore对象开始创建。

KeyStore

KeyStore对象实际上就是一个证书库,我们前面提到的jdk内置证书库{JAVA_HOME}/jre/lib/security/cacerts加载到内存后,最终就是形成KeyStore对象。

证书库的创建方式有多种:

通过证书库文件创建:

String key="{JAVA_HOME}/jre/lib/security/cacerts";
String password = "123456";
KeyStore keystore=KeyStore.getInstance("JKS");
keystore.load(new FileInputStream(key),password.toCharArray());

在上面的代码中,我们创建了一个使用{JAVA_HOME}/jre/lib/security/cacerts文件作为证书库源的证书库对象。

证书库文件可以通过jdk提供的keytool命令自行生成,这里不展开讲。

随机生成自签名证书库:

String algorithm = "RSA";
String algorithmSign = "MD5WithRSA";
try {
    CertAndKeyGen cak = new CertAndKeyGen(algorithm,algorithmSign);
    cak.generate(1024);
    X500Name subject = new X500Name("CN=localhost,o=netease");
    X509Certificate certificate = cak.getSelfCertificate(subject,10);
    
    KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
    keyStore.load(null);
    
    keyStore.setKeyEntry("local", cak.getPrivateKey(),password.toCharArray(), new Certificate[]{certificate});
    return keyStore;
}catch (Exception e){
    throw new RuntimeException(e);
}

这里我们使用RSA算法创建了一个证书生成器,并且指定随机生成密钥对,长度为1024,接着使用密钥对生成一个证书对象X509Certificate

然后我们通过keyStore.load(null);初始化了一个空的证书库,最后讲我们随机生成的证书设置到证书库中:

keyStore.setKeyEntry("local", cak.getPrivateKey(),password.toCharArray(), new Certificate[]{certificate});

KeyManager

KeyManager是密钥管理器,主要用于验证证书有效性的,密钥管理器对象通常有两种方式创建。

KeyManagerFactory工厂创建:

String password = 123456“;
KeyStore keystore = null; // 省略keystore创建过程,可以参考前面的小结
KeyManagerFactory kmf=KeyManagerFactory.getInstance("SunX509");  
kmf.init(keystore,password.toCharArray());
KeyManager[] kms = kmf.getKeyManagers();

这段代码中,我们获取了一个SunX509的密钥管理器工厂,并使用自己指定的keystore和密码进行初始化,初始化之后使用工厂创建了一个密钥管理器数组。

这个密钥管理器数组即是根据我们的密钥库生成的,支持校验密钥库中的证书的密钥管理器数组。

自己创建一个密钥管理器数组:

KeyManager[] kms = new KeyManager[]{
        new X509KeyManager() {
            @Override
            public String[] getClientAliases(String s, Principal[] principals) {
                return new String[0];
            }

            @Override
            public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
                return null;
            }

            @Override
            public String[] getServerAliases(String s, Principal[] principals) {
                return new String[0];
            }

            @Override
            public String chooseServerAlias(String s, Principal[] principals, Socket socket) {
                return null;
            }

            @Override
            public X509Certificate[] getCertificateChain(String s) {
                return new X509Certificate[0];
            }

            @Override
            public PrivateKey getPrivateKey(String s) {
                return null;
            }
        }
};

这种方式相对少用,因为密钥管理器主要的功能是讲自己的密钥传给连接的对方进行校验,通常我们只需要使用KeyManagerFactory配合KeyStore创建即可直接使用jdk的默认实现,很少有需要自己定制密钥管理器的场景。

TrustManager

TrustManager是校验管理器,一般用于校验连接的对方发来的证书是否有效,通常也会有两种方式创建.

使用TrustManagerFactory创建:

String password = 123456“;
KeyStore keystore = null; // 省略keystore创建过程,可以参考前面的小结
TrustManagerFactory tmf=TrustManagerFactory.getInstance("SunX509");  
tmf.init(keystore);
TrustManager[] tms = tmf.getTrustManagers();

这里我们使用TrustManagerFactory的实例和KeyStore创建了一个认证管理器数组,这个数组可以用于校验连接的另一边发来的所有证书,并且只信任KeyStore中信任的证书。

自行创建一个信任全部证书的认证管理器数据:

TrustManager[] tms = new TrustManager[]{
    X509TrustManager trustManager = new X509ExtendedTrustManager(){
        public X509Certificate[] getAcceptedIssuers(){ return null; }
        public void checkClientTrusted(X509Certificate[] certs, String authType){}
        public void checkServerTrusted(X509Certificate[] certs, String authType){}
        public void checkClientTrusted(X509Certificate[] x509Certificates, String s, Socket socket) throws CertificateException {}
        public void checkServerTrusted(X509Certificate[] x509Certificates, String s, Socket socket) throws CertificateException {}
        public void checkClientTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) throws CertificateException {}
        public void checkServerTrusted(X509Certificate[] x509Certificates, String s, SSLEngine sslEngine) throws CertificateException {}
    }
}

这里我们直接创建了一个X509TrustManager对象作为认证管理器数组的元素,并且这个对象对所有的证书都不进行校验,即表示信任所有的证书。

这个是我们常见的针对自签名证书的信任的实现方式,当然这是不安全的。

SSLContext

终于到了SSLContext对象,前面我们创建的所有对象,最终都是为了构造一个加密传输的上下文环境,这个环境就是SSLContext

现在我们可以轻松的创建一个SSLContext对象:

KeyManager[] kms = null; // 省略创建过程
TrustManager[] tms = null; // 省略创建过程
SecureRandom sr = new SecureRandom();
SSLContext ssl=SSLContext.getInstance("TLS");  
ssl.init(kms,tms,sr);  

ssl对象通过密钥管理器,认证管理器和随机数SecureRandom即可初始化完成,这里SecureRandom也可以为null。

值得注意的是,在这个安全上下文中,其实密钥管理器和认证管理器并不是必须的。

当你的程序作为客户端连接服务端时(如http客户端连接http服务端),如果服务器没有要求认证客户端证书(通常http服务端不会要求认证客户端证书)时,可以不传kms,此时只需要你自己觉得你的程序是否需要认证服务端证书,此时你只需要传tms决定如何认证服务端证书即可:

ssl.init(null,tms,sr);  

当你的程序作为服务端连接客户端时,假设你不要求客户端证书认证(http服务端大多数不会要求认证客户端证书),此时只需要给客户端发送你的证书即可,客户端可能会校验:

ssl.init(kms,null,sr);  

当你的程序和另一个程序连接的过程,双方都要求证书认证时,才需要同时传递kms和tms:

ssl.init(kms,tms,sr);

SSLSocketFactory

现在所有的SSL上下文都构建完成,SSLSocketFactory只需要直接通过SSLContext获取即可:

SSLContext ssl=null;//省略创建过程
SSLSocketFactory sf = ssl.getSocketFactory();

到这里我们就创建了一个完全定制化的SSLSocketFactory,只要传给对应的组件作为socket工厂即可。

HttpsURLConnection

常见的第三方http组件确实可以通过自定义SSLSocketFactory实现指定请求的忽略证书,而非全局忽略证书,但是jdk内置的HttpsURLConnection就比较坑了,不支持针对连接单独制定socket工厂,而是直接使用SSLSocketFactory.getDefault()获取默认的socket工厂,这就导致我们只能通过全局设置忽略证书的方式实现忽略证书校验。

不过这也不是完全无法改变,查看SSLSocketFactory.getDefault()源码可以发现如下代码:

String var0 = getSecurityProperty("ssl.SocketFactory.provider");
if (var0 != null) {
    log("setting up default SSLSocketFactory");

    try {
        Class var1 = null;

        try {
            var1 = Class.forName(var0);
        } catch (ClassNotFoundException var5) {
            ClassLoader var3 = ClassLoader.getSystemClassLoader();
            if (var3 != null) {
                var1 = var3.loadClass(var0);
            }
        }

        log("class " + var0 + " is loaded");
        SSLSocketFactory var2 = (SSLSocketFactory)var1.newInstance();
        log("instantiated an instance of class " + var0);
        theFactory = var2;
        return var2;
    } catch (Exception var6) {
        log("SSLSocketFactory instantiation failed: " + var6.toString());
        theFactory = new DefaultSSLSocketFactory(var6);
        return theFactory;
    }
}

这里看到String var0 = getSecurityProperty("ssl.SocketFactory.provider");提供了一种通过系统属性指定socket工厂实现类的方式,因此我们可以通过ThreadLocal和自定义实现类的方式来使得针对单个连接使用我们的定制socket工厂:

public class CustomSocketFactory implement SSLSocketFactory{

}

//...
ThreadLocal<Boolean> useCustom = new InheritableThreadLocal<>();

HttpsURLConnect conn = null;//省略创建过程
String old = Security.getProperty("ssl.SocketFactory.provider");
try{
    if(useCustom.get()){
        // 指定socket工厂
        Security.setProperty("ssl.SocketFactory.provider", CustomSocketFactory.class.getName());
    }
    conn.connect();
}finally{
    // 恢复socket工厂
    Security.setProperty("ssl.SocketFactory.provider", old);
}
//...

最后

以上就是我对jdk中实现的ssl连接的一个思考和理解,不一定是对的,可能还需要后续继续学习和使用才能理解更加深刻,在本文中所有的类都基于jdk8的实现,没有使用外部依赖,如果使用外部依赖的话,可能实现起来会更加简单。