在使用 TLS 协议加密通讯时,CA 证书文件、服务端和客户端的证书文件和私钥文件如何配置是最基本的,但对证书吊销检查的配置则文档甚少,甚至许多程序并不支持,下面总结下在 Java 中如何做 X.509 证书的吊销检查。

PKI

Public Key Infrastructure(公钥基础设施) 指一套生成和管理 CA 公钥/私钥/证书、服务端和客户端公钥、私钥、签名请求、证书的软件和服务,狭义来说,指一套证书管理软件,知名的有四套开源实现:

  1. https://github.com/OpenVPN/easy-rsa OpenVPN 项目对 OpenSSL 命令行包装的一个脚本,使用非常方便
  2. https://github.com/cloudflare/cfssl Cloudflare 公司使用 Go 编写的一套工具
  3. https://www.vaultproject.io/ Hashicorp 公司打造的敏感信息托管服务,包含了 X.509 证书管理能力
  4. JDK 自带的 KeyTool 工具

CRL 和 OCSP

Certification Revocation List(证书吊销列表)很容易理解,它是 CA 签发的一个文件,包含了被吊销的证书的序列号,吊销时间等,TLS 服务端或者客户端用这个文件来检查一个证书是否吊销了。

$ openssl crl -in pki/crl.pem -noout -text
Certificate Revocation List (CRL):
        Version 2 (0x1)
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: /CN=XXX CA
        Last Update: Aug 15 09:22:59 2019 GMT
        Next Update: Feb 11 09:22:59 2020 GMT
        CRL extensions:
            X509v3 Authority Key Identifier: 
                keyid:F6:B4:78:B4:14:21:BA:7D:58:D8:DA:73:62:60:85:0B:C9:24:80:94
                DirName:/CN=XXX CA
                serial:D8:C4:F0:05:BD:56:59:20

Revoked Certificates:
    Serial Number: 62BC93FB20D968DE659834627CF817BB
        Revocation Date: Aug 15 09:21:59 2019 GMT
        CRL entry extensions:
            X509v3 CRL Reason Code: 
                Key Compromise
    Signature Algorithm: sha256WithRSAEncryption
         c3:65:a7:7a:52:3b:9c:45:71:3a:67:12:6f:d2:53:5a:29:d4:
         9c:cf:6e:87:e9:de:3e:6a:05:5b:b0:d1:e0:75:00:3c:af:90:
         ......

Online Certificate Status Protocol(在线证书状态协议)也很容易理解,它是一个网络协议,TLS 服务端或者客户端请求这个服务来检查一个证书是否吊销了,由于不用下载 CRL 文件了,所以轻量高效。

KeyStore 和 CertStore

Java 标准类库里有两个类用来加载证书、私钥、CRL:

  1. KeyStore: 存取 X.509 证书和私钥,JDK 1.2 引入。在 Java 的 TLS 实现中引入了两个概念,trust store 和 key store,前者只包含证书,后者可以包含证书以及私钥,两个概念都是用的 KeyStore 类,都可以用 keytool 命令行工具操作。Java 的 TLS 实现包含了几个 system properties 可以自定义全局的 trust store 和 key store,当然,代码里也可以显式的设置。
    • TrustStoreManager,按优先级从高到低,使用 javax.net.ssl.trustStore 系统属性指定的 keystore 文件、JSSE 带的 jssecacerts 文件、JDK 带的 cacerts 文件
    • SSLContextImpl, 使用 javax.net.ssl.keyStore 系统属性指定的 keystore 文件
  2. CertStore: 存取 X.509 证书和CRL,JDK 1.4 引入。没有现成的命令行工具可以操作。

非常可惜的是,JDK 搞了两个类来加载这三种东西,而要命的是,CertStore 并没有 javax.net.ssl.certStore 系统属性来指定 CRL 文件路径,不知何故,还好 X509 证书提供了两个扩展,JDK 自己也提供了一个救急办法,可以不修改代码就用上证书吊销检查。

blacklisted.certs

在 JDK 安装目录里有个 lib/security/blacklisted.certs 文件,源文件来自 blacklisted.certs.pem,这个文件的意义很直接,就不赘述了。

X.509 证书扩展:CRL Distribution Points

在生成 X.509 证书时,可以添加这个扩展信息,其值有两种,一种是 LDAP 类型,指向一个 LDAP 服务器地址,由 LDAPCertStore 类加载,另一种是 URI 类型,指向一个通用的 uri,由 URICertStore 类加载,支持 HTTP/FTP/LDAP/FILE 协议。URICertStore 实现了个简单的缓存,30s 内不会重复下载 CRL 文件,30s 之后会使用 HTTP 头部 “If-Modified-Since” 避免不必要的下载,所以还是很高效的。

首先,生成包含 crlDistributionPoints 扩展的证书:

$ easyrsa gen-crl
$ cp pki/crl.pem /etc/kafka/kafka.crl

# 多个扩展之间要换行,所以这里用了 echo 命令输出,多条 echo 可以插入多个扩展
$ export EASYRSA_EXTRA_EXTS=`echo "crlDistributionPoints = URI:file:/etc/kafka/kafka.crl"`
$ easyrsa build-client-full some-user

然后,开启 Java 的证书吊销检查:

# -Djava.security.debug=certpath 开启证书检查调试日志
# -Dcom.sun.security.enableCRLDP=true 开启 crlDistributionPoints 扩展支持
# -Dcom.sun.net.ssl.checkRevocation=true 开启证书吊销检查
$ export KAFKA_OPTS="-Dcom.sun.security.enableCRLDP=true \
 -Dcom.sun.net.ssl.checkRevocation=true"

$ bin/zookeeper-server-start.sh -daemon config/zookeeper.properties

# 需要配置如下参数:
# listeners=SSL://:9092
# advertised.listeners=SSL://...hostname...:9092
# ssl.client.auth=required
# security.inter.broker.protocol=SSL
# ssl.truststore.location=/path/to/truststore.jks
# ssl.truststore.password=xxxx
# ssl.keystore.location=/path/to/keystore.jks
# ssl.keystore.password=xxx
# ssl.key.password=xxx
$ bin/kafka-server-start.sh -daemon config/server.properties

X.509 证书扩展:authorityInfoAccess

这个扩展支持两种信息,一种是上级 CA 证书信息,一种是 OCSP 服务器地址信息,跟 crlDistributionPoints 用法类似:

$ export EASYRSA_EXTRA_EXTS="authorityInfoAccess = OCSP;URI:http://ocsp.example.com"
$ easyrsa build-client-full some-user

$ cat > /path/to/security.properties <<EOF
ocsp.enable=true
# 如果证书没有 OCSP 信息,则可以显式指定
# ocsp.responderURL=http://ocsp.example.com
EOF

$ export KAFKA_OPTS="-Dcom.sun.security.enableCRLDP=true \
 -Dcom.sun.net.ssl.checkRevocation=true \
 -Djava.security.properties=/path/to/security.properties"

总结

虽然 crlDistributionPoints 和 authorityInfoAccess 扩展可以使得不需要修改代码就可以支持证书吊销检查,但是要求制作证书时记得添加这些扩展,而修改 lib/security/blacklisted.certs 往往需要管理员权限并且不支持程序运行时动态更新,所以正经做法还是需要程序显式支持 CRL 文件加载,可惜的是支持指定 CRL path 的 Java 程序非常少!🤦‍♂️

参考

  1. Java PKI Programmer’s Guide - Basic Certification Path Classes
  2. Java PKI Programmer’s Guide - OCSP Support
  3. Fixing Certification Revocation