Autenticación de certificado de cliente HTTPS de Java

222

Soy bastante nuevo HTTPS/SSL/TLSy estoy un poco confundido sobre lo que se supone que deben presentar exactamente los clientes al autenticarse con certificados.

Estoy escribiendo un cliente Java que necesita hacer un simple POSTde datos a un particular URL. Esa parte funciona bien, el único problema es que se supone que debe hacer más HTTPS. La HTTPSparte es bastante fácil de manejar (ya sea con HTTPcliento utilizando el HTTPSsoporte incorporado de Java ), pero estoy atascado en la autenticación con certificados de cliente. He notado que ya hay una pregunta muy similar aquí, que aún no he probado con mi código (lo haré lo suficientemente pronto). Mi problema actual es que, haga lo que haga, el cliente Java nunca envía el certificado (puedo verificarlo con los PCAPvolcados).

¿Me gustaría saber qué se supone que debe presentar exactamente el cliente al servidor cuando se autentica con certificados (específicamente para Java, si eso es importante)? ¿Es esto un JKSarchivo o PKCS#12? Lo que se supone que debe estar en ellos; solo el certificado del cliente o una clave? Si es así, ¿qué clave? Hay bastante confusión sobre todos los diferentes tipos de archivos, tipos de certificados y demás.

Como he dicho antes, soy nuevo, HTTPS/SSL/TLSpor lo que agradecería también algunos antecedentes (no tiene que ser un ensayo; me conformaré con enlaces a buenos artículos).

tmbrggmn
fuente
He dado dos certificados del cliente sobre cómo identificar cuál necesita agregar en el almacén de claves y en el almacén de confianza, ¿podría ayudarme a identificar este problema ya que ya pasó por un tipo de problema similar? Este problema que he planteado no tiene ni idea de qué hacer stackoverflow .com / preguntas / 61374276 / ...
henrycharles

Respuestas:

233

Finalmente logré resolver todos los problemas, así que responderé mi propia pregunta. Estas son las configuraciones / archivos que he usado para administrar para resolver mis problemas particulares;

El almacén de claves del cliente es un archivo de formato PKCS # 12 que contiene

  1. El certificado público del cliente (en este caso firmado por una CA autofirmada)
  2. La clave privada del cliente.

Para generarlo utilicé el pkcs12comando de OpenSSL , por ejemplo;

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

Consejo: asegúrese de obtener la última versión de OpenSSL, no la versión 0.9.8h, porque parece sufrir un error que no le permite generar correctamente los archivos PKCS # 12.

El cliente Java utilizará este archivo PKCS # 12 para presentar el certificado del cliente al servidor cuando el servidor haya solicitado explícitamente que el cliente se autentique. Consulte el artículo de Wikipedia sobre TLS para obtener una descripción general de cómo funciona realmente el protocolo para la autenticación del certificado del cliente (también explica por qué necesitamos la clave privada del cliente aquí).

El almacén de confianza del cliente es un archivo de formato JKS que contiene los certificados de CA raíz o intermedios . Estos certificados de CA determinarán con qué puntos finales podrá comunicarse, en este caso permitirá que su cliente se conecte a cualquier servidor que presente un certificado firmado por una de las CA del almacén de confianza.

Para generarlo, puede utilizar la herramienta de claves estándar de Java, por ejemplo;

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

Con este almacén de confianza, su cliente intentará hacer un protocolo de enlace SSL completo con todos los servidores que presenten un certificado firmado por la CA identificada por myca.crt.

Los archivos anteriores son estrictamente para el cliente solamente. Cuando desee configurar también un servidor, el servidor necesita sus propios archivos de almacén de claves y de confianza. En este sitio web se puede encontrar un gran tutorial para configurar un ejemplo completamente funcional para un cliente y servidor Java (usando Tomcat) .

Problemas / Observaciones / Consejos

  1. La autenticación del certificado del cliente solo puede ser forzada por el servidor.
  2. ( ¡Importante! ) Cuando el servidor solicita un certificado de cliente (como parte del protocolo de enlace TLS), también proporcionará una lista de CA de confianza como parte de la solicitud de certificado. Cuando el certificado de cliente que desea presentar para la autenticación no está firmado por una de estas CA, no se presentará en absoluto (en mi opinión, este es un comportamiento extraño, pero estoy seguro de que hay una razón para ello). Esta fue la causa principal de mis problemas, ya que la otra parte no había configurado su servidor correctamente para aceptar mi certificado de cliente autofirmado y asumimos que el problema estaba en mi extremo por no proporcionar adecuadamente el certificado de cliente en la solicitud.
  3. Consigue Wireshark. Tiene un excelente análisis de paquetes SSL / HTTPS y será de gran ayuda para depurar y encontrar el problema. Es similar -Djavax.net.debug=sslpero está más estructurado y (posiblemente) más fácil de interpretar si no se siente cómodo con la salida de depuración SSL de Java.
  4. Es perfectamente posible usar la biblioteca httpclient de Apache. Si desea usar httpclient, simplemente reemplace la URL de destino con el equivalente HTTPS y agregue los siguientes argumentos JVM (que son los mismos para cualquier otro cliente, independientemente de la biblioteca que desee usar para enviar / recibir datos a través de 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
fuente
66
"Cuando el certificado de cliente que desea presentar para la autenticación no está firmado por una de estas CA, no se presentará en absoluto". Los certificados no se presentan porque el cliente sabe que el servidor no los aceptará. Además, su certificado puede ser firmado por una CA "ICA" intermedia, y el servidor puede presentarle a su cliente la CA "RCA" raíz, y su navegador web aún le permitirá elegir su certificado aunque esté firmado por ICA y no por RCA.
KyleM
2
Como ejemplo del comentario anterior, considere una situación en la que tiene una CA raíz (RCA1) y dos CA intermedias (ICA1 e ICA2). En Apache Tomcat si importa RCA1 en la tienda de confianza, su navegador web presentará TODOS los certificados firmados por ICA1 e ICA2, aunque no estén en su tienda de confianza. Esto se debe a que es la cadena lo que importa, no los certificados individuales.
KyleM
2
"En mi opinión, este es un comportamiento extraño, pero estoy seguro de que hay una razón para ello". La razón es que eso es lo que dice en RFC 2246. No tiene nada de extraño. Permitir que los clientes presenten certificados que no serán aceptados por el servidor es lo que sería extraño, y una completa pérdida de tiempo y espacio.
Marqués de Lorne
1
Recomiendo encarecidamente no utilizar un único almacén de claves JVM (¡y un almacén de confianza!) Como lo ha hecho en su ejemplo anterior. Personalizar una sola conexión es más seguro y más flexible, pero requiere que escriba un poco más de código. Tienes que personalizar el SSLContextcomo en la respuesta de @ Magnus.
Christopher Schultz
63

Otras respuestas muestran cómo configurar globalmente los certificados de cliente. Sin embargo, si desea definir mediante programación la clave del cliente para una conexión en particular, en lugar de definirla globalmente en todas las aplicaciones que se ejecutan en su JVM, puede configurar su propio SSLContext de la siguiente manera:

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
fuente
Tuve que usar sslContext = SSLContexts.custom().loadTrustMaterial(keyFile, PASSWORD).build();. No pude hacerlo funcionar loadKeyMaterial(...).
Conor Svensson el
44
El material de @ConorSvensson Trust es para el cliente que confía en el certificado del servidor remoto, el material clave es para el servidor que confía en el cliente.
Magnus
1
Realmente me gusta esta respuesta concisa y puntual. En caso de que la gente esté interesada, proporciono un ejemplo trabajado aquí con instrucciones de compilación. stackoverflow.com/a/46821977/154527
Alain O'Dea
3
¡Salvaste mi día! Solo un cambio adicional que tuve que hacer, ¡enviar la contraseña en el loadKeyMaterial(keystore, keyPassphrase.toCharArray())código 'también!
AnandShanbhag
1
@peterh Sí, es específico de Apache http. Cada biblioteca HTTP tendrá su propia forma de configuración, pero la mayoría de ellas de alguna manera deberían usar un SSLContext.
Magnus
30

El archivo JKS es solo un contenedor para certificados y pares de claves. En un escenario de autenticación del lado del cliente, las diversas partes de las claves se ubicarán aquí:

  • El cliente de la tienda 's contendrá del cliente público y privado par de claves. Se llama un almacén de claves .
  • La tienda del servidor contendrá la clave pública del cliente . Se llama un almacén de confianza .

La separación del almacén de confianza y el almacén de claves no es obligatoria pero se recomienda. Pueden ser el mismo archivo físico.

Para establecer las ubicaciones del sistema de archivos de las dos tiendas, use las siguientes propiedades del sistema:

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

y en el servidor:

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

Para exportar el certificado del cliente (clave pública) a un archivo, para poder copiarlo en el servidor, use

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

Para importar la clave pública del cliente en el almacén de claves del servidor, use (como se menciona en el póster, esto ya lo han hecho los administradores del servidor)

keytool -import -file publicclientkey.cer -store serversidestore.jks
revs mhaller
fuente
Probablemente debería mencionar que no tengo control sobre el servidor. El servidor ha importado nuestro certificado público. Los administradores de ese sistema me han dicho que debo proporcionar explícitamente el certificado para que pueda enviarse durante el protocolo de enlace (su servidor lo solicita explícitamente).
tmbrggmn 03 de
Necesitará claves públicas y privadas para su certificado público (aquel que el servidor conoce) como un archivo JKS.
sfussenegger 03 de
Gracias por el código de ejemplo. En el código anterior, ¿qué es exactamente "mykey-public.cer"? ¿Es este el certificado público del cliente (utilizamos certificados autofirmados)?
tmbrggmn
Sí, lo he cambiado de nombre a los archivos en los fragmentos de código en consecuencia. Espero que esto quede claro.
mhaller el
Righto, gracias. Sigo confundiéndome porque aparentemente "clave" y "certificado" se usan indistintamente.
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
fuente
Si prefiere tener el certificado disponible para todas las aplicaciones que utilizan una instalación JVM particular, siga esta respuesta .
ADTC
¿Es configureRequest()correcto el método para configurar el proxy del proyecto del cliente?
shareef
sí, es la configuración del cliente http y es la configuración proxy en este caso
kinjelom
tal vez tengo una pregunta tonta, pero ¿cómo creo un archivo cacert.jks? Tengo una excepción de excepción en el hilo "main" java.io.FileNotFoundException:. \ Cacert.jks (El sistema no puede encontrar el archivo especificado)
Patlatus
6

Para aquellos de ustedes que simplemente desean configurar una autenticación bidireccional (certificados de servidor y cliente), una combinación de estos dos enlaces lo llevará hasta allí:

Configuración de autenticación bidireccional:

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

No necesita usar el archivo de configuración openssl que mencionan; Solo usa

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

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

para generar su propio certificado de CA y luego generar y firmar las claves del servidor y del cliente a través de:

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

  • $ openssl req -new -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

y

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

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

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

Para el resto, siga los pasos en el enlace. La administración de los certificados para Chrome funciona igual que en el ejemplo para Firefox que se menciona.

A continuación, configure el servidor a través de:

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

Tenga en cuenta que ya ha creado el servidor .crt y .key para que ya no tenga que seguir ese paso.

Hans
fuente
1
Piensa que tiene errata en fase de generación de CSR servidor: debe utilizar server.keynoclient.key
the.Legend
0

Me he conectado al banco con SSL de dos vías (certificado de cliente y servidor) con Spring Boot. Así que describa aquí todos mis pasos, espero que ayude a alguien (la solución de trabajo más simple que he encontrado):

  1. Generar solicitud de certificado:

    • Generar clave privada:

      openssl genrsa -des3 -passout pass:MY_PASSWORD -out user.key 2048
    • Generar solicitud de certificado:

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

    Conservar user.key(y contraseña) y enviar una solicitud de certificado user.csral banco para mi certificado

  2. Recibir 2 certificados: mi certificado raíz del cliente clientId.crty el certificado raíz del banco:bank.crt

  3. Cree el almacén de claves Java (ingrese la contraseña de clave y establezca la contraseña del almacén de claves):

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

    No preste atención a la salida: unable to write 'random state'. Java PKCS12 keystore.p12creado.

  4. Agregar al almacén de claves bank.crt(por simplicidad, he usado un almacén de claves):

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

    Verifique los certificados del almacén de claves por:

    keytool -list -keystore keystore.p12
  5. Listo para el código Java :) He usado Spring Boot RestTemplatecon agregar org.apache.httpcomponents.httpcoredependencia:

    @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
fuente
Puedes hacerlo todo con la herramienta clave. No hay necesidad de OpenSSL en esto en absoluto.
Marqués de Lorne
0

Dado un archivo p12 con el certificado y la clave privada (generada por openssl, por ejemplo), el siguiente código lo usará para una conexión 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);

La SSLContextinicialización demora un poco, por lo que es posible que desee almacenarlo en caché.

Johannes Brodwall
fuente
-1

Creo que la solución aquí fue el tipo de almacén de claves, pkcs12 (pfx) siempre tiene clave privada y el tipo JKS puede existir sin clave privada. A menos que especifique en su código o seleccione un certificado a través del navegador, el servidor no tiene forma de saber que representa a un cliente en el otro extremo.

ObiWanKenobi
fuente
1
El formato PKCS12 se usaba tradicionalmente para privatekey-AND-cert, pero Java desde 8 en 2014 (más de un año antes de esta respuesta) ha admitido PKCS12 que contiene cert (s) sin privatekey (s). Independientemente del formato del almacén de claves, la autenticación del cliente requiere privatekey-AND-cert. No entiendo su segunda oración, pero el cliente Java puede seleccionar automáticamente un certificado de cliente y una clave si al menos una entrada adecuada está disponible o un administrador de claves puede configurarse para usar una especificada.
dave_thompson_085
Tu segunda oración es totalmente incorrecta. El servidor proporciona sus firmantes de confianza, y el cliente muere o no proporciona un certificado que satisfaga esa restricción. Automáticamente, no a través de 'en su código'. Esa es la "forma de saber que el servidor representa a un cliente".
Marqués de Lorne