我对HTTPS/SSL/TLS相当陌生,我有点困惑,当使用证书进行身份验证时,客户端究竟应该呈现什么。

I'm writing a Java client that needs to do a simple POST of data to a particular URL. That part works fine, the only problem is it's supposed to be done over HTTPS. The HTTPS part is fairly easy to handle (either with HTTPclient or using Java's built-in HTTPS support), but I'm stuck on authenticating with client certificates. I've noticed there's already a very similar question on here, which I haven't tried out with my code yet (will do so soon enough). My current issue is that - whatever I do - the Java client never sends along the certificate (I can check this with PCAP dumps).

我想知道客户端在使用证书进行身份验证时到底应该向服务器提供什么(特别是对于Java -如果这很重要的话)?这是一个JKS文件,还是PKCS#12?里面应该有什么;只有客户端证书,还是密钥?如果是,是哪个钥匙?关于所有不同类型的文件、证书类型等,有相当多的困惑。

正如我之前说过的,我是HTTPS/SSL/TLS的新手,所以我也希望能了解一些背景知识(不必是一篇文章;我将满足于好文章的链接)。


当前回答

我已经用Spring Boot用双向SSL(客户端和服务器证书)连接到银行。所以在这里描述我所有的步骤,希望它能帮助到别人(我发现了最简单的工作解决方案):

生成证书请求:

生成私钥: openssl genrsa -des3 -passout pass:MY_PASSWORD -out user。关键的2048 生成证书请求: Openssl req -new -key user。退出用户。csr -passin pass:MY_PASSWORD

保持用户。密钥(和密码)和发送证书请求到银行

Receive 2 certificate: my client root certificate user.pem and bank root certificate: bank.crt Create Java keystore (enter key password and set keystore password): openssl pkcs12 -export -in user.pem -inkey user.key -out keystore.p12 -name clientId -CAfile ca.crt -caname root Don't pay attention on output: unable to write 'random state'. Java PKCS12 keystore.p12 created. Add into keystore bank.crt (for simplicity I've used one keystore): keytool -import -alias bankca -file bank.crt -keystore keystore.p12 -storepass MY_PASS Check keystore certificates by: keytool -list -keystore keystore.p12 Ready for Java code:) I've used Spring Boot RestTemplate with add org.apache.httpcomponents.httpcore dependency: @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; }

其他回答

最终解决了所有问题,所以我来回答我自己的问题。这些是我用来解决我的特定问题的设置/文件;

客户端的密钥存储库是一个PKCS#12格式文件,包含

客户端的公共证书(在本例中由自签名CA签名) 客户端的私钥

例如,我使用OpenSSL的pkcs12命令来生成它;

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

提示:确保你得到最新的OpenSSL,而不是0.9.8h版本,因为它似乎有一个错误,不允许你正确生成PKCS#12文件。

当服务器显式地请求客户端进行身份验证时,Java客户端将使用这个PKCS#12文件向服务器提供客户端证书。请参阅维基百科上关于TLS的文章,了解用于客户端证书身份验证的协议实际上是如何工作的(还解释了为什么这里需要客户端私钥)。

客户端的信任库是一个直接的JKS格式文件,其中包含根证书或中间CA证书。这些CA证书将决定允许您与哪些端点通信,在这种情况下,它将允许您的客户端连接到提供由信任存储库的CA之一签名的证书的任何服务器。

例如,你可以使用标准的Java keytool来生成它;

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

使用这个信任库,您的客户端将尝试与所有提供由myca.crt标识的CA签名的证书的服务器进行一次完整的SSL握手。

上面的文件严格地只针对客户端。当您还想设置服务器时,服务器需要自己的密钥和信任库文件。在这个网站上可以找到关于为Java客户端和服务器(使用Tomcat)设置一个完全工作的示例的很好的演练。

问题/评论/建议

Client certificate authentication can only be enforced by the server. (Important!) When the server requests a client certificate (as part of the TLS handshake), it will also provide a list of trusted CA's as part of the certificate request. When the client certificate you wish to present for authentication is not signed by one of these CA's, it won't be presented at all (in my opinion, this is weird behaviour, but I'm sure there's a reason for it). This was the main cause of my issues, as the other party had not configured their server properly to accept my self-signed client certificate and we assumed that the problem was at my end for not properly providing the client certificate in the request. Get Wireshark. It has great SSL/HTTPS packet analysis and will be a tremendous help debugging and finding the problem. It's similar to -Djavax.net.debug=ssl but is more structured and (arguably) easier to interpret if you're uncomfortable with the Java SSL debug output. It's perfectly possible to use the Apache httpclient library. If you want to use httpclient, just replace the destination URL with the HTTPS equivalent and add the following JVM arguments (which are the same for any other client, regardless of the library you want to use to send/receive data over 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

我已经用Spring Boot用双向SSL(客户端和服务器证书)连接到银行。所以在这里描述我所有的步骤,希望它能帮助到别人(我发现了最简单的工作解决方案):

生成证书请求:

生成私钥: openssl genrsa -des3 -passout pass:MY_PASSWORD -out user。关键的2048 生成证书请求: Openssl req -new -key user。退出用户。csr -passin pass:MY_PASSWORD

保持用户。密钥(和密码)和发送证书请求到银行

Receive 2 certificate: my client root certificate user.pem and bank root certificate: bank.crt Create Java keystore (enter key password and set keystore password): openssl pkcs12 -export -in user.pem -inkey user.key -out keystore.p12 -name clientId -CAfile ca.crt -caname root Don't pay attention on output: unable to write 'random state'. Java PKCS12 keystore.p12 created. Add into keystore bank.crt (for simplicity I've used one keystore): keytool -import -alias bankca -file bank.crt -keystore keystore.p12 -storepass MY_PASS Check keystore certificates by: keytool -list -keystore keystore.p12 Ready for Java code:) I've used Spring Boot RestTemplate with add org.apache.httpcomponents.httpcore dependency: @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; }

其他回答说明如何全局配置客户端证书。 然而,如果你想通过编程方式为一个特定的连接定义客户端密钥,而不是在JVM上运行的每个应用程序上全局定义它,那么你可以像这样配置自己的SSLContext:

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"));

对于那些只想建立双向身份验证(服务器和客户端证书)的人来说,这两个链接的组合将使您达到目的:

双向认证设置:

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

你不需要使用他们提到的openssl配置文件;只使用

$ openssl genrsa -des3 -out ca.key 4096 $ openssl req -new -x509 -days 365 -key ca.key out ca.crt . out

生成您自己的CA证书,然后通过以下方式生成并签署服务器和客户端密钥:

$ openssl genrsa -des3 -out服务器。关键的4096 $ openssl req -new -key服务器。输入-out server.csr $ openssl x509 -req -days 365 -in server。csr -CA ca.crt -CAkey ca.key -set_serial 100 -out server.crt

and

$ openssl genrsa -des3 -out客户端关键的4096 $ openssl req -new -key客户端。输出client.csr $ openssl x509 -req -days 365 -in客户端csr -CA ca.crt -CAkey ca.key -set_serial 101 -out client.crt

其余的请按照链接中的步骤进行操作。Chrome的证书管理方法与上面提到的firefox的证书管理方法相同。

接下来,通过以下方式设置服务器:

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

请注意,您已经创建了服务器的.crt和.key,因此您不必再执行这一步。

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>

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

}