【ssl认证、证书】java中的ssl语法API说明(SSLContext)、与keytool 工具的联系


相关文章:
//-----------Java SSL begin----------------------
【ssl认证、证书】SSL双向认证和SSL单向认证的区别(示意图)
【ssl认证、证书】java中的ssl语法API说明(SSLContext)、与keytool 工具的联系
【ssl认证、证书】SSL双向认证java实战、keytool创建证书
【ssl认证、证书】Wireshark抓包分析
【ssl认证、证书】 查看keystore文件内容
//------------Java SSL end--------------------------

//-----------下面的是CA证书和openssl相关的知识--------------
【ssl认证、证书】TLS/SSL双向认证概念、openssl genrsa示例
【ssl认证、证书】openssl genrsa 命令详解
【ssl认证、证书】SSL 证书基本概念、证书格式、openssl和keytool的区别

1. 前言

在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连接异常的问题。

这个默认的已信任证书库如何使用,参见下文的<2.3.1.1 使用默认的证书库>章节

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

SocketFactory.setDefault(sf);

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

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

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

java中的ssl语法与keytool 工具的联系

标准的SSL协议在Java的落地,具体来说是用通过后面提到的SSLContext实现的,并且必须按照JKS (Java KeyStone)格式提供证书,而keytool 正是生成JKS格式文件的工具,即使你用openssl工具创建了证书,也需要先利用相应的工具,将其转化为JKS格式,然后才能在Java中使用。

2. SSLContext的体系

结合 【ssl认证、证书】SSL双向认证java实现、keytool创建证书 中的示例来看

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

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

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

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

我们先来看看在jdk中的SSL体系结构:
在这里插入图片描述

  • ① 通信核心类——SSLSocket和SSLServerSocket。对于使用过socket进行通信开发的朋友比较好理解,它们对应的就是Socket与ServerSocket,只是表示实现了SSL协议的Socket和ServerSocket,同时它们也是Socket与ServerSocket的子类。SSLSocket负责的事情包括设置加密套件、管理SSL会话、处理握手结束时间、设置客户端模式或服务器模式。

  • ② 客户端与服务器端Socket工厂——SSLSocketFactory和SSLServerSocketFactory。在设计模式中工厂模式是专门用于生产出需要的实例,这里也是把SSLSocket、SSLServerSocket对象创建的工作交给这两个工厂类。

  • ③ SSL会话——SSLSession。安全通信握手过程需要一个会话,为了提高通信的效率,SSL协议允许多个SSLSocket共享同一个SSL会话,在同一个会话中,只有第一个打开的SSLSocket需要进行SSL握手,负责生成密钥及交换密钥,其余SSLSocket都共享密钥信息。

  • ④ SSL上下文——SSLContext。它是对整个SSL/TLS协议的封装,表示了安全套接字协议的实现。主要负责设置安全通信过程中的各种信息,例如跟证书相关的信息。并且负责构建SSLSocketFactory、SSLServerSocketFactory和SSLEngine等工厂类。

  • ⑤ SSL非阻塞引擎——SSLEngine。假如你要进行NIO通信,那么将使用这个类,它让通过过程支持非阻塞的安全通信。

  • ⑥ 密钥管理器——KeyManager。此接口负责选择用于证实自己身份的安全证书,发给通信另一方。KeyManager对象由KeyManagerFactory工厂类生成。

  • ⑦ 信任管理器——TrustManager。此接口负责判断决定是否信任对方的安全证书,TrustManager对象由TrustManagerFactory工厂类生成。

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

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

2.1 KeyStore

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

KeyStore 是一个仓库,但是后文会提到KeyManager(入参是一个包含本端私钥+本端证书的KeyStore)和TrustManager(入参是一个包含对端证书的KeyStore),也就是说根据不同的协议,KeyStore 会实际存储不同的内容

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

2.1.1 通过证书库文件创建:

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命令自行生成,这里不展开讲。

2.1.2 随机生成自签名证书库

随机生成的证书没有存储到实际物理目录,直接被导入到keyStore中:

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});

2.2 KeyManager

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

这里需要强调一点,KeyManager的入参是一个包含本端私钥+本端证书的KeyStore,当然,由于访问keyStone需要密码,因此,也是比较安全的;而
TrustManager的入参是一个仅包含对端证书的KeyStore

2.2.1 KeyManagerFactory工厂创建:

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

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

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

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

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

下面是 X509KeyManager接口的匿名抽象类实现,在此,你可以知道有那些抽象方法:

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;
            }
        }
};

2.3 TrustManager

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

TrustManager保存服务端的授权证书的副本,功能类似白名单,用于判断对端传送过来的证书是否合法,只有在tks的证书判定有效,否则直接拒绝连接。

2.3.1 使用TrustManagerFactory创建:

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

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

2.3.1.1 使用默认的证书库

tmf.init(null);时,会自动使用 {JAVA_HOME}/jre/lib/security/cacerts 作为信任证书库

public javax.net.ssl.TrustManagerFactory  {
    
    
 public final void init(KeyStore ks)  throws KeyStoreException{
    
    
 factorySpi.engineInit(ks);  //跟进去
}

实现类 TrustManagerFactoryImpl:

public   sun.security.ssl.TrustManagerFactoryImpl  {
    
    
    protected void engineInit(KeyStore ks) throws KeyStoreException{
    
    
     if (ks == null) {
    
    
            try {
    
    
                trustManager = getInstance(TrustStoreManager.getTrustedCerts());   //跟进去TrustStoreManager.getTrustedCerts()
                ......
}
public     TrustStoreManager  {
    
    
	public static Set<X509Certificate> getTrustedCerts() throws Exception {
    
    
        return tam.getTrustedCerts(TrustStoreDescriptor.createInstance());  //TrustStoreDescriptor.createInstance()
    }
public  TrustStoreDescriptor   {
    
    

    private static final String defaultStore =
                defaultStorePath + fileSep + "cacerts";     //{JAVA_HOME}/jre/lib/security/cacerts

   static TrustStoreDescriptor createInstance() {
    
    
             return AccessController.doPrivileged(
                    new PrivilegedAction<TrustStoreDescriptor>() {
    
    

                @Override
                public TrustStoreDescriptor run() {
    
    
                    // Get the system properties for trust store.
                    String storePropName = System.getProperty(
                            "javax.net.ssl.trustStore", jsseDefaultStore);
                    String storePropType = System.getProperty(
                            "javax.net.ssl.trustStoreType",
                            KeyStore.getDefaultType());
                    String storePropProvider = System.getProperty(
                            "javax.net.ssl.trustStoreProvider", "");
                    String storePropPassword = System.getProperty(
                            "javax.net.ssl.trustStorePassword", "");     
   。。。。
    if (!"NONE".equals(storePropName)) {
    
    
                        String[] fileNames =
                                new String[] {
    
    storePropName, defaultStore};               //defaultStore是前文定义的变量                

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

通过下面的X509ExtendedTrustManager接口的匿名抽象类,我们可以知道有抽象方法用于对端证书的校验:

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对象作为认证管理器数组的元素,并且这个对象对所有的证书都不进行校验(没抛异常,即认为校验通过),即表示信任所有的证书。

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

2.4 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);

2.5 SSLSocketFactory

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

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

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

2.6 HttpsURLConnection

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

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


  public static synchronized ServerSocketFactory 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 () :

//...
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);  //貌似也不对,万一是并发的请求,
														       //那么修改该环境变量的,没有恢复的话,会影响其他的线程读取该环境变量
}
//...

在本文中所有的类都基于jdk8的实现,没有使用外部依赖,如果使用外部依赖的话,可能实现起来会更加简单。

参考

java中的ssl连接
Java SSL实现使用详解
SSL/TLS通信及其在Java中的实践

猜你喜欢

转载自blog.csdn.net/m0_45406092/article/details/129959801