From ee89e0effd0170b09e4eb1d07360c3c85828b513 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Sun, 1 Sep 2019 19:49:03 +0100 Subject: [PATCH] Fix client auth with Jetty Fixes gh-17541 --- .../embedded/jetty/SslServerCustomizer.java | 7 ++-- .../jetty/SslServerCustomizerTests.java | 3 +- ...AbstractReactiveWebServerFactoryTests.java | 25 +++------------ .../AbstractServletWebServerFactoryTests.java | 30 +++--------------- .../spring-boot/src/test/resources/test.jks | Bin 5510 -> 1276 bytes .../spring-boot/src/test/resources/test.p12 | Bin 6099 -> 3136 bytes 6 files changed, 15 insertions(+), 50 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizer.java index 92da9e7dab..b1ae3d5038 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizer.java @@ -68,7 +68,8 @@ class SslServerCustomizer implements JettyServerCustomizer { @Override public void customize(Server server) { - SslContextFactory sslContextFactory = new SslContextFactory(); + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + sslContextFactory.setEndpointIdentificationAlgorithm(null); configureSsl(sslContextFactory, this.ssl, this.sslStoreProvider); ServerConnector connector = createConnector(server, sslContextFactory, this.address); server.setConnectors(new Connector[] { connector }); @@ -131,7 +132,7 @@ class SslServerCustomizer implements JettyServerCustomizer { * @param ssl the ssl details. * @param sslStoreProvider the ssl store provider */ - protected void configureSsl(SslContextFactory factory, Ssl ssl, SslStoreProvider sslStoreProvider) { + protected void configureSsl(SslContextFactory.Server factory, Ssl ssl, SslStoreProvider sslStoreProvider) { factory.setProtocol(ssl.getProtocol()); configureSslClientAuth(factory, ssl); configureSslPasswords(factory, ssl); @@ -158,7 +159,7 @@ class SslServerCustomizer implements JettyServerCustomizer { } } - private void configureSslClientAuth(SslContextFactory factory, Ssl ssl) { + private void configureSslClientAuth(SslContextFactory.Server factory, Ssl ssl) { if (ssl.getClientAuth() == Ssl.ClientAuth.NEED) { factory.setNeedClientAuth(true); factory.setWantClientAuth(true); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizerTests.java index 4b08cec3af..71b062d69a 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizerTests.java @@ -81,7 +81,8 @@ public class SslServerCustomizerTests { Ssl ssl = new Ssl(); SslServerCustomizer customizer = new SslServerCustomizer(null, ssl, null, null); assertThatExceptionOfType(Exception.class) - .isThrownBy(() -> customizer.configureSsl(new SslContextFactory(), ssl, null)).satisfies((ex) -> { + .isThrownBy(() -> customizer.configureSsl(new SslContextFactory.Server(), ssl, null)) + .satisfies((ex) -> { assertThat(ex).isInstanceOf(WebServerException.class); assertThat(ex).hasMessageContaining("Could not load key store 'null'"); }); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java index 56ee2ae64f..d6ae411e2b 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java @@ -21,15 +21,11 @@ import java.io.FileInputStream; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.security.KeyStore; -import java.security.PrivateKey; -import java.security.cert.X509Certificate; import java.time.Duration; import java.util.Arrays; -import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLException; -import javax.net.ssl.X509KeyManager; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; @@ -170,22 +166,11 @@ public abstract class AbstractReactiveWebServerFactoryTests { KeyManagerFactory clientKeyManagerFactory = KeyManagerFactory .getInstance(KeyManagerFactory.getDefaultAlgorithm()); clientKeyManagerFactory.init(clientKeyStore, "password".toCharArray()); - for (KeyManager keyManager : clientKeyManagerFactory.getKeyManagers()) { - if (keyManager instanceof X509KeyManager) { - X509KeyManager x509KeyManager = (X509KeyManager) keyManager; - PrivateKey privateKey = x509KeyManager.getPrivateKey("spring-boot"); - if (privateKey != null) { - X509Certificate[] certificateChain = x509KeyManager.getCertificateChain("spring-boot"); - SslContextBuilder builder = SslContextBuilder.forClient().sslProvider(SslProvider.JDK) - .trustManager(InsecureTrustManagerFactory.INSTANCE) - .keyManager(privateKey, certificateChain); - HttpClient client = HttpClient.create().wiretap(true) - .secure((sslContextSpec) -> sslContextSpec.sslContext(builder)); - return new ReactorClientHttpConnector(client); - } - } - } - throw new IllegalStateException("Key with alias 'spring-boot' not found"); + SslContextBuilder builder = SslContextBuilder.forClient().sslProvider(SslProvider.JDK) + .trustManager(InsecureTrustManagerFactory.INSTANCE).keyManager(clientKeyManagerFactory); + HttpClient client = HttpClient.create().wiretap(true) + .secure((sslContextSpec) -> sslContextSpec.sslContext(builder)); + return new ReactorClientHttpConnector(client); } protected void testClientAuthSuccess(Ssl sslConfiguration, ReactorClientHttpConnector clientConnector) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java index b1b3860ba7..e2adea25c3 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java @@ -25,7 +25,6 @@ import java.io.PrintWriter; import java.net.InetSocketAddress; import java.net.MalformedURLException; import java.net.ServerSocket; -import java.net.Socket; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -75,8 +74,6 @@ import org.apache.http.conn.ssl.TrustSelfSignedStrategy; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClients; import org.apache.http.protocol.HttpContext; -import org.apache.http.ssl.PrivateKeyDetails; -import org.apache.http.ssl.PrivateKeyStrategy; import org.apache.http.ssl.SSLContextBuilder; import org.apache.http.ssl.TrustStrategy; import org.apache.jasper.EmbeddedServletOptions; @@ -402,7 +399,7 @@ public abstract class AbstractServletWebServerFactoryTests { new ExampleServlet(true, false), "/hello"); this.webServer = factory.getWebServer(registration); this.webServer.start(); - TrustStrategy trustStrategy = new SerialNumberValidatingTrustSelfSignedStrategy("5c7ae101"); + TrustStrategy trustStrategy = new SerialNumberValidatingTrustSelfSignedStrategy("3a3aaec8"); SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, trustStrategy).build(); HttpClient httpClient = HttpClients.custom().setSSLSocketFactory(new SSLConnectionSocketFactory(sslContext)) .build(); @@ -464,14 +461,7 @@ public abstract class AbstractServletWebServerFactoryTests { keyStore.load(new FileInputStream(new File("src/test/resources/test.p12")), "secret".toCharArray()); SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory( new SSLContextBuilder().loadTrustMaterial(null, new TrustSelfSignedStrategy()) - .loadKeyMaterial(keyStore, "secret".toCharArray(), new PrivateKeyStrategy() { - - @Override - public String chooseAlias(Map aliases, Socket socket) { - return "spring-boot"; - } - - }).build()); + .loadKeyMaterial(keyStore, "secret".toCharArray()).build()); HttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory).build(); HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory)).isEqualTo("test"); @@ -488,13 +478,7 @@ public abstract class AbstractServletWebServerFactoryTests { keyStore.load(new FileInputStream(new File("src/test/resources/test.jks")), "secret".toCharArray()); SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory( new SSLContextBuilder().loadTrustMaterial(null, new TrustSelfSignedStrategy()) - .loadKeyMaterial(keyStore, "password".toCharArray(), new PrivateKeyStrategy() { - - @Override - public String chooseAlias(Map aliases, Socket socket) { - return "spring-boot"; - } - }).build()); + .loadKeyMaterial(keyStore, "password".toCharArray()).build()); HttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory).build(); HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory)).isEqualTo("test"); @@ -565,13 +549,7 @@ public abstract class AbstractServletWebServerFactoryTests { keyStore.load(new FileInputStream(new File("src/test/resources/test.jks")), "secret".toCharArray()); SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory( new SSLContextBuilder().loadTrustMaterial(null, new TrustSelfSignedStrategy()) - .loadKeyMaterial(keyStore, "password".toCharArray(), new PrivateKeyStrategy() { - - @Override - public String chooseAlias(Map aliases, Socket socket) { - return "spring-boot"; - } - }).build()); + .loadKeyMaterial(keyStore, "password".toCharArray()).build()); HttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory).build(); HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory)).isEqualTo("test"); diff --git a/spring-boot-project/spring-boot/src/test/resources/test.jks b/spring-boot-project/spring-boot/src/test/resources/test.jks index f8a5f70596b9209b220d6d93f894918fe36a6f54..0fc3e802f75461dd074facb9611d350db4d5960f 100644 GIT binary patch literal 1276 zcmezO_TO6u1_mZ5W@O+hNi8nXP0YzmEM{O}OjTL=XFE`?-k{cikBv*4jgf^>i%F1? zk(GfZ`?F{4vBFug6<%MmmXJ+)mEgQoslXV6!5_H^yana6V1?kw1w zbE0Bi&GHlLHzfosgjrwL)qKcc5I;l0LF?W2lpQg%-cHp$l(#o)?Jkath1@gQN{hG8 zig@wKv#0R7vd_QC=JG%%Ffy=4=$RT=0v*d`(8R=M(8RcU0W%XL6BCP-)w&Y~JZv0V zZ64=rS(uqv84M~6g$xAPm_u3EggJBalM{0?@{3DgVjNh+*s+LlVG-lTBF2m)W*{fd zYiMC$VQ64zW@K(?5e4L0B5?=MWswHLZ0z7LVq$~_7BeF|vl9agPmO-znfkD()@R+bGrT$(AXmN24#zv*XRX z#$UNu(Lmln78u;Jd@N!tBKmU@J0!OJc3G%!N>OO@P1n+F-CorAVRmOQaA8six!iWP z)M3lXpnJ*TI=kIlH(Yxia-ls?xvctEx&P5B6()tKm`>%bo~@fX9{j%TtMU1G!|pw& zZ6BRjIqQ^`bIxR@OmMno&8^H%tpq36Esh&T(+MJ_la+#pV>+3scS%+y$FIpf`A}35D+PXKmb8Qh!g<@f)r7T zK%_}YP>>?hix32qB8Zd^_x>)DxcOouC9tu@)NZYYE#jWl1YF4Jh6Eiw*O^7SpaRJEKQRdlpMRp zB}nN5J%|KxV)rgy?4DF(vhRFV&lHUC=sYTZw5)M7nz5P7CO1!mubrGb&@+xKX-a1p zlL`xT=(*7YOQ4S(AF52WiHmvtROOFz_`ET0rwrDrQF`cv%viKvbMs~M}iFz%hVfG+Zwz)jFp?s!sH zbL}Tj7Uc}j4{y#sT16herJ)^^Uj@8r9=`yyP>~W}Lf-mxMLVkndZ;qMZ}#()Q33Vv zm8`+R2-!|1;svi+lC4ART)UA(OEY(GN*GKLW4G0)GCAs_0uqt{*zlvfTQ|LRI_XP2 z6x~cpw{hA`teS45e&^V>`Vp7pySM7>=+f;?a@$|V-9zC@gN!5}rdM5~@I{uHCRWi#lr+lZ%hQRg|xT}Wc#3O)E~W-?XSU+pru_Wk;Z6XjLm;lL zLxhv)m%;C4^Cb-7)PIvhqqpx}9Z|-uE?7%noI~(O+{S+Gi$~XbI_3)tEv{$83V+Fb z>Tpw>JX4Y;VNpc6EB`U_J%2=Fjj>DY=h1-@hG|$qWyL9z+kJO^1)e@}C?`xy{YZVd zN}Q;tWgpTvlq4+Hh3X#&KQ_8j_hu|_kWzlmv2oIBn>T(j_Gm2D%l36@AX(s>iPHtx zg=VUY4`f+u-3&MaUS))+#TE}=ow_Z}%ZmDOTztlVLjGr=woIJcd!XctlAegQ^s8%_ z0b~kpd`it~@{N1B(G2vvf&nBMXTaw>w$QdV(^zw}m5X%g5gp8BZfV1E4g-su;UxrET%HvobAGW!E(?>6&FOXjX8m}M+h z9@Tu&)wKF1nr0jHWC`O>q>jnBd_a$$zRFy<-pF$t?(b@RA4boXU8sbU=ydv&=EZX6 z8ZNY}TtTI&nL@P_6XsRbrG$;vA1pYFaSJ+6o4Sa{LbH!HoKDem-8vK!lGQj0ei~J4pv-v2sI*7zVl_ zJbZ#YkTCWWtv$&zg6S^%gdl_6{9tS+zdy+^foZM&P-qwzl=F9j3e5CpLPYOGvY|q} zEHHJbqJlb18L9?Vvgc({_#LYJZ}`_-3Jnzd$Ksr<4k|Q|4FE+089`_u5Ks{-!=+eT zqUINYoem1pFXi7Dc2THdX?3|f#3W5NgJQ0?`wg37dXo<&F~M$UaGoi2*ROivT{B~t z)oyV_x}OCK0WicUoOD!{+=TTx^W!Hzmy++2P@@f7=RQAdhRBNGmwF4@iYk!FKanMa zo-eX*P|9F)uG^pFjyrM%9R#nDZeol|mxKjR)8ULvS$G~N-5x;-Z+;MY)pJ>rAp|1u ze4WMjM?-lG>f>SJonFDJi+0cMw^yj(a^TTRdBL(-aWCuj)`FZ;eMxn`h13VvR4Us^ zVWTLEKBeh*L2MfxYaK;a8;-{rwYvGN2KcEnHcxhX9VDcxlcL$oodiKtKmf2v3#thf zIC;|39MmVhf@bhTdbs&{BZGqPQKL>)G4%BBZ>-cj)Lcq?p^{G(R}agw0(Y?iDLZ=> z%Ip7F;FInF84Qzxh`_q^EUacGixFnN{37%oHBjJju0>frhl=TZq6KRc_ z$dYmms+cpTUs4ZWkC$x9)!8@ zUF_JwVu8lyA;lSdh>71CTJe&S`f$@2b#Fo?zuK_J&^*PvfL#*7PC>gUzVA!d+3o)( ztAAltK&P^DV&31UC|CH2w;0XLa{=Zk{(&vW`P0jw<@I5zPXj`&LP`yW`man*0~&^b z+msn>4PtA+ixv%fV{a!{JJMU_merKSP()`G0}G=?$lVpP&Al|q$^o-;#gY`;^dGMN zC}!7uxBCdINL&n|SMuU;&+yI<;WT(+(vTX!KC9f~?KQZ7Pt(&;jN(0>FRg1f?KyR( z`jdq2aG;H-G`p{Zcz?1x!D2@kPvy!wh#QEo{~Skc6<-z}z=&Nl(sEuPYZml6zP%XW ztbomuS#U(x&3}V^NtzJR)!Vpzz9l#?D&UmD2gOTyuWuqdDN{kb5n^L_WM=_UExEz= zinKdVo}c5rq8Lixc;42-3730uWvX|hm4wt+r91_O(h!w)fuy($oPGi#Lvn)(e!&#GU)X*jh>q- z^_+)r_BAJox)c~TW%Q9R&%;OMCE(=}mVsHSD9}#~VY^3N{%t;$$d~vB%9q@ZtS+Ul zYY(y}Hmk!8deR*)T*LJ&qa2>wS1=~OM3mbnHcGJ*RF-vx3vBtY$^QNlt3tclJ%>8w zB3Z6$7@IBjJ@Qq0xiThJ`d{p9j4c>VviYUvV%bL(S`nc;)VdI61VV;#XwZOr89{ex zcoQ(gz^5x@F}zX7DVGi+kLXT9k8Yx~+uBgmz4hjowMxw`WzN1>Q<3h%l)5|f`Ab-) zR3&Xrh9J}UOLUo%!tXhg(B#3V7v&z=#Y>Vuh{=nUmROa)-~1eJ9Y!kfIq*DCgh-AL zZ%*X$s#d~`&m~@YIP;FTk>e2Ym{5ZrZr67u0f7+Ty;t|~i#qQ+!)X$&TvB$w7mgLS z(=R-3yEp7q;#WHE!o4AAAK{L_rJ1&q$j;{~@1@z)SxHY+6<{Vu4K|qO6D>b>$?HAP z!&c7Ckx7uS(IL+|D(DXUWq3Nz;3i)Rps(xNO-_$WNP!8#aNxT0!T5rjwkH2sOfb6a zjV~e9Ea{w!=}v!|&2On-Q5C0liMH@5Z>J$85wl3nT_db>P>whqxuAKe&h?MO9q z_?j)+IX--qw!ucmtYfp9g)AA@6GL0u6qB^W$9xVe_QiX8L(cE5bU#kv>-WQdzm3Ah`3yCXpQ;=$ z?gbNX?B3`FQ;0fNkIUKH#>GR28Bj>U@3J~MWKFExbd3+P?hkSNw?N|)m_kq!F^9%DDYs>z}>`natrXWr6tXw$h?ib zhYIoO=KeuESD-g`W=9;7*I+p_gSnWver*PKGl5M`E_uG|l!z^}J_bPF_-JL`7!%XH%>3oHfX>;kBsj z;xA}mX1(GR7WZt<4_QMgd%xQpc%D^wk?TrAMXd@7;gfU@T(dpTQbO6&_t?GF0#F2+ zhAQyPXq=f4o!FP2|Gxqa@d3&ytRA|4aA%0W{)5@Wg^EJ`y}) z213|G@KmCWGODV+*vNX~-m*3j9Yj`<`LO_oUJynG3SeOaYhp|=762ysF(?UIubskOTFC+2Hcc&7k%|k~~ zjCzS+xlUivK9lQ8E-Gw!qFUZfixuRAkQG?~N*>jcl7xoau5GW}+PM5}%u1coT2$6f zNBponkzNaa{W6u&?d_?6iU$1ylo!39wx?B{vB=ipN0Ke;WWzc^I)Oj8hJuuQ70YO-DqO^GqbBCq(zA|}1OPi7Vbpz#m$^$h!iWsawJ w0snS{|Mn#R$teA|5&qkg{Ow8pKRn5O+3Xs&>mmbFDG8?ut#Vh9U{;O)0rfI7DF6Tf diff --git a/spring-boot-project/spring-boot/src/test/resources/test.p12 b/spring-boot-project/spring-boot/src/test/resources/test.p12 index 1d6a25829c389d112598c7e2a9b437e8c3ff09a4..de3664b9d7d32a971fa971ad7146219238703ed3 100644 GIT binary patch literal 3136 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