Skip to content

Commit 0c72514

Browse files
authored
Enhance SSL handling and testing
Enhance SSL handling and testing: Update SslUntrustedCertDialog for better certificate validation, remove hostname verifier in HttpClient, and add HttpClientTlsTest for SSL peer verification scenarios.
1 parent a388bc0 commit 0c72514

6 files changed

Lines changed: 139 additions & 2 deletions

File tree

opencloudApp/src/main/java/eu/opencloud/android/ui/dialog/SslUntrustedCertDialog.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,9 @@ public static SslUntrustedCertDialog newInstanceForFullSslError(CertificateCombi
8282
throw new IllegalArgumentException("Trying to create instance with parameter sslException == null");
8383
}
8484
SslUntrustedCertDialog dialog = new SslUntrustedCertDialog();
85-
dialog.m509Certificate = sslException.getServerCertificate();
85+
dialog.m509Certificate = sslException.getSslPeerUnverifiedException() == null
86+
? sslException.getServerCertificate()
87+
: null;
8688
dialog.mErrorViewAdapter = new CertificateCombinedExceptionViewAdapter(sslException);
8789
dialog.mCertificateViewAdapter = new X509CertificateViewAdapter(sslException.getServerCertificate());
8890
return dialog;
@@ -144,6 +146,11 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa
144146
Button cancel = mView.findViewById(R.id.btnCancel);
145147
cancel.setOnClickListener(new OnCertificateNotTrusted());
146148

149+
if (m509Certificate == null) {
150+
ok.setText(android.R.string.ok);
151+
mView.findViewById(R.id.question).setVisibility(View.GONE);
152+
cancel.setVisibility(View.GONE);
153+
}
147154
Button details = mView.findViewById(R.id.details_btn);
148155
details.setOnClickListener(new OnClickListener() {
149156

@@ -219,6 +226,8 @@ public void onClick(View v) {
219226
((OnSslUntrustedCertListener) activity).onFailedSavingCertificate();
220227
Timber.e(e, "Server certificate could not be saved in the known-servers trust store ");
221228
}
229+
} else if (mHandler == null) {
230+
((OnSslUntrustedCertListener) getActivity()).onCancelCertificate();
222231
}
223232
}
224233

opencloudComLibrary/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ dependencies {
2727
testImplementation 'org.robolectric:robolectric:4.15.1'
2828
// MockWebServer for HTTP integration tests
2929
testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.2'
30+
testImplementation 'com.squareup.okhttp3:okhttp-tls:4.9.2'
3031
// AndroidX test core to obtain application context in unit tests
3132
testImplementation 'androidx.test:core:1.5.0'
3233
debugImplementation 'com.facebook.stetho:stetho-okhttp3:1.6.0'

opencloudComLibrary/src/main/java/eu/opencloud/android/lib/common/http/HttpClient.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,6 @@ private OkHttpClient buildNewOkHttpClient(SSLSocketFactory sslSocketFactory, X50
125125
.connectTimeout(HttpConstants.DEFAULT_CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS)
126126
.followRedirects(false)
127127
.sslSocketFactory(sslSocketFactory, trustManager)
128-
.hostnameVerifier((asdf, usdf) -> true)
129128
.cookieJar(cookieJar)
130129
.build();
131130
}

opencloudComLibrary/src/main/java/eu/opencloud/android/lib/common/network/AdvancedX509TrustManager.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,16 @@ public void checkClientTrusted(X509Certificate[] certificates, String authType)
8484
mStandardTrustManager.checkClientTrusted(certificates, authType);
8585
}
8686

87+
public static final ThreadLocal<X509Certificate> sLastCert = new ThreadLocal<>();
88+
8789
/**
8890
* @see javax.net.ssl.X509TrustManager#checkServerTrusted(X509Certificate[],
8991
* String authType)
9092
*/
9193
public void checkServerTrusted(X509Certificate[] certificates, String authType) {
94+
if (certificates != null && certificates.length > 0) {
95+
sLastCert.set(certificates[0]);
96+
}
9297
if (!isKnownServer(certificates[0])) {
9398
CertificateCombinedException result = new CertificateCombinedException(certificates[0]);
9499
try {

opencloudComLibrary/src/main/java/eu/opencloud/android/lib/common/operations/RemoteOperationResult.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import eu.opencloud.android.lib.common.accounts.AccountUtils;
3333
import eu.opencloud.android.lib.common.http.HttpConstants;
3434
import eu.opencloud.android.lib.common.http.methods.HttpBaseMethod;
35+
import eu.opencloud.android.lib.common.network.AdvancedX509TrustManager;
3536
import eu.opencloud.android.lib.common.network.CertificateCombinedException;
3637
import okhttp3.Headers;
3738
import org.apache.commons.lang3.exception.ExceptionUtils;
@@ -148,6 +149,13 @@ public RemoteOperationResult(Exception e) {
148149

149150
} else if (e instanceof SSLException || e instanceof RuntimeException) {
150151
if (e instanceof SSLPeerUnverifiedException) {
152+
java.security.cert.X509Certificate lastCert = AdvancedX509TrustManager.sLastCert.get();
153+
AdvancedX509TrustManager.sLastCert.remove();
154+
CertificateCombinedException sslPeerUnverifiedException =
155+
new CertificateCombinedException(lastCert);
156+
sslPeerUnverifiedException.setSslPeerUnverifiedException((SSLPeerUnverifiedException) e);
157+
sslPeerUnverifiedException.initCause(e);
158+
mException = sslPeerUnverifiedException;
151159
mCode = ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED;
152160
} else {
153161
CertificateCombinedException se = getCertificateCombinedException(e);
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package eu.opencloud.android.lib.common.http
2+
3+
import android.content.Context
4+
import android.os.Build
5+
import androidx.test.core.app.ApplicationProvider
6+
import eu.opencloud.android.lib.common.network.CertificateCombinedException
7+
import eu.opencloud.android.lib.common.network.NetworkUtils
8+
import eu.opencloud.android.lib.common.operations.RemoteOperationResult
9+
import okhttp3.Request
10+
import okhttp3.mockwebserver.MockResponse
11+
import okhttp3.mockwebserver.MockWebServer
12+
import okhttp3.tls.HandshakeCertificates
13+
import okhttp3.tls.HeldCertificate
14+
import org.junit.After
15+
import org.junit.Assert.assertEquals
16+
import org.junit.Assert.assertNotNull
17+
import org.junit.Assert.assertSame
18+
import org.junit.Assert.assertThrows
19+
import org.junit.Assert.assertTrue
20+
import org.junit.Before
21+
import org.junit.Test
22+
import org.junit.runner.RunWith
23+
import org.robolectric.RobolectricTestRunner
24+
import org.robolectric.annotation.Config
25+
import java.lang.reflect.Field
26+
import javax.net.ssl.SSLPeerUnverifiedException
27+
28+
@RunWith(RobolectricTestRunner::class)
29+
@Config(sdk = [Build.VERSION_CODES.O], manifest = Config.NONE)
30+
class HttpClientTlsTest {
31+
32+
private val context by lazy { ApplicationProvider.getApplicationContext<Context>() }
33+
private lateinit var server: MockWebServer
34+
35+
@Before
36+
fun setUp() {
37+
resetKnownServersStore()
38+
server = MockWebServer()
39+
}
40+
41+
@After
42+
fun tearDown() {
43+
runCatching { server.shutdown() }
44+
resetKnownServersStore()
45+
}
46+
47+
@Test
48+
fun `rejects trusted certificate for the wrong hostname`() {
49+
val wrongHostnameCertificate = HeldCertificate.Builder()
50+
.commonName(WRONG_HOSTNAME)
51+
.addSubjectAlternativeName(WRONG_HOSTNAME)
52+
.build()
53+
val serverCertificates = HandshakeCertificates.Builder()
54+
.heldCertificate(wrongHostnameCertificate)
55+
.build()
56+
57+
server.useHttps(serverCertificates.sslSocketFactory(), false)
58+
server.enqueue(MockResponse().setResponseCode(200).setBody("ok"))
59+
server.start()
60+
61+
NetworkUtils.addCertToKnownServersStore(wrongHostnameCertificate.certificate, context)
62+
63+
val request = Request.Builder()
64+
.url(server.url("/"))
65+
.build()
66+
67+
val thrown = assertThrows(Exception::class.java) {
68+
TestHttpClient(context).okHttpClient.newCall(request).execute().use { }
69+
}
70+
71+
assertNotNull(findCause<SSLPeerUnverifiedException>(thrown))
72+
}
73+
74+
@Test
75+
fun `wraps hostname mismatch as certificate combined exception`() {
76+
val peerUnverifiedException = SSLPeerUnverifiedException("Hostname localhost not verified")
77+
78+
val result = RemoteOperationResult<Unit>(peerUnverifiedException)
79+
80+
assertEquals(
81+
RemoteOperationResult.ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED,
82+
result.code
83+
)
84+
assertTrue(result.exception is CertificateCombinedException)
85+
86+
val combinedException = result.exception as CertificateCombinedException
87+
assertSame(peerUnverifiedException, combinedException.sslPeerUnverifiedException)
88+
}
89+
90+
private inline fun <reified T : Throwable> findCause(throwable: Throwable): T? {
91+
var current: Throwable? = throwable
92+
while (current != null) {
93+
if (current is T) {
94+
return current
95+
}
96+
current = current.cause
97+
}
98+
return null
99+
}
100+
101+
private fun resetKnownServersStore() {
102+
context.deleteFile(KNOWN_SERVERS_STORE_FILE)
103+
104+
val field: Field = NetworkUtils::class.java.getDeclaredField("mKnownServersStore")
105+
field.isAccessible = true
106+
field.set(null, null)
107+
}
108+
109+
private class TestHttpClient(context: Context) : HttpClient(context)
110+
111+
companion object {
112+
private const val WRONG_HOSTNAME = "wrong.example.test"
113+
private const val KNOWN_SERVERS_STORE_FILE = "knownServers.bks"
114+
}
115+
}

0 commit comments

Comments
 (0)