From 13e034fc5f904f5a79aef21bbdf01016aac3293b Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Fri, 10 Jul 2020 12:19:28 +1000 Subject: [PATCH] Issue #5019 - add testing for the SslKeyStoreScanner Signed-off-by: Lachlan Roberts --- .../jetty/ssl/reload/KeystoreReloadTest.java | 212 ++++++++++++++++++ .../src/test/resources/badKeystore | Bin 0 -> 2457 bytes .../test/resources/jetty-logging.properties | 4 + .../src/test/resources/newKeystore | Bin 0 -> 2071 bytes .../src/test/resources/oldKeystore | Bin 0 -> 2303 bytes 5 files changed, 216 insertions(+) create mode 100644 jetty-ssl-reload/src/test/java/org/eclipse/jetty/ssl/reload/KeystoreReloadTest.java create mode 100644 jetty-ssl-reload/src/test/resources/badKeystore create mode 100644 jetty-ssl-reload/src/test/resources/jetty-logging.properties create mode 100644 jetty-ssl-reload/src/test/resources/newKeystore create mode 100644 jetty-ssl-reload/src/test/resources/oldKeystore diff --git a/jetty-ssl-reload/src/test/java/org/eclipse/jetty/ssl/reload/KeystoreReloadTest.java b/jetty-ssl-reload/src/test/java/org/eclipse/jetty/ssl/reload/KeystoreReloadTest.java new file mode 100644 index 00000000000..e5aeaf7c4a9 --- /dev/null +++ b/jetty-ssl-reload/src/test/java/org/eclipse/jetty/ssl/reload/KeystoreReloadTest.java @@ -0,0 +1,212 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.ssl.reload; + +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.util.Calendar; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.util.log.StacklessLogging; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class KeystoreReloadTest +{ + private static final int scanInterval = 1; + private Server server; + + public static void useKeystore(String keystore) throws Exception + { + Path keystoreDir = MavenTestingUtils.getTestResourcePath("keystoreDir"); + Path keystorePath = keystoreDir.resolve("keystore"); + if (Files.exists(keystorePath)) + Files.delete(keystorePath); + + if (keystore == null) + return; + + Files.copy(MavenTestingUtils.getTestResourceFile(keystore).toPath(), keystorePath); + keystorePath.toFile().deleteOnExit(); + } + + @BeforeEach + public void start() throws Exception + { + useKeystore("oldKeystore"); + + String keystore = MavenTestingUtils.getTestResourceFile("keystoreDir/keystore").getAbsolutePath(); + SslContextFactory sslContextFactory = new SslContextFactory.Server(); + sslContextFactory.setKeyStorePath(keystore); + sslContextFactory.setKeyStorePassword("storepwd"); + sslContextFactory.setKeyManagerPassword("keypwd"); + + server = new Server(); + SslConnectionFactory sslConnectionFactory = new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()); + HttpConfiguration httpsConfig = new HttpConfiguration(); + httpsConfig.addCustomizer(new SecureRequestCustomizer()); + HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(httpsConfig); + ServerConnector connector = new ServerConnector(server, sslConnectionFactory, httpConnectionFactory); + connector.setPort(8443); + server.addConnector(connector); + + // Configure Keystore Reload. + SslKeyStoreScanner keystoreScanner = new SslKeyStoreScanner(sslContextFactory); + keystoreScanner.setScanInterval(scanInterval); + server.addBean(keystoreScanner); + + server.start(); + } + + @AfterEach + public void stop() throws Exception + { + server.stop(); + } + + @Test + public void testKeystoreHotReload() throws Exception + { + URL serverUrl = server.getURI().toURL(); + + // Check the original certificate expiry. + X509Certificate cert1 = getCertificateFromUrl(serverUrl); + assertThat(getExpiryYear(cert1), is(2015)); + + // Switch to use newKeystore which has a later expiry date. + useKeystore("newKeystore"); + Thread.sleep(Duration.ofSeconds(scanInterval * 2).toMillis()); + + // The scanner should have detected the updated keystore, expiry should be renewed. + X509Certificate cert2 = getCertificateFromUrl(serverUrl); + assertThat(getExpiryYear(cert2), is(2020)); + } + + @Test + public void testReloadWithBadKeystore() throws Exception + { + URL serverUrl = server.getURI().toURL(); + + // Check the original certificate expiry. + X509Certificate cert1 = getCertificateFromUrl(serverUrl); + assertThat(getExpiryYear(cert1), is(2015)); + + // Switch to use badKeystore which has the incorrect passwords. + try (StacklessLogging ignored = new StacklessLogging(SslKeyStoreScanner.class)) + { + useKeystore("badKeystore"); + Thread.sleep(Duration.ofSeconds(scanInterval * 2).toMillis()); + } + + // The good keystore is removed, now the bad keystore now causes an SSL Handshake exception. + assertThrows(SSLHandshakeException.class, () -> getCertificateFromUrl(serverUrl)); + } + + @Test + public void testKeystoreRemoval() throws Exception + { + URL serverUrl = server.getURI().toURL(); + + // Check the original certificate expiry. + X509Certificate cert1 = getCertificateFromUrl(serverUrl); + assertThat(getExpiryYear(cert1), is(2015)); + + // Delete the keystore. + try (StacklessLogging ignored = new StacklessLogging(SslKeyStoreScanner.class)) + { + useKeystore(null); + Thread.sleep(Duration.ofSeconds(scanInterval * 2).toMillis()); + } + + // The good keystore is removed, having no keystore causes an SSL Handshake exception. + assertThrows(SSLHandshakeException.class, () -> getCertificateFromUrl(serverUrl)); + + // Switch to use keystore2 which has a later expiry date. + useKeystore("newKeystore"); + Thread.sleep(Duration.ofSeconds(scanInterval * 2).toMillis()); + X509Certificate cert2 = getCertificateFromUrl(serverUrl); + assertThat(getExpiryYear(cert2), is(2020)); + } + + public static int getExpiryYear(X509Certificate cert) + { + Calendar instance = Calendar.getInstance(); + instance.setTime(cert.getNotAfter()); + return instance.get(Calendar.YEAR); + } + + public static X509Certificate getCertificateFromUrl(URL serverUrl) throws Exception + { + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(new KeyManager[0], new TrustManager[] {new DefaultTrustManager()}, new SecureRandom()); + SSLContext.setDefault(ctx); + + HttpsURLConnection connection = (HttpsURLConnection)serverUrl.openConnection(); + connection.setHostnameVerifier((a, b) -> true); + connection.connect(); + Certificate[] certs = connection.getServerCertificates(); + connection.disconnect(); + + assertThat(certs.length, is(1)); + return (X509Certificate)certs[0]; + } + + private static class DefaultTrustManager implements X509TrustManager + { + @Override + public void checkClientTrusted(X509Certificate[] arg0, String arg1) + { + } + + @Override + public void checkServerTrusted(X509Certificate[] arg0, String arg1) + { + } + + @Override + public X509Certificate[] getAcceptedIssuers() + { + return null; + } + } +} diff --git a/jetty-ssl-reload/src/test/resources/badKeystore b/jetty-ssl-reload/src/test/resources/badKeystore new file mode 100644 index 0000000000000000000000000000000000000000..568291d71eddf760f6456162653859a89a397b18 GIT binary patch literal 2457 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 literal 0 HcmV?d00001 diff --git a/jetty-ssl-reload/src/test/resources/jetty-logging.properties b/jetty-ssl-reload/src/test/resources/jetty-logging.properties new file mode 100644 index 00000000000..6af29424dc1 --- /dev/null +++ b/jetty-ssl-reload/src/test/resources/jetty-logging.properties @@ -0,0 +1,4 @@ +org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog +org.eclipse.jetty.deploy.LEVEL=WARN +org.eclipse.jetty.util.Scanner=WARN +org.eclipse.jetty.ssl.reload.LEVEL=DEBUG diff --git a/jetty-ssl-reload/src/test/resources/newKeystore b/jetty-ssl-reload/src/test/resources/newKeystore new file mode 100644 index 0000000000000000000000000000000000000000..133fc4ead39773e902330933d5dcb1313e8a12e1 GIT binary patch literal 2071 zcmV+y2HH0004VGm@@~ z000F5FoFd9Fb)O^D+U1s0V)C!0RaU71cC(U{wI^t3YE`=z}E|&JEa+8N6rw|yD3y4 zAN*HyF5vS<;(7i3@TH+4$ET8=hax^i-@`A~Ex%nWK~s=()Z)S`0>S-HdMt2S!dugs zHSi;)A=gNO6XJA%w4$OD8Npr{g-nb{=kEP+v`_Vo0!T{0$u;ESx2}3^@unzW;6Js= zPU8^-crX&@&`ifO%4-%DNK|n9a8^ATViZyTF^mbk_)=Ac8u4#2`;-QPlD|IZ%&s+k zrhwJ_O}-_L;t$K|dKx)7@y}}XmGUIDBjsgt-&-{-ks&fDyY)acP*^CXTWLqW!mIfl zQcSl_kh^^Zaw{n~E_$9v2c98@ICWD!ly699aqre)kTGw`nbW9w2yVUZfIaRtQrIoM z&FY5GL!-UAY2q!Hb5*S`l!=3&?ldM#Q)~qX<}HLe0AHVjgNkb6lcNAdB%NSOG~xFo zXR@HCIh@>jCW_10eM>C{(qF*E!oe`tP?2Om!fsIU?m+CK5C!j6`4~-N{Zs-F(+4Cv z()#C!*v5|pu!tMiSOdFK#}E+V)*->CFh;s8_H4DOqj-aQoSKM?A>(>u>1W<&6&75woCz?5TtS%rL(RjfPA{5*-^Bz` z&B$PCo2A*i9yI2)aDV!e;~$MdJFt_gNb=wo8tw&uAv9!1Z!@r189|@T%z`~?oXEvn zLB;x4ob&{2@9Z1ZZ`(`NOeADLyw>(4D7u)=H22Gnd*fs4~Nz~N`$HNnTT4uXHUEllS$7r-w;aD0q;UuMFF z;veO^nF+b1Qlx%Kd$F^+q{?$9Owf8&{W(1Uc^okiTZdZ;sODd{ZSj@oAjsCHD3@$LOTH5Suv^fT<4sWJXlAGa(nUrwN zN-!k-GWbkER{+|N?X8K%I*JL+g`7FBGyq+5#A4~o!8umpVg__yU|L!xIr-Bbbx2cO z`VAG1Wn6M@2PvWiH{(}DMGQQ@&7}>y@!We_`k<01f2h9*-?FuR#%PMEf?#J)ZmlVY z@d>Ov*MP4lU&as*%mbFd=ij>n=oiHVcT@!s5-qAnf1Et1r)s7pe-v#1pc|NWZ5oVi zyLUC;kzg*fIv=`R*4h9V4tOPQ>()((4QkO{BXOg00GlRWE@l|%pSRBCY+i{7Yw3j5sQjOEzOh(b6Ejo~ z&A98fLQgHX)*kOnf19D@m!GB9C+#P*{kd}Q0ytdFp5dv99h%1KeLoGx2U&AqOaOXD z4){lXQrr%}C{d3>4pP!SF;!Q+QFL(az%-6X25%!V*=f_#TT%e^`mvjJkt#@UgPL$_ zFmQ=|nS8BaCvA!i@wue)1r~%qtFBKaXQ%WyX|(t?<0+GQ0000100mesH842<00P4> zf&#xVf&r(X0|Eg80t9m|*Ca3v1_>&LNQUyR6n?!E9Zya?f@^W5SBggm)v#!;oZ1CJO$_Fzcg29R>qc9S#H*1QaUvR}e>W zuhYx_Sz4e_D!#?={6H`b1_>&LNQUjflyiJJgX&|NRC{VF3;m+~d# zlTCCrz*zlxK-D14CB?qkV-XZBVEB^o+clQU^v0vpJ-7H@@t}8z4mh5y8Iry2GIM+{ z7Lt*Ey=$+QpSbWMGu33S{s?U06!c4pM!aglWxFVrQ>&1K@O!lyH5#W?6)Z2NgG&S7 z>o^%(^7(;D5=5E8!b6Q0T4gg)=z#Kv8q--$dnylevZcMy$N54AT;Ptp7N3{4?FF^9 zwMsLbbPI6aVt%@+&%y Bv6TP- literal 0 HcmV?d00001 diff --git a/jetty-ssl-reload/src/test/resources/oldKeystore b/jetty-ssl-reload/src/test/resources/oldKeystore new file mode 100644 index 0000000000000000000000000000000000000000..8325b026098e23fd9b3cd6607fc55c63c247a45a GIT binary patch literal 2303 zcmd6o`8O2&9>-^~WNgDUjg);~hG9mAFtYCkL*kKb7$XcCF^Y#tJocq-M4s%8L?IPQ zkwnZB4Ur`#VQeX)XyU4S@43%8_aC@Fe13Sp&-ZiA=e$4P^ZM+r?ydp=0MLE`e-&?N z5S2>XSB_Cq-_!sAUI2>D!$pbkK{WV)JV14j7!b${;Gy$y3XMHRSvnI2ZR_;7RXOj? zZTnjLn_TM4qXB5*%~{dvx>08>g}hP|nu{UhWZR5EM10RNMMIOeRsoO!o-gsSnOolO zWc-psgp1--%-S-(oD**_YzRb9FJ_rRn)5B@*0Zl1&x5C592D?t3cp>lE1CA4G*SEx z0BaDd7VPuDVyfbwT<^&WsdcV&O7ZNoMfeN^UI7a5cgF5(&|U8!J!Q zB+bQZYo4nwy+6fnc}MdMx}-Uu(zb`g9^t!_u2$EgPXJB%AI3b*xcS6t)@-C<2X4DJ zSeSxYZiY$}S0mnb?45MEAj9x~r{3r;?a+cqD1A$PRx`S#uE&8k9^A)f`T#m6rOk%{q;3RQy~jP-!2UMZ{yH*!|AK2*%d z#ZrD`>-&TgZui)hF>&idC8NQF-@*1))0MVqBX6eFqoM)+-a%qP3X*SfA%%c-yA1x( zvXfV~qnIe16DV`Uzz)yXQ$p$HbnR_?rBEw@Y@g^Y7}6IK$3GvN#|k_b4wklCvP4 zg;`qAJ1Q`Qr5Y@&5A2mQZr{CoDQkQ_3cpsmXzR3ui>MZAdff}r4A&5MJpbWr>^I+G zvcKh4^Gs3aHk&Aas|lUhoB5?xW3V1sRG-m7Ik?GIs%fur0I;B^lTvFBmxt~V;W(9V z7*eA{WM{V4fl=EQ$84`TMq+H>)hAJ zpNj&Cd3TlX+LQ{oj#qRWxRv+Fd37s^TO0S>VG`Jw<`Em3RKHgZec7TfTz!4aK1G6H zU+A~%H&V=LJK}=P4JKVakQpo5Uzd~qu#-1Lp6aiAsjJ1hn6DsfBKG>g(tx=VM$GxD z&$E8Sy3qp%wp3%Y=jl@$Tb8I#xb8z6F0z8paLFlCGfc+uLYulm#ZLaGRsl@Uir0U1 zB5Bv4G37rx?OJ@eOGt|Z*pjHZ>t-1h*X^e!zgweE!_o*R4E^LNGqKzVxd`JJfZVfPwI zQgZ>qt}DScoTXu&=~|@QF!Fxa_ajnfnCt7>7P-CAaUhcL@p4X!jPGnFEx82rI(<;T z`3E}h+mr9Lq@;E%@7ayaFQSYe)&_}Ohz`7YwCk%?rqgweGrC4XA|1j# zes~SxE&XSbZ3`(+&vT%zVEdmW_M2u14FZO3J9)#~YL z_pg8^EauZ~w1B1A>O99?D#=5>Mg{jBeaq^Lp5$Qw4Hxp(NjwgWdHzx zTTygy3yKaZEdzsqAP^6)TZ;lpm`^}G)h6|;Fc2uX56t8Y9q2wW!R|bKFn;%lGZB>7 zi2pbT{>S0}ucM4o{MiM8NjXxYsj_DNG+A>>#MziIDmj8Ai>K1GWF4qPEgcb*(9d^* zFkZW#wAPVCN&M7cnAk6RYXt>{kt5FrX;Gp{C~1VSjvh)+7p0?v#-h+(za&-%`)~RG zH7p${|MxKWFAbOu6x}~TKnREq1OifoG*q$YuM+js#yC;rVoaQ>v%c&CT576`vAv8K zG7pIFh5JMVPF6)f#NVkQ-Cbk$Pek89XVu;lDVub{2#P+UI_f?6YGXX#d4UO+Moz)W zvAZuM@c07mxLC36Zx{=q+>*EQK@`34Q&H?Bigt)Dvf0$sMbY_^k{?p>5dK9D^hloMqN!dVGR{yr`e~Neq}Z?| zD>o|%4Y@SrNAa48X%VRSlNfaVBWQ6`aC)Ki&FJ!0o`iBZY z&m;=DbncZd;o2$fvTxE*TzMMVkqoAl8{rUA?ISDx*}oYQkh4>u%xdW zXL1QHr#;?=xuQ~d&b=v@>^{hhxlAD_>9alq%%G!r)#vOelxM-iB=GM684XBU|kmX zlG3}aCnG_K(9mYVbjy`DH$S<@^uCwiIp$sK=iUTik<2(Mh+