如何读取 OkHttpClient 的证书链

How to read a certificate chain for OkHttpClient

KeyFactory keyFactory = KeyFactory.getInstance("RSA");
byte[] keyBytes = Files.readAllBytes((Paths.get("/path/to/chain.pem")));
X509EncodedKeySpec spec =
    new X509EncodedKeySpec(keyBytes);
PublicKey publicKey = keyFactory.generatePublic(spec);

byte[] privateKeyBytes = Files.readAllBytes(Paths.get("/path/to/key.pem"));
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
PrivateKey privateKey =  keyFactory.generatePrivate(pkcs8EncodedKeySpec);
HeldCertificate cert = new HeldCertificate.Builder().keyPair(publicKey, privateKey).build();
HandshakeCertificates clientCertificates = new HandshakeCertificates.Builder()
    .heldCertificate(cert)
    .build();
OkHttpClient client = new OkHttpClient.Builder()
    .sslSocketFactory(clientCertificates.sslSocketFactory(), clientCertificates.trustManager())
    .build();

我正在尝试使用 Okhttpclient 进行客户端身份验证,这就是我目前拥有的。 chain.pem 文件有多个形式为

的证书
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----

keyFactory.generatePublic 方法在尝试解析证书链时失败并返回 Caused by: java.security.InvalidKeyException: invalid key format。我如何为 OkhttpClient 解析这个证书链?我是否必须将链拆分为多个 pems?

这些是证书,而不是一个甚至几个公钥。证书包含公钥,但证书不是公钥,不能作为公钥读取。此外,这些证书是出于某种原因颁发给您的;如果您使用提供的证书链(和私钥),服务器将信任它们,但如果您生成自己的自签名证书,即使是相同的密钥,这就是您的 HeldCertificate.Builder() 会做的,服务器将不会'不要信任该证书,因为它不是由有效的 CA 颁发的。数字证书有时被类比为护照;如果你有你的政府签发的带有你的名字和照片的护照,其他国家(和国内实体)通常会接受它作为你身份的证明,但如果你在上面写上你的名字和单词 'passport'一张纸贴上你自己的照片,没有人会接受它作为证据——这就是自签名证书的样子。

阅读 Java 中的文件 相当容易。证书最简单——CertificateFactory 可以直接读取该格式:

byte[] certBytes = Files.readAllBytes(Paths.get("/path/to/chain.pem"));
InputStream certstream = new ByteArrayInputStream(certBytes);
X509Certificate[] certs = CertificateFactory.getInstance("X.509")
    .generateCertificates(certstream) .toArray(new X509Certificate[0]);
certstream.close(); // or use try-resources if you prefer

钥匙可能更难;如果它是 PEM 格式,如名称 key.pem 建议 KeyFactory(不同于 CertificateFactory)不读取 PEM。如果它是 一种特定的 PEM 格式 即 PKCS8 根据 RFC7468 section 10 未加密——由 -----BEGIN PRIVATE KEY----- 和类似的 END 标记(如图所示)在BEGIN/END和PRIVATE KEY之间没有其他词——你可以这样转换它:

byte[] pkeyPEM = Files.readAllBytes(Paths.get("/path/to/key.pem"));
byte[] pkeyDER = Base64.getDecoder().decode( new String(pkeyBytes)
    .replaceAll("-----(BEGIN|END) PRIVATE KEY-----","").replaceAll("\r?\n","") ); 
RSAPrivateKey privateKey =  (RSAPrivateKey) KeyFactory.getInstance("RSA")
    .generatePrivate(new PKCS8EncodedKeySpec(pkeyDER));

但是,至少有十几种其他 PEM 格式用于 Java 无法直接读取的私钥。 其中大多数 由 OpenSSL 使用,如果 BEGINEND 行显示 ENCRYPTED PRIVATE KEY{RSA|DSA|EC} PRIVATE KEY,您可以使用 openssl 命令行将其转换为 Java 可以处理的格式:

openssl pkey -in badPEM -out goodPEM # only 1.0.0 up but that is now very common
openssl pkcs8 -topk8 -nocrypt -in badPEM -out goodPEM # even old versions

此外,如果您将 -outform der 添加到其中任何一个(请相应地更改文件名以避免混淆),您不再需要 de-PEM 步骤,您可以将 Files.readAllBytes 结果直接在 PKCS8EncodedKeySpec。如果您的密钥文件是其他任何东西,那将更加困难或可能是不可能的;您必须提供更多详细信息。

在 OkHttp 中使用 坦率地说 this API 在我看来它是由不知道自己在做什么的人设计的;将 KeyPairCertificate 一起使用完全没有意义。但从逻辑上讲,这个 应该 有效:

RSAPublicKey publicKey = (RSAPublicKey) certs[0].getPublicKey(); 
if( ! publicKey.getModulus().equals( privateKey.getModulus() ) )
    throw new Exception ("key does not match cert"); // or other error handling
HeldCertificate client1 = new HeldCertificate( new KeyPair(publicKey, privateKey), certs[0]);
HandshakeCertificates client2 = new HandshakeCertificates.Builder()
    .addPlatformTrustedCertificates() // or more specific if necessary
    .heldCertificate(client1,Arrays.copyOfRange(certs,1,certs.length) )
    .build();
// use client2 as you do now to set sslSocketFactory and trustManager