why doesn't java send the client certificate during SSL handshake?

前端 未结 4 1783
长情又很酷
长情又很酷 2020-11-22 12:15

I\'m trying to connect to a secure webservice.

I was getting a handshake failure even though my keystore and truststore have been set correctly.

After severa

4条回答
  •  醉酒成梦
    2020-11-22 12:58

    Most of the solutions I've seen revolve around using the keytool and none of them matched my case.

    Here is a very brief description: I've got a PKCS12 (.p12) which works fine in Postman with disabled certificate verification, however programmatically I always ended up getting server error "400 Bad Request" / "No required SSL certificate was sent".

    The reason was a missing TLS extension SNI (Server Name Indication) and following is the solution.


    Adding an extension/parameter to SSL Context

    After SSLContext init, add the following:

    SSLSocketFactory factory = sslContext.getSocketFactory();
        try (SSLSocket socket = (SSLSocket) factory.createSocket(host, port)) {
            SSLParameters sslParameters = socket.getSSLParameters();
            sslParameters.setServerNames(Collections.singletonList(new SNIHostName(hostName)));
            socket.setSSLParameters(sslParameters);
            socket.startHandshake();
        }
    

    Full HTTP Client class for this case (NOT FOR PRODUCTION)

    Note 1: SSLContextException and KeyStoreFactoryException simply extend RuntimeException.

    Note 2: Certificate validations are disabled, this example was intended for dev use only.

    Note 3: Disabling hostname verification was not required in my case, but I included it as a commented line

    import org.apache.http.conn.ssl.NoopHostnameVerifier;
    import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
    import org.apache.http.impl.client.CloseableHttpClient;
    import org.apache.http.impl.client.HttpClients;
    import org.apache.http.ssl.SSLContexts;
    
    import javax.net.ssl.*;
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.IOException;
    import java.net.URL;
    import java.security.*;
    import java.security.cert.CertificateException;
    import java.security.cert.X509Certificate;
    import java.util.Collections;
    import java.util.Objects;
    
    public class SecureClientBuilder {
    
        private String host;
        private int port;
        private boolean keyStoreProvided;
        private String keyStorePath;
        private String keyStorePassword;
    
        public SecureClientBuilder withSocket(String host, int port) {
            this.host = host;
            this.port = port;
            return this;
        }
    
        public SecureClientBuilder withKeystore(String keyStorePath, String keyStorePassword) {
            this.keyStoreProvided = true;
            this.keyStorePath = keyStorePath;
            this.keyStorePassword = keyStorePassword;
            return this;
        }
    
        public CloseableHttpClient build() {
            SSLContext sslContext = keyStoreProvided
                    ? getContextWithCertificate()
                    : SSLContexts.createDefault();
    
            SSLConnectionSocketFactory sslSocketFactory =
                    new SSLConnectionSocketFactory(sslContext);
    
            return HttpClients.custom()
                    .setSSLSocketFactory(sslSocketFactory)
                    //.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE)
                    .build();
        }
    
        private SSLContext getContextWithCertificate() {
            try {
                // Generate TLS context with specified KeyStore and
                SSLContext sslContext = SSLContext.getInstance("TLS");
                sslContext.init(getKeyManagerFactory().getKeyManagers(), new TrustManager[]{getTrustManager()}, new SecureRandom());
    
                SSLSocketFactory factory = sslContext.getSocketFactory();
                try (SSLSocket socket = (SSLSocket) factory.createSocket(host, port)) {
                    SSLParameters sslParameters = socket.getSSLParameters();
                    sslParameters.setServerNames(Collections.singletonList(new SNIHostName(host)));
                    socket.setSSLParameters(sslParameters);
                    socket.startHandshake();
                }
    
                return sslContext;
            } catch (NoSuchAlgorithmException | KeyManagementException | IOException e) {
                throw new SSLContextException("Could not create an SSL context with specified keystore.\nError: " + e.getMessage());
            }
        }
    
        private KeyManagerFactory getKeyManagerFactory() {
            try (FileInputStream fileInputStream = getResourceFile(keyStorePath)) {
                // Read specified keystore
                KeyStore keyStore = KeyStore.getInstance("PKCS12");
                keyStore.load(fileInputStream, keyStorePassword.toCharArray());
    
                // Init keystore manager
                KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
                keyManagerFactory.init(keyStore, keyStorePassword.toCharArray());
                return keyManagerFactory;
            } catch (NoSuchAlgorithmException | CertificateException | UnrecoverableKeyException | IOException | KeyStoreException e) {
                throw new KeyStoreFactoryException("Could not read the specified keystore.\nError: " + e.getMessage());
            }
        }
    
        // Bypasses error: "unable to find valid certification path to requested target"
        private TrustManager getTrustManager() {
            return new X509TrustManager() {
                @Override
                public void checkClientTrusted(java.security.cert.X509Certificate[] arg0, String arg1) {
                }
    
                @Override
                public void checkServerTrusted(java.security.cert.X509Certificate[] arg0, String arg1) {
                }
    
                @Override
                public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                    return new X509Certificate[0];
                }
            };
        }
    
        private FileInputStream getResourceFile(String keyStorePath) throws FileNotFoundException {
            URL resourcePath = getClass().getClassLoader().getResource(keyStorePath);
            return new FileInputStream(resourcePath.getFile());
        }
    
    }
    

    Using the client builder above

    Note 1: keystore (.p12) is looked for in "resources" folder.

    Note 2: Header "Host" is set to avoid server error "400 - Bad Request".

    String hostname = "myHost";
    int port = 443;
    String keyStoreFile = "keystore.p12";
    String keyStorePass = "somepassword";
    String endpoint = String.format("https://%s:%d/endpoint", host, port);
    
    CloseableHttpClient apacheClient = new SecureClientBuilder()
            .withSocket(hostname, port)
            .withKeystore(keyStoreFile, keyStorePass)
            .build();
    
    HttpGet get = new HttpGet(endpoint);
    get.setHeader("Host", hostname + ":" + port);
    CloseableHttpResponse httpResponse = apacheClient.execute(get);
    
    assert httpResponse.getStatusLine().getStatusCode() == 200;
    

    Reference docs

    https://docs.oracle.com/en/java/javase/11/security/java-secure-socket-extension-jsse-reference-guide.html

提交回复
热议问题