-
-
Notifications
You must be signed in to change notification settings - Fork 134
Offer a way to start the RealJenkinsRule instance with https only #858
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
Merged
Merged
Changes from all 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 fb23dfb
Require a custom SSLContext when using https
Vlatombe 6492730
Forgot one call
Vlatombe faf6540
Proper name
Vlatombe 7481bcc
Remove https in favor for checking SSLContext nullity
Vlatombe 2c2e321
Update src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java
Vlatombe a9929e7
Replace SSLContext by SSLSocketFactory
Vlatombe 024f45c
createWebClient
Vlatombe eb274db
Missing decoration
Vlatombe 3631d45
Fix compilation
Vlatombe 43483ea
Have RealJenkinsRule generates a https certificate automatically
Vlatombe 7d23b14
Add a basic https test
Vlatombe 40bfc04
Demove depmgmt
Vlatombe da7a566
Apply suggestion from Jesse
Vlatombe 88c0932
javadoc
Vlatombe b6568c3
Align instance-identity with bcpkix
Vlatombe 3a51e3a
Add an inbound agent test
Vlatombe 4bfcba9
Allow to override truststore
Vlatombe 9117865
Update src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java
Vlatombe bc4ba6e
Prepare for keystore password removal
Vlatombe 58c5c7b
Test with a static test cert and keystore instead of generating a new…
Vlatombe 8aff08f
Update src/test/java/org/jvnet/hudson/test/RealJenkinsRuleHttpsTest.java
Vlatombe ce5aede
Set up delegation to KeyStoreManager to configure web client
Vlatombe a7f74a6
Revert instance-identity version bump
Vlatombe b5eeb48
Apply suggestions from code review
Vlatombe b2f6ddb
Add missing license header
Vlatombe 56cdc95
Adresssing my OCD ✅
Vlatombe 8bfc28e
GH formats tab as 4 spaces
Vlatombe 222d622
Spaces better
Vlatombe e43d2df
Fix javadoc
Vlatombe 4208388
Damn
Vlatombe File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
|
|
||
| /** | ||
| * Manages a Java keystore file. | ||
| */ | ||
| public final class KeyStoreManager { | ||
| @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. | ||
| */ | ||
| 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); | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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