From c8b26c51aa67fc9345b624e36aab6f819e7eed74 Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Sun, 25 Nov 2012 11:35:31 -0500
Subject: [PATCH] Gitblit Certificate Authority revisions.  Eliminate certificate scripts.

---
 src/com/gitblit/authority/NewCertificateConfig.java       |   33 +
 src/com/gitblit/authority/NewWebCertificateDialog.java    |  131 ++++++
 src/com/gitblit/utils/X509Utils.java                      |  114 +++--
 src/com/gitblit/authority/DefaultOidsPanel.java           |   80 ++++
 tests/com/gitblit/tests/X509UtilsTest.java                |   29 
 src/com/gitblit/authority/UserOidsPanel.java              |   95 +++++
 src/com/gitblit/authority/GitblitAuthority.java           |  185 +++++++++
 resources/settings_32x32.png                              |    0 
 /dev/null                                                 |  230 ------------
 src/com/gitblit/authority/X509CertificateViewer.java      |    6 
 src/com/gitblit/wicket/GitBlitWebApp.properties           |    7 
 src/com/gitblit/GitBlitServer.java                        |   28 +
 resources/rosette_32x32.png                               |    0 
 src/com/gitblit/authority/NewClientCertificateDialog.java |   14 
 src/com/gitblit/authority/UserCertificatePanel.java       |   88 ----
 src/com/gitblit/authority/Utils.java                      |   22 +
 build.xml                                                 |   19 
 17 files changed, 665 insertions(+), 416 deletions(-)

diff --git a/build.xml b/build.xml
index 8a7fd54..aa247d3 100644
--- a/build.xml
+++ b/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 -->
diff --git a/distrib/makeclientcertificate.cmd b/distrib/makeclientcertificate.cmd
deleted file mode 100644
index 7294142..0000000
--- a/distrib/makeclientcertificate.cmd
+++ /dev/null
@@ -1 +0,0 @@
-@java -cp gitblit.jar;"%CD%\ext\*" com.gitblit.authority.MakeClientCertificate
diff --git a/distrib/makeclientcertificate.sh b/distrib/makeclientcertificate.sh
deleted file mode 100644
index 76a195e..0000000
--- a/distrib/makeclientcertificate.sh
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/bin/sh
-java -cp gitblit.jar:$PWD/ext/* com.gitblit.authority.MakeClientCertificate
diff --git a/distrib/makekeystore.cmd b/distrib/makekeystore.cmd
deleted file mode 100644
index 8a72e03..0000000
--- a/distrib/makekeystore.cmd
+++ /dev/null
@@ -1,6 +0,0 @@
-@REM --------------------------------------------------------------------------
-@REM Set HOSTNAME to the server's hostname
-@REM --------------------------------------------------------------------------
-@SET HOSTNAME=localhost
-@del serverKeyStore.jks
-@java -cp gitblit.jar;"%CD%\ext\*" com.gitblit.MakeCertificate --hostname %HOSTNAME% --subject "CN=%HOSTNAME%, OU=Gitblit, O=Gitblit, L=Some Town, ST=Some State, C=US"
diff --git a/distrib/makekeystore.sh b/distrib/makekeystore.sh
deleted file mode 100644
index 59bba90..0000000
--- a/distrib/makekeystore.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/bin/sh
-# SET HOSTNAME to the server's hostname
-HOSTNAME=localhost
-rm keystore
-java -cp gitblit.jar:$PWD/ext/* com.gitblit.MakeCertificate --hostname $HOSTNAME --subject "CN=$HOSTNAME, OU=Gitblit, O=Gitblit, L=Some Town, ST=Some State, C=US"
diff --git a/distrib/makekeystore_jdk.cmd b/distrib/makekeystore_jdk.cmd
deleted file mode 100644
index 121c671..0000000
--- a/distrib/makekeystore_jdk.cmd
+++ /dev/null
@@ -1,6 +0,0 @@
-@REM --------------------------------------------------------------------------
-@REM Set HOSTNAME to the server's hostname
-@REM --------------------------------------------------------------------------
-@SET HOSTNAME=localhost
-@del serverKeyStore.jks
-@keytool -keystore serverKeyStore.jks -alias %HOSTNAME% -genkey -keyalg RSA -dname "CN=%HOSTNAME%, OU=Gitblit, O=Gitblit, L=Some Town, ST=Some State, C=US"
\ No newline at end of file
diff --git a/distrib/makekeystore_jdk.sh b/distrib/makekeystore_jdk.sh
deleted file mode 100644
index 97e5dc9..0000000
--- a/distrib/makekeystore_jdk.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/bin/sh
-# --------------------------------------------------------------------------
-# SET HOSTNAME to the server's hostname
-# --------------------------------------------------------------------------
-HOSTNAME=localhost
-rm keystore
-keytool -keystore keystore -alias $HOSTNAME -genkey -keyalg RSA -dname "CN=$HOSTNAME, OU=Gitblit, O=Gitblit, L=Some Town, ST=Some State, C=US"
\ No newline at end of file
diff --git a/resources/rosette_32x32.png b/resources/rosette_32x32.png
new file mode 100644
index 0000000..ecbcc0a
--- /dev/null
+++ b/resources/rosette_32x32.png
Binary files differ
diff --git a/resources/settings_32x32.png b/resources/settings_32x32.png
new file mode 100644
index 0000000..39af4be
--- /dev/null
+++ b/resources/settings_32x32.png
Binary files differ
diff --git a/src/com/gitblit/GitBlitServer.java b/src/com/gitblit/GitBlitServer.java
index 52afb54..d98f891 100644
--- a/src/com/gitblit/GitBlitServer.java
+++ b/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,
diff --git a/src/com/gitblit/MakeCertificate.java b/src/com/gitblit/MakeCertificate.java
deleted file mode 100644
index e3c39ff..0000000
--- a/src/com/gitblit/MakeCertificate.java
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * Copyright 2011 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.FileOutputStream;
-import java.math.BigInteger;
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
-import java.security.KeyStore;
-import java.security.SecureRandom;
-import java.security.Security;
-import java.security.cert.X509Certificate;
-import java.util.Date;
-
-import javax.security.auth.x500.X500Principal;
-
-import org.bouncycastle.cert.X509v3CertificateBuilder;
-import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
-import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
-import org.bouncycastle.operator.ContentSigner;
-import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
-
-import com.beust.jcommander.JCommander;
-import com.beust.jcommander.Parameter;
-import com.beust.jcommander.ParameterException;
-import com.beust.jcommander.Parameters;
-import com.gitblit.utils.TimeUtils;
-
-/**
- * Utility class to generate self-signed certificates.
- * 
- * @author James Moger
- * 
- */
-public class MakeCertificate {
-
-	private static final String BC = org.bouncycastle.jce.provider.BouncyCastleProvider.PROVIDER_NAME;
-
-	public static void main(String... args) {
-		Params params = new Params();
-		JCommander jc = new JCommander(params);
-		try {
-			jc.parse(args);
-		} catch (ParameterException t) {
-			System.err.println(t.getMessage());
-			jc.usage();
-		}
-		File keystore = new File("serverKeyStore.jks");
-		generateSelfSignedCertificate(params.hostname, keystore, params.storePassword,
-				params.subject);
-	}
-
-	public static void generateSelfSignedCertificate(String hostname, File keystore,
-			String keystorePassword, String info) {
-		try {
-			Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
-
-			KeyPairGenerator kpGen = KeyPairGenerator.getInstance("RSA", BC);
-			kpGen.initialize(1024, new SecureRandom());
-			KeyPair pair = kpGen.generateKeyPair();
-
-			// Generate self-signed certificate
-			X500Principal principal = new X500Principal(info);
-
-			Date notBefore = new Date(System.currentTimeMillis() - TimeUtils.ONEDAY);
-			Date notAfter = new Date(System.currentTimeMillis() + 10 * TimeUtils.ONEYEAR);
-			BigInteger serial = BigInteger.valueOf(System.currentTimeMillis());
-
-			X509v3CertificateBuilder certGen = new JcaX509v3CertificateBuilder(principal, serial,
-					notBefore, notAfter, principal, pair.getPublic());
-			ContentSigner sigGen = new JcaContentSignerBuilder("SHA256WithRSAEncryption")
-					.setProvider(BC).build(pair.getPrivate());
-			X509Certificate cert = new JcaX509CertificateConverter().setProvider(BC)
-					.getCertificate(certGen.build(sigGen));
-			cert.checkValidity(new Date());
-			cert.verify(cert.getPublicKey());
-
-			// Save to keystore
-			KeyStore store = KeyStore.getInstance("JKS");
-			if (keystore.exists()) {
-				FileInputStream fis = new FileInputStream(keystore);
-				store.load(fis, keystorePassword.toCharArray());
-				fis.close();
-			} else {
-				store.load(null);
-			}
-			store.setKeyEntry(hostname, pair.getPrivate(), keystorePassword.toCharArray(),
-					new java.security.cert.Certificate[] { cert });
-			FileOutputStream fos = new FileOutputStream(keystore);
-			store.store(fos, keystorePassword.toCharArray());
-			fos.close();
-		} catch (Throwable t) {
-			t.printStackTrace();
-			throw new RuntimeException("Failed to generate self-signed certificate!", t);
-		}
-	}
-
-	/**
-	 * JCommander Parameters class for MakeCertificate.
-	 */
-	@Parameters(separators = " ")
-	private static class Params {
-
-		private static final FileSettings FILESETTINGS = new FileSettings(Constants.PROPERTIES_FILE);
-
-		@Parameter(names = { "--hostname" }, description = "Server Hostname", required = true)
-		public String hostname;
-
-		@Parameter(names = { "--subject" }, description = "Certificate subject", required = true)
-		public String subject;
-
-		@Parameter(names = "--storePassword", description = "Password for SSL (https) keystore.")
-		public String storePassword = FILESETTINGS.getString(Keys.server.storePassword, "");
-	}
-}
diff --git a/src/com/gitblit/authority/DefaultOidsPanel.java b/src/com/gitblit/authority/DefaultOidsPanel.java
new file mode 100644
index 0000000..12b919f
--- /dev/null
+++ b/src/com/gitblit/authority/DefaultOidsPanel.java
@@ -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();
+	}
+}
diff --git a/src/com/gitblit/authority/GitblitAuthority.java b/src/com/gitblit/authority/GitblitAuthority.java
index 5e8b30e..7734a15 100644
--- a/src/com/gitblit/authority/GitblitAuthority.java
+++ b/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) {
+				}
+			}
+		}
+	}
 }
diff --git a/src/com/gitblit/authority/MakeClientCertificate.java b/src/com/gitblit/authority/MakeClientCertificate.java
deleted file mode 100644
index 5829fc1..0000000
--- a/src/com/gitblit/authority/MakeClientCertificate.java
+++ /dev/null
@@ -1,230 +0,0 @@
-/*
- * 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.io.File;
-import java.text.MessageFormat;
-import java.util.Date;
-
-import javax.activation.DataHandler;
-import javax.activation.FileDataSource;
-import javax.mail.Message;
-import javax.mail.Multipart;
-import javax.mail.internet.MimeBodyPart;
-import javax.mail.internet.MimeMultipart;
-
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.util.FS;
-
-import com.beust.jcommander.JCommander;
-import com.beust.jcommander.Parameter;
-import com.beust.jcommander.ParameterException;
-import com.beust.jcommander.Parameters;
-import com.gitblit.ConfigUserService;
-import com.gitblit.Constants;
-import com.gitblit.FileSettings;
-import com.gitblit.IUserService;
-import com.gitblit.Keys;
-import com.gitblit.MailExecutor;
-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.X509Metadata;
-
-/**
- * Utility class to generate self-signed certificates.
- * 
- * @author James Moger
- * 
- */
-public class MakeClientCertificate {
-
-	public static void main(String... args) throws Exception {
-		Params params = new Params();
-		JCommander jc = new JCommander(params);
-		try {
-			jc.parse(args);
-		} catch (ParameterException t) {
-			System.err.println(t.getMessage());
-			jc.usage();
-			System.exit(-1);
-		}
-
-		// Load the user list
-		String us = Params.FILESETTINGS.getString(Keys.realm.userService, "users.conf");
-		String ext = us.substring(us.lastIndexOf(".") + 1).toLowerCase();
-		IUserService service = null;
-		if (!ext.equals("conf") && !ext.equals("properties")) {
-			if (us.equals("com.gitblit.LdapUserService")) {
-				us = Params.FILESETTINGS.getString(Keys.realm.ldap.backingUserService, "users.conf");		
-			} else if (us.equals("com.gitblit.LdapUserService")) {
-				us = Params.FILESETTINGS.getString(Keys.realm.redmine.backingUserService, "users.conf");
-			}
-		}
-
-		if (us.endsWith(".conf")) {
-			service = new ConfigUserService(new File(us));
-		} else {
-			throw new RuntimeException("Unsupported user service: " + us);
-		}
-		
-		// Confirm the user exists
-		UserModel user = service.getUserModel(params.username);
-		if (user == null) {
-			System.out.println(MessageFormat.format("Failed to find user \"{0}\" in {1}", params.username, us));
-			System.exit(-1);
-		}
-				
-		File folder = new File(System.getProperty("user.dir"));
-		X509Metadata serverMetadata = new X509Metadata("localhost", params.storePassword);		
-		X509Utils.prepareX509Infrastructure(serverMetadata, folder);
-		
-		File caStore = new File(folder, X509Utils.CA_KEY_STORE);
-		
-		X509Metadata clientMetadata = new X509Metadata(params.username, params.password);
-		clientMetadata.userDisplayname = user.getDisplayName();
-		clientMetadata.emailAddress = user.emailAddress;
-		clientMetadata.serverHostname = params.serverHostname;
-		clientMetadata.passwordHint = params.hint;
-		
-		UserCertificateModel ucm = null;
-		
-		// set default values from config file
-		File certificatesConfigFile = new File(folder, X509Utils.CA_CONFIG);
-		FileBasedConfig config = new FileBasedConfig(certificatesConfigFile, FS.detect());
-		if (certificatesConfigFile.exists()) {
-			config.load();
-			NewCertificateConfig certificateConfig = NewCertificateConfig.KEY.parse(config);
-			certificateConfig.update(clientMetadata);
-			
-			ucm = UserCertificateConfig.KEY.parse(config).getUserCertificateModel(params.username);
-		}
-		
-		// set user's specified OID values
-		if (!StringUtils.isEmpty(user.organizationalUnit)) {
-			clientMetadata.oids.put("OU", user.organizationalUnit);
-		}
-		if (!StringUtils.isEmpty(user.organization)) {
-			clientMetadata.oids.put("O", user.organization);
-		}
-		if (!StringUtils.isEmpty(user.locality)) {
-			clientMetadata.oids.put("L", user.locality);
-		}
-		if (!StringUtils.isEmpty(user.stateProvince)) {
-			clientMetadata.oids.put("ST", user.stateProvince);
-		}
-		if (!StringUtils.isEmpty(user.countryCode)) {
-			clientMetadata.oids.put("C", user.countryCode);
-		}
-
-		if (params.duration > 0) {
-			// overriding duration from command-line parameter
-			clientMetadata.notAfter = new Date(System.currentTimeMillis() + TimeUtils.ONEDAY * params.duration);
-		}
-
-		// generate zip bundle
-		File zip = X509Utils.newClientBundle(clientMetadata, caStore, params.storePassword);		
-		
-		String indent = "  ";
-		System.out.println(MessageFormat.format("Client certificate bundle generated for {0}", params.username));
-		System.out.print(indent);
-		System.out.println(zip);
-		
-		// update certificates.conf
-		if (ucm == null) {
-			ucm = new UserCertificateModel(new UserModel(params.username));
-		}
-
-		// save latest expiration date
-		if (ucm.expires == null || clientMetadata.notAfter.after(ucm.expires)) {
-			ucm.expires = clientMetadata.notAfter;
-		}
-		ucm.update(config);
-		config.save();
-		
-		if (params.sendEmail) {
-			if (StringUtils.isEmpty(user.emailAddress)) {
-				System.out.print(indent);
-				System.out.println(MessageFormat.format("User \"{0}\" does not have an email address.", user.username));
-			} else {
-				// send email
-				MailExecutor mail = new MailExecutor(Params.FILESETTINGS);
-				if (mail.isReady()) {
-					Message message = mail.createMessage(user.emailAddress);
-					message.setSubject("Your Gitblit client certificate for " + clientMetadata.serverHostname);
-
-					// body of email
-					String body = X509Utils.processTemplate(new File(caStore.getParentFile(), "mail.tmpl"), clientMetadata);
-					if (StringUtils.isEmpty(body)) {
-						body = MessageFormat.format("Hi {0}\n\nHere is your client certificate bundle.\nInside the zip file are installation instructions.", user.getDisplayName());
-					}
-					Multipart mp = new MimeMultipart();
-					MimeBodyPart messagePart = new MimeBodyPart();
-					messagePart.setText(body);
-					mp.addBodyPart(messagePart);
-
-					// attach zip
-					MimeBodyPart filePart = new MimeBodyPart();
-					FileDataSource fds = new FileDataSource(zip);
-					filePart.setDataHandler(new DataHandler(fds));
-					filePart.setFileName(fds.getName());
-					mp.addBodyPart(filePart);
-
-					message.setContent(mp);
-
-					mail.sendNow(message);
-					System.out.println();
-					System.out.println("Mail sent.");
-				} else {
-					System.out.print(indent);
-					System.out.println("Mail server is not properly configured.  Can not send email.");
-				}
-			}
-		}
-	}
-
-	/**
-	 * JCommander Parameters class for MakeClientCertificate.
-	 */
-	@Parameters(separators = " ")
-	private static class Params {
-
-		private static final FileSettings FILESETTINGS = new FileSettings(Constants.PROPERTIES_FILE);
-
-		@Parameter(names = { "--username" }, description = "Username for certificate (CN)", required = true)
-		public String username;
-
-		@Parameter(names = { "--password" }, description = "Password to secure user's certificate (<=7 chars unless JCE Unlimited Strength installed)", required = true)
-		public String password;
-
-		@Parameter(names = { "--hint" }, description = "Hint for password", required = true)
-		public String hint;
-		
-		@Parameter(names = "--duration", description = "Number of days from now until the certificate expires")
-		public int duration = 0;
-
-		@Parameter(names = "--storePassword", description = "Password for CA keystore.")
-		public String storePassword = FILESETTINGS.getString(Keys.server.storePassword, "");
-		
-		@Parameter(names = "--server", description = "Hostname or server identity")
-		public String serverHostname = Params.FILESETTINGS.getString(Keys.web.siteName, "localhost");
-
-		@Parameter(names = "--sendEmail", description = "Send an email to the user with their bundle")
-		public boolean sendEmail;
-		
-	}
-}
diff --git a/src/com/gitblit/authority/NewCertificateConfig.java b/src/com/gitblit/authority/NewCertificateConfig.java
index e4db130..ca047c8 100644
--- a/src/com/gitblit/authority/NewCertificateConfig.java
+++ b/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);
+			}
+		}
 	}
\ No newline at end of file
diff --git a/src/com/gitblit/authority/NewClientCertificateDialog.java b/src/com/gitblit/authority/NewClientCertificateDialog.java
index ad4fe9a..b04ae73 100644
--- a/src/com/gitblit/authority/NewClientCertificateDialog.java
+++ b/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;
 		}
diff --git a/src/com/gitblit/authority/NewWebCertificateDialog.java b/src/com/gitblit/authority/NewWebCertificateDialog.java
new file mode 100644
index 0000000..a6846c5
--- /dev/null
+++ b/src/com/gitblit/authority/NewWebCertificateDialog.java
@@ -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;
+	}
+}
diff --git a/src/com/gitblit/authority/UserCertificatePanel.java b/src/com/gitblit/authority/UserCertificatePanel.java
index 6b1daee..79c0d94 100644
--- a/src/com/gitblit/authority/UserCertificatePanel.java
+++ b/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);
diff --git a/src/com/gitblit/authority/UserOidsPanel.java b/src/com/gitblit/authority/UserOidsPanel.java
new file mode 100644
index 0000000..8c3adf6
--- /dev/null
+++ b/src/com/gitblit/authority/UserOidsPanel.java
@@ -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();
+	}
+}
diff --git a/src/com/gitblit/authority/Utils.java b/src/com/gitblit/authority/Utils.java
index 5c82493..45e028e 100644
--- a/src/com/gitblit/authority/Utils.java
+++ b/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));
diff --git a/src/com/gitblit/authority/X509CertificateViewer.java b/src/com/gitblit/authority/X509CertificateViewer.java
index d29c6e7..1b09515 100644
--- a/src/com/gitblit/authority/X509CertificateViewer.java
+++ b/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));
diff --git a/src/com/gitblit/utils/X509Utils.java b/src/com/gitblit/utils/X509Utils.java
index 3caff35..1510b2c 100644
--- a/src/com/gitblit/utils/X509Utils.java
+++ b/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}",
diff --git a/src/com/gitblit/wicket/GitBlitWebApp.properties b/src/com/gitblit/wicket/GitBlitWebApp.properties
index 6b2102e..1a1ee17 100644
--- a/src/com/gitblit/wicket/GitBlitWebApp.properties
+++ b/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
\ No newline at end of file
+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
\ No newline at end of file
diff --git a/tests/com/gitblit/tests/X509UtilsTest.java b/tests/com/gitblit/tests/X509UtilsTest.java
index 85afce0..85d30de 100644
--- a/tests/com/gitblit/tests/X509UtilsTest.java
+++ b/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));

--
Gitblit v1.9.1