转载:http://blog.rongzhiwang.com/%E8%9E%8D%E6%99%BA%E7%BD%91/archive/2012/04/05/java-ssl.aspx
如何分析Java SSL错误
原文作者:Jos Dirksen
在我最近的项目中,我处理了很多认证,像Java和HTTPS的客户端身份验证。在这些项目中,无论是在测试过程中,还是在创建一个新的环境中,我都碰到了各种SSL配置错误,这些错误往往导致一个uncomprehensive错误:
01.javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated
02. at com.sun.net.ssl.internal.ssl.SSLSessionImpl.getPeerCertificates(SSLSessionImpl.java:352)
03. at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:128)
04. at org.apache.http.conn.ssl.SSLSocketFactory.connectSocket(SSLSocketFactory.java:397)
05. at org.apache.http.impl.conn.DefaultClientConnectionOperator.openConnection(DefaultClientConnectionOperator.java:148)
06. at org.apache.http.impl.conn.AbstractPoolEntry.open(AbstractPoolEntry.java:150)
07. at org.apache.http.impl.conn.AbstractPooledConnAdapter.open(AbstractPooledConnAdapter.java:121)
08. at org.apache.http.impl.client.DefaultRequestDirector.tryConnect(DefaultRequestDirector.java:575)
09. at org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:425)
10. at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:820)
11. at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:754)
12. at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:732)
在大多数情况下,这是个错误的配置,在这里的密钥库并没有containt的正确证书,证书链是不完整的,或者客户端没有提供有效的凭证。因此,在最后一个项目中,我决定记录发生的事件,并分析导致在SSL握手期间的特定错误的原因。
在这篇文章中,我会告诉你特定的SSL错误发生的原因,并且如何通过分析握手信息进行检测,以及如何解决这些问题。为此,我使用以下情形:
服务器使用由CA颁发的证书,并要求客户端进行身份验证。服务器使用一个简单的信任列,可以信任的将CA列出。
客户端连接使用这个单一的可信任的由CA颁发的证书,并有它自己的trustore还包含从服务器发来的证书。
这不是一个非常复杂的环境,而是你经常可以看到的。请注意以下信息,当你不使用客户端的证书或使用自签名的证书时,也可以找出问题。在这些事例中,确定问题的方式基本上是相同的。
Happy流
首先,我们来看看happy流量,当我们使用客户端证书时,在握手阶段会发生什么。我们只有等到客户端和服务器交换证书并且验证接收到的证书时,才会看到完整的协商阶段。如果一切顺利的话,其余的内容就会很快完成的。当你使用Java虚拟机参数运行客户端和服务器时,以下是你所看到的:-Djavax.net.debug=ssl:handshake。
第一件事就是客户端使用TLS协议版本发送一个ClientHello消息,这个消息包含一个随机数和建议的加密算法套件和压缩方法的列表。从我们的客户来看,下面所示:
客户端发送:
1.*** ClientHello, TLSv1
2.RandomCookie: GMT: 1331663143 bytes = { 141, 219, 18, 140, 148, 60, 33, 241, 10, 21, 31, 90, 88, 145, 34, 153, 238, 105, 148, 72, 163, 210, 233, 49, 99, 224, 226, 64 }
3.Session ID: {}
4.Cipher Suites: [SSL_RSA_WITH_RC4_128_MD5, SSL_RSA_WITH_RC4_128_SHA, TLS_RSA_WITH_AES_128_CBC_SHA, TLS_RSA_WITH_AES_256_CBC_SHA, TLS_DHE_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_RSA_WITH_AES_256_CBC_SHA, TLS_DHE_DSS_WITH_AES_128_CBC_SHA, TLS_DHE_DSS_WITH_AES_256_CBC_SHA, SSL_RSA_WITH_3DES_EDE_CBC_SHA, SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA, SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA, SSL_RSA_WITH_DES_CBC_SHA, SSL_DHE_RSA_WITH_DES_CBC_SHA, SSL_DHE_DSS_WITH_DES_CBC_SHA, SSL_RSA_EXPORT_WITH_RC4_40_MD5, SSL_RSA_EXPORT_WITH_DES40_CBC_SHA, SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA, SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA, TLS_EMPTY_RENEGOTIATION_INFO_SCSV]
5.Compression Methods: { 0 }
6.***
服务器刚开始用 ServerHello 消息来响应,这个消息包含由客户提供的信息的基础上的另一个随机数和一个可选的会话 ID 作出的选择。
服务器端发送:
1.*** ServerHello, TLSv1
2.RandomCookie: GMT: 1331663143 bytes = { 172, 233, 79, 197, 14, 21, 187, 161, 114, 206, 7, 38, 188, 228, 120, 102, 115, 214, 155, 86, 211, 41, 156, 179, 138, 2, 230, 81 }
3.Session ID: {79, 96, 145, 39, 203, 136, 206, 69, 170, 46, 194, 17, 154, 175, 13, 138, 143, 199, 162, 193, 110, 86, 113, 109, 248, 187, 220, 169, 47, 180, 44, 68}
4.Cipher Suite: SSL_RSA_WITH_RC4_128_MD5
5.Compression Method: 0
6.Extension renegotiation_info, renegotiated_connection: <empty>
7.***
因此,在这种情况下,我们要使用SSL_RSA_WITH_RC4_128_MD5作为密码套件。下一步也是由服务器来完成。服务器接下来发送一个证书的消息,其中包含完整的证书链:
服务器端发送:
01.*** Certificate chain
02.chain [0] = [
03.[
04. Version: V1
05. Subject: CN=server, C=NL
06. Signature Algorithm: SHA1withRSA, OID = 1.2.840.113549.1.1.5
07.
08. Key: Sun RSA public key, 1024 bits
09. modulus: 143864428144045085986129639694300995179398936575198896494655652087658861594939489453166811774109137006267822033915476680673848164790815913192075840268069822357600376998775923266017630332239546722181180383155088413406178660120548292599278819762883993031950564327152510982887716901499177102158407884939613382007
10. public exponent: 65537
11. Validity: [From: Wed Mar 14 13:32:04 CET 2012,
12. To: Thu Mar 14 13:32:04 CET 2013]
13. Issuer: CN=Application CA, OU=GKD, O=Smartjava, L=Maasland, ST=ZH, C=NL
14. SerialNumber: [ a881d144 5e631f21]
15.
16.]
17. Algorithm: [SHA1withRSA]
18. Signature:
19.0000: C3 56 81 7F 33 91 8A FF 84 5E 0B BA 7A 01 D8 41 .V..3....^..z..A
20.0010: 6B 47 B2 F7 8F FB B5 77 23 D8 FB B2 35 19 6E C4 kG.....w#...5.n.
21.0020: A4 6A BC 23 BB 69 92 F6 85 5A 1E CB FE 23 C6 98 .j.#.i...Z...#..
22.0030: A0 57 F8 FB E9 DB B0 40 BD 8E F8 35 F8 77 E1 09 .W.....@...5.w..
23.0040: 5A 2E 45 71 80 F6 89 E7 0B 93 E2 48 EB 40 92 13 Z.Eq.......H.@..
24.0050: 14 AA 1F 59 AA 98 67 46 9B 52 33 49 9A 3C 91 9B ...Y..gF.R3I.<..
25.0060: F1 CB 8A BD 7D D4 DD 76 C4 15 00 36 A3 B2 87 A7 .......v...6....
26.0070: D5 FF 52 E3 68 D4 F0 E0 32 86 74 02 DD 92 EC 1D ..R.h...2.t.....
27.
28.]
29.chain [1] = [
30.[
31. Version: V3
32. Subject: CN=Application CA, OU=SL, O=SmartJava, L=Waalwijk, ST=ZH, C=NL
33. Signature Algorithm: SHA1withRSA, OID = 1.2.840.113549.1.1.5
34.
35. Key: Sun RSA public key, 1024 bits
36. modulus: 159927271510058538658170959055540487654246676457579822126433656091883150307639380685203152841988861440546492270915750324654620063428634486478674507234742748515614639629692189315918046446256610037776978028900716455223387878926383828815082154427031884246429239077082613371662803582187768145965112751392402313823
37. public exponent: 65537
38. Validity: [From: Mon Mar 12 13:35:16 CET 2012,
39. To: Wed Apr 11 14:35:16 CEST 2012]
40. Issuer: CN=Application CA, OU=CA, O=Blaat, L=Waalwijk, ST=ZH, C=NL
41. SerialNumber: [ fe7636c5 6804e69c]
42.
43.Certificate Extensions: 3
44.[1]: ObjectId: 2.5.29.14 Criticality=false
45.SubjectKeyIdentifier [
46.KeyIdentifier [
47.0000: 6C CC 48 03 E4 BE 07 D6 9E F6 4C 78 53 54 A2 B8 l.H.......LxST..
48.0010: 7B DA 40 09 ..@.
49.]
50.]
51.
52.[2]: ObjectId: 2.5.29.35 Criticality=false
53.AuthorityKeyIdentifier [
54.KeyIdentifier [
55.0000: 6C CC 48 03 E4 BE 07 D6 9E F6 4C 78 53 54 A2 B8 l.H.......LxST..
56.0010: 7B DA 40 09 ..@.
57.]
58.
59.]
60.
61.[3]: ObjectId: 2.5.29.19 Criticality=false
62.BasicConstraints:[
63. CA:true
64. PathLen:2147483647
65.]
66.
67.]
68. Algorithm: [SHA1withRSA]
69. Signature:
70.0000: 1A 30 08 15 01 8E A6 36 5F 38 22 C6 81 5E 69 B1 .0.....6_8"..^i.
71.0010: 42 9A 1E FF 0F C4 D7 40 5F 85 0E 42 35 E0 CC 00 B......@_..B5...
72.0020: 6E A5 2E 70 6B 79 64 C5 99 AE A4 29 CB 26 DE 60 n..pkyd....).&.`
73.0030: 0B A6 AB 19 06 6F 19 54 6C 1A 88 9E 3A 6A D4 BB .....o.Tl...:j..
74.0040: CB 28 85 2F 72 4D DE 35 C0 9B F4 2F EF 8E 6D E8 .(./rM.5.../..m.
75.0050: 30 AC 12 7D B4 0D A3 08 DA D4 60 46 94 BD 12 AF 0.........`F....
76.0060: 44 F7 C3 B8 9D 69 2D 6A 32 C8 4D AE 12 60 05 09 D....i-j2.M..`..
77.0070: FE AE D0 1A 72 6D 91 CE DA 7C 8E D5 31 14 31 4C ....rm......1.1L
78.
79.]
在此消息中,你可以看到,此证书的发行者是我们的例子CA。在这种情况下,我们的客户端会检测到此证书是否可信。因为从客户端的角度来看,我们需要客户端验证服务器的请求的证书,并且在这之后,发送一个helloDone报文。
服务器端发送:
1.*** CertificateRequest
2.Cert Types: RSA, DSS
3.Cert Authorities:
4.<CN=Application CA, OU=CA, O=Blaat, L=Waalwijk, ST=ZH, C=NL>
5.*** ServerHelloDone
在此消息中,你可以看到该服务器提供了一个它信任的证书颁发机构的列表。客户端将使用此信息来确定它是否有一个匹配此CA的密钥。在我们的happy流中,它有一个密钥和证书消息的响应。
客户端发送:
01.*** Certificate chain
02.chain [0] = [
03.[
04. Version: V1
05. Subject: CN=Application 3, OU=Smartjava, O=Smartjava, L=NL, ST=ZH, C=NL
06. Signature Algorithm: SHA1withRSA, OID = 1.2.840.113549.1.1.5
07.
08. Key: Sun RSA public key, 1024 bits
09. modulus: 90655907749318585147523875906892969031300830816947226352221659107570169820452561428696751943383590982109524990627182456571533992582229229163232831159652561902456847954385746762477844009336466314872376131553489447601649924116778337873632641536164462534398137791450495316700015095054427027256393580022887087767
10. public exponent: 65537
11. Validity: [From: Mon Mar 12 15:13:24 CET 2012,
12. To: Tue Mar 12 15:13:24 CET 2013]
13. Issuer: CN=Application CA, OU=Smartjava, O=Smartjava, L=Maasland, ST=ZH, C=NL
14. SerialNumber: [ b247ffb2 ce060768]
15.
16.]
17. Algorithm: [SHA1withRSA]
18. Signature:
19.0000: 97 58 36 C5 28 87 B3 16 9B DD 31 0C E0 C6 23 76 .X6.(.....1...#v
20.0010: 72 82 5B 13 4D 23 B6 0E A9 2F 9F 0C 3F 97 15 6E r.[.M#.../..?..n
21.0020: 7B 38 EC DE E2 57 D7 AA 07 12 E3 98 B7 86 A7 CE .8...W..........
22.0030: 57 8E A1 29 96 C9 F0 30 57 67 C7 F1 F2 98 90 64 W..)...0Wg.....d
23.0040: 6C B9 6C 05 24 8B 56 3F B1 FF 03 62 3D 81 DB 45 l.l.$.V?...b=..E
24.0050: D3 1F C1 B2 DD 77 CF 74 54 EB 9D 82 23 89 1A 70 .....w.tT...#..p
25.0060: F8 C4 68 6A B7 41 C7 DE 7B B6 3A 0C 17 E7 FA 98 ..hj.A....:.....
26.0070: 19 0C D8 91 FB 5E FE D2 B3 92 FD 2D 2A 6B 51 10 .....^.....-*kQ.
27.
28.]
29.chain [1] = [
30.[
31. Version: V3
32. Subject: CN=Application CA, OU=Smartjava, O=Smartjava, L=Maasland, ST=ZH, C=NL
33. Signature Algorithm: SHA1withRSA, OID = 1.2.840.113549.1.1.5
34.
35. Key: Sun RSA public key, 1024 bits
36. modulus: 159927271510058538658170959055540487654246676457579822126433656091883150307639380685203152841988861440546492270915750324654620063428634486478674507234742748515614639629692189315918046446256610037776978028900716455223387878926383828815082154427031884246429239077082613371662803582187768145965112751392402313823
37. public exponent: 65537
38. Validity: [From: Mon Mar 12 13:35:16 CET 2012,
39. To: Wed Apr 11 14:35:16 CEST 2012]
40. Issuer: CN=Application CA, OU=Smartjava, O=Smartjava, L=Maasland, ST=ZH, C=NL
41. SerialNumber: [ fe7636c5 6804e69c]
42.
43.Certificate Extensions: 3
44.[1]: ObjectId: 2.5.29.14 Criticality=false
45.SubjectKeyIdentifier [
46.KeyIdentifier [
47.0000: 6C CC 48 03 E4 BE 07 D6 9E F6 4C 78 53 54 A2 B8 l.H.......LxST..
48.0010: 7B DA 40 09 ..@.
49.]
50.]
51.
52.[2]: ObjectId: 2.5.29.35 Criticality=false
53.AuthorityKeyIdentifier [
54.KeyIdentifier [
55.0000: 6C CC 48 03 E4 BE 07 D6 9E F6 4C 78 53 54 A2 B8 l.H.......LxST..
56.0010: 7B DA 40 09 ..@.
57.]
58.
59.]
60.
61.[3]: ObjectId: 2.5.29.19 Criticality=false
62.BasicConstraints:[
63. CA:true
64. PathLen:2147483647
65.]
66.
67.]
68. Algorithm: [SHA1withRSA]
69. Signature:
70.0000: 1A 30 08 15 01 8E A6 36 5F 38 22 C6 81 5E 69 B1 .0.....6_8"..^i.
71.0010: 42 9A 1E FF 0F C4 D7 40 5F 85 0E 42 35 E0 CC 00 B......@_..B5...
72.0020: 6E A5 2E 70 6B 79 64 C5 99 AE A4 29 CB 26 DE 60 n..pkyd....).&.`
73.0030: 0B A6 AB 19 06 6F 19 54 6C 1A 88 9E 3A 6A D4 BB .....o.Tl...:j..
74.0040: CB 28 85 2F 72 4D DE 35 C0 9B F4 2F EF 8E 6D E8 .(./rM.5.../..m.
75.0050: 30 AC 12 7D B4 0D A3 08 DA D4 60 46 94 BD 12 AF 0.........`F....
76.0060: 44 F7 C3 B8 9D 69 2D 6A 32 C8 4D AE 12 60 05 09 D....i-j2.M..`..
77.0070: FE AE D0 1A 72 6D 91 CE DA 7C 8E D5 31 14 31 4C ....rm......1.1L
78.
79.]
此证书在服务器端进行检查,如果一切顺利的话,在握手的最后步骤中会执行设置安全连接。请注意,有一个CertificateVerify步骤。在此步骤中,客户端用其私钥来签署消息。服务器可以验证客户端访问其私钥。这似乎是步骤里可以在不正确的配置环境中的错误。在默认的java实现中,这将不会发生。 Java实现在客户端,如果私钥是可用的,就可以确定哪些证书提交到服务器的阶段会被检查到。
可能出现什么错误
那么在握手过程中,会出现什么问题?在下面两节中,我们将看看在某些情况下,如何检测它们。
密码
既然我们已经看到这么做下去会发生什么,那么让我们看看在下面情况下,会出现的这几个问题。我们先简单地用下面的异常来开始,在我们得到这个异常的一瞬间时,我们启动客户端应用程序:
Exception in thread "main" java.security.UnrecoverableKeyException: Cannot recover key
at sun.security.provider.KeyProtector.recover(KeyProtector.java:311)
at sun.security.provider.JavaKeyStore.engineGetKey(JavaKeyStore.java:121)
at sun.security.provider.JavaKeyStore$JKS.engineGetKey(JavaKeyStore.java:38)
at java.security.KeyStore.getKey(KeyStore.java:763)
at com.sun.net.ssl.internal.ssl.SunX509KeyManagerImpl.(SunX509KeyManagerImpl.java:113)
at com.sun.net.ssl.internal.ssl.KeyManagerFactoryImpl$SunX509.engineInit(KeyManagerFactoryImpl.java:48)
at javax.net.ssl.KeyManagerFactory.init(KeyManagerFactory.java:239)
at org.apache.http.conn.ssl.SSLSocketFactory.createSSLContext(SSLSocketFactory.java:186)
at org.apache.http.conn.ssl.SSLSocketFactory.(SSLSocketFactory.java:260)
当从Javadoc中,“在keystore中的关键不能被收回”时,这个被抛出的消息是非常有用的。会发生这种情况的原因有很多,但通常来说,这会发生在keystore的密码中,它能够访问错误的密码。通常,当你使用keytool创建和管理你的密钥时,keystore密码通常与密钥的密码相同。不过,如果你从一个PKCS#12类型的keystore导入密钥的话,那么密钥库的密码可以很容易地被设置为不同的值。并非所有的SSL客户端允许您指定一个不同的密钥和密钥库的密码。如果是这样的话,你可以使用下面的命令来改变密码的关键字段:
1.keytool -keypasswd -alias <keyalias> -keystore <keystore>
为keystore设置一个不正确的密码是很有可能的。幸运的是,在这种情况下,被抛出的错误消息会更有帮助:
1.Exception in thread "main" java.io.IOException: Keystore was tampered with, or password was incorrect
2. at sun.security.provider.JavaKeyStore.engineLoad(JavaKeyStore.java:771)
3. at sun.security.provider.JavaKeyStore$JKS.engineLoad(JavaKeyStore.java:38)
4. at java.security.KeyStore.load(KeyStore.java:1185)
5. ...
6.Caused by: java.security.UnrecoverableKeyException: Password verification failed
7. at sun.security.provider.JavaKeyStore.engineLoad(JavaKeyStore.java:769)
8. ... 3 more
如果这发生在服务器端的话,我们在创建SSL侦听器的时候,可以看到相同的消息。
不完整的CA链
现在让我们来看看 “同行没有经过认证” 的第一个异常。在日志中,我们看到在客户端中的这种异常:
1.javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated
2. at com.sun.net.ssl.internal.ssl.SSLSessionImpl.getPeerCertificates(SSLSessionImpl.java:352)
3. at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:128)
4. at org.apache.http.conn.ssl.SSLSocketFactory.connectSocket(SSLSocketFactory.java:397)
5. at org.apache.http.impl.conn.DefaultClientConnectionOperator.openConnection(DefaultClientConnectionOperator.java:148)
6. at org.apache.http.impl.conn.AbstractPoolEntry.open(AbstractPoolEntry.java:150)
因此,启用SSL日志记录,然后再运行,我们会开始分析握手阶段。我们首先从客户端来看,进行这个分析。如果我们浏览一下这个日志,我们会从服务器和ServerHelloDone中发现以下的CertificateRequest消息。
1.*** CertificateRequest
2.Cert Types: RSA, DSS
3.Cert Authorities:
4.*** ServerHelloDone
因此,到目前为止,一切都OK。服务器已发送了其证书,并因为我们的客户没有抛出这部分的错误,我们也可以假设它是客户可信赖的。从服务器发生此消息后,这样做似乎是错误的步骤。
如果仔细看此消息,你可以看到,服务器不指定它信任的核证机关。这可能是一个在服务器端的配置错误,或者它可能是该服务器预计受信任的Root CA之一。在任何情况下,客户端发送他想要的一些证书是自由的。因此,客户端发送以下证书:
01.*** Certificate chain
02.chain [0] = [
03.[
04. Version: V1
05. Subject: CN=Application4, OU=Smartjava, O=Smartjava, L=NL, ST=NB, C=NL
06. Signature Algorithm: SHA1withDSA, OID = 1.2.840.10040.4.3
07.
08....
09.]
10.chain [1] = [
11.[
12. Version: V3
13. Subject: EMAILADDRESS=jos.dirksen@gmail.com, CN=CA2, OU=Smartjava, O=Smartjava, L=Waalwijk, ST=NB, C=NL
14. Signature Algorithm: SHA1withDSA, OID = 1.2.840.10040.4.3
15. ...
16.]
根据该规范,现在客户端继续与密钥交换,并产生交换的秘密。顺着这个思路下去,我们可以看到如下结果:
1.pool-1-thread-1, WRITE: TLSv1 Handshake, length = 32
2.pool-1-thread-1, READ: TLSv1 Alert, length = 2
3.pool-1-thread-1, RECV TLSv1 ALERT: fatal, internal_error
4.pool-1-thread-1, called closeSocket()
这意味着我们已经收到了一个内部错误。因此,在服务器端的内容出了问题。纵观我们在SSL转储中可以在服务器端看到以下内容:
01.*** Certificate chain
02.chain [0] = [
03.[
04. Version: V1
05. Subject: CN=Application4, OU=Smartjava, O=Smartjava, L=NL, ST=NB, C=NL
06. Signature Algorithm: SHA1withDSA, OID = 1.2.840.10040.4.3
07. ...
08.]
09.chain [1] = [
10.[
11. Version: V3
12. Subject: EMAILADDRESS=jos.dirksen@gmail.com, CN=CA2, OU=Smartjava, O=Smartjava, L=Waalwijk, ST=NB, C=NL
13. Signature Algorithm: SHA1withDSA, OID = 1.2.840.10040.4.3
14. ...
15.]
16.***
17.qtp1735121130-17, handling exception: java.lang.RuntimeException: Unexpected error: java.security.InvalidAlgorithmParameterException: the trustAnchors parameter must be non-empty
18.qtp1735121130-17, SEND TLSv1 ALERT: fatal, description = internal_error
19.qtp1735121130-17, WRITE: TLSv1 Alert, length = 2
你可以看到,我们收到来自客户端的证书,并是在我们得到这个错误之后收到的。然而,这个错误并没真正地告诉我们任何内容。然而,我们确实有足够的信息至少可以限制可能出现的错误。我们知道,服务器没有发送CA列表,我们可以看到,客户端发送了一个有效的证书,在某种程度上,该服务器无法进行处理。它看起来像一个服务器truststore的问题。在这种情况下,最好的办法是看服务器信任的证书。无论是在cacerts文件中或是在它自己的truststore中,验证我们的客户端发送的CA证书是否在服务器的信任库中,同时服务器实际上载入了我们所期望的库。
这当然也可能是客户端从服务器收到的证书信任不完整的链。在这种情况下,我们再次在客户端获取到“同行没有经过身份验证”的错误。如果我们看一下SSL调试日志,我们可以在客户端看到下面发生的异常:
1.pool-1-thread-1, handling exception: java.lang.RuntimeException: Unexpected error: java.security.InvalidAlgorithmParameterException: the trustAnchors parameter must be non-empty
2.pool-1-thread-1, SEND TLSv1 ALERT: fatal, description = internal_error
3.pool-1-thread-1, WRITE: TLSv1 Alert, length = 2
此异常直接发生在服务器使用“证书信息”发送出的证书后:
1.*** Certificate chain
2.chain [0] = [
3.[
4. Version: V1
5. Subject: CN=server, C=NL
6. Signature Algorithm: SHA1withRSA, OID = 1.2.840.113549.1.1.5
对于我们在客户端的信任库中出现的错误来说,下面的服务器端的道理也是相同的。为了完整起见,当这种情况发生在客户端时,服务器会收到此错误消息:
1.qtp1500389297-17, READ: TLSv1 Alert, length = 2
2.qtp1500389297-17, RECV TLSv1 ALERT: fatal, internal_error
3.qtp1500389297-17, called closeSocket()
4.qtp1500389297-17, handling exception: javax.net.ssl.SSLException: Received fatal alert: internal_error
5.qtp1500389297-17, called close()
6.qtp1500389297-17, called closeInternal(true)
无效密钥
在接下来的练习中,我们来看看下面在握手过程中出现的错误。在客户端的日志记录中,我们可以看到在SSL输出中的以下错误消息:
1.pool-1-thread-1, WRITE: TLSv1 Handshake, length = 32
2.pool-1-thread-1, READ: TLSv1 Alert, length = 2
3.pool-1-thread-1, RECV TLSv1 ALERT: fatal, internal_error
4.pool-1-thread-1, called closeSocket()
5.pool-1-thread-1, handling exception: javax.net.ssl.SSLException: Received fatal alert: internal_error
6.pool-1-thread-1, IOException in getSession(): javax.net.ssl.SSLException: Received fatal alert: internal_error
这些异常的出现是没有用的:
01.javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated
02. at com.sun.net.ssl.internal.ssl.SSLSessionImpl.getPeerCertificates(SSLSessionImpl.java:352)
03. at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:128)
04. at org.apache.http.conn.ssl.SSLSocketFactory.connectSocket(SSLSocketFactory.java:397)
05. at org.apache.http.impl.conn.DefaultClientConnectionOperator.openConnection(DefaultClientConnectionOperator.java:148)
06. at org.apache.http.impl.conn.AbstractPoolEntry.open(AbstractPoolEntry.java:150)
07. at org.apache.http.impl.conn.AbstractPooledConnAdapter.open(AbstractPooledConnAdapter.java:121)
08. at org.apache.http.impl.client.DefaultRequestDirector.tryConnect(DefaultRequestDirector.java:575)
09. at org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:425)
10. at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:820)
11. at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:754)
12. at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:732)
当你收到一个内部错误的时候,通常是在服务器端出现了错误。所以在服务器端我们可以看到引起这种错误的原因。
1.***
2.qtp2044601711-16, handling exception: java.lang.RuntimeException: Unexpected error: java.security.InvalidAlgorithmParameterException: the trustAnchors parameter must be non-empty
3.qtp2044601711-16, SEND TLSv1 ALERT: fatal, description = internal_error
那么,从某种程度上来讲,这是很有用的。好像我们使用的算法有些错误,所以客户端提供了一个不正确的证书。但是,这是怎么回事呢?如果你看看happy流,您可以在某一个确定的时间内,发送服务器要求客户端使用的证书消息。让我们看看在这个消息中的响应:
01.*** CertificateRequest
02.Cert Types: RSA, DSS
03.Cert Authorities:
04.<EMAILADDRESS=jos.dirksen@gmail.com, CN=CA2, OU=Smartjava, O=Smartjava, L=Waalwijk, ST=NB, C=NL>
05.*** ServerHelloDone
06.matching alias: application4
07.*** Certificate chain
08.chain [0] = [
09.[
10. Version: V1
11. Subject: CN=Application4, OU=Smartjava, O=Smartjava, L=NL, ST=NB, C=NL
12. Signature Algorithm: SHA1withDSA, OID = 1.2.840.10040.4.3
13.
14. Key: Sun DSA Public Key
15. ...
你可以在这里看到该服务器指定的它可接受的证书类型和接受的授权机构。然而在这种情况下,客户端响应了DSA公钥。根据在服务器上的实现过程,可能会导致这种奇怪的消息。我见过(尤其是自签名的证书)的另一种可能的方案是带有“CertificateRequest” 消息的:
1.*** CertificateRequest
2.Cert Types: RSA, DSS
3.Cert Authorities:
4.*** ServerHelloDone
此客户端完全不会回应所有证书,如果你只在您的keystore中,有DSA基础密钥。它不是在客户端抛出一个错误,而是会造成一个“空证书链” 消息作为服务器端。虽然我还没有在这种情况下见过你不使用自签名证书。
证书过期
到目前为止,我们已经看到了如何分析SSL握手过程,以确定寻找配置错误的地方。在最后一个例子中,我们来看看在证书到期时会发生什么。在这种情况下,我们再次看到在客户端非常隐秘的消息:
01.pool-1-thread-1, READ: TLSv1 Alert, length = 2
02.pool-1-thread-1, RECV TLSv1 ALERT: fatal, certificate_unknown
03.pool-1-thread-1, called closeSocket()
04.pool-1-thread-1, handling exception: javax.net.ssl.SSLHandshakeException: Received fatal alert: certificate_unknown
05.pool-1-thread-1, IOException in getSession(): javax.net.ssl.SSLHandshakeException: Received fatal alert: certificate_unknown
06.pool-1-thread-1, called close()
07.pool-1-thread-1, called closeInternal(true)
08.pool-1-thread-1, called close()
09.pool-1-thread-1, called closeInternal(true)
10.javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated
11. at com.sun.net.ssl.internal.ssl.SSLSessionImpl.getPeerCertificates(SSLSessionImpl.java:352)
12. at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:128)
13. at org.apache.http.conn.ssl.SSLSocketFactory.connectSocket(SSLSocketFactory.java:397)
14. at org.apache.http.impl.conn.DefaultClientConnectionOperator.openConnection(DefaultClientConnectionOperator.java:148)
15. at org.apache.http.impl.conn.AbstractPoolEntry.open(AbstractPoolEntry.java:150)
如果我们看一下SSL握手的阶段,我们可以看到,当我们收到此错误时,我们已经发送了我们的客户端证书,并完成了握手阶段。服务器端的错误实际上是非常有帮助的。接收证书无效后,在调试日志里可以向我们展示出来:
01.***
02.qtp1735121130-17, SEND TLSv1 ALERT: fatal, description = certificate_unknown
03.qtp1735121130-17, WRITE: TLSv1 Alert, length = 2
04.[Raw write]: length = 7
05.0000: 15 03 01 00 02 02 2E .......
06.qtp1735121130-17, called closeSocket()
07.qtp1735121130-17, handling exception: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path validation failed: java.security.cert.CertPathValidatorException: timestamp check failed
08.qtp1735121130-17, called close()
09.qtp1735121130-17, called closeInternal(true)
它告诉我们,在证书验证时,时间戳检查失败。这就告诉我们,我们应该在我们的证书链中查看证书的有效性。
总结
在这篇文章中,你见过一些SSL异常和方法来判别异常原因。引起异常的原因有很多,但最常见的有以下几种:
在客户端信任库中不正确的证书
在服务器端信任库中不正确的证书
私钥使用的无效密钥算法
证书过期或过期的CA证书
访问密钥使用的不正确的密码
从多密钥中进行选取
如果你用这样的异常提出一个很好的一般方法是这样的。那么你先检查所涉及的密钥库。使用Java keytool:
1.keytool -list -v -keystore <location_of_keystore>
这将打印出所有在keystore中的证书和密钥。检查密钥是否支持某些类型,所需的CA证书被存储起来,并且您的应用程序使用的是正确的(我花了几个小时才搞清楚一个问题,因为我寻找到了我的私钥信任)。如果一切都很顺利的话,那么就该启用SSL调试了(-Djavax.net.debug=ssl:handshake),并检查发送握手消息的时间。维基百科有一个很好的概述,即在特定的时间发送消息。对于消息的内容的更多信息请参看 RFC 5246(或你使用的SSL / TLS的版本之一,但版本之间的握手变化比较小)。使用消息和握手可以确定在什么地方会出现握手错误,同时考虑到当服务器处理证书时,该客户端将继续进行握手进程。