Skip to content

Commit

Permalink
Merge pull request #72 from jenkinsci/jenkins-32980
Browse files Browse the repository at this point in the history
[FIXED JENKINS-32980] Adds a new CLI option to specify additional X.509 certs
  • Loading branch information
oleg-nenashev committed Feb 18, 2016
2 parents 7a50b43 + 818e58b commit 93c42ab
Show file tree
Hide file tree
Showing 2 changed files with 234 additions and 0 deletions.
159 changes: 159 additions & 0 deletions src/main/java/hudson/remoting/Engine.java
Expand Up @@ -24,6 +24,28 @@
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.annotation.CheckForNull;
import javax.annotation.concurrent.NotThreadSafe;
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 All @@ -49,6 +71,7 @@
*
* @author Kohsuke Kawaguchi
*/
@NotThreadSafe // the fields in this class should not be modified by multiple threads concurrently
public class Engine extends Thread {
/**
* Thread pool that sets {@link #CURRENT}.
Expand Down Expand Up @@ -83,6 +106,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 +173,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 +214,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 +241,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 +466,102 @@ 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;
}

@CheckForNull
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 93c42ab

Please sign in to comment.