Autenticação de certificado de cliente HTTPS do Java

Sou relativamente novo em HTTPS / SSL / TLS e estou um pouco confuso sobre o que exatamente os clientes devem apresentar ao autenticar com certificados.

Eu estou escrevendo um cliente Java que precisa fazer um simples POST de dados para uma determinada URL. Essa parte funciona bem, o único problema é que deve ser feito através de HTTPS. A parte HTTPS é bastante fácil de manipular (com HTTPclient ou usando o suporte HTTPS embutido do Java), mas estou preso na autenticação com certificados de cliente. Eu notei que já existe uma pergunta muito semelhante aqui, que eu ainda não experimentei com o meu código (irá fazê-lo em breve). Meu problema atual é que – o que quer que eu faça – o cliente Java nunca envia o certificado (eu posso verificar isso com os dumps do PCAP).

Gostaria de saber exatamente o que o cliente deve apresentar ao servidor ao autenticar com certificados (especificamente para Java – se isso realmente importa)? Este é um arquivo JKS ou PKCS # 12? O que deveria estar neles? apenas o certificado do cliente ou uma chave? Se sim, qual tecla? Há um pouco de confusão sobre todos os diferentes tipos de arquivos, tipos de certificados e tal.

Como eu disse antes, sou novo em HTTPS / SSL / TLS, então eu gostaria de receber algumas informações básicas (não precisa ser uma redação; vou me contentar com links para bons artigos).

Finalmente consegui resolver todos os problemas, então responderei minha própria pergunta. Estas são as configurações / arquivos que usei para gerenciar meu problema (s) resolvido (s);

O keystore do cliente é um arquivo no formato PKCS # 12 contendo

  1. O certificado público do cliente (neste caso, assinado por uma CA auto-assinada)
  2. A chave privada do cliente

Para gerá-lo eu usei o comando pkcs12 do OpenSSL, por exemplo;

 openssl pkcs12 -export -in client.crt -inkey client.key -out client.p12 -name "Whatever" 

Dica: certifique-se de obter o OpenSSL mais recente, não a versão 0.9.8h, porque isso parece sofrer de um bug que não permite gerar corretamente os arquivos PKCS # 12.

Esse arquivo PKCS # 12 será usado pelo cliente Java para apresentar o certificado do cliente ao servidor quando o servidor solicitar explicitamente que o cliente autentique. Veja o artigo da Wikipedia sobre TLS para obter uma visão geral de como o protocolo para autenticação de certificado de cliente realmente funciona (também explica por que precisamos da chave privada do cliente aqui).

O truststore do cliente é um arquivo de formato JKS direto que contém os certificados raiz ou CA intermediários . Esses certificados de autoridade de certificação determinarão com quais endpoints você poderá se comunicar, neste caso, permitirá que seu cliente se conecte a qualquer servidor que apresente um certificado assinado por uma das CAs do truststore.

Para gerá-lo, você pode usar o keytool Java padrão, por exemplo;

 keytool -genkey -dname "cn=CLIENT" -alias truststorekey -keyalg RSA -keystore ./client-truststore.jks -keypass whatever -storepass whatever keytool -import -keystore ./client-truststore.jks -file myca.crt -alias myca 

Usando esse armazenamento confiável, seu cliente tentará fazer um handshake SSL completo com todos os servidores que apresentarem um certificado assinado pela CA identificado por myca.crt .

Os arquivos acima são estritamente apenas para o cliente. Quando você deseja configurar um servidor também, o servidor precisa de seus próprios arquivos de chave e armazenamento confiável. Uma ótima explicação para configurar um exemplo totalmente funcional para um cliente e servidor Java (usando o Tomcat) pode ser encontrada neste site .

Questões / Observações / Dicas

  1. A autenticação de certificado de cliente só pode ser aplicada pelo servidor.
  2. ( Importante! ) Quando o servidor solicita um certificado de cliente (como parte do handshake de TLS), ele também fornece uma lista de CAs confiáveis ​​como parte da solicitação de certificado. Quando o certificado do cliente que você deseja apresentar para autenticação não é assinado por um desses CAs, ele não será apresentado (na minha opinião, esse comportamento é estranho, mas tenho certeza de que há uma razão para isso). Essa foi a principal causa dos meus problemas, pois a outra parte não havia configurado seu servidor adequadamente para aceitar meu certificado de cliente auto-assinado e presumimos que o problema estava no meu fim de não fornecer corretamente o certificado do cliente na solicitação.
  3. Obtenha o Wireshark. Ele tem uma ótima análise de pacotes SSL / HTTPS e será uma grande ajuda na debugging e na localização do problema. É semelhante a -Djavax.net.debug=ssl mas é mais estruturado e (possivelmente) mais fácil de interpretar se você não estiver à vontade com a saída de debugging SSL do Java.
  4. É perfeitamente possível usar a biblioteca httpclient do Apache. Se você quiser usar httpclient, basta replace a URL de destino pelo equivalente HTTPS e include os seguintes argumentos da JVM (que são os mesmos para qualquer outro cliente, independentemente da biblioteca que você deseja usar para enviar / receber dados via HTTP / HTTPS) :

     -Djavax.net.debug=ssl -Djavax.net.ssl.keyStoreType=pkcs12 -Djavax.net.ssl.keyStore=client.p12 -Djavax.net.ssl.keyStorePassword=whatever -Djavax.net.ssl.trustStoreType=jks -Djavax.net.ssl.trustStore=client-truststore.jks -Djavax.net.ssl.trustStorePassword=whatever 

Outras respostas mostram como configurar globalmente certificados de clientes. No entanto, se você quiser definir programaticamente a chave do cliente para uma conexão específica, em vez de defini-la globalmente em cada aplicativo em execução na sua JVM, configure seu próprio SSLContext da seguinte maneira:

 String keyPassphrase = ""; KeyStore keyStore = KeyStore.getInstance("PKCS12"); keyStore.load(new FileInputStream("cert-key-pair.pfx"), keyPassphrase.toCharArray()); SSLContext sslContext = SSLContexts.custom() .loadKeyMaterial(keyStore, null) .build(); HttpClient httpClient = HttpClients.custom().setSSLContext(sslContext).build(); HttpResponse response = httpClient.execute(new HttpGet("https://example.com")); 

O arquivo JKS é apenas um contêiner para certificados e pares de chaves. Em um cenário de autenticação do lado do cliente, as várias partes das chaves serão localizadas aqui:

  • A loja do cliente conterá o par de chaves privada e pública do cliente. É chamado de keystore .
  • A loja do servidor conterá a chave pública do cliente. É chamado de truststore .

A separação de truststore e keystore não é obrigatória, mas recomendada. Eles podem ser o mesmo arquivo físico.

Para definir os locais do sistema de arquivos dos dois armazenamentos, use as seguintes propriedades do sistema:

 -Djavax.net.ssl.keyStore=clientsidestore.jks 

e no servidor:

 -Djavax.net.ssl.trustStore=serversidestore.jks 

Para exportar o certificado do cliente (chave pública) para um arquivo, você pode copiá-lo para o servidor, usar

 keytool -export -alias MYKEY -file publicclientkey.cer -store clientsidestore.jks 

Para importar a chave pública do cliente para o keystore do servidor, use (como o pôster mencionado, isso já foi feito pelos administradores do servidor)

 keytool -import -file publicclientkey.cer -store serversidestore.jks 

Maven pom.xml:

 < ?xml version="1.0" encoding="UTF-8"?>  4.0.0 some.examples sslcliauth 1.0-SNAPSHOT jar sslcliauth   org.apache.httpcomponents httpclient 4.4    

Código Java:

 package some.examples; import java.io.FileInputStream; import java.io.IOException; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.util.logging.Level; import java.util.logging.Logger; import javax.net.ssl.SSLContext; import org.apache.http.HttpEntity; import org.apache.http.HttpHost; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.ssl.SSLContexts; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import org.apache.http.entity.InputStreamEntity; public class SSLCliAuthExample { private static final Logger LOG = Logger.getLogger(SSLCliAuthExample.class.getName()); private static final String CA_KEYSTORE_TYPE = KeyStore.getDefaultType(); //"JKS"; private static final String CA_KEYSTORE_PATH = "./cacert.jks"; private static final String CA_KEYSTORE_PASS = "changeit"; private static final String CLIENT_KEYSTORE_TYPE = "PKCS12"; private static final String CLIENT_KEYSTORE_PATH = "./client.p12"; private static final String CLIENT_KEYSTORE_PASS = "changeit"; public static void main(String[] args) throws Exception { requestTimestamp(); } public final static void requestTimestamp() throws Exception { SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory( createSslCustomContext(), new String[]{"TLSv1"}, // Allow TLSv1 protocol only null, SSLConnectionSocketFactory.getDefaultHostnameVerifier()); try (CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(csf).build()) { HttpPost req = new HttpPost("https://changeit.com/changeit"); req.setConfig(configureRequest()); HttpEntity ent = new InputStreamEntity(new FileInputStream("./bytes.bin")); req.setEntity(ent); try (CloseableHttpResponse response = httpclient.execute(req)) { HttpEntity entity = response.getEntity(); LOG.log(Level.INFO, "*** Reponse status: {0}", response.getStatusLine()); EntityUtils.consume(entity); LOG.log(Level.INFO, "*** Response entity: {0}", entity.toString()); } } } public static RequestConfig configureRequest() { HttpHost proxy = new HttpHost("changeit.local", 8080, "http"); RequestConfig config = RequestConfig.custom() .setProxy(proxy) .build(); return config; } public static SSLContext createSslCustomContext() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, KeyManagementException, UnrecoverableKeyException { // Trusted CA keystore KeyStore tks = KeyStore.getInstance(CA_KEYSTORE_TYPE); tks.load(new FileInputStream(CA_KEYSTORE_PATH), CA_KEYSTORE_PASS.toCharArray()); // Client keystore KeyStore cks = KeyStore.getInstance(CLIENT_KEYSTORE_TYPE); cks.load(new FileInputStream(CLIENT_KEYSTORE_PATH), CLIENT_KEYSTORE_PASS.toCharArray()); SSLContext sslcontext = SSLContexts.custom() //.loadTrustMaterial(tks, new TrustSelfSignedStrategy()) // use it to customize .loadKeyMaterial(cks, CLIENT_KEYSTORE_PASS.toCharArray()) // load client certificate .build(); return sslcontext; } } 

Para aqueles que simplesmente desejam configurar uma autenticação bidirecional (certificados de servidor e cliente), uma combinação desses dois links levará você até lá:

Configuração de autenticação bidirecional:

https://linuxconfig.org/apache-web-server-ssl-authentication

Você não precisa usar o arquivo de configuração openssl que eles mencionam; Apenas use

  • $ openssl genrsa -des3 -out ca.key 4096

  • $ openssl req -new -x509 -days 365 -key ca.key -out ca.crt

para gerar seu próprio certificado de CA e, em seguida, gerar e assinar as chaves do servidor e do cliente por meio de:

  • $ openssl genrsa -des3 -out server.key 4096

  • $ openssl req -new -key client.key -out server.csr

  • $ openssl x509 -req -days 365 -no servidor.csr -CA ca.crt -CAkey ca.key -set_serial 100 -out server.crt

e

  • $ openssl genrsa -des3 -out client.key 4096

  • $ openssl req -new -key client.key -out client.csr

  • $ openssl x509 -req -days 365 -no cliente.csr -CA ca.crt -CAkey ca.key -set_serial 101 -out client.crt

Para o resto, siga os passos no link. O gerenciamento dos certificados para o Chrome funciona da mesma forma que no exemplo do firefox mencionado.

Em seguida, configure o servidor via:

https://www.digitalocean.com/community/tutorials/how-to-create-a-ssl-certificate-on-apache-for-ubuntu-14-04

Note que você já criou o servidor .crt e .key para não ter que fazer mais essa etapa.

Acho que a correção aqui era do tipo keystore, pkcs12 (pfx) sempre tem chave privada e o tipo JKS pode existir sem chave privada. A menos que você especifique em seu código ou selecione um certificado através do navegador, o servidor não tem como saber que está representando um cliente do outro lado.