如何優(yōu)雅處理HTTPS中的證書問題

Java中HTTPS會(huì)遇到的問題

  • 訪問自簽名的HTTPS網(wǎng)站
  • 高版本JRE訪問SSLv3/SSLv2站點(diǎn)
  • 一些銀行接口需要加載keystore的場(chǎng)景

* 如果要了解SSL歷史也可以看看這篇文章。

1 訪問自簽名的HTTPS網(wǎng)站

常??吹降幕卮鹗侵苯油ㄟ^信任所有來支持, 這不優(yōu)雅; 優(yōu)雅的操作應(yīng)該:

  • 下載服務(wù)端的CA證書
# 方式1: 導(dǎo)出DER格式的證書
# 這里需要通過指定servername來保證導(dǎo)出的證書和當(dāng)前域名匹配
openssl s_client -showcerts -connect self-signed.badssl.com:443 -servername self-signed.badssl.com </dev/null 2>/dev/null|openssl x509 -outform der >self-signed.badssl.com.der
  • 代碼中通過加載服務(wù)端證書后通過自定義SSLContext訪問目標(biāo)服務(wù)器:
private SSLContext sslContext(File certificateFile, String certificateType) {

    InputStream inputStream = null;
    try {
        inputStream = new FileInputStream(certificateFile);
        CertificateFactory cf = CertificateFactory.getInstance(certificateType);
        Certificate certificate = cf.generateCertificate(inputStream);
        System.out.println("ca=" + ((X509Certificate) certificate).getSubjectDN());
        String alias = ((X509Certificate) certificate).getSubjectDN().toString();

        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(null, null);
        keyStore.setCertificateEntry(alias, certificate);

        // Create a KeyStore containing our trusted CAs
        SSLContext sslcontext = SSLContexts.custom()
                .loadTrustMaterial(keyStore, new TrustSelfSignedStrategy())
                .build();
        return sslcontext;
    } catch (IOException e) {
        throw new RuntimeException(e);
    } catch (CertificateException e) {
        throw new RuntimeException(e);
    } catch (NoSuchAlgorithmException e) {
        throw new RuntimeException(e);
    } catch (KeyStoreException e) {
        throw new RuntimeException(e);
    } catch (KeyManagementException e) {
        throw new RuntimeException(e);
    } finally {
        if (inputStream != null) try { inputStream.close(); } catch (IOException e) { }
    }
}

@Test
public void testSelfSign() throws IOException {
    File certFile = ResourceUtils.getFile(this.getClass().getResource("/root.cer"));

    CloseableHttpClient httpclient = HttpClients.custom()
            .setSSLContext(sslContext(certFile, "X.509"))  //
            .build();

    String body = Executor.newInstance(httpclient).execute(Request
            .Get("https://self-signed.badssl.com/"))
            .returnContent()
            .asString();
    System.out.println(body);
}

2 高版本JRE訪問SSLv3/SSLv2站點(diǎn)

通常你得到的答案是通過修改${JRE_HOME}/lib/security/java.security目錄下某些配置項(xiàng)來取消高版本SDK對(duì)某些不安全SSL協(xié)議版本或算法的限制。

各版本對(duì)SSL的支持情況

JDK8 JDK7 JDK6
TLS Protocols TLSv1.2 (default)
TLSv1.1
TLSv1
SSLv3
TLSv1.2
TLSv1.1
TLSv1 (default)
SSLv3
TLS v1.1 (JDK 6 update 111 and above)
TLSv1 (default)
SSLv3
JSSE Ciphers: Ciphers in JDK 8 Ciphers in JDK 7 Ciphers in JDK 6
Reference: JDK 8 JSSE JDK 7 JSSE JDK 6 JSSE
Java Cryptography Extension, Unlimited Strength (explained later) JCE for JDK 8 JCE for JDK 7 JCE for JDK 6

* 在2015年1月發(fā)布的升級(jí)補(bǔ)丁中也已經(jīng)禁用對(duì)SSLv3的支持。

所以為什么會(huì)出現(xiàn)在某些高版本無法訪問某些HTTPS站點(diǎn)的原因就是由于有以下可能:

  • 服務(wù)端支持對(duì)SSL版本在本地JRE已經(jīng)被禁用, 例如服務(wù)端只支持SSLv3而JDK已經(jīng)默認(rèn)關(guān)閉了對(duì)SSLv3的支持。
  • 服務(wù)端使用的JSSE Ciphers和本地支持的JSSE Ciphers沒有共同項(xiàng)導(dǎo)致無法正常選擇加密算法。

2.1 確認(rèn)當(dāng)前JRE啟用SSL協(xié)議

@Test
public void sslSupport() throws IOException {
    SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault();
    SSLSocket soc = (SSLSocket) factory.createSocket();

    // Returns the names of the protocol versions which are
    // currently enabled for use on this connection.
    String[] protocols = soc.getEnabledProtocols();

    System.out.println("Enabled protocols:");
    for (String s : protocols) {
        System.out.println(s);
    }
}

輸出:

Enabled protocols:
TLSv1
TLSv1.1
TLSv1.2

2.2 確認(rèn)當(dāng)前JRE啟用CipherSutes

String[] cipers = soc.getEnabledCipherSuites();
System.out.println("Enabled CipherSutes:");
for (String s : cipers) {
    System.out.println(s);
}

輸出

Enabled CipherSutes:
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384
TLS_RSA_WITH_AES_256_CBC_SHA256
TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384
TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384
TLS_DHE_RSA_WITH_AES_256_CBC_SHA256
TLS_DHE_DSS_WITH_AES_256_CBC_SHA256
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
TLS_RSA_WITH_AES_256_CBC_SHA
TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA
TLS_ECDH_RSA_WITH_AES_256_CBC_SHA
TLS_DHE_RSA_WITH_AES_256_CBC_SHA
TLS_DHE_DSS_WITH_AES_256_CBC_SHA
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
TLS_RSA_WITH_AES_128_CBC_SHA256
TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256
TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256
TLS_DHE_RSA_WITH_AES_128_CBC_SHA256
TLS_DHE_DSS_WITH_AES_128_CBC_SHA256
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
TLS_RSA_WITH_AES_128_CBC_SHA
TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA
TLS_ECDH_RSA_WITH_AES_128_CBC_SHA
TLS_DHE_RSA_WITH_AES_128_CBC_SHA
TLS_DHE_DSS_WITH_AES_128_CBC_SHA
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
TLS_RSA_WITH_AES_256_GCM_SHA384
TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384
TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384
TLS_DHE_RSA_WITH_AES_256_GCM_SHA384
TLS_DHE_DSS_WITH_AES_256_GCM_SHA384
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
TLS_RSA_WITH_AES_128_GCM_SHA256
TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256
TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256
TLS_DHE_RSA_WITH_AES_128_GCM_SHA256
TLS_DHE_DSS_WITH_AES_128_GCM_SHA256
TLS_EMPTY_RENEGOTIATION_INFO_SCSV

2.3 檢查服務(wù)器支持的SSL協(xié)議

這里我推薦使用nmap檢測(cè):

nmap --script ssl-enum-ciphers -p 443 badssl.com

如果出現(xiàn)服務(wù)器正常卻提示Nmap done: 1 IP address (0 hosts up) scanned可使用-Pn參數(shù):

nmap -Pn --script ssl-enum-ciphers -p 443 badssl.com

輸出:

Starting Nmap 7.70 ( https://nmap.org ) at 2019-01-01 22:26 CST
Nmap scan report for badssl.com (104.154.89.105)
Host is up (0.32s latency).
rDNS record for 104.154.89.105: 105.89.154.104.bc.googleusercontent.com

PORT    STATE SERVICE
443/tcp open  https
| ssl-enum-ciphers:
|   TLSv1.0:
|     ciphers:
|       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (secp256r1) - A
|       TLS_DHE_RSA_WITH_AES_128_CBC_SHA (dh 2048) - A
|       TLS_DHE_RSA_WITH_AES_256_CBC_SHA (dh 2048) - A
|       TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA (secp256r1) - C
|       TLS_RSA_WITH_AES_128_CBC_SHA (rsa 2048) - A
|       TLS_RSA_WITH_AES_256_CBC_SHA (rsa 2048) - A
|       TLS_RSA_WITH_3DES_EDE_CBC_SHA (rsa 2048) - C
|       TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA (dh 2048) - A
|       TLS_RSA_WITH_CAMELLIA_256_CBC_SHA (rsa 2048) - A
|       TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA (dh 2048) - A
|       TLS_RSA_WITH_CAMELLIA_128_CBC_SHA (rsa 2048) - A
|     compressors:
|       NULL
|     cipher preference: server
|     warnings:
|       64-bit block cipher 3DES vulnerable to SWEET32 attack
|   TLSv1.1:
|     ciphers:
|       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (secp256r1) - A
|       TLS_DHE_RSA_WITH_AES_128_CBC_SHA (dh 2048) - A
|       TLS_DHE_RSA_WITH_AES_256_CBC_SHA (dh 2048) - A
|       TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA (secp256r1) - C
|       TLS_RSA_WITH_AES_128_CBC_SHA (rsa 2048) - A
|       TLS_RSA_WITH_AES_256_CBC_SHA (rsa 2048) - A
|       TLS_RSA_WITH_3DES_EDE_CBC_SHA (rsa 2048) - C
|       TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA (dh 2048) - A
|       TLS_RSA_WITH_CAMELLIA_256_CBC_SHA (rsa 2048) - A
|       TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA (dh 2048) - A
|       TLS_RSA_WITH_CAMELLIA_128_CBC_SHA (rsa 2048) - A
|     compressors:
|       NULL
|     cipher preference: server
|     warnings:
|       64-bit block cipher 3DES vulnerable to SWEET32 attack
|   TLSv1.2:
|     ciphers:
|       TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (secp256r1) - A
|       TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 (dh 2048) - A
|       TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 (dh 2048) - A
|       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (secp256r1) - A
|       TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 (dh 2048) - A
|       TLS_DHE_RSA_WITH_AES_128_CBC_SHA (dh 2048) - A
|       TLS_DHE_RSA_WITH_AES_256_CBC_SHA256 (dh 2048) - A
|       TLS_DHE_RSA_WITH_AES_256_CBC_SHA (dh 2048) - A
|       TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA (secp256r1) - C
|       TLS_RSA_WITH_AES_128_GCM_SHA256 (rsa 2048) - A
|       TLS_RSA_WITH_AES_256_GCM_SHA384 (rsa 2048) - A
|       TLS_RSA_WITH_AES_128_CBC_SHA256 (rsa 2048) - A
|       TLS_RSA_WITH_AES_256_CBC_SHA256 (rsa 2048) - A
|       TLS_RSA_WITH_AES_128_CBC_SHA (rsa 2048) - A
|       TLS_RSA_WITH_AES_256_CBC_SHA (rsa 2048) - A
|       TLS_RSA_WITH_3DES_EDE_CBC_SHA (rsa 2048) - C
|       TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA (dh 2048) - A
|       TLS_RSA_WITH_CAMELLIA_256_CBC_SHA (rsa 2048) - A
|       TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA (dh 2048) - A
|       TLS_RSA_WITH_CAMELLIA_128_CBC_SHA (rsa 2048) - A
|     compressors:
|       NULL
|     cipher preference: server
|     warnings:
|       64-bit block cipher 3DES vulnerable to SWEET32 attack
|_  least strength: C

Nmap done: 1 IP address (1 host up) scanned in 39.91 seconds

根據(jù)輸出可以看到badssl.com同時(shí)支持TLSv1.0、TLSv1.1以及TLSv1.2, 同時(shí)也可以看到當(dāng)前對(duì)應(yīng)協(xié)議支持的加密算法。

2.4 解決方案

JRE在${JRE_HOME}/lib/security/java.security配置了一些算法的配置, 例如本地我的${JRE_HOME}/lib/security/java.security配置內(nèi)容為:

jdk.tls.disabledAlgorithms=SSLv3, RC4, DES, MD5withRSA, DH keySize < 1024, \
    EC keySize < 224, 3DES_EDE_CBC

表示當(dāng)前JRE要禁用SSLv3協(xié)議以及RC4、DES等算法。

當(dāng)然我們可以通過手動(dòng)修改該文件來取消這些限制來達(dá)到我們對(duì)目的, 但這樣程序在部署到新環(huán)境就可能不能正常運(yùn)行, 不優(yōu)雅! 優(yōu)雅對(duì)操作如下:

  • 根據(jù)協(xié)議版本動(dòng)態(tài)對(duì)JRE對(duì)設(shè)置取消設(shè)置, 在JRE中管理相關(guān)SSL協(xié)議以及算法的配置項(xiàng)主要是jdk.tls.disabledAlgorithms。
// 取消當(dāng)前運(yùn)行環(huán)境對(duì)SSLv3、RC4、DES以及3DES_EDE_CBC的禁用限制
static {
    String disabledAlgorithms = Security.getProperty("jdk.tls.disabledAlgorithms");
    HashSet<String> keys = Sets.newHashSet(disabledAlgorithms.split(", +"));
    if (keys.contains("SSLv3")) keys.remove("SSLv3");
    if (keys.contains("RC4")) keys.remove("RC4");
    if (keys.contains("DES")) keys.remove("DES");
    if (keys.contains("3DES_EDE_CBC")) keys.remove("3DES_EDE_CBC");

    Security.setProperty("jdk.tls.disabledAlgorithms", StringUtils.join(keys, ", "));
    log.debug("SECURITY PROPERTY UPDATED \"jdk.tls.disabledAlgorithms\" = " + Security.getProperty("jdk.tls.disabledAlgorithms"));
}
  • 自定義SSLConnectionSocketFactory兼容對(duì)低版本協(xié)議對(duì)支持, 突破JRE默認(rèn)限制。
// Allow SSLv3, TLSv1, TLSv1.2 protocol only
SSLConnectionSocketFactory sslConnectionSocketFactory =  new SSLConnectionSocketFactory(
        sslContext,
        new String[] { "SSLv3", "TLSv1", "TLSv1.2"},
        null,
        NoopHostnameVerifier.INSTANCE);

至此你就可以完美且優(yōu)雅的解決這個(gè)問題。

附錄

[1] HTTPS
https://en.wikipedia.org/wiki/HTTPS
[2] Obtain a Certificate from Server
https://ldapwiki.com/wiki/Obtain%20a%20Certificate%20from%20Server
[3] Transport Level Security (TLS) and Java
http://www.ateam-oracle.com/tls-and-java/
[4] Diagnosing TLS, SSL, and HTTPS
https://blogs.oracle.com/java-platform-group/diagnosing-tls,-ssl,-and-https

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容