James Moger
2012-11-25 c8b26c51aa67fc9345b624e36aab6f819e7eed74
Gitblit Certificate Authority revisions.  Eliminate certificate scripts.
5 files added
8 files deleted
11 files modified
1238 ■■■■ changed files
build.xml 19 ●●●● patch | view | raw | blame | history
distrib/makeclientcertificate.cmd 1 ●●●● patch | view | raw | blame | history
distrib/makeclientcertificate.sh 2 ●●●●● patch | view | raw | blame | history
distrib/makekeystore.cmd 6 ●●●●● patch | view | raw | blame | history
distrib/makekeystore.sh 5 ●●●●● patch | view | raw | blame | history
distrib/makekeystore_jdk.cmd 6 ●●●●● patch | view | raw | blame | history
distrib/makekeystore_jdk.sh 7 ●●●●● patch | view | raw | blame | history
resources/rosette_32x32.png patch | view | raw | blame | history
resources/settings_32x32.png patch | view | raw | blame | history
src/com/gitblit/GitBlitServer.java 28 ●●●●● patch | view | raw | blame | history
src/com/gitblit/MakeCertificate.java 130 ●●●●● patch | view | raw | blame | history
src/com/gitblit/authority/DefaultOidsPanel.java 80 ●●●●● patch | view | raw | blame | history
src/com/gitblit/authority/GitblitAuthority.java 185 ●●●●● patch | view | raw | blame | history
src/com/gitblit/authority/MakeClientCertificate.java 230 ●●●●● patch | view | raw | blame | history
src/com/gitblit/authority/NewCertificateConfig.java 33 ●●●● patch | view | raw | blame | history
src/com/gitblit/authority/NewClientCertificateDialog.java 14 ●●●●● patch | view | raw | blame | history
src/com/gitblit/authority/NewWebCertificateDialog.java 131 ●●●●● patch | view | raw | blame | history
src/com/gitblit/authority/UserCertificatePanel.java 88 ●●●● patch | view | raw | blame | history
src/com/gitblit/authority/UserOidsPanel.java 95 ●●●●● patch | view | raw | blame | history
src/com/gitblit/authority/Utils.java 22 ●●●●● patch | view | raw | blame | history
src/com/gitblit/authority/X509CertificateViewer.java 6 ●●●● patch | view | raw | blame | history
src/com/gitblit/utils/X509Utils.java 114 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/GitBlitWebApp.properties 7 ●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/X509UtilsTest.java 29 ●●●●● patch | view | raw | blame | history
build.xml
@@ -190,9 +190,9 @@
            <fileset dir="${basedir}">
                <include name="LICENSE" />
                <include name="NOTICE" />
                <include name="authority*.jar" />
            </fileset>
            </fileset>
        </copy>
        <copy tofile="${project.deploy.dir}/authority.jar" file="${basedir}/authority-${gb.version}.jar" />
        
        <!-- Certificate templates -->
        <mkdir dir="${project.deploy.dir}/certs"/>
@@ -461,7 +461,6 @@
                <exclude name="com/gitblit/AddIndexedBranch*.class" />
                <exclude name="com/gitblit/GitBlitServer*.class" />
                <exclude name="com/gitblit/Launcher*.class" />
                <exclude name="com/gitblit/MakeCertificate*.class" />
                <exclude name="com/gitblit/authority/**" />
            </fileset>
        </copy>
@@ -494,7 +493,6 @@
                <exclude name="com/gitblit/AddIndexedBranch*.class" />
                <exclude name="com/gitblit/GitBlitServer*.class" />
                <exclude name="com/gitblit/Launcher*.class" />
                <exclude name="com/gitblit/MakeCertificate*.class" />
            </fileset>
        </copy>
@@ -671,7 +669,6 @@
                <exclude name="com/gitblit/client/**" />
                <exclude name="com/gitblit/GitBlitServer*.class" />
                <exclude name="com/gitblit/Launcher*.class" />
                <exclude name="com/gitblit/MakeCertificate*.class" />
                <exclude name="com/gitblit/authority/**" />
            </fileset>
        </jar>
@@ -767,6 +764,7 @@
            <resource file="${basedir}/resources/rosette_16x16.png" />
            <resource file="${basedir}/resources/vcard_16x16.png" />
            <resource file="${basedir}/resources/settings_16x16.png" />
            <resource file="${basedir}/resources/settings_32x32.png" />
            <resource file="${basedir}/resources/search-icon.png" />
            <resource file="${basedir}/resources/blank.png" />
            <resource file="${basedir}/resources/bullet_green.png" />
@@ -775,6 +773,7 @@
            <resource file="${basedir}/resources/bullet_white.png" />
            <resource file="${basedir}/resources/bullet_delete.png" />
            <resource file="${basedir}/resources/bullet_key.png" />
            <resource file="${basedir}/src/log4j.properties" />
            <resource file="${basedir}/src/com/gitblit/wicket/GitBlitWebApp.properties" />
            <resource file="${basedir}/src/com/gitblit/wicket/GitBlitWebApp_es.properties" />
            <resource file="${basedir}/src/com/gitblit/wicket/GitBlitWebApp_ja.properties" />
@@ -1114,16 +1113,6 @@
            filename="${manager.zipfile}" 
            targetfilename="manager-${gb.version}.zip"
            summary="Gitblit Manager v${gb.version} (Swing tool to remotely administer a Gitblit server)"
            labels="Featured, Type-Package, OpSys-All" />
        <!-- Upload Gitblit Authority -->
        <gcupload
            username="${googlecode.user}"
            password="${googlecode.password}"
            projectname="gitblit"
            filename="${authority.zipfile}"
            targetfilename="authority-${gb.version}.zip"
            summary="Gitblit Authority v${gb.version} (Swing tool to manage client SSL certificates)"
            labels="Featured, Type-Package, OpSys-All" />
        <!-- Upload Gitblit API Library -->
distrib/makeclientcertificate.cmd
File was deleted
distrib/makeclientcertificate.sh
File was deleted
distrib/makekeystore.cmd
File was deleted
distrib/makekeystore.sh
File was deleted
distrib/makekeystore_jdk.cmd
File was deleted
distrib/makekeystore_jdk.sh
File was deleted
resources/rosette_32x32.png
resources/settings_32x32.png
src/com/gitblit/GitBlitServer.java
@@ -16,7 +16,9 @@
package com.gitblit;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
@@ -55,10 +57,12 @@
import com.beust.jcommander.Parameter;
import com.beust.jcommander.ParameterException;
import com.beust.jcommander.Parameters;
import com.gitblit.authority.GitblitAuthority;
import com.gitblit.authority.NewCertificateConfig;
import com.gitblit.utils.StringUtils;
import com.gitblit.utils.TimeUtils;
import com.gitblit.utils.X509Utils;
import com.gitblit.utils.X509Utils.X509Log;
import com.gitblit.utils.X509Utils.X509Metadata;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
@@ -194,7 +198,7 @@
        // conditionally configure the https connector
        if (params.securePort > 0) {
            File folder = new File(System.getProperty("user.dir"));
            final File folder = new File(System.getProperty("user.dir"));
            File certificatesConf = new File(folder, X509Utils.CA_CONFIG);
            File serverKeyStore = new File(folder, X509Utils.SERVER_KEY_STORE);
            File serverTrustStore = new File(folder, X509Utils.SERVER_TRUST_STORE);
@@ -215,7 +219,27 @@
            }
            
            metadata.notAfter = new Date(System.currentTimeMillis() + 10*TimeUtils.ONEYEAR);
            X509Utils.prepareX509Infrastructure(metadata, folder);
            X509Utils.prepareX509Infrastructure(metadata, folder, new X509Log() {
                @Override
                public void log(String message) {
                    BufferedWriter writer = null;
                    try {
                        writer = new BufferedWriter(new FileWriter(new File(folder, X509Utils.CERTS + File.separator + "log.txt"), true));
                        writer.write(MessageFormat.format("{0,date,yyyy-MM-dd HH:mm}: {1}", new Date(), message));
                        writer.newLine();
                        writer.flush();
                    } catch (Exception e) {
                        LoggerFactory.getLogger(GitblitAuthority.class).error("Failed to append log entry!", e);
                    } finally {
                        if (writer != null) {
                            try {
                                writer.close();
                            } catch (IOException e) {
                            }
                        }
                    }
                }
            });
            if (serverKeyStore.exists()) {                
                Connector secureConnector = createSSLConnector(serverKeyStore, serverTrustStore, params.storePassword,
src/com/gitblit/MakeCertificate.java
File was deleted
src/com/gitblit/authority/DefaultOidsPanel.java
New file
@@ -0,0 +1,80 @@
/*
 * 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.authority;
import java.awt.GridLayout;
import javax.swing.JPanel;
import javax.swing.JTextField;
import com.gitblit.client.Translation;
import com.gitblit.utils.X509Utils.X509Metadata;
public class DefaultOidsPanel extends JPanel {
    private static final long serialVersionUID = 1L;
    private JTextField organizationalUnit;
    private JTextField organization;
    private JTextField locality;
    private JTextField stateProvince;
    private JTextField countryCode;
    public DefaultOidsPanel(X509Metadata metadata) {
        super();
        organizationalUnit = new JTextField(metadata.getOID("OU", ""), 20);
        organization = new JTextField(metadata.getOID("O", ""), 20);
        locality = new JTextField(metadata.getOID("L", ""), 20);
        stateProvince = new JTextField(metadata.getOID("ST", ""), 20);
        countryCode = new JTextField(metadata.getOID("C", ""), 20);
        setLayout(new GridLayout(0, 1, Utils.MARGIN, Utils.MARGIN));
        add(Utils.newFieldPanel(Translation.get("gb.organizationalUnit") + " (OU)", organizationalUnit));
        add(Utils.newFieldPanel(Translation.get("gb.organization") + " (O)", organization));
        add(Utils.newFieldPanel(Translation.get("gb.locality") + " (L)", locality));
        add(Utils.newFieldPanel(Translation.get("gb.stateProvince") + " (ST)", stateProvince));
        add(Utils.newFieldPanel(Translation.get("gb.countryCode") + " (C)", countryCode));
    }
    public void update(X509Metadata metadata) {
        metadata.setOID("OU", organizationalUnit.getText());
        metadata.setOID("O", organization.getText());
        metadata.setOID("L", locality.getText());
        metadata.setOID("ST", stateProvince.getText());
        metadata.setOID("C", countryCode.getText());
    }
    public String getOrganizationalUnit() {
        return organizationalUnit.getText();
    }
    public String getOrganization() {
        return organization.getText();
    }
    public String getLocality() {
        return locality.getText();
    }
    public String getStateProvince() {
        return stateProvince.getText();
    }
    public String getCountryCode() {
        return countryCode.getText();
    }
}
src/com/gitblit/authority/GitblitAuthority.java
@@ -29,10 +29,13 @@
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.security.PrivateKey;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.text.MessageFormat;
@@ -51,6 +54,9 @@
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMultipart;
import javax.swing.ImageIcon;
import javax.swing.InputVerifier;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
@@ -60,6 +66,7 @@
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.RowFilter;
import javax.swing.SwingConstants;
import javax.swing.UIManager;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
@@ -69,6 +76,7 @@
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.util.FS;
import org.slf4j.LoggerFactory;
import com.gitblit.ConfigUserService;
import com.gitblit.Constants;
@@ -81,8 +89,10 @@
import com.gitblit.client.Translation;
import com.gitblit.models.UserModel;
import com.gitblit.utils.StringUtils;
import com.gitblit.utils.TimeUtils;
import com.gitblit.utils.X509Utils;
import com.gitblit.utils.X509Utils.RevocationReason;
import com.gitblit.utils.X509Utils.X509Log;
import com.gitblit.utils.X509Utils.X509Metadata;
/**
@@ -91,7 +101,7 @@
 * @author James Moger
 *
 */
public class GitblitAuthority extends JFrame {
public class GitblitAuthority extends JFrame implements X509Log {
    private static final long serialVersionUID = 1L;
    
@@ -112,6 +122,10 @@
    private int defaultDuration;
    
    private TableRowSorter<UserCertificateTableModel> defaultSorter;
    private MailExecutor mail;
    private JButton certificateDefaultsButton;
    public static void main(String... args) {
        EventQueue.invokeLater(new Runnable() {
@@ -170,7 +184,7 @@
        // try to restore saved window size
        if (StringUtils.isEmpty(sz)) {
            setSize(850, 500);
            setSize(900, 600);
        } else {
            String[] chunks = sz.split("x");
            int width = Integer.parseInt(chunks[0]);
@@ -218,6 +232,7 @@
            return null;
        }
        gitblitSettings = new FileSettings(file.getAbsolutePath());
        mail = new MailExecutor(gitblitSettings);
        caKeystorePassword = gitblitSettings.getString(Keys.server.storePassword, null);
        String us = gitblitSettings.getString(Keys.realm.userService, "users.conf");
        String ext = us.substring(us.lastIndexOf(".") + 1).toLowerCase();
@@ -243,7 +258,9 @@
    private void load(File folder) {
        this.folder = folder;
        this.userService = loadUsers(folder);
        if (userService != null) {
        if (userService == null) {
            JOptionPane.showMessageDialog(this, MessageFormat.format("Sorry, {0} doesn't look like a Gitblit GO installation.", folder));
        } else {
            // build empty certificate model for all users
            Map<String, UserCertificateModel> map = new HashMap<String, UserCertificateModel>();
            for (String user : userService.getAllUsernames()) {
@@ -273,7 +290,18 @@
            Collections.sort(tableModel.list);
            tableModel.fireTableDataChanged();
            Utils.packColumns(table, Utils.MARGIN);
            File caKeystore = new File(folder, X509Utils.CA_KEY_STORE);
            if (!caKeystore.exists()) {
                // show certificate defaults dialog
                certificateDefaultsButton.doClick();
            }
        }
    }
    private void prepareX509Infrastructure() {
        X509Metadata metadata = new X509Metadata("localhost", caKeystorePassword);
        X509Utils.prepareX509Infrastructure(metadata, folder, this);
    }
    
    private List<X509Certificate> findCerts(File folder, String username) {
@@ -310,6 +338,11 @@
            public Insets getInsets() {
                return Utils.INSETS;
            }
            @Override
            public boolean isAllowEmail() {
                return mail.isReady();
            }
            @Override
            public Date getDefaultExpiration() {
@@ -329,6 +362,7 @@
            
            @Override
            public void newCertificate(UserCertificateModel ucm, X509Metadata metadata, boolean sendEmail) {
                prepareX509Infrastructure();
                Date notAfter = metadata.notAfter;
                metadata.serverHostname = gitblitSettings.getString(Keys.web.siteName, "localhost");
                UserModel user = ucm.user;                
@@ -367,8 +401,8 @@
                }
                File caKeystoreFile = new File(folder, X509Utils.CA_KEY_STORE);
                File zip = X509Utils.newClientBundle(metadata, caKeystoreFile, caKeystorePassword);
                File zip = X509Utils.newClientBundle(metadata, caKeystoreFile, caKeystorePassword, GitblitAuthority.this);
                // save latest expiration date
                if (ucm.expires == null || metadata.notAfter.after(ucm.expires)) {
                    ucm.expires = metadata.notAfter;
@@ -389,7 +423,6 @@
                if (sendEmail) {
                    // send email
                    try {
                        MailExecutor mail = new MailExecutor(gitblitSettings);
                        if (mail.isReady()) {
                            Message message = mail.createMessage(user.emailAddress);
                            message.setSubject("Your Gitblit client certificate for " + metadata.serverHostname);
@@ -427,7 +460,7 @@
            public void revoke(UserCertificateModel ucm, X509Certificate cert, RevocationReason reason) {
                File caRevocationList = new File(folder, X509Utils.CA_REVOCATION_LIST);
                File caKeystoreFile = new File(folder, X509Utils.CA_KEY_STORE);
                if (X509Utils.revoke(cert, reason, caRevocationList, caKeystoreFile, caKeystorePassword)) {
                if (X509Utils.revoke(cert, reason, caRevocationList, caKeystoreFile, caKeystorePassword, GitblitAuthority.this)) {
                    File certificatesConfigFile = new File(folder, X509Utils.CA_CONFIG);
                    FileBasedConfig config = new FileBasedConfig(certificatesConfigFile, FS.detect());
                    if (certificatesConfigFile.exists()) {
@@ -451,6 +484,7 @@
                    int modelIndex = table.convertRowIndexToModel(table.getSelectedRow());
                    tableModel.fireTableDataChanged();
                    table.getSelectionModel().setSelectionInterval(modelIndex, modelIndex);
                }
            }
        };
@@ -491,7 +525,99 @@
        usersPanel.add(new JScrollPane(table), BorderLayout.CENTER);
        usersPanel.setMinimumSize(new Dimension(400, 10));
        
        final JTextField filterTextfield = new JTextField(20);
        certificateDefaultsButton = new JButton(new ImageIcon(getClass().getResource("/settings_16x16.png")));
        certificateDefaultsButton.setFocusable(false);
        certificateDefaultsButton.setToolTipText(Translation.get("gb.certificateDefaults"));
        certificateDefaultsButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                X509Metadata metadata = new X509Metadata("whocares", "whocares");
                File certificatesConfigFile = new File(folder, X509Utils.CA_CONFIG);
                FileBasedConfig config = new FileBasedConfig(certificatesConfigFile, FS.detect());
                NewCertificateConfig certificateConfig = null;
                if (certificatesConfigFile.exists()) {
                    try {
                        config.load();
                    } catch (Exception x) {
                        Utils.showException(GitblitAuthority.this, x);
                    }
                    certificateConfig = NewCertificateConfig.KEY.parse(config);
                    certificateConfig.update(metadata);
                }
                InputVerifier verifier = new InputVerifier() {
                    public boolean verify(JComponent comp) {
                        boolean returnValue;
                        JTextField textField = (JTextField) comp;
                        try {
                            Integer.parseInt(textField.getText());
                            returnValue = true;
                        } catch (NumberFormatException e) {
                            returnValue = false;
                        }
                        return returnValue;
                    }
                };
                JTextField durationTF = new JTextField(4);
                durationTF.setInputVerifier(verifier);
                durationTF.setVerifyInputWhenFocusTarget(true);
                durationTF.setText("" + certificateConfig.duration);
                JPanel durationPanel = Utils.newFieldPanel(Translation.get("gb.duration"), durationTF, Translation.get("gb.duration.days").replace("{0}",  "").trim());
                DefaultOidsPanel oids = new DefaultOidsPanel(metadata);
                JPanel panel = new JPanel(new BorderLayout());
                panel.add(durationPanel, BorderLayout.NORTH);
                panel.add(oids, BorderLayout.CENTER);
                int result = JOptionPane.showConfirmDialog(GitblitAuthority.this,
                        panel, Translation.get("gb.certificateDefaults"), JOptionPane.OK_CANCEL_OPTION,
                        JOptionPane.QUESTION_MESSAGE, new ImageIcon(getClass().getResource("/settings_32x32.png")));
                if (result == JOptionPane.OK_OPTION) {
                    try {
                        oids.update(metadata);
                        certificateConfig.duration = Integer.parseInt(durationTF.getText());
                        certificateConfig.store(config, metadata);
                        config.save();
                        prepareX509Infrastructure();
                    } catch (Exception e1) {
                        Utils.showException(GitblitAuthority.this, e1);
                    }
                }
            }
        });
        JButton newWebCertificate = new JButton(new ImageIcon(getClass().getResource("/rosette_16x16.png")));
        newWebCertificate.setFocusable(false);
        newWebCertificate.setToolTipText(Translation.get("gb.newWebCertificate"));
        newWebCertificate.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                Date defaultExpiration = new Date(System.currentTimeMillis() + 10*TimeUtils.ONEYEAR);
                NewWebCertificateDialog dialog = new NewWebCertificateDialog(GitblitAuthority.this, defaultExpiration);
                dialog.setModal(true);
                dialog.setVisible(true);
                if (dialog.isCanceled()) {
                    return;
                }
                prepareX509Infrastructure();
                Date expires = dialog.getExpiration();
                String hostname = dialog.getHostname();
                // read CA private key and certificate
                File caKeystoreFile = new File(folder, X509Utils.CA_KEY_STORE);
                PrivateKey caPrivateKey = X509Utils.getPrivateKey(X509Utils.CA_ALIAS, caKeystoreFile, caKeystorePassword);
                X509Certificate caCert = X509Utils.getCertificate(X509Utils.CA_ALIAS, caKeystoreFile, caKeystorePassword);
                // generate new SSL certificate
                X509Metadata metadata = new X509Metadata(hostname, caKeystorePassword);
                metadata.notAfter = expires;
                File serverKeystoreFile = new File(folder, X509Utils.SERVER_KEY_STORE);
                X509Utils.newSSLCertificate(metadata, caPrivateKey, caCert, serverKeystoreFile, GitblitAuthority.this);
            }
        });
        final JTextField filterTextfield = new JTextField(15);
        filterTextfield.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                filterUsers(filterTextfield.getText());
@@ -502,16 +628,32 @@
                filterUsers(filterTextfield.getText());
            }
        });
        JPanel buttonControls = new JPanel(new FlowLayout(FlowLayout.LEFT, Utils.MARGIN, Utils.MARGIN));
        buttonControls.add(certificateDefaultsButton);
        buttonControls.add(newWebCertificate);
        JPanel userControls = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 5));
        JPanel userControls = new JPanel(new FlowLayout(FlowLayout.RIGHT, Utils.MARGIN, Utils.MARGIN));
        userControls.add(new JLabel(Translation.get("gb.filter")));
        userControls.add(filterTextfield);
        
        JPanel topPanel = new JPanel(new BorderLayout(0, 0));
        topPanel.add(buttonControls, BorderLayout.WEST);
        topPanel.add(userControls, BorderLayout.EAST);
        JPanel leftPanel = new JPanel(new BorderLayout());
        leftPanel.add(userControls, BorderLayout.NORTH);
        leftPanel.add(topPanel, BorderLayout.NORTH);
        leftPanel.add(usersPanel, BorderLayout.CENTER);
        
        userCertificatePanel.setMinimumSize(new Dimension(375, 10));
        JLabel statusLabel = new JLabel();
        statusLabel.setHorizontalAlignment(SwingConstants.RIGHT);
        if (X509Utils.unlimitedStrength) {
            statusLabel.setText("JCE Unlimited Strength Jurisdiction Policy");
        } else {
            statusLabel.setText("JCE Standard Encryption Policy");
        }
        
        JPanel root = new JPanel(new BorderLayout()) {
            private static final long serialVersionUID = 1L;
@@ -521,7 +663,8 @@
        };
        JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, userCertificatePanel);
        splitPane.setDividerLocation(1d);
        root.add(splitPane);
        root.add(splitPane, BorderLayout.CENTER);
        root.add(statusLabel, BorderLayout.SOUTH);
        return root;
    }
    
@@ -545,4 +688,24 @@
        sorter.setRowFilter(containsFilter);
        table.setRowSorter(sorter);
    }
    @Override
    public void log(String message) {
        BufferedWriter writer = null;
        try {
            writer = new BufferedWriter(new FileWriter(new File(folder, X509Utils.CERTS + File.separator + "log.txt"), true));
            writer.write(MessageFormat.format("{0,date,yyyy-MM-dd HH:mm}: {1}", new Date(), message));
            writer.newLine();
            writer.flush();
        } catch (Exception e) {
            LoggerFactory.getLogger(GitblitAuthority.class).error("Failed to append log entry!", e);
        } finally {
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                }
            }
        }
    }
}
src/com/gitblit/authority/MakeClientCertificate.java
File was deleted
src/com/gitblit/authority/NewCertificateConfig.java
@@ -36,13 +36,13 @@
            }
        };
        public final String OU;
        public final String O;
        public final String L;
        public final String ST;
        public final String C;
        public String OU;
        public String O;
        public String L;
        public String ST;
        public String C;
        
        public final int duration;
        public int duration;
        
        private NewCertificateConfig(final Config c) {
            duration = c.getInt("new",  null, "duration", 0);
@@ -69,4 +69,25 @@
                metadata.oids.put(oid, value);
            }
        }
        public void store(Config c, X509Metadata metadata) {
            store(c, "new", "organizationalUnit", metadata.getOID("OU", null));
            store(c, "new", "organization", metadata.getOID("O", null));
            store(c, "new", "locality", metadata.getOID("L", null));
            store(c, "new", "stateProvince", metadata.getOID("ST", null));
            store(c, "new", "countryCode", metadata.getOID("C", null));
            if (duration <= 0) {
                c.unset("new", null, "duration");
            } else {
                c.setInt("new", null, "duration", duration);
            }
        }
        private void store(Config c, String section, String name, String value) {
            if (StringUtils.isEmpty(value)) {
                c.unset(section, null, name);
            } else {
                c.setString(section, null, name, value);
            }
        }
    }
src/com/gitblit/authority/NewClientCertificateDialog.java
@@ -50,12 +50,12 @@
    JCheckBox sendEmail;
    boolean isCanceled = true;
    public NewClientCertificateDialog(Frame owner, String displayname, Date defaultExpiration) {
    public NewClientCertificateDialog(Frame owner, String displayname, Date defaultExpiration, boolean allowEmail) {
        super(owner);
        
        setTitle(Translation.get("gb.newCertificate"));
        
        JPanel content = new JPanel(new BorderLayout(5, 5)) {
        JPanel content = new JPanel(new BorderLayout(Utils.MARGIN, Utils.MARGIN)) {
            private static final long serialVersionUID = 1L;
            @Override
@@ -72,7 +72,7 @@
        hint = new JTextField(20);
        sendEmail = new JCheckBox(Translation.get("gb.sendEmail"));
        
        JPanel panel = new JPanel(new GridLayout(0, 2, 5, 5));
        JPanel panel = new JPanel(new GridLayout(0, 2, Utils.MARGIN, Utils.MARGIN));
        
        panel.add(new JLabel(Translation.get("gb.expires")));
        panel.add(expirationDate);
@@ -86,8 +86,10 @@
        panel.add(new JLabel(Translation.get("gb.passwordHint")));
        panel.add(hint);
        
        panel.add(new JLabel(""));
        panel.add(sendEmail);
        if (allowEmail) {
            panel.add(new JLabel(""));
            panel.add(sendEmail);
        }
        content.add(panel, BorderLayout.CENTER);
        
@@ -123,7 +125,7 @@
    private boolean validateInputs() {
        if (getExpiration().getTime() < System.currentTimeMillis()) {
            // expires before now
            JOptionPane.showMessageDialog(this, Translation.get("gb.invalidExpiraitonDate"),
            JOptionPane.showMessageDialog(this, Translation.get("gb.invalidExpirationDate"),
                    Translation.get("gb.error"), JOptionPane.ERROR_MESSAGE);
            return false;
        }
src/com/gitblit/authority/NewWebCertificateDialog.java
New file
@@ -0,0 +1,131 @@
/*
 * 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.authority;
import java.awt.BorderLayout;
import java.awt.Frame;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Date;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
import com.gitblit.client.HeaderPanel;
import com.gitblit.client.Translation;
import com.gitblit.utils.StringUtils;
import com.toedter.calendar.JDateChooser;
public class NewWebCertificateDialog extends JDialog {
    private static final long serialVersionUID = 1L;
    JDateChooser expirationDate;
    JTextField hostname;
    boolean isCanceled = true;
    public NewWebCertificateDialog(Frame owner, Date defaultExpiration) {
        super(owner);
        setTitle(Translation.get("gb.newWebCertificate"));
        JPanel content = new JPanel(new BorderLayout(Utils.MARGIN, Utils.MARGIN)) {
            private static final long serialVersionUID = 1L;
            @Override
            public Insets getInsets() {
                return Utils.INSETS;
            }
        };
        content.add(new HeaderPanel(Translation.get("gb.newWebCertificate"), "rosette_16x16.png"), BorderLayout.NORTH);
        expirationDate = new JDateChooser(defaultExpiration);
        hostname = new JTextField(20);
        JPanel panel = new JPanel(new GridLayout(0, 2, Utils.MARGIN, Utils.MARGIN));
        panel.add(new JLabel(Translation.get("gb.hostname")));
        panel.add(hostname);
        panel.add(new JLabel(Translation.get("gb.expires")));
        panel.add(expirationDate);
        content.add(panel, BorderLayout.CENTER);
        JButton ok = new JButton(Translation.get("gb.ok"));
        ok.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (validateInputs()) {
                    isCanceled = false;
                    setVisible(false);
                }
            }
        });
        JButton cancel = new JButton(Translation.get("gb.cancel"));
        cancel.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                isCanceled = true;
                setVisible(false);
            }
        });
        JPanel controls = new JPanel();
        controls.add(ok);
        controls.add(cancel);
        content.add(controls, BorderLayout.SOUTH);
        getContentPane().add(content, BorderLayout.CENTER);
        pack();
        setLocationRelativeTo(owner);
    }
    private boolean validateInputs() {
        if (getExpiration().getTime() < System.currentTimeMillis()) {
            // expires before now
            JOptionPane.showMessageDialog(this, Translation.get("gb.invalidExpirationDate"),
                    Translation.get("gb.error"), JOptionPane.ERROR_MESSAGE);
            return false;
        }
        if (StringUtils.isEmpty(getHostname())) {
            // must have hostname
            JOptionPane.showMessageDialog(this, Translation.get("gb.hostnameRequired"),
                    Translation.get("gb.error"), JOptionPane.ERROR_MESSAGE);
            return false;
        }
        return true;
    }
    public String getHostname() {
        return hostname.getText();
    }
    public Date getExpiration() {
        return expirationDate.getDate();
    }
    public boolean isCanceled() {
        return isCanceled;
    }
}
src/com/gitblit/authority/UserCertificatePanel.java
@@ -16,12 +16,9 @@
package com.gitblit.authority;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Frame;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
@@ -31,12 +28,10 @@
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.TableRowSorter;
@@ -55,15 +50,8 @@
    
    private UserCertificateModel ucm;
    
    private JTextField displayname;
    private JTextField username;
    private JTextField emailAddress;
    private JTextField organizationalUnit;
    private JTextField organization;
    private JTextField locality;
    private JTextField stateProvince;
    private JTextField countryCode;
    private UserOidsPanel oidsPanel;
    private CertificatesTableModel tableModel;
    private JButton saveUserButton;
@@ -80,29 +68,10 @@
        super(new BorderLayout());
        
        this.owner = owner;
        oidsPanel = new UserOidsPanel();
        
        displayname = new JTextField(20);
        username = new JTextField(20);
        username.setEditable(false);
        emailAddress = new JTextField(20);
        organizationalUnit = new JTextField(20);
        organization = new JTextField(20);
        locality = new JTextField(20);
        stateProvince = new JTextField(20);
        countryCode = new JTextField(20);
        JPanel fields = new JPanel(new GridLayout(0, 1, 5, 5));
        fields.add(newFieldPanel(Translation.get("gb.displayName"), displayname));
        fields.add(newFieldPanel(Translation.get("gb.username") + " (CN)", username));
        fields.add(newFieldPanel(Translation.get("gb.emailAddress") + " (E)", emailAddress));
        fields.add(newFieldPanel(Translation.get("gb.organizationalUnit") + " (OU)", organizationalUnit));
        fields.add(newFieldPanel(Translation.get("gb.organization") + " (O)", organization));
        fields.add(newFieldPanel(Translation.get("gb.locality") + " (L)", locality));
        fields.add(newFieldPanel(Translation.get("gb.stateProvince") + " (ST)", stateProvince));
        fields.add(newFieldPanel(Translation.get("gb.countryCode") + " (C)", countryCode));
        JPanel fp = new JPanel(new BorderLayout(5, 5));
        fp.add(fields, BorderLayout.NORTH);
        JPanel fp = new JPanel(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
        fp.add(oidsPanel, BorderLayout.NORTH);
        
        JPanel fieldsPanel = new JPanel(new BorderLayout());
        fieldsPanel.add(new HeaderPanel(Translation.get("gb.properties"), "vcard_16x16.png"), BorderLayout.NORTH);
@@ -114,7 +83,7 @@
            public void actionPerformed(ActionEvent e) {
                setEditable(false);
                String username = ucm.user.username;
                updateUser();
                oidsPanel.updateUser(ucm);
                saveUser(username, ucm);
            }
        });
@@ -176,12 +145,12 @@
                        // save changes
                        String username = ucm.user.username;
                        setEditable(false);
                        updateUser();
                        oidsPanel.updateUser(ucm);
                        saveUser(username, ucm);
                    }
                    
                    NewClientCertificateDialog dialog = new NewClientCertificateDialog(UserCertificatePanel.this.owner,
                            ucm.user.getDisplayName(), getDefaultExpiration());
                            ucm.user.getDisplayName(), getDefaultExpiration(), isAllowEmail());
                    dialog.setModal(true);
                    dialog.setVisible(true);
                    if (dialog.isCanceled()) {
@@ -224,7 +193,7 @@
                    
                    Object choice = JOptionPane.showInputDialog(UserCertificatePanel.this.owner,
                            Translation.get("gb.revokeCertificateReason"), Translation.get("gb.revokeCertificate"),
                            JOptionPane.PLAIN_MESSAGE, new ImageIcon(getClass().getResource("/rosette_16x16.png")), choices, Translation.get("gb.unspecified"));
                            JOptionPane.PLAIN_MESSAGE, new ImageIcon(getClass().getResource("/rosette_32x32.png")), choices, Translation.get("gb.unspecified"));
                    if (choice == null) {
                        return;
                    }
@@ -273,26 +242,10 @@
        setEditable(false);
    }
    
    private JPanel newFieldPanel(String label, Component c) {
        JLabel jlabel = new JLabel(label);
        jlabel.setPreferredSize(new Dimension(175, 20));
        JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT));
        panel.add(jlabel);
        panel.add(c);
        return panel;
    }
    public void setUserCertificateModel(UserCertificateModel ucm) {
        this.ucm = ucm;
        setEditable(false);
        displayname.setText(ucm.user.getDisplayName());
        username.setText(ucm.user.username);
        emailAddress.setText(ucm.user.emailAddress);
        organizationalUnit.setText(ucm.user.organizationalUnit);
        organization.setText(ucm.user.organization);
        locality.setText(ucm.user.locality);
        stateProvince.setText(ucm.user.stateProvince);
        countryCode.setText(ucm.user.countryCode);
        oidsPanel.setUserCertificateModel(ucm);
        
        tableModel.setUserCertificateModel(ucm);
        tableModel.fireTableDataChanged();
@@ -300,14 +253,7 @@
    }
    
    public void setEditable(boolean editable) {
        displayname.setEditable(editable);
//        username.setEditable(editable);
        emailAddress.setEditable(editable);
        organizationalUnit.setEditable(editable);
        organization.setEditable(editable);
        locality.setEditable(editable);
        stateProvince.setEditable(editable);
        countryCode.setEditable(editable);
        oidsPanel.setEditable(editable);
        
        editUserButton.setEnabled(!editable && ucm != null);
        saveUserButton.setEnabled(editable && ucm != null);
@@ -316,18 +262,8 @@
        revokeCertificateButton.setEnabled(false);
    }
    
    private void updateUser() {
        ucm.user.displayName = displayname.getText();
        ucm.user.username = username.getText();
        ucm.user.emailAddress = emailAddress.getText();
        ucm.user.organizationalUnit = organizationalUnit.getText();
        ucm.user.organization = organization.getText();
        ucm.user.locality = locality.getText();
        ucm.user.stateProvince = stateProvince.getText();
        ucm.user.countryCode = countryCode.getText();
    }
    public abstract Date getDefaultExpiration();
    public abstract boolean isAllowEmail();
    
    public abstract void saveUser(String username, UserCertificateModel ucm);
    public abstract void newCertificate(UserCertificateModel ucm, X509Metadata metadata, boolean sendEmail);
src/com/gitblit/authority/UserOidsPanel.java
New file
@@ -0,0 +1,95 @@
/*
 * 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.authority;
import java.awt.GridLayout;
import javax.swing.JPanel;
import javax.swing.JTextField;
import com.gitblit.client.Translation;
public class UserOidsPanel extends JPanel {
    private static final long serialVersionUID = 1L;
    private JTextField displayname;
    private JTextField username;
    private JTextField emailAddress;
    private JTextField organizationalUnit;
    private JTextField organization;
    private JTextField locality;
    private JTextField stateProvince;
    private JTextField countryCode;
    public UserOidsPanel() {
        super();
        displayname = new JTextField(20);
        username = new JTextField(20);
        username.setEditable(false);
        emailAddress = new JTextField(20);
        organizationalUnit = new JTextField(20);
        organization = new JTextField(20);
        locality = new JTextField(20);
        stateProvince = new JTextField(20);
        countryCode = new JTextField(20);
        setLayout(new GridLayout(0, 1, Utils.MARGIN, Utils.MARGIN));
        add(Utils.newFieldPanel(Translation.get("gb.displayName"), displayname));
        add(Utils.newFieldPanel(Translation.get("gb.username") + " (CN)", username));
        add(Utils.newFieldPanel(Translation.get("gb.emailAddress") + " (E)", emailAddress));
        add(Utils.newFieldPanel(Translation.get("gb.organizationalUnit") + " (OU)", organizationalUnit));
        add(Utils.newFieldPanel(Translation.get("gb.organization") + " (O)", organization));
        add(Utils.newFieldPanel(Translation.get("gb.locality") + " (L)", locality));
        add(Utils.newFieldPanel(Translation.get("gb.stateProvince") + " (ST)", stateProvince));
        add(Utils.newFieldPanel(Translation.get("gb.countryCode") + " (C)", countryCode));
    }
    public void setUserCertificateModel(UserCertificateModel ucm) {
        setEditable(false);
        displayname.setText(ucm.user.getDisplayName());
        username.setText(ucm.user.username);
        emailAddress.setText(ucm.user.emailAddress);
        organizationalUnit.setText(ucm.user.organizationalUnit);
        organization.setText(ucm.user.organization);
        locality.setText(ucm.user.locality);
        stateProvince.setText(ucm.user.stateProvince);
        countryCode.setText(ucm.user.countryCode);
    }
    public void setEditable(boolean editable) {
        displayname.setEditable(editable);
//        username.setEditable(editable);
        emailAddress.setEditable(editable);
        organizationalUnit.setEditable(editable);
        organization.setEditable(editable);
        locality.setEditable(editable);
        stateProvince.setEditable(editable);
        countryCode.setEditable(editable);
    }
    protected void updateUser(UserCertificateModel ucm) {
        ucm.user.displayName = displayname.getText();
        ucm.user.username = username.getText();
        ucm.user.emailAddress = emailAddress.getText();
        ucm.user.organizationalUnit = organizationalUnit.getText();
        ucm.user.organization = organization.getText();
        ucm.user.locality = locality.getText();
        ucm.user.stateProvince = stateProvince.getText();
        ucm.user.countryCode = countryCode.getText();
    }
}
src/com/gitblit/authority/Utils.java
@@ -3,13 +3,16 @@
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.Insets;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Date;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextArea;
@@ -20,8 +23,11 @@
import com.gitblit.client.DateCellRenderer;
import com.gitblit.client.Translation;
import com.gitblit.utils.StringUtils;
public class Utils {
    public final static int LABEL_WIDTH = 175;
    public final static int MARGIN = 5;
@@ -44,6 +50,22 @@
        return table;
    }
    
    public static JPanel newFieldPanel(String label, Component c) {
        return newFieldPanel(label, c, null);
    }
    public static JPanel newFieldPanel(String label, Component c, String trailingLabel) {
        JLabel jlabel = new JLabel(label);
        jlabel.setPreferredSize(new Dimension(Utils.LABEL_WIDTH, 20));
        JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT));
        panel.add(jlabel);
        panel.add(c);
        if (!StringUtils.isEmpty(trailingLabel)) {
            panel.add(new JLabel(trailingLabel));
        }
        return panel;
    }
    public static void showException(Component c, Throwable t) {
        StringWriter writer = new StringWriter();
        t.printStackTrace(new PrintWriter(writer));
src/com/gitblit/authority/X509CertificateViewer.java
@@ -47,7 +47,7 @@
        
        setTitle(Translation.get("gb.viewCertificate"));
        
        JPanel content = new JPanel(new BorderLayout(5, 5)) {
        JPanel content = new JPanel(new BorderLayout(Utils.MARGIN, Utils.MARGIN)) {
            private static final long serialVersionUID = 1L;
            @Override
@@ -63,7 +63,7 @@
        int l1 = 15;
        int l2 = 25;
        int l3 = 45;
        JPanel panel = new JPanel(new GridLayout(0, 1, 0, 10));
        JPanel panel = new JPanel(new GridLayout(0, 1, 0, 2*Utils.MARGIN));
        panel.add(newField(Translation.get("gb.version"), "" + cert.getVersion(), 3));
        panel.add(newField(Translation.get("gb.subject"), cert.getSubjectDN().getName(), l3));
        panel.add(newField(Translation.get("gb.issuer"), cert.getIssuerDN().getName(), l3));
@@ -103,7 +103,7 @@
    }
    
    private JPanel newField(String label, String value, int cols) {
        JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0));
        JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 2*Utils.MARGIN, 0));
        JLabel lbl = new JLabel(label);
        lbl.setHorizontalAlignment(SwingConstants.RIGHT);
        lbl.setPreferredSize(new Dimension(125, 20));
src/com/gitblit/utils/X509Utils.java
@@ -15,7 +15,6 @@
 */
package com.gitblit.utils;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
@@ -108,7 +107,7 @@
    public static final String CA_CN = "Gitblit Certificate Authority";
    
    public static final String CA_FN = CA_CN;
    public static final String CA_ALIAS = CA_CN;
    private static final String BC = org.bouncycastle.jce.provider.BouncyCastleProvider.PROVIDER_NAME;
    
@@ -149,6 +148,10 @@
         public String toString() {
             return name() +  " (" + ordinal() + ")";
         }
    }
    public interface X509Log {
        void log(String message);
    }
    
    public static class X509Metadata {
@@ -211,6 +214,21 @@
            clone.userDisplayname = userDisplayname;
            return clone;
        }
        public String getOID(String oid, String defaultValue) {
            if (oids.containsKey(oid)) {
                return oids.get(oid);
            }
            return defaultValue;
        }
        public void setOID(String oid, String value) {
            if (StringUtils.isEmpty(value)) {
                oids.remove(oid);
            } else {
                oids.put(oid, value);
            }
        }
    }
    
    /**
@@ -218,9 +236,9 @@
     * 
     * @param metadata
     * @param folder
     * @param logger
     * @param x509log
     */
    public static void prepareX509Infrastructure(X509Metadata metadata, File folder) {
    public static void prepareX509Infrastructure(X509Metadata metadata, File folder, X509Log x509log) {
        // make the specified folder, if necessary
        folder.mkdirs();
        
@@ -228,7 +246,7 @@
        File caKeyStore = new File(folder, CA_KEY_STORE);            
        if (!caKeyStore.exists()) {
            logger.info(MessageFormat.format("Generating {0} ({1})", CA_CN, caKeyStore.getAbsolutePath()));
            X509Certificate caCert = newCertificateAuthority(metadata, caKeyStore);
            X509Certificate caCert = newCertificateAuthority(metadata, caKeyStore, x509log);
            saveCertificate(caCert, new File(caKeyStore.getParentFile(), "ca.cer"));
        }
@@ -236,7 +254,8 @@
        File caRevocationList = new File(folder, CA_REVOCATION_LIST);            
        if (!caRevocationList.exists()) {
            logger.info(MessageFormat.format("Generating {0} CRL ({1})", CA_CN, caRevocationList.getAbsolutePath()));
            newCertificateRevocationList(caRevocationList, caKeyStore, metadata.password);
            newCertificateRevocationList(caRevocationList, caKeyStore, metadata.password);
            x509log.log("new certificate revocation list created");
        }
        // rename the old keystore to the new name
@@ -250,17 +269,17 @@
        File serverKeyStore = new File(folder, SERVER_KEY_STORE);
        if (!serverKeyStore.exists()) {
            logger.info(MessageFormat.format("Generating SSL certificate for {0} signed by {1} ({2})", metadata.commonName, CA_CN, serverKeyStore.getAbsolutePath()));
            PrivateKey caPrivateKey = getPrivateKey(CA_FN, caKeyStore, metadata.password);
            X509Certificate caCert = getCertificate(CA_FN, caKeyStore, metadata.password);
            newSSLCertificate(metadata, caPrivateKey, caCert, serverKeyStore);
            PrivateKey caPrivateKey = getPrivateKey(CA_ALIAS, caKeyStore, metadata.password);
            X509Certificate caCert = getCertificate(CA_ALIAS, caKeyStore, metadata.password);
            newSSLCertificate(metadata, caPrivateKey, caCert, serverKeyStore, x509log);
        }
        // server certificate trust store holds trusted public certificates
        File serverTrustStore = new File(folder, X509Utils.SERVER_TRUST_STORE);
        if (!serverTrustStore.exists()) {
            logger.info(MessageFormat.format("Importing {0} into trust store ({1})", CA_FN, serverTrustStore.getAbsolutePath()));
            X509Certificate caCert = getCertificate(CA_FN, caKeyStore, metadata.password);
            addTrustedCertificate(CA_FN, caCert, serverTrustStore, metadata.password);
            logger.info(MessageFormat.format("Importing {0} into trust store ({1})", CA_ALIAS, serverTrustStore.getAbsolutePath()));
            X509Certificate caCert = getCertificate(CA_ALIAS, caKeyStore, metadata.password);
            addTrustedCertificate(CA_ALIAS, caCert, serverTrustStore, metadata.password);
        }
    }
    
@@ -506,8 +525,9 @@
     * @param caPrivateKey
     * @param caCert
     * @param targetStoreFile
     * @param x509log
     */
    public static X509Certificate newSSLCertificate(X509Metadata sslMetadata, PrivateKey caPrivateKey, X509Certificate caCert, File targetStoreFile) {
    public static X509Certificate newSSLCertificate(X509Metadata sslMetadata, PrivateKey caPrivateKey, X509Certificate caCert, File targetStoreFile, X509Log x509log) {
        try {
            KeyPair pair = newKeyPair();
@@ -541,8 +561,7 @@
                    new Certificate[] { cert, caCert });
            saveKeyStore(targetStoreFile, serverStore, sslMetadata.password);
            
            log(targetStoreFile.getParentFile(), MessageFormat.format("New web certificate {0,number,0} [{1}]", cert.getSerialNumber(), webDN.toString()));
            x509log.log(MessageFormat.format("New web certificate {0,number,0} [{1}]", cert.getSerialNumber(), cert.getSubjectDN().getName()));
            return cert;
        } catch (Throwable t) {
            throw new RuntimeException("Failed to generate SSL certificate!", t);
@@ -556,9 +575,10 @@
     * @param metadata
     * @param storeFile
     * @param keystorePassword
     * @param x509log
     * @return
     */
    public static X509Certificate newCertificateAuthority(X509Metadata metadata, File storeFile) {
    public static X509Certificate newCertificateAuthority(X509Metadata metadata, File storeFile, X509Log x509log) {
        try {
            KeyPair caPair = newKeyPair();
            
@@ -597,11 +617,12 @@
            
            // Save private key and certificate to new keystore
            KeyStore store = openKeyStore(storeFile, caMetadata.password);
            store.setKeyEntry(CA_FN, caPair.getPrivate(), caMetadata.password.toCharArray(),
            store.setKeyEntry(CA_ALIAS, caPair.getPrivate(), caMetadata.password.toCharArray(),
                    new Certificate[] { cert });
            saveKeyStore(storeFile, store, caMetadata.password);
            
            log(storeFile.getParentFile(), MessageFormat.format("New CA certificate {0,number,0} [{1}]", cert.getSerialNumber(), issuerDN.toString()));
            x509log.log(MessageFormat.format("New CA certificate {0,number,0} [{1}]", cert.getSerialNumber(), cert.getIssuerDN().getName()));
            return cert;
        } catch (Throwable t) {
            throw new RuntimeException("Failed to generate Gitblit CA certificate!", t);
@@ -621,8 +642,8 @@
        try {
            // read the Gitblit CA key and certificate
            KeyStore store = openKeyStore(caKeystoreFile, caKeystorePassword);
            PrivateKey caPrivateKey = (PrivateKey) store.getKey(CA_FN, caKeystorePassword.toCharArray());
            X509Certificate caCert = (X509Certificate) store.getCertificate(CA_FN);
            PrivateKey caPrivateKey = (PrivateKey) store.getKey(CA_ALIAS, caKeystorePassword.toCharArray());
            X509Certificate caCert = (X509Certificate) store.getCertificate(CA_ALIAS);
            X500Name issuerDN = new X500Name(PrincipalUtil.getIssuerX509Principal(caCert).getName());
            X509v2CRLBuilder crlBuilder = new X509v2CRLBuilder(issuerDN, new Date());
@@ -642,8 +663,6 @@
                    caRevocationList.delete();
                }
                tmpFile.renameTo(caRevocationList);
                log(caRevocationList.getParentFile(), "new certificate revocation list created");
            } finally {
                if (fos != null) {
                    fos.close();
@@ -685,19 +704,22 @@
     * @param clientMetadata a container for dynamic parameters needed for generation
     * @param caKeystoreFile
     * @param caKeystorePassword
     * @param x509log
     * @return a zip file containing the P12, PEM, and personalized README
     */
    public static File newClientBundle(X509Metadata clientMetadata, File caKeystoreFile, String caKeystorePassword) {
    public static File newClientBundle(X509Metadata clientMetadata, File caKeystoreFile,
            String caKeystorePassword, X509Log x509log) {
        try {
            // read the Gitblit CA key and certificate
            KeyStore store = openKeyStore(caKeystoreFile, caKeystorePassword);
            PrivateKey caPrivateKey = (PrivateKey) store.getKey(CA_FN, caKeystorePassword.toCharArray());
            X509Certificate caCert = (X509Certificate) store.getCertificate(CA_FN);
            PrivateKey caPrivateKey = (PrivateKey) store.getKey(CA_ALIAS, caKeystorePassword.toCharArray());
            X509Certificate caCert = (X509Certificate) store.getCertificate(CA_ALIAS);
            
            // generate the P12 and PEM files
            File targetFolder = new File(caKeystoreFile.getParentFile(), clientMetadata.commonName);
            newClientCertificate(clientMetadata, caPrivateKey, caCert, targetFolder);
            X509Certificate cert = newClientCertificate(clientMetadata, caPrivateKey, caCert, targetFolder);
            x509log.log(MessageFormat.format("New client certificate {0,number,0} [{1}]", cert.getSerialNumber(), cert.getSubjectDN().getName()));
            // process template message
            String readme = processTemplate(new File(caKeystoreFile.getParentFile(), "instructions.tmpl"), clientMetadata);
            
@@ -830,8 +852,6 @@
            // save certificate after successfully creating the key stores
            saveCertificate(userCert, certFile);
            
            log(targetFolder.getParentFile(), MessageFormat.format("New client certificate {0,number,0} [{1}]", userCert.getSerialNumber(), userDN.toString()));
            return userCert;
        } catch (Throwable t) {
            throw new RuntimeException("Failed to generate client certificate!", t);
@@ -924,25 +944,6 @@
        return content;
    }
    
    private static void log(File folder, String message) {
        BufferedWriter writer = null;
        try {
            writer = new BufferedWriter(new FileWriter(new File(folder, "log.txt"), true));
            writer.write(MessageFormat.format("{0,date,yyyy-MM-dd HH:mm}: {1}", new Date(), message));
            writer.newLine();
            writer.flush();
        } catch (Exception e) {
            logger.error("Failed to append log entry!", e);
        } finally {
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                }
            }
        }
    }
    /**
     * Revoke a certificate.
     * 
@@ -951,15 +952,17 @@
     * @param caRevocationList
     * @param caKeystoreFile
     * @param caKeystorePassword
     * @param x509log
     * @return true if the certificate has been revoked
     */
    public static boolean revoke(X509Certificate cert, RevocationReason reason,
            File caRevocationList, File caKeystoreFile, String caKeystorePassword) {
            File caRevocationList, File caKeystoreFile, String caKeystorePassword,
            X509Log x509log) {
        try {
            // read the Gitblit CA key and certificate
            KeyStore store = openKeyStore(caKeystoreFile, caKeystorePassword);
            PrivateKey caPrivateKey = (PrivateKey) store.getKey(CA_FN, caKeystorePassword.toCharArray());
            return revoke(cert, reason, caRevocationList, caPrivateKey);
            PrivateKey caPrivateKey = (PrivateKey) store.getKey(CA_ALIAS, caKeystorePassword.toCharArray());
            return revoke(cert, reason, caRevocationList, caPrivateKey, x509log);
        } catch (Exception e) {
            logger.error(MessageFormat.format("Failed to revoke certificate {0,number,0} [{1}] in {2}",
                    cert.getSerialNumber(), cert.getSubjectDN().getName(), caRevocationList));
@@ -974,12 +977,12 @@
     * @param reason
     * @param caRevocationList
     * @param caPrivateKey
     * @param x509log
     * @return true if the certificate has been revoked
     */
    public static boolean revoke(X509Certificate cert, RevocationReason reason,
             File caRevocationList, PrivateKey caPrivateKey) {
             File caRevocationList, PrivateKey caPrivateKey, X509Log x509log) {
        try {
            X500Name subjectDN = new X500Name(PrincipalUtil.getSubjectX509Principal(cert).getName());
            X500Name issuerDN = new X500Name(PrincipalUtil.getIssuerX509Principal(cert).getName());
            X509v2CRLBuilder crlBuilder = new X509v2CRLBuilder(issuerDN, new Date());
            if (caRevocationList.exists()) {
@@ -1005,8 +1008,6 @@
                }
                tmpFile.renameTo(caRevocationList);
                
                log(caRevocationList.getParentFile(), MessageFormat.format("Revoked certificate {0,number,0} reason: {1} [{2}]",
                        cert.getSerialNumber(), reason.toString(), subjectDN.toString()));
            } finally {
                if (fos != null) {
                    fos.close();
@@ -1015,6 +1016,9 @@
                    tmpFile.delete();
                }
            }
            x509log.log(MessageFormat.format("Revoked certificate {0,number,0} reason: {1} [{2}]",
                    cert.getSerialNumber(), reason.toString(), cert.getSubjectDN().getName()));
            return true;
        } catch (Exception e) {
            logger.error(MessageFormat.format("Failed to revoke certificate {0,number,0} [{1}] in {2}",
src/com/gitblit/wicket/GitBlitWebApp.properties
@@ -414,4 +414,9 @@
gb.privilegeWithdrawn = privilege withdrawn
gb.time.inMinutes = in {0} mins
gb.time.inHours = in {0} hours
gb.time.inDays = in {0} days
gb.time.inDays = in {0} days
gb.hostname = hostname
gb.hostnameRequired = Please enter a hostname
gb.newWebCertificate = new server SSL certificate
gb.certificateDefaults = certificate defaults
gb.duration = duration
tests/com/gitblit/tests/X509UtilsTest.java
@@ -34,6 +34,7 @@
import com.gitblit.utils.HttpUtils;
import com.gitblit.utils.X509Utils;
import com.gitblit.utils.X509Utils.RevocationReason;
import com.gitblit.utils.X509Utils.X509Log;
import com.gitblit.utils.X509Utils.X509Metadata;
/**
@@ -48,12 +49,18 @@
    // based on the JCE policy files
    String caPassword = "aBcDeFg";
    File folder = new File(System.getProperty("user.dir"), "x509test");
    X509Log log = new X509Log() {
        public void log(String message) {
            System.out.println(message);
        }
    };
    @Before
    public void prepare() throws Exception {
        cleanUp();
        X509Metadata goMetadata = new X509Metadata("localhost", caPassword);
        X509Utils.prepareX509Infrastructure(goMetadata, folder);
        X509Utils.prepareX509Infrastructure(goMetadata, folder, log);
    }
    
    @After
@@ -66,16 +73,16 @@
    @Test
    public void testNewCA() throws Exception {        
        File storeFile = new File(folder, X509Utils.CA_KEY_STORE);
        X509Utils.getPrivateKey(X509Utils.CA_FN, storeFile, caPassword);
        X509Certificate cert = X509Utils.getCertificate(X509Utils.CA_FN, storeFile, caPassword);
        X509Utils.getPrivateKey(X509Utils.CA_ALIAS, storeFile, caPassword);
        X509Certificate cert = X509Utils.getCertificate(X509Utils.CA_ALIAS, storeFile, caPassword);
        assertEquals("O=Gitblit,OU=Gitblit,CN=Gitblit Certificate Authority", cert.getIssuerDN().getName());
    }    
    @Test
    public void testCertificateUserMapping() throws Exception {        
        File storeFile = new File(folder, X509Utils.CA_KEY_STORE);
        PrivateKey caPrivateKey = X509Utils.getPrivateKey(X509Utils.CA_FN, storeFile, caPassword);
        X509Certificate caCert = X509Utils.getCertificate(X509Utils.CA_FN, storeFile, caPassword);
        PrivateKey caPrivateKey = X509Utils.getPrivateKey(X509Utils.CA_ALIAS, storeFile, caPassword);
        X509Certificate caCert = X509Utils.getCertificate(X509Utils.CA_ALIAS, storeFile, caPassword);
        
        X509Metadata userMetadata = new X509Metadata("james", "james");
        userMetadata.serverHostname = "www.myserver.com";
@@ -108,7 +115,7 @@
        userMetadata.userDisplayname = "James Moger";
        userMetadata.passwordHint = "your name";
        File zip = X509Utils.newClientBundle(userMetadata, storeFile, caPassword);
        File zip = X509Utils.newClientBundle(userMetadata, storeFile, caPassword, log);
        assertTrue(zip.exists());
        
        List<String> expected = Arrays.asList(userMetadata.commonName + ".pem", userMetadata.commonName + ".p12", "README.TXT");
@@ -124,8 +131,8 @@
    @Test
    public void testCertificateRevocation() throws Exception {        
        File storeFile = new File(folder, X509Utils.CA_KEY_STORE);
        PrivateKey caPrivateKey = X509Utils.getPrivateKey(X509Utils.CA_FN, storeFile, caPassword);
        X509Certificate caCert = X509Utils.getCertificate(X509Utils.CA_FN, storeFile, caPassword);
        PrivateKey caPrivateKey = X509Utils.getPrivateKey(X509Utils.CA_ALIAS, storeFile, caPassword);
        X509Certificate caCert = X509Utils.getCertificate(X509Utils.CA_ALIAS, storeFile, caPassword);
        
        X509Metadata userMetadata = new X509Metadata("james", "james");
        userMetadata.serverHostname = "www.myserver.com";
@@ -140,7 +147,7 @@
        assertFalse(X509Utils.isRevoked(cert1, caRevocationList));
        
        // revoke certificate and then confirm it IS revoked
        X509Utils.revoke(cert1, RevocationReason.ACompromise, caRevocationList, storeFile, caPassword);
        X509Utils.revoke(cert1, RevocationReason.ACompromise, caRevocationList, storeFile, caPassword, log);
        assertTrue(X509Utils.isRevoked(cert1, caRevocationList));
        
        // generate a second certificate
@@ -151,7 +158,7 @@
        assertFalse(X509Utils.isRevoked(cert2, caRevocationList));
        
        // revoke second certificate and then confirm it IS revoked
        X509Utils.revoke(cert2, RevocationReason.ACompromise, caRevocationList, caPrivateKey);
        X509Utils.revoke(cert2, RevocationReason.ACompromise, caRevocationList, caPrivateKey, log);
        assertTrue(X509Utils.isRevoked(cert1, caRevocationList));
        assertTrue(X509Utils.isRevoked(cert2, caRevocationList));
        
@@ -164,7 +171,7 @@
        assertFalse(X509Utils.isRevoked(cert3, caRevocationList));
        
        // revoke third certificate and then confirm it IS revoked
        X509Utils.revoke(cert3, RevocationReason.ACompromise, caRevocationList, caPrivateKey);
        X509Utils.revoke(cert3, RevocationReason.ACompromise, caRevocationList, caPrivateKey, log);
        assertTrue(X509Utils.isRevoked(cert1, caRevocationList));
        assertTrue(X509Utils.isRevoked(cert2, caRevocationList));
        assertTrue(X509Utils.isRevoked(cert3, caRevocationList));