【问题标题】:Java client certificates over HTTPS/SSL通过 HTTPS/SSL 的 Java 客户端证书
【发布时间】:2010-10-26 21:41:30
【问题描述】:

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

不管怎样,把根证书加到这个商店里解决了臭名昭著的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

我什至不确定客户端证书是这里的问题。

【问题讨论】:

标签: java ssl jsse sslhandshakeexception


【解决方案1】:

终于解决了;)。得到了一个强烈的暗示here(甘道夫的回答也触及了一点)。缺少的链接(大部分)是下面的第一个参数,在某种程度上我忽略了密钥库和信任库之间的区别。

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

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

【讨论】:

  • 我使用了类似的网址:localhost:8443/Application_Name/getAttributes。我有一个带有 /getAttribute url 映射的方法。此方法返回元素列表。我使用了 HttpsUrlConnection ,连接响应代码是 200,但是当我使用 inputStream 时它没有给我属性列表,它给了我登录页面的 html 内容。我已经完成了身份验证并将内容类型设置为 JSON。请建议
【解决方案2】:

虽然不推荐,但您也可以一起禁用 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) {}
  }
}

【讨论】:

  • 应该注意,像这样禁用证书验证会打开与可能的 MITM 攻击的连接:不要在生产中使用
  • 代码无法编译,谢天谢地。这种“解决方案”根本不安全。
  • @neu242,不,这并不取决于您使用它的目的。如果你想使用 SSL/TLS,你想保护你的连接免受 MITM 攻击,这就是重点。如果您可以保证没有人能够更改流量,则不需要服务器身份验证,但是您怀疑可能存在也无法更改网络流量的窃听者的情况非常罕见。跨度>
  • @neu242 感谢代码 sn-p。我实际上正在考虑在生产中将其用于非常特定的目的(网络爬虫),并且我在一个问题中提到了您的实现。(stackoverflow.com/questions/13076511/…)。如果您有时间,能否请您看一下,如果我遗漏了任何安全风险,请告诉我?
  • 问题不是关于使用客户端证书的问题吗?那 sn-p “解决”了服务器证书的问题。怎么可能是投票率最高的问题?
【解决方案3】:

您是否设置了 KeyStore 和/或 TrustStore 系统属性?

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

或来自代码

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

与 javax.net.ssl.trustStore 相同

【讨论】:

    【解决方案4】:

    如果您使用 Axis 框架处理 Web 服务调用,则有一个更简单的答案。如果只希望您的客户端能够调用 SSL Web 服务并忽略 SSL 证书错误,只需在调用任何 Web 服务之前添加以下语句:

    System.setProperty("axis.socketSecureFactory", "org.apache.axis.components.net.SunFakeTrustSocketFactory");

    关于这是在生产环境中做的非常糟糕的事情的常见免责声明适用。

    我在the Axis wiki 找到了这个。

    【讨论】:

    • OP 处理的是 HttpsURLConnection,而不是 Axis
    • 我明白了。我无意暗示我的回答在一般情况下更好。只是如果您正在使用 Axis 框架,您可能会在该上下文中遇到 OP 的问题。 (这就是我最初发现这个问题的方式。)在这种情况下,我提供的方式更简单。
    【解决方案5】:

    对我来说,这就是使用 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 包含私钥和证书。
    • 您需要包含以下 2 个依赖项才能使 convertPEMtoP12 代码正常工作:org.bouncycastlebcprov-jdk15on1.53 org.bouncycastlebcpkix-jdk15on1.53
    • @EpicPandaForce 我收到一个错误:捕获:org.codehaus.groovy.runtime.typehandling.GroovyCastException:无法将具有类 'org.bouncycastle.jcajce.provider.asymmetric.x509.X509CertificateObject' 的对象转换为ks.setKeyEntry 行中的 class 'int' - 任何可能出错的线索
    • 是的,您使用的是 Groovy 而不是严格类型的语言。 (从技术上讲,该方法需要一个 ID 和一个证书,而不仅仅是一个证书)
    【解决方案6】:

    我在我当前的项目中使用 Apache commons HTTP Client 包来执行此操作,它可以与 SSL 和自签名证书一起正常工作(在将其安装到您提到的 cacerts 之后)。请看这里:

    http://hc.apache.org/httpclient-3.x/tutorial.html

    http://hc.apache.org/httpclient-3.x/sslguide.html

    【讨论】:

    • 这似乎是一个非常简洁的包,但是应该使它全部工作的类 'AuthSSLProtocolSocketFactory' 显然不是官方发行版的一部分,在 4.0beta 中也没有(尽管发行说明指出它是),或在 3.1 中。我已经对它进行了一些修改,现在似乎在它断开连接之前永久卡住了 5 分钟。这真的很奇怪 - 如果我将 CA 和客户端证书加载到任何浏览器中,它就会飞起来。
    • Apache HTTP Client 4 可以直接使用SSLContext,所以你可以这样配置,而不是使用AuthSSLProtocolSocketFactory
    • 有没有办法在内存中完成所有客户端证书的工作,而不是通过外部密钥库?
    【解决方案7】:

    我认为您的服务器证书有问题,不是有效的证书(我认为这就是“handshake_failure”在这种情况下的含义):

    将您的服务器证书导入到客户端 JRE 上的 trustcacerts 密钥库中。这很容易通过keytool 完成:

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

    【讨论】:

    • 我尝试清理并重新开始,握手失败消失了。现在,在连接终止之前,我只有 5 分钟的死寂:o
    【解决方案8】:

    使用下面的代码

    -Djavax.net.ssl.keyStoreType=pkcs12
    

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

    根本不需要。此外,无需创建您自己的自定义 SSL 工厂。

    我也遇到了同样的问题,在我的例子中,完整的证书链没有导入到信任库中。使用 keytool 工具权限从根证书导入证书,您也可以在记事本中打开 cacerts 文件,查看是否导入了完整的证书链。检查您在导入证书时提供的别名,打开证书并查看其中包含多少个证书,cacerts 文件中应该有相同数量的证书。

    还应在您运行应用程序的服务器中配置 cacerts 文件,两台服务器将使用公钥/私钥相互验证。

    【讨论】:

    • 创建您自己的自定义 SSL 工厂比设置两个系统属性复杂且容易出错。
    【解决方案9】:

    虽然这个问题已有 12 多年的历史并且有很多很好的答案,但我想提供一个替代方案。这是加载密钥库和信任库并获取 sslsocketfactory 或 sslcontext 的小 sn-p:

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

    此示例代码 sn-p 来自库:GitHub - SSLContext Kickstart 您可以使用以下 sn-p 添加它:

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

    【讨论】:

      猜你喜欢
      • 2012-02-15
      • 1970-01-01
      • 2013-10-18
      • 2010-12-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-01-02
      • 2017-04-28
      相关资源
      最近更新 更多