以编程方式从 PEM 获取 KeyStore

Posted

技术标签:

【中文标题】以编程方式从 PEM 获取 KeyStore【英文标题】:Programmatically Obtain KeyStore from PEM 【发布时间】:2012-09-12 03:21:45 【问题描述】:

如何以编程方式从包含证书和私钥的 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();
 

我注意到服务器在日志中输出 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();

【问题讨论】:

对于一次性转换,您可以试用我们的 SecureBlackbox 产品,从 PEM 加载证书并将其保存为 JKS 或 PKCS#12 格式。当然,您也可以完全用 SecureBlackbox 替换 BouncyCastle。 如果您成功地将 PEM 作为密钥库加载,构造了 KeyManager、SSLContext 和 SSLSocketFactory,则必须假定问题出在 信任证书本身,而不是任何加载的东西。 嗨@all,请有人提供代码实现以使用pem文件在java中开发服务器和客户端。 【参考方案1】:

我想通了。问题是 X509Certificate 本身是不够的。我还需要将私钥放入动态生成的密钥库中。 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));      

【讨论】:

注意:我正在处理的 PEM 文件是由 openssl 生成的 非常感谢瑞恩...这正是我几乎一天所寻找的。很棒的东西.. 对我来说,分隔符是 "-----BEGIN RSA PRIVATE KEY-----" ,相应调整【参考方案2】:

虽然 Ryan 的回答效果很好,但我想为其他开发人员提供一个替代方案,因为我过去也面临过类似的挑战,我还需要处理 pem 格式的加密私钥。我创建了一个库来简化加载 pem 文件并从中创建 SSLSocketFactory 或 SSLContext,请参见此处:GitHub - SSLContext Kickstart 我希望你喜欢它:)

pem 文件可以使用以下 sn-p 加载:

var keyManager = PemUtils.loadIdentityMaterial("certificate-chain.pem", "private-key.pem");
var trustManager = PemUtils.loadTrustMaterial("some-trusted-certificate.pem");

var sslFactory = SSLFactory.builder()
          .withIdentityMaterial(keyManager)
          .withTrustMaterial(trustManager)
          .build();

var sslContext = sslFactory.getSslContext();
var sslSocketFactory = sslFactory.getSslSocketFactory();

回到您的主要问题,使用上面的 sn-p 不需要从 pem 文件创建密钥库对象。它会在幕后处理这个问题,并将其映射到 KeyManager 实例。

【讨论】:

以上是关于以编程方式从 PEM 获取 KeyStore的主要内容,如果未能解决你的问题,请参考以下文章

使用 OpenSSL 以编程方式将 .PEM 证书转换为 .PFX

是否可以在 Windows 服务器存储上存储 .PEM 文件并以编程方式读取它

如何在 Python 中以编程方式传递密码

以编程方式将证书导入 IIS?

如何以编程方式从动态 JObject 获取属性

以编程方式从iPhone获取IMEI号码[重复]