Java HTTPS 客户端证书认证
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/1666052/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Java HTTPS client certificate authentication
提问by tmbrggmn
I'm fairly new to HTTPS/SSL/TLS
and I'm a bit confused over what exactly the clients are supposed to present when authenticating with certificates.
我是新手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 客户端,它需要对POST
特定的URL
. 那部分工作正常,唯一的问题是它应该完成HTTPS
。这HTTPS
部分相当容易处理(使用HTTPclient
或使用 Java 的内置HTTPS
支持),但我坚持使用客户端证书进行身份验证。我注意到这里已经有一个非常相似的问题,我还没有用我的代码尝试过(很快就会这样做)。我当前的问题是 - 无论我做什么 - Java 客户端都不会随证书一起发送(我可以使用PCAP
转储进行检查)。
I would like to know what exactly the client is supposed to present to the server when authenticating with certificates (specifically for Java - if that matters at all)? Is this a JKS
file, or PKCS#12
? What's supposed to be in them; just the client certificate, or a key? If so, which key? There's quite a bit of confusion about all the different kinds of files, certificate types and such.
我想知道客户端在使用证书进行身份验证时究竟应该向服务器呈现什么(特别是对于 Java - 如果这很重要的话)?这是一个JKS
文件,还是PKCS#12
?他们应该有什么;只是客户端证书,还是密钥?如果是,是哪个键?关于所有不同类型的文件、证书类型等,存在相当多的混淆。
As I've said before I'm new to HTTPS/SSL/TLS
so I would appreciate some background information as well (doesn't have to be an essay; I'll settle for links to good articles).
正如我在刚接触之前所说的HTTPS/SSL/TLS
那样,我也希望了解一些背景信息(不必是一篇文章;我会选择好文章的链接)。
采纳答案by tmbrggmn
Finally managed to solve all the issues, so I'll answer my own question. These are the settings/files I've used to manage to get my particular problem(s) solved;
终于设法解决了所有问题,所以我将回答我自己的问题。这些是我用来解决我的特定问题的设置/文件;
The client's keystoreis a PKCS#12 formatfile containing
该客户端的密钥库是一个PKCS#12格式文件包含
- The client's publiccertificate (in this instance signed by a self-signed CA)
- The client's privatekey
- 客户端的公共证书(在本例中由自签名 CA 签名)
- 客户端的私有密钥
To generate it I used OpenSSL's pkcs12
command, for example;
例如,为了生成它,我使用了 OpenSSL 的pkcs12
命令;
openssl pkcs12 -export -in client.crt -inkey client.key -out client.p12 -name "Whatever"
Tip:make sure you get the latest OpenSSL, notversion 0.9.8h because that seems to suffer from a bug which doesn't allow you to properly generate PKCS#12 files.
提示:确保您获得最新的 OpenSSL,而不是0.9.8h 版本,因为这似乎存在不允许您正确生成 PKCS#12 文件的错误。
This PKCS#12 file will be used by the Java client to present the client certificate to the server when the server has explicitly requested the client to authenticate. See the Wikipedia article on TLSfor an overview of how the protocol for client certificate authentication actually works (also explains why we need the client's private key here).
当服务器明确请求客户端进行身份验证时,Java 客户端将使用此 PKCS#12 文件向服务器提供客户端证书。请参阅关于 TLS的维基百科文章,了解客户端证书身份验证协议的实际工作原理(还解释了我们为什么需要客户端的私钥)。
The client's truststoreis a straight forward JKS formatfile containing the rootor intermediate CA certificates. These CA certificates will determine which endpoints you will be allowed to communicate with, in this case it will allow your client to connect to whichever server presents a certificate which was signed by one of the truststore's CA's.
所述客户机的信任是直向前JKS格式包含该文件的根或中间CA证书。这些 CA 证书将决定您将被允许与哪些端点进行通信,在这种情况下,它将允许您的客户端连接到任何提供由信任库的 CA 之一签署的证书的服务器。
To generate it you can use the standard Java keytool, for example;
例如,要生成它,您可以使用标准的 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
Using this truststore, your client will try to do a complete SSL handshake with all servers who present a certificate signed by the CA identified by myca.crt
.
使用此信任库,您的客户端将尝试与所有提供由 标识的 CA 签名的证书的服务器进行完整的 SSL 握手myca.crt
。
The files above are strictly for the client only. When you want to set-up a server as well, the server needs its own key- and truststore files. A great walk-through for setting up a fully working example for both a Java client and server (using Tomcat) can be found on this website.
以上文件仅供客户使用。当您还想设置服务器时,服务器需要自己的密钥和信任库文件。可以在此网站上找到为 Java 客户端和服务器(使用 Tomcat)设置完整工作示例的一个很好的演练。
Issues/Remarks/Tips
问题/备注/提示
- Client certificate authenticationcan 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 notsigned 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
- 客户端证书身份验证只能由服务器强制执行。
- (重要!)当服务器请求客户端证书(作为 TLS 握手的一部分)时,它还将提供可信 CA 的列表作为证书请求的一部分。当您希望提供用于身份验证的客户端证书没有由这些 CA 之一签署时,它根本不会提供(在我看来,这是一种奇怪的行为,但我确信这是有原因的)。这是我出现问题的主要原因,因为对方没有正确配置他们的服务器来接受我的自签名客户端证书,我们认为问题出在我这边,因为我没有在请求中正确提供客户端证书。
- 获取 Wireshark。它具有出色的 SSL/HTTPS 数据包分析功能,对调试和查找问题有很大帮助。
-Djavax.net.debug=ssl
如果您对 Java SSL 调试输出感到不舒服,它类似于但更结构化并且(可以说)更容易解释。 完全可以使用 Apache httpclient 库。如果您想使用 httpclient,只需将目标 URL 替换为等效的 HTTPS 并添加以下 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
回答by mhaller
They JKS file is just a container for certificates and key pairs. In a client-side authentication scenario, the various parts of the keys will be located here:
他们的 JKS 文件只是证书和密钥对的容器。在客户端身份验证方案中,密钥的各个部分将位于此处:
- The client's store will contain the client's private and publickey pair. It is called a keystore.
- The server's store will contain the client's publickey. It is called a truststore.
- 该客户端的商店将包含客户端的私有和公共密钥对。它被称为密钥库。
- 该服务器的商店将包含客户端的公共密钥。它被称为信任库。
The separation of truststore and keystore is not mandatory but recommended. They can be the same physical file.
信任库和密钥库的分离不是强制性的,但建议。它们可以是相同的物理文件。
To set the filesystem locations of the two stores, use the following system properties:
要设置两个存储的文件系统位置,请使用以下系统属性:
-Djavax.net.ssl.keyStore=clientsidestore.jks
and on the server:
并在服务器上:
-Djavax.net.ssl.trustStore=serversidestore.jks
To export the client's certificate (public key) to a file, so you can copy it to the server, use
要将客户端的证书(公钥)导出到文件,以便您可以将其复制到服务器,请使用
keytool -export -alias MYKEY -file publicclientkey.cer -store clientsidestore.jks
To import the client's public key into the server's keystore, use (as the the poster mentioned, this has already been done by the server admins)
要将客户端的公钥导入服务器的密钥库,请使用(如海报所述,这已由服务器管理员完成)
keytool -import -file publicclientkey.cer -store serversidestore.jks
回答by kinjelom
Maven pom.xml:
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 code:
爪哇代码:
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;
}
}
回答by Magnus
Other answers show how to globally configure client certificates. However if you want to programmatically define the client key for one particular connection, rather than globally define it across every application running on your JVM, then you can configure your own SSLContext like so:
其他答案显示了如何全局配置客户端证书。但是,如果您想以编程方式为一个特定连接定义客户端密钥,而不是在 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"));
回答by ObiWanKenobi
I think the fix here was the keystore type, pkcs12(pfx) always have private key and JKS type can exist without private key. Unless you specify in your code or select a certificate thru browser, the server have no way of knowing it is representing a client on the other end.
我认为这里的修复是密钥库类型,pkcs12(pfx) 总是有私钥,而 JKS 类型可以在没有私钥的情况下存在。除非您在代码中指定或通过浏览器选择证书,否则服务器无法知道它代表另一端的客户端。
回答by hans
For those of you who simply want to set up a two-way authentication (server and client certificates), a combination of these two links will get you there :
对于那些只想设置双向身份验证(服务器和客户端证书)的人,这两个链接的组合将使您达到目的:
Two-way auth setup:
双向身份验证设置:
https://linuxconfig.org/apache-web-server-ssl-authentication
https://linuxconfig.org/apache-web-server-ssl-authentication
You don't need to use the openssl config file that they mention; just use
您不需要使用他们提到的 openssl 配置文件;只是使用
$ openssl genrsa -des3 -out ca.key 4096
$ openssl req -new -x509 -days 365 -key ca.key -out ca.crt
$ openssl genrsa -des3 -out ca.key 4096
$ openssl req -new -x509 -days 365 -key ca.key -out ca.crt
to generate your own CA certificate, and then generate and sign the server and client keys via:
生成您自己的 CA 证书,然后通过以下方式生成并签署服务器和客户端密钥:
$ 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
$ 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
and
和
$ 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
$ 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
For the rest follow the steps in the link. Managing the certificates for Chrome works the same as in the example for firefox that is mentioned.
其余的请按照链接中的步骤操作。管理 Chrome 证书的工作方式与上述 firefox 示例中的相同。
Next, setup the server via:
接下来,通过以下方式设置服务器:
Note that you have already created the server .crt and .key so you don't have to do that step anymore.
请注意,您已经创建了服务器 .crt 和 .key,因此您不必再执行该步骤。
回答by Grigory Kislin
I've connected to bank with two-way SSL (client and server certificate) with Spring Boot. So describe here all my steps, hope it helps someone (simplest working solution, I've found):
我已经通过 Spring Boot 使用双向 SSL(客户端和服务器证书)连接到银行。所以在这里描述我的所有步骤,希望它对某人有所帮助(我找到了最简单的工作解决方案):
Generate sertificate request:
Generate private key:
openssl genrsa -des3 -passout pass:MY_PASSWORD -out user.key 2048
Generate certificate request:
openssl req -new -key user.key -out user.csr -passin pass:MY_PASSWORD
Keep
user.key
(and password) and send certificate requestuser.csr
to bank for my sertificateReceive 2 certificate: my client root certificate
clientId.crt
and bank root certificate:bank.crt
Create Java keystore (enter key password and set keystore password):
openssl pkcs12 -export -in clientId.crt -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 PKCS12keystore.p12
created.Add into keystore
bank.crt
(for simplicity I've used one keystore):keytool -import -alias banktestca -file banktestca.crt -keystore keystore.p12 -storepass javaops
Check keystore certificates by:
keytool -list -keystore keystore.p12
Ready for Java code:) I've used Spring Boot
RestTemplate
with addorg.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; }
生成证书请求:
生成私钥:
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个证书:我的客户根证书
clientId.crt
和银行根证书:bank.crt
创建 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 PKCS12keystore.p12
创建。添加到密钥库
bank.crt
(为简单起见,我使用了一个密钥库):keytool -import -alias banktestca -file banktestca.crt -keystore keystore.p12 -storepass javaops
通过以下方式检查密钥库证书:
keytool -list -keystore keystore.p12
准备好 Java 代码:) 我使用 Spring Boot
RestTemplate
并添加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; }
回答by Johannes Brodwall
Given a p12 file with both the certificate and the private key (generated by openssl, for example), the following code will use that for a specific HttpsURLConnection:
给定一个包含证书和私钥(例如,由 openssl 生成)的 p12 文件,以下代码将使用它用于特定的 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);
The SSLContext
takes some time to initialize, so you might want to cache it.
将SSLContext
需要一些时间进行初始化,所以你可能要进行缓存。