使用 OkHttp 进行真实 HTTP 请求的 Robolectric 测试抛出 java.lang.NullPointerException:没有为 PKCS#12 KeyStore 提供密码

Posted

技术标签:

【中文标题】使用 OkHttp 进行真实 HTTP 请求的 Robolectric 测试抛出 java.lang.NullPointerException:没有为 PKCS#12 KeyStore 提供密码【英文标题】:Robolectric test that uses OkHttp for real HTTP requests throws java.lang.NullPointerException: No password supplied for PKCS#12 KeyStore 【发布时间】:2020-06-13 19:08:46 【问题描述】:

我正在使用 Robolectric 4.3.1 (testImplementation "org.robolectric:robolectric:4.3.1") 为我的集成测试创建一个 android sqlite 环境。我的系统使用OkHttp (implementation 'com.squareup.okhttp3:okhttp:3.14.7') 来处理真正的HTTP 请求。我没有使用MockWebServer

升级到 Android 10 SDK 后,我不得不将我的单元测试 JVM 更新到 JDK 9,per the Robolectric instructions:

在 Android API 29 上运行测试现在严格要求 Java9 运行时或更新版本。如果您在通过 Android Studio 在 API 29 上运行测试时看到有关不支持的 Java 版本的错误;您可以使用“运行配置”对话框中的“JRE”字段来配置更新的 Java 运行时。有关更多背景信息,请参阅https://developer.android.com/studio/run/rundebugconfig。

但是,现在我的集成测试失败了:

java.lang.NullPointerException: No password supplied for PKCS#12 KeyStore.

    at org.bouncycastle.jcajce.provider.keystore.pkcs12.PKCS12KeyStoreSpi.engineLoad(Unknown Source)
    at java.base/java.security.KeyStore.load(KeyStore.java:1479)
    at java.base/sun.security.ssl.TrustStoreManager$TrustAnchorManager.loadKeyStore(TrustStoreManager.java:367)
    at java.base/sun.security.ssl.TrustStoreManager$TrustAnchorManager.getTrustedCerts(TrustStoreManager.java:315)
    at java.base/sun.security.ssl.TrustStoreManager.getTrustedCerts(TrustStoreManager.java:59)
    at java.base/sun.security.ssl.TrustManagerFactoryImpl.engineInit(TrustManagerFactoryImpl.java:51)
    at java.base/javax.net.ssl.TrustManagerFactory.init(TrustManagerFactory.java:278)
    at okhttp3.internal.Util.platformTrustManager(Util.java:640)
    at okhttp3.OkHttpClient.<init>(OkHttpClient.java:228)
    at okhttp3.OkHttpClient.<init>(OkHttpClient.java:202)

我怎样才能再次进行真正的 HTTP 调用?

【问题讨论】:

【参考方案1】:

TLDR

要解决此问题,请将javax.net.ssl.trustStoreType 系统属性设置为JKS

-Djavax.net.ssl.trustStoreType=JKS

详情

我找到this workaround:

在尝试了很多事情之后,我终于找到了一种解决方法:

System.setProperty("javax.net.ssl.trustStore", "NONE")

MockWebServer()

测试通过了这个额外的配置。

但是,当我尝试该解决方法时,我的所有 HTTP 调用都失败了,而是使用以下 ConnectException

java.net.ConnectException: Failed to connect to myhost.com:443
    at okhttp3.internal.connection.RealConnection.connectSocket(RealConnection.java:265)
    at okhttp3.internal.connection.RealConnection.connect(RealConnection.java:183)
    at okhttp3.internal.connection.ExchangeFinder.findConnection(ExchangeFinder.java:224)
    at okhttp3.internal.connection.ExchangeFinder.findHealthyConnection(ExchangeFinder.java:108)
    at okhttp3.internal.connection.ExchangeFinder.find(ExchangeFinder.java:88)
    at okhttp3.internal.connection.Transmitter.newExchange(Transmitter.java:169)
    at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.java:41)
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:142)
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:117)
    at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.java:94)
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:142)
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:117)
    at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.java:93)
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:142)
    at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.java:88)
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:142)
    at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:117)
    at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.java:229)
    at okhttp3.RealCall.execute(RealCall.java:81)

我怀疑 OkHttp 正确地拒绝了所有 TLS 连接,因为它不信任它们,因为解决方法建议将 javax.net.ssl.trustStore 设置为 NONE

我还尝试通过this workaround 指定java 信任库的默认密码changeit

解决方法:-Djavax.net.ssl.trustStorePassword=changeit

但是在尝试实例化 OkHttpClient 时出现以下异常:

java.lang.AssertionError: No System TLS

    at okhttp3.internal.Util.platformTrustManager(Util.java:648)
    at okhttp3.OkHttpClient.<init>(OkHttpClient.java:228)
    at okhttp3.OkHttpClient.<init>(OkHttpClient.java:202)

这是由以下原因引起的:

Caused by: java.security.KeyStoreException: problem accessing trust store
    at java.base/sun.security.ssl.TrustManagerFactoryImpl.engineInit(TrustManagerFactoryImpl.java:75)
    at java.base/javax.net.ssl.TrustManagerFactory.init(TrustManagerFactory.java:278)
    at okhttp3.internal.Util.platformTrustManager(Util.java:640)
    ... 22 more
Caused by: java.io.IOException: stream does not represent a PKCS12 key store
    at org.bouncycastle.jcajce.provider.keystore.pkcs12.PKCS12KeyStoreSpi.engineLoad(Unknown Source)
    at java.base/java.security.KeyStore.load(KeyStore.java:1479)
    at java.base/sun.security.ssl.TrustStoreManager$TrustAnchorManager.loadKeyStore(TrustStoreManager.java:367)
    at java.base/sun.security.ssl.TrustStoreManager$TrustAnchorManager.getTrustedCerts(TrustStoreManager.java:315)
    at java.base/sun.security.ssl.TrustStoreManager.getTrustedCerts(TrustStoreManager.java:59)
    at java.base/sun.security.ssl.TrustManagerFactoryImpl.engineInit(TrustManagerFactoryImpl.java:51)
    ... 24 more

这是一个很好的线索。我开始使用 Android Studio 调试 JDK 9 源代码,我注意到当我使用 org.junit.runners.BlockJUnit4ClassRunner 运行时,我的 KeyStore.keystoreSpisun.security.pkcs12.PKCS12KeyStore$DualFormatPKCS12,但是当我使用 org.robolectric.RobolectricTestRunner 运行时,我的 KeyStore.keystoreSpiorg.bouncycastle.jcajce.provider.keystore.pkcs12.PKCS12KeyStoreSpi$BCPKCS12KeyStore .

根据JEP-229

此功能将默认密钥库类型从 JKS 更改为 PKCS12。默认情况下,将以 PKCS12 密钥库格式创建新的密钥库。现有的密钥库不会改变,密钥库应用程序可以继续明确指定所需的密钥库类型。

不得中断现有应用程序。密钥库往往是长期存在的,因此我们需要支持跨多个 JDK 版本的访问。访问由早期 JDK 版本创建的密钥库的应用程序必须在 JDK 9 上原样运行。同样,访问由 JDK 9 创建的密钥库的应用程序应该在早期 JDK 版本上原样运行。

这个要求是通过引入一个可以理解 JKS 和 PKCS12 格式的密钥库检测机制来实现的。在加载之前检查密钥库的格式以确定其类型,然后使用适当的密钥库实现来访问它。该机制默认启用,但可以根据需要禁用。

对这种密钥库检测机制的支持可能会向后移植到早期的 JDK 版本。

因此,经典的 $JAVA_HOME/lib/security/cacerts 仍然是 Java 密钥库,我可以验证:

$ file /Library/Java/JavaVirtualMachines/jdk-9.0.4.jdk/Contents/Home/lib/security/cacerts
/Library/Java/JavaVirtualMachines/jdk-9.0.4.jdk/Contents/Home/lib/security/cacerts: Java KeyStore

但是,由于 JDK 想要默认为 PKCS12 密钥库,如果以 PKCS12 读取文件失败,JDK 的 DualFormatPKCS12 将退回到 JKS。 Bouncycastle 假设当 javax.net.ssl.trustStoreTypepkcs12 时,这就是我们的意思。

因此,要解决此问题,请将javax.net.ssl.trustStoreType 系统属性设置为JKS

-Djavax.net.ssl.trustStoreType=JKS

我就此向bouncycastle 和robolectric 提出了问题。

【讨论】:

我需要在哪里设置 -Djavax.net.ssl.trustStoreType=JKS 为 gradle? 尝试将此添加到您的 gradle.properties 文件中:org.gradle.jvmargs=-Xms4g -Xmx4096m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -Djavax.net.ssl.trustStoreType=JKS 好消息,此问题已在 Robolectric 4.4 版中得到解决!【参考方案2】:

如果您在使用 Robolectric 时遇到此问题,请将版本升级到 4.4,因为这些问题已在 4.4 版中得到解决

testImplementation ('org.robolectric:robolectric:4.4')

【讨论】:

以上是关于使用 OkHttp 进行真实 HTTP 请求的 Robolectric 测试抛出 java.lang.NullPointerException:没有为 PKCS#12 KeyStore 提供密码的主要内容,如果未能解决你的问题,请参考以下文章

使用OkHttp进行网络同步异步操作

OkHttp 如何在不使用线程的情况下通过看似同步的 HTTP 连接执行并行 HTTP 请求?

Okhttp

如何使用 OkHttp/Retrofit 重试 HTTP 请求?

安卓网络请求之——OkHttp学习

OkHttp - 启用日志