web-dev-qa-db-ja.com

プログラムでPEMからキーストアを取得する

証明書と秘密キーの両方を含むPEMファイルからプログラムでキーストアを取得するにはどうすればよいですか? HTTPS接続でサーバーにクライアント証明書を提供しようとしています。 opensslとkeytoolを使用して動的にロードするjksファイルを取得すると、クライアント証明書が機能することを確認しました。さらに、p12(PKCS12)ファイルを動的に読み取ることで機能させることもできます。

BouncyCastleのPEMReaderクラスの使用を検討していますが、いくつかのエラーを回避できません。 Javaクライアントに-Djavax.net.debug = allオプションを付けて実行し、Apache WebサーバーにデバッグLogLevelを付けて実行しています。何を探すべきかわかりません。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();
} 

サーバーがログにSSLv3を出力しているのに、クライアントがTLSv1であることに気付きました。システムプロパティ-Dhttps.protocols = SSLv3を追加すると、クライアントもSSLv3を使用しますが、同じエラーメッセージが表示されます。また、結果を変更せずに-Dsun.security.ssl.allowUnsafeRenegotiation = trueを追加してみました。

私はググってみましたが、この質問に対する通常の答えは、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();
26
Ryan

私はそれを考え出した。問題は、X509Certificateだけでは十分ではないことです。動的に生成されたキーストアにも秘密鍵を配置する必要がありました。 BouncyCastle PEMReaderは、証明書と秘密鍵の両方を含むPEMファイルを一度にすべて処理できるようには見えませんが、各部分を個別に処理できます。 PEMを自分でメモリに読み込み、2つの個別のストリームに分割して、それぞれを個別のPEMReaderにフィードできます。私が扱っているPEMファイルには、最初に証明書があり、次に秘密鍵があることを知っているので、堅牢性を犠牲にしてコードを単純化できます。また、END CERTIFICATE区切り文字が常に5つのハイフンで囲まれることも知っています。私のために働く実装は:

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

pdate:これは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));      
}
34
Ryan