My problem:
I want to connect to servers (not limited to the HTTPS protocol -- could be LDAP-over-SSL, could be SMTPS, could be IMAPS, etc.) that may be using certificates that Java will not trust by default (because they are self-signed).
The desired workflow is to attempt the connection, retrieve the certificate info, present that to the user and if he accepts it, add it to to the truststore so it'll be trusted going forward.
I am stuck at retrieving the certificate. I have code (see at the end of the post) that I've cribbed from here and from sites pointed to by answers to questions about java SSL. The code simply creates an SSLSocket
, starts the SSL handshake, and asks the SSL session for the Certificate[]
. The code works fine when I'm connecting to a server using an already-trustable certificate. But when I connect to a server using a self-signed cert I get the usual:
Exception in thread "main" javax.net.ssl.SSLHandshakeException:
sun.security.validator.ValidatorException: PKIX path building failed:
sun.security.provider.certpath.SunCertPathBuilderException: unable to
find valid certification path to requested target
at sun.security.ssl.Alerts.getSSLException(Unknown Source)
at sun.security.ssl.SSLSocketImpl.fatal(Unknown Source)
at sun.security.ssl.Handshaker.fatalSE(Unknown Source)
at sun.security.ssl.Handshaker.fatalSE(Unknown Source)
at sun.security.ssl.ClientHandshaker.serverCertificate(Unknown Source)
[etc]
If I run with -Djavax.net.debug=all
I see that the JVM does retrieve the self-signed cert but will blow up the connection for using an untrusted cert before getting to the point where it'll return the certificates.
Seems like a chicken-and-egg problem. It will not let me see the certificates because they are not trusted. But I need to see the certificates to be able to add them to the truststore so they will be trusted. How do you break out of this?
For example, if I run the program as:
java SSLTest www.google.com 443
I get a printout of the certs Google is using. But if I run it as
java SSLTest my.imap.server 993
I get the exception referenced above.
The code:
import java.io.InputStream;
import java.io.OutputStream;
import java.security.cert.*;
import javax.net.SocketFactory;
import javax.net.ssl.*;
public class SSLTest
{
public static void main(String[] args) throws Exception {
if (args.length != 2) {
System.err.println("Usage: SSLTest host port");
return;
}
String host = args[0];
int port = Integer.parseInt(args[1]);
SocketFactory factory = SSLSocketFactory.getDefault();
SSLSocket socket = (SSLSocket) factory.createSocket(host, port);
socket.startHandshake();
Certificate[] certs = socket.getSession().getPeerCertificates();
System.out.println("Certs retrieved: " + certs.length);
for (Certificate cert : certs) {
System.out.println("Certificate is: " + cert);
if(cert instanceof X509Certificate) {
try {
( (X509Certificate) cert).checkValidity();
System.out.println("Certificate is active for current date");
} catch(CertificateExpiredException cee) {
System.out.println("Certificate is expired");
}
}
}
}
}
You can do this implementing a temporary TrustManager
that accepts all certificates and a temporary HostnameVerifier
that verifies all names (obviously you have to use them only to retrieve the certificate and not to send private data).
The following code retrieve the certificates from an arbitrary https url and save them to a file:
URL url = new URL("https://<yoururl>");
SSLContext sslCtx = SSLContext.getInstance("TLS");
sslCtx.init(null, new TrustManager[]{ new X509TrustManager() {
private X509Certificate[] accepted;
@Override
public void checkClientTrusted(X509Certificate[] xcs, String string) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] xcs, String string) throws CertificateException {
accepted = xcs;
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return accepted;
}
}}, null);
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String string, SSLSession ssls) {
return true;
}
});
connection.setSSLSocketFactory(sslCtx.getSocketFactory());
if (connection.getResponseCode() == 200) {
Certificate[] certificates = connection.getServerCertificates();
for (int i = 0; i < certificates.length; i++) {
Certificate certificate = certificates[i];
File file = new File("/tmp/newcert_" + i + ".crt");
byte[] buf = certificate.getEncoded();
FileOutputStream os = new FileOutputStream(file);
os.write(buf);
os.close();
}
}
connection.disconnect();
See this copy of Andreas Sterbenz's InstallCert utility.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With