Java HTTPS客户端证书认证


222

我还很陌生,对于HTTPS/SSL/TLS使用证书进行身份验证时客户端应该显示的内容有些困惑。

我正在编写一个Java客户端,该客户端需要对POST特定对象进行简单的数据处理URL。那部分工作正常,唯一的问题是应该完成HTTPS。该HTTPS部分相当容易处理(无论是HTTPclient使用Java的内置HTTPS支持还是使用Java的内置支持),但是我一直坚持使用客户端证书进行身份验证。我注意到这里已经存在一个非常类似的问题,我还没有尝试使用我的代码(很快就会这样做)。我当前的问题是-无论做什么,Java客户端都永远不会发送证书(我可以通过PCAP转储检查它)。

我想知道在使用证书进行身份验证时,客户端到底应该向服务器呈现什么(特别是对于Java,如果有的话)?这是JKS文件PKCS#12吗?里面应该有什么?只是客户证书还是密钥?如果是这样,哪个键?关于所有不同类型的文件,证书类型等,都存在很多困惑。

正如我之前刚接触过的内容所说的HTTPS/SSL/TLS那样,我也将不胜感激一些背景信息(不必是散文;我会选择好文章的链接)。


我已经从客户端给了两张证书,如何识别需要在密钥库和信任库中添加的证书,因为您已经经历过类似的问题,请问是否可以帮助您识别此问题,实际上,我已经提出了这个问题,不知道该怎么做 stackoverflow .com / questions / 61374276 /…
henrycharles

Answers:


233

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

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

  1. 客户的公共证书(在这种情况下,由自签名CA签署)
  2. 客户端的私有密钥

为了生成它,我使用了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上Wikipedia文章(在此处还说明了为什么需要客户端的私钥)。

所述客户机的信任是直向前JKS格式包含该文件的根中间CA证书。这些CA证书将确定允许您与哪些端点进行通信,在这种情况下,它将允许您的客户端连接到提供由信任库的CA之一签名的证书的任何服务器。

例如,要生成它,可以使用标准的Java密钥工具;

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

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

上面的文件仅适用于客户端。当您还要设置服务器时,服务器需要自己的密钥和信任库文件。在此网站上可以找到有关为Java客户端和服务器(使用Tomcat)设置完全正常工作的示例的出色演练。

问题/备注/提示

  1. 客户端证书身份验证只能由服务器强制执行。
  2. 重要!)当服务器请求客户端证书(作为TLS握手的一部分)时,它还将在证书请求中提供受信任CA的列表。如果您希望提供的用于身份验证的客户端证书没有被这些CA之一签名,则根本不会提供(我认为这是很奇怪的行为,但是我确定是有原因的)。这是导致我出现问题的主要原因,因为另一方未正确配置其服务器以接受我的自签名客户端证书,并且我们认为问题出在我端,原因是未在请求中正确提供客户端证书。
  3. 获取Wireshark。它具有出色的SSL / HTTPS数据包分析功能,将有助于调试和发现问题。-Djavax.net.debug=ssl如果您对Java SSL调试输出不满意,它类似于但更结构化,并且(可以说)更易于解释。
  4. 完全可以使用Apache httpclient库。如果要使用httpclient,只需用等效的HTTPS替换目标URL,然后添加以下JVM参数(对于其他任何客户端,该参数都是相同的,而不管您要用于通过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

6
“当您希望提供用于身份验证的客户端证书没有被这些CA之一签名时,根本不会提供。” 由于客户端知道服务器不会接受证书,因此不会显示证书。同样,您的证书可以由中间CA“ ICA”签名,服务器可以为您的客户端提供根CA“ RCA”,即使您的证书由ICA而非RCA签名,您的Web浏览器仍然可以让您选择证书。
KyleM 2014年

2
作为上述注释的示例,请考虑您有一个根CA(RCA1)和两个中间CA(ICA1和ICA2)的情况。在Apache Tomcat上,如果将RCA1导入到信任库中,则Web浏览器将显示由ICA1和ICA2签名的所有证书,即使它们不在信任库中也是如此。这是因为与个人证书无关紧要的是链条。
KyleM 2014年

2
“在我看来,这是很奇怪的行为,但是我相信这是有原因的”。其原因是RFC 2246中所说的。没有什么奇怪的。允许客户端提供服务器不接受的证书是很奇怪的,并且完全浪费时间和空间。
洛恩侯爵,

1
我强烈建议您不要像上面的示例那样使用单个JVM范围的密钥库(和信任库!)。自定义单个连接更安全,更灵活,但这确实需要您编写一些代码。您必须自定义SSLContext@Magnus的答案中的as。
克里斯托弗·舒尔茨

63

其他答案显示了如何全局配置客户端证书。但是,如果要以编程方式为一个特定的连接定义客户端密钥,而不是在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"));

我不得不用sslContext = SSLContexts.custom().loadTrustMaterial(keyFile, PASSWORD).build();。我无法使用它loadKeyMaterial(...)
科纳·史文森

4
@ConorSvensson信任材料适用于信任远程服务器证书的客户端,关键材料适用于信任客户端的服务器。
Magnus

1
我真的很喜欢这个简洁明了的答案。如果人们感兴趣,我在这里提供一个工作示例以及构建说明。stackoverflow.com/a/46821977/154527
Alain O'Dea

3
你救了我的一天!我只需要做一个额外的更改,就可以用loadKeyMaterial(keystore, keyPassphrase.toCharArray())代码发送密码了!
AnandShanbhag

1
@peterh是的,它特定于Apache http。每个HTTP库都有其自己的配置方式,但是其中大多数都应该以某种方式使用SSLContext
Magnus

30

它们的JKS文件只是证书和密钥对的容器。在客户端身份验证方案中,密钥的各个部分将位于此处:

  • 客户端的商店将包含客户端的私有和公共密钥对。它称为密钥库
  • 服务器的商店将包含客户端的公共密钥。它称为信任库

信任库和密钥库的分隔不是强制性的,但建议这样做。它们可以是相同的物理文件。

要设置两个存储的文件系统位置,请使用以下系统属性:

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

在服务器上:

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

要将客户端的证书(公钥)导出到文件中,以便可以将其复制到服务器,请使用

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

要将客户端的公共密钥导入服务器的密钥库,请使用(如海报所述,服务器管理员已完成此操作)

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

我可能应该提到我无法控制服务器。服务器已导入我们的公共证书。该系统的管理员告诉我,我需要显式提供证书,以便可以在握手期间将其发送(其服务器显式请求此证书)。
tmbrggmn

您将需要作为JKS文件的公共证书(服务器已知的公共证书)的公共密钥和私有密钥。
sfussenegger,2009年

感谢您的示例代码。在上面的代码中,“ mykey-public.cer”到底是什么?这是客户端公共证书(我们使用自签名证书)吗?
tmbrggmn

是的,我已经相应地重命名了代码片段中的文件。我希望这一点很清楚。
mhaller

Righto,谢谢。我一直感到困惑,因为显然“密钥”和“证书”可互换使用。
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>

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

}

如果您希望证书可用于使用特定JVM安装的所有应用程序,请改用此答案
ADTC

configureRequest()设置客户端项目代理的方法正确吗?
shareef '16

是的,它是http客户端配置,在这种情况下是代理配置
kinjelom 2016年

也许我有一个愚蠢的问题,但是如何创建文件cacert.jks?我在线程“主”中有异常java.io.FileNotFoundException:。\ cacert.jks(系统找不到指定的文件)
Patlatus

6

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

双向身份验证设置:

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

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

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

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

  • $ 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

对于其余的,请遵循链接中的步骤。管理适用于Chrome的证书的功能与上述提到的Firefox示例相同。

接下来,通过以下步骤设置服务器:

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

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


1
想想你在服务器CSR生成步骤错字:应使用server.keyclient.key
the.Legend

0

我已经使用Spring Boot通过双向SSL(客户端和服务器证书)连接到银行。因此,请在此处描述我的所有步骤,希望对您有所帮助(我发现了最简单的解决方案):

  1. 生成证书请求:

    • 生成私钥:

      openssl genrsa -des3 -passout pass:MY_PASSWORD -out user.key 2048
    • 生成证书请求:

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

    保留user.key(和密码)并将证书请求发送user.csr给银行以获取我的证书

  2. 收到2个证书:我的客户根证书clientId.crt和银行根证书:bank.crt

  3. 创建Java密钥库(输入密钥密码并设置密钥库密码):

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

    请勿关注输出:unable to write 'random state'。Java PKCS12已keystore.p12创建。

  4. 添加到密钥库中bank.crt(为简单起见,我使用了一个密钥库):

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

    通过以下方式检查密钥库证书:

    keytool -list -keystore keystore.p12
  5. 准备好使用Java代码:)我已经将Spring Boot RestTemplate与add org.apache.httpcomponents.httpcore依赖项结合使用:

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

您可以使用keytool完成所有操作。完全不需要OpenSSL。
在洛恩侯爵

0

给定一个同时包含证书和私钥的p12文件(例如,由openssl生成),以下代码会将其用于特定的HttpsURLConnection:

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

SSLContext需要一些时间进行初始化,所以你可能要进行缓存。


-1

我认为这里的解决方法是密钥库类型,pkcs12(pfx)始终具有私钥,而JKS类型可以不存在私钥而存在。除非您在代码中指定或通过浏览器选择证书,否则服务器将无法得知服务器代表另一端的客户端。


1
PKCS12格式传统上用于私钥和证书,但是Java自2014年8月起(此答案问世一年多之前)一直支持包含证书的PKCS12,但不包含私钥。不管密钥库的格式如何,客户端身份验证确实都需要private-AND-cert。我不明白您的第二句话,但是,如果至少有一个合适的条目可用,或者可以将密钥管理配置为使用指定的条目,则Java客户端可以自动选择客户端证书和密钥。
dave_thompson_085 '19

您的第二句话完全不正确。服务器提供其受信任的签名者,而客户端死亡或不提供满足该约束的证书。自动,而不是通过“在您的代码中”。那就是服务器的“知道它代表客户的方式”。
在洛恩侯爵
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.