Skip to content

Commit 5cbb4d5

Browse files
committed
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 59cfa6e commit 5cbb4d5

5 files changed

Lines changed: 131 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;
@@ -141,6 +143,11 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa
141143
Button ok = mView.findViewById(R.id.ok);
142144
ok.setOnClickListener(new OnCertificateTrusted());
143145

146+
if (m509Certificate == null) {
147+
ok.setText(android.R.string.ok);
148+
mView.findViewById(R.id.question).setVisibility(View.GONE);
149+
}
150+
144151
Button cancel = mView.findViewById(R.id.btnCancel);
145152
cancel.setOnClickListener(new OnCertificateNotTrusted());
146153

@@ -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 {
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/operations/RemoteOperationResult.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,11 @@ public RemoteOperationResult(Exception e) {
148148

149149
} else if (e instanceof SSLException || e instanceof RuntimeException) {
150150
if (e instanceof SSLPeerUnverifiedException) {
151+
CertificateCombinedException sslPeerUnverifiedException =
152+
new CertificateCombinedException(null);
153+
sslPeerUnverifiedException.setSslPeerUnverifiedException((SSLPeerUnverifiedException) e);
154+
sslPeerUnverifiedException.initCause(e);
155+
mException = sslPeerUnverifiedException;
151156
mCode = ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED;
152157
} else {
153158
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)