From fc510083800334c5a91ea8ab22c9a0c8126d067b Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Fri, 4 Oct 2024 16:47:17 +0200 Subject: [PATCH 01/31] Offer a way to start the RealJenkinsRule instance with https only --- .../jvnet/hudson/test/RealJenkinsRule.java | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java index d69e7f3a0..425bc117b 100644 --- a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java +++ b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java @@ -61,6 +61,7 @@ import java.nio.file.attribute.FileAttribute; import java.security.MessageDigest; import java.util.ArrayList; +import java.util.Collection; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; @@ -211,6 +212,7 @@ public final class RealJenkinsRule implements TestRule { private static final Pattern SNAPSHOT_INDEX_JELLY = Pattern.compile("(file:/.+/target)/classes/index.jelly"); private final PrefixedOutputStream.Builder prefixedOutputStreamBuilder = PrefixedOutputStream.builder(); + private boolean https; public RealJenkinsRule() { home = new AtomicReference<>(); @@ -728,7 +730,15 @@ public URL getUrl() throws MalformedURLException { if (port == 0) { throw new IllegalStateException("This method must be called after calling #startJenkins."); } - return new URL("http", host, port, "/jenkins/"); + return new URL(https ? "https" : "http", host, port, "/jenkins/"); + } + + public RealJenkinsRule https(File keyStoreFile, String keyStorePassword) { + this.https = true; + this.jenkinsOptions( + "--httpsKeyStore=" + keyStoreFile.getAbsolutePath(), + "--httpsKeyStorePassword=" + keyStorePassword); + return this; } private URL endpoint(String method) throws MalformedURLException { @@ -821,9 +831,9 @@ public void startJenkins() throws Throwable { argv.addAll(List.of( "-jar", war.getAbsolutePath(), "--enable-future-java", - "--httpPort=" + port, // initially port=0. On subsequent runs, the port is set to the port used allocated randomly on the first run. "--httpListenAddress=" + httpListenAddress, "--prefix=/jenkins")); + argv.addAll(getPortOptions()); argv.addAll(jenkinsOptions); Map env = new TreeMap<>(); env.put("JENKINS_HOME", getHome().getAbsolutePath()); @@ -891,6 +901,15 @@ public void startJenkins() throws Throwable { addTimeout(); } + private Collection getPortOptions() { + // initially port=0. On subsequent runs, the port is set to the port used allocated randomly on the first run.); + if (https) { + return List.of("--httpPort=-1", "--httpsPort=" + port); + } else { + return List.of("--httpPort=" + port); + } + } + @CheckForNull public static String checkResult(HttpURLConnection conn) throws IOException { int code = conn.getResponseCode(); From fb23dfbf812be769cbeb6f9bb3bc707387b300d8 Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Mon, 7 Oct 2024 15:33:07 +0200 Subject: [PATCH 02/31] Require a custom SSLContext when using https --- .../java/org/jvnet/hudson/test/RealJenkinsRule.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java index 425bc117b..678b89763 100644 --- a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java +++ b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java @@ -89,6 +89,8 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -213,6 +215,7 @@ public final class RealJenkinsRule implements TestRule { private final PrefixedOutputStream.Builder prefixedOutputStreamBuilder = PrefixedOutputStream.builder(); private boolean https; + private SSLContext sslContext; public RealJenkinsRule() { home = new AtomicReference<>(); @@ -733,8 +736,9 @@ public URL getUrl() throws MalformedURLException { return new URL(https ? "https" : "http", host, port, "/jenkins/"); } - public RealJenkinsRule https(File keyStoreFile, String keyStorePassword) { + public RealJenkinsRule https(File keyStoreFile, String keyStorePassword, SSLContext sslContext) { this.https = true; + this.sslContext = sslContext; this.jenkinsOptions( "--httpsKeyStore=" + keyStoreFile.getAbsolutePath(), "--httpsKeyStorePassword=" + keyStorePassword); @@ -871,6 +875,9 @@ public void startJenkins() throws Throwable { try { URL status = endpoint("status"); HttpURLConnection conn = (HttpURLConnection) status.openConnection(); + if (https && sslContext != null) { + ((HttpsURLConnection) conn).setSSLSocketFactory(sslContext.getSocketFactory()); + } String checkResult = checkResult(conn); if (checkResult == null) { @@ -1015,6 +1022,9 @@ public void runRemotely(Step... steps) throws Throwable { @SuppressWarnings("unchecked") public T runRemotely(Step2 s) throws Throwable { HttpURLConnection conn = (HttpURLConnection) endpoint("step").openConnection(); + if (https && sslContext != null) { + ((HttpsURLConnection) conn).setSSLSocketFactory(sslContext.getSocketFactory()); + } conn.setRequestProperty("Content-Type", "application/octet-stream"); conn.setDoOutput(true); From 6492730873c9221c36b9f1569bad654130dd3a0f Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Mon, 7 Oct 2024 15:49:18 +0200 Subject: [PATCH 03/31] Forgot one call --- .../jvnet/hudson/test/RealJenkinsRule.java | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java index 678b89763..1bf0a7324 100644 --- a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java +++ b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java @@ -25,6 +25,7 @@ package org.jvnet.hudson.test; import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.ExtensionList; import hudson.model.UnprotectedRootAction; @@ -54,6 +55,7 @@ import java.net.URI; import java.net.URL; import java.net.URLClassLoader; +import java.net.URLConnection; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -874,10 +876,7 @@ public void startJenkins() throws Throwable { if (port != 0) { try { URL status = endpoint("status"); - HttpURLConnection conn = (HttpURLConnection) status.openConnection(); - if (https && sslContext != null) { - ((HttpsURLConnection) conn).setSSLSocketFactory(sslContext.getSocketFactory()); - } + HttpURLConnection conn = decorateConnection(status.openConnection()); String checkResult = checkResult(conn); if (checkResult == null) { @@ -991,7 +990,7 @@ public void stopJenkins() throws Throwable { proc = null; if (_proc.isAlive()) { try { - endpoint("exit").openStream().close(); + decorateConnection(endpoint("exit").openConnection()).getInputStream().close(); } catch (SocketException e) { System.err.println("Unable to connect to the Jenkins process to stop it: " + e); } @@ -1021,10 +1020,7 @@ public void runRemotely(Step... steps) throws Throwable { @SuppressWarnings("unchecked") public T runRemotely(Step2 s) throws Throwable { - HttpURLConnection conn = (HttpURLConnection) endpoint("step").openConnection(); - if (https && sslContext != null) { - ((HttpsURLConnection) conn).setSSLSocketFactory(sslContext.getSocketFactory()); - } + HttpURLConnection conn = decorateConnection(endpoint("step").openConnection()); conn.setRequestProperty("Content-Type", "application/octet-stream"); conn.setDoOutput(true); @@ -1050,6 +1046,13 @@ public T runRemotely(Step2 s) throws Throwable { } } + private HttpURLConnection decorateConnection(@NonNull URLConnection step) { + if (https && sslContext != null) { + ((HttpsURLConnection) step).setSSLSocketFactory(sslContext.getSocketFactory()); + } + return (HttpURLConnection) step; + } + @FunctionalInterface public interface StepWithOneArg extends Serializable { void run(JenkinsRule r, A1 arg1) throws Throwable; From faf65402e2c6ab8c2f327ffb25ad576ac2cbd651 Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Mon, 7 Oct 2024 17:06:19 +0200 Subject: [PATCH 04/31] Proper name --- src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java index 1bf0a7324..7983c55fc 100644 --- a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java +++ b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java @@ -1046,11 +1046,11 @@ public T runRemotely(Step2 s) throws Throwable { } } - private HttpURLConnection decorateConnection(@NonNull URLConnection step) { + private HttpURLConnection decorateConnection(@NonNull URLConnection urlConnection) { if (https && sslContext != null) { - ((HttpsURLConnection) step).setSSLSocketFactory(sslContext.getSocketFactory()); + ((HttpsURLConnection) urlConnection).setSSLSocketFactory(sslContext.getSocketFactory()); } - return (HttpURLConnection) step; + return (HttpURLConnection) urlConnection; } @FunctionalInterface From 7481bcc72c37c5cd900634cf0a60ddf69d4105b1 Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Tue, 8 Oct 2024 12:25:04 +0200 Subject: [PATCH 05/31] Remove https in favor for checking SSLContext nullity --- src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java index 7983c55fc..97a01cec2 100644 --- a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java +++ b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java @@ -216,7 +216,7 @@ public final class RealJenkinsRule implements TestRule { private static final Pattern SNAPSHOT_INDEX_JELLY = Pattern.compile("(file:/.+/target)/classes/index.jelly"); private final PrefixedOutputStream.Builder prefixedOutputStreamBuilder = PrefixedOutputStream.builder(); - private boolean https; + @CheckForNull private SSLContext sslContext; public RealJenkinsRule() { @@ -735,11 +735,10 @@ public URL getUrl() throws MalformedURLException { if (port == 0) { throw new IllegalStateException("This method must be called after calling #startJenkins."); } - return new URL(https ? "https" : "http", host, port, "/jenkins/"); + return new URL(sslContext != null ? "https" : "http", host, port, "/jenkins/"); } public RealJenkinsRule https(File keyStoreFile, String keyStorePassword, SSLContext sslContext) { - this.https = true; this.sslContext = sslContext; this.jenkinsOptions( "--httpsKeyStore=" + keyStoreFile.getAbsolutePath(), @@ -909,7 +908,7 @@ public void startJenkins() throws Throwable { private Collection getPortOptions() { // initially port=0. On subsequent runs, the port is set to the port used allocated randomly on the first run.); - if (https) { + if (sslContext != null) { return List.of("--httpPort=-1", "--httpsPort=" + port); } else { return List.of("--httpPort=" + port); @@ -1047,7 +1046,7 @@ public T runRemotely(Step2 s) throws Throwable { } private HttpURLConnection decorateConnection(@NonNull URLConnection urlConnection) { - if (https && sslContext != null) { + if (sslContext != null) { ((HttpsURLConnection) urlConnection).setSSLSocketFactory(sslContext.getSocketFactory()); } return (HttpURLConnection) urlConnection; From 2c2e3214759bc83ab2f46b7cf327360adac77896 Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Tue, 8 Oct 2024 12:25:48 +0200 Subject: [PATCH 06/31] Update src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java Co-authored-by: Jesse Glick --- src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java index 97a01cec2..67a35c737 100644 --- a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java +++ b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java @@ -907,7 +907,7 @@ public void startJenkins() throws Throwable { } private Collection getPortOptions() { - // initially port=0. On subsequent runs, the port is set to the port used allocated randomly on the first run.); + // Initially port=0. On subsequent runs, this is set to the port allocated randomly on the first run. if (sslContext != null) { return List.of("--httpPort=-1", "--httpsPort=" + port); } else { From a9929e74d6f8517ce00fae1956290011e0cc2c34 Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Tue, 8 Oct 2024 12:31:16 +0200 Subject: [PATCH 07/31] Replace SSLContext by SSLSocketFactory --- .../org/jvnet/hudson/test/RealJenkinsRule.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java index 67a35c737..edb408cc6 100644 --- a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java +++ b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java @@ -92,7 +92,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -217,7 +217,7 @@ public final class RealJenkinsRule implements TestRule { private final PrefixedOutputStream.Builder prefixedOutputStreamBuilder = PrefixedOutputStream.builder(); @CheckForNull - private SSLContext sslContext; + private SSLSocketFactory sslSocketFactory; public RealJenkinsRule() { home = new AtomicReference<>(); @@ -735,11 +735,11 @@ public URL getUrl() throws MalformedURLException { if (port == 0) { throw new IllegalStateException("This method must be called after calling #startJenkins."); } - return new URL(sslContext != null ? "https" : "http", host, port, "/jenkins/"); + return new URL(sslSocketFactory != null ? "https" : "http", host, port, "/jenkins/"); } - public RealJenkinsRule https(File keyStoreFile, String keyStorePassword, SSLContext sslContext) { - this.sslContext = sslContext; + public RealJenkinsRule https(File keyStoreFile, String keyStorePassword, SSLSocketFactory sslSocketFactory) { + this.sslSocketFactory = sslSocketFactory; this.jenkinsOptions( "--httpsKeyStore=" + keyStoreFile.getAbsolutePath(), "--httpsKeyStorePassword=" + keyStorePassword); @@ -908,7 +908,7 @@ public void startJenkins() throws Throwable { private Collection getPortOptions() { // Initially port=0. On subsequent runs, this is set to the port allocated randomly on the first run. - if (sslContext != null) { + if (sslSocketFactory != null) { return List.of("--httpPort=-1", "--httpsPort=" + port); } else { return List.of("--httpPort=" + port); @@ -1046,8 +1046,8 @@ public T runRemotely(Step2 s) throws Throwable { } private HttpURLConnection decorateConnection(@NonNull URLConnection urlConnection) { - if (sslContext != null) { - ((HttpsURLConnection) urlConnection).setSSLSocketFactory(sslContext.getSocketFactory()); + if (sslSocketFactory != null) { + ((HttpsURLConnection) urlConnection).setSSLSocketFactory(sslSocketFactory); } return (HttpURLConnection) urlConnection; } From 024f45c1767512a36a7ff4ca262aede9bb321d25 Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Tue, 8 Oct 2024 12:45:54 +0200 Subject: [PATCH 08/31] createWebClient --- .../jvnet/hudson/test/RealJenkinsRule.java | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java index edb408cc6..1ef88e13e 100644 --- a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java +++ b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java @@ -61,6 +61,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.FileAttribute; +import java.security.KeyStore; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Collection; @@ -103,6 +104,7 @@ import jenkins.model.JenkinsLocationConfiguration; import jenkins.util.Timer; import org.apache.commons.io.FileUtils; +import org.htmlunit.WebClient; import org.junit.AssumptionViolatedException; import org.junit.rules.DisableOnDebug; import org.junit.rules.TemporaryFolder; @@ -218,6 +220,9 @@ public final class RealJenkinsRule implements TestRule { private final PrefixedOutputStream.Builder prefixedOutputStreamBuilder = PrefixedOutputStream.builder(); @CheckForNull private SSLSocketFactory sslSocketFactory; + private File keyStoreFile; + private String keyStorePassword; + private URL keyStoreUrl; public RealJenkinsRule() { home = new AtomicReference<>(); @@ -738,11 +743,11 @@ public URL getUrl() throws MalformedURLException { return new URL(sslSocketFactory != null ? "https" : "http", host, port, "/jenkins/"); } - public RealJenkinsRule https(File keyStoreFile, String keyStorePassword, SSLSocketFactory sslSocketFactory) { + public RealJenkinsRule https(File keyStoreFile, String keyStorePassword, SSLSocketFactory sslSocketFactory) throws MalformedURLException { this.sslSocketFactory = sslSocketFactory; - this.jenkinsOptions( - "--httpsKeyStore=" + keyStoreFile.getAbsolutePath(), - "--httpsKeyStorePassword=" + keyStorePassword); + this.keyStoreFile = keyStoreFile; + this.keyStoreUrl = keyStoreFile.toURI().toURL(); + this.keyStorePassword = keyStorePassword; return this; } @@ -786,6 +791,17 @@ private static File findJenkinsWar() throws Exception { return WarExploder.findJenkinsWar(); } + /** + * Create a client configured to trust the self-signed certificate used by Jenkins. + */ + public WebClient createWebClient() { + var wc = new WebClient(); + if (keyStoreUrl != null && keyStorePassword != null) { + wc.getOptions().setSSLTrustStore(keyStoreUrl, keyStorePassword, KeyStore.getDefaultType()); + } + return wc; + } + @SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "irrelevant") public void startJenkins() throws Throwable { if (proc != null) { @@ -839,6 +855,9 @@ public void startJenkins() throws Throwable { "--httpListenAddress=" + httpListenAddress, "--prefix=/jenkins")); argv.addAll(getPortOptions()); + if (keyStoreFile != null && keyStorePassword != null) { + argv.addAll(List.of("--httpsKeyStore=" + keyStoreFile.getAbsolutePath(), "--httpsKeyStorePassword=" + keyStorePassword)); + } argv.addAll(jenkinsOptions); Map env = new TreeMap<>(); env.put("JENKINS_HOME", getHome().getAbsolutePath()); From eb274dbc4f6581480bc2954cea263c4065def35e Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Tue, 8 Oct 2024 14:30:53 +0200 Subject: [PATCH 09/31] Missing decoration --- src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java index 1ef88e13e..16ff2ecd8 100644 --- a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java +++ b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java @@ -962,7 +962,7 @@ private void addTimeout() { if (proc != null) { LOGGER.warning("Test timeout expired, stopping steps…"); try { - endpoint("timeout").openStream().close(); + decorateConnection(endpoint("timeout").openConnection()).openStream().close(); } catch (IOException x) { x.printStackTrace(); } From 3631d45dabbc285c11b9428aebac61de56fce402 Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Tue, 8 Oct 2024 14:45:42 +0200 Subject: [PATCH 10/31] Fix compilation --- src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java index 16ff2ecd8..d6a485849 100644 --- a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java +++ b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java @@ -962,7 +962,7 @@ private void addTimeout() { if (proc != null) { LOGGER.warning("Test timeout expired, stopping steps…"); try { - decorateConnection(endpoint("timeout").openConnection()).openStream().close(); + decorateConnection(endpoint("timeout").openConnection()).getInputStream().close(); } catch (IOException x) { x.printStackTrace(); } From 43483eaa9386ea02876d8bcf8fe7c74be4622ae6 Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Tue, 8 Oct 2024 15:31:19 +0200 Subject: [PATCH 11/31] Have RealJenkinsRule generates a https certificate automatically --- pom.xml | 9 + .../jenkins/test/https/KeyStoreManager.java | 261 ++++++++++++++++++ .../test/https/SelfSignedCertificates.java | 167 +++++++++++ .../SelfSignedCertificatesException.java | 38 +++ .../jvnet/hudson/test/RealJenkinsRule.java | 118 ++++++-- 5 files changed, 577 insertions(+), 16 deletions(-) create mode 100644 src/main/java/jenkins/test/https/KeyStoreManager.java create mode 100644 src/main/java/jenkins/test/https/SelfSignedCertificates.java create mode 100644 src/main/java/jenkins/test/https/SelfSignedCertificatesException.java diff --git a/pom.xml b/pom.xml index 0134d06da..542ca1681 100644 --- a/pom.xml +++ b/pom.xml @@ -96,6 +96,11 @@ THE SOFTWARE. access-modifier-annotation 1.34 + + org.bouncycastle + bcpkix-jdk18on + 1.78.1 + @@ -212,6 +217,10 @@ THE SOFTWARE. jmh-generator-annprocess ${jmh.version} + + org.bouncycastle + bcpkix-jdk18on + org.jenkins-ci test-annotations diff --git a/src/main/java/jenkins/test/https/KeyStoreManager.java b/src/main/java/jenkins/test/https/KeyStoreManager.java new file mode 100644 index 000000000..364140860 --- /dev/null +++ b/src/main/java/jenkins/test/https/KeyStoreManager.java @@ -0,0 +1,261 @@ +/* + * The MIT License + * + * Copyright 2024 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package jenkins.test.https; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.List; +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 javax.net.ssl.X509TrustManager; + +/** + * Allows to manage a java keystore file more easily than base JDK. + */ +public class KeyStoreManager { + @NonNull + private final Path path; + @NonNull + private final URL url; + @NonNull + private final String password; + @NonNull + private final KeyStore keyStore; + @NonNull + private final String type; + + /** + * Creates a new instance using the default keystore type. + * @param path path of the keystore file. If it exists, it will be loaded automatically. + * @param password password for the keystore file. + */ + public KeyStoreManager(@NonNull Path path, @NonNull String password) + throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException { + this(path, password, KeyStore.getDefaultType()); + } + + /** + * Creates a new instance using the specified keystore type. + * @param path path of the keystore file. If it exists, it will be loaded automatically. + * @param password password for the keystore file. + * @param type type of the keystore file. + */ + public KeyStoreManager(@NonNull Path path, @NonNull String password, @NonNull String type) + throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException { + this.path = path; + this.url = path.toUri().toURL(); + this.password = password; + this.type = type; + var tmpKeyStore = KeyStore.getInstance(type); + if (Files.exists(path)) { + try (var is = Files.newInputStream(path)) { + tmpKeyStore.load(is, password.toCharArray()); + } + } else { + tmpKeyStore.load(null); + } + this.keyStore = tmpKeyStore; + } + + @NonNull + private static X509TrustManager getDefaultX509CertificateTrustManager(TrustManagerFactory trustManagerFactory) { + return Arrays.stream(trustManagerFactory.getTrustManagers()) + .filter(X509TrustManager.class::isInstance) + .map(X509TrustManager.class::cast) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Could not load default trust manager")); + } + + /** + * @return the password for the managed keystore + */ + public String getPassword() { + return password; + } + + /** + * @return the path where the managed keystore is persisted to. + * Make sure {@link #save()} has been called before using the path. + */ + public Path getPath() { + return path; + } + + /** + * @return the type of the managed keystore. + */ + public String getType() { + return type; + } + + /** + * @return returns the URL representation of the keystore file. + *

+ * Make sure {@link #save()} has been called before using the path. + */ + public URL getURL() { + return url; + } + + /** + * Persists the current keystore to disk. + */ + public void save() throws IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException { + try (var os = Files.newOutputStream(path)) { + keyStore.store(os, password.toCharArray()); + } + } + + /** + * Build a custom SSL context that trusts the default certificates as well as those in the current keystore. + */ + @NonNull + public SSLContext buildClientSSLContext() + throws NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException, + KeyManagementException { + X509TrustManager result; + try (var myKeysInputStream = Files.newInputStream(path)) { + var myTrustStore = KeyStore.getInstance(type); + myTrustStore.load(myKeysInputStream, password.toCharArray()); + var trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(myTrustStore); + result = getDefaultX509CertificateTrustManager(trustManagerFactory); + } + var trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init((KeyStore) null); + var wrapper = new MergedTrustManager(getDefaultX509CertificateTrustManager(trustManagerFactory), result); + var context = SSLContext.getInstance("TLS"); + context.init(null, new TrustManager[] {wrapper}, null); + return context; + } + + /** + * Build server context for server usage. + * @return a SSLContext instance configured with the key store. + */ + public SSLContext buildServerSSLContext() { + final KeyManager[] keyManagers; + try { + var keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keyStore, password.toCharArray()); + keyManagers = keyManagerFactory.getKeyManagers(); + } catch (NoSuchAlgorithmException | UnrecoverableKeyException | KeyStoreException e) { + throw new RuntimeException("Unable to initialise KeyManager[]", e); + } + + final TrustManager[] trustManagers; + try { + var trustManagerFactory = TrustManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(keyStore); + trustManagers = trustManagerFactory.getTrustManagers(); + } catch (NoSuchAlgorithmException | KeyStoreException e) { + throw new RuntimeException("Unable to initialise TrustManager[]", e); + } + + try { + var sslContext = SSLContext.getInstance("TLS"); + sslContext.init(keyManagers, trustManagers, null); + return sslContext; + } catch (NoSuchAlgorithmException | KeyManagementException e) { + throw new RuntimeException("Unable to create and initialise the SSLContext", e); + } + } + + /** + * @see KeyStore#setCertificateEntry(String, Certificate) + */ + public void setCertificateEntry(String alias, X509Certificate certificate) throws KeyStoreException { + keyStore.setCertificateEntry(alias, certificate); + } + + /** + * @see KeyStore#setKeyEntry(String, byte[], Certificate[]) + */ + public void setKeyEntry(String host, PrivateKey privateKey, Certificate[] certificates) throws KeyStoreException { + keyStore.setKeyEntry(host, privateKey, password.toCharArray(), certificates); + } + + private static class MergedTrustManager implements X509TrustManager { + private final X509TrustManager defaultTrustManager; + private final List trustManagers; + + public MergedTrustManager(X509TrustManager defaultTrustManager, X509TrustManager... trustManagers) { + this.defaultTrustManager = defaultTrustManager; + this.trustManagers = List.of(trustManagers); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return trustManagers.stream() + .map(X509TrustManager::getAcceptedIssuers) + .flatMap(Arrays::stream) + .toArray(X509Certificate[]::new); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + CertificateException exceptionResult = null; + for (var trustManager : trustManagers) { + try { + trustManager.checkServerTrusted(chain, authType); + return; + } catch (CertificateException e) { + if (exceptionResult == null) { + exceptionResult = e; + } else { + exceptionResult.addSuppressed(e); + } + } + } + if (exceptionResult != null) { + throw exceptionResult; + } + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + defaultTrustManager.checkClientTrusted(trustManagers.stream() + .map(X509TrustManager::getAcceptedIssuers) + .flatMap(Arrays::stream) + .toArray(X509Certificate[]::new), authType); + } + } +} diff --git a/src/main/java/jenkins/test/https/SelfSignedCertificates.java b/src/main/java/jenkins/test/https/SelfSignedCertificates.java new file mode 100644 index 000000000..3999ca6f4 --- /dev/null +++ b/src/main/java/jenkins/test/https/SelfSignedCertificates.java @@ -0,0 +1,167 @@ +/* + * The MIT License + * + * Copyright 2024 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package jenkins.test.https; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.Security; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Date; +import java.util.List; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.CertIOException; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; + +/** + * A utility class to generate self-signed certificates. + *

    + *
  • A root CA + *
  • An intermediate CA signed by the root CA + *
+ * Then you can generate user certificates for a given DNS name signed by the intermediate CA. + * + * @see #createRootCAs() + * @see #createUserCert(String, CertificateKeyPair) + */ +public record SelfSignedCertificates(CertificateKeyPair root, CertificateKeyPair intermediate) { + private static final SecureRandom RANDOM = new SecureRandom(); + private static final String CERTIFICATE_ALGORITHM = "RSA"; + private static final int CERTIFICATE_BITS = 2048; + private static final String ROOT_DN = "CN=Root"; + + static { + // adds the Bouncy castle provider to java security + Security.addProvider(new BouncyCastleProvider()); + } + + @NonNull + public static SelfSignedCertificates createRootCAs() { + try { + // create root CA self-signed. + var rootKeyPair = generateKeyPair(); + var rootBuilder = new JcaX509v3CertificateBuilder( + new X500Name(ROOT_DN), + generateSerialNumber(), + Date.from(LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant()), + Date.from(LocalDate.now() + .plusDays(4) + .atStartOfDay(ZoneId.systemDefault()) + .toInstant()), + new X500Name(ROOT_DN), + SubjectPublicKeyInfo.getInstance(rootKeyPair.getPublic().getEncoded())); + rootBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign)); + rootBuilder.addExtension(Extension.basicConstraints, false, new BasicConstraints(true)); + var root = new CertificateKeyPair(rootKeyPair, new JcaX509CertificateConverter().getCertificate(rootBuilder.build(newContentSigner(rootKeyPair)))); + + // create Intermediate CA cert signed by Root CA + var intermediateKeyPair = generateKeyPair(); + var intermediateBuilder = new JcaX509v3CertificateBuilder( + root.certificate(), + generateSerialNumber(), + Date.from(LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant()), + Date.from(LocalDate.now() + .plusDays(2) + .atStartOfDay(ZoneId.systemDefault()) + .toInstant()), + new X500Name("CN=Intermediate"), + intermediateKeyPair.getPublic()); + intermediateBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign)); + intermediateBuilder.addExtension(Extension.basicConstraints, false, new BasicConstraints(true)); + var intermediate = new CertificateKeyPair(intermediateKeyPair, new JcaX509CertificateConverter().getCertificate(intermediateBuilder.build(newContentSigner(root.keyPair())))); + return new SelfSignedCertificates(root, intermediate); + } catch (OperatorCreationException | CertificateException | CertIOException | NoSuchAlgorithmException e) { + throw new SelfSignedCertificatesException(e); + } + } + + private static ContentSigner newContentSigner(KeyPair keyPair) throws OperatorCreationException { + return new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC").build(keyPair.getPrivate()); + } + + @NonNull + static BigInteger generateSerialNumber() { + return BigInteger.valueOf(RANDOM.nextInt()); + } + + /** + * Generate a user certificate signed by the intermediate CA. + * @param dnsName The DNS name to use in the certificate. + * @param issuer The intermediate CA to sign the certificate. + * @return The user certificate and key pair. + */ + public static CertificateKeyPair createUserCert(String dnsName, CertificateKeyPair issuer) { + try { + var keyPair = generateKeyPair(); + // create end user cert signed by Intermediate CA + var builder = new JcaX509v3CertificateBuilder( + issuer.certificate(), + generateSerialNumber(), + Date.from(LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant()), + Date.from(LocalDate.now() + .plusDays(1) + .atStartOfDay(ZoneId.systemDefault()) + .toInstant()), + new X500Name("CN=endUserCert"), + keyPair.getPublic()); + builder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature)); + builder.addExtension(Extension.basicConstraints, false, new BasicConstraints(false)); + var altNames = List.of(new GeneralName(GeneralName.dNSName, dnsName)); + var subjectAltNames = GeneralNames.getInstance(new DERSequence(altNames.toArray(new GeneralName[0]))); + builder.addExtension(Extension.subjectAlternativeName, false, subjectAltNames); + return new CertificateKeyPair( + keyPair, + new JcaX509CertificateConverter().getCertificate(builder.build(newContentSigner(issuer.keyPair())))); + } catch (OperatorCreationException | CertificateException | CertIOException | NoSuchAlgorithmException e) { + throw new SelfSignedCertificatesException(e); + } + } + + private static KeyPair generateKeyPair() throws NoSuchAlgorithmException { + var keyPairGenerator = KeyPairGenerator.getInstance(CERTIFICATE_ALGORITHM); + keyPairGenerator.initialize(CERTIFICATE_BITS, RANDOM); + return keyPairGenerator.generateKeyPair(); + } + + public record CertificateKeyPair(KeyPair keyPair, X509Certificate certificate) {} +} diff --git a/src/main/java/jenkins/test/https/SelfSignedCertificatesException.java b/src/main/java/jenkins/test/https/SelfSignedCertificatesException.java new file mode 100644 index 000000000..f05858941 --- /dev/null +++ b/src/main/java/jenkins/test/https/SelfSignedCertificatesException.java @@ -0,0 +1,38 @@ +/* + * The MIT License + * + * Copyright 2024 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package jenkins.test.https; + +/** + * Raised when something went wrong when generating self-signed certificates. + */ +public class SelfSignedCertificatesException extends RuntimeException { + public SelfSignedCertificatesException(Throwable cause) { + super(cause); + } + + public SelfSignedCertificatesException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java index d6a485849..b7fbb9c06 100644 --- a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java +++ b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java @@ -26,6 +26,7 @@ import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.ExtensionList; import hudson.model.UnprotectedRootAction; @@ -61,9 +62,16 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.FileAttribute; +import java.security.KeyManagementException; import java.security.KeyStore; +import java.security.KeyStoreException; import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Enumeration; import java.util.HashMap; @@ -93,7 +101,9 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManagerFactory; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -112,6 +122,8 @@ import org.junit.rules.Timeout; import org.junit.runner.Description; import org.junit.runners.model.Statement; +import jenkins.test.https.KeyStoreManager; +import jenkins.test.https.SelfSignedCertificates; import org.jvnet.hudson.test.recipes.LocalData; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; @@ -218,11 +230,10 @@ public final class RealJenkinsRule implements TestRule { private static final Pattern SNAPSHOT_INDEX_JELLY = Pattern.compile("(file:/.+/target)/classes/index.jelly"); private final PrefixedOutputStream.Builder prefixedOutputStreamBuilder = PrefixedOutputStream.builder(); - @CheckForNull + private boolean https; + private KeyStoreManager keyStoreManager; private SSLSocketFactory sslSocketFactory; - private File keyStoreFile; - private String keyStorePassword; - private URL keyStoreUrl; + private X509Certificate rootCA; public RealJenkinsRule() { home = new AtomicReference<>(); @@ -319,8 +330,13 @@ public RealJenkinsRule withTimeout(int timeout) { * might accidentally be shared across otherwise distinct services. *

Calling this method does not change the fact that Jenkins will be configured to listen only on localhost for security reasons * (so others in the same network cannot access your system under test, especially if it lacks authentication). + *

+ * When using HTTPS, use @{link #https(String)} instead. */ public RealJenkinsRule withHost(String host) { + if (https) { + throw new IllegalStateException("Don't call this method when using HTTPS"); + } this.host = host; return this; } @@ -740,17 +756,87 @@ public URL getUrl() throws MalformedURLException { if (port == 0) { throw new IllegalStateException("This method must be called after calling #startJenkins."); } - return new URL(sslSocketFactory != null ? "https" : "http", host, port, "/jenkins/"); + return new URL(https ? "https" : "http", host, port, "/jenkins/"); + } + + /** + * Sets up HTTPS for the current instance, and disables plain HTTP. + * This generates a self-signed certificate for localhost. The corresponding root CA that needs to be trusted by HTTP client can be obtained using {@link #getRootCA()}. + * + * @return the current instance + * @see #createWebClient() + */ + public RealJenkinsRule https() { + return https("localhost"); } - public RealJenkinsRule https(File keyStoreFile, String keyStorePassword, SSLSocketFactory sslSocketFactory) throws MalformedURLException { - this.sslSocketFactory = sslSocketFactory; - this.keyStoreFile = keyStoreFile; - this.keyStoreUrl = keyStoreFile.toURI().toURL(); - this.keyStorePassword = keyStorePassword; + + /** + * Sets up HTTPS for the current instance, and disables plain HTTP. + * This generates a self-signed certificate for the given host name. The corresponding root CA that needs to be trusted by HTTP client can be obtained using {@link #getRootCA()}. + *

+ * You don't need to call {@link #withHost(String)} when calling this method. + * + * @param host the host name to use in the certificate + * @return the current instance + * @see #createWebClient() + * @see #withHost(String) + */ + public RealJenkinsRule https(@NonNull String host) { + this.host = host; + this.https = true; + try { + var keyStoreManager = new KeyStoreManager(createTempDirectory("keystore").resolve("keyStore.p12"), token); + var rootCAs = SelfSignedCertificates.createRootCAs(); + var userCert = SelfSignedCertificates.createUserCert(host, rootCAs.intermediate()); + keyStoreManager.setCertificateEntry(host, rootCAs.root().certificate()); + keyStoreManager.setKeyEntry(host, userCert.keyPair().getPrivate(), new Certificate[]{ + userCert.certificate(), + rootCAs.intermediate().certificate(), + rootCAs.root().certificate() + }); + keyStoreManager.save(); + this.rootCA = rootCAs.root().certificate(); + this.keyStoreManager = keyStoreManager; + this.sslSocketFactory = keyStoreManager.buildClientSSLContext().getSocketFactory(); + } catch (CertificateException | KeyStoreException | NoSuchAlgorithmException | KeyManagementException | + IOException e) { + throw new RuntimeException(e); + } return this; } + /** + * @return the current autogenerated root CA or null if {@link #https(String)} has not been called. + */ + @Nullable + public X509Certificate getRootCA() { + return rootCA; + } + + /** + * Builds a {@link SSLContext} trusting the current instance. + */ + @NonNull + public SSLContext buildSSLContext() throws NoSuchAlgorithmException { + if (rootCA != null) { + try { + var myTrustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + myTrustStore.load(null, null); + myTrustStore.setCertificateEntry(getName() != null ? getName() : UUID.randomUUID().toString(), rootCA); + var trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(myTrustStore); + var context = SSLContext.getInstance("TLS"); + context.init(null, trustManagerFactory.getTrustManagers(), null); + return context; + } catch (CertificateException | KeyManagementException | IOException | KeyStoreException e) { + throw new RuntimeException(e); + } + } else { + return SSLContext.getDefault(); + } + } + private URL endpoint(String method) throws MalformedURLException { return new URL(getUrl(), "RealJenkinsRule/" + method + "?token=" + token); } @@ -792,12 +878,12 @@ private static File findJenkinsWar() throws Exception { } /** - * Create a client configured to trust the self-signed certificate used by Jenkins. + * Create a client configured to trust the self-signed certificate used by this instance. */ public WebClient createWebClient() { var wc = new WebClient(); - if (keyStoreUrl != null && keyStorePassword != null) { - wc.getOptions().setSSLTrustStore(keyStoreUrl, keyStorePassword, KeyStore.getDefaultType()); + if (keyStoreManager != null) { + wc.getOptions().setSSLTrustStore(keyStoreManager.getURL(), keyStoreManager.getPassword(), keyStoreManager.getType()); } return wc; } @@ -855,8 +941,8 @@ public void startJenkins() throws Throwable { "--httpListenAddress=" + httpListenAddress, "--prefix=/jenkins")); argv.addAll(getPortOptions()); - if (keyStoreFile != null && keyStorePassword != null) { - argv.addAll(List.of("--httpsKeyStore=" + keyStoreFile.getAbsolutePath(), "--httpsKeyStorePassword=" + keyStorePassword)); + if (https) { + argv.addAll(List.of("--httpsKeyStore=" + keyStoreManager.getPath().toAbsolutePath(), "--httpsKeyStorePassword=" + token)); } argv.addAll(jenkinsOptions); Map env = new TreeMap<>(); @@ -927,7 +1013,7 @@ public void startJenkins() throws Throwable { private Collection getPortOptions() { // Initially port=0. On subsequent runs, this is set to the port allocated randomly on the first run. - if (sslSocketFactory != null) { + if (https) { return List.of("--httpPort=-1", "--httpsPort=" + port); } else { return List.of("--httpPort=" + port); From 7d23b1487f6b05f0c19d3e6dc04abcc5ac4334dc Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Thu, 10 Oct 2024 08:56:18 +0200 Subject: [PATCH 12/31] Add a basic https test --- .../hudson/test/RealJenkinsRuleHttpsTest.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/test/java/org/jvnet/hudson/test/RealJenkinsRuleHttpsTest.java diff --git a/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleHttpsTest.java b/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleHttpsTest.java new file mode 100644 index 000000000..c0d35ee26 --- /dev/null +++ b/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleHttpsTest.java @@ -0,0 +1,28 @@ +package org.jvnet.hudson.test; + +import java.io.IOException; +import java.util.logging.Logger; +import org.junit.Rule; +import org.junit.Test; + +public class RealJenkinsRuleHttpsTest { + private static final Logger LOGGER = Logger.getLogger(RealJenkinsRuleHttpsTest.class.getName()); + + @Rule + public final RealJenkinsRule rr = new RealJenkinsRule().https(); + + @Test + public void runningStepAndUsingHtmlUnit() throws Throwable { + rr.startJenkins(); + // We can run steps + rr.runRemotely(RealJenkinsRuleHttpsTest::log); + // replica1 directly + try (var wc = rr.createWebClient()) { + wc.getPage(rr.getUrl()); + } + } + + private static void log(JenkinsRule r) throws IOException { + LOGGER.info("Running on " + r.getURL().toExternalForm()); + } +} From 40bfc04d2d3f4e27ad4c46d7c7cb01fdda0acc5f Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Thu, 10 Oct 2024 08:56:53 +0200 Subject: [PATCH 13/31] Demove depmgmt --- pom.xml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 542ca1681..f8cedd94c 100644 --- a/pom.xml +++ b/pom.xml @@ -96,11 +96,6 @@ THE SOFTWARE. access-modifier-annotation 1.34 - - org.bouncycastle - bcpkix-jdk18on - 1.78.1 - @@ -220,6 +215,7 @@ THE SOFTWARE. org.bouncycastle bcpkix-jdk18on + 1.78.1 org.jenkins-ci From da7a566301ff997aac2cbc2d3e7c21fa487b2cce Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Thu, 10 Oct 2024 08:58:31 +0200 Subject: [PATCH 14/31] Apply suggestion from Jesse --- src/main/java/jenkins/test/https/SelfSignedCertificates.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/jenkins/test/https/SelfSignedCertificates.java b/src/main/java/jenkins/test/https/SelfSignedCertificates.java index 3999ca6f4..4505fc637 100644 --- a/src/main/java/jenkins/test/https/SelfSignedCertificates.java +++ b/src/main/java/jenkins/test/https/SelfSignedCertificates.java @@ -36,7 +36,6 @@ import java.time.LocalDate; import java.time.ZoneId; import java.util.Date; -import java.util.List; import org.bouncycastle.asn1.DERSequence; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x509.BasicConstraints; @@ -146,9 +145,7 @@ public static CertificateKeyPair createUserCert(String dnsName, CertificateKeyPa keyPair.getPublic()); builder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature)); builder.addExtension(Extension.basicConstraints, false, new BasicConstraints(false)); - var altNames = List.of(new GeneralName(GeneralName.dNSName, dnsName)); - var subjectAltNames = GeneralNames.getInstance(new DERSequence(altNames.toArray(new GeneralName[0]))); - builder.addExtension(Extension.subjectAlternativeName, false, subjectAltNames); + builder.addExtension(Extension.subjectAlternativeName, false, GeneralNames.getInstance(new DERSequence(new GeneralName(GeneralName.dNSName, dnsName)))); return new CertificateKeyPair( keyPair, new JcaX509CertificateConverter().getCertificate(builder.build(newContentSigner(issuer.keyPair())))); From 88c0932750e239357ed4eac83933be555d3e491b Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Thu, 10 Oct 2024 09:08:09 +0200 Subject: [PATCH 15/31] javadoc --- .../java/jenkins/test/https/SelfSignedCertificates.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/java/jenkins/test/https/SelfSignedCertificates.java b/src/main/java/jenkins/test/https/SelfSignedCertificates.java index 4505fc637..7481dce55 100644 --- a/src/main/java/jenkins/test/https/SelfSignedCertificates.java +++ b/src/main/java/jenkins/test/https/SelfSignedCertificates.java @@ -54,11 +54,10 @@ /** * A utility class to generate self-signed certificates. - *

    - *
  • A root CA - *
  • An intermediate CA signed by the root CA - *
- * Then you can generate user certificates for a given DNS name signed by the intermediate CA. + *

Use static method {@link #createRootCAs()} to generate a root CA. + *

Then you can generate user certificates for a given DNS name signed by the intermediate CA. + * @param root The root CA certificate and key pair. + * @param intermediate The intermediate CA certificate and key pair. * * @see #createRootCAs() * @see #createUserCert(String, CertificateKeyPair) From b6568c3a0a418910a8d254bf74fcf14db8601107 Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Thu, 10 Oct 2024 09:17:36 +0200 Subject: [PATCH 16/31] Align instance-identity with bcpkix --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f8cedd94c..253863e23 100644 --- a/pom.xml +++ b/pom.xml @@ -225,7 +225,7 @@ THE SOFTWARE. org.jenkins-ci.modules instance-identity - 3.1 + 201.vd2a_b_5a_468a_a_6 test From 3a51e3a9a0d4dbeef8fa403b8b161fb88f4f53ae Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Thu, 10 Oct 2024 09:37:14 +0200 Subject: [PATCH 17/31] Add an inbound agent test --- .../jenkins/test/https/KeyStoreManager.java | 8 ++++++++ .../jvnet/hudson/test/InboundAgentRule.java | 1 + .../jvnet/hudson/test/RealJenkinsRule.java | 4 ++++ .../hudson/test/RealJenkinsRuleHttpsTest.java | 20 ++++++++++++++++++- 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/main/java/jenkins/test/https/KeyStoreManager.java b/src/main/java/jenkins/test/https/KeyStoreManager.java index 364140860..7781053e3 100644 --- a/src/main/java/jenkins/test/https/KeyStoreManager.java +++ b/src/main/java/jenkins/test/https/KeyStoreManager.java @@ -40,6 +40,7 @@ import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.List; +import java.util.stream.Stream; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; @@ -213,6 +214,13 @@ public void setKeyEntry(String host, PrivateKey privateKey, Certificate[] certif keyStore.setKeyEntry(host, privateKey, password.toCharArray(), certificates); } + public String[] getTruststoreJavaOptions() { + return Stream.of( + "-Djavax.net.ssl.trustStore=" + getPath().toAbsolutePath(), + "-Djavax.net.ssl.trustStorePassword=" + getPassword() + ).toArray(String[]::new); + } + private static class MergedTrustManager implements X509TrustManager { private final X509TrustManager defaultTrustManager; private final List trustManagers; diff --git a/src/main/java/org/jvnet/hudson/test/InboundAgentRule.java b/src/main/java/org/jvnet/hudson/test/InboundAgentRule.java index 2fc7c1bd2..789dcb64d 100644 --- a/src/main/java/org/jvnet/hudson/test/InboundAgentRule.java +++ b/src/main/java/org/jvnet/hudson/test/InboundAgentRule.java @@ -333,6 +333,7 @@ public void start(@NonNull RealJenkinsRule r, Options options) throws Throwable stop(r, name); var args = r.runRemotely(InboundAgentRule::getAgentArguments, name); jars.add(args.agentJar); + options.javaOptions.addAll(List.of(r.getTruststoreJavaOptions())); start(args, options); r.runRemotely(InboundAgentRule::waitForAgentOnline, name, options.loggers); } diff --git a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java index b7fbb9c06..57d092b4f 100644 --- a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java +++ b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java @@ -704,6 +704,10 @@ public boolean isAlive() { return proc != null && proc.isAlive(); } + public String[] getTruststoreJavaOptions() { + return keyStoreManager != null ? keyStoreManager.getTruststoreJavaOptions() : new String[0]; + } + /** * One step to run. *

Since this thunk will be sent to a different JVM, it must be serializable. diff --git a/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleHttpsTest.java b/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleHttpsTest.java index c0d35ee26..921af5b30 100644 --- a/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleHttpsTest.java +++ b/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleHttpsTest.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.util.logging.Logger; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -11,9 +12,16 @@ public class RealJenkinsRuleHttpsTest { @Rule public final RealJenkinsRule rr = new RealJenkinsRule().https(); + @Rule + public InboundAgentRule iar = new InboundAgentRule(); + + @Before + public void setUp() throws Throwable { + rr.startJenkins(); + } + @Test public void runningStepAndUsingHtmlUnit() throws Throwable { - rr.startJenkins(); // We can run steps rr.runRemotely(RealJenkinsRuleHttpsTest::log); // replica1 directly @@ -22,6 +30,16 @@ public void runningStepAndUsingHtmlUnit() throws Throwable { } } + @Test + public void inboundAgent() throws Throwable { + var options = InboundAgentRule.Options + .newBuilder() + .name("remote") + .webSocket() + .color(PrefixedOutputStream.Color.YELLOW); + iar.createAgent(rr, options.build()); + } + private static void log(JenkinsRule r) throws IOException { LOGGER.info("Running on " + r.getURL().toExternalForm()); } From 4bfcba9c2e6a4860f83e25d21ace55b64940d80b Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Thu, 10 Oct 2024 09:54:38 +0200 Subject: [PATCH 18/31] Allow to override truststore --- .../jvnet/hudson/test/InboundAgentRule.java | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/jvnet/hudson/test/InboundAgentRule.java b/src/main/java/org/jvnet/hudson/test/InboundAgentRule.java index 789dcb64d..f51c21aca 100644 --- a/src/main/java/org/jvnet/hudson/test/InboundAgentRule.java +++ b/src/main/java/org/jvnet/hudson/test/InboundAgentRule.java @@ -109,6 +109,8 @@ public static final class Options implements Serializable { private final LinkedHashMap loggers = new LinkedHashMap<>(); private String label; private final PrefixedOutputStream.Builder prefixedOutputStreamBuilder = PrefixedOutputStream.builder(); + private String trustStorePath; + private String trustStorePassword; public String getName() { return name; @@ -138,6 +140,21 @@ public String getLabel() { return label; } + /** + * Compute java options requied to connect to the given RealJenkinsRule instance. + * @param r The instance to compute Java options for + */ + private void computeJavaOptions(RealJenkinsRule r) { + if (trustStorePath != null && trustStorePassword != null) { + javaOptions.addAll(List.of( + "-Djavax.net.ssl.trustStore=" + trustStorePath, + "-Djavax.net.ssl.trustStorePassword=" + trustStorePassword + )); + } else { + javaOptions.addAll(List.of(r.getTruststoreJavaOptions())); + } + } + /** * A builder of {@link Options}. * @@ -219,6 +236,18 @@ public Builder javaOptions(String... opts) { return this; } + /** + * Provide a custom truststore for the agent JVM. Can be useful when using a setup with a reverse proxy. + * @param path the path to the truststore + * @param password the password for the truststore + * @return this builder + */ + public Builder trustStore(String path, String password) { + options.trustStorePath = path; + options.trustStorePassword = password; + return this; + } + /** * Skip starting the agent. * @@ -333,7 +362,7 @@ public void start(@NonNull RealJenkinsRule r, Options options) throws Throwable stop(r, name); var args = r.runRemotely(InboundAgentRule::getAgentArguments, name); jars.add(args.agentJar); - options.javaOptions.addAll(List.of(r.getTruststoreJavaOptions())); + options.computeJavaOptions(r); start(args, options); r.runRemotely(InboundAgentRule::waitForAgentOnline, name, options.loggers); } From 91178657f4caa271169a624770b494e5d0ab7f4c Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Thu, 10 Oct 2024 10:28:48 +0200 Subject: [PATCH 19/31] Update src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java Co-authored-by: Jesse Glick --- src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java index 57d092b4f..534fd0174 100644 --- a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java +++ b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java @@ -882,7 +882,7 @@ private static File findJenkinsWar() throws Exception { } /** - * Create a client configured to trust the self-signed certificate used by this instance. + * Create a client configured to trust any self-signed certificate used by this instance. */ public WebClient createWebClient() { var wc = new WebClient(); From bc4ba6e41f41eaaf6683c9269285bd6324a379a4 Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Thu, 10 Oct 2024 10:47:23 +0200 Subject: [PATCH 20/31] Prepare for keystore password removal --- .../jenkins/test/https/KeyStoreManager.java | 44 ++++++++++++------- .../jvnet/hudson/test/RealJenkinsRule.java | 6 ++- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/main/java/jenkins/test/https/KeyStoreManager.java b/src/main/java/jenkins/test/https/KeyStoreManager.java index 7781053e3..d2ff247cf 100644 --- a/src/main/java/jenkins/test/https/KeyStoreManager.java +++ b/src/main/java/jenkins/test/https/KeyStoreManager.java @@ -24,6 +24,7 @@ package jenkins.test.https; +import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; import java.net.URL; @@ -38,9 +39,9 @@ import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.stream.Stream; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; @@ -56,19 +57,28 @@ public class KeyStoreManager { private final Path path; @NonNull private final URL url; - @NonNull - private final String password; + @CheckForNull + private final char[] password; @NonNull private final KeyStore keyStore; @NonNull private final String type; + /** + * Creates a new instance using the default keystore type. + * @param path path of the keystore file. If it exists, it will be loaded automatically. + */ + public KeyStoreManager(@NonNull Path path) + throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException { + this(path, null, KeyStore.getDefaultType()); + } + /** * Creates a new instance using the default keystore type. * @param path path of the keystore file. If it exists, it will be loaded automatically. * @param password password for the keystore file. */ - public KeyStoreManager(@NonNull Path path, @NonNull String password) + public KeyStoreManager(@NonNull Path path, @CheckForNull String password) throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException { this(path, password, KeyStore.getDefaultType()); } @@ -79,16 +89,16 @@ public KeyStoreManager(@NonNull Path path, @NonNull String password) * @param password password for the keystore file. * @param type type of the keystore file. */ - public KeyStoreManager(@NonNull Path path, @NonNull String password, @NonNull String type) + public KeyStoreManager(@NonNull Path path, @CheckForNull String password, @NonNull String type) throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException { this.path = path; this.url = path.toUri().toURL(); - this.password = password; + this.password = password == null ? null : password.toCharArray(); this.type = type; var tmpKeyStore = KeyStore.getInstance(type); if (Files.exists(path)) { try (var is = Files.newInputStream(path)) { - tmpKeyStore.load(is, password.toCharArray()); + tmpKeyStore.load(is, this.password); } } else { tmpKeyStore.load(null); @@ -109,7 +119,7 @@ private static X509TrustManager getDefaultX509CertificateTrustManager(TrustManag * @return the password for the managed keystore */ public String getPassword() { - return password; + return password == null ? null : new String(password); } /** @@ -141,7 +151,7 @@ public URL getURL() { */ public void save() throws IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException { try (var os = Files.newOutputStream(path)) { - keyStore.store(os, password.toCharArray()); + keyStore.store(os, password); } } @@ -155,7 +165,7 @@ public SSLContext buildClientSSLContext() X509TrustManager result; try (var myKeysInputStream = Files.newInputStream(path)) { var myTrustStore = KeyStore.getInstance(type); - myTrustStore.load(myKeysInputStream, password.toCharArray()); + myTrustStore.load(myKeysInputStream, password); var trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(myTrustStore); result = getDefaultX509CertificateTrustManager(trustManagerFactory); @@ -176,7 +186,7 @@ public SSLContext buildServerSSLContext() { final KeyManager[] keyManagers; try { var keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); - keyManagerFactory.init(keyStore, password.toCharArray()); + keyManagerFactory.init(keyStore, password); keyManagers = keyManagerFactory.getKeyManagers(); } catch (NoSuchAlgorithmException | UnrecoverableKeyException | KeyStoreException e) { throw new RuntimeException("Unable to initialise KeyManager[]", e); @@ -211,14 +221,16 @@ public void setCertificateEntry(String alias, X509Certificate certificate) throw * @see KeyStore#setKeyEntry(String, byte[], Certificate[]) */ public void setKeyEntry(String host, PrivateKey privateKey, Certificate[] certificates) throws KeyStoreException { - keyStore.setKeyEntry(host, privateKey, password.toCharArray(), certificates); + keyStore.setKeyEntry(host, privateKey, password, certificates); } public String[] getTruststoreJavaOptions() { - return Stream.of( - "-Djavax.net.ssl.trustStore=" + getPath().toAbsolutePath(), - "-Djavax.net.ssl.trustStorePassword=" + getPassword() - ).toArray(String[]::new); + var list = new ArrayList(); + list.add("-Djavax.net.ssl.trustStore=" + getPath().toAbsolutePath()); + if (password != null) { + list.add("-Djavax.net.ssl.trustStorePassword=" + new String(password)); + } + return list.toArray(new String[0]); } private static class MergedTrustManager implements X509TrustManager { diff --git a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java index 534fd0174..963ab1994 100644 --- a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java +++ b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java @@ -790,6 +790,7 @@ public RealJenkinsRule https(@NonNull String host) { this.host = host; this.https = true; try { + // TODO remove password once we have https://github.com/jenkinsci/winstone/pull/417 var keyStoreManager = new KeyStoreManager(createTempDirectory("keystore").resolve("keyStore.p12"), token); var rootCAs = SelfSignedCertificates.createRootCAs(); var userCert = SelfSignedCertificates.createUserCert(host, rootCAs.intermediate()); @@ -946,7 +947,10 @@ public void startJenkins() throws Throwable { "--prefix=/jenkins")); argv.addAll(getPortOptions()); if (https) { - argv.addAll(List.of("--httpsKeyStore=" + keyStoreManager.getPath().toAbsolutePath(), "--httpsKeyStorePassword=" + token)); + argv.add("--httpsKeyStore=" + keyStoreManager.getPath().toAbsolutePath()); + if (keyStoreManager.getPassword() != null) { + argv.add("--httpsKeyStorePassword=" + keyStoreManager.getPassword()); + } } argv.addAll(jenkinsOptions); Map env = new TreeMap<>(); From 58c5c7b17a0e9c7e3e2e11143f352c76baddeb39 Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Thu, 10 Oct 2024 14:06:31 +0200 Subject: [PATCH 21/31] Test with a static test cert and keystore instead of generating a new one each run --- pom.xml | 5 - .../test/https/SelfSignedCertificates.java | 163 ------------------ .../SelfSignedCertificatesException.java | 38 ---- .../jvnet/hudson/test/RealJenkinsRule.java | 47 +++-- .../keystore/generate_self_signed_cert.sh | 47 +++++ src/main/resources/https/test-cert.pem | 24 +++ src/main/resources/https/test-keystore.p12 | Bin 0 -> 2771 bytes src/src/main/resources/https/test-cert.pem | 24 +++ .../main/resources/https/test-keystore.p12 | Bin 0 -> 2787 bytes 9 files changed, 118 insertions(+), 230 deletions(-) delete mode 100644 src/main/java/jenkins/test/https/SelfSignedCertificates.java delete mode 100644 src/main/java/jenkins/test/https/SelfSignedCertificatesException.java create mode 100755 src/main/keystore/generate_self_signed_cert.sh create mode 100644 src/main/resources/https/test-cert.pem create mode 100644 src/main/resources/https/test-keystore.p12 create mode 100644 src/src/main/resources/https/test-cert.pem create mode 100644 src/src/main/resources/https/test-keystore.p12 diff --git a/pom.xml b/pom.xml index 253863e23..d57017f27 100644 --- a/pom.xml +++ b/pom.xml @@ -212,11 +212,6 @@ THE SOFTWARE. jmh-generator-annprocess ${jmh.version} - - org.bouncycastle - bcpkix-jdk18on - 1.78.1 - org.jenkins-ci test-annotations diff --git a/src/main/java/jenkins/test/https/SelfSignedCertificates.java b/src/main/java/jenkins/test/https/SelfSignedCertificates.java deleted file mode 100644 index 7481dce55..000000000 --- a/src/main/java/jenkins/test/https/SelfSignedCertificates.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * The MIT License - * - * Copyright 2024 CloudBees, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package jenkins.test.https; - -import edu.umd.cs.findbugs.annotations.NonNull; -import java.math.BigInteger; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.security.Security; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.time.LocalDate; -import java.time.ZoneId; -import java.util.Date; -import org.bouncycastle.asn1.DERSequence; -import org.bouncycastle.asn1.x500.X500Name; -import org.bouncycastle.asn1.x509.BasicConstraints; -import org.bouncycastle.asn1.x509.Extension; -import org.bouncycastle.asn1.x509.GeneralName; -import org.bouncycastle.asn1.x509.GeneralNames; -import org.bouncycastle.asn1.x509.KeyUsage; -import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; -import org.bouncycastle.cert.CertIOException; -import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; -import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.operator.ContentSigner; -import org.bouncycastle.operator.OperatorCreationException; -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; - -/** - * A utility class to generate self-signed certificates. - *

Use static method {@link #createRootCAs()} to generate a root CA. - *

Then you can generate user certificates for a given DNS name signed by the intermediate CA. - * @param root The root CA certificate and key pair. - * @param intermediate The intermediate CA certificate and key pair. - * - * @see #createRootCAs() - * @see #createUserCert(String, CertificateKeyPair) - */ -public record SelfSignedCertificates(CertificateKeyPair root, CertificateKeyPair intermediate) { - private static final SecureRandom RANDOM = new SecureRandom(); - private static final String CERTIFICATE_ALGORITHM = "RSA"; - private static final int CERTIFICATE_BITS = 2048; - private static final String ROOT_DN = "CN=Root"; - - static { - // adds the Bouncy castle provider to java security - Security.addProvider(new BouncyCastleProvider()); - } - - @NonNull - public static SelfSignedCertificates createRootCAs() { - try { - // create root CA self-signed. - var rootKeyPair = generateKeyPair(); - var rootBuilder = new JcaX509v3CertificateBuilder( - new X500Name(ROOT_DN), - generateSerialNumber(), - Date.from(LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant()), - Date.from(LocalDate.now() - .plusDays(4) - .atStartOfDay(ZoneId.systemDefault()) - .toInstant()), - new X500Name(ROOT_DN), - SubjectPublicKeyInfo.getInstance(rootKeyPair.getPublic().getEncoded())); - rootBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign)); - rootBuilder.addExtension(Extension.basicConstraints, false, new BasicConstraints(true)); - var root = new CertificateKeyPair(rootKeyPair, new JcaX509CertificateConverter().getCertificate(rootBuilder.build(newContentSigner(rootKeyPair)))); - - // create Intermediate CA cert signed by Root CA - var intermediateKeyPair = generateKeyPair(); - var intermediateBuilder = new JcaX509v3CertificateBuilder( - root.certificate(), - generateSerialNumber(), - Date.from(LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant()), - Date.from(LocalDate.now() - .plusDays(2) - .atStartOfDay(ZoneId.systemDefault()) - .toInstant()), - new X500Name("CN=Intermediate"), - intermediateKeyPair.getPublic()); - intermediateBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign)); - intermediateBuilder.addExtension(Extension.basicConstraints, false, new BasicConstraints(true)); - var intermediate = new CertificateKeyPair(intermediateKeyPair, new JcaX509CertificateConverter().getCertificate(intermediateBuilder.build(newContentSigner(root.keyPair())))); - return new SelfSignedCertificates(root, intermediate); - } catch (OperatorCreationException | CertificateException | CertIOException | NoSuchAlgorithmException e) { - throw new SelfSignedCertificatesException(e); - } - } - - private static ContentSigner newContentSigner(KeyPair keyPair) throws OperatorCreationException { - return new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC").build(keyPair.getPrivate()); - } - - @NonNull - static BigInteger generateSerialNumber() { - return BigInteger.valueOf(RANDOM.nextInt()); - } - - /** - * Generate a user certificate signed by the intermediate CA. - * @param dnsName The DNS name to use in the certificate. - * @param issuer The intermediate CA to sign the certificate. - * @return The user certificate and key pair. - */ - public static CertificateKeyPair createUserCert(String dnsName, CertificateKeyPair issuer) { - try { - var keyPair = generateKeyPair(); - // create end user cert signed by Intermediate CA - var builder = new JcaX509v3CertificateBuilder( - issuer.certificate(), - generateSerialNumber(), - Date.from(LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant()), - Date.from(LocalDate.now() - .plusDays(1) - .atStartOfDay(ZoneId.systemDefault()) - .toInstant()), - new X500Name("CN=endUserCert"), - keyPair.getPublic()); - builder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature)); - builder.addExtension(Extension.basicConstraints, false, new BasicConstraints(false)); - builder.addExtension(Extension.subjectAlternativeName, false, GeneralNames.getInstance(new DERSequence(new GeneralName(GeneralName.dNSName, dnsName)))); - return new CertificateKeyPair( - keyPair, - new JcaX509CertificateConverter().getCertificate(builder.build(newContentSigner(issuer.keyPair())))); - } catch (OperatorCreationException | CertificateException | CertIOException | NoSuchAlgorithmException e) { - throw new SelfSignedCertificatesException(e); - } - } - - private static KeyPair generateKeyPair() throws NoSuchAlgorithmException { - var keyPairGenerator = KeyPairGenerator.getInstance(CERTIFICATE_ALGORITHM); - keyPairGenerator.initialize(CERTIFICATE_BITS, RANDOM); - return keyPairGenerator.generateKeyPair(); - } - - public record CertificateKeyPair(KeyPair keyPair, X509Certificate certificate) {} -} diff --git a/src/main/java/jenkins/test/https/SelfSignedCertificatesException.java b/src/main/java/jenkins/test/https/SelfSignedCertificatesException.java deleted file mode 100644 index f05858941..000000000 --- a/src/main/java/jenkins/test/https/SelfSignedCertificatesException.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * The MIT License - * - * Copyright 2024 CloudBees, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package jenkins.test.https; - -/** - * Raised when something went wrong when generating self-signed certificates. - */ -public class SelfSignedCertificatesException extends RuntimeException { - public SelfSignedCertificatesException(Throwable cause) { - super(cause); - } - - public SelfSignedCertificatesException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java index 963ab1994..de0410b2e 100644 --- a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java +++ b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java @@ -67,11 +67,10 @@ import java.security.KeyStoreException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.security.cert.Certificate; import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Enumeration; import java.util.HashMap; @@ -112,8 +111,10 @@ import io.jenkins.test.fips.FIPSTestBundleProvider; import jenkins.model.Jenkins; import jenkins.model.JenkinsLocationConfiguration; +import jenkins.test.https.KeyStoreManager; import jenkins.util.Timer; import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; import org.htmlunit.WebClient; import org.junit.AssumptionViolatedException; import org.junit.rules.DisableOnDebug; @@ -122,8 +123,6 @@ import org.junit.rules.Timeout; import org.junit.runner.Description; import org.junit.runners.model.Statement; -import jenkins.test.https.KeyStoreManager; -import jenkins.test.https.SelfSignedCertificates; import org.jvnet.hudson.test.recipes.LocalData; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; @@ -765,54 +764,54 @@ public URL getUrl() throws MalformedURLException { /** * Sets up HTTPS for the current instance, and disables plain HTTP. - * This generates a self-signed certificate for localhost. The corresponding root CA that needs to be trusted by HTTP client can be obtained using {@link #getRootCA()}. + * This generates a self-signed certificate for localhost. The corresponding root CA that needs to be trusted by HTTP client can be obtained using {@link #getRootCA()}. * * @return the current instance * @see #createWebClient() */ public RealJenkinsRule https() { - return https("localhost"); + try { + var keyStorePath = tmp.allocate().toPath().resolve("test-keystore.p12"); + IOUtils.copy(getClass().getResource("/https/test-keystore.p12"), keyStorePath.toFile()); + var keyStoreManager = new KeyStoreManager(keyStorePath, "changeit"); + try (var is = getClass().getResourceAsStream("/https/test-cert.pem")) { + var cert = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(is); + https("localhost", keyStoreManager, cert); + } + } catch (CertificateException | KeyStoreException | NoSuchAlgorithmException | IOException e) { + throw new RuntimeException(e); + } + return this; } - /** * Sets up HTTPS for the current instance, and disables plain HTTP. - * This generates a self-signed certificate for the given host name. The corresponding root CA that needs to be trusted by HTTP client can be obtained using {@link #getRootCA()}. *

* You don't need to call {@link #withHost(String)} when calling this method. * * @param host the host name to use in the certificate + * @param keyStoreManager a key store manager containing the key and certificate to use for HTTPS. It needs to be valid for the given host + * @param rootCA the certificate that needs to be trusted by callers. * @return the current instance * @see #createWebClient() * @see #withHost(String) */ - public RealJenkinsRule https(@NonNull String host) { + public RealJenkinsRule https(@NonNull String host, @NonNull KeyStoreManager keyStoreManager, @NonNull X509Certificate rootCA) { this.host = host; this.https = true; + this.keyStoreManager = keyStoreManager; try { - // TODO remove password once we have https://github.com/jenkinsci/winstone/pull/417 - var keyStoreManager = new KeyStoreManager(createTempDirectory("keystore").resolve("keyStore.p12"), token); - var rootCAs = SelfSignedCertificates.createRootCAs(); - var userCert = SelfSignedCertificates.createUserCert(host, rootCAs.intermediate()); - keyStoreManager.setCertificateEntry(host, rootCAs.root().certificate()); - keyStoreManager.setKeyEntry(host, userCert.keyPair().getPrivate(), new Certificate[]{ - userCert.certificate(), - rootCAs.intermediate().certificate(), - rootCAs.root().certificate() - }); - keyStoreManager.save(); - this.rootCA = rootCAs.root().certificate(); - this.keyStoreManager = keyStoreManager; this.sslSocketFactory = keyStoreManager.buildClientSSLContext().getSocketFactory(); - } catch (CertificateException | KeyStoreException | NoSuchAlgorithmException | KeyManagementException | + } catch (NoSuchAlgorithmException | KeyManagementException | CertificateException | KeyStoreException | IOException e) { throw new RuntimeException(e); } + this.rootCA = rootCA; return this; } /** - * @return the current autogenerated root CA or null if {@link #https(String)} has not been called. + * @return the current autogenerated root CA or null if {@link #https()} has not been called. */ @Nullable public X509Certificate getRootCA() { diff --git a/src/main/keystore/generate_self_signed_cert.sh b/src/main/keystore/generate_self_signed_cert.sh new file mode 100755 index 000000000..9754a5e9b --- /dev/null +++ b/src/main/keystore/generate_self_signed_cert.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")/../.." + +tmpDir=$(mktemp -d) +trap 'rm -rf $tmpDir' EXIT + +cat > "$tmpDir/req.cnf" <7o&XvAMuO}6H!FXosJx-t39!ku;wUafwyrZDWq{y z!~W9-_F$`@w3eA6T&)5tI+k&&lKtwPeGNj$Xrgx_S#6Vps))COKAun5?b4i%4p}#` zU5lEw%jM#$OvY5awK*HY<8SDs#q{U(A?frpioPIGM68L8z# z$bF(K(0qX7P6k9rixj8sct?_pU-pQKo`1>l$Ui^2B&)@UZIP&+_gG_-fqWgA-xG8s zP{|d?|Nf{c?!Mc%+jGal3RlZzJGL1-!f+WJ1R*N=W0oV?mN{w&F`^M2%Xn9TWf%?C zEf*ZlTVV+ytzo&pdQ(q@fBjNcIhPa6&z~C1lewx+z4ytp3=-eMWHGFU4WWlA;2otH zhVI2>m!4p35%&Vf!!_XEu~-a`)*UI@k60g ziHWfEiSH*rMv2R*SH8$W&8bzPhNdmDdyVLbuW93Xgj^n8xck^T@KZn~o=*aPIZ-|> zy-!nLP;i`&%yYhBZz#!+)j~QE@Awg&bcKvXVL)x?kQ_@+;uXrn7^La6wBwJLt~qwO z)DT)nvogaq5k?Dxl7(!lwfB1-V)4w(aI@&j-R|ql?iHo14+GQJ8nk@uJOeqCNXCq< zU#;gTO*(x))^qyv%?^w1KT|jQR?fF-R-^)BD0!?5CFuM($UZ9udnHmHb2 z1c^oQ>_7AqQL+?m{KWsnWd*jBx9J1w6LrDeVF@(np(w;ql{s-gN(ewwCn8E?>WKN; zxekqvye<%jw>QH>3`{&?JBcmZ#Nu;a&|+po%=6KLFFsdvgHw9V>JoQ< z8g(kFh9S&7`kjHnPd*E08tW|^-XST!rWu`qn!*T$Fq$bUt zVpA`AUC;0E9pQy1fYX0->L2idV?YVu$TR!a+1;RH{67*>P=h%9&6oZgbi=dVP_^0R&$(_{{Hs?iCP4|H*JZXj#TqVktoHVG6^LAY8rK0r z$`~uGHpl*OKFgvjhXK1As!~8=H%|Vj@_hj*W{J}4YCo{){mxCwAV710bB;PWPARO8S`vpr7|H=4rCUcBjHpHDs8R$kU>nZ7a{KM9ojljH?9K^b}j6 z;e2vEbU(v2U0)u5qY9r+=L0hW{Szu+WRfJ>QhEwKxJ`|iL2W-8b&l%2<+bB=Ut+T6 zE4?D`)8RhQ~HdB|k7c1MmbLroZ044Sy>9i9fv(sSM4WD2WvuIv*s z=^528UbzokDC4S;k1*2WiQWhX-^N~xLPIxVRQLUm1Sa8E31Lh+Ji6I>mn zPaoMU_$xSi$$Gqa^!W=}tIe@mMY(2a!zoTd1*For+C#?J@^Q~U|C|@)iwg6TWu_yt z3sTCp{N{B@VuUCxR9rPqf$oJvEa1gFS3|AB-nA4}Js5W5NpMIZG^F2TL5L}!@#g8A zn^(4I5ZPt%$g@_dVI zUsgy-c0)E8p0|vP0q80}Xx-alB+9)eM87$;$ew5*sMTSUV-?>Wn~oY?gZz1zxoidY z-p%?@^hxV43xtC94A~9wWeZw>xxRnnI7SN9N$^!`|43mJTihzTUa{DvhU?Yy^;zvs z3#7y}B&D}3v>v{C?0Q3p^u0n?1xcM7YvW;KL_qk=6^8tpB4jl$QF#jo#qK+mT%6-F z9sssVFj=cd9(c1fPps2T?`?{PE8#<1k1tf;N)Rltx5C+qTLiT^K&*&Z|Iulx zdb32y%EejrB*W?F3s#C=lH z!3cZ+v(Hn*eU1)31FW<=8GYh(4y{&|2%r06wDlK|B5dO|4`$ZsJIqj<5~l^{-i*_m zt^-e8WVB1a)y;7J;L#8r+qd^8s1MRpk2)`x48hnwsMxw9%~!xJ@cTJmKmq~Khu z2r+Mu@^Rwj^_?;J2Jg}!qqs%ZS(6vxv5sZ)##u$?xg4uvMn7h&BME{hUTJz|@GqUW z(J?d)hAy&~b%SB4lbt~bNx1lL&_cKn+#pVTc01CSifYOhchDv9!X)C$(}{3>I06p) zJp%!>U>cCf!=1aI7hcG#M2}E6#n*aa+ literal 0 HcmV?d00001 diff --git a/src/src/main/resources/https/test-cert.pem b/src/src/main/resources/https/test-cert.pem new file mode 100644 index 000000000..abf4f0ff9 --- /dev/null +++ b/src/src/main/resources/https/test-cert.pem @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIID9TCCAt2gAwIBAgIUKgbdov3IOHFFX1ILK9nG6FE94oUwDQYJKoZIhvcNAQEL +BQAwgYoxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTERMA8GA1UEBwwITmV3IFlv +cmsxEDAOBgNVBAoMB0plbmtpbnMxDTALBgNVBAsMBFRlc3QxFzAVBgNVBAMMDlNl +bGYtU2lnbmVkIENBMSEwHwYJKoZIhvcNAQkBFhJub3JlcGx5QGplbmtpbnMuaW8w +IBcNMjQxMDEwMTIwNDQ0WhgPMjEyNDA5MTYxMjA0NDRaMIGKMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCTlkxETAPBgNVBAcMCE5ldyBZb3JrMRAwDgYDVQQKDAdKZW5r +aW5zMQ0wCwYDVQQLDARUZXN0MRcwFQYDVQQDDA5TZWxmLVNpZ25lZCBDQTEhMB8G +CSqGSIb3DQEJARYSbm9yZXBseUBqZW5raW5zLmlvMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA8nHUtTQRhvooEAkfUowZVMbXhNKPpTA2pAoR3Gq3wt2M +kDha7n43qfBDcGAT7MZ9RtSnyErrVGzRnvhU++VUwhaCZHoc0CZ7IYL9MXAdzUOS +CXzzEXpUXXJRYBHG6lXPBXWpKHe+ds561bGr3j8rgJGrA1Vqwp284FXDzSR4rphT +q6jjCYh1MQkmA7QZMItHsSL4BLekopH7IVmv6f7WCnjZMX1c8awuHcwWbf4k4ujM +uJdvTCr8SO/y8MuYuwk2esa4qGBSoStE1dX8hktjWJyLw+m+5fxofmywBdTWQ6ju +ci0u3bJC2mlYVxbh4KnuZojlb8BpXA9Ki7YiiSqruwIDAQABo08wTTALBgNVHQ8E +BAMCB4AwCQYDVR0TBAIwADAUBgNVHREEDTALgglsb2NhbGhvc3QwHQYDVR0OBBYE +FF20ENS6djGGyP6otWNk/C7dTGO/MA0GCSqGSIb3DQEBCwUAA4IBAQDSVh+fD/o6 +8rr4L7HupIqxod+jzqP9IOYgXPgEa3e9GRcgMPFkUMV9Nzfy9nU4dVO330axO8gc +041m2drTM+hajHOuCMlJCEkPCWnFqxo6Xd1SqdOjxLRudfcysiUi7vYph5MTPzgD +Bci3u/LT9LcKsAPb0Mz6qNEC/XPPyzE+9q+YCM9l2wiiNUkTAmVPW1hKxL/yrsrq ++DqUUMeSNtIZRaIRbSOPjq64wPtBUVZKoXy3MVPkwNL2hQvL0KUg6Nx2N2xM9eWK +c80BT7yK8U79KXf5HtZNkmd+SaaV70smYDb7H08VBIGjZjUil83rJIhrYEkdJ4xn +fqkPI9hkUIQK +-----END CERTIFICATE----- diff --git a/src/src/main/resources/https/test-keystore.p12 b/src/src/main/resources/https/test-keystore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..f58179327dc9edc02d7c58c0da1e9e9102903fcc GIT binary patch literal 2787 zcmai$X*3iH8^>qH%-9WM$-b1aFJs?{Y}vOYlzo{Z##l0z7?~*9NwP1|2t(PI5XO?S zTuLTcODJNz`!_Nmg~81AmJKseTQaUg zASlg_S;R{ahz5XUfYSW$Du@;WK=MIpUGc_1FDful0?eFhJ;jx@oa=5^Dl28_Hpmx) z1G^Tgb40J|G-NJm_hrY~zLA#^GA^=bC)~*$l`wC+=HWE1c`n*X>Lr?QV88rUc}nz> zo}zD=X8&DCkhGI9w7%n!BbJ~5_^zDX_#?HtnJ(&uMppm@>oM0U^d#f+8kC-a$|&gS z7&B3uHyLWmeGw|{8c*c!{E5nt3 zZZHLJ0%s%@;Ma60Fm!;OpXld0Xv3e$G4zfrX=OWG#K$dk=R83!!*82GwfEuDU4az- zung}}uuAGs!geVAjc<-2o#D%`C2i|CDSnw1dT}|T*<~&_vcoek-Kegh9oJsIrjhx| zE;Pr%?p`aELTX1(yphmwk9SJhoz$INDv(voG9tf8hO9z+lV#A$j7aRXO#S z)3>N}a$q92&hlI!SSK3*QrbsGX_ZJ`aGo#R4q8L%wwDyWJv9!tn_M2=d=W9jVn`C#-#}P=W*JwnDvdMt6XWEVxF`E>W$E9TjPEDnH=( zPzl~hsF)h(Auy|lQ*cQv4Ok|NXH%+N>o0=Mlza=|X5#e+GPXI|T1|^1Fn?v$1l_Rq zAJv$3jy;tKq3nBzsJ0dPoq~<5*2!SnX`L*f*hDDPbE6U&KzrUIp`bM;i_FL}|Ipv- z_K2B=Nxoj=REf4duccMtn;uxtk8Jz2qtI+V|Gk#EJKyXG)mDf@@Z$;_XVgMoJ?<3s zZbbC=E^*eXpq|GZ{Pwci?2t1Q?M8K3Zbft5y-3O_hKY$P!(GUy^Z|!ujPlm6zm)yI zlg`a-K5{C;i-r-XSmau+W3K1MQ``MKvH``+qM3qNW2V+6n;1mkxvJ7Dk?H+UHkt{mn&N|EPT8NThPt^qm5~6cKvajjzb-5)uqd;42PTz8cP3v zM>5a>L1(4f8C3c&@!i}h0nxV!ma*U2^25j{`u?Z*YK*yElo3zJ`h6z%t#NR7@R?`< zb4WyfRx18gdEnjf>zhi0`nL6m@WKg497!lZa7l$nOD00Ji*0SEX_nuBR=9EBL(bA0 z?o)GYzd&*#uNSPuWGiu1Dc1&-WrOURMDm^6wmx_~yy)}Uf&%Q6W+(ipHo#2lXTB~E z3vN<$&2XMfxsR!BON~nlqYYPj-DlK1Rxl#8(Avv85Zk`R&EMXi=R@tO{Yl`mJGqLa z>5qQouposb;SDT%ob?nC(T)v z#-EE^fm%JV4cMdro+KsQ_los^ zR5$TZ`#!phafC$i(W@{S@tf-!1$^!_pX)Mdi>H0jU09TSpPiGrNM{3aaP!&rM@d%U z8kukOc3?rtWPgTu9I<+h@j?2qr}kEJc^$j#k*+QjQ7 zGps7jYz^?5yzNCUC{q&zu$HYi$ynr{&l@h;mVbb`CtI9AVdGht-$^)!gWYy4-V>;3 zBX%2`j+Kq{782(wXEH6lj9&3Vn$Q2hJqkswn(&NCl+)cawz+j}u_`6yF}(?EkvJWF zVtI`7VjZ=98kWUF{j?W%*ZDYQDWP^-xX?e$*}_9;Wy?nO8pTSL%1c#`r&vEJCC3Fb zj~D#vYLTDw)uxwWyZC7Gvi0oj?754>W+|o4cE)=|s}IS+r1vnL2!f7KiwLhhAWTY% zJ1Hf8CnBQi$WuMS*~AH6t)PS5#n9`UcBiRgqDS99<*6T0vzJ;}y7VgLVivjr%QtlT zfK0iwii_hswwqJqWA3sL%9QS~X8IXaH8yDJCZd9E7>VR}Ld?oFqaF-aNCFgd1w;;1h$agyG3gM)1Hl zIs1$KGXca=$G|$$s+#RBPr;5F*7UXN_1lqVj)G{V6 zg$ z$gNJ!$XPVCQTb_j$H1)3bwjUsz#!87uj+poC{#p{H@FXMI2%8m66V z4@>N|`byoPlGf3^d!<9g;G_*Ad+|BaH=AY+GI7(hG5w_^)?bW4-)j}~N19B4%v!BlI~b7nHpWv+xM{$X zWIX3uSd-J?l9l^DFH#pNhh+To7J`5f09erc=0O{&cOiUT-5X)}z4$9hNzC{dSL01H Yxa#~NPLc;q<0~LyK3dCu?H|?t4ejdxXaE2J literal 0 HcmV?d00001 From 8aff08f37cad5327581aa63709caa664bcd31bef Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Thu, 10 Oct 2024 14:27:46 +0200 Subject: [PATCH 22/31] Update src/test/java/org/jvnet/hudson/test/RealJenkinsRuleHttpsTest.java Co-authored-by: Jesse Glick --- .../java/org/jvnet/hudson/test/RealJenkinsRuleHttpsTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleHttpsTest.java b/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleHttpsTest.java index 921af5b30..185659746 100644 --- a/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleHttpsTest.java +++ b/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleHttpsTest.java @@ -24,7 +24,7 @@ public void setUp() throws Throwable { public void runningStepAndUsingHtmlUnit() throws Throwable { // We can run steps rr.runRemotely(RealJenkinsRuleHttpsTest::log); - // replica1 directly + // web client trusts the cert try (var wc = rr.createWebClient()) { wc.getPage(rr.getUrl()); } From ce5aede1efee4ca5982ec278469a8da11af63e45 Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Thu, 10 Oct 2024 14:37:21 +0200 Subject: [PATCH 23/31] Set up delegation to KeyStoreManager to configure web client --- src/main/java/jenkins/test/https/KeyStoreManager.java | 5 +++++ src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/jenkins/test/https/KeyStoreManager.java b/src/main/java/jenkins/test/https/KeyStoreManager.java index d2ff247cf..10b63683e 100644 --- a/src/main/java/jenkins/test/https/KeyStoreManager.java +++ b/src/main/java/jenkins/test/https/KeyStoreManager.java @@ -48,6 +48,7 @@ import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; +import org.htmlunit.WebClient; /** * Allows to manage a java keystore file more easily than base JDK. @@ -233,6 +234,10 @@ public String[] getTruststoreJavaOptions() { return list.toArray(new String[0]); } + public void configureWebClient(WebClient wc) { + wc.getOptions().setSSLTrustStore(getURL(), getPassword(), getType()); + } + private static class MergedTrustManager implements X509TrustManager { private final X509TrustManager defaultTrustManager; private final List trustManagers; diff --git a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java index de0410b2e..c55c94b97 100644 --- a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java +++ b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java @@ -887,7 +887,7 @@ private static File findJenkinsWar() throws Exception { public WebClient createWebClient() { var wc = new WebClient(); if (keyStoreManager != null) { - wc.getOptions().setSSLTrustStore(keyStoreManager.getURL(), keyStoreManager.getPassword(), keyStoreManager.getType()); + keyStoreManager.configureWebClient(wc); } return wc; } From a7f74a63dc3ebf6f1e4694b0eea21659b777fbb0 Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Thu, 10 Oct 2024 14:52:10 +0200 Subject: [PATCH 24/31] Revert instance-identity version bump --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d57017f27..0134d06da 100644 --- a/pom.xml +++ b/pom.xml @@ -220,7 +220,7 @@ THE SOFTWARE. org.jenkins-ci.modules instance-identity - 201.vd2a_b_5a_468a_a_6 + 3.1 test From b5eeb4870bf6beb2cba83a70cce10b312dbe09b1 Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Thu, 10 Oct 2024 14:52:42 +0200 Subject: [PATCH 25/31] Apply suggestions from code review Co-authored-by: Jesse Glick --- src/main/java/jenkins/test/https/KeyStoreManager.java | 4 ++-- src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java | 2 +- src/main/keystore/generate_self_signed_cert.sh | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/jenkins/test/https/KeyStoreManager.java b/src/main/java/jenkins/test/https/KeyStoreManager.java index 10b63683e..a729946da 100644 --- a/src/main/java/jenkins/test/https/KeyStoreManager.java +++ b/src/main/java/jenkins/test/https/KeyStoreManager.java @@ -51,9 +51,9 @@ import org.htmlunit.WebClient; /** - * Allows to manage a java keystore file more easily than base JDK. + * Manages a Java keystore file. */ -public class KeyStoreManager { +public final class KeyStoreManager { @NonNull private final Path path; @NonNull diff --git a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java index c55c94b97..f066eb338 100644 --- a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java +++ b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java @@ -330,7 +330,7 @@ public RealJenkinsRule withTimeout(int timeout) { *

Calling this method does not change the fact that Jenkins will be configured to listen only on localhost for security reasons * (so others in the same network cannot access your system under test, especially if it lacks authentication). *

- * When using HTTPS, use @{link #https(String)} instead. + * When using HTTPS, use {@link #https(String)} instead. */ public RealJenkinsRule withHost(String host) { if (https) { diff --git a/src/main/keystore/generate_self_signed_cert.sh b/src/main/keystore/generate_self_signed_cert.sh index 9754a5e9b..ecf71e2ff 100755 --- a/src/main/keystore/generate_self_signed_cert.sh +++ b/src/main/keystore/generate_self_signed_cert.sh @@ -36,7 +36,6 @@ mkdir -p "$tmpDir/output" key="$tmpDir/output/key.pem" cert="$tmpDir/output/cert.pem" certP12="$tmpDir/output/cert.p12" -# Generate a self-signed cert valid for 100 years echo "Generate self-signed cert for localhost for 100 years" openssl req -newkey rsa:2048 -nodes -keyout "$key" -x509 -days 36500 -out "$cert" -config "$tmpDir/req.cnf" echo "Generate PKCS12 keystore" From b2f6ddbf4771be166bfc77df0b4b1aed9524d0e4 Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Thu, 10 Oct 2024 14:54:22 +0200 Subject: [PATCH 26/31] Add missing license header --- .../hudson/test/RealJenkinsRuleHttpsTest.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleHttpsTest.java b/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleHttpsTest.java index 185659746..1fe495936 100644 --- a/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleHttpsTest.java +++ b/src/test/java/org/jvnet/hudson/test/RealJenkinsRuleHttpsTest.java @@ -1,3 +1,27 @@ +/* + * The MIT License + * + * Copyright 2024 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + package org.jvnet.hudson.test; import java.io.IOException; From 56cdc95f3d23ca821c00e55eaba10544b7002a53 Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Thu, 10 Oct 2024 14:56:42 +0200 Subject: [PATCH 27/31] =?UTF-8?q?Adresssing=20my=20OCD=20=E2=9C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../keystore/generate_self_signed_cert.sh | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/main/keystore/generate_self_signed_cert.sh b/src/main/keystore/generate_self_signed_cert.sh index ecf71e2ff..9dd98e2c8 100755 --- a/src/main/keystore/generate_self_signed_cert.sh +++ b/src/main/keystore/generate_self_signed_cert.sh @@ -8,28 +8,28 @@ trap 'rm -rf $tmpDir' EXIT cat > "$tmpDir/req.cnf" < Date: Thu, 10 Oct 2024 14:58:09 +0200 Subject: [PATCH 28/31] GH formats tab as 4 spaces --- .../keystore/generate_self_signed_cert.sh | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/keystore/generate_self_signed_cert.sh b/src/main/keystore/generate_self_signed_cert.sh index 9dd98e2c8..e67b76150 100755 --- a/src/main/keystore/generate_self_signed_cert.sh +++ b/src/main/keystore/generate_self_signed_cert.sh @@ -8,28 +8,28 @@ trap 'rm -rf $tmpDir' EXIT cat > "$tmpDir/req.cnf" < Date: Thu, 10 Oct 2024 15:00:32 +0200 Subject: [PATCH 29/31] Spaces better --- .../keystore/generate_self_signed_cert.sh | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/main/keystore/generate_self_signed_cert.sh b/src/main/keystore/generate_self_signed_cert.sh index e67b76150..01f0f9dc2 100755 --- a/src/main/keystore/generate_self_signed_cert.sh +++ b/src/main/keystore/generate_self_signed_cert.sh @@ -8,28 +8,28 @@ trap 'rm -rf $tmpDir' EXIT cat > "$tmpDir/req.cnf" < Date: Thu, 10 Oct 2024 15:27:04 +0200 Subject: [PATCH 30/31] Fix javadoc --- src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java index f066eb338..7b1c33314 100644 --- a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java +++ b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java @@ -330,7 +330,7 @@ public RealJenkinsRule withTimeout(int timeout) { *

Calling this method does not change the fact that Jenkins will be configured to listen only on localhost for security reasons * (so others in the same network cannot access your system under test, especially if it lacks authentication). *

- * When using HTTPS, use {@link #https(String)} instead. + * When using HTTPS, use {@link #https(String,KeyStoreManager, X509Certificate) instead. */ public RealJenkinsRule withHost(String host) { if (https) { From 4208388ae1f299c3d52b84376b900e75a6db90b3 Mon Sep 17 00:00:00 2001 From: Vincent Latombe Date: Thu, 10 Oct 2024 16:16:49 +0200 Subject: [PATCH 31/31] Damn --- src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java index 7b1c33314..dca15ead9 100644 --- a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java +++ b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java @@ -330,7 +330,7 @@ public RealJenkinsRule withTimeout(int timeout) { *

Calling this method does not change the fact that Jenkins will be configured to listen only on localhost for security reasons * (so others in the same network cannot access your system under test, especially if it lacks authentication). *

- * When using HTTPS, use {@link #https(String,KeyStoreManager, X509Certificate) instead. + * When using HTTPS, use {@link #https(String,KeyStoreManager, X509Certificate)} instead. */ public RealJenkinsRule withHost(String host) { if (https) {