问题
I'm running a basic web app within Tomcat and Java 6. In my app I have to connect to two different remote systems and each requires a unique client certificate. Over a year ago, someone on the team was able to import both PFX files into 1 JKS file, and we could successfully communicate to the remote systems. Unfortunately, the team-member that created this JKS file isn't around anymore and both of the certificates have expired.
When I tried to re-create the combined JKS file, only 1 certificate was ever used, regardless of the system we were connecting to. That obviously caused one of the connections to fail. Here's a list of the keystore entries
comodo-root, Jul 18, 2012, trustedCertEntry,
Certificate fingerprint (MD5): 1D:35:54:04:85:78:B0:3F:42:42:4D:BF:20:73:0A:3F
comodo-intermediate, Jul 18, 2012, trustedCertEntry,
Certificate fingerprint (MD5): 2B:EE:B7:93:D7:C5:DD:65:E3:16:E9:98:EF:85:9B:F7
le-2f6efe10-57f8-4224-ba41-59940bd5422a, May 20, 2014, PrivateKeyEntry,
Certificate fingerprint (MD5): 75:1C:60:72:4A:23:33:19:26:15:7F:27:8B:C6:65:A6
aa, May 20, 2014, PrivateKeyEntry,
Certificate fingerprint (MD5): D2:D8:73:DA:FD:A0:09:42:12:27:B0:50:E8:98:4C:48
Where le-2f6efe10-57f8-4224-ba41-59940bd5422a
and aa
are the two client certificates/stores that I imported.
I have validated that both certificates work independently if I use them outside of the JKS. I have also verified that if I import only one of the PFX files into the JKS I'm able to successfully connect to that system that has the certificate imported.
I'm setting the keystore properties at runtime during JVM start
System.setProperty("javax.net.ssl.keyStore", "path to my keystore" );
System.setProperty("javax.net.ssl.keyStorePassword", "password" );
The problem only comes in when I have both PFX files imported, and the errors I'm receiving are from the remote servers indicating that the client certificate that's being passed is bad.
回答1:
You don't say where you got your private keys, but I suspect that what is happening here is due to the fact that your new certificates have been signed by the same CA, or you have generated two new secrets using the same algorithm, or both.
The way that SSL is set up, if the server requires client authentication it will send a list of acceptable certificate types, possibly along with a list of acceptable CAs (good overview here). Your Java client then applies the following algorithm:
- For each certificate type (RSA, DSA, EC) accepted by the server, find any public/private key pairs in the key store which have been generated with the specified algorithm
- If the server sent a list of acceptable CAs, remove any key pair that doesn't contain any of the preferred CAs in its certificate chain
- If there is at least one key pair remaining, select the private key corresponding to the first one; otherwise go back to step 1 for the next key type.
So, for example, let us imagine you wish to connect to 2 servers, one of which asks for DSA, RSA or EC keys signed by VeriSign and one of which asks for RSA or DSA keys, with no preferred CAs. (You can find out the contents of the certificate requests by passing -Djavax.net.debug=ssl:handshake
to your client and looking for lines starting with *** CertificateRequest
)
If your keystore contains two key pairs generated with the DSA key algorithm and signed by VeriSign, you will get ambiguities in both cases. If you have one DSA key pair signed by VeriSign, one DSA key pair signed by Go Daddy and one RSA key pair signed by your internal CA there will be no ambiguities in either case.
Concretely in your case, your best way forward may be to inspect your old certificate chains to see who signed them and what type they are and make sure that your new set of certificates match the old ones. Or you may have to bite the bullet and write some code to make sure that you are always working with a key store that only contains one key each time, as in the linked questions.
回答2:
Firstly, a quick comment: using the javax.net.ssl.keyStore
system property (and password) globally for your JRE when your application is running within a container might not always be a good idea, especially if there are other webapps running within the same container.
The choice of client certificate is normally done by the chooseClientAlias of the X509KeyManager
in use.
When using the default SSLContext
(i.e. the one you use when setting javax.net.ssl.keyStore
and using the default SSLSocketFactory
, if your application doesn't do anything specific), this will depend on the JRE implementation. Assuming an Oracle/OpenJDK 7, you'll find a description in sun.security.ssl.X509KeyManagerImpl (note, after a discussion with @rxg about a similar question, I've just realised the comment I quoted ealier would only be used with the PKIX
or NewSunX509
KeyManagerFactory, so not the default one):
/* * Return the best alias that fits the given parameters. * The algorithm we use is: * . scan through all the aliases in all builders in order * . as soon as we find a perfect match, return * (i.e. a match with a cert that has appropriate key usage * and is not expired). * . if we do not find a perfect match, keep looping and remember * the imperfect matches * . at the end, sort the imperfect matches. we prefer expired certs * with appropriate key usage to certs with the wrong key usage. * return the first one of them. */
(The default (SunX509
) would use this implementation, which doesn't seem to check the key usage (or extended key usage).)
Which certificate will be chose is therefore likely to depend on how the keystore was created.
One of the reasons that could cause this failure would be a missing intermediate certificate in the chain. Your private key entry not only needs to contain the matching certificate, but it may also need to contain the full chain (independently of whether those intermediate certs are also in other entries) to be presentable to the server. More details in this question.
Another edge case (I'd say it's rather unlikely) would be that one of the servers sends an empty list of acceptable CAs. This is allowed (at least explicitly since TLS 1.1), but what the client should do is undefined. It's possible that, if your new keystore was constructed in a different way, a different alias is preferred in this case (so one server would work as usual, because it's using the normal CA list, but the other one would fail, because it now picks a different cert, depending on how the keystore was constructed). I must say this is an unlikely scenario, but you can check what the server sends you with Wireshark or using
-Djavax.net.debug=ssl
(look for the content of the Certificate Request message sent by the server).
In any case, and coming back to my initial comment, you'd be better off using two distinct keystores and using them as appropriate for each required connections. This way, you wouldn't have to expose your keystore globally do all the code running within that container and you would also have more control. There is an example in this answer. (By the way, you might not even need to convert your PFX files, just load them as PKCS12
keystores.)
Alternatively, if you really must use a single keystore, but assuming you have some control over your SSLContext
(worst case you could affect the default SSLContext
, but that's very clean), you could initialise a default X509KeyManager
using that keystore and wrap it into your own implementation where you implement your own chooseClientAlias
(by "wrapping", I mean delegate all the other methods to the wrapped instance from your own class, except the one you want to change). If you know in advance some of the expected remote hosts, you might be able to do something with the 3rd parameter (socket
).
来源:https://stackoverflow.com/questions/23768843/jks-with-multiple-privatekeyentries