Skip to content

Commit

Permalink
[FIXED JENKINS-32980] Adds a new CLI option to specify additional X.5…
Browse files Browse the repository at this point in the history
…09 PEM encoded certificates to trust when performing JNLP port discovery on the supplied Jenkins URLs
  • Loading branch information
stephenc committed Feb 16, 2016
1 parent 08a19d8 commit efaea8e
Show file tree
Hide file tree
Showing 2 changed files with 230 additions and 0 deletions.
155 changes: 155 additions & 0 deletions src/main/java/hudson/remoting/Engine.java
Expand Up @@ -24,6 +24,26 @@
package hudson.remoting;

import hudson.remoting.Channel.Mode;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.security.AccessController;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
import org.jenkinsci.remoting.engine.JnlpProtocol;
import org.jenkinsci.remoting.engine.JnlpProtocolFactory;

Expand Down Expand Up @@ -83,6 +103,11 @@ public void run() {
* "http://foo.bar/jenkins/".
*/
private List<URL> candidateUrls;
/**
* The list of {@link X509Certificate} instances to trust when connecting to any of the {@link #candidateUrls}
* or {@code null} to use the JVM default trust store.
*/
private List<X509Certificate> candidateCertificates;

/**
* URL that points to Jenkins's tcp slave agent listener, like <tt>http://myhost/hudson/</tt>
Expand Down Expand Up @@ -145,6 +170,19 @@ public void setNoReconnect(boolean noReconnect) {
this.noReconnect = noReconnect;
}

public void setCandidateCertificates(List<X509Certificate> candidateCertificates) {
this.candidateCertificates = candidateCertificates == null
? null
: new ArrayList<X509Certificate>(candidateCertificates);
}

public void addCandidateCertificate(X509Certificate certificate) {
if (candidateCertificates == null) {
candidateCertificates = new ArrayList<X509Certificate>();
}
candidateCertificates.add(certificate);
}

public void addListener(EngineListener el) {
events.add(el);
}
Expand Down Expand Up @@ -173,6 +211,25 @@ public void run() {
Throwable firstError=null;
String host=null;
String port=null;
SSLSocketFactory sslSocketFactory = null;
if (candidateCertificates != null && !candidateCertificates.isEmpty()) {
KeyStore keyStore = getCacertsKeyStore();
// load the keystore
keyStore.load(null, null);
int i = 0;
for (X509Certificate c : candidateCertificates) {
keyStore.setCertificateEntry(String.format("alias-%d", i++), c);
}
// prepare the trust manager
TrustManagerFactory trustManagerFactory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
// prepare the SSL context
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, trustManagerFactory.getTrustManagers(), null);
// now we have our custom socket factory
sslSocketFactory = ctx.getSocketFactory();
}

for (URL url : candidateUrls) {
String s = url.toExternalForm();
Expand All @@ -181,6 +238,9 @@ public void run() {

// find out the TCP port
HttpURLConnection con = (HttpURLConnection)Util.openURLConnection(salURL);
if (con instanceof HttpsURLConnection && sslSocketFactory != null) {
((HttpsURLConnection) con).setSSLSocketFactory(sslSocketFactory);
}
if (credentials != null) {
// TODO /tcpSlaveAgentListener is unprotected so why do we need to pass any credentials?
String encoding = Base64.encode(credentials.getBytes("UTF-8"));
Expand Down Expand Up @@ -403,6 +463,101 @@ public static Engine current() {

private static final Logger LOGGER = Logger.getLogger(Engine.class.getName());

private static KeyStore getCacertsKeyStore()
throws PrivilegedActionException, KeyStoreException, NoSuchProviderException, CertificateException,
NoSuchAlgorithmException, IOException {
Map<String, String> properties = AccessController.doPrivileged(
new PrivilegedExceptionAction<Map<String, String>>() {
public Map<String, String> run() throws Exception {
Map<String, String> result = new HashMap<String, String>();
result.put("trustStore", System.getProperty("javax.net.ssl.trustStore"));
result.put("javaHome", System.getProperty("java.home"));
result.put("trustStoreType",
System.getProperty("javax.net.ssl.trustStoreType", KeyStore.getDefaultType()));
result.put("trustStoreProvider", System.getProperty("javax.net.ssl.trustStoreProvider", ""));
result.put("trustStorePasswd", System.getProperty("javax.net.ssl.trustStorePassword", ""));
return result;
}
});
KeyStore keystore = null;

FileInputStream trustStoreStream = null;
try {
String trustStore = properties.get("trustStore");
if (!"NONE".equals(trustStore)) {
File trustStoreFile;
if (trustStore != null) {
trustStoreFile = new File(trustStore);
trustStoreStream = getFileInputStream(trustStoreFile);
} else {
String javaHome = properties.get("javaHome");
trustStoreFile = new File(
javaHome + File.separator + "lib" + File.separator + "security" + File.separator
+ "jssecacerts");
if ((trustStoreStream = getFileInputStream(trustStoreFile)) == null) {
trustStoreFile = new File(
javaHome + File.separator + "lib" + File.separator + "security" + File.separator
+ "cacerts");
trustStoreStream = getFileInputStream(trustStoreFile);
}
}

if (trustStoreStream != null) {
trustStore = trustStoreFile.getPath();
} else {
trustStore = "No File Available, using empty keystore.";
}
}

String trustStoreType = properties.get("trustStoreType");
String trustStoreProvider = properties.get("trustStoreProvider");
LOGGER.log(Level.FINE, "trustStore is: {0}", trustStore);
LOGGER.log(Level.FINE, "trustStore type is: {0}", trustStoreType);
LOGGER.log(Level.FINE, "trustStore provider is: {0}", trustStoreProvider);

if (trustStoreType.length() != 0) {
LOGGER.log(Level.FINE, "init truststore");

if (trustStoreProvider.length() == 0) {
keystore = KeyStore.getInstance(trustStoreType);
} else {
keystore = KeyStore.getInstance(trustStoreType, trustStoreProvider);
}

char[] trustStorePasswdChars = null;
String trustStorePasswd = properties.get("trustStorePasswd");
if (trustStorePasswd.length() != 0) {
trustStorePasswdChars = trustStorePasswd.toCharArray();
}

keystore.load(trustStoreStream, trustStorePasswdChars);
if (trustStorePasswdChars != null) {
for (int i = 0; i < trustStorePasswdChars.length; ++i) {
trustStorePasswdChars[i] = 0;
}
}
}
} finally {
if (trustStoreStream != null) {
trustStoreStream.close();
}
}

return keystore;
}

private static FileInputStream getFileInputStream(final File file) throws PrivilegedActionException {
return AccessController.doPrivileged(new PrivilegedExceptionAction<FileInputStream>() {
public FileInputStream run() throws Exception {
try {
return file.exists() ? new FileInputStream(file) : null;
} catch (FileNotFoundException e) {
return null;
}
}
});
}

/**
* @deprecated Use {@link JnlpProtocol#GREETING_SUCCESS}.
*/
Expand Down
75 changes: 75 additions & 0 deletions src/main/java/hudson/remoting/jnlp/Main.java
Expand Up @@ -24,6 +24,13 @@
package hudson.remoting.jnlp;

import hudson.remoting.FileSystemJarCache;
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.UnsupportedEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import org.kohsuke.args4j.Option;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Argument;
Expand Down Expand Up @@ -77,6 +84,12 @@ public class Main {
usage="If the connection ends, don't retry and just exit.")
public boolean noReconnect = false;

@Option(name = "-cert",
usage = "Specify additional X.509 encoded PEM certificates to trust when connecting to Jenkins " +
"root URLs. If starting with @ then the remainder is assumed to be the name of the " +
"certificate file to read.")
public List<String> candidateCertificates;

/**
* @since 2.24
*/
Expand Down Expand Up @@ -159,6 +172,68 @@ public Engine createEngine() {
if(jarCache!=null)
engine.setJarCache(new FileSystemJarCache(jarCache,true));
engine.setNoReconnect(noReconnect);
if (candidateCertificates != null && !candidateCertificates.isEmpty()) {
CertificateFactory factory;
try {
factory = CertificateFactory.getInstance("X.509");
} catch (CertificateException e) {
throw new IllegalStateException("Java platform specification mandates support for X.509", e);
}
List<X509Certificate> certificates = new ArrayList<X509Certificate>(candidateCertificates.size());
for (String certOrAtFilename : candidateCertificates) {
certOrAtFilename = certOrAtFilename.trim();
byte[] cert;
if (certOrAtFilename.startsWith("@")) {
File file = new File(certOrAtFilename.substring(1));
long length;
if (file.isFile()
&& (length = file.length()) < 65536
&& length > "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----".length()) {
FileInputStream fis = null;
try {
// we do basic size validation, if there are x509 certificates that have a PEM encoding
// larger
// than 64kb we can revisit the upper bound.
cert = new byte[(int) length];
fis = new FileInputStream(file);
int read = fis.read(cert);
if (cert.length == read) {
LOGGER.log(Level.WARNING, "Only read {0} bytes from {1}, expected to read {2}",
new Object[]{read, file, cert.length});
// skip it
continue;
}
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Could not read certificate from " + file, e);
continue;
}
} else {
if (file.isFile()) {
LOGGER.log(Level.WARNING, "Could not read certificate from {0}. File size is not within " +
"the expected range for a PEM encoded X.509 certificate", file.getAbsolutePath());
} else {
LOGGER.log(Level.WARNING, "Could not read certificate from {0}. File not found",
file.getAbsolutePath());
}
continue;
}
} else {
try {
cert = certOrAtFilename.getBytes("US-ASCII");
} catch (UnsupportedEncodingException e) {
LOGGER.log(Level.WARNING, "Could not parse certificate " + certOrAtFilename, e);
continue;
}
}
try {
certificates.add((X509Certificate) factory.generateCertificate(new ByteArrayInputStream(cert)));
} catch (ClassCastException e) {
LOGGER.log(Level.WARNING, "Expected X.509 certificate from " + certOrAtFilename, e);
} catch (CertificateException e) {
LOGGER.log(Level.WARNING, "Could not parse X.509 certificate from " + certOrAtFilename, e);
}
}
}
return engine;
}

Expand Down

0 comments on commit efaea8e

Please sign in to comment.