From c19f40e91523bc2d6019799126f8e381cbf345d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Tue, 23 Apr 2024 10:20:41 +0200 Subject: [PATCH] Support certs generation when SSL enabled to fix FIPS scenarios --- .../META-INF/resources/server.keystore | Bin 2407 -> 0 bytes .../META-INF/resources/server.truststore | Bin 2407 -> 0 bytes .../src/main/resources/application.properties | 3 - .../quarkus/qe/DevModeGreetingResourceIT.java | 3 +- .../src/test/java/io/quarkus/qe/HttpIT.java | 7 + .../io/quarkus/qe/UnixGreetingResourceIT.java | 4 +- .../quarkus/qe/WindowsGreetingResourceIT.java | 7 +- .../resources/windows.application.properties | 2 - quarkus-test-core/pom.xml | 4 + .../certificate/CertificateBuilder.java | 162 ++++++++++++++++++ .../io/quarkus/test/services/Certificate.java | 52 ++++++ .../services/DevModeQuarkusApplication.java | 5 + .../test/services/QuarkusApplication.java | 5 + ...rkusApplicationManagedResourceBuilder.java | 3 + ...rkusApplicationManagedResourceBuilder.java | 3 + ...rkusApplicationManagedResourceBuilder.java | 26 ++- .../bootstrap/inject/OpenShiftClient.java | 20 ++- quarkus-test-service-kafka/pom.xml | 4 - .../StrimziKafkaContainerManagedResource.java | 33 +--- 19 files changed, 289 insertions(+), 54 deletions(-) delete mode 100644 examples/https/src/main/resources/META-INF/resources/server.keystore delete mode 100644 examples/https/src/main/resources/META-INF/resources/server.truststore delete mode 100644 examples/https/src/test/resources/windows.application.properties create mode 100644 quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/CertificateBuilder.java create mode 100644 quarkus-test-core/src/main/java/io/quarkus/test/services/Certificate.java diff --git a/examples/https/src/main/resources/META-INF/resources/server.keystore b/examples/https/src/main/resources/META-INF/resources/server.keystore deleted file mode 100644 index cdef1caf3c6da29b3dfff68f816ecd0171727dbf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2407 zcmY+EX*d)L7stn#8OD-*3_}=OXv~bA8e|%~OR`4EPEiVpFiaS^Mp4OnQI?E5wn8Ja zX1OBS*BFGkV~MN@Z})xP_ul7zIOjai?|;tu_`{H(o`ak~p903x( zGl05=yRmd*Gp>W~N}RCU*j@peu_J#<>?zJD+15MH-1Jsa@ByP4IA)-dF-pzBspX z98Mv|&(NP7%&wF8%3@0<5c*Q4QnlMLi@$z!?xd#Ma%>i3p*hO)AnJSub#QYobQh2~ z7ApM=p&?g8*`8}GQ~G5RP3Y11H6V)7Vjo47luePUYGvp-rKBb&$7koL`F0R!NBe~2 zPrr9x3pWyXFfnqeHjz(tLQb1IEOpbex)W!i@2#x4Y;I9}GLEf#Nl zZih>2E{u@YUd#uS^$)U#$sf60PLxm%RT7 z{Y=;remnSQ#KISw9k+^RYfg<5`+1fn*|8jJ&mZkI;LJh8L3*d-F&{J0rdNoPPjtbF z;A`_J`LW9mgIjlv-v-o#QoUMz1Wv)&O>GR6)d~@U3e#_Q*k4&cNZIv6iXVv%>V4yr zZcFR1y2=_937LOi`r^)RY9>+7)x1gl*b+C;+`4s8qOEv1Qz61s{w8L!UO;_{VV*5? z8-ip8bchVBAC1r?{LYvp<@ng2C>=JaSyT^?;K(U#Q0F6>%IpGe6&R@226wJ%*~XfB zOR@JS&vV~=OofU28WuU`YI+9oi{1)`DdK0tY+3v@_Vjt}=2^7!PL&ed><3)0_Vi7! z_z5PYg*+j#kQ-c29!)l$%A2FZV27$Sh1%1%3Aati@TdplB`zAMT9*^0x2A*XfPM)-PiWWuMe`b4OVoER!w&>cRfk_qNlR7 zH;h}nP+i$6(sg@ceNfwG7WGpx?J9Y3NHre(mPt<`mY0neL^p!xo}cUUjyz_(8H=AC zxKuz^Epo&Olu~Smpy9RV277=+1DajJu3pls#n3A*5VtTHX`$@S&`pf~_ zvQ%~2ih_r(p60P5)~7BrfU=2r&x7`bM4oe&m{cSShtm zQF15TL%`);PJ1kH$urU%VG}*qkCYW(I&`=+f ze;c?1C%F4(Mbt&&@;c{HD(8IXJ5iwvdhmxFKGm~{=^#KN-NBRwwxmL!B+>dNgQ zSVIirZ&gBtu_sRQ0D=MDfZKpOe|3z^KcNU#7$j)tALuQO)xv1&YGW{3nwpwg7!v!F zzn3^5`6TxEpC}f{2KYH5{z=&WWmw2xhNbxEkFu92TyT=-hMsbnU7uXj(EqPtV@T|V z^dU1BrlP__h7lF%BI6+^FKfM~!mlFVhnR7y>E~fe4kb0MRu+GB4ln5+k58Hyh_n^x zAAZc$kJgCr{4oKQ#}rhHoHc{i@;WdEodxSBb29vN$lT$udVV(!)EnR;nRz9nRH@Mz z(M3y&=vh=<_j2#mHkeL|%KI2+p|wZJ@O-HpvsQkkO8e2{*YwqqR{5yJb;9Nj9}>5O z|J<5L8^T64=S9d^HWf~By7>_+bOqw}w1t-$22Q=Y6 zA$_RFa1~J|RTCcKjy<1%5d1TZMQO=ni)oR8O9gImb188iQjUsV&p;1$@f#Dlk|nYZ z(bZ?K7|?0K=I~WbE_p6{I!`rpt=hc;riglQrHg-JOxGt@h%<)p^>zXqtKi5zu zD$poUvufFaw>n*U^t{+KmH*Txb%OG1x=H&z_m5yp5~)om5V3b%x9jV(wd-A`{FY8ttiy_#I>UVR&>t7K^tM;_1yE?59Uy(n zWGG#GG+5sTvJ$MGv7;b%S(Y|Zq|<;wzj;+lRvGeSqOe|53iSgvup*K35HM8VcX<;p^XmPZ}jWHI^} z84MHxRyn~A6bA!9!oxkh8y?8tzwn!7iwge!p-i~XbsHq?`qUS#CFl3(DiX7ghgeh& NtHU8ccD984{{ZbYXC(jt diff --git a/examples/https/src/main/resources/META-INF/resources/server.truststore b/examples/https/src/main/resources/META-INF/resources/server.truststore deleted file mode 100644 index cdef1caf3c6da29b3dfff68f816ecd0171727dbf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2407 zcmY+EX*d)L7stn#8OD-*3_}=OXv~bA8e|%~OR`4EPEiVpFiaS^Mp4OnQI?E5wn8Ja zX1OBS*BFGkV~MN@Z})xP_ul7zIOjai?|;tu_`{H(o`ak~p903x( zGl05=yRmd*Gp>W~N}RCU*j@peu_J#<>?zJD+15MH-1Jsa@ByP4IA)-dF-pzBspX z98Mv|&(NP7%&wF8%3@0<5c*Q4QnlMLi@$z!?xd#Ma%>i3p*hO)AnJSub#QYobQh2~ z7ApM=p&?g8*`8}GQ~G5RP3Y11H6V)7Vjo47luePUYGvp-rKBb&$7koL`F0R!NBe~2 zPrr9x3pWyXFfnqeHjz(tLQb1IEOpbex)W!i@2#x4Y;I9}GLEf#Nl zZih>2E{u@YUd#uS^$)U#$sf60PLxm%RT7 z{Y=;remnSQ#KISw9k+^RYfg<5`+1fn*|8jJ&mZkI;LJh8L3*d-F&{J0rdNoPPjtbF z;A`_J`LW9mgIjlv-v-o#QoUMz1Wv)&O>GR6)d~@U3e#_Q*k4&cNZIv6iXVv%>V4yr zZcFR1y2=_937LOi`r^)RY9>+7)x1gl*b+C;+`4s8qOEv1Qz61s{w8L!UO;_{VV*5? z8-ip8bchVBAC1r?{LYvp<@ng2C>=JaSyT^?;K(U#Q0F6>%IpGe6&R@226wJ%*~XfB zOR@JS&vV~=OofU28WuU`YI+9oi{1)`DdK0tY+3v@_Vjt}=2^7!PL&ed><3)0_Vi7! z_z5PYg*+j#kQ-c29!)l$%A2FZV27$Sh1%1%3Aati@TdplB`zAMT9*^0x2A*XfPM)-PiWWuMe`b4OVoER!w&>cRfk_qNlR7 zH;h}nP+i$6(sg@ceNfwG7WGpx?J9Y3NHre(mPt<`mY0neL^p!xo}cUUjyz_(8H=AC zxKuz^Epo&Olu~Smpy9RV277=+1DajJu3pls#n3A*5VtTHX`$@S&`pf~_ zvQ%~2ih_r(p60P5)~7BrfU=2r&x7`bM4oe&m{cSShtm zQF15TL%`);PJ1kH$urU%VG}*qkCYW(I&`=+f ze;c?1C%F4(Mbt&&@;c{HD(8IXJ5iwvdhmxFKGm~{=^#KN-NBRwwxmL!B+>dNgQ zSVIirZ&gBtu_sRQ0D=MDfZKpOe|3z^KcNU#7$j)tALuQO)xv1&YGW{3nwpwg7!v!F zzn3^5`6TxEpC}f{2KYH5{z=&WWmw2xhNbxEkFu92TyT=-hMsbnU7uXj(EqPtV@T|V z^dU1BrlP__h7lF%BI6+^FKfM~!mlFVhnR7y>E~fe4kb0MRu+GB4ln5+k58Hyh_n^x zAAZc$kJgCr{4oKQ#}rhHoHc{i@;WdEodxSBb29vN$lT$udVV(!)EnR;nRz9nRH@Mz z(M3y&=vh=<_j2#mHkeL|%KI2+p|wZJ@O-HpvsQkkO8e2{*YwqqR{5yJb;9Nj9}>5O z|J<5L8^T64=S9d^HWf~By7>_+bOqw}w1t-$22Q=Y6 zA$_RFa1~J|RTCcKjy<1%5d1TZMQO=ni)oR8O9gImb188iQjUsV&p;1$@f#Dlk|nYZ z(bZ?K7|?0K=I~WbE_p6{I!`rpt=hc;riglQrHg-JOxGt@h%<)p^>zXqtKi5zu zD$poUvufFaw>n*U^t{+KmH*Txb%OG1x=H&z_m5yp5~)om5V3b%x9jV(wd-A`{FY8ttiy_#I>UVR&>t7K^tM;_1yE?59Uy(n zWGG#GG+5sTvJ$MGv7;b%S(Y|Zq|<;wzj;+lRvGeSqOe|53iSgvup*K35HM8VcX<;p^XmPZ}jWHI^} z84MHxRyn~A6bA!9!oxkh8y?8tzwn!7iwge!p-i~XbsHq?`qUS#CFl3(DiX7ghgeh& NtHU8ccD984{{ZbYXC(jt diff --git a/examples/https/src/main/resources/application.properties b/examples/https/src/main/resources/application.properties index 894a44aeb..d177df7fd 100644 --- a/examples/https/src/main/resources/application.properties +++ b/examples/https/src/main/resources/application.properties @@ -1,5 +1,2 @@ # HTTPS -quarkus.http.ssl.certificate.key-store-file=META-INF/resources/server.keystore -quarkus.http.ssl.certificate.key-store-file-type=JKS -quarkus.http.ssl.certificate.key-store-password=password quarkus.ssl.native=true diff --git a/examples/https/src/test/java/io/quarkus/qe/DevModeGreetingResourceIT.java b/examples/https/src/test/java/io/quarkus/qe/DevModeGreetingResourceIT.java index 9b030f9ff..68bd26de4 100644 --- a/examples/https/src/test/java/io/quarkus/qe/DevModeGreetingResourceIT.java +++ b/examples/https/src/test/java/io/quarkus/qe/DevModeGreetingResourceIT.java @@ -5,11 +5,12 @@ import io.quarkus.test.bootstrap.DevModeQuarkusService; import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.services.Certificate; import io.quarkus.test.services.DevModeQuarkusApplication; @QuarkusScenario public class DevModeGreetingResourceIT { - @DevModeQuarkusApplication(ssl = true) + @DevModeQuarkusApplication(ssl = true, certificates = @Certificate(configureKeystore = true)) static DevModeQuarkusService app = new DevModeQuarkusService(); @Test diff --git a/examples/https/src/test/java/io/quarkus/qe/HttpIT.java b/examples/https/src/test/java/io/quarkus/qe/HttpIT.java index 2a7188abb..a8a002513 100644 --- a/examples/https/src/test/java/io/quarkus/qe/HttpIT.java +++ b/examples/https/src/test/java/io/quarkus/qe/HttpIT.java @@ -1,5 +1,6 @@ package io.quarkus.qe; +import static io.quarkus.test.services.Certificate.Format.JKS; import static io.quarkus.test.utils.AwaitilityUtils.untilAsserted; import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.is; @@ -7,7 +8,10 @@ import org.apache.http.HttpStatus; import org.junit.jupiter.api.Test; +import io.quarkus.test.bootstrap.RestService; import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.services.Certificate; +import io.quarkus.test.services.QuarkusApplication; import io.restassured.specification.RequestSpecification; @QuarkusScenario @@ -15,6 +19,9 @@ public class HttpIT { private final RequestSpecification spec = given(); + @QuarkusApplication(ssl = true, certificates = @Certificate(configureKeystore = true, format = JKS)) + static final RestService app = new RestService(); + @Test public void shouldSayHelloWorld() { untilAsserted(() -> spec.get("/greeting") diff --git a/examples/https/src/test/java/io/quarkus/qe/UnixGreetingResourceIT.java b/examples/https/src/test/java/io/quarkus/qe/UnixGreetingResourceIT.java index c66e2b9b3..3063a0df8 100644 --- a/examples/https/src/test/java/io/quarkus/qe/UnixGreetingResourceIT.java +++ b/examples/https/src/test/java/io/quarkus/qe/UnixGreetingResourceIT.java @@ -1,5 +1,6 @@ package io.quarkus.qe; +import static io.quarkus.test.services.Certificate.Format.JKS; import static io.quarkus.test.utils.AwaitilityUtils.untilAsserted; import static org.hamcrest.Matchers.is; @@ -10,12 +11,13 @@ import io.quarkus.test.bootstrap.RestService; import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.services.Certificate; import io.quarkus.test.services.QuarkusApplication; @QuarkusScenario @DisabledOnOs(OS.WINDOWS) public class UnixGreetingResourceIT { - @QuarkusApplication(ssl = true) + @QuarkusApplication(ssl = true, certificates = @Certificate(configureKeystore = true, format = JKS)) static final RestService app = new RestService(); @Test diff --git a/examples/https/src/test/java/io/quarkus/qe/WindowsGreetingResourceIT.java b/examples/https/src/test/java/io/quarkus/qe/WindowsGreetingResourceIT.java index fb948f2c5..b8cd8e20e 100644 --- a/examples/https/src/test/java/io/quarkus/qe/WindowsGreetingResourceIT.java +++ b/examples/https/src/test/java/io/quarkus/qe/WindowsGreetingResourceIT.java @@ -1,5 +1,6 @@ package io.quarkus.qe; +import static io.quarkus.test.services.Certificate.Format.JKS; import static io.quarkus.test.utils.AwaitilityUtils.untilAsserted; import static org.hamcrest.Matchers.is; @@ -10,14 +11,14 @@ import io.quarkus.test.bootstrap.RestService; import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.services.Certificate; import io.quarkus.test.services.QuarkusApplication; @QuarkusScenario @EnabledOnOs(OS.WINDOWS) public class WindowsGreetingResourceIT { - @QuarkusApplication(ssl = true) - static final RestService app = new RestService() - .withProperties("windows.application.properties"); + @QuarkusApplication(ssl = true, certificates = @Certificate(configureKeystore = true, format = JKS)) + static final RestService app = new RestService(); @Test public void shouldSayHelloWorld() { diff --git a/examples/https/src/test/resources/windows.application.properties b/examples/https/src/test/resources/windows.application.properties deleted file mode 100644 index d5cdae14c..000000000 --- a/examples/https/src/test/resources/windows.application.properties +++ /dev/null @@ -1,2 +0,0 @@ -# HTTPS -quarkus.http.ssl.certificate.key-store-file=META-INF\\resources\\server.keystore diff --git a/quarkus-test-core/pom.xml b/quarkus-test-core/pom.xml index 7444d3bd2..db3736780 100644 --- a/quarkus-test-core/pom.xml +++ b/quarkus-test-core/pom.xml @@ -57,6 +57,10 @@ io.smallrye.reactive smallrye-mutiny-vertx-web-client + + me.escoffier.certs + certificate-generator + org.apache.maven.surefire diff --git a/quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/CertificateBuilder.java b/quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/CertificateBuilder.java new file mode 100644 index 000000000..8dd45a012 --- /dev/null +++ b/quarkus-test-core/src/main/java/io/quarkus/test/security/certificate/CertificateBuilder.java @@ -0,0 +1,162 @@ +package io.quarkus.test.security.certificate; + +import static io.quarkus.test.utils.PropertiesUtils.DESTINATION_TO_FILENAME_SEPARATOR; +import static io.quarkus.test.utils.PropertiesUtils.SECRET_WITH_DESTINATION_PREFIX; +import static io.quarkus.test.utils.TestExecutionProperties.isKubernetesPlatform; +import static io.quarkus.test.utils.TestExecutionProperties.isOpenshiftPlatform; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import io.quarkus.test.services.Certificate.Format; +import io.quarkus.test.utils.FileUtils; +import me.escoffier.certs.CertificateGenerator; +import me.escoffier.certs.CertificateRequest; +import me.escoffier.certs.JksCertificateFiles; +import me.escoffier.certs.PemCertificateFiles; +import me.escoffier.certs.Pkcs12CertificateFiles; + +public interface CertificateBuilder { + + /** + * Test context instance key. + */ + String INSTANCE_KEY = "io.quarkus.test.security.certificate#INSTANCE"; + + List certificates(); + + interface Certificate { + + String keystorePath(); + + String truststorePath(); + + Map configProperties(); + + } + + static CertificateBuilder of(io.quarkus.test.services.Certificate[] certificates) { + if (certificates == null || certificates.length == 0) { + return null; + } + return createBuilder(certificates); + } + + static Certificate of(String prefix, Format format, String password) { + return of(prefix, format, password, false, false); + } + + private static Certificate of(String prefix, Format format, String password, boolean keystoreProps, + boolean truststoreProps) { + Path certsDir = createCertsTempDir(prefix); + CertificateGenerator generator = new CertificateGenerator(certsDir, true); + CertificateRequest request = (new CertificateRequest()).withName(prefix) + .withClientCertificate(false) + .withFormat(me.escoffier.certs.Format.valueOf(format.toString())) + .withCN("localhost") + .withPassword(password) + .withDuration(Duration.ofDays(2)); + String trustStoreLocation = null; + String keyStoreLocation = null; + try { + var certFile = generator.generate(request).get(0); + if (certFile instanceof Pkcs12CertificateFiles pkcs12CertFile) { + if (pkcs12CertFile.trustStoreFile() != null) { + trustStoreLocation = pkcs12CertFile.trustStoreFile().toAbsolutePath().toString(); + } + if (pkcs12CertFile.keyStoreFile() != null) { + keyStoreLocation = pkcs12CertFile.keyStoreFile().toAbsolutePath().toString(); + } + } else if (certFile instanceof PemCertificateFiles pemCertsFile) { + if (pemCertsFile.serverTrustFile() != null) { + trustStoreLocation = pemCertsFile.serverTrustFile().toAbsolutePath().toString(); + } + } else if (certFile instanceof JksCertificateFiles jksCertFile) { + if (jksCertFile.trustStoreFile() != null) { + trustStoreLocation = jksCertFile.trustStoreFile().toAbsolutePath().toString(); + } + if (jksCertFile.keyStoreFile() != null) { + keyStoreLocation = jksCertFile.keyStoreFile().toAbsolutePath().toString(); + } + } + } catch (Exception e) { + throw new RuntimeException("Failed to generate certificate", e); + } + Map props = new HashMap<>(); + if (trustStoreLocation != null) { + if (isOpenshiftPlatform() || isKubernetesPlatform()) { + // mount truststore to the pod + props.put(getRandomPropKey("truststore"), toSecretProperty(trustStoreLocation)); + } + if (truststoreProps) { + props.put("quarkus.http.ssl.certificate.trust-store-file", trustStoreLocation); + props.put("quarkus.http.ssl.certificate.trust-store-file-type", format.toString()); + props.put("quarkus.http.ssl.certificate.trust-store-password", password); + } + } + if (keyStoreLocation != null) { + if (isOpenshiftPlatform() || isKubernetesPlatform()) { + keyStoreLocation = makeFileMountPathUnique(prefix, keyStoreLocation); + // mount keystore to the pod + props.put(getRandomPropKey("keystore"), toSecretProperty(keyStoreLocation)); + } + if (keystoreProps) { + props.put("quarkus.http.ssl.certificate.key-store-file", keyStoreLocation); + props.put("quarkus.http.ssl.certificate.key-store-file-type", format.toString()); + props.put("quarkus.http.ssl.certificate.key-store-password", password); + } + } + return new CertificateImpl(keyStoreLocation, trustStoreLocation, Map.copyOf(props)); + } + + private static String makeFileMountPathUnique(String prefix, String storeLocation) { + var newTempCertDir = createCertsTempDir(prefix); + var storeFile = Path.of(storeLocation).toFile(); + FileUtils.copyFileTo(storeFile, newTempCertDir); + return newTempCertDir.resolve(storeFile.getName()).toAbsolutePath().toString(); + } + + private static Path createCertsTempDir(String prefix) { + Path certsDir; + try { + certsDir = Files.createTempDirectory(prefix + "-certs"); + } catch (IOException e) { + throw new RuntimeException(e); + } + return certsDir; + } + + private static CertificateBuilder createBuilder(io.quarkus.test.services.Certificate[] certificates) { + Certificate[] generatedCerts = new Certificate[certificates.length]; + for (int i = 0; i < certificates.length; i++) { + generatedCerts[i] = of(certificates[i].prefix(), certificates[i].format(), certificates[i].password(), + certificates[i].configureKeystore(), certificates[i].configureTruststore()); + } + return new CertificateBuilderImp(List.of(generatedCerts)); + } + + private static String getRandomPropKey(String store) { + return store + "-" + new Random().nextInt(); + } + + private static String toSecretProperty(String path) { + int fileNameSeparatorIdx = path.lastIndexOf(File.separator); + String fileName = path.substring(fileNameSeparatorIdx + 1); + String pathToFile = path.substring(0, fileNameSeparatorIdx); + return SECRET_WITH_DESTINATION_PREFIX + pathToFile + DESTINATION_TO_FILENAME_SEPARATOR + fileName; + } + + record CertificateBuilderImp(List certificates) implements CertificateBuilder { + } + + record CertificateImpl(String keystorePath, String truststorePath, + Map configProperties) implements Certificate { + } +} diff --git a/quarkus-test-core/src/main/java/io/quarkus/test/services/Certificate.java b/quarkus-test-core/src/main/java/io/quarkus/test/services/Certificate.java new file mode 100644 index 000000000..07c6e0714 --- /dev/null +++ b/quarkus-test-core/src/main/java/io/quarkus/test/services/Certificate.java @@ -0,0 +1,52 @@ +package io.quarkus.test.services; + +/** + * Defines certificate request for which this framework will generate a keystore and truststore. + */ +public @interface Certificate { + + enum Format { + PEM, + JKS, + PKCS12 + } + + /** + * Prefix keystore and truststore name with this attribute. + */ + String prefix() default "quarkus-qe"; + + /** + * Secure file format. + */ + Format format() default Format.PKCS12; + + /** + * Keystore and truststore password. + */ + String password() default "password"; + + /** + * Whether following configuration properties should be set for you: + * + * - `quarkus.http.ssl.certificate.key-store-file` + * - `quarkus.http.ssl.certificate.key-store-file-type` + * - `quarkus.http.ssl.certificate.key-store-password` + * + * You still can set and/or override these properties + * with {@link io.quarkus.test.bootstrap.BaseService#withProperty(String, String)} service method. + */ + boolean configureKeystore() default false; + + /** + * Whether following configuration properties should be set for you: + * + * - `quarkus.http.ssl.certificate.trust-store-file` + * - `quarkus.http.ssl.certificate.trust-store-file-type` + * - `quarkus.http.ssl.certificate.trust-store-password` + * + * You still can set and/or override these properties + * with {@link io.quarkus.test.bootstrap.BaseService#withProperty(String, String)} service method. + */ + boolean configureTruststore() default false; +} diff --git a/quarkus-test-core/src/main/java/io/quarkus/test/services/DevModeQuarkusApplication.java b/quarkus-test-core/src/main/java/io/quarkus/test/services/DevModeQuarkusApplication.java index 5492650cc..63550da83 100644 --- a/quarkus-test-core/src/main/java/io/quarkus/test/services/DevModeQuarkusApplication.java +++ b/quarkus-test-core/src/main/java/io/quarkus/test/services/DevModeQuarkusApplication.java @@ -26,4 +26,9 @@ * `quarkus.http.ssl.certificate.key-store-password` to be set. */ boolean ssl() default false; + + /** + * Certificates that should be generated by this framework. + */ + Certificate[] certificates() default {}; } diff --git a/quarkus-test-core/src/main/java/io/quarkus/test/services/QuarkusApplication.java b/quarkus-test-core/src/main/java/io/quarkus/test/services/QuarkusApplication.java index 894df3418..5c52359a6 100644 --- a/quarkus-test-core/src/main/java/io/quarkus/test/services/QuarkusApplication.java +++ b/quarkus-test-core/src/main/java/io/quarkus/test/services/QuarkusApplication.java @@ -32,6 +32,11 @@ */ boolean ssl() default false; + /** + * Certificates that should be generated by this framework. + */ + Certificate[] certificates() default {}; + /** * Enable GRPC configuration. This property will map the gPRC service to a random port. */ diff --git a/quarkus-test-core/src/main/java/io/quarkus/test/services/quarkus/DevModeQuarkusApplicationManagedResourceBuilder.java b/quarkus-test-core/src/main/java/io/quarkus/test/services/quarkus/DevModeQuarkusApplicationManagedResourceBuilder.java index b4d679cac..d38f343c1 100644 --- a/quarkus-test-core/src/main/java/io/quarkus/test/services/quarkus/DevModeQuarkusApplicationManagedResourceBuilder.java +++ b/quarkus-test-core/src/main/java/io/quarkus/test/services/quarkus/DevModeQuarkusApplicationManagedResourceBuilder.java @@ -8,6 +8,7 @@ import io.quarkus.test.bootstrap.ManagedResource; import io.quarkus.test.bootstrap.ServiceContext; +import io.quarkus.test.security.certificate.CertificateBuilder; import io.quarkus.test.services.DevModeQuarkusApplication; import io.quarkus.test.services.quarkus.model.QuarkusProperties; import io.quarkus.test.utils.FileUtils; @@ -21,6 +22,7 @@ public void init(Annotation annotation) { setPropertiesFile(metadata.properties()); setGrpcEnabled(metadata.grpc()); setSslEnabled(metadata.ssl()); + setCertificateBuilder(CertificateBuilder.of(metadata.certificates())); } @Override @@ -32,6 +34,7 @@ protected Path getResourcesApplicationFolder() { public ManagedResource build(ServiceContext context) { setContext(context); configureLogging(); + configureCertificates(); if (QuarkusProperties.disableBuildAnalytics()) { getContext() .getOwner() diff --git a/quarkus-test-core/src/main/java/io/quarkus/test/services/quarkus/ProdQuarkusApplicationManagedResourceBuilder.java b/quarkus-test-core/src/main/java/io/quarkus/test/services/quarkus/ProdQuarkusApplicationManagedResourceBuilder.java index bcb0477a9..c39b9ce99 100644 --- a/quarkus-test-core/src/main/java/io/quarkus/test/services/quarkus/ProdQuarkusApplicationManagedResourceBuilder.java +++ b/quarkus-test-core/src/main/java/io/quarkus/test/services/quarkus/ProdQuarkusApplicationManagedResourceBuilder.java @@ -25,6 +25,7 @@ import io.quarkus.test.bootstrap.ManagedResource; import io.quarkus.test.bootstrap.ServiceContext; import io.quarkus.test.common.PathTestHelper; +import io.quarkus.test.security.certificate.CertificateBuilder; import io.quarkus.test.services.QuarkusApplication; import io.quarkus.test.services.quarkus.model.QuarkusProperties; import io.quarkus.test.utils.Command; @@ -68,12 +69,14 @@ public void init(Annotation annotation) { setGrpcEnabled(metadata.grpc()); initAppClasses(metadata.classes()); initForcedDependencies(metadata.dependencies()); + setCertificateBuilder(CertificateBuilder.of(metadata.certificates())); } @Override public ManagedResource build(ServiceContext context) { setContext(context); configureLogging(); + configureCertificates(); managedResource = findManagedResource(); build(); diff --git a/quarkus-test-core/src/main/java/io/quarkus/test/services/quarkus/QuarkusApplicationManagedResourceBuilder.java b/quarkus-test-core/src/main/java/io/quarkus/test/services/quarkus/QuarkusApplicationManagedResourceBuilder.java index 0c838022b..fea2c7495 100644 --- a/quarkus-test-core/src/main/java/io/quarkus/test/services/quarkus/QuarkusApplicationManagedResourceBuilder.java +++ b/quarkus-test-core/src/main/java/io/quarkus/test/services/quarkus/QuarkusApplicationManagedResourceBuilder.java @@ -24,8 +24,8 @@ import io.quarkus.builder.Version; import io.quarkus.test.bootstrap.ManagedResourceBuilder; import io.quarkus.test.bootstrap.ServiceContext; +import io.quarkus.test.security.certificate.CertificateBuilder; import io.quarkus.test.services.Dependency; -import io.quarkus.test.services.quarkus.model.QuarkusProperties; import io.quarkus.test.utils.ClassPathUtils; import io.quarkus.test.utils.FileUtils; import io.quarkus.test.utils.MapUtils; @@ -61,6 +61,7 @@ public abstract class QuarkusApplicationManagedResourceBuilder implements Manage private boolean sslEnabled = false; private boolean grpcEnabled = false; private Map propertiesSnapshot; + private CertificateBuilder certificateBuilder; protected abstract void build(); @@ -212,6 +213,21 @@ protected void configureLogging() { context.getOwner().withProperty("quarkus.log.console.format", "%d{HH:mm:ss,SSS} %s%e%n"); } + protected void configureCertificates() { + if (certificateBuilder != null) { + getContext().put(CertificateBuilder.INSTANCE_KEY, certificateBuilder); + certificateBuilder + .certificates() + .forEach(certificate -> certificate + .configProperties() + .forEach((k, v) -> context.getOwner().withProperty(k, v))); + } + } + + protected void setCertificateBuilder(CertificateBuilder certificateBuilder) { + this.certificateBuilder = certificateBuilder; + } + protected void copyResourcesToAppFolder() { copyResourcesInFolderToAppFolder(RESOURCES_FOLDER); copyResourcesInFolderToAppFolder(TEST_RESOURCES_FOLDER); @@ -279,14 +295,6 @@ private String propertyWithProfile(String name) { return "%" + context.getScenarioContext().getRunningTestClassName() + "." + name; } - private boolean isQuarkusVersion2Dot3OrAbove() { - String quarkusVersion = QuarkusProperties.getVersion(); - return !quarkusVersion.startsWith("2.2.") - && !quarkusVersion.startsWith("2.1.") - && !quarkusVersion.startsWith("2.0.") - && !quarkusVersion.startsWith("1."); - } - public boolean useSeparateManagementInterface() { return getContext().getOwner().getProperty("quarkus.management.enabled") .map("true"::equals) diff --git a/quarkus-test-openshift/src/main/java/io/quarkus/test/bootstrap/inject/OpenShiftClient.java b/quarkus-test-openshift/src/main/java/io/quarkus/test/bootstrap/inject/OpenShiftClient.java index a44150c99..63b606619 100644 --- a/quarkus-test-openshift/src/main/java/io/quarkus/test/bootstrap/inject/OpenShiftClient.java +++ b/quarkus-test-openshift/src/main/java/io/quarkus/test/bootstrap/inject/OpenShiftClient.java @@ -97,6 +97,7 @@ public final class OpenShiftClient { private static final Duration TIMEOUT_DEFAULT = Duration.ofMinutes(5); private static final int PROJECT_NAME_SIZE = 10; private static final int PROJECT_CREATION_RETRIES = 5; + private static final int SPECS_SECRET_NAME_LIMIT = 63; private static final String OPERATOR_PHASE_INSTALLED = "Succeeded"; private static final String BUILD_FAILED_STATUS = "Failed"; private static final String CUSTOM_RESOURCE_EXPECTED_TYPE = "Ready"; @@ -892,14 +893,15 @@ private Map enrichProperties(Map properties, Dep } } else if (propertyValue.startsWith(SECRET_WITH_DESTINATION_PREFIX)) { String path = entry.getValue().replace(SECRET_WITH_DESTINATION_PREFIX, StringUtils.EMPTY); - int separatorIdx = path.indexOf(DESTINATION_TO_FILENAME_SEPARATOR); + int separatorIdx = path.lastIndexOf(DESTINATION_TO_FILENAME_SEPARATOR); final String mountPath = path.substring(0, separatorIdx); - final String filename = getFileName(path.substring(separatorIdx + 1)); - final String secretName = normalizeConfigMapName(mountPath, filename); + final String filename = path.substring(separatorIdx + 1); + String secretName = normalizeConfigMapName(mountPath, filename); // Push secret file - doCreateSecretFromFile(secretName, getFilePath(SLASH + filename)); propertyValue = joinMountPathAndFileName(mountPath, filename); + String filePath = Files.exists(Path.of(propertyValue)) ? propertyValue : getFilePath(SLASH + filename); + doCreateSecretFromFile(secretName, filePath); volumes.putIfAbsent(mountPath, new CustomVolume(secretName, "", SECRET)); } else if (isSecret(propertyValue)) { String path = entry.getValue().replace(SECRET_PREFIX, StringUtils.EMPTY); @@ -1002,9 +1004,17 @@ private String getFilePath(String path) { private String normalizeConfigMapName(String mountPath, String fileName) { // /some/mount/path/file-name => some-mount-path-file-name - return StringUtils.removeStart(joinMountPathAndFileName(mountPath, fileName), SLASH) + var newName = StringUtils.removeStart(joinMountPathAndFileName(mountPath, fileName), SLASH) .replaceAll(Pattern.quote("."), "-") .replaceAll(SLASH, "-"); + if (newName.length() > SPECS_SECRET_NAME_LIMIT) { + newName = newName.substring(newName.length() - SPECS_SECRET_NAME_LIMIT); + while (newName.startsWith("-")) { + // must not start with '-something' as it's considered to be a flag + newName = newName.substring(1); + } + } + return newName; } private static String joinMountPathAndFileName(String mountPath, String fileName) { diff --git a/quarkus-test-service-kafka/pom.xml b/quarkus-test-service-kafka/pom.xml index 5bf157d24..eb0c29307 100644 --- a/quarkus-test-service-kafka/pom.xml +++ b/quarkus-test-service-kafka/pom.xml @@ -52,9 +52,5 @@ true provided - - me.escoffier.certs - certificate-generator - diff --git a/quarkus-test-service-kafka/src/main/java/io/quarkus/test/services/containers/StrimziKafkaContainerManagedResource.java b/quarkus-test-service-kafka/src/main/java/io/quarkus/test/services/containers/StrimziKafkaContainerManagedResource.java index 707539aea..c43521c24 100644 --- a/quarkus-test-service-kafka/src/main/java/io/quarkus/test/services/containers/StrimziKafkaContainerManagedResource.java +++ b/quarkus-test-service-kafka/src/main/java/io/quarkus/test/services/containers/StrimziKafkaContainerManagedResource.java @@ -1,30 +1,26 @@ package io.quarkus.test.services.containers; -import static me.escoffier.certs.Format.PKCS12; +import static io.quarkus.test.services.Certificate.Format.PKCS12; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import org.apache.commons.lang3.StringUtils; import org.testcontainers.containers.GenericContainer; import io.quarkus.test.bootstrap.KafkaService; import io.quarkus.test.bootstrap.Protocol; +import io.quarkus.test.security.certificate.CertificateBuilder; import io.quarkus.test.services.URILike; import io.quarkus.test.services.containers.model.KafkaProtocol; import io.quarkus.test.services.containers.model.KafkaVendor; import io.quarkus.test.services.containers.strimzi.ExtendedStrimziKafkaContainer; import io.quarkus.test.utils.DockerUtils; -import me.escoffier.certs.CertificateGenerator; -import me.escoffier.certs.CertificateRequest; -import me.escoffier.certs.Pkcs12CertificateFiles; public class StrimziKafkaContainerManagedResource extends BaseKafkaContainerManagedResource { @@ -139,25 +135,10 @@ protected String[] getKafkaConfigResources() { final String trustStoreLocation; if (useDefaultServerProperties()) { if (useDefaultTrustStore()) { - // generate certs - final Path certsDir; - try { - certsDir = Files.createTempDirectory("certs"); - } catch (IOException e) { - throw new RuntimeException(e); - } - CertificateGenerator generator = new CertificateGenerator(certsDir, true); - CertificateRequest request = (new CertificateRequest()).withName(STRIMZI_SERVER_SSL) - .withClientCertificate(false).withFormat(PKCS12).withCN("localhost").withPassword("top-secret") - .withDuration(Duration.ofDays(2)); - try { - var certFile = (Pkcs12CertificateFiles) generator.generate(request).get(0); - trustStoreLocation = certFile.trustStoreFile().toString(); - effectiveUserKafkaConfigResources.add(trustStoreLocation); - effectiveUserKafkaConfigResources.add(certFile.keyStoreFile().toString()); - } catch (Exception e) { - throw new RuntimeException(e); - } + var cert = CertificateBuilder.of(STRIMZI_SERVER_SSL, PKCS12, "top-secret"); + trustStoreLocation = Objects.requireNonNull(cert.truststorePath()); + effectiveUserKafkaConfigResources.add(trustStoreLocation); + effectiveUserKafkaConfigResources.add(Objects.requireNonNull(cert.keystorePath())); } else { // truststore in application resources dir trustStoreLocation = SSL_SERVER_TRUSTSTORE;