以编程方式从PEM获取KeyStore

如何以编程方式从包含证书和私钥的PEM文件中获取KeyStore? 我试图通过HTTPS连接向服务器提供客户端证书。 我已经确认,如果我使用openssl和keytool获取jks文件,我会动态加载客户端证书。 我甚至可以通过动态读取p12(PKCS12)文件来使其工作。

我正在研究使用BouncyCastle的PEMReader类,但我无法解决一些错误。 我正在运行带有-Djavax.net.debug = all选项的Java客户端和带有调试LogLevel的Apache Web服务器。 我不知道该寻找什么。 Apache错误日志表明:

... OpenSSL: Write: SSLv3 read client certificate B OpenSSL: Exit: error in SSLv3 read client certificate B Re-negotiation handshake failed: Not accepted by client!? 

Java客户端程序指示:

 ... main, WRITE: TLSv1 Handshake, length = 48 main, waiting for close_notify or alert: state 3 main, Exception while waiting for close java.net.SocketException: Software caused connection abort: recv failed main, handling exception: java.net.SocketException: Software caused connection abort: recv failed %% Invalidated: [Session-3, TLS_RSA_WITH_AES_128_CBC_SHA] main, SEND TLSv1 ALERT: fatal, description = unexpected_message ... 

客户端代码:

 public void testClientCertPEM() throws Exception { String requestURL = "https://mydomain/authtest"; String pemPath = "C:/Users/myusername/Desktop/client.pem"; HttpsURLConnection con; URL url = new URL(requestURL); con = (HttpsURLConnection) url.openConnection(); con.setSSLSocketFactory(getSocketFactoryFromPEM(pemPath)); con.setRequestMethod("GET"); con.setDoInput(true); con.setDoOutput(false); con.connect(); String line; BufferedReader reader = new BufferedReader(new InputStreamReader(con.getInputStream())); while((line = reader.readLine()) != null) { System.out.println(line); } reader.close(); con.disconnect(); } public SSLSocketFactory getSocketFactoryFromPEM(String pemPath) throws Exception { Security.addProvider(new BouncyCastleProvider()); SSLContext context = SSLContext.getInstance("TLS"); PEMReader reader = new PEMReader(new FileReader(pemPath)); X509Certificate cert = (X509Certificate) reader.readObject(); KeyStore keystore = KeyStore.getInstance("JKS"); keystore.load(null); keystore.setCertificateEntry("alias", cert); KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); kmf.init(keystore, null); KeyManager[] km = kmf.getKeyManagers(); context.init(km, null, null); return context.getSocketFactory(); } 

我注意到服务器在客户端是TLSv1时在日志中输出SSLv3。 如果我添加系统属性-Dhttps.protocols = SSLv3,那么客户端也将使用SSLv3,但我收到相同的错误消息。 我也尝试添加-Dsun.security.ssl.allowUnsafeRenegotiation = true而不改变结果。

我已经google了,这个问题的通常答案是首先使用openssl和keytool。 在我的情况下,我需要直接读取PEM。 我实际上已经移植了一个已经完成此操作的C ++程序,坦率地说,我很惊讶在Java中执行此操作是多么困难。 C ++代码:

  curlpp::Easy request; ... request.setOpt(new Options::Url(myurl)); request.setOpt(new Options::SslVerifyPeer(false)); request.setOpt(new Options::SslCertType("PEM")); request.setOpt(new Options::SslCert(cert)); request.perform(); 

我想到了。 问题是X509证书本身是不够的。 我还需要将私钥放入动态生成的密钥库中。 BouncyCastle PEMReader似乎不能一次性处理带有证书和私钥的PEM文件,但它可以单独处理每个部分。 我可以自己将PEM读入内存并将其分成两个独立的流,然后将每个流送入一个单独的PEMReader。 因为我知道我正在处理的PEM文件将首先获得证书,而私钥第二,我可以以稳健性为代价来简化代码。 我也知道END CERTIFICATE分隔符将始终用五个连字符包围。 适合我的实现是:

 protected static SSLSocketFactory getSocketFactoryPEM(String pemPath) throws Exception { Security.addProvider(new BouncyCastleProvider()); SSLContext context = SSLContext.getInstance("TLS"); byte[] certAndKey = fileToBytes(new File(pemPath)); String delimiter = "-----END CERTIFICATE-----"; String[] tokens = new String(certAndKey).split(delimiter); byte[] certBytes = tokens[0].concat(delimiter).getBytes(); byte[] keyBytes = tokens[1].getBytes(); PEMReader reader; reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(certBytes))); X509Certificate cert = (X509Certificate)reader.readObject(); reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(keyBytes))); PrivateKey key = (PrivateKey)reader.readObject(); KeyStore keystore = KeyStore.getInstance("JKS"); keystore.load(null); keystore.setCertificateEntry("cert-alias", cert); keystore.setKeyEntry("key-alias", key, "changeit".toCharArray(), new Certificate[] {cert}); KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); kmf.init(keystore, "changeit".toCharArray()); KeyManager[] km = kmf.getKeyManagers(); context.init(km, null, null); return context.getSocketFactory(); } 

更新 :看来这可以在没有BouncyCastle的情况下完成:

  byte[] certAndKey = fileToBytes(new File(pemPath)); byte[] certBytes = parseDERFromPEM(certAndKey, "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----"); byte[] keyBytes = parseDERFromPEM(certAndKey, "-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----"); X509Certificate cert = generateCertificateFromDER(certBytes); RSAPrivateKey key = generatePrivateKeyFromDER(keyBytes); 

 protected static byte[] parseDERFromPEM(byte[] pem, String beginDelimiter, String endDelimiter) { String data = new String(pem); String[] tokens = data.split(beginDelimiter); tokens = tokens[1].split(endDelimiter); return DatatypeConverter.parseBase64Binary(tokens[0]); } protected static RSAPrivateKey generatePrivateKeyFromDER(byte[] keyBytes) throws InvalidKeySpecException, NoSuchAlgorithmException { PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes); KeyFactory factory = KeyFactory.getInstance("RSA"); return (RSAPrivateKey)factory.generatePrivate(spec); } protected static X509Certificate generateCertificateFromDER(byte[] certBytes) throws CertificateException { CertificateFactory factory = CertificateFactory.getInstance("X.509"); return (X509Certificate)factory.generateCertificate(new ByteArrayInputStream(certBytes)); } 
Interesting Posts