问题来了
昨天,我还在我的工位上愉快的敲的代码,有位开发组的同事Z给我发消息。
开发组同事Z:大哥,这个PKI的登录功能是你做的吗?
我:是呀!N年前的事了。。。
开发组同事Z:PKI登录功能出问题了!有位客户使用他的PKI登录我们做的系统,登录时报错,换了几台电脑也不行。但是他使用他的PKI能登录其他的系统。能帮忙看一下吗,我找过好多同事看过了,他们看了下,都不知道如何入手
我:报错是报什么错?能具体的描述下吗?
开发组同事Z:访问系统时,能弹出证书的选择框,选择完证书后,Chrome浏览器的报错信息是:ERR_SSL_PROTOCOL_ERROR
我:行吧,我待会过去瞧瞧
问题分析
之前在配置tomcat的SSL时,我已经把客户证书的根证书已经导入到服务器信任证书列表内(配置见附录),“能弹出证书的选择框”说明服务器端是能识别出客户证书,但是为什么在登录的过程中,就报错了,可能有如下的几种原因:
- 服务器端的证书和服务器端的信任证书配置有误;(我一直以为是服务器端证书中的IP地址和服务器的IP不一致,后来才发现这个是不可能的)
- 登录过程中,tomcat能读取到客户端的证书,在程序处理的过程中,拒绝该证书;(客户端的证书的格式不被服务器端接受)
- 客户端证书过期了?(通过上面的描述好像不是这个错误)
- tomcat或者JDK有问题?(这个不太可能,其他用户使用PKI能登录呀)
解决问题
博主当时也是有点懵逼,不知道该怎么解决这个问题。好在博主有敢于尝试的精神,决定一个一个试一下。
重新安装服务器端证书
首先使用keytool
工具生成服务器端证书,再使用keytool
导入客户的根证书到服务器端的信任证书库中。然后把服务器端证书和服务器端的信任证书库放入tomcat,配置、重启tomcat(这里省略了具体的配置过程,具体的配置步骤见附录)。经过一番尝试,发现选择完证书还是报错。又经过一番折腾,还是没有找过是啥原因,正在灰心丧气的时候,灵机一动,程序员不是应该从日志中找错误吗?于是从网上找了一下java中如何开启SSL的日志信息。
日志带来曙光
在tomcat中加入JVM参数: -Djavax.net.debug=SSL,handshake,data,trustmanager
,重启tomcat。使用客户的证书重新登录,看到后台打印出了很多的日志信息,浏览了一遍后,发现了如下的异常:
http-nio-8443-exec-4, fatal error: 46: General SSLEngine problem
sun.security.validator.ValidatorException: PKIX path validation failed: java.security.cert.CertPathValidatorException: algorithm constraints check failed
%% Invalidated: [Session-7, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256]
http-nio-8443-exec-4, SEND TLSv1.2 ALERT: fatal, description = certificate_unknown
http-nio-8443-exec-4, WRITE: TLSv1.2 Alert, length = 2
http-nio-8443-exec-4, fatal: engine already closed. Rethrowing javax.net.ssl.SSLHandshakeException: General SSLEngine problem
http-nio-8443-exec-4, called closeOutbound()
http-nio-8443-exec-4, closeOutboundInternal()
拿着这个异常信息java.security.cert.CertPathValidatorException: algorithm constraints check failed
去google上搜索了一下,找到了如下的这两个网站上的解决方法:
- https://*.com/questions/21218217/ssl-handshake-exception-algorithm-constraints-check-failed-md5withrsa
- https://developer.ibm.com/answers/questions/366684/why-am-i-getting-algorithm-constraints-check-faile/
于是乎,按照上面的描述修改了JDK的配置信息,重启tomcat,使用证书登录,居然能正常登录了。心中暗自高兴了好一会儿,满满的成就感,想不到居然修改了一点配置就解决了......在高兴之余,想着搞清楚这到底是怎么一回事!接下来,容我慢慢道来。
刨根到底
到底是做了什么神奇的操作,就解决这问题了呢!首先找到JDK安装目录下的这个文件:
$JAVA_HOME/jre/lib/security/java.security
然后修改其中的两项配置(为了确保不出报错,把这两项禁用的算法全置为空):
# 处理证书路径时禁用的算法
jdk.certpath.disabledAlgorithms=
# 处理SSL/TLS时禁用的算法
jdk.tls.disabledAlgorithms=
然后重启tomcat就行了。
先看一下禁用的算法的语法,如下:
# 总体的语法
禁用的算法1, 禁用的算法2, 禁用的算法3...
# 禁用的每一项算法语法
算法名称 keySize 操作符 数值值
例如:DSA
表示禁用DSA算法,RSA keySize < 2048
表示禁用密钥长度小于2048的RSA算法,RSA keySize > 1024, RSA keySize < 2048
表示禁用密钥长度大于1024小于2048的RSA算法。
后来,仔细看了一下日志,发现在客户证书的信息有这么一行:
Signature Algorithm: SHA1withRSA, OID = 1.2.***.11***9.1.1.*
Key: Sun RSA public key, 1023 bits
客户证书的密钥长度居然是1023,而JDK8中为了安全性默认禁用了密钥长度小于2048的算法。最终修改的配置如下:
jdk.certpath.disabledAlgorithms=MD2, RSA keySize < 512
问题就这样解决了,哈哈!!!
总结
有时遇到问题,真的是让人摸不到头脑,这时别着急,把自己能想到的方法先尝试一遍,说不定就能行了呢。遇到问题首先可以借助日志来分析问题(就像上面那样打开JDK的SSL的调试日志的开关),通过日志大概就能定位到时哪里出问题了。在实际的开发中,很多程序猿基本上不会记录日志,稍微好一点的可能会在控制台使用System.out.println(xxxx)
输出日志,不习惯使用像log4j这样的日志框架来记录日志。有人会说,出了问题,我可以直接debug呀!你有没想过在生产环境中,你还能用IDE来进行调试吗?
附录
tomcat的SSL配置
编辑conf/server.xml文件加入如下的配置:
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" SSLEnabled="true"
maxThreads="150" scheme="https" secure="true"
clientAuth="true" sslProtocol="TLS"
keystoreFile="${catalina.base}/server.ks" keystorePass="123456"
truststoreFile ="${catalina.base}/server_trust.ks"
truststorePass="123456"/>
说明:
- clientAuth为true表示开启SSL双向认证
- keystoreFile指定服务器端的证书位置
- truststoreFile指定服务器端信任证书库
服务器端证书配置
-
生成服务器端证书
keytool -genkeypair -v -alias server -keyalg RSA -validity 3650 -keystore ./server.ks -storepass 123456 -keypass 123456 -dname "CN=192.168.2.89,OU=zfx,O=zfx,L=gz,ST=gd,C=cn"
-
导出服务器端证书
keytool -exportcert -alias server -keystore ./server.ks -file ./server.cer -storepass 123456
-
导入服务器端证书到服务器信任证书列表
keytool -importcert -alias serverca -keystore ./server_trust.ks -file ./server.cer -storepass 123456
-
导入客户根证书到服务器信任证书列表
keytool -importcert -alias urootca -keystore ./server_trust.ks -file ./uroot.cer -storepass 123456
使用如下的命令查看信任的证书信息:
keytool -list -keystore ./server_trust.ks -storepass 123456
Keytool命令常用参数
- -genkeypair在用户主目录中创建一个默认文件”.keystore”,还会产生一个mykey的别名,mykey中包含用户的公钥、私钥和证书(在没有指定生成位置的情况下,keystore会存在用户系统默认目录)
- -alias 产生别名 每个keystore都关联这一个独一无二的alias,这个alias通常不区分大小写
- -keystore 指定密钥库的路径(产生的各类信息将不在.keystore文件中)
- -keyalg 指定密钥的算法 (如 RSA,DSA,默认值为:DSA)
- -validity 指定创建的证书有效期多少天(默认 90)
- -keysize 指定密钥长度 (默认 1024
- -storepass 指定密钥库的密码(获取keystore信息所需的密码)
- -keypass 指定别名条目的密码(私钥的密码)
- -dname 指定证书发行者信息 其中: “CN=名字与姓氏,OU=组织单位名称,O=组织名称,L=城市或区域名 称,ST=州或省份名称,C=单位的两字母国家代码”
- -list 显示密钥库中的证书信息如:keytool -list -v –keystore path/to/keystore -storepass password
- -v 显示密钥库中的证书详细信息
- -exportcert 导出指定别名的证书,如:keytool - exportcert -alias theAlias -keystore path/to/keystore -file path/to/keystore/cert -storepass pass
- -file 参数指定导出到文件的文件名
- -delete 删除密钥库中某条目 keytool -delete -alias theAlias -keystore path/to/keystore –storepass pass
- -printcert 控制台打印证书的详细信息,如:keytool -printcert -file path/to/keystore/cert -v
- -keypasswd 修改密钥库中指定条目口令 keytool -keypasswd -alias theAlias -keypass oldPass -new newPass -storepass keystorePass -keystore path/to/keystore
- -storepasswd 修改keystore口令 keytool -storepasswd -keystore path/to/keystore -storepass oldPass -new newPass
- -importcert 将已签名数字证书导入密钥库 keytool -importcert -alias certAlias -keystore path/to/keystore -file path/to/keystore/cert