From 79e75303908d2eb41518821be46d4ae65b914c3e Mon Sep 17 00:00:00 2001 From: Piotr Fus Date: Mon, 29 Jan 2018 21:14:11 +0100 Subject: [PATCH] Add https client authentication Change-Id: Ib04eaa8e534e2ac83fb4c11a169f110a5f3a580d --- .../xsd/pl/touk/mockserver/api/common.xsd | 3 + .../tests/MockServerHttpsTest.groovy | 99 ++++++++++++++++-- .../src/test/resources/trusted.jks | Bin 0 -> 2235 bytes .../src/test/resources/truststore.jks | Bin 0 -> 1880 bytes .../src/test/resources/untrusted.jks | Bin 0 -> 2255 bytes .../server/HttpServerWrapper.groovy | 29 ++++- .../touk/mockserver/server/HttpsConfig.groovy | 28 +++++ 7 files changed, 145 insertions(+), 14 deletions(-) create mode 100644 mockserver-tests/src/test/resources/trusted.jks create mode 100644 mockserver-tests/src/test/resources/truststore.jks create mode 100644 mockserver-tests/src/test/resources/untrusted.jks create mode 100644 mockserver/src/main/groovy/pl/touk/mockserver/server/HttpsConfig.groovy diff --git a/mockserver-api/src/main/xsd/pl/touk/mockserver/api/common.xsd b/mockserver-api/src/main/xsd/pl/touk/mockserver/api/common.xsd index 953b839..673be7d 100644 --- a/mockserver-api/src/main/xsd/pl/touk/mockserver/api/common.xsd +++ b/mockserver-api/src/main/xsd/pl/touk/mockserver/api/common.xsd @@ -24,6 +24,9 @@ + + + diff --git a/mockserver-tests/src/test/groovy/pl/touk/mockserver/tests/MockServerHttpsTest.groovy b/mockserver-tests/src/test/groovy/pl/touk/mockserver/tests/MockServerHttpsTest.groovy index 91b851a..0bef47f 100644 --- a/mockserver-tests/src/test/groovy/pl/touk/mockserver/tests/MockServerHttpsTest.groovy +++ b/mockserver-tests/src/test/groovy/pl/touk/mockserver/tests/MockServerHttpsTest.groovy @@ -16,8 +16,10 @@ import pl.touk.mockserver.client.Util import pl.touk.mockserver.server.HttpMockServer import spock.lang.Shared import spock.lang.Specification +import spock.lang.Unroll import javax.net.ssl.SSLContext +import javax.net.ssl.SSLHandshakeException import java.security.KeyStore class MockServerHttpsTest extends Specification { @@ -27,14 +29,20 @@ class MockServerHttpsTest extends Specification { HttpMockServer httpMockServer @Shared - SSLContext sslContext = SSLContexts.custom() + SSLContext noClientAuthSslContext = SSLContexts.custom() .loadTrustMaterial(trustStore()) .build() @Shared - CloseableHttpClient client = HttpClients.custom() - .setHostnameVerifier(SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER) - .setSslcontext(sslContext) + SSLContext trustedCertificateSslContext = SSLContexts.custom() + .loadKeyMaterial(trustedCertificateKeystore(), 'changeit'.toCharArray()) + .loadTrustMaterial(trustStore()) + .build() + + @Shared + SSLContext untrustedCertificateSslContext = SSLContexts.custom() + .loadKeyMaterial(untrustedCertificateKeystore(), 'changeit'.toCharArray()) + .loadTrustMaterial(trustStore()) .build() def setup() { @@ -64,17 +72,90 @@ class MockServerHttpsTest extends Specification { when: HttpPost restPost = new HttpPost('https://localhost:10443/testEndpoint') restPost.entity = new StringEntity('', ContentType.create("text/xml", "UTF-8")) - CloseableHttpResponse response = client.execute(restPost) + CloseableHttpResponse response = client(noClientAuthSslContext).execute(restPost) then: GPathResult restPostResponse = Util.extractXmlResponse(response) restPostResponse.name() == 'goodResponse-request' - and: - remoteMockServer.removeMock('testHttps')?.size() == 1 + } + + def 'should handle HTTPS server with client auth' () { + expect: + remoteMockServer.addMock(new AddMock( + name: 'testHttps', + path: 'testEndpoint', + port: 10443, + predicate: '''{req -> req.xml.name() == 'request'}''', + response: '''{req -> ""}''', + https: new Https( + keyPassword: 'changeit', + keystorePassword: 'changeit', + keystorePath: MockServerHttpsTest.classLoader.getResource('keystore.jks').path, + truststorePath: MockServerHttpsTest.classLoader.getResource('truststore.jks').path, + truststorePassword: 'changeit', + requireClientAuth: true + ), + soap: false + )) + when: + HttpPost restPost = new HttpPost('https://localhost:10443/testEndpoint') + restPost.entity = new StringEntity('', ContentType.create("text/xml", "UTF-8")) + CloseableHttpResponse response = client(trustedCertificateSslContext).execute(restPost) + then: + GPathResult restPostResponse = Util.extractXmlResponse(response) + restPostResponse.name() == 'goodResponse-request' + } + + @Unroll + def 'should handle HTTPS server with wrong client auth' () { + expect: + remoteMockServer.addMock(new AddMock( + name: 'testHttps', + path: 'testEndpoint', + port: 10443, + predicate: '''{req -> req.xml.name() == 'request'}''', + response: '''{req -> ""}''', + https: new Https( + keyPassword: 'changeit', + keystorePassword: 'changeit', + keystorePath: MockServerHttpsTest.classLoader.getResource('keystore.jks').path, + truststorePath: MockServerHttpsTest.classLoader.getResource('truststore.jks').path, + truststorePassword: 'changeit', + requireClientAuth: true + ), + soap: false + )) + when: + HttpPost restPost = new HttpPost('https://localhost:10443/testEndpoint') + restPost.entity = new StringEntity('', ContentType.create("text/xml", "UTF-8")) + client(sslContext).execute(restPost) + then: + thrown(SSLHandshakeException) + where: + sslContext << [noClientAuthSslContext, untrustedCertificateSslContext] + } + + private CloseableHttpClient client(SSLContext sslContext) { + return HttpClients.custom() + .setHostnameVerifier(SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER) + .setSslcontext(sslContext) + .build() + } + + private KeyStore trustedCertificateKeystore() { + return loadKeystore('trusted.jks') + } + + private KeyStore untrustedCertificateKeystore() { + return loadKeystore('untrusted.jks') } private KeyStore trustStore() { - KeyStore truststore = KeyStore.getInstance(KeyStore.defaultType) - truststore.load(new FileInputStream(MockServerHttpsTest.classLoader.getResource('truststore.jks').path), "changeit".toCharArray()); + return loadKeystore('truststore.jks') + } + + private KeyStore loadKeystore(String fileName) { + KeyStore truststore = KeyStore.getInstance(KeyStore.defaultType) + truststore.load(new FileInputStream(MockServerHttpsTest.classLoader.getResource(fileName).path), "changeit".toCharArray()); return truststore } } diff --git a/mockserver-tests/src/test/resources/trusted.jks b/mockserver-tests/src/test/resources/trusted.jks new file mode 100644 index 0000000000000000000000000000000000000000..e6fa704c24008010af9739e39947c464b0c58b26 GIT binary patch literal 2235 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 literal 0 HcmV?d00001 diff --git a/mockserver-tests/src/test/resources/untrusted.jks b/mockserver-tests/src/test/resources/untrusted.jks new file mode 100644 index 0000000000000000000000000000000000000000..ca94b454c5d413ac28dbcf921a5b678eb20896a5 GIT binary patch literal 2255 zcmc(gX*kr29>?cz#xNNBQWQd6hL{<<7(&QSS(6T8h-(?k8D$-eP-Mx3v9?)eLLn^} zB8@HTpzJ3p3{qySgX5g%-skqB_xHv3#rN~-_k5q<^Zo8E?=6EsAdUk8{|FWt9D$1r zkMO;GK)g)SSI&Y!oFF6t!bb8#d6c051VC^I0RSfmLV(OGZ&JRCH8R4@^J^DW8`_3_ z6rWi>v{S9NoI!bfsxbh%!D-lhck{xHgzm4F`ofr$A%t{j5 zdc0=sU3`1phd6T`3$b&HimkERIZfVAy3#GxALgM%6)?5r;%jNw6xJ8k_DcBBwPaVv zQ?7%r)s=P3^yE8?#e;h>1(c5Tq6j6XS=N%6rP})QE)# z--?D8&4V3+ZFN8_TdqDUz?0qv%rcRhb<^$r9vi z_dDnEWWa7GcA{ZDZ$wmm#uePdT#MUl-uhyzx)czlPJ@haBC=CxBH?`7{h`RYCUgn+ zB-Vck7mi6;PrMg1FUd9~{n@E`gAkPPCCY-wW7nK``3B~+OU)I7--4Lqzu&K%=KR5; zR@t5XF#LIZeEMr{i=S-}Z~xQ@TCL%Vs_G&2m->=XH%^FJL91(L4IVjU@v6SX!}e?yC76w~E|)0H8s3&r zNYd5JE8Z)}UY+jP;=QMc@!%X3c}I&WSw4l6a8&ndR4XA@{;O_5aM`rKH-BiPQeTH~ z!FQ``?DLusjeFY9rcGew`Ki@p94B_SxT^pgQaA*vx!LdK^vng3 ztP!#z=*|^jN`_|I?`rKEzR-jzxc+!xN%wY?D_@a|-si$;oGfb@|9nX~J&cNH8?%^u zu%q}lZYbW{$1iPvOv~vJW=TXH?bYMFc-BwtzKqwx9i%*cl$yM__~ClQsC+&h?>WpY zKGr?W(5Gpw)LsZ%JyVNKxr^}Sz8O^B-gibulwGu+%PNg{>w!l(LP}@iDXaUWxO}HQDl}GF6R&nF(y3aDf4CyM5d&e=A03)sBDree>QlQP1WwYlZvIXcB#!Y z`t9%dS*=@-MV#wnwL}WAP8|FxemO6clgw(iaP;3+`#<*oaS{QL`#G3{)c_L! zfrB{#csK|E0HR*a@y3nEJSY^;9)a2fPB+sEbrp5nzoX|`^Qa#+wXjtpyv zf6r6k6!Ttm6YLoFO1Gq8uw}>ogt1|4g)w%SNDf1~O?}owG1BG-e z0jO3(_A3VEiE%t1$btTQ*#h%Xo)qQcxe7Hl2B0CoB7G_bD*6W>pRVHaa|<$SnWWOG z8o636{^c&c&4?E}qt91=lrE)F`#-29AE$h+&zYrK^{GLkE8q$afqn0TPq?%=e9KQbX|(gsBXRbvB;F4Lp=DdgAf6b z8)ItfjFEyO5j*Q{CvzNwpy!Ez1wGPD`0i(3LSQmve z6h-!@kDm-vl7lk2ge$!w^X@)nRkv}=b@I5elZ>EM{tI1n%*@B9R#hEpZH`%CdmHN-9^-;H_Artw!Fcc;HOhxwt?)s||HqqyZYN(KMPJDq#mZzvJ! z6RK-A0_wMsUa3A0&FVv5G80c!ys=TW)_*95(`|27pf8$eNqO8vu5RAv`;yvJAqxuu WR1{UN&PgWK`+(=#3qkv-?7slilILmw literal 0 HcmV?d00001 diff --git a/mockserver/src/main/groovy/pl/touk/mockserver/server/HttpServerWrapper.groovy b/mockserver/src/main/groovy/pl/touk/mockserver/server/HttpServerWrapper.groovy index 9121c6b..0ab3ca2 100644 --- a/mockserver/src/main/groovy/pl/touk/mockserver/server/HttpServerWrapper.groovy +++ b/mockserver/src/main/groovy/pl/touk/mockserver/server/HttpServerWrapper.groovy @@ -2,15 +2,16 @@ package pl.touk.mockserver.server import com.sun.net.httpserver.HttpHandler import com.sun.net.httpserver.HttpServer -import com.sun.net.httpserver.HttpsConfigurator import com.sun.net.httpserver.HttpsServer import groovy.transform.PackageScope import groovy.util.logging.Slf4j import pl.touk.mockserver.api.common.Https +import javax.net.ssl.KeyManager import javax.net.ssl.KeyManagerFactory import javax.net.ssl.SSLContext import javax.net.ssl.TrustManager +import javax.net.ssl.TrustManagerFactory import java.security.KeyStore import java.security.SecureRandom import java.util.concurrent.Executor @@ -36,7 +37,7 @@ class HttpServerWrapper { private HttpServer buildServer(InetSocketAddress addr, Https https) { if (https) { HttpsServer httpsServer = HttpsServer.create(addr, 0) - httpsServer.httpsConfigurator = new HttpsConfigurator(buildSslContext(https)) + httpsServer.httpsConfigurator = new HttpsConfig(buildSslContext(https), https) return httpsServer } else { return HttpServer.create(addr, 0) @@ -44,14 +45,32 @@ class HttpServerWrapper { } private SSLContext buildSslContext(Https https) { + KeyManager[] keyManagers = buildKeyManager(https) + TrustManager[] trustManagers = buildTrustManager(https) + + SSLContext ssl = SSLContext.getInstance('TLSv1') + ssl.init(keyManagers, trustManagers, new SecureRandom()) + return ssl + } + + private KeyManager[] buildKeyManager(Https https) { KeyStore keyStore = KeyStore.getInstance(KeyStore.defaultType) keyStore.load(new FileInputStream(https.keystorePath), https.keystorePassword.toCharArray()) KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.defaultAlgorithm) kmf.init(keyStore, https.keyPassword.toCharArray()) + return kmf.keyManagers + } - SSLContext ssl = SSLContext.getInstance('TLSv1') - ssl.init(kmf.keyManagers, [] as TrustManager[], new SecureRandom()) - return ssl + private TrustManager[] buildTrustManager(Https https) { + if (https.requireClientAuth) { + KeyStore trustStore = KeyStore.getInstance(KeyStore.defaultType) + trustStore.load(new FileInputStream(https.truststorePath), https.truststorePassword.toCharArray()) + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.defaultAlgorithm) + tmf.init(trustStore) + return tmf.trustManagers + } else { + return [] + } } void createContext(String context, HttpHandler handler) { diff --git a/mockserver/src/main/groovy/pl/touk/mockserver/server/HttpsConfig.groovy b/mockserver/src/main/groovy/pl/touk/mockserver/server/HttpsConfig.groovy new file mode 100644 index 0000000..68b5550 --- /dev/null +++ b/mockserver/src/main/groovy/pl/touk/mockserver/server/HttpsConfig.groovy @@ -0,0 +1,28 @@ +package pl.touk.mockserver.server + +import com.sun.net.httpserver.HttpsConfigurator +import com.sun.net.httpserver.HttpsParameters +import groovy.transform.CompileStatic +import pl.touk.mockserver.api.common.Https + +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLParameters + +@CompileStatic +class HttpsConfig extends HttpsConfigurator { + private final Https https + + HttpsConfig(SSLContext sslContext, Https https) { + super(sslContext) + this.https = https + } + + @Override + void configure(HttpsParameters httpsParameters) { + SSLContext sslContext = getSSLContext() + SSLParameters sslParameters = sslContext.defaultSSLParameters + sslParameters.needClientAuth = https.requireClientAuth + httpsParameters.needClientAuth = https.requireClientAuth + httpsParameters.SSLParameters = sslParameters + } +}