SSL 双向认证的一个小问题

最近一直在研究 SSL 双向认证,工作中也经常用到。然后今天遇到了一个非常奇怪的问题,那就是就算配置了

ssl_verify_client optional_no_ca

客户端的访问仍然会失败,证书认证仍然不过。调试了半天,发现了两个问题,那就是:
1. optional_no_ca 并不会像 off 一样放过所有请求,而是对于提交了证书的请求,如果证书验证不过就会握手出错
2. 对于下面这个配置项的设置存在错误:

ssl_verify_depth 1

经过查找资料,找到了这个选项相关的一些说明。(注:以下实验均在 Nginx 和 Apache2.2 上同时进行过)

The depth actually is the maximum number of intermediate certificate issuers, i.e. the number of CA certificates which are max allowed to be followed while verifying the client certificate. A depth of 0 means that self-signed client certificates are accepted only, the default depth of 1 means the client certificate can be self-signed or has to be signed by a CA which is directly known to the server (i.e. the CA’s certificate is under SSLCACertificatePath), etc.

也就是说,当 depth 设置为 1(Nginx 的默认值)的时候,服务端只会接受直接被 CA 签发的客户端证书或自签名的证书。
但是!这段话没说的是,当你放在 SSLCACertificatePath 的文件中的 CA 证书只有中级 CA 的时候,验证仍然是会不过的,看后台的报错会发现:(下面这个是 Apache 的日志,Nginx 的只有 “Error (2): unable to get issuer certificate” 一句)

[Wed Feb 24 04:19:39.975324 2016] [ssl:debug] [pid 13380] ssl_engine_kernel.c(1381): [client xx.xx.xx.xx:48806] AH02275: Certificate Verification, depth 1, CRL checking mode: none [subject: CN=IMM_CA,OU=ROOT,O=ROOT,ST=SH,C=CN / issuer: CN=ST,OU=UP,O=ROOT,ST=SH,C=CN / serial: 02 / notbefore: Jan 14 10:31:00 2013 GMT / notafter: Jan 14 10:31:00 2017 GMT]
[Wed Feb 24 04:19:39.975409 2016] [ssl:info] [pid 13380] [client xx.xx.xx.xx:48806] AH02276: Certificate Verification: Error (2): unable to get issuer certificate [subject: CN=IMM_CA,OU=ROOT,O=ROOT,ST=SH,C=CN / issuer: CN=ROOT_CA,OU=ROOT,O=ROOT,ST=SH,C=CN / serial: 02 / notbefore: Jan 14 10:31:00 2013 GMT / notafter: Jan 14 10:31:00 2017 GMT]

也就是说,直接尝试使用中级 CA 来验证客户端是无法通过的,openssl 会自动的去找中级 CA 的签发者一层层验证上去,直到找到根。
所以,就算将 中级 CA 和 根 CA 都放在信任证书列表中,由于最终 depth 为 2 的缘故,验证还是过不了。

因此,在实际使用的时候,需要注意一下两点:
1. CA 文件中必须同时存在 中级 CA 和 根 CA,必须构成完整证书链,不能少任何一个;
2. 默认的验证深度 SslVerifyDepth ssl_verify_depth 是 1,也就是说只要是中级 CA 签发的客户端证书一律无法通过认证,需要增大该值。

同时还要注意 optional_no_ca 的问题,开了这个选项并不是会放过所有的请求,也会要求证书链完整才能通过。关于这一点个人比较奇怪,因为证书链完整了那就算开 require 应该也能通过了才对。。。
关于这一点做了一些尝试发现以下两种情况:
1. 如果 SSLCACertificateFile/ssl_trusted_certificate SSLCACertificatePath 这两个参数都不设置,那么 optional_no_ca 会放过所有有效的客户端证书(满足客户端证书的基本条件即可);
2. 如果 SSLCACertificateFile/ssl_trusted_certificate SSLCACertificatePath 这两个参数设置了,但是客户端提交的证书并不是这里的 CA 签发的,也会验证成功
但是!如果 SSLCACertificateFile/ssl_trusted_certificate SSLCACertificatePath 这两个参数里设置的证书包括了中级 CA 证书,但是没有包括中级 CA 证书的完整证书链,那么就会报 Error (2): unable to get issuer certificate 错误,验证失败。