Autenticação de certificado do cliente Java HTTPS

222

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

Estou escrevendo um cliente Java que precisa fazer um simples POSTdado para um determinado URL. Essa parte funciona bem, o único problema é que deve ser resolvida HTTPS. A HTTPSparte é bastante fácil de manusear (com HTTPclientou usando o HTTPSsuporte interno do Java ), mas estou preso na autenticação com certificados de cliente. Percebi que já existe uma pergunta muito semelhante aqui, que ainda não testei com o meu código (o fará em breve). Meu problema atual é que - o que quer que eu faça - o cliente Java nunca envia o certificado (posso verificar isso com PCAPdespejos).

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

Como eu disse antes, sou iniciante HTTPS/SSL/TLSe gostaria de receber algumas informações básicas (não precisa ser um ensaio; vou aceitar links para bons artigos).

tmbrggmn
fonte
Eu dei dois certificado do cliente como identificar qual deles precisa adicionar no keystore e truststore você poderia por favor me ajude a identificar esta questão como você já tenha passado por semelhante tipo de emissão, esta questão que levantei realmente não ficando idéia do que fazer stackoverflow .com / questions / 61374276 /…
henrycharles em

Respostas:

233

Finalmente consegui resolver todos os problemas, então vou responder minha própria pergunta. Essas são as configurações / arquivos que eu usei para gerenciar para resolver meus problemas específicos;

O keystore do cliente é um arquivo no formato PKCS # 12 que contém

  1. O certificado público do cliente (nesse caso, assinado por uma CA autoassinada)
  2. A chave privada do cliente

Para gerá-lo, usei o pkcs12comando 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 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 a autenticação do cliente. Consulte 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 armazenamento confiável do cliente é um arquivo de formato JKS direto que contém os certificados CA raiz ou intermediários . Esses certificados de autoridade de certificação determinarão com quais pontos de extremidade você poderá se comunicar; nesse caso, permitirá que seu cliente se conecte a qualquer servidor que apresente um certificado que foi assinado por uma das autoridades de certificação do armazenamento confiável.

Para gerá-lo, você pode usar a ferramenta chave do 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á executar 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ê também deseja configurar um servidor, ele precisa de seus próprios arquivos de armazenamento de chaves e de confiança. Um ótimo passo a passo para configurar um exemplo completo de trabalho para um cliente e servidor Java (usando o Tomcat) pode ser encontrado neste site .

Questões / Comentários / Dicas

  1. A autenticação de certificado de cliente pode ser aplicada apenas pelo servidor.
  2. ( Importante! ) Quando o servidor solicita um certificado de cliente (como parte do handshake TLS), ele também fornece uma lista de CAs confiáveis ​​como parte da solicitação de certificado. Quando o certificado de cliente que você deseja apresentar para autenticação não for assinado por uma dessas autoridades de certificação, ele não será apresentado (na minha opinião, esse é um 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 o servidor corretamente para aceitar meu certificado de cliente autoassinado e presumimos que o problema estava ao meu alcance por não fornecer corretamente o certificado de cliente na solicitação.
  3. Obtenha o Wireshark. Possui excelente análise de pacotes SSL / HTTPS e será uma tremenda ajuda para depurar e encontrar o problema. É semelhante, -Djavax.net.debug=sslmas é mais estruturado e (sem dúvida) mais fácil de interpretar se você não se sentir confortável com a saída de depuração SSL do Java.
  4. É perfeitamente possível usar a biblioteca httpclient Apache. Se você deseja usar o httpclient, substitua o URL de destino pelo equivalente HTTPS e adicione os seguintes argumentos da JVM (que são iguais para qualquer outro cliente, independentemente da biblioteca que você deseja usar para enviar / receber dados por 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
tmbrggmn
fonte
6
"Quando o certificado do cliente que você deseja apresentar para autenticação não for assinado por uma dessas autoridades de certificação, ele não será apresentado". Os certificados não são apresentados porque o cliente sabe que não será aceito pelo servidor. Além disso, seu certificado pode ser assinado por uma CA "ICA" intermediária e o servidor pode apresentar ao seu cliente a CA "RCA" raiz, e seu navegador da Web ainda permitirá que você escolha seu certificado, mesmo que seja assinado pela ICA e não pela RCA.
KyleM
2
Como exemplo do comentário acima, considere uma situação em que você tem uma CA raiz (RCA1) e duas CAs intermediárias (ICA1 e ICA2). No Apache Tomcat, se você importar o RCA1 para o armazenamento confiável, seu navegador apresentará TODOS os certificados assinados por ICA1 e ICA2, mesmo que não estejam no seu armazenamento confiável. Isso ocorre porque é a cadeia que não importa os certificados individuais.
KyleM
2
"na minha opinião, esse é um comportamento estranho, mas tenho certeza de que há uma razão para isso". A razão disso é que é o que diz a RFC 2246. Nada de estranho nisso. Permitir que os clientes apresentem certificados que não serão aceitos pelo servidor é o que seria estranho e um completo desperdício de tempo e espaço.
Marquês de Lorne
1
Eu recomendo o uso de um único keystore da JVM (e armazenamento confiável!), Como no exemplo acima. A personalização de uma conexão única é mais segura e flexível, mas requer que você escreva um pouco mais de código. Você precisa personalizar o SSLContextque está na resposta de @ Magnus.
Christopher Schultz
63

Outras respostas mostram como configurar globalmente certificados de cliente. No entanto, se você desejar definir programaticamente a chave do cliente para uma conexão específica, em vez de defini-la globalmente em todos os aplicativos em execução na sua JVM, poderá configurar 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"));
Magnus
fonte
Eu tive que usar sslContext = SSLContexts.custom().loadTrustMaterial(keyFile, PASSWORD).build();. Não consegui trabalhar com isso loadKeyMaterial(...).
Conor Svensson
4
O material @ConorSvensson Trust é para o cliente que confia no certificado do servidor remoto, o material principal é para o servidor que confia no cliente.
Magnus
1
Eu realmente gosto dessa resposta concisa e pontual. Caso as pessoas estejam interessadas, eu forneço um exemplo aqui, com instruções de construção. stackoverflow.com/a/46821977/154527
Alain O'Dea
3
Você salvou o meu dia! Apenas uma alteração extra que eu tive que fazer, envie a senha no loadKeyMaterial(keystore, keyPassphrase.toCharArray())código 'também!
AnandShanbhag 26/0318
1
@ Peterh Sim, é específico para o Apache http. Toda biblioteca HTTP terá sua própria maneira de ser configurada, mas a maioria delas deve usar um SSLContext.
Magnus
30

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 estarão localizadas aqui:

  • O cliente loja 's irá conter o cliente privado e público par de chaves. É chamado de keystore .
  • O repositório do servidor conterá a chave pública do cliente . É chamado de armazenamento confiável .

A separação do armazenamento confiável 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, para copiá-lo para o servidor, use

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
mhaller
fonte
Eu provavelmente deveria mencionar que não tenho controle sobre o servidor. O servidor importou nosso certificado público. Os administradores desse sistema informaram que preciso fornecer explicitamente o certificado para que ele possa ser enviado durante o handshake (o servidor solicita isso explicitamente).
tmbrggmn
Você precisará de chaves públicas e privadas para o seu certificado público (aquele conhecido pelo servidor) como um arquivo JKS.
Sfussenegger 03/11/2009
Obrigado pelo código de exemplo. No código acima, o que é "mykey-public.cer" exatamente? Esse é o certificado público do cliente (usamos certificados autoassinados)?
tmbrggmn
Sim, renomeei os arquivos nos trechos de código de acordo. Espero que isso deixe claro.
mhaller
Direito, obrigado. Fico confuso porque aparentemente "chave" e "certificado" são usados ​​de forma intercambiável.
tmbrggmn
10

Maven pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>some.examples</groupId>
    <artifactId>sslcliauth</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>sslcliauth</name>
    <dependencies>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.4</version>
        </dependency>
    </dependencies>
</project>

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

}
kinjelom
fonte
Se você preferir que o certificado esteja disponível para todos os aplicativos que usam uma instalação específica da JVM, siga esta resposta .
ADTC
é o método configureRequest()para definir o proxy do projeto do cliente, certo?
shareef 29/02
sim, é http configuração do cliente e é de configuração do proxy neste caso
kinjelom
talvez eu tenha uma pergunta idiota, mas como faço para criar um arquivo cacert.jks? Eu tenho Exception Exception in thread "main" java.io.FileNotFoundException: \ cacert.jks (O sistema não pode encontrar o arquivo especificado).
Patlatus
6

Para aqueles que simplesmente desejam configurar uma autenticação bidirecional (certificados de servidor e cliente), uma combinação desses dois links o levará 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 -novo -x509 -days 365 -key ca.key -out ca.crt

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

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

  • $ openssl req -novo -key server.key -out server.csr

  • $ openssl x509 -req -days 365 -in server.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 -em client.csr -CA ca.crt -CAkey ca.key -set_serial 101 -out client.crt

De resto, siga as etapas no link. O gerenciamento dos certificados para o Chrome funciona da mesma maneira que no exemplo mencionado para o Firefox.

Em seguida, configure o servidor via:

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

Observe que você já criou o servidor .crt e .key para não precisar mais executar essa etapa.

Hans
fonte
1
Pense que você tem erro de digitação na etapa geração CSR servidor: deve usar server.keynãoclient.key
the.Legend
0

Conectei-me ao banco com SSL bidirecional (certificado de cliente e servidor) com o Spring Boot. Portanto, descreva aqui todas as minhas etapas, espero que ajude alguém (a solução de trabalho mais simples, eu encontrei):

  1. Gere solicitação de certificado:

    • Gere chave privada:

      openssl genrsa -des3 -passout pass:MY_PASSWORD -out user.key 2048
    • Gere solicitação de certificado:

      openssl req -new -key user.key -out user.csr -passin pass:MY_PASSWORD

    Manter user.key(e senha) e enviar solicitação de certificado user.csrao banco para meu certificado

  2. Receber 2 certificados: meu certificado raiz do cliente clientId.crte certificado raiz do banco:bank.crt

  3. Crie o keystore Java (digite a senha da chave e configure a senha do keystore):

    openssl pkcs12 -export -in clientId.crt -inkey user.key -out keystore.p12 -name clientId -CAfile ca.crt -caname root

    Não preste atenção na saída: unable to write 'random state'. Java PKCS12 keystore.p12criado.

  4. Adicionar ao keystore bank.crt(para simplificar, usei um keystore):

    keytool -import -alias banktestca -file banktestca.crt -keystore keystore.p12 -storepass javaops

    Verifique os certificados de keystore:

    keytool -list -keystore keystore.p12
  5. Pronto para o código Java :) Eu usei o Spring Boot RestTemplatecom org.apache.httpcomponents.httpcoredependência adicional :

    @Bean("sslRestTemplate")
    public RestTemplate sslRestTemplate() throws Exception {
      char[] storePassword = appProperties.getSslStorePassword().toCharArray();
      URL keyStore = new URL(appProperties.getSslStore());
    
      SSLContext sslContext = new SSLContextBuilder()
            .loadTrustMaterial(keyStore, storePassword)
      // use storePassword twice (with key password do not work)!!
            .loadKeyMaterial(keyStore, storePassword, storePassword) 
            .build();
    
      // Solve "Certificate doesn't match any of the subject alternative names"
      SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
    
      CloseableHttpClient client = HttpClients.custom().setSSLSocketFactory(socketFactory).build();
      HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(client);
      RestTemplate restTemplate = new RestTemplate(factory);
      // restTemplate.setMessageConverters(List.of(new Jaxb2RootElementHttpMessageConverter()));
      return restTemplate;
    }
Grigory Kislin
fonte
Você pode fazer tudo isso com o keytool. Não há necessidade de OpenSSL nisso.
Marquês de Lorne,
0

Dado um arquivo p12 com o certificado e a chave privada (gerada pelo openssl, por exemplo), o código a seguir o utilizará para uma HttpsURLConnection específica:

    KeyStore keyStore = KeyStore.getInstance("pkcs12");
    keyStore.load(new FileInputStream(keyStorePath), keystorePassword.toCharArray());
    KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
    kmf.init(keyStore, keystorePassword.toCharArray());
    SSLContext ctx = SSLContext.getInstance("TLS");
    ctx.init(kmf.getKeyManagers(), null, null);
    SSLSocketFactory sslSocketFactory = ctx.getSocketFactory();

    HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
    connection.setSSLSocketFactory(sslSocketFactory);

A SSLContextleva algum tempo para inicializar, então você pode querer cache-lo.

Johannes Brodwall
fonte
-1

Acho que a correção aqui foi 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 se está representando um cliente do outro lado.

Obi wan Kenobi
fonte
1
O formato PKCS12 era tradicionalmente usado para chave-privada-e-certificado, mas o Java desde 8 em 2014 (mais de um ano antes desta resposta) suporta o PKCS12 que contém certificados ou chaves privadas. Independentemente do formato do keystore, a autenticação do cliente requer privatekey-AND-cert. Não entendo sua segunda frase, mas o cliente Java pode selecionar automaticamente uma chave e certificado de cliente se pelo menos uma entrada adequada estiver disponível ou um gerenciador de chaves puder ser configurado para usar uma especificada.
Dave_thompson_085 14/05/19
Sua segunda frase está totalmente incorreta. O servidor fornece seus assinantes confiáveis ​​e o cliente morre ou não fornece um certificado que satisfaça essa restrição. Automaticamente, não via 'in your code'. Essa é a 'maneira de o servidor saber que está representando um cliente'.
Marquês de Lorne