Skip to content

Commit

Permalink
Added the ability to trust untrusted certificates
Browse files Browse the repository at this point in the history
Added

- A "NaiveTrustManager", which allows to bypass certificate checks
  -> Enables us to accept any certificate
- TrustAllCertificates option for Jenkins Admins, adding new
  remote hosts to jenkins

Refactored
- Better error handling in RemoteJenkinsServer
  • Loading branch information
rezaizad authored and svenhickstein committed Aug 13, 2019
1 parent edc1287 commit 08ec3ec
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 20 deletions.
Expand Up @@ -3,17 +3,19 @@
import static org.apache.commons.lang.StringUtils.trimToEmpty;

import java.io.Serializable;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import javax.net.ssl.*;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.List;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;

import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2;
import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.Auth2.Auth2Descriptor;
import org.jenkinsci.plugins.ParameterizedRemoteTrigger.auth2.NoneAuth;
import org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils.NaiveTrustManager;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.DataBoundConstructor;
Expand Down Expand Up @@ -50,6 +52,7 @@ public class RemoteJenkinsServer extends AbstractDescribableImpl<RemoteJenkinsSe
@CheckForNull
private String displayName;
private boolean hasBuildTokenRootSupport;
private boolean trustAllCertificates;
@CheckForNull
private Auth2 auth2;
@CheckForNull
Expand Down Expand Up @@ -77,6 +80,10 @@ protected Object readResolve() {
return this;
}

@DataBoundSetter
public void setTrustAllCertificates(boolean trustAllCertificates) {
this.trustAllCertificates = trustAllCertificates;
}

@DataBoundSetter
public void setDisplayName(String displayName)
Expand Down Expand Up @@ -145,6 +152,8 @@ public DescriptorImpl getDescriptor() {
return (DescriptorImpl) super.getDescriptor();
}

public boolean getTrustAllCertificates() { return trustAllCertificates; }


@Extension
public static class DescriptorImpl extends Descriptor<RemoteJenkinsServer> {
Expand All @@ -153,6 +162,28 @@ public String getDisplayName() {
return "";
}

/**
* Sets the TrustManager to be a "NaiveTrustManager", allowing us to ignore untrusted certificates
* Will set the connection to null, if a key management error occurred.
*
* ATTENTION: THIS IS VERY DANGEROUS AND SHOULD ONLY BE USED IF YOU KNOW WHAT YOU DO!
* @param conn The HttpsURLConnection you want to modify.
* @param trustAllCertificates A boolean, gotten from the Remote Hosts description
*/
public void makeConnectionTrustAllCertificates(HttpsURLConnection conn, boolean trustAllCertificates)
throws NoSuchAlgorithmException, KeyManagementException {
if (trustAllCertificates) {
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(new KeyManager[0], new TrustManager[]{new NaiveTrustManager()}, new SecureRandom());
// SSLContext.setDefault(ctx);
conn.setSSLSocketFactory(ctx.getSocketFactory());

// Trust every hostname
HostnameVerifier allHostsValid = (hostname, session) -> true;
conn.setHostnameVerifier(allHostsValid);
}
}

/**
* Validates the given address to see that it's well-formed, and is reachable.
*
Expand All @@ -161,7 +192,7 @@ public String getDisplayName() {
* @return FormValidation object
*/
@Restricted(NoExternalUse.class)
public FormValidation doCheckAddress(@QueryParameter String address) {
public FormValidation doCheckAddress(@QueryParameter String address, @QueryParameter boolean trustAllCertificates) {

URL host = null;

Expand All @@ -180,11 +211,22 @@ public FormValidation doCheckAddress(@QueryParameter String address) {

// check that the host is reachable
try {
HttpURLConnection connection = (HttpURLConnection) host.openConnection();
connection.setConnectTimeout(5000);
connection.connect();
HttpsURLConnection conn = (HttpsURLConnection) host.openConnection();
try {
makeConnectionTrustAllCertificates(conn, trustAllCertificates);
} catch (NoSuchAlgorithmException | KeyManagementException e) {
return FormValidation.error(e, "A key management error occurred.");
}
conn.setConnectTimeout(5000);
conn.connect();

if (trustAllCertificates) {
return FormValidation.warning(
"Connection established! Accepting all certificates is potentially unsafe."
);
}
} catch (Exception e) {
return FormValidation.warning("Address looks good, but a connection could not be stablished.");
return FormValidation.warning("Address looks good, but a connection could not be established.");
}

return FormValidation.ok();
Expand Down
Expand Up @@ -12,12 +12,15 @@
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import javax.net.ssl.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.time.Instant;
Expand Down Expand Up @@ -186,7 +189,7 @@ public static String encodeValue(String dirtyValue) {
return cleanValue;
}

private static String readInputStream(HttpURLConnection connection) throws IOException {
private static String readInputStream(HttpsURLConnection connection) throws IOException {
BufferedReader rd = null;
try {

Expand Down Expand Up @@ -243,7 +246,7 @@ private static JenkinsCrumb getCrumb(BuildContext context, Auth2 overrideAuth, b
context.logger.println("reuse cached crumb: " + globalHost);
return jenkinsCrumb;
}
HttpURLConnection connection = getAuthorizedConnection(context, crumbProviderUrl, overrideAuth);
HttpsURLConnection connection = getAuthorizedConnection(context, crumbProviderUrl, overrideAuth);
int responseCode = connection.getResponseCode();
if (responseCode == 401) {
throw new UnauthorizedException(crumbProviderUrl);
Expand Down Expand Up @@ -277,7 +280,7 @@ private static JenkinsCrumb getCrumb(BuildContext context, Auth2 overrideAuth, b
* @param context
* @throws IOException
*/
private static void addCrumbToConnection(HttpURLConnection connection, BuildContext context, Auth2 overrideAuth,
private static void addCrumbToConnection(HttpsURLConnection connection, BuildContext context, Auth2 overrideAuth,
boolean isCacheEnabled) throws IOException {
String method = connection.getRequestMethod();
if (method != null && method.equalsIgnoreCase("POST")) {
Expand All @@ -288,7 +291,19 @@ private static void addCrumbToConnection(HttpURLConnection connection, BuildCont
}
}

private static HttpURLConnection getAuthorizedConnection(BuildContext context, URL url, Auth2 overrideAuth)
/**
* Returns an authorized HttpsURLConnection
* If the user wanted to trust all certificates, the TrustManager and HostVerifier of the connection
* will be set properly.
*
* ATTENTION: THIS IS VERY DANGEROUS AND SHOULD ONLY BE USED IF YOU KNOW WHAT YOU DO!
* @param context The build context
* @param url The url to the remote build
* @param overrideAuth
* @return An authorized connection with or without a NaiveTrustManager
* @throws IOException
*/
private static HttpsURLConnection getAuthorizedConnection(BuildContext context, URL url, Auth2 overrideAuth)
throws IOException {
URLConnection connection = context.effectiveRemoteServer.isUseProxy() ? ProxyConfiguration.open(url)
: url.openConnection();
Expand All @@ -302,8 +317,24 @@ private static HttpURLConnection getAuthorizedConnection(BuildContext context, U
// Set Authorization Header configured globally for remoteServer
serverAuth.setAuthorizationHeader(connection, context);
}
HttpsURLConnection conn = (HttpsURLConnection) connection;

return (HttpURLConnection) connection;
if (context.effectiveRemoteServer.getTrustAllCertificates()){
// Installing the naive manage
try {
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(new KeyManager[0], new TrustManager[]{new NaiveTrustManager()}, new SecureRandom());
// SSLContext.setDefault(ctx);
conn.setSSLSocketFactory(ctx.getSocketFactory());

// Trust every hostname
HostnameVerifier allHostsValid = (hostname, session) -> true;
conn.setHostnameVerifier(allHostsValid);
} catch (NoSuchAlgorithmException | KeyManagementException e) {
context.logger.println("Unable to trust all certificates.");
}
}
return conn;
}

private static String getUrlWithoutParameters(String url) {
Expand Down Expand Up @@ -387,7 +418,6 @@ public static String buildTriggerUrl(String jobNameOrUrl, String securityToken,
* of a failed connection, the method calls it self recursively and increments
* the number of attempts.
*
* @see sendHTTPCall
* @param urlString
* the URL that needs to be called.
* @param requestType
Expand Down Expand Up @@ -429,7 +459,7 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT
}

URL url = new URL(urlString);
HttpURLConnection conn = getAuthorizedConnection(context, url, overrideAuth);
HttpsURLConnection conn = getAuthorizedConnection(context, url, overrideAuth);

try {
conn.setDoInput(true);
Expand All @@ -440,7 +470,7 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT
// wait up to 5 seconds for the connection to be open
conn.setConnectTimeout(5000);
if (HTTP_POST.equalsIgnoreCase(requestType)) {
// use longer timeout during POST due to not performing retrys since POST is not idem-potent
// use longer timeout during POST due to not performing retries since POST is not idem-potent
conn.setReadTimeout(30000);
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
conn.setRequestProperty("Content-Length", String.valueOf(postDataBytes.length));
Expand Down Expand Up @@ -491,7 +521,7 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT
// non-parameterized jobs), it returns a string indicating the status.
// But in newer versions of Jenkins, it just returns an empty response.
// So we need to compensate and check for both.
if (responseCode >= 400 || JSONUtils.mayBeJSON(response) == false) {
if (responseCode >= 400 || !JSONUtils.mayBeJSON(response)) {
return new ConnectionResponse(responseHeader, response, responseCode);
} else {
responseObject = (JSONObject) JSONSerializer.toJSON(response);
Expand All @@ -509,12 +539,12 @@ private static ConnectionResponse sendHTTPCall(String urlString, String requestT
// If we have connectionRetryLimit set to > 0 then retry that many times.
if (numberOfAttempts <= retryLimit) {
context.logger.println(String.format(
"Connection to remote server failed %s, waiting for to retry - %s seconds until next attempt. URL: %s, parameters: %s",
"Connection to remote server failed %s, waiting to retry - %s seconds until next attempt. URL: %s, parameters: %s",
(responseCode == 0 ? "" : "[" + responseCode + "]"), pollInterval,
getUrlWithoutParameters(urlString), parmsString));

// Sleep for 'pollInterval' seconds.
// Sleep takes miliseconds so need to convert this.pollInterval to milisecopnds
// Sleep takes milliseconds so need to convert this.pollInterval to milliseconds
// (x 1000)
try {
// Could do with a better way of sleeping...
Expand Down
@@ -0,0 +1,18 @@
package org.jenkinsci.plugins.ParameterizedRemoteTrigger.utils;

import javax.net.ssl.*;
import java.security.cert.X509Certificate;

// Trust every server
public class NaiveTrustManager implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] arg0, String arg1) {}

@Override
public void checkServerTrusted(X509Certificate[] arg0, String arg1) {}

@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}
@@ -0,0 +1,21 @@
<div>
<div style="font-weight: bolder; text-decoration: underline">
Trust all certificates
</div>

<p>
It is possible to override/rewrite the 'Trust all certificate'-setting for each Job separately.
Setting this checkbox to 'true' will result in accepting all certificates for the given Job.
</p>

<div>
If your remote Jenkins host has a
<a href="https://en.wikipedia.org/wiki/Self-signed_certificate" target="_blank">
self-signed certificate
</a>
or its certificate is not trusted, you may want to enable this option.
It will accept untrusted certificates for the given host.
</div>

<p><strong>This is unsafe and should only be used for testing or if you trust the host.</strong></p>
</div>
Expand Up @@ -8,6 +8,9 @@
<f:checkbox />
</f:entry>

<f:entry title="Trust all certificates" field="trustAllCertificates">
<f:checkbox />
</f:entry>

<f:dropdownDescriptorSelector field="auth2" title="Authentication" descriptors="${descriptor.getAuth2Descriptors()}" default="${descriptor.getDefaultAuth2Descriptor()}"/>

Expand Down
@@ -0,0 +1,16 @@
<div>
<div style="font-weight: bolder; text-decoration: underline">
Trust all certificates
</div>

<div>
If your remote Jenkins host has a
<a href="https://en.wikipedia.org/wiki/Self-signed_certificate" target="_blank">
self-signed certificate
</a>
or its certificate is not trusted, you may want to enable this option.
It will accept untrusted certificates for the given host.
</div>

<p><strong>This is unsafe and should only be used for testing or if you trust the host.</strong></p>
</div>

0 comments on commit 08ec3ec

Please sign in to comment.