Java证书程序化导入及SSL握手失败问题排查求助
问题:程序化导入SSL证书后仍出现PKIX路径构建失败错误
目标与手动操作流程
编写程序的目标是复现以下手动获取SSL证书并导入JRE cacerts证书库的流程:
keytool -printcert -sslserver {host}:{port} -rfc >> {host}.crt keytool -importcert -alias {host} -keystore /usr/local/openjdk-8/jre/lib/security/cacerts -file {host} -storepass changeit -noprompt
实现代码
程序的核心代码如下:
InputStream in = new FileInputStream(keystore); KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); ks.load(in, passphrase); in.close(); SSLContext context = SSLContext.getInstance("TLS"); TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509"); tmf.init(ks); X509TrustManager defaultTrustManager = null; //(X509TrustManager) tmf.getTrustManagers()[0]; TrustManager[] tms = tmf.getTrustManagers(); for (TrustManager tm : tms) { if (tm instanceof X509TrustManager) { defaultTrustManager = (X509TrustManager) tm; break; } } if (defaultTrustManager == null) { if (isLog) log.error("No Trust Manager found!"); return false; } SaveTrustManager tm = new SaveTrustManager(defaultTrustManager); context.init(null, new TrustManager[]{tm}, null); SSLSocketFactory factory = context.getSocketFactory(); if (isLog) log.info("Opening connection -> {}:{} ...", host, port); // Initiate socket SSLSocket socket = null; Socket proxiedSocket = null; if (proxyHost != null) { if (isLog) log.info("Using Proxy -> {}:{} ...", proxyHost, proxyPort); try { if (proxyUser == null || proxyPass == null) { Proxy prx = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)); proxiedSocket = new Socket(prx); proxiedSocket.connect(new InetSocketAddress(host, port)); socket = (SSLSocket) factory.createSocket(proxiedSocket, host, port, true); } else { String proxyUserPass = String.format("%s:%s", proxyUser, proxyPass); String proxyConnect = "CONNECT " + host + ":" + port + " HTTP/1.0\r\n" + "Proxy-Authorization: Basic " + Base64.getEncoder().encodeToString(proxyUserPass.getBytes(ASCII7)).replace("\r\n", "") + "\r\n" + "Connection: close\r\n" + "\r\n"; if (isLog) log.info("Socket: {}", proxyConnect); proxiedSocket = new Socket(proxyHost, proxyPort); proxiedSocket.getOutputStream().write(proxyConnect.getBytes(ASCII7)); proxiedSocket.getOutputStream().flush(); this.readSocketConnection(proxiedSocket); proxiedSocket.getOutputStream().close(); socket = (SSLSocket) factory.createSocket(proxiedSocket, host, port, true); } } catch (Throwable t) { t.printStackTrace(); if (isLog) log.info("Proxy Error: {}", t.getMessage()); } } else { socket = (SSLSocket) factory.createSocket(host, port); } socket.setSoTimeout(10000); try { if (isLog) log.info("Initiating SSL handshake..."); socket.startHandshake(); socket.close(); if (proxiedSocket != null) proxiedSocket.close(); if (isLog) log.info("Certificate is already trusted..."); } catch (SSLException e) { //e.printStackTrace(System.out); success = false; if (isLog) log.warn("Socket Warning: {}", e.getMessage()); } X509Certificate[] chain = tm.chain; if (chain == null) { log.error("Could not obtain server certificate chain"); return false; } if (isLog) log.info("Server sent " + chain.length + " certificate(s):"); MessageDigest sha1 = MessageDigest.getInstance("SHA1"); MessageDigest md5 = MessageDigest.getInstance("MD5"); for (int i = 0; i < chain.length; i++) { X509Certificate cert = chain[i]; try { cert.checkValidity(); if (isLog) log.info("\tValid certificate..."); } catch (Throwable t) { log.info("\tCertificate invalid!"); continue; } sha1.update(cert.getEncoded()); md5.update(cert.getEncoded()); if (isLog) { log.info("\tIndex: {} Subject: {}", (i + 1), cert.getSubjectDN()); log.info("\tIssuer: {}", cert.getIssuerDN()); log.info("\tSHA-1: {}", toHexString(sha1.digest())); log.info("\tMD5: {}", toHexString(md5.digest())); } String alias = host + "-" + (i + 1); ks.setCertificateEntry(alias, cert); if (isLog) { log.info("{}", cert); log.info("Added certificate to keystore '{}' using alias '{}'", keystore, alias); } } OutputStream out = new FileOutputStream(keystore); ks.store(out, passphrase); out.close();
异常现象
程序在本地及其他虚拟机上均可正常运行,但在一台特定的Ubuntu 20(Debian)虚拟机上运行时,出现以下错误:
javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
额外异常细节:
- 程序化下载的证书转换为PEM格式后,与keytool导出的证书完全一致
- 即使通过程序化方式导入keytool手动下载的PEM文件,使用restTemplate发起请求时仍会出现上述相同错误
已执行的排查操作
- 启用
-Djavax.net.debug=ssl,handshake参数获取调试信息,未找到明确原因 - 通过wget/curl/openssl可正常访问目标HTTPS站点
- 确认该虚拟机未使用代理
- 已为cacerts文件设置全权限
- 验证目标站点返回的3条证书链中的1条,与keytool导出的一致
- 程序运行环境为docker化的
tomcat:9.0.35-jdk8-openjdk
内容的提问来源于stack exchange,提问作者Francesco Durante
相关产品推荐
相关产品推荐

