如何将 SSL 证书添加到 okHttp 的双向认证连接中?

5
我已经有.pem和.key文件,但不想在本地安装/导入它们,只想告诉客户端使用这些文件,是否可能? 这不是一个自签名的证书。
基本上,在我的curl中,我正在做这样的事情:
curl --key mykey.key --cert mycert.pem https://someurl.com/my-endpoint

我已经查看了这个答案,以及这篇文章和代码示例。它们与Retrofit中的SSL证书相关。如果您已经有了一个okHttpClient对象,那么您可以使用CustomTrust类来创建一个带有自定义信任管理器的SSL Socket Factory。这将允许您通过HTTPS请求访问受保护的API端点。
val okHttpClient = OkHttpClient.Builder()
    .sslSocketFactory(?, ?) //here I tried to call sslSocketFactory, trustManager following the example from the CustomTrust.java
    .build()

有解决方案的想法吗?

已经检查了这份文档,但是 SSL 部分并没有完成,也没有示例。

https://square.github.io/okhttp/https/#customizing-trusted-certificates-kt-java

因此,我尝试按照 okhttp 示例进行以下操作:

private fun trustedCertificatesInputStream(): InputStream {
        val comodoRsaCertificationAuthority = (""
            + "-----BEGIN CERTIFICATE-----\n" +
            "-----END CERTIFICATE-----")
        return Buffer()
            .writeUtf8(comodoRsaCertificationAuthority)
            .inputStream()
    }


    val loggingInterceptor = HttpLoggingInterceptor().apply {
        level = HttpLoggingInterceptor.Level.BODY
    }


    fun createClient() : OkHttpClient {

        val trustManager: X509TrustManager
        val sslSocketFactory: SSLSocketFactory
        try {
            trustManager = trustManagerForCertificates(trustedCertificatesInputStream())
            val sslContext = SSLContext.getInstance("TLS")
            sslContext.init(null, arrayOf<TrustManager>(trustManager), null)
            sslSocketFactory = sslContext.socketFactory



        } catch (e: GeneralSecurityException) {
            throw RuntimeException(e)
        }
        return OkHttpClient.Builder()
            .sslSocketFactory(sslSocketFactory, trustManager)
            .connectTimeout(45, TimeUnit.SECONDS)
            .readTimeout(45, TimeUnit.SECONDS)
            .protocols(listOf(Protocol.HTTP_1_1))
            .addInterceptor(loggingInterceptor)
            .build()
    }


    @Throws(GeneralSecurityException::class)
    private fun trustManagerForCertificates(input: InputStream): X509TrustManager {
        val certificateFactory: CertificateFactory = CertificateFactory.getInstance("X.509")
        val certificates: Collection<Certificate?> = certificateFactory.generateCertificates(input)
        val password = "password".toCharArray() // Any password will work.
        val keyStore = newEmptyKeyStore(password)

        for ((index, certificate) in certificates.withIndex()) {
            val certificateAlias = index.toString()
            keyStore.setCertificateEntry(certificateAlias, certificate)
        }
        // Use it to build an X509 trust manager.
        val keyManagerFactory: KeyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
        keyManagerFactory.init(keyStore, password)

        val trustManagerFactory: TrustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
        trustManagerFactory.init(keyStore)

        val trustManagers: Array<TrustManager> = trustManagerFactory.getTrustManagers()
        return trustManagers[0]!! as X509TrustManager
    }

    @Throws(GeneralSecurityException::class)
    private fun newEmptyKeyStore(password: CharArray): KeyStore {
        return try {
            val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
            val inputStream: InputStream? = null // By convention, 'null' creates an empty key store.
            keyStore.load(inputStream, password)
            keyStore
        } catch (e: IOException) {
            throw AssertionError(e)
        }

    }

我遇到了一个错误,错误信息为:
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

搜索与错误相关的信息,看起来我应该在本地安装 SSL,但我不希望这样做,因为在服务器上不能以这种方式安装它,有没有办法让它工作?

2个回答

6
看了您的curl命令和java代码后,我发现它们没有进行相同的配置。让我们首先看一下您的curl命令并分析它: 您的curl命令: curl --key mykey.key --cert mycert.pem https://someurl.com/my-endpoint curl选项解释:
  • key => 客户端私钥
  • cert => 客户端证书链
  • cacert => 受信任服务器证书
如果您的curl传递了,那么您的cacert选项为空,这意味着它基于curl中提供的默认受信任证书匹配了服务器证书。curl中的默认受信任证书可能与java中的默认受信任证书不同,因此可能会导致不同的行为。我建议将服务器证书添加到您的curl命令和java代码片段中。
根据您的curl命令,我们可以将选项转换为java: 从curl到java的映射
  • key => KeyManager
  • cert => KeyManager
  • cacert => TrustManager
默认java类对解析PEM格式的私钥的支持有限。据我所知,它只能解析未加密的私钥。我可以推荐使用Bouncy Castle轻松解析加密的PEM格式私钥。下面的示例假设您有一个未加密的私钥。 选项1
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;

public class App {

    public static void main(String[] args) throws Exception {
        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");

        InputStream trustedCertificateAsInputStream = Files.newInputStream(Paths.get("/path/to/server-certificate.pem"), StandardOpenOption.READ);
        Certificate trustedCertificate = certificateFactory.generateCertificate(trustedCertificateAsInputStream);
        KeyStore trustStore = createEmptyKeyStore("secret".toCharArray());
        trustStore.setCertificateEntry("server-certificate", trustedCertificate);

        String privateKeyContent = new String(Files.readAllBytes(Paths.get("/path/to/mykey.key")), Charset.defaultCharset())
                .replace("-----BEGIN PRIVATE KEY-----", "")
                .replaceAll(System.lineSeparator(), "")
                .replace("-----END PRIVATE KEY-----", "");

        byte[] privateKeyAsBytes = Base64.getDecoder().decode(privateKeyContent);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyAsBytes);

        InputStream certificateChainAsInputStream = Files.newInputStream(Paths.get("/path/to/mycert.pem"), StandardOpenOption.READ);
        Certificate certificateChain = certificateFactory.generateCertificate(certificateChainAsInputStream);

        KeyStore identityStore = createEmptyKeyStore("secret".toCharArray());
        identityStore.setKeyEntry("client", keyFactory.generatePrivate(keySpec), "secret".toCharArray(), new Certificate[]{certificateChain});

        trustedCertificateAsInputStream.close();
        certificateChainAsInputStream.close();

        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(trustStore);
        TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();

        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(identityStore, "secret".toCharArray());
        KeyManager[] keyManagers = keyManagerFactory.getKeyManagers();

        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(keyManagers, trustManagers, null);

        SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();

        OkHttpClient okHttpClient = OkHttpClient.Builder()
                .sslSocketFactory(sslSocketFactory, trustManagers[0])
                .build();
    }

    public static KeyStore createEmptyKeyStore(char[] keyStorePassword) throws CertificateException, NoSuchAlgorithmException, IOException, KeyStoreException {
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(null, keyStorePassword);
        return keyStore;
    }
}

这段代码有点啰嗦,但它应该能解决你的问题。

选项2

如果您更喜欢简洁的替代方案,可以尝试以下代码片段:

X509ExtendedKeyManager keyManager = PemUtils.loadIdentityMaterial(Paths.get("/path/to/mycert.pem"), Paths.get("/path/to/mycert.pem"));
X509ExtendedTrustManager trustManager = PemUtils.loadTrustMaterial(Paths.get("/path/to/server-certificate.pem"));

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

SSLSocketFactory sslSocketFactory = sslFactory.getSslSocketFactory();
X509ExtendedtrustManager trustManager = sslFactory.getTrustManager().orElseThrow();

OkHttpClient okHttpClient = OkHttpClient.Builder()
          .sslSocketFactory(sslSocketFactory, trustManager)
          .build();

上述库由我维护,您可以在此找到它:GitHub - SSLContext Kickstart

谢谢@Hakan54,这非常有帮助!要获取OP原始curl示例中缺失的受信任服务器(根)证书,可以使用https://serverfault.com/a/661982-如果您信任您的网络的话;-)不要将其导向`openssl x509,而是将其导向cat`或文件以获取证书链。在使用PemUtils读取时,定界符之间的所有内容都将被忽略。 - cheppsn
@cheppsn,您也可以尝试使用以下示例从CLI中提取服务器证书,请参见此处以获取更多信息:https://dev59.com/Smsz5IYBdhLWcg3weHgA#68277430 - Hakan54

3
我猜你想配置TLS相互认证,那么这个密钥就是用来做什么的吗?
可以查看okhttp-tls,它提供API将证书和私钥转换为相应的Java对象。

2
嗨,感谢你的回答。我不太理解我应该如何实现它的方式,因为我没有中间证书,你有什么想法可以让它工作吗? - jpganz18
这个部分记录了如何添加根证书,但没有提到密钥。还有一个HeldCertificate类,但没有使用示例;我猜那就是你需要的。 - Luciano

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接