Keytool创建可信的自签名证书

7

我正在尝试使用(java)keytool创建自签名证书,但当我尝试使用它时,我会收到以下异常(请参见底部的完整异常信息)。

...<5 more exceptions above this>
Caused by: sun.security.validator.ValidatorException: No trusted certificate found
        at sun.security.validator.SimpleValidator.buildTrustedChain(SimpleValidator.java:304)
        at sun.security.validator.SimpleValidator.engineValidate(SimpleValidator.java:107)
        at sun.security.validator.Validator.validate(Validator.java:203)
        at com.sun.net.ssl.internal.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:172)
        at com.sun.net.ssl.internal.ssl.JsseX509TrustManager.checkServerTrusted(SSLContextImpl.java:320)
        at com.sun.net.ssl.internal.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:841)
        ... 22 more

我知道可以通过以下代码绕过此问题:

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSession;

HostnameVerifier hv = new HostnameVerifier() {
    public boolean verify(String urlHostName, SSLSession session) {
        System.out.println("Warning: URL Host: " + urlHostName + " vs. " + session.getPeerHost());
        return true;
    }
};

HttpsURLConnection.setDefaultHostnameVerifier(hv);

(来源)

但我对此解决方案不感兴趣,因为我认为它会产生安全漏洞(如果我错了,请纠正我)。

有人能指点我正确的方向吗?目前我正在本地测试,所以更改内容非常容易。我可以访问服务器代码、客户端代码和.keystore文件。

更新

我试图使用一个.keystore文件来同时为客户端和服务器提供服务,但为了简化问题,我创建了server.keystore(见下文)和client.truststore(见下文)。我相当自信证书是正确的,但如果有人能验证一下,我将不胜感激。

server.keystore

hostname[username:/this/is/a/path][711]% keytool -list -keystore server.keystore -v
Enter keystore password:

Keystore type: JKS
Keystore provider: SUN

Your keystore contains 1 entry

Alias name: hostname
Creation date: Feb 4, 2010
Entry type: PrivateKeyEntry
Certificate chain length: 1
Certificate[1]:
Owner: CN=hostname, OU=hostname, O=hostname, L=hostname, ST=hostname, C=hostname
Issuer: CN=hostname, OU=hostname, O=hostname, L=hostname, ST=hostname, C=hostname
Serial number: 4b6b0ea7
Valid from: Thu Feb 04 13:15:03 EST 2010 until: Wed May 05 14:15:03 EDT 2010
Certificate fingerprints:
         MD5:  81:C0:3F:EC:AD:5B:7B:C4:DA:08:CC:D7:11:1F:1D:38
         SHA1: F1:78:AD:C8:D0:3A:4C:0C:9A:4F:89:C0:2A:2F:E2:E6:D5:13:96:40
         Signature algorithm name: SHA1withDSA
         Version: 3


*******************************************
*******************************************

client.truststore

hostname[username:/this/is/a/path][713]% keytool -list -keystore client.truststore -v
Enter keystore password:

Keystore type: JKS
Keystore provider: SUN

Your keystore contains 1 entry

Alias name: mykey
Creation date: Feb 4, 2010
Entry type: trustedCertEntry

Owner: CN=hostname, OU=hostname, O=hostname, L=hostname, ST=hostname, C=hostname
Issuer: CN=hostname, OU=hostname, O=hostname, L=hostname, ST=hostname, C=hostname
Serial number: 4b6b0ea7
Valid from: Thu Feb 04 13:15:03 EST 2010 until: Wed May 05 14:15:03 EDT 2010
Certificate fingerprints:
         MD5:  81:C0:3F:EC:AD:5B:7B:C4:DA:08:CC:D7:11:1F:1D:38
         SHA1: F1:78:AD:C8:D0:3A:4C:0C:9A:4F:89:C0:2A:2F:E2:E6:D5:13:96:40
         Signature algorithm name: SHA1withDSA
         Version: 3


*******************************************
*******************************************

更新

我认为包含完整的异常信息可能很有用:

javax.xml.soap.SOAPException: java.io.IOException: Could not transmit message
        at org.jboss.ws.core.soap.SOAPConnectionImpl.callInternal(SOAPConnectionImpl.java:115)
        at org.jboss.ws.core.soap.SOAPConnectionImpl.call(SOAPConnectionImpl.java:66)
        at com.alcatel.tpapps.common.utils.SOAPClient.execute(SOAPClient.java:193)
        at com.alcatel.tpapps.common.utils.SOAPClient.main(SOAPClient.java:280)
Caused by: java.io.IOException: Could not transmit message
        at org.jboss.ws.core.client.RemotingConnectionImpl.invoke(RemotingConnectionImpl.java:192)
        at org.jboss.ws.core.client.SOAPRemotingConnection.invoke(SOAPRemotingConnection.java:77)
        at org.jboss.ws.core.soap.SOAPConnectionImpl.callInternal(SOAPConnectionImpl.java:106)
        ... 3 more
Caused by: org.jboss.remoting.CannotConnectException: Can not connect http client invoker. sun.security.validator.ValidatorException: No trusted certificate found.
        at org.jboss.remoting.transport.http.HTTPClientInvoker.useHttpURLConnection(HTTPClientInvoker.java:368)
        at org.jboss.remoting.transport.http.HTTPClientInvoker.transport(HTTPClientInvoker.java:148)
        at org.jboss.remoting.MicroRemoteClientInvoker.invoke(MicroRemoteClientInvoker.java:141)
        at org.jboss.remoting.Client.invoke(Client.java:1858)
        at org.jboss.remoting.Client.invoke(Client.java:718)
        at org.jboss.ws.core.client.RemotingConnectionImpl.invoke(RemotingConnectionImpl.java:171)
        ... 5 more
Caused by: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: No trusted certificate found
        at com.sun.net.ssl.internal.ssl.Alerts.getSSLException(Alerts.java:150)
        at com.sun.net.ssl.internal.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1584)
        at com.sun.net.ssl.internal.ssl.Handshaker.fatalSE(Handshaker.java:174)
        at com.sun.net.ssl.internal.ssl.Handshaker.fatalSE(Handshaker.java:168)
        at com.sun.net.ssl.internal.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:848)
        at com.sun.net.ssl.internal.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:106)
        at com.sun.net.ssl.internal.ssl.Handshaker.processLoop(Handshaker.java:495)
        at com.sun.net.ssl.internal.ssl.Handshaker.process_record(Handshaker.java:433)
        at com.sun.net.ssl.internal.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:877)
        at com.sun.net.ssl.internal.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1089)
        at com.sun.net.ssl.internal.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1116)
        at com.sun.net.ssl.internal.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1100)
        at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:402)
        at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:170)
        at sun.net.www.protocol.http.HttpURLConnection.getOutputStream(HttpURLConnection.java:857)
        at sun.net.www.protocol.https.HttpsURLConnectionImpl.getOutputStream(HttpsURLConnectionImpl.java:230)
        at org.jboss.remoting.transport.http.HTTPClientInvoker.useHttpURLConnection(HTTPClientInvoker.java:288)
        ... 10 more
Caused by: sun.security.validator.ValidatorException: No trusted certificate found
        at sun.security.validator.SimpleValidator.buildTrustedChain(SimpleValidator.java:304)
        at sun.security.validator.SimpleValidator.engineValidate(SimpleValidator.java:107)
        at sun.security.validator.Validator.validate(Validator.java:203)
        at com.sun.net.ssl.internal.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:172)
        at com.sun.net.ssl.internal.ssl.JsseX509TrustManager.checkServerTrusted(SSLContextImpl.java:320)
        at com.sun.net.ssl.internal.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:841)
        ... 22 more
5个回答

15
你需要在服务器和客户端之间建立“信任关系”(我假设你只需要进行服务器端身份验证)。这是因为你使用自签名证书。这涉及将服务器证书导入客户端信任存储中:
在服务器端:
keytool -keystore <keystore file> -alias <alias> -export -file <certfilename>.cert

将.cert文件复制到客户端,然后执行以下操作:

keytool -keystore <truststore file> -alias <alias> -import -file <certfilename>.cert

1
我自己没有尝试过那种配置,但是它可能有效。您需要为密钥材料和证书使用不同的别名,因为它们具有不同的目的(并且两者都必须存在)。看起来您已经意识到这个设置仅适用于测试目的,而不是生产环境... - Armadillo
1
不确定这是否是实际问题,但在您的8443端口连接器中,您拥有clientAuth="true"。这将要求在另一个方向(客户端->服务器)也建立信任。你的443端口没有那个。这是您的要求吗? - Armadillo
1
好的。那么你可以发布你的客户端代码,这样我就可以看一下了吗? 另外,你能否将浏览器指向相同的端口?你得到了什么?(你应该能够查询你的 Web 服务的 WSDL) - Armadillo
你的问题引导我做了一个有趣的观察。我的客户试图访问:https://sco-up:443/common/ws/A2。但我很好奇那是否是问题所在。我将该地址更改为https://sco-up:443/asdfasdhahdhadsfkjlasd(即垃圾)。结果我得到了相同的异常。这对我来说非常奇怪。我会进一步调查此事。 - sixtyfootersdude
作为回应你的问题,如果我在浏览器中导航到 https://sco-up:443/common/ws/A2,我会得到纯文本:"HTTP GET not supported"(我认为这似乎是合理的)。如果我导航到 https://sco-up:443/asdfasdfkd/asdfads,我会得到一个带有标题的 HTML 表格:"HTTP Status 404 - /asdfasdfkd/asdfadsf"。 - sixtyfootersdude
显示剩余6条评论

10

无法在客户端和服务器之间共享密钥库,因为密钥库包含私钥。在进行身份验证时,客户端会跳过具有私钥的证书。正如上面所说,您需要在客户端部署信任库。

密钥库中的证书的行为取决于您如何生成或导入它们。

导入的证书条目类型(使用 -list -v 命令详细列出整个密钥库时可见)为“trustedCertEntry”。生成的证书条目类型为“PrivateKeyEntry”。当您导出证书时,只会导出其公钥和可选的颁发者引用。

看起来您需要将密钥库中的自签名证书导出为信任库中的受信任证书(名称在此处有意义)。

我不建议这样做,因为 SSL/TLS 实现可能不支持它。从实际角度来看,这就像是将 Verisign 的最终秘密私钥部署到某个晦涩的 Web 服务器上以签署普通页面,而这个私钥的唯一目的是保持安全并签署其他证书。SSL/TLS 实现者可能不会在代码中污染这样的用例,而且,“KeyUsage”证书扩展程序可能限制证书用途只能用于签署,从而防止加密。

这就是为什么我建议重新构建测试的证书链。

keytool 文档包含一个有关创建链的有趣部分(-gencert 命令),但它只是一个非常简略的示例,没有涵盖密钥库 - 信任库之间的关系。我对其进行了增强,以模拟第三方认证机构。

临时存储库 their-keystore.jks 表示发出证书的机构。我使用“ca2 -> ca1 -> ca” 的证书链填充它,并将 ca 视为根证书。每个非根证书(即 ca1ca2)都引用其颁发者作为 Certificate[2]。请注意,每个证书都是“PrivateKeyEntry”。

然后,我按顺序使用这些证书来填充 my-keystore.jks:先是 ca、然后是 ca1、最后是 ca2。我使用 -trustcacerts 选项导入 ca,使其成为根证书。在 my-keystore.jks 中,现在每个导入的证书都是“trustedCertEntry”,这意味着只有公钥。颁发关系仅出现在“Issuer”字段中,但这没问题,因为在导入时信任关系最为重要。

此时,my-keystore.jks 模拟了包含一些受信任证书的环境,就像一个全新的 JRE 一样。而 their-keystore.jks 则模拟了这些证书的所有者,他们具有签署证书请求的权力。

我也是这样做的:在my-keystore.jks中创建一个自签名证书e1,通过their-keystore.jksca2(间接)签署它,并将签署结果导入my-keystore.jks。现在e1 -> ca2 -> ca1构成了证书链,尽管e1仍然是“PrivateKeyEntry”(因为其私钥仍留在my-keystore.jks中)。看起来ca1 -> ca是隐含的,其中ca是认证机构。

要构建信任库,只需以与my-keystore.jks相同的方式导入证书caca1ca2。请注意,我不会导入e1,因为我希望SSL/TLS客户端对其进行验证,以验证ca2

我认为这已经非常接近实际情况了。这里的好处在于您可以完全控制证书,并且不依赖于JRE的cacerts。

以下是将我的说法付诸实践的代码。似乎可以与Jetty(客户端和服务器)一起使用,只要您禁用证书吊销列表(这是另一天留下的话题)。

#!/bin/bash

rm  their-keystore.jks 2> /dev/null
rm  my-keystore.jks    2> /dev/null
rm  my-truststore.jks  2> /dev/null

echo "===================================================="
echo "Creating fake third-party chain ca2 -> ca1 -> ca ..."
echo "===================================================="

keytool -genkeypair -alias ca  -dname cn=ca                           \
  -validity 10000 -keyalg RSA -keysize 2048                           \
  -ext BasicConstraints:critical=ca:true,pathlen:10000                \
  -keystore their-keystore.jks -keypass Keypass -storepass Storepass

keytool -genkeypair -alias ca1 -dname cn=ca1                          \
  -validity 10000 -keyalg RSA -keysize 2048                           \
  -keystore their-keystore.jks -keypass Keypass -storepass Storepass

keytool -genkeypair -alias ca2 -dname cn=ca2                          \
  -validity 10000 -keyalg RSA -keysize 2048                           \
  -keystore their-keystore.jks -keypass Keypass -storepass Storepass


  keytool -certreq -alias ca1                                            \
    -keystore their-keystore.jks -keypass Keypass -storepass Storepass   \
| keytool -gencert -alias ca                                             \
    -ext KeyUsage:critical=keyCertSign                                   \
    -ext SubjectAlternativeName=dns:ca1                                  \
    -keystore their-keystore.jks -keypass Keypass -storepass Storepass   \
| keytool -importcert -alias ca1                                         \
    -keystore   their-keystore.jks -keypass Keypass -storepass Storepass

#echo "Debug exit" ; exit 0

  keytool -certreq -alias ca2                                           \
    -keystore their-keystore.jks -keypass Keypass -storepass Storepass  \
| keytool -gencert -alias ca1                                           \
    -ext KeyUsage:critical=keyCertSign                                  \
    -ext SubjectAlternativeName=dns:ca2                                 \
    -keystore their-keystore.jks -keypass Keypass -storepass Storepass  \
| keytool -importcert -alias ca2                                        \
    -keystore their-keystore.jks -keypass Keypass -storepass Storepass

keytool -list -v -storepass Storepass -keystore their-keystore.jks


echo  "===================================================================="
echo  "Fake third-party chain generated. Now generating my-keystore.jks ..."
echo  "===================================================================="
read -p "Press a key to continue."

# Import authority's certificate chain

  keytool -exportcert -alias ca                                         \
    -keystore their-keystore.jks -keypass Keypass -storepass Storepass  \
| keytool -importcert -trustcacerts -noprompt -alias ca                 \
    -keystore  my-keystore.jks -keypass Keypass -storepass Storepass

  keytool -exportcert -alias ca1                                        \
    -keystore their-keystore.jks -keypass Keypass -storepass Storepass  \
| keytool -importcert -noprompt -alias ca1                              \
    -keystore  my-keystore.jks -keypass Keypass -storepass Storepass

  keytool -exportcert -alias ca2                                        \
    -keystore their-keystore.jks -keypass Keypass -storepass Storepass  \
| keytool -importcert -noprompt -alias ca2                              \
    -keystore  my-keystore.jks -keypass Keypass -storepass Storepass

# Create our own certificate, the authority signs it.

keytool -genkeypair -alias e1  -dname cn=e1                        \
  -validity 10000 -keyalg RSA -keysize 2048                        \
  -keystore my-keystore.jks -keypass Keypass -storepass Storepass

  keytool -certreq -alias e1                                            \
    -keystore my-keystore.jks -keypass Keypass -storepass Storepass     \
| keytool -gencert -alias ca2                                           \
    -ext SubjectAlternativeName=dns:localhost                           \
    -ext KeyUsage:critical=keyEncipherment,digitalSignature             \
    -ext ExtendedKeyUsage=serverAuth,clientAuth                         \
    -keystore their-keystore.jks -keypass Keypass -storepass Storepass  \
| keytool -importcert -alias e1                                         \
    -keystore my-keystore.jks -keypass Keypass -storepass Storepass

keytool -list -v  -storepass Storepass -keystore  my-keystore.jks

echo "================================================="
echo "Keystore generated. Now generating truststore ..."
echo "================================================="
read -p "Press a key to continue."

  keytool -exportcert -alias ca                                        \
    -keystore my-keystore.jks -keypass Keypass -storepass Storepass    \
| keytool -importcert -trustcacerts -noprompt -alias ca                \
    -keystore my-truststore.jks -keypass Keypass -storepass Storepass

  keytool -exportcert -alias ca1                                       \
    -keystore my-keystore.jks -keypass Keypass -storepass Storepass    \
| keytool -importcert -noprompt -alias ca1                             \
    -keystore my-truststore.jks -keypass Keypass -storepass Storepass

  keytool -exportcert -alias ca2                                       \
    -keystore my-keystore.jks -keypass Keypass -storepass Storepass    \
| keytool -importcert -noprompt -alias ca2                             \
    -keystore my-truststore.jks -keypass Keypass -storepass Storepass

keytool -list -v  -storepass Storepass -keystore  my-truststore.jks

rm  their-keystore.jks 2> /dev/null

这篇回答的基础是一个谬论,即自签名证书无法工作和/或不受支持和/或会不安全地泄露私钥。由于这些都不是真实情况,因此该回答毫无意义,也没有回答所提出的问题。 - user207421
加油,@EJP。我在我的脚本中使用自签名证书,所以我认为它们是有效的。 我建议sixtyfootersdude尝试使用一个被充分理解的信任链来重现他的问题。 - Laurent Caillette
别自己糊弄自己了。你在第五段中所说的是“SSL/TLS实现可能不支持它”,随后的评论似乎暗示你认为证书包含私钥。 - user207421
不,你误解了重点。当我写“这就像在某些晦涩的 Web 服务器上部署来自 Verisign 的终极秘密私钥以签名普通页面”时,并没有涉及到信任存储库。我明确提到了“KeyUsage”扩展,它设置了认证机构和域名认证之间的区别。这一切都关乎证书签名(如果你愿意,可以说是在“密钥库侧”),并且涉及到信任存储库。【续...】 - Laurent Caillette
这是我在这个网站上看到的10个最佳答案之一。我从这个答案中学到的比其他100篇博客文章中混乱的命令更多。你的答案不同,因为它解释了“为什么”。这个答案是我理解为本地主机创建可信证书链问题的“关键”(无双关语!)。我还能够实现你的命令的极简版本——一个单独的CA证书和一个单独的本地主机签名证书。然后,在我的MS Windows上信任CA证书(certmgr.msc)。重新启动Google Chrome。就像魔术一样……它工作了! - kevinarpe
显示剩余3条评论

3

你不能那样做。密钥库是严格保密的。如果你将它泄露给任何人,你就会严重危及安全。这种做法没有意义,仅仅是为了让它起作用而已,因为它并工作——这只是一种安全漏洞。你必须正确地执行:从服务器的密钥库导出到客户端的信任库,从客户端的密钥库(如果有)导出到服务器的密钥库。


0

我不太懂。你是在客户端使用服务器密钥库吗?你的使用情况是什么?你是想建立双向认证吗?

如果是的话,在这里走错路了。你需要一个客户端密钥库(用于存储客户端自签名证书和私钥)和一个客户端信任库(用于存储服务器的“独立自签名证书”,即没有私钥)。这两者与服务器密钥库是不同的。


我认为我可以在测试时使用服务器的密钥库与客户端配合使用。这将确保客户端拥有服务器的证书。尽管可能客户端确实需要自己的自签名证书... - sixtyfootersdude
就像我之前所说,这取决于您想要做什么(对我来说并不清楚)。如果您想要实现双向身份验证,客户端也需要其证书。在这种情况下,我认为您不能使用服务器密钥存储作为客户端信任存储。 - Pascal Thivent

-1
Springboot 2.1.5 , java 1.8, keytool(it is part of JDK 8) 

these are the steps to follow to generate the self signed ssl certifcate in spring boot


1. Generate self signed ssl certificate

keytool -genkeypair -alias tomcat -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore keystore.p12 -validity 3650

ex: D:\example> <here run the above command>

if it is not working then make sure that your java bin path is set at environment variables to the PATH variable as

C:\Program Files\Java\jdk1.8.0_191\bin

2. after key generation has done then copy that file in to the src/main/resources folder in your project

3. add key store properties in applicaiton.properties 

server.port: 8443
server.ssl.key-store:classpath:keystore.p12
server.ssl.key-store-password: test123     # change the pwd
server.ssl.keyStoreType: PKCS12
server.ssl.keyAlias: tomcat

3. change your postman ssl verification settings to turn OFF
    go to settings and select the  ssl verification  to turn off

now verify the url ( your applicaiton url)
https://localhost:8443/test/hello

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接