• 05-HTTPS 秘钥库与证书(Java)


    一、关于 Java 里的证书

    上面所介绍的是浏览器对证书进行验证的过程,浏览器保存了一个常用的 CA 证书列表,在验证证书链的有效性时,直接使用保存的证书里的公钥进行校验,如果在证书列表中没有找到或者找到了但是校验不通过,那么浏览器会警告用户,由用户决定是否继续。与此类似的,操作系统也一样保存有一份可信的证书列表,譬如在 Windows 系统下,你可以运行 certmgr.msc 打开证书管理器查看,这些证书实际上是存储在 Windows 的注册表中,一般情况下位于:\SOFTWARE\Microsoft\SystemCertificates\ 路径下。那么在 Java 程序中是如何验证证书的呢?

    和浏览器和操作系统类似,Java 在 JRE 的安装目录下也保存了一份默认可信的证书列表,这个列表一般是保存在 $JRE/lib/security/cacerts 文件中。要查看这个文件,可以使用类似 KeyStore Explorer 这样的软件,当然也可以使用 JRE 自带的 keytool 工具(后面再介绍),cacerts 文件的默认密码为 changeit (但是我保证,大多数人都不会 change it)。

    我们知道,证书有很多种不同的存储格式,譬如 CA 在发布证书时,常常使用 PEM 格式,这种格式的好处是纯文本,内容是 BASE64 编码的,证书中使用 “-----BEGIN CERTIFICATE-----” 和 “-----END CERTIFICATE-----” 来标识。另外还有比较常用的二进制 DER 格式,在 Windows 平台上较常使用的 PKCS#12 格式等等。当然,不同格式的证书之间是可以相互转换的

    二、Java中不同类型的密钥库(Keystore) 、

    在 Java 平台下,证书常常被存储在 KeyStore 文件中,上面说的 cacerts 文件就是一个 KeyStore 文件,KeyStore 不仅可以存储数字证书,还可以存储密钥,存储在 KeyStore 文件中的对象有三种类型:Certificate、PrivateKey 和 SecretKey 。Certificate 就是证书,PrivateKey 是非对称加密中的私钥,SecretKey 用于对称加密,是对称加密中的密钥。KeyStore 文件根据用途,也有很多种不同的格式:JKS、JCEKS、PKCS12、DKS 等等,PixelsTech 上有一系列文章对 KeyStore 有深入的介绍,可以学习下:Different types of keystore in Java

    • JKS,Java Key Store。可以参见sun.security.provider.JavaKeyStore类,此密钥库是特定于Java平台的,通常具有jks的扩展名。此类型的密钥库可以包含私钥和证书,但不能用于存储密钥。由于它是Java特定的密钥库,因此不能在其他编程语言中使用。存储在JKS中的私钥无法在Java中提取。关于JKS的详细介绍可以参考

    • JCEKS,JCE密钥库(Java Cryptography Extension KeyStore)。可以认为是增强式的JKS密钥库,支持更多算法。可以参考com.sun.crypto.provider.JceKeyStore类,此密钥库具有jceks的扩展名。可以放入JCEKS密钥库的条目是私钥,密钥和证书。此密钥库通过使用Triple DES加密为存储的私钥提供更强大的保护。

       JCEKS的提供者是SunJCE,它是在Java 1.4中引入的。因此,在Java 1.4之前,只能使用JKS。
      
      • 1
    • PKCS12,一种标准的密钥库类型,可以在Java和其他语言中使用。可以参考sun.security.pkcs12.PKCS12KeyStore类。它通常具有p12或pfx的扩展名。可以在此类型上存储私钥,密钥和证书。与JKS不同,PKCS12密钥库上的私钥可以用Java提取。此类型是可以与其他语言(如C,C ++或C#)编写的其他库一起使用。

    目前,Java中的默认密钥库类型是JKS,即如果在使用keytool创建密钥库时未指定-storetype,则密钥库格式将为JKS。但是,默认密钥库类型将在Java 9中更改为PKCS12,因为与JKS相比,它具有增强的兼容性。可以在$ JRE / lib / security / java.security文件中检查默认密钥库类型。

    三、KeyStore 和 TrustStore

    到目前为止,我们所说的 KeyStore 其实只是一种文件格式而已,实际上在 Java 的世界里 KeyStore 文件分成两种:KeyStore 和 TrustStore,这是两个比较容易混淆的概念。
    不过这两个东西从文件格式来看其实是一样的。KeyStore 保存私钥,用来加解密或者为别人做签名TrustStore 保存一些可信任的证书,访问 HTTPS 时对被访问者进行认证,以确保它是可信任的。所以准确来说,上面的 **cacerts 文件应该叫做 TrustStore **而不是 KeyStore,只是它的文件格式是 KeyStore 文件格式罢了。

    除了 KeyStore 和 TrustStore ,Java 里还有两个类 KeyManager 和 TrustManager 与此息息相关。JSSE 的参考手册中有一张示意图,说明了各个类之间的关系:

    ![](https://img-blog.csdnimg.cn/img_convert/b6906a79ad2a8068c87a88cf8d8a3851.webp?x-oss-process=image/format,png#averageHue=#e3dcb7&clientId=u431af540-a2ee-4&crop=0&crop=0&crop=1&crop=1&errorMessage=unknown error&from=paste&id=u00f404d8&margin=[object Object]&originHeight=422&originWidth=581&originalType=url&ratio=1&rotation=0&showTitle=false&status=error&style=none&taskId=ue5fdd832-b618-4fa2-b52f-8f617475a7d&title=)
    可以看出如果要进行 SSL 会话,必须得新建一个 SSLSocket 对象,而 SSLSocket 对象是通过 SSLSocketFactory 来管理的,SSLSocketFactory 对象则依赖于 SSLContext ,SSLContext 对象又依赖于 keyManager、TrustManager 和 SecureRandom。我们这里最关心的是 TrustManager 对象,另外两个暂且忽略,因为正是 TrustManager 负责证书的校验,对网站进行认证,要想在访问 HTTPS 时通过认证,不报 sun.security.validator.ValidatorException 异常,必须从这里开刀。

    四、自定义 TrustManager 绕过证书检查

    我们知道了 TrustManager 是专门负责校验证书的,那么最容易想到的方法应该就是改写 TrustManager 类,让它不要对证书做校验,这种方法虽然粗暴,但是却相当有效,而且 Java 中的 TrustManager 也确实可以被重写,下面是示例代码:

    public static void main(String[] args) throws Exception {
        String url = "https://kyfw.12306.cn/otn/";
    
        // Create a trust manager that does not validate certificate chains
        TrustManager[] trustAllCerts = new TrustManager[] {new X509TrustManager() {
            public X509Certificate[] getAcceptedIssuers() {
                return null;
            }
    
            public void checkClientTrusted(X509Certificate[] certs, String authType) {
                // don't check
            }
    
            public void checkServerTrusted(X509Certificate[] certs, String authType) {
                // don't check
            }
        }};
    
        SSLContext ctx = SSLContext.getInstance("TLS");
        ctx.init(null, trustAllCerts, null);
        LayeredConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(ctx);
        CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslSocketFactory).build();
    
        HttpGet request = new HttpGet(url);
        request.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) ...");
        CloseableHttpResponse response = httpclient.execute(request);
        String content = EntityUtils.toString(response.getEntity(), "UTF-8");
        System.out.println(content);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    五、使用证书

    对于有些证书,我们基本上确定是可以信任的,但是这些证书又不在 Java 的 cacerts 文件中,譬如 12306 网站,或者使用了 Let’s Encrypt 证书的一些网站,对于这些网站,我们可以将其添加到信任列表中,而不是使用上面的方法统统都相信,这样程序的安全性仍然可以得到保障。

    5.1 使用 keytool 导入证书

    简单的做法是将这些网站的证书导入到 cacerts 文件中,这样 Java 程序在校验证书的时候就可以从 cacerts 文件中找到并成功校验这个证书了。上面我们介绍过 JRE 自带的 keytool 这个工具,这个工具小巧而强悍,拥有很多功能。首先我们可以使用它查看 KeyStore 文件,使用下面的命令可以列出 KeyStore 文件中的所有内容(包括证书、私钥等):

    $ keytool -list -keystore cacerts 
    
    • 1

    然后通过下面的命令,将证书导入到 cacerts 文件中:

    $ keytool -import -alias 12306 -keystore cacerts -file 12306.cer 
    
    • 1

    要想将网站的证书导入 cacerts 文件中,首先要获取网站的证书,譬如上面命令中的 12306.cer 文件,它是使用浏览器的证书导出向导保存的。如下图所示:
    ![](https://img-blog.csdnimg.cn/img_convert/19d15f3b2ad344c08c9bd2edd7a24000.webp?x-oss-process=image/format,png#clientId=u431af540-a2ee-4&crop=0&crop=0&crop=1&crop=1&errorMessage=unknown error&from=paste&id=ua94f44cc&margin=[object Object]&originHeight=761&originWidth=1299&originalType=url&ratio=1&rotation=0&showTitle=false&status=error&style=none&taskId=uf98f6132-d8f8-4187-8dd1-cb7abe27e1a&title=)
    关于 keytool 的更多用法,可以参考 keytool 的官网手册,SSLShopper 上也有一篇文章列出了常用的 keytool 命令

    5.2 使用 KeyStore 动态加载证书

    使用 keytool 导入证书,这种方法不仅简单,而且保证了代码的安全性,最关键的是代码不用做任何修改。所以我比较推荐这种方法。但是这种方法有一个致命的缺陷,那就是你需要修改 JRE 目录下的文件,如果你的程序只是在自己的电脑上运行,那倒没什么,可如果你的程序要部署在其他人的电脑上或者公司的服务器上,而你没有权限修改 JRE 目录下的文件,这该怎么办?如果你的程序是一个分布式的程序要部署在成百上千台机器上,难道还得修改每台机器的 JRE 文件吗?好在我们还有另一种通过编程的手段来实现的思路,在代码中动态的加载 KeyStore 文件来完成证书的校验,抱着知其然知其所以然的态度,我们在最后也实践下这种方法。通过编写代码可以更深刻的了解 KeyStore、TrustManagerFactory、SSLContext 以及 SSLSocketFactory 这几个类之间的关系。

    @Test
    public void basicHttpsGetUsingSslSocketFactory() throws Exception {
     
        String keyStoreFile = "D:\\code\\ttt.ks";
        String password = "poiuyt";
        KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
        FileInputStream in = new FileInputStream(keyStoreFile);
        ks.load(in, password.toCharArray());
         
        System.out.println(KeyStore.getDefaultType().toString());
        System.out.println(TrustManagerFactory.getDefaultAlgorithm().toString());
         
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(ks);
        SSLContext ctx = SSLContext.getInstance("TLS");
        ctx.init(null, tmf.getTrustManagers(), null);
         
        LayeredConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(ctx);
         
        String url = "https://ttt.aneasystone.com";
         
        /**
         * Return the page with content:
         *  401 Authorization Required
         */
         
        CloseableHttpClient httpclient = HttpClients.custom()
                .setSSLSocketFactory(sslSocketFactory)
                .build();
         
        HttpGet request = new HttpGet(url);
        request.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) ...");
         
        CloseableHttpResponse response = httpclient.execute(request);
        String responseBody = readResponseBody(response);
        System.out.println(responseBody);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    最后的最后,我们还可以通过下面的属性来指定 trustStore ,这样也不需要编写像上面那样大量繁琐的代码,另外,参考我前面的博客,这些属性还可以通过 JVM 的参数来设置。

    System.setProperty("javax.net.ssl.trustStore", "D:\\code\\ttt.ks");
    System.setProperty("javax.net.ssl.trustStorePassword", "poiuyt");
    
    • 1
    • 2

    六、参考

    更多内容关注微信公众号 ”前后端技术精选“,或者语雀,里面有更多知识:https://www.yuque.com/riverzmm/uu60c9?# 《安全》> 更多内容关注微信公众号 ”前后端技术精选“,或者语雀,里面有更多知识:https://www.yuque.com/riverzmm/uu60c9?# 《安全》> 更多内容关注微信公众号 ”前后端技术精选“,或者语雀,里面有更多知识:https://www.yuque.com/riverzmm/uu60c9?# 《安全》> 更多内容关注微信公众号 ”前后端技术精选“,或者语雀,里面有更多知识:https://www.yuque.com/riverzmm/uu60c9?# 《安全》> 更多内容关注微信公众号 ”前后端技术精选“,或者语雀,里面有更多知识:https://www.yuque.com/riverzmm/uu60c9?# 《安全》> 更多内容关注微信公众号 ”前后端技术精选“,或者语雀,里面有更多知识:https://www.yuque.com/riverzmm/uu60c9?# 《安全》

  • 相关阅读:
    mathlab 数据维度调换函数permute()
    前端vue实现圣杯布局【flex布局、浮动布局】
    C语言assert断言
    “暗蚊”黑产团伙通过国内下载站传播Mac远控木马攻击活动分析
    2_ZYBO FPGA 按键控制蜂鸣器 key_beep=>key_led
    使用HandlerInterceptor 中注入其他service时为null分析及解决
    02.相关术语MVC、MTV、ORM介绍
    关于pytorch的数据处理-数据加载Dataset
    Redis 主从模式
    并发容器介绍(一)
  • 原文地址:https://blog.csdn.net/weixin_40304387/article/details/127910057