一不小心又到中秋节了!昨天上午依旧十分忙碌的工作,完全没想起再过一天就是节假日,直到下班前才突然醒悟~
这几天项目中的web服务应客户的需求要加入https相关的机能,因为组里面只有我一个人或多或少接触了一点点https的东西,所以老大就把这个任务全权交给我一个人来处理了。过程十分的艰辛,说实话里面涉及到东西实在太多,openssl,keytool,java版本限制,tomcat限制,加密套件,DHparameter等等,每个东西都能把我折磨的半死。眼下有了一些眉目,正好趁这个假日好好整理一下,学着费劲,所以可能就格外珍惜吧。

所有的内容分为以下几点吧:

  1. 密钥证书相关
  2. https服务搭建
  3. 加密套件选取

1. 密钥证书相关

密钥和证书的制作其实并不算困难,openssl和keytool工具都可以用来制作,不过我一般都是用openssl来创建的,个人感觉openssl功能更强大一点,参数也更为复杂。
使用openssl制作证书密钥可以参考以前写过的一片博文:openssl密钥证书相关操作,不过这里面只是讲述了一下制作密钥证书的一个简单流程,其实在实际开发过程中,会更具客户的需求,为密钥和证书制定大量的参数设置,比如在我这个项目中涉及到了通配符证书,多域名证书的创建。在这个过程中比较重要的是修改在创建证书过程中使用到的那个配置文件(不指定的话使用默认的/etc/pki/tls/openssl.cnf),里面涉及到了很多密钥和证书的各个属性,需要按照需求更改。推荐的做法是拷贝一份出来并修改,而不要在默认的配置文件上做改动。还有一点要注意的是:测试的时候可以使用自签署的证书,但是一般客户会给你一份他们花钱购买的证书给你,最好还是用那个来做最终的测试。

2. https服务搭建

由于我们项目中使用的是tomcat7.0,所以从http到https的变更还是比较简单的,具体的做法可以参照之前写的两篇博文:
1.非APR方式实现tomcat https
2.APR方式实现tomcat https
这两种方式在性能上比较好的应该是用APR的那种方式,但是安装部署起来比较困难。需要注意的是两种方式使用的密钥格式不一样,如果用openssl工具制作好了密钥和证书,可能需要将其转换成jks的格式(非APR)。
转换方法如下:

#先用密钥和证书创建p12证书
openssl pkcs12 -export -in mycert.crt -inkey mykey.key -out mycert.p12 -name tomcat -CAfile myCA.crt  -caname root -chain
# 将p12证书转换成jks文件
keytool -importkeystore -v -srckeystore mycert.p12 -srcstoretype pkcs12 -srcstorepass 123456 -destkeystore tomcat_https.keystore -deststoretype jks -deststorepass 123456 

这里有一点需要特别注意,我在这上面吃过亏,如果生成的jks文件要被tomcat正确使用的话,这里p12设置的保护口令和jks设置的保护口令必须是一致的。否则tomcat无法启动成功,在log中会发现如下错误:

java.security.UnrecoverableKeyException: Cannot recover key

这个问题相信大部分人也能自己解决。

到这里还有一点想要补充的,就是客户需求在配置文件中所有涉及到密码的内容都不允许明文显示(要密文显示),所以上面用非APR方式配置的话,Connector中的keystorePass就不能直接写上去了,这边提供一个我使用的方法,重写protocol属性指定的java类,将org.apache.coyote.http11.Http11Protocol这个类修改为自己写的子类(包名类名都需要修改)。
如下所以:

package com.test.coyote.http11;

import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.apache.coyote.http11.Http11Protocol;

public class MyHttp11Protocol extends Http11Protocol {

    @Override
    public void init() throws Exception {
        final String passFromServerXml = getKeystorePass();
        final String password = decipher(passFromServerXml);
        setKeystorePass(password);
        super.init();
    }

    private String decipher(final String passFromServerXml) throws Exception {
        if (passFromServerXml == null) {
            return null;
        }
        return decrypt(passFromServerXml);
    }

    public static String decrypt(String content) throws Exception {
        KeyGenerator kgen = KeyGenerator.getInstance("AES");
        SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
        secureRandom.setSeed("for_test".getBytes());
        kgen.init(128, secureRandom);
        SecretKey secretKey = kgen.generateKey();
        byte[] enCodeFormat = secretKey.getEncoded();
        SecretKeySpec key = new SecretKeySpec(enCodeFormat, "AES");
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.DECRYPT_MODE, key);
        return new String(cipher.doFinal(parseHexStr2Byte(content)));
    }

    private static byte[] parseHexStr2Byte(String hexStr) {
        if (hexStr.length() < 1)
            return null;
        byte[] result = new byte[hexStr.length() / 2];
        for (int i = 0; i < hexStr.length() / 2; i++) {
            int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16);
            int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2),
                    16);
            result[i] = (byte) (high * 16 + low);
        }
        return result;
    }
}
# end

在该类中对server.xml中获取到的keystorePass进行AES解密后使用(当然你也可以使用其他解密方式),这样的话我们就能在keystorePass上填写我们加密后的字符串了(与加密方式向对应即可)。

3. 加密套件选取

在tomcat中指定加密套件只需要在server.xml中设置ciphers属性就行了,多个加密套件用逗号隔开,也可以用表达式。缺省情况下,可以使用任何可用的加密套件。

这里有必要给大家解释一下什么是加密套件,按照赵春平著的《openssl编程》中的说法:一个加密套件指明了 SSL 握手阶段和通信阶段所应该采用的各种算法。这些算法包括:认证算法、密钥交换算法、对称算法和摘要算法等。
举个例子:TLS_DHE_RSA_WITH_AES_128_CBC_SHA,这就是一个加密套件的名称。TLS是命名的开头,DHE_RSA是密钥交换算法,后面跟WITH,AES_128_CBC是加密算法,SHA是散列算法。
服务器和客户端各自都会有一个自己支持的加密套件列表,如果服务端只设置了一种加密套件,那么客户端要么接受要么返回错误。加密套件的选择是由服务端做出的。

在这里其实还涉及到一个内容,就是Diffie-Hellman Parameters,简称DH Parameters。如果加密套件中有使用到DHE的话,就需要设定这个参数,一般来讲DH密钥需要在1024位以上才能保障安全,在使用测试工具测试我们的https服务的时候发现DH key的长度只有768,翻阅了大量文档想修改DH parameter,增加长度。但是调查后了解到java不能通过指定DH密钥参数来设定,他只能使用自身内部的参数,而且JAVA7的HD KEY size为768,无法更改。无奈只好更改jdk版本为1.8,使用java8。根据资料,JAVA8支持两种长度的DH Key size,分别为1024位和2048位。然后修改jvm启动参数来更改DH key size:

-Djdk.tls.ephemeralDHKeySize=2048

加到tomcat的catlina.sh脚本中即可

最后推荐两个测试工具:

  1. testssl.sh
  2. cipherscan
  3. openssl s_client命令

【参考文档:】
Guide to Deploying Diffie-Hellman for TLS
Logjam, FREAK and Upcoming Changes in OpenSSL