Certificados de cliente Java em HTTPS / SSL

116

Estou usando o Java 6 e tentando criar um HttpsURLConnectionem um servidor remoto, usando um certificado de cliente.
O servidor está usando um certificado raiz autoassinado e requer a apresentação de um certificado de cliente protegido por senha. Eu adicionei o certificado raiz do servidor e o certificado do cliente a um keystore java padrão que encontrei em /System/Library/Frameworks/JavaVM.framework/Versions/1.6.0/Home/lib/security/cacerts(OSX 10.5). O nome do arquivo de armazenamento de chave parece sugerir que o certificado do cliente não deve ir lá.

De qualquer forma, adicionar o certificado raiz a esta loja resolveu o infame javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed' problem.

No entanto, agora estou sem saber como usar o certificado do cliente. Tentei duas abordagens e nenhuma me levou a lugar nenhum.
Primeiro, e de preferência, tente:

SSLSocketFactory sslsocketfactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
URL url = new URL("https://somehost.dk:3049");
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
conn.setSSLSocketFactory(sslsocketfactory);
InputStream inputstream = conn.getInputStream();
// The last line fails, and gives:
// javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure

Tentei pular a classe HttpsURLConnection (não é o ideal, pois quero falar HTTP com o servidor) e, em vez disso:

SSLSocketFactory sslsocketfactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
SSLSocket sslsocket = (SSLSocket) sslsocketfactory.createSocket("somehost.dk", 3049);
InputStream inputstream = sslsocket.getInputStream();
// do anything with the inputstream results in:
// java.net.SocketTimeoutException: Read timed out

Eu nem tenho certeza se o certificado do cliente é o problema aqui.

Jan
fonte
Eu dei dois certificados do cliente para identificar qual deles precisa adicionar no keystore e no truststore. Você poderia ajudar a identificar esse problema, visto que você já passou por um tipo de problema semelhante stackoverflow.com/questions/61374276/…
henrycharles

Respostas:

100

Finalmente resolvi;). Tenho uma dica forte aqui (a resposta de Gandalf também tocou um pouco nisso). Os links ausentes eram (principalmente) o primeiro dos parâmetros abaixo e, até certo ponto, esqueci a diferença entre keystores e truststores.

O certificado do servidor autoassinado deve ser importado para um armazenamento confiável:

keytool -import -alias gridserver -file gridserver.crt -storepass $ PASS -keystore gridserver.keystore

Essas propriedades precisam ser definidas (na linha de comando ou no código):

-Djavax.net.ssl.keyStoreType=pkcs12
-Djavax.net.ssl.trustStoreType=jks
-Djavax.net.ssl.keyStore=clientcertificate.p12
-Djavax.net.ssl.trustStore=gridserver.keystore
-Djavax.net.debug=ssl # very verbose debug
-Djavax.net.ssl.keyStorePassword=$PASS
-Djavax.net.ssl.trustStorePassword=$PASS

Código de exemplo de trabalho:

SSLSocketFactory sslsocketfactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
URL url = new URL("https://gridserver:3049/cgi-bin/ls.py");
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
conn.setSSLSocketFactory(sslsocketfactory);
InputStream inputstream = conn.getInputStream();
InputStreamReader inputstreamreader = new InputStreamReader(inputstream);
BufferedReader bufferedreader = new BufferedReader(inputstreamreader);

String string = null;
while ((string = bufferedreader.readLine()) != null) {
    System.out.println("Received " + string);
}
Jan
fonte
Eu usei um url como: localhost: 8443 / Application_Name / getAttributes . Eu tenho um método com mapeamento de url / getAttribute. Este método retorna uma lista de elementos. Eu usei HttpsUrlConnection, o código de resposta de conexão é 200, mas ele não me dá a lista de atributos quando uso inputStream, ele me dá o conteúdo html da minha página de login. Eu fiz a autenticação e defini o tipo de conteúdo como JSON. Por favor, sugira
Deepak
83

Embora não seja recomendado, você também pode desativar a validação do certificado SSL por completo:

import javax.net.ssl.*;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;

public class SSLTool {

  public static void disableCertificateValidation() {
    // Create a trust manager that does not validate certificate chains
    TrustManager[] trustAllCerts = new TrustManager[] { 
      new X509TrustManager() {
        public X509Certificate[] getAcceptedIssuers() { 
          return new X509Certificate[0]; 
        }
        public void checkClientTrusted(X509Certificate[] certs, String authType) {}
        public void checkServerTrusted(X509Certificate[] certs, String authType) {}
    }};

    // Ignore differences between given hostname and certificate hostname
    HostnameVerifier hv = new HostnameVerifier() {
      public boolean verify(String hostname, SSLSession session) { return true; }
    };

    // Install the all-trusting trust manager
    try {
      SSLContext sc = SSLContext.getInstance("SSL");
      sc.init(null, trustAllCerts, new SecureRandom());
      HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
      HttpsURLConnection.setDefaultHostnameVerifier(hv);
    } catch (Exception e) {}
  }
}
neu242
fonte
72
Deve-se notar que desabilitar a validação de certificado como este abre a conexão para possíveis ataques MITM: não use em produção .
Bruno
3
O código não compila, felizmente. Esta 'solução' é radicalmente insegura.
Marquês de Lorne
5
@ neu242, não, realmente não depende de para que você o usa. Se você deseja usar SSL / TLS, deseja proteger sua conexão contra ataques MITM, esse é o ponto principal. A autenticação do servidor não é necessária se você puder garantir que ninguém será capaz de alterar o tráfego, mas situações em que você suspeita que pode haver bisbilhoteiros que também não estariam em posição de alterar o tráfego da rede são bastante raras.
Bruno
1
@ neu242 Obrigado pelo snippet de código. Na verdade, estou pensando em usar isso na produção para uma finalidade muito específica (rastreamento da web) e mencionei sua implementação em uma pergunta ( stackoverflow.com/questions/13076511/… ). Se você tiver tempo, poderia dar uma olhada e me informar se eu perdi algum risco de segurança.
Sal
1
@Bruno Se eu estiver apenas fazendo ping no servidor, isso realmente me afetaria, por ataques MITM?
PhoonOne,
21

Você definiu as propriedades do sistema KeyStore e / ou TrustStore?

java -Djavax.net.ssl.keyStore=pathToKeystore -Djavax.net.ssl.keyStorePassword=123456

ou de com o código

System.setProperty("javax.net.ssl.keyStore", pathToKeyStore);

Mesmo com javax.net.ssl.trustStore

Gandalf
fonte
13

Se você estiver lidando com uma chamada de serviço da Web usando a estrutura Axis, há uma resposta muito mais simples. Se tudo que você deseja é que seu cliente possa chamar o serviço da web SSL e ignorar os erros do certificado SSL, basta colocar esta instrução antes de invocar qualquer serviço da web:

System.setProperty("axis.socketSecureFactory", "org.apache.axis.components.net.SunFakeTrustSocketFactory");

As isenções de responsabilidade usuais sobre isso ser uma coisa muito ruim de se fazer em um ambiente de produção se aplicam.

Eu encontrei isso no wiki do Axis .

Mark Meuer
fonte
O OP está lidando com uma HttpsURLConnection, não Axis
neu242
2
Compreendo. Não tive a intenção de sugerir que minha resposta fosse melhor no caso geral. Acontece que, se você estiver usando a estrutura Axis, pode ter a pergunta do OP nesse contexto. (Foi assim que encontrei esta questão em primeiro lugar.) Nesse caso, a forma como forneci é mais simples.
Mark Meuer
5

Para mim, isso é o que funcionou usando Apache HttpComponents ~ HttpClient 4.x:

    KeyStore keyStore  = KeyStore.getInstance("PKCS12");
    FileInputStream instream = new FileInputStream(new File("client-p12-keystore.p12"));
    try {
        keyStore.load(instream, "helloworld".toCharArray());
    } finally {
        instream.close();
    }

    // Trust own CA and all self-signed certs
    SSLContext sslcontext = SSLContexts.custom()
        .loadKeyMaterial(keyStore, "helloworld".toCharArray())
        //.loadTrustMaterial(trustStore, new TrustSelfSignedStrategy()) //custom trust store
        .build();
    // Allow TLSv1 protocol only
    SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
        sslcontext,
        new String[] { "TLSv1" },
        null,
        SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); //TODO
    CloseableHttpClient httpclient = HttpClients.custom()
        .setHostnameVerifier(SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER) //TODO
        .setSSLSocketFactory(sslsf)
        .build();
    try {

        HttpGet httpget = new HttpGet("https://localhost:8443/secure/index");

        System.out.println("executing request" + httpget.getRequestLine());

        CloseableHttpResponse response = httpclient.execute(httpget);
        try {
            HttpEntity entity = response.getEntity();

            System.out.println("----------------------------------------");
            System.out.println(response.getStatusLine());
            if (entity != null) {
                System.out.println("Response content length: " + entity.getContentLength());
            }
            EntityUtils.consume(entity);
        } finally {
            response.close();
        }
    } finally {
        httpclient.close();
    }

O arquivo P12 contém o certificado do cliente e a chave privada do cliente, criado com BouncyCastle:

public static byte[] convertPEMToPKCS12(final String keyFile, final String cerFile,
    final String password)
    throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException,
    NoSuchProviderException
{
    // Get the private key
    FileReader reader = new FileReader(keyFile);

    PEMParser pem = new PEMParser(reader);
    PEMKeyPair pemKeyPair = ((PEMKeyPair)pem.readObject());
    JcaPEMKeyConverter jcaPEMKeyConverter = new JcaPEMKeyConverter().setProvider("BC");
    KeyPair keyPair = jcaPEMKeyConverter.getKeyPair(pemKeyPair);

    PrivateKey key = keyPair.getPrivate();

    pem.close();
    reader.close();

    // Get the certificate
    reader = new FileReader(cerFile);
    pem = new PEMParser(reader);

    X509CertificateHolder certHolder = (X509CertificateHolder) pem.readObject();
    java.security.cert.Certificate x509Certificate =
        new JcaX509CertificateConverter().setProvider("BC")
            .getCertificate(certHolder);

    pem.close();
    reader.close();

    // Put them into a PKCS12 keystore and write it to a byte[]
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    KeyStore ks = KeyStore.getInstance("PKCS12", "BC");
    ks.load(null);
    ks.setKeyEntry("key-alias", (Key) key, password.toCharArray(),
        new java.security.cert.Certificate[]{x509Certificate});
    ks.store(bos, password.toCharArray());
    bos.close();
    return bos.toByteArray();
}
EpicPandaForce
fonte
O keyStoreé o que contém a chave privada e certificado.
EpicPandaForce
1
Você precisa incluir estas 2 dependências para que o código convertPEMtoP12 funcione: <dependency> <groupId> org.bouncycastle </groupId> <artifactId> bcprov-jdk15on </artifactId> <version> 1,53 </version> </dependency> < dependency> <groupId> org.bouncycastle </groupId> <artifactId> bcpkix-jdk15on </artifactId> <version> 1,53 </version> </dependency>
BirdOfPrey
@EpicPandaForce Recebo um erro: Caught: org.codehaus.groovy.runtime.typehandling.GroovyCastException: Não é possível lançar o objeto com a classe 'org.bouncycastle.jcajce.provider.asymmetric.x509.X509CertificateObject' para a classe 'int' na linha ks. setKeyEntry - alguma pista do que pode estar errado
Vishal Biyani
Sim, você está usando Groovy em vez de uma linguagem estritamente tipada. (tecnicamente, esse método requer um ID e um certificado, não apenas um certificado)
EpicPandaForce
4

Eu uso o pacote do cliente HTTP Apache commons para fazer isso no meu projeto atual e ele funciona bem com SSL e um certificado autoassinado (após instalá-lo em cacerts como você mencionou). Por favor, dê uma olhada nele:

http://hc.apache.org/httpclient-3.x/tutorial.html

http://hc.apache.org/httpclient-3.x/sslguide.html

Taylor Leese
fonte
1
Este parece ser um pacote muito legal, mas a classe que deve fazer tudo funcionar 'AuthSSLProtocolSocketFactory' aparentemente não faz parte da distribuição oficial, nem no 4.0beta (apesar das notas de lançamento afirmarem que é), ou no 3.1. Eu mudei um pouco com ele e agora pareço estar permanentemente preso em um travamento de 5 minutos antes de simplesmente desligar a conexão. É muito estranho - se eu carregar o CA e o certificado do cliente em qualquer navegador, ele simplesmente voa.
Janeiro de
1
O Apache HTTP Client 4 pode receber um SSLContextdiretamente, então você pode configurar tudo dessa forma, em vez de usar AuthSSLProtocolSocketFactory.
Bruno
1
Existe uma maneira de fazer todo o material do certificado do cliente na memória em vez de por meio de um armazenamento de chaves externo?
Sridhar Sarnobat
4

Acho que você tem um problema com o certificado do servidor, não é um certificado válido (acho que é isso que "handshake_failure" significa neste caso):

Importe o certificado do servidor para o armazenamento de chaves trustcacerts no JRE do cliente. Isso é feito facilmente com o keytool :

keytool
    -import
    -alias <provide_an_alias>
    -file <certificate_file>
    -keystore <your_path_to_jre>/lib/security/cacerts
sourcerebels
fonte
Tentei limpar e começar de novo, e a falha no aperto de mão foi embora. Agora eu só obter 5 minutos de silêncio morto antes de a conexão é encerrada: o
Jan
1

Usando o código abaixo

-Djavax.net.ssl.keyStoreType=pkcs12

ou

System.setProperty("javax.net.ssl.keyStore", pathToKeyStore);

não é necessário. Além disso, não há necessidade de criar sua própria fábrica SSL personalizada.

Eu também encontrei o mesmo problema, no meu caso, houve um problema que a cadeia de certificação completa não foi importada para armazenamentos confiáveis. Importe certificados usando o utilitário keytool diretamente do certificado raiz, você também pode abrir o arquivo cacerts no bloco de notas e ver se a cadeia de certificados completa é importada ou não. Verifique o nome alternativo que você forneceu ao importar certificados, abra os certificados e veja quantos ele contém; o mesmo número de certificados deve estar no arquivo cacerts.

Além disso, o arquivo cacerts deve ser configurado no servidor em que você está executando o aplicativo, os dois servidores se autenticarão com chaves públicas / privadas.

Ankur Singhal
fonte
Criar sua própria fábrica SSL personalizada é muito mais complicado e sujeito a erros do que definir duas propriedades do sistema.
Marquês de Lorne