Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Offer a way to start the RealJenkinsRule instance with https only #858

Merged
merged 31 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
fc51008
Offer a way to start the RealJenkinsRule instance with https only
Vlatombe Oct 4, 2024
fb23dfb
Require a custom SSLContext when using https
Vlatombe Oct 7, 2024
6492730
Forgot one call
Vlatombe Oct 7, 2024
faf6540
Proper name
Vlatombe Oct 7, 2024
7481bcc
Remove https in favor for checking SSLContext nullity
Vlatombe Oct 8, 2024
2c2e321
Update src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java
Vlatombe Oct 8, 2024
a9929e7
Replace SSLContext by SSLSocketFactory
Vlatombe Oct 8, 2024
024f45c
createWebClient
Vlatombe Oct 8, 2024
eb274db
Missing decoration
Vlatombe Oct 8, 2024
3631d45
Fix compilation
Vlatombe Oct 8, 2024
43483ea
Have RealJenkinsRule generates a https certificate automatically
Vlatombe Oct 8, 2024
7d23b14
Add a basic https test
Vlatombe Oct 10, 2024
40bfc04
Demove depmgmt
Vlatombe Oct 10, 2024
da7a566
Apply suggestion from Jesse
Vlatombe Oct 10, 2024
88c0932
javadoc
Vlatombe Oct 10, 2024
b6568c3
Align instance-identity with bcpkix
Vlatombe Oct 10, 2024
3a51e3a
Add an inbound agent test
Vlatombe Oct 10, 2024
4bfcba9
Allow to override truststore
Vlatombe Oct 10, 2024
9117865
Update src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java
Vlatombe Oct 10, 2024
bc4ba6e
Prepare for keystore password removal
Vlatombe Oct 10, 2024
58c5c7b
Test with a static test cert and keystore instead of generating a new…
Vlatombe Oct 10, 2024
8aff08f
Update src/test/java/org/jvnet/hudson/test/RealJenkinsRuleHttpsTest.java
Vlatombe Oct 10, 2024
ce5aede
Set up delegation to KeyStoreManager to configure web client
Vlatombe Oct 10, 2024
a7f74a6
Revert instance-identity version bump
Vlatombe Oct 10, 2024
b5eeb48
Apply suggestions from code review
Vlatombe Oct 10, 2024
b2f6ddb
Add missing license header
Vlatombe Oct 10, 2024
56cdc95
Adresssing my OCD ✅
Vlatombe Oct 10, 2024
8bfc28e
GH formats tab as 4 spaces
Vlatombe Oct 10, 2024
222d622
Spaces better
Vlatombe Oct 10, 2024
e43d2df
Fix javadoc
Vlatombe Oct 10, 2024
4208388
Damn
Vlatombe Oct 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ THE SOFTWARE.
<dependency>
<groupId>org.jenkins-ci.modules</groupId>
<artifactId>instance-identity</artifactId>
<version>3.1</version>
<version>201.vd2a_b_5a_468a_a_6</version>
Vlatombe marked this conversation as resolved.
Show resolved Hide resolved
Vlatombe marked this conversation as resolved.
Show resolved Hide resolved
<scope>test</scope>
</dependency>
<dependency>
Expand Down
286 changes: 286 additions & 0 deletions src/main/java/jenkins/test/https/KeyStoreManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
/*
* 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.CheckForNull;
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.ArrayList;
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;
import org.htmlunit.WebClient;

/**
* Allows to manage a java keystore file more easily than base JDK.
Vlatombe marked this conversation as resolved.
Show resolved Hide resolved
*/
public class KeyStoreManager {
Vlatombe marked this conversation as resolved.
Show resolved Hide resolved
@NonNull
private final Path path;
@NonNull
private final URL url;
@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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be optional, since for this kind of test we do not care about keystore passwords?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for now; jenkinsci/winstone#417

*/
public KeyStoreManager(@NonNull Path path, @CheckForNull 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, @CheckForNull String password, @NonNull String type)
throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException {
this.path = path;
this.url = path.toUri().toURL();
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, this.password);
}
} 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 == null ? null : new String(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.
* <p>
* 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);
}
}

/**
* 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);
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);
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, certificates);
}

public String[] getTruststoreJavaOptions() {
var list = new ArrayList<String>();
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]);
}

public void configureWebClient(WebClient wc) {
wc.getOptions().setSSLTrustStore(getURL(), getPassword(), getType());
}

private static class MergedTrustManager implements X509TrustManager {
private final X509TrustManager defaultTrustManager;
private final List<X509TrustManager> 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);
}
}
}
30 changes: 30 additions & 0 deletions src/main/java/org/jvnet/hudson/test/InboundAgentRule.java
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ public static final class Options implements Serializable {
private final LinkedHashMap<String, Level> loggers = new LinkedHashMap<>();
private String label;
private final PrefixedOutputStream.Builder prefixedOutputStreamBuilder = PrefixedOutputStream.builder();
private String trustStorePath;
private String trustStorePassword;

public String getName() {
return name;
Expand Down Expand Up @@ -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}.
*
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -333,6 +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.computeJavaOptions(r);
start(args, options);
r.runRemotely(InboundAgentRule::waitForAgentOnline, name, options.loggers);
}
Expand Down
Loading