3

I need to get the public keys of a secured website programmatically through java. I have read this, this, this and this and others as well. But I haven't found a solution of getting it through java.

EDIT:::

Based on Zielu's answer I wrote the following program:

import java.security.PublicKey;
import java.security.cert.Certificate;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;

public class RetrievePublicKey {

    private static PublicKey getKey(String hostname, int port) throws Exception {
        SSLSocketFactory factory = HttpsURLConnection.getDefaultSSLSocketFactory();        
        SSLSocket socket = (SSLSocket) factory.createSocket(hostname, port);
        socket.startHandshake();
        Certificate[] certs = socket.getSession().getPeerCertificates();
        Certificate cert = certs[0];
        PublicKey key = cert.getPublicKey();
        System.out.println(key);
        return key;
    }
    public static void main(String[] args) throws Exception {
        System.out.println(getKey("bctcl-parasuram.bctchn.local", 8443));
    }

}

But when I run it, i get the following exception:

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(Alerts.java:192)
    at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1937)
    at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:302)
    at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:296)
    at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1478)
    at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:212)
    at sun.security.ssl.Handshaker.processLoop(Handshaker.java:957)
    at sun.security.ssl.Handshaker.process_record(Handshaker.java:892)
    at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1050)
    at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1363)
    at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1391)
    at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1375)
    at RetrievePublicKey.getKey(RetrievePublicKey.java:22)
    at RetrievePublicKey.main(RetrievePublicKey.java:30)
Caused by: 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.validator.PKIXValidator.doBuild(PKIXValidator.java:387)
    at sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:292)
    at sun.security.validator.Validator.validate(Validator.java:260)
    at sun.security.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:324)
    at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:229)
    at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:124)
    at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1460)
    ... 9 more
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    at sun.security.provider.certpath.SunCertPathBuilder.build(SunCertPathBuilder.java:145)
    at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:131)
    at java.security.cert.CertPathBuilder.build(CertPathBuilder.java:280)
    at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:382)
    ... 15 more
Community
  • 1
  • 1
Parasu
  • 192
  • 2
  • 13
  • What's your CA? This exception maybe your public key is not present in [0] position of certificates? – Mateus Apr 13 '16 at 14:52

2 Answers2

11

You can use SSLSocket to get the certificate and its public key:

import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import java.security.cert.Certificate;
...

    String hostname = "your.host";
    SSLSocketFactory factory = HttpsURLConnection.getDefaultSSLSocketFactory();        
    SSLSocket socket = (SSLSocket) factory.createSocket(hostname, 443);
    socket.startHandshake();
    Certificate[] certs = socket.getSession().getPeerCertificates();
    Certificate cert = certs[0];
    PublicKey key = cert.getPublicKey();

It works only if the certificate is valid (not self signed or signed by unknown authority). For self signed certificates, you can define your own TrustManager that will trust everything. See Allowing Java to use an untrusted certificate for SSL/HTTPS connection

But it should be avoided if can, as this kind of code left behind creates a security issue later on.

Community
  • 1
  • 1
Zielu
  • 8,312
  • 4
  • 28
  • 41
  • Thanks for your reply. However I quickly tested your code, but I get an exception _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_ – Parasu Apr 10 '15 at 12:30
  • I tested mine and it works. It means that the certificate of your site is not valid (self signed??? or signed by unkown authority) and the handshake could not be established. If it is signed then you can import the authority certificate to the java key store. – Zielu Apr 10 '15 at 12:33
  • yes, you are correct. Its a self signed certificate generated using java's keytool. I tried with a valid https site and it worked. Is there a way to get the key even for this self signed certificates? – Parasu Apr 10 '15 at 13:22
  • Thanks Zielu. I must use a Trust Manager to get those certificates. I understand the security risks there. This is only for a local testing and not for production. Thanks again. – Parasu Apr 23 '15 at 03:47
  • If it is a self signed or signed by unknown authority, its better to get a confirmation from the user before using that certificate. I might show a popup for user confirmation. – Parasu Apr 28 '15 at 03:44
  • Works perfectly when NOT use self-signed certificate. Thanks Zielu! – Mateus Apr 13 '16 at 14:49
0

Getting the cert when the server is recognized by the app can be done using Zielu's answer. However if the server is unrecognized (e.g. self signed or signed by unknown root authority that is not contained in your JVM's keystore) you can retrieve the server's cert programmatically using InstallCert. An abbreviated version follows: [Warning: as stated by others, using an untrusted cert this way should be done only if you know and trust the cert owner otherwise it is a security risk.]

import javax.net.ssl.*;
import java.io.*;
import java.security.*;
import java.security.cert.*;

public class FetchCert {

    public static void main(String[] args) throws Exception {
        //REPLACE THIS WITH YOUR TARGET HOST NAME
        String hostname = "example.com";
        SSLSocketFactory factory = HttpsURLConnection.getDefaultSSLSocketFactory();

        SSLSocket socket = (SSLSocket) factory.createSocket(hostname, 443);
        try {
            socket.startHandshake();
            socket.close();
            System.out.println("No errors, certificate is already trusted");
            return;
        } catch (SSLException e) {
            System.out.println("cert likely not found in keystore, will pull cert...");
        }


        KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
        char[] password = "changeit".toCharArray();
        ks.load(null, password);

        SSLContext context = SSLContext.getInstance("TLS");
        TrustManagerFactory tmf =
                TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(ks);
        X509TrustManager defaultTrustManager = (X509TrustManager) tmf.getTrustManagers()[0];
        SavingTrustManager tm = new SavingTrustManager(defaultTrustManager);
        context.init(null, new TrustManager[]{tm}, null);
        factory = context.getSocketFactory();

        socket = (SSLSocket) factory.createSocket(hostname, 443);
        try {
            socket.startHandshake();
        } catch (SSLException e) {
            //we should get to here
        }
        X509Certificate[] chain = tm.chain;
        if (chain == null) {
            System.out.println("Could not obtain server certificate chain");
            return;
        }

        X509Certificate cert = chain[0];
        String alias = hostname;
        ks.setCertificateEntry(alias, cert);

        System.out.println("saving file jssecacerts to working dir");
        System.out.println("copy this file to your jre/lib/security folder");
        FileOutputStream fos = new FileOutputStream("jssecacerts");
        ks.store(fos, password);
        fos.close();
    }
    private static class SavingTrustManager implements X509TrustManager {

        private final X509TrustManager tm;
        private X509Certificate[] chain;

        SavingTrustManager(X509TrustManager tm) {
            this.tm = tm;
        }

        public X509Certificate[] getAcceptedIssuers() {

        return new X509Certificate[0];  
        }

        public void checkClientTrusted(X509Certificate[] chain, String authType)
                throws CertificateException {
            throw new UnsupportedOperationException();
        }

        public void checkServerTrusted(X509Certificate[] chain, String authType)
                throws CertificateException {
            this.chain = chain;
            tm.checkServerTrusted(chain, authType);
        }
    }
}