Skip to content

Commit 354dd91

Browse files
committed
fix #772 - implement SSLHostnameVerifier on OkHttp
1 parent f479b2c commit 354dd91

File tree

3 files changed

+116
-21
lines changed

3 files changed

+116
-21
lines changed

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,12 @@
290290
<version>4.11</version>
291291
<scope>test</scope>
292292
</dependency>
293+
<dependency>
294+
<groupId>org.mockito</groupId>
295+
<artifactId>mockito-all</artifactId>
296+
<version>1.10.19</version>
297+
<scope>test</scope>
298+
</dependency>
293299
<dependency>
294300
<groupId>xmlunit</groupId>
295301
<artifactId>xmlunit</artifactId>

src/main/java/com/marklogic/client/impl/OkHttpServices.java

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@
117117
import javax.mail.internet.ContentDisposition;
118118
import javax.mail.internet.MimeMultipart;
119119
import javax.mail.util.ByteArrayDataSource;
120+
import javax.naming.InvalidNameException;
121+
import javax.naming.ldap.LdapName;
122+
import javax.naming.ldap.Rdn;
120123
import javax.net.ssl.HostnameVerifier;
121124
import javax.net.ssl.SSLContext;
122125
import javax.net.ssl.SSLException;
@@ -137,6 +140,7 @@
137140
import java.nio.file.Files;
138141
import java.nio.file.Path;
139142
import java.security.cert.Certificate;
143+
import java.security.cert.CertificateParsingException;
140144
import java.security.cert.X509Certificate;
141145
import java.util.ArrayList;
142146
import java.util.Calendar;
@@ -188,8 +192,38 @@ public boolean verify(String hostname, SSLSession session) {
188192
}
189193

190194
public void verify(String hostname, X509Certificate cert) throws SSLException {
191-
// verifier.verify(hostname, cns, subjectAlts);
192-
throw new IllegalStateException("Not yet implemented");
195+
ArrayList<String> cnArray = new ArrayList<String>();
196+
try {
197+
LdapName ldapDN = new LdapName(cert.getSubjectX500Principal().getName());
198+
for(Rdn rdn: ldapDN.getRdns()) {
199+
Object value = rdn.getValue();
200+
if ( "CN".equalsIgnoreCase(rdn.getType()) && value instanceof String ) {
201+
cnArray.add((String) value);
202+
}
203+
}
204+
205+
int type_dnsName = 2;
206+
int type_ipAddress = 7;
207+
ArrayList<String> subjectAltArray = new ArrayList<String>();
208+
Collection<List<?>> alts = cert.getSubjectAlternativeNames();
209+
if ( alts != null ) {
210+
for ( List<?> alt : alts ) {
211+
if ( alt != null && alt.size() == 2 && alt.get(1) instanceof String ) {
212+
Integer type = (Integer) alt.get(0);
213+
if ( type == type_dnsName || type == type_ipAddress ) {
214+
subjectAltArray.add((String) alt.get(1));
215+
}
216+
}
217+
}
218+
}
219+
String[] cns = cnArray.toArray(new String[cnArray.size()]);
220+
String[] subjectAlts = subjectAltArray.toArray(new String[subjectAltArray.size()]);
221+
verifier.verify(hostname, cns, subjectAlts);
222+
} catch(CertificateParsingException e) {
223+
throw new MarkLogicIOException(e);
224+
} catch(InvalidNameException e) {
225+
throw new MarkLogicIOException(e);
226+
}
193227
}
194228
}
195229

src/test/java/com/marklogic/client/test/SSLTest.java

Lines changed: 74 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,25 @@
1515
*/
1616
package com.marklogic.client.test;
1717

18+
import static org.junit.Assert.assertArrayEquals;
1819
import static org.junit.Assert.assertEquals;
20+
import static org.mockito.Mockito.mock;
21+
import static org.mockito.Mockito.when;
1922

2023
import javax.net.ssl.SSLContext;
24+
import javax.net.ssl.SSLException;
2125
import javax.net.ssl.TrustManager;
2226
import javax.net.ssl.X509TrustManager;
27+
import javax.security.auth.x500.X500Principal;
2328
import java.security.KeyManagementException;
2429
import java.security.NoSuchAlgorithmException;
30+
import java.security.cert.CertificateParsingException;
2531
import java.security.cert.X509Certificate;
26-
32+
import java.util.ArrayList;
33+
import java.util.Arrays;
34+
import java.util.Collection;
35+
import java.util.List;
36+
import java.util.concurrent.atomic.AtomicReference;
2737
import org.junit.Test;
2838

2939
import com.marklogic.client.DatabaseClient;
@@ -32,34 +42,19 @@
3242
import com.marklogic.client.DatabaseClientFactory.SSLHostnameVerifier;
3343
import com.marklogic.client.document.TextDocumentManager;
3444
import com.marklogic.client.io.StringHandle;
45+
import com.marklogic.client.impl.OkHttpServices;
3546

3647
public class SSLTest {
3748
@Test
3849
public void testSSLAuth() throws NoSuchAlgorithmException, KeyManagementException {
3950

40-
// create a trust manager
41-
// (note: a real application should verify certificates)
42-
TrustManager naiveTrustMgr = new X509TrustManager() {
43-
@Override
44-
public void checkClientTrusted(X509Certificate[] chain, String authType) {
45-
}
46-
@Override
47-
public void checkServerTrusted(X509Certificate[] chain, String authType) {
48-
}
49-
@Override
50-
public X509Certificate[] getAcceptedIssuers() {
51-
return new X509Certificate[0];
52-
}
53-
};
54-
5551
// create an SSL context
5652
SSLContext sslContext = SSLContext.getInstance("SSLv3");
57-
sslContext.init(null, new TrustManager[] { naiveTrustMgr }, null);
53+
sslContext.init(null, new TrustManager[] { mock(X509TrustManager.class) }, null);
5854

5955
// create the client
6056
DatabaseClient client = DatabaseClientFactory.newClient(Common.HOST, Common.PORT,
61-
"MyFooUser", "x", Authentication.DIGEST, sslContext, SSLHostnameVerifier.ANY);
62-
57+
"MyFooUser", "x", Authentication.DIGEST, sslContext, SSLHostnameVerifier.ANY);
6358

6459
String expectedException = "com.marklogic.client.MarkLogicIOException: " +
6560
"javax.net.ssl.SSLException: Unrecognized SSL message, plaintext connection?";
@@ -79,6 +74,66 @@ public X509Certificate[] getAcceptedIssuers() {
7974
exception = e.toString();
8075
}
8176
assertEquals(expectedException, exception);
77+
}
78+
79+
private static class SSLTestServices extends OkHttpServices {
80+
static class SSLTestHostnameVerifierAdapter extends SSLTestServices.HostnameVerifierAdapter {
81+
SSLTestHostnameVerifierAdapter(SSLHostnameVerifier hostnameVerifier) {
82+
super(hostnameVerifier);
83+
}
84+
}
85+
}
86+
87+
@Test
88+
public void testHostnameVerifier() throws SSLException, CertificateParsingException {
89+
// three things our SSLHostnameVerifier will capture
90+
final AtomicReference<String> capturedHost = new AtomicReference<String>();
91+
final AtomicReference<String[]> capturedCNs = new AtomicReference<String[]>();
92+
final AtomicReference<String[]> capturedSAs = new AtomicReference<String[]>();
93+
94+
// this adapter is an SSLHostnameVerifier we'd normally pass to withSSLHostnameVerifier
95+
SSLHostnameVerifier verifier = new SSLHostnameVerifier() {
96+
public void verify(String host, String[] cns, String[] alts) {
97+
capturedHost.set(host);
98+
capturedCNs.set(cns);
99+
capturedSAs.set(alts);
100+
}
101+
};
102+
// rather than attempt a real SSL connection, let's just test the implementation
103+
// with some mocks
104+
SSLTestServices.SSLTestHostnameVerifierAdapter adapter = new SSLTestServices.SSLTestHostnameVerifierAdapter(verifier);
105+
106+
// three things we'll pass and expect we can capture when verify is called
107+
String passedHost = "somehost";
108+
String[] passedCns = new String[] {"\u82b1\u5b50.co.jp", "bar.com", "foo.com"};
109+
String[] passedSas = new String[] {"a.foo.com", "104.198.163.83", "a.bar.com", "\u82b1\u5b50.co.jp"};
110+
// throw some extra information in like a real SSL cert would have
111+
// but what we're really wanting here are the CNs (common names)
112+
X500Principal principal = new X500Principal("C=US, ST=California, L=San Carlos, O=API Team, OU=test certificates, " +
113+
"CN=" + passedCns[2] + ", CN=" + passedCns[1] + ", CN=" + passedCns[0]);
114+
X509Certificate cert = mock(X509Certificate.class);
115+
when(cert.getSubjectX500Principal()).thenReturn(principal);
116+
117+
int type_dnsName = 2;
118+
int type_ipAddress = 7;
119+
// subject alts come out as a Collection of 2-entry lists where the first entry
120+
// is the Integer type and the second entry is the value
121+
// if the entry is 2 it's a DNS or 7 then it's an IP address
122+
// according to https://docs.oracle.com/javase/8/docs/api/java/security/cert/X509Certificate.html#getSubjectAlternativeNames--
123+
Collection<List<?>> listSas = new ArrayList<List<?>>();
124+
listSas.add(Arrays.asList(new Object[] {new Integer(type_dnsName), passedSas[0]}));
125+
listSas.add(Arrays.asList(new Object[] {new Integer(type_ipAddress), passedSas[1]}));
126+
listSas.add(Arrays.asList(new Object[] {new Integer(type_dnsName), passedSas[2]}));
127+
listSas.add(Arrays.asList(new Object[] {new Integer(type_dnsName), passedSas[3]}));
128+
when(cert.getSubjectAlternativeNames()).thenReturn(listSas);
129+
130+
// now that we have the cert all mocked with common names and subject alts, call the
131+
// implementation method HostnameVerifierAdapter.verify to make sure it calls
132+
// SSLHostnameVerifier.verify(String, String[], String[]) with the expected hostname, cns, and subjectAlts
133+
adapter.verify(passedHost, cert);
82134

135+
assertEquals(passedHost, capturedHost.get());
136+
assertArrayEquals(passedCns, capturedCNs.get());
137+
assertArrayEquals(passedSas, capturedSAs.get());
83138
}
84139
}

0 commit comments

Comments
 (0)