James Moger
2012-12-05 6db4261a984da3d70cd3ac35869f19a75edc0ce8
Implemented hot-reloadable CRL
2 files added
1 files modified
259 ■■■■ changed files
src/com/gitblit/GitBlitServer.java 40 ●●●● patch | view | raw | blame | history
src/com/gitblit/GitblitSslContextFactory.java 94 ●●●●● patch | view | raw | blame | history
src/com/gitblit/GitblitTrustManager.java 125 ●●●●● patch | view | raw | blame | history
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;
    }
    
src/com/gitblit/GitblitSslContextFactory.java
New file
@@ -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;
    }
}
src/com/gitblit/GitblitTrustManager.java
New file
@@ -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) {
                }
            }
        }
    }
}