porque é que o java não envia o certificado de cliente durante o aperto de mão SSL?

Estou a tentar ligar-me a um serviço web seguro.

Estava a ter uma falha no aperto de mão, apesar de o meu teclado e a loja terem sido correctamente definidos.

Depois de vários dias de frustração, pesquisa interminável e perguntar a todos ao redor eu descobri que o único problema era que java optou por não enviar o certificado de cliente para o servidor durante o aperto de mão.

especificamente:

  1. o servidor solicitou um certificado de cliente (CN=RootCA) - ou seja, "dá-me uma certeza que é assinada pela raiz CA"
  2. Java olhou para o teclado e só encontrou o certificado do meu cliente que é assinado pela" SubCA", que por sua vez é emitido pela"RootCA". Não se deu ao trabalho de olhar para a loja...está bem, acho eu. Infelizmente, quando tentei adicionar o certificado "SubCA" ao teclado, isso não ajudou em nada. Eu verifiquei se os certificados são carregados no teclado. Sim, mas o KeyManager ignora todos os certificados excepto o cliente um.
  3. Tudo o que precede leva ao fato de que java decide que não tem nenhum certificado que satisfaça a solicitação do servidor e não envia nada...falha no aperto de mão tadaaa: - ([11]}

As minhas perguntas:

    É possível que tenha adicionado o certificado " SubCA "ao teclado de uma forma que" quebrou a cadeia de certificados " ou algo assim para que o KeyManager só carregue o certificado do cliente e ignore o resto? (Chrome e openssl conseguem descobrir isso assim porque não pode java? - note que o certificado "SubCA" é sempre apresentado separadamente como a Autoridade de confiança, por isso o Chrome aparentemente embala-o correctamente juntamente com o certificado do cliente durante o aperto de mão)
  1. isto é um "problema de configuração" formal do lado do servidor? O servidor é um terceiro. Eu esperaria que o servidor pedisse um certificado assinado pela Autoridade "Subma", já que foi isso que eles nos forneceram. Eu suspeito que o fato de que isso funciona em Chrome e openssl é porque eles são " menos restritivo "e java apenas vai" de acordo com as regras " e falha.
Eu consegui montar um trabalho sujo para isto, mas não estou muito feliz com isso então eu vou ficar feliz se alguém pode esclarecer este para mim.

Author: Camilo Martinez, 2012-02-15

4 answers

É possível que tenha importado o certificado de AC intermediário para o teclado sem o associar à entrada onde tem o certificado de cliente e a sua chave privada. Você deve ser capaz de ver isso usando keytool -v -list -keystore store.jks. Se você só conseguir um certificado por entrada alias, eles não estão juntos. Teria de importar o seu certificado e a sua cadeia juntos para o nome falso que tem a sua chave privada.

Para descobrir qual é o pseudónimo Keystone a chave privada, use keytool -list -keystore store.jks (Estou assumindo o tipo de loja JKS aqui). Isto vai dizer - te algo assim:

Your keystore contains 1 entry

myalias, Feb 15, 2012, PrivateKeyEntry, 
Certificate fingerprint (MD5): xxxxxxxx
Aqui, o nome falso é myalias. Se utilizar -v para além disto, deve ver Alias Name: myalias.

Se não o tiver já separadamente, exporte o seu certificado de cliente a partir do teclado:

keytool -exportcert -rfc -file clientcert.pem -keystore store.jks -alias myalias
Isto deve dar-te um ficheiro PEM.

Usando um editor de texto (ou cat), prepare o ficheiro (vamos chamá-lo bundle.pem) com o certificado do cliente e o intermediário Certificado de AC (e possivelmente o certificado de root CA em si, se você quiser), de modo que o certificado de cliente está no início e seu certificado de Emissor está logo abaixo.

Isto deve parecer como:

-----BEGIN CERTIFICATE-----
MIICajCCAdOgAwIBAgIBAjANBgkqhkiG9w0BAQUFADA7MQswCQYDVQQGEwJVSzEa
....
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICkjCCAfugAwIBAgIJAKm5bDEMxZd7MA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNV
....
-----END CERTIFICATE-----
Agora, importem este pacote de volta para o nome falso onde a vossa chave privada é:
keytool -importcert -keystore store.jks -alias myalias -file bundle.pem
 102
Author: Bruno, 2015-11-04 10:25:58

Como uma adição aqui, poderá usar % > OpenSSL S_ Client-connect host. exemplo. com: 443 e ver o 'dump' e verificar se todo o certificado principal é válido para o cliente. Você está procurando por isso na parte inferior da saída. Verificar o código de retorno: 0 (ok)

Se adicionar - showcerts , irá descarregar toda a informação do chaveiro que foi enviado juntamente com o certificado da máquina, que é o que carregou no seu chaveiro.

 7
Author: tls, 2017-11-22 13:45:50
A maioria das soluções que vi giram em torno do uso do teclado e nenhuma delas correspondia ao meu caso. Aqui está uma breve descrição: eu tenho um PKCS12 (.p12) que funciona bem no carteiro com verificação de certificado desativado, no entanto programaticamente eu sempre acabou obtendo erro do servidor "400 Bad Request" / "nenhum certificado SSL necessário foi enviado".

a razão foi a falta de uma extensão TLS SNI (indicação do nome do servidor) e a seguir é o solucao.


Adicionar uma extensão / parâmetro ao contexto SSL

Após a introdução do SSLContext, adicione o seguinte:

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)

Nota 1: Sslcontextextexception and KeyStoreFactoryException simply extend RuntimeException.

Nota 2: as validações dos certificados estão desactivadas, este exemplo destinava-se apenas à utilização do dev.

Nota 3: a desactivação da verificação do nome da máquina não foi necessária no meu caso, mas eu incluiu-o como uma linha comentada

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());
    }

}

Usando o construtor do cliente acima

Nota 1: keystore (.p12) é procurado na pasta" Recursos".

Nota 2: o cabeçalho "máquina" está definido para evitar o erro do servidor "400 - má solicitação".

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;

Docs de Referência

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

 0
Author: Alex Ureche, 2020-08-10 09:05:36

A coisa é que ao usar o certificado cliente assinado por um intermediário você precisa incluir o intermediário em seu trustore para que java possa encontrá-lo. Seja solo ou como um pacote com a ca emissora de raiz.

 -1
Author: IgorC, 2019-02-06 06:59:25