From 6db4261a984da3d70cd3ac35869f19a75edc0ce8 Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Wed, 05 Dec 2012 19:05:59 -0500
Subject: [PATCH] Implemented hot-reloadable CRL

---
 src/com/gitblit/GitblitSslContextFactory.java |   94 ++++++++++++++++++
 src/com/gitblit/GitBlitServer.java            |   40 +------
 src/com/gitblit/GitblitTrustManager.java      |  125 +++++++++++++++++++++++++
 3 files changed, 226 insertions(+), 33 deletions(-)

diff --git a/src/com/gitblit/GitBlitServer.java b/src/com/gitblit/GitBlitServer.java
index 5eaa4c9..4c0e89f 100644
--- a/src/com/gitblit/GitBlitServer.java
+++ b/src/com/gitblit/GitBlitServer.java
@@ -44,7 +44,6 @@
 import org.eclipse.jetty.server.ssl.SslConnector;
 import org.eclipse.jetty.server.ssl.SslSelectChannelConnector;
 import org.eclipse.jetty.server.ssl.SslSocketConnector;
-import org.eclipse.jetty.util.ssl.SslContextFactory;
 import org.eclipse.jetty.util.thread.QueuedThreadPool;
 import org.eclipse.jetty.webapp.WebAppContext;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
@@ -426,53 +425,28 @@
 	private static Connector createSSLConnector(String certAlias, File keyStore, File clientTrustStore,
 			String storePassword, File caRevocationList, boolean useNIO, int port, 
 			boolean requireClientCertificates) {
-		SslContextFactory sslContext = new SslContextFactory(SslContextFactory.DEFAULT_KEYSTORE_PATH);
+		GitblitSslContextFactory factory = new GitblitSslContextFactory(certAlias,
+				keyStore, clientTrustStore, storePassword, caRevocationList);
 		SslConnector connector;
 		if (useNIO) {
 			logger.info("Setting up NIO SslSelectChannelConnector on port " + port);
-			SslSelectChannelConnector ssl = new SslSelectChannelConnector(sslContext);
+			SslSelectChannelConnector ssl = new SslSelectChannelConnector(factory);
 			ssl.setSoLingerTime(-1);
 			if (requireClientCertificates) {
-				sslContext.setNeedClientAuth(true);
+				factory.setNeedClientAuth(true);
 			} else {
-				sslContext.setWantClientAuth(true);
+				factory.setWantClientAuth(true);
 			}
 			ssl.setThreadPool(new QueuedThreadPool(20));
 			connector = ssl;
 		} else {
 			logger.info("Setting up NIO SslSocketConnector on port " + port);
-			SslSocketConnector ssl = new SslSocketConnector(sslContext);
+			SslSocketConnector ssl = new SslSocketConnector(factory);
 			connector = ssl;
-		}
-		// disable renegotiation unless this is a patched JVM
-		boolean allowRenegotiation = false;
-		String v = System.getProperty("java.version");
-		if (v.startsWith("1.7")) {
-			allowRenegotiation = true;
-		} else if (v.startsWith("1.6")) {
-			// 1.6.0_22 was first release with RFC-5746 implemented fix.
-			if (v.indexOf('_') > -1) {
-				String b = v.substring(v.indexOf('_') + 1);
-				if (Integer.parseInt(b) >= 22) {
-					allowRenegotiation = true;
-				}
-			}
-		}
-		if (allowRenegotiation) {
-			logger.info("   allowing SSL renegotiation on Java " + v);
-			sslContext.setAllowRenegotiate(allowRenegotiation);
-		}
-		sslContext.setKeyStorePath(keyStore.getAbsolutePath());
-		sslContext.setKeyStorePassword(storePassword);
-		sslContext.setTrustStore(clientTrustStore.getAbsolutePath());
-		sslContext.setTrustStorePassword(storePassword);
-		sslContext.setCrlPath(caRevocationList.getAbsolutePath());
-		if (!StringUtils.isEmpty(certAlias)) {
-			logger.info("   certificate alias = " + certAlias);
-			sslContext.setCertAlias(certAlias);
 		}
 		connector.setPort(port);
 		connector.setMaxIdleTime(30000);
+
 		return connector;
 	}
 	
diff --git a/src/com/gitblit/GitblitSslContextFactory.java b/src/com/gitblit/GitblitSslContextFactory.java
new file mode 100644
index 0000000..f025c45
--- /dev/null
+++ b/src/com/gitblit/GitblitSslContextFactory.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.io.File;
+import java.security.KeyStore;
+import java.security.cert.CRL;
+import java.util.Collection;
+
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Special SSL context factory that configures Gitblit GO and replaces the
+ * primary trustmanager with a GitblitTrustManager.
+ *  
+ * @author James Moger
+ */
+public class GitblitSslContextFactory extends SslContextFactory {
+
+	private static final Logger logger = LoggerFactory.getLogger(GitblitSslContextFactory.class);
+
+	private final File caRevocationList;
+	
+	public GitblitSslContextFactory(String certAlias, File keyStore, File clientTrustStore,
+			String storePassword, File caRevocationList) {
+		super(keyStore.getAbsolutePath());
+		
+		this.caRevocationList = caRevocationList;
+
+		// disable renegotiation unless this is a patched JVM
+		boolean allowRenegotiation = false;
+		String v = System.getProperty("java.version");
+		if (v.startsWith("1.7")) {
+			allowRenegotiation = true;
+		} else if (v.startsWith("1.6")) {
+			// 1.6.0_22 was first release with RFC-5746 implemented fix.
+			if (v.indexOf('_') > -1) {
+				String b = v.substring(v.indexOf('_') + 1);
+				if (Integer.parseInt(b) >= 22) {
+					allowRenegotiation = true;
+				}
+			}
+		}
+		if (allowRenegotiation) {
+			logger.info("   allowing SSL renegotiation on Java " + v);
+			setAllowRenegotiate(allowRenegotiation);
+		}
+		
+		
+		if (!StringUtils.isEmpty(certAlias)) {
+			logger.info("   certificate alias = " + certAlias);
+			setCertAlias(certAlias);
+		}
+		setKeyStorePassword(storePassword);
+		setTrustStore(clientTrustStore.getAbsolutePath());
+		setTrustStorePassword(storePassword);
+		
+		logger.info("   keyStorePath   = " + keyStore.getAbsolutePath());
+		logger.info("   trustStorePath = " + clientTrustStore.getAbsolutePath());
+		logger.info("   crlPath        = " + caRevocationList.getAbsolutePath());
+	}
+
+	@Override
+	protected TrustManager[] getTrustManagers(KeyStore trustStore, Collection<? extends CRL> crls)
+			throws Exception {
+		TrustManager[] managers = super.getTrustManagers(trustStore, crls);
+		X509TrustManager delegate = (X509TrustManager) managers[0];
+		GitblitTrustManager root = new GitblitTrustManager(delegate, caRevocationList);
+
+		// replace first manager with the GitblitTrustManager
+		managers[0] = root;
+		return managers;
+	}
+}
diff --git a/src/com/gitblit/GitblitTrustManager.java b/src/com/gitblit/GitblitTrustManager.java
new file mode 100644
index 0000000..4127caf
--- /dev/null
+++ b/src/com/gitblit/GitblitTrustManager.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509CRL;
+import java.security.cert.X509CRLEntry;
+import java.security.cert.X509Certificate;
+import java.text.MessageFormat;
+import java.util.concurrent.atomic.AtomicLong;
+
+import javax.net.ssl.X509TrustManager;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * GitblitTrustManager is a wrapper trust manager that hot-reloads a local file 
+ * CRL and enforces client certificate revocations.  The GitblitTrustManager
+ * also implements fuzzy revocation enforcement in case of issuer mismatch BUT
+ * serial number match.  These rejecions are specially noted in the log.
+ *  
+ * @author James Moger
+ */
+public class GitblitTrustManager implements X509TrustManager {
+	
+	private static final Logger logger = LoggerFactory.getLogger(GitblitTrustManager.class);
+	
+	private final X509TrustManager delegate;
+	private final File caRevocationList;
+	
+	private final AtomicLong lastModified = new AtomicLong(0);
+	private volatile X509CRL crl;
+
+	public GitblitTrustManager(X509TrustManager delegate, File crlFile) {
+		this.delegate = delegate;
+		this.caRevocationList = crlFile;
+	}
+
+	@Override
+	public void checkClientTrusted(X509Certificate[] chain, String authType)
+			throws CertificateException {
+		X509Certificate cert = chain[0];
+		if (isRevoked(cert)) {
+			String message = MessageFormat.format("Rejecting revoked certificate {0,number,0} for {1}",
+					cert.getSerialNumber(), cert.getSubjectDN().getName());
+			logger.warn(message);
+			throw new CertificateException(message);
+		}
+		delegate.checkClientTrusted(chain, authType);
+	}
+
+	@Override
+	public void checkServerTrusted(X509Certificate[] chain, String authType)
+			throws CertificateException {
+		delegate.checkServerTrusted(chain, authType);
+	}
+
+	@Override
+	public X509Certificate[] getAcceptedIssuers() {
+		return delegate.getAcceptedIssuers();
+	}
+	
+	protected boolean isRevoked(X509Certificate cert) {
+		if (!caRevocationList.exists()) {
+			return false;
+		}
+		read();
+
+		if (crl.isRevoked(cert)) {
+			// exact cert is revoked
+			return true;
+		}
+		
+		X509CRLEntry entry = crl.getRevokedCertificate(cert.getSerialNumber());
+		if (entry != null) {
+			logger.warn("Certificate issuer does not match CRL issuer, but serial number has been revoked!");
+			logger.warn("   cert issuer = " + cert.getIssuerX500Principal());
+			logger.warn("   crl issuer  = " + crl.getIssuerX500Principal());
+			return true;
+		}
+		
+		return false;
+	}
+	
+	protected synchronized void read() {
+		if (lastModified.get() == caRevocationList.lastModified()) {
+			return;
+		}
+		logger.info("Reloading CRL from " + caRevocationList.getAbsolutePath());
+		InputStream inStream = null;
+		try {
+			inStream = new FileInputStream(caRevocationList);
+			CertificateFactory cf = CertificateFactory.getInstance("X.509");
+			X509CRL list = (X509CRL)cf.generateCRL(inStream);
+			crl = list;
+			lastModified.set(caRevocationList.lastModified());
+		} catch (Exception e) {
+		} finally {
+			if (inStream != null) {
+				try {
+					inStream.close();
+				} catch (Exception e) {
+				}
+			}
+		}
+	}
+}
\ No newline at end of file

--
Gitblit v1.9.1