解决异常Certificates does not conform to algorithm constraints

  这两天线上一批测试服的环境从JDK7升级到JDK8,上线之后突然不能充值了,后台日志报错如下:

java.lang.RuntimeException: javax.net.ssl.SSLHandshakeException: java.security.cert.CertificateException: Certificates does not conform to algorithm constraints

  这个异常字面的意思就是:在进行SSL握手时,所采用的证书算法不符合约束条件,抛了异常。结合堆栈信息,很快定位到代码层面:是我们游戏充值时要向腾讯的服务器发送一个https请求以获取token,然而因为腾讯服务器所采用的ssl证书算法不符合某种约束条件而抛异常了,从而导致充值异常!那到底什么约束条件呢?网上搜索了一下基本都是让你改这个文件:

  $JAVA_HOME/jre/lib/security/java.security
把其中的配置项:
  #jdk.certpath.disabledAlgorithms=MD2, MD5, RSA keySize < 1024, \ DSA keySize < 1024, EC keySize < 224
改成:
  jdk.certpath.disabledAlgorithms=,
或者直接注释掉:
  #jdk.certpath.disabledAlgorithms=MD2, MD5, RSA keySize < 1024, \ DSA keySize < 1024, EC keySize < 224
  这个配置的意思很明了:被禁掉的证书算法:MD2,MD5,key长度小于1024的RSA等,那么把这行注释掉就意味着系统可以接受这些不安全的算法,安全级别被调低了。但是呢,让运维这么操作了之后发现并不生效。还有人说应该把下面这个配置项注释掉:
  jdk.tls.disabledAlgorithms=SSLv3, RC4, MD5withRSA, DH keySize < 768, \ EC keySize < 224
  但是我没有尝试,为什么呢?
  因为这并不是解决问题的正确思路,这么做会有如下缺点:

  a.系统安全级别降低,JDK8对SSL证书的算法安全要求提高很明显是一种更安全的举措
  b.运维同志要把线上所有服务器的JDK配置都改掉,更重要的是如果新开服,买了新机器是不是还得改?太麻烦,需要额外维护

  所以还是要从代码入手,我们的代码是发送一个普通的https请求,不会对服务器的证书进行验证,关键代码如下:

public static String sendSSLGetRequest(){
    //...
    try{
        //...
        SSLContext sc = SSLContext.getInstance("SSL");
        sc.init(null, new TrustManager[] {new TrustAnyTrustManager()}, new SecureRandom());
        //...
    }catch(Exception e){

    }
    //...
    return null;
}

private static class TrustAnyTrustManager implements X509TrustManager {
    //...
    //这里省略了一些必须要实现的用于验证证书的方法,在这里都是空方法
    //...
}

  解决方法是:只要把自己实现的类TrustAnyTrustManager改写成继承新的抽象类:X509ExtendedTrustManager即可,代码如下:

private static class TrustAnyTrustManager extends X509ExtendedTrustManager {
  //...
  //这里省略了一些必须要实现的用于验证证书的方法,在这里都是空方法
  //...    
}

  那为什么这么改就能生效呢?

  首先看官方文档对这个类的功能说明:

In the Java SE 7 release, the X509ExtendedTrustManager class is an abstract implementation of the X509TrustManager interface. It adds methods for connection-sensitive trust management. In addition, it enables endpoint verification at the TLS layer.
...
Besides TLS 1.2 support, the X509ExtendedTrustManager class also support algorithm constraints and SSL layer hostname verification.

  这里的功能说的很含糊,好像跟我们的问题没多大关系啊。其实之所以使用这个类能生效跟他的新功能没有半毛钱关系,这完全是JDK内部的逻辑在作怪。具体地,我们来剖析下JDK的这段源码:

  源码位置:sun.security.ssl.SSLContextImpl

private X509TrustManager chooseTrustManager(TrustManager[] arg0) throws KeyManagementException {
    for (int arg1 = 0; arg0 != null && arg1 < arg0.length; ++arg1) {
        if (arg0[arg1] instanceof X509TrustManager) {
        if (SunJSSE.isFIPS() && !(arg0[arg1] instanceof X509TrustManagerImpl)) {
            throw new KeyManagementException("FIPS mode: only SunJSSE TrustManagers may be used");
        }

        if (arg0[arg1] instanceof X509ExtendedTrustManager) {
            return (X509TrustManager) arg0[arg1];
        }

        return new AbstractTrustManagerWrapper((X509TrustManager) arg0[arg1]);
        }
    }
    return DummyX509TrustManager.INSTANCE;
}

  看懂这段逻辑后发现:如果TrustAnyTrustManager是实现接口X509TrustManager,而不是继承抽象类:X509ExtendedTrustManager,那么JDK则会默认用自己的实现类AbstractTrustManagerWrapper来对服务器的证书算法进行验证,检查她们是否符合java.security里的配置要求!而如果我们的TrustAnyTrustManager继承了抽象类:X509ExtendedTrustManager,则SSLContextImpl就会使用我们自己的TrustAnyTrustManager来验证证书算法,而我们这个类所有的验证方法都是空方法(也就是不验证),那么自然也就不会抛异常了。

  OK,简单总结下解决这个问题的两个方法:

  a.修改文件$JAVA_HOME/jre/lib/security/java.security,修改或注释掉这两项配置:

jdk.certpath.disabledAlgorithms 
jdk.tls.disabledAlgorithms

  b.如果实现了老的接口:X509TrustManager,则改为继承新的抽象类:X509ExtendedTrustManager

  这里顺便记录一个指令:如何查看服务器的SSL证书信息?

openssl s_client -showcerts -connect openapi.tencentyun.com:443

 

参考:
https://stackoverflow.com/questions/14149545/java-security-cert-certificateexception-certificates-does-not-conform-to-algori
java 发送HTTPS请求

posted on 2017-09-12 06:56  bijian1013  阅读(2570)  评论(0编辑  收藏  举报

导航