Java客户端证书在HTTPS/SSL上的应用

124

我正在使用Java 6,尝试创建一个HttpsURLConnection与远程服务器建立连接,并使用客户端证书。
服务器使用的是自签名根证书,并要求提供受密码保护的客户端证书。我已将服务器根证书和客户端证书添加到默认的Java密钥库中,该密钥库位于/System/Library/Frameworks/JavaVM.framework/Versions/1.6.0/Home/lib/security/cacerts(OSX 10.5)中。
密钥库文件的名称似乎表明客户端证书不应该放在那里?

无论如何,将根证书添加到此存储中解决了臭名昭著的javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed' problem.

但是,现在我被卡住了,不知道如何使用客户端证书。我尝试了两种方法,但都没有进展。
首先,也是首选,请尝试:

SSLSocketFactory sslsocketfactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
URL url = new URL("https://somehost.dk:3049");
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
conn.setSSLSocketFactory(sslsocketfactory);
InputStream inputstream = conn.getInputStream();
// The last line fails, and gives:
// javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure

我尝试跳过HttpsURLConnection类(这并不理想,因为我想用HTTP与服务器通信),而改为执行以下操作:

SSLSocketFactory sslsocketfactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
SSLSocket sslsocket = (SSLSocket) sslsocketfactory.createSocket("somehost.dk", 3049);
InputStream inputstream = sslsocket.getInputStream();
// do anything with the inputstream results in:
// java.net.SocketTimeoutException: Read timed out

我甚至不确定客户端证书是问题所在。


我从客户那里得到了两个证书,如何确定哪一个需要添加到密钥库和信任库中?你能否帮忙解决这个问题,因为你已经遇到过类似的问题。参考链接:https://stackoverflow.com/questions/61374276/access-https-restful-service-using-web-client-in-spring-boot-2-0-throwing-except - henrycharles
9个回答

107

最终解决了这个问题;)。在这里得到了一个很有力的提示(Gandalfs的答案也稍微涉及到了这一点)。缺少的链接主要是下面参数中的第一个,而且我忽略了密钥库和信任库之间的差异。

必须将自签名服务器证书导入信任库:

keytool -import -alias gridserver -file gridserver.crt -storepass $PASS -keystore gridserver.keystore

这些属性需要设置(可以在命令行或代码中进行):

-Djavax.net.ssl.keyStoreType=pkcs12
-Djavax.net.ssl.trustStoreType=jks
-Djavax.net.ssl.keyStore=clientcertificate.p12
-Djavax.net.ssl.trustStore=gridserver.keystore
-Djavax.net.debug=ssl # very verbose debug
-Djavax.net.ssl.keyStorePassword=$PASS
-Djavax.net.ssl.trustStorePassword=$PASS

工作示例代码:

SSLSocketFactory sslsocketfactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
URL url = new URL("https://gridserver:3049/cgi-bin/ls.py");
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
conn.setSSLSocketFactory(sslsocketfactory);
InputStream inputstream = conn.getInputStream();
InputStreamReader inputstreamreader = new InputStreamReader(inputstream);
BufferedReader bufferedreader = new BufferedReader(inputstreamreader);

String string = null;
while ((string = bufferedreader.readLine()) != null) {
    System.out.println("Received " + string);
}

我使用了一个URL:https://localhost:8443/Application_Name/getAttributes。我有一个与/getAttribute URL映射的方法。该方法返回一个元素列表。我使用了HttpsUrlConnection,连接响应代码为200,但当我使用inputStream时,它没有给我属性列表,而是给我我的登录页面的HTML内容。我已经进行了身份验证并将内容类型设置为JSON。请建议。 - Deepak

85

虽然不建议这样做,但您也可以使用来自Java开发者年鉴的以下代码完全禁用SSL证书验证:

import javax.net.ssl.*; import java.security.SecureRandom; import
java.security.cert.X509Certificate;

public class SSLTool {

  public static void disableCertificateValidation() {
    // Create a trust manager that does not validate certificate chains
    TrustManager[] trustAllCerts = new TrustManager[] { 
      new X509TrustManager() {
        public X509Certificate[] getAcceptedIssuers() { 
          return new X509Certificate[0]; 
        }
        public void checkClientTrusted(X509Certificate[] certs, String authType) {}
        public void checkServerTrusted(X509Certificate[] certs, String authType) {}
    }};

    // Ignore differences between given hostname and certificate hostname
    HostnameVerifier hv = new HostnameVerifier() {
      public boolean verify(String hostname, SSLSession session) { return true; }
    };

    // Install the all-trusting trust manager
    try {
      SSLContext sc = SSLContext.getInstance("SSL");
      sc.init(null, trustAllCerts, new SecureRandom());
      HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
      HttpsURLConnection.setDefaultHostnameVerifier(hv);
    } catch (Exception e) {}   } }

77
需要注意的是,像这样禁用证书验证会打开连接以可能的中间人攻击:不要在生产环境中使用 - Bruno
3
代码无法编译,谢天谢地。这个“解决方案”极不安全。 - user207421
7
@neu242,不,这并不取决于您使用它的目的。如果您想使用SSL/TLS,您希望保护连接免受中间人攻击,这就是整个意义所在。如果您可以保证没有人能够更改流量,那么服务器认证并不是必需的,但是您怀疑可能存在窃听者而这些人也无法改变网络流量的情况非常罕见。 - Bruno
2
问题不是关于使用客户端证书的问题吗?那段代码片段“解决”的是服务器证书的问题。这怎么能成为得票最高的问题呢? - Piotr Sobczyk
1
@Bruno 如果我只是ping服务器,这会对我受到中间人攻击产生影响吗? - PhoonOne
显示剩余11条评论

21

你是否已经设置了KeyStore和/或TrustStore系统属性?

java -Djavax.net.ssl.keyStore=pathToKeystore -Djavax.net.ssl.keyStorePassword=123456

或从代码内部

System.setProperty("javax.net.ssl.keyStore", pathToKeyStore);

与javax.net.ssl.trustStore相同


13
如果你正在使用 Axis 框架处理 Web 服务调用,有一个更简单的方法。如果你的客户端只是想调用 SSL Web 服务并忽略 SSL 证书错误,那么在调用任何 Web 服务之前,只需添加以下语句即可: System.setProperty("axis.socketSecureFactory", "org.apache.axis.components.net.SunFakeTrustSocketFactory"); 通常情况下,这样做在生产环境中非常危险,请注意。
我在Axis wiki找到了这个方法。

OP正在处理HttpsURLConnection,而不是Axis。 - anon
2
我明白。我的回答并不是要暗示在一般情况下它更好。只是如果您正在使用Axis框架,那么您可能会对OP的问题在这个背景下进行了解。(这就是我第一次发现这个问题的方式。)在这种情况下,我提供的方法更简单。 - Mark Meuer

6

对我而言,使用Apache HttpComponents ~ HttpClient 4.x这个方案行之有效:

    KeyStore keyStore  = KeyStore.getInstance("PKCS12");
    FileInputStream instream = new FileInputStream(new File("client-p12-keystore.p12"));
    try {
        keyStore.load(instream, "helloworld".toCharArray());
    } finally {
        instream.close();
    }

    // Trust own CA and all self-signed certs
    SSLContext sslcontext = SSLContexts.custom()
        .loadKeyMaterial(keyStore, "helloworld".toCharArray())
        //.loadTrustMaterial(trustStore, new TrustSelfSignedStrategy()) //custom trust store
        .build();
    // Allow TLSv1 protocol only
    SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
        sslcontext,
        new String[] { "TLSv1" },
        null,
        SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); //TODO
    CloseableHttpClient httpclient = HttpClients.custom()
        .setHostnameVerifier(SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER) //TODO
        .setSSLSocketFactory(sslsf)
        .build();
    try {

        HttpGet httpget = new HttpGet("https://localhost:8443/secure/index");

        System.out.println("executing request" + httpget.getRequestLine());

        CloseableHttpResponse response = httpclient.execute(httpget);
        try {
            HttpEntity entity = response.getEntity();

            System.out.println("----------------------------------------");
            System.out.println(response.getStatusLine());
            if (entity != null) {
                System.out.println("Response content length: " + entity.getContentLength());
            }
            EntityUtils.consume(entity);
        } finally {
            response.close();
        }
    } finally {
        httpclient.close();
    }

该P12文件包含使用BouncyCastle创建的客户端证书和客户端私钥:

public static byte[] convertPEMToPKCS12(final String keyFile, final String cerFile,
    final String password)
    throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException,
    NoSuchProviderException
{
    // Get the private key
    FileReader reader = new FileReader(keyFile);

    PEMParser pem = new PEMParser(reader);
    PEMKeyPair pemKeyPair = ((PEMKeyPair)pem.readObject());
    JcaPEMKeyConverter jcaPEMKeyConverter = new JcaPEMKeyConverter().setProvider("BC");
    KeyPair keyPair = jcaPEMKeyConverter.getKeyPair(pemKeyPair);

    PrivateKey key = keyPair.getPrivate();

    pem.close();
    reader.close();

    // Get the certificate
    reader = new FileReader(cerFile);
    pem = new PEMParser(reader);

    X509CertificateHolder certHolder = (X509CertificateHolder) pem.readObject();
    java.security.cert.Certificate x509Certificate =
        new JcaX509CertificateConverter().setProvider("BC")
            .getCertificate(certHolder);

    pem.close();
    reader.close();

    // Put them into a PKCS12 keystore and write it to a byte[]
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    KeyStore ks = KeyStore.getInstance("PKCS12", "BC");
    ks.load(null);
    ks.setKeyEntry("key-alias", (Key) key, password.toCharArray(),
        new java.security.cert.Certificate[]{x509Certificate});
    ks.store(bos, password.toCharArray());
    bos.close();
    return bos.toByteArray();
}

keyStore 包含私钥和证书。 - EpicPandaForce
1
您需要包含这两个依赖项,以使 convertPEMtoP12 代码正常工作: org.bouncycastle bcprov-jdk15on 1.53 org.bouncycastle bcpkix-jdk15on 1.53 - BirdOfPrey
@EpicPandaForce 我出现了一个错误: 捕获到异常:org.codehaus.groovy.runtime.typehandling.GroovyCastException: 在 ks.setKeyEntry 行中不能将类为 'org.bouncycastle.jcajce.provider.asymmetric.x509.X509CertificateObject' 的对象强制转换为类 'int' - 你有什么线索,可能是哪里出错了? - Vishal Biyani
是的,你正在使用Groovy而不是严格类型的语言。(从技术上讲,该方法需要一个ID和证书,而不仅仅是证书) - EpicPandaForce

4

我认为您的服务器证书存在问题,不是有效证书(在这种情况下,“handshake_failure”可能就是这个意思):

请将您的服务器证书导入客户端JRE的trustcacerts密钥库中。您可以使用keytool轻松完成此操作:

keytool
    -import
    -alias <provide_an_alias>
    -file <certificate_file>
    -keystore <your_path_to_jre>/lib/security/cacerts

我尝试了清理并重新开始,握手失败问题消失了。现在,在连接终止之前,我只会得到5分钟的死寂:o - Jan

4

1
这似乎是一个相当不错的软件包,但是应该使其全部工作的类“AuthSSLProtocolSocketFactory”显然不是官方发行版的一部分,无论是在4.0beta(尽管发布说明中声明它是),还是在3.1中都不是。我已经对其进行了一些修改,现在似乎永远卡在了5分钟的挂起状态,然后就会断开连接。这真的很奇怪 - 如果我将CA和客户端证书加载到任何浏览器中,它就会飞快地运行。 - Jan
1
Apache HTTP Client 4可以直接使用SSLContext,因此您可以通过这种方式配置所有内容,而无需使用AuthSSLProtocolSocketFactory - Bruno
1
有没有一种方法可以在内存中完成所有客户端证书的操作,而不是通过外部密钥库? - Sridhar Sarnobat

1
使用以下代码。
-Djavax.net.ssl.keyStoreType=pkcs12

或者

System.setProperty("javax.net.ssl.keyStore", pathToKeyStore);

不需要创建自己的自定义SSL工厂。也没有必要这样做。
我也遇到了同样的问题,我的情况是 完整的证书链 没有被导入到信任库中。使用keytool实用程序导入证书,从根证书开始,您还可以在记事本中打开cacerts文件,查看是否已导入完整的证书链。针对导入证书时提供的别名进行检查,打开证书并查看其中包含的数量,cacerts文件中应该有相同数量的证书。
此外,cacerts文件应该在运行应用程序的服务器中进行配置,两个服务器将通过公共/私有密钥进行身份验证。

1
创建自己的定制 SSL 工厂比设置两个系统属性要复杂得多,出错的可能性也更大。 - user207421

0

虽然这个问题已经超过12年了,并且有很多好的答案,但我想提供一种替代方案。以下是加载密钥库和信任库以及获取sslsocketfactory或sslcontext的小片段:

SSLFactory sslFactory = SSLFactory.builder()
        .withIdentityMaterial("clientcertificate.p12", "password".toCharArray(), "PKCS12")
        .withTrustMaterial("gridserver.keystore", "password".toCharArray(), "PKCS12")
        .build();

SSLSocketFactory sslSocketFactory = sslFactory.getSslSocketFactory();
SSLContext sslContext = sslFactory.getSslContext();

这个代码片段示例来自库:GitHub - SSLContext Kickstart 您可以使用以下代码片段添加它:

<dependency>
    <groupId>io.github.hakky54</groupId>
    <artifactId>sslcontext-kickstart</artifactId>
    <version>7.0.2</version>
</dependency>

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