Robolectric 使用 OkHttp 进行真实 HTTP 请求的测试抛出 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
我正在使用 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:
Running tests on Android API 29 now strictly requires a Java9 runtime or newer. If you are seeing errors about unsupported Java version when running tests on API 29 via Android Studio; you can use the 'JRE' field in the Run Configuration dialog to configure a newer Java runtime. See https://developer.android.com/studio/run/rundebugconfig for more background.
但是,现在我的集成测试失败了:
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 调用?
TLDR
要解决此问题,请将 javax.net.ssl.trustStoreType
系统 属性 设置为 JKS
:
-Djavax.net.ssl.trustStoreType=JKS
详情
我发现 this workaround:
After trying many things I finally found one workaround:
System.setProperty("javax.net.ssl.trustStore", "NONE")
MockWebServer()
The tests are passing with this additional configuration.
但是,当我尝试该解决方法时,我的所有 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
Workaround: -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.keystoreSpi
是 sun.security.pkcs12.PKCS12KeyStore$DualFormatPKCS12
, but when I ran with org.robolectric.RobolectricTestRunner
my KeyStore.keystoreSpi
was a org.bouncycastle.jcajce.provider.keystore.pkcs12.PKCS12KeyStoreSpi$BCPKCS12KeyStore
.
根据JEP-229
:
This feature changes the default keystore type from JKS to PKCS12. By default, new keystores will be created in the PKCS12 keystore format. Existing keystores will not change and keystore applications can continue to explicitly specify the keystore type they require.
Existing applications must not be disrupted. Keystores tend to be long-lived, so we need to support access across several JDK releases. Applications that access keystores created by earlier JDK releases must run unaltered on JDK 9. Similarly, applications that access keystores created by JDK 9 should run unaltered on earlier JDK releases.
This requirement is achieved by introducing a keystore detection mechanism that understands both the JKS and PKCS12 formats. A keystore's format is examined before it is loaded to determine its type and then the appropriate keystore implementation is used to access it. The mechanism is enabled by default but can be disabled if required.
Support for this keystore-detection mechanism may be backported to earlier JDK releases.
因此,经典 $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
密钥库,因此 JDK 的 DualFormatPKCS12
将退回到 JKS
如果阅读PKCS12
文件失败。 Bouncycastle 假设当 javax.net.ssl.trustStoreType
是 pkcs12
时,这正是我们的意思。
因此,要解决此问题,请将 javax.net.ssl.trustStoreType
系统 属性 设置为 JKS
:
-Djavax.net.ssl.trustStoreType=JKS
我就此向 bouncycastle and robolectric 提交了问题。
如果您在使用 Robolectric 时遇到此问题,请将版本升级到 4.4,因为这些问题已在版本 4.4
中得到解决
testImplementation ('org.robolectric:robolectric:4.4')
我正在使用 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:
Running tests on Android API 29 now strictly requires a Java9 runtime or newer. If you are seeing errors about unsupported Java version when running tests on API 29 via Android Studio; you can use the 'JRE' field in the Run Configuration dialog to configure a newer Java runtime. See https://developer.android.com/studio/run/rundebugconfig for more background.
但是,现在我的集成测试失败了:
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 调用?
TLDR
要解决此问题,请将 javax.net.ssl.trustStoreType
系统 属性 设置为 JKS
:
-Djavax.net.ssl.trustStoreType=JKS
详情
我发现 this workaround:
After trying many things I finally found one workaround:
System.setProperty("javax.net.ssl.trustStore", "NONE") MockWebServer()
The tests are passing with this additional configuration.
但是,当我尝试该解决方法时,我的所有 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
Workaround:
-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.keystoreSpi
是 sun.security.pkcs12.PKCS12KeyStore$DualFormatPKCS12
, but when I ran with org.robolectric.RobolectricTestRunner
my KeyStore.keystoreSpi
was a org.bouncycastle.jcajce.provider.keystore.pkcs12.PKCS12KeyStoreSpi$BCPKCS12KeyStore
.
根据JEP-229
:
This feature changes the default keystore type from JKS to PKCS12. By default, new keystores will be created in the PKCS12 keystore format. Existing keystores will not change and keystore applications can continue to explicitly specify the keystore type they require.
Existing applications must not be disrupted. Keystores tend to be long-lived, so we need to support access across several JDK releases. Applications that access keystores created by earlier JDK releases must run unaltered on JDK 9. Similarly, applications that access keystores created by JDK 9 should run unaltered on earlier JDK releases.
This requirement is achieved by introducing a keystore detection mechanism that understands both the JKS and PKCS12 formats. A keystore's format is examined before it is loaded to determine its type and then the appropriate keystore implementation is used to access it. The mechanism is enabled by default but can be disabled if required.
Support for this keystore-detection mechanism may be backported to earlier JDK releases.
因此,经典 $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
密钥库,因此 JDK 的 DualFormatPKCS12
将退回到 JKS
如果阅读PKCS12
文件失败。 Bouncycastle 假设当 javax.net.ssl.trustStoreType
是 pkcs12
时,这正是我们的意思。
因此,要解决此问题,请将 javax.net.ssl.trustStoreType
系统 属性 设置为 JKS
:
-Djavax.net.ssl.trustStoreType=JKS
我就此向 bouncycastle and robolectric 提交了问题。
如果您在使用 Robolectric 时遇到此问题,请将版本升级到 4.4,因为这些问题已在版本 4.4
中得到解决testImplementation ('org.robolectric:robolectric:4.4')