From e571c4ae9d9883ba4c4a1739dd9948bf096a2cff Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Mon, 26 Nov 2012 21:31:12 -0500
Subject: [PATCH] More refinements to GCA

---
 src/com/gitblit/authority/AuthorityWorker.java            |   58 ++++++++
 src/com/gitblit/utils/X509Utils.java                      |   18 +
 resources/mail_16x16.png                                  |    0 
 distrib/gitblit.properties                                |   11 +
 src/com/gitblit/authority/GitblitAuthority.java           |  176 +++++++++++++++++-------
 src/com/gitblit/authority/X509CertificateViewer.java      |    2 
 src/com/gitblit/wicket/GitBlitWebApp.properties           |   14 +
 src/com/gitblit/authority/UserCertificateModel.java       |   23 ++
 src/com/gitblit/authority/NewClientCertificateDialog.java |   14 +
 src/com/gitblit/authority/UserCertificatePanel.java       |   57 ++++++--
 src/com/gitblit/authority/UserCertificateConfig.java      |    1 
 build.xml                                                 |    1 
 src/com/gitblit/authority/NewSSLCertificateDialog.java    |   13 -
 13 files changed, 300 insertions(+), 88 deletions(-)

diff --git a/build.xml b/build.xml
index aa247d3..59d79b5 100644
--- a/build.xml
+++ b/build.xml
@@ -766,6 +766,7 @@
 			<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/mail_16x16.png" />
 			<resource file="${basedir}/resources/blank.png" />
 			<resource file="${basedir}/resources/bullet_green.png" />
 			<resource file="${basedir}/resources/bullet_orange.png" />
diff --git a/distrib/gitblit.properties b/distrib/gitblit.properties
index 9daabde..65e19ed 100644
--- a/distrib/gitblit.properties
+++ b/distrib/gitblit.properties
@@ -1133,6 +1133,17 @@
 # This is provided for convenience, its probably more secure to set this value
 # using the --storePassword command line parameter.
 #
+# If you are using the official JRE or JDK from Oracle you may not have the
+# JCE Unlimited Strength Jurisdiction Policy files bundled with your JVM.  Because
+# of this, your store/key password can not exceed 7 characters.  If you require
+# longer passwords you may need to install the JCE Unlimited Strength Jurisdiction
+# Policy files from Oracle.
+#
+# http://www.oracle.com/technetwork/java/javase/downloads/index.html
+#
+# Gitblit and the Gitblit Certificate Authority will both indicate if Unlimited
+# Strength encryption is available.
+#
 # SINCE 0.5.0
 # RESTART REQUIRED
 server.storePassword = gitblit
diff --git a/resources/mail_16x16.png b/resources/mail_16x16.png
new file mode 100644
index 0000000..8327873
--- /dev/null
+++ b/resources/mail_16x16.png
Binary files differ
diff --git a/src/com/gitblit/authority/AuthorityWorker.java b/src/com/gitblit/authority/AuthorityWorker.java
new file mode 100644
index 0000000..262bbb5
--- /dev/null
+++ b/src/com/gitblit/authority/AuthorityWorker.java
@@ -0,0 +1,58 @@
+/*
+ * 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.Component;
+import java.awt.Cursor;
+import java.io.IOException;
+
+import javax.swing.SwingWorker;
+
+public abstract class AuthorityWorker extends SwingWorker<Boolean, Void> {
+
+	private final Component parent;
+
+	public AuthorityWorker(Component parent) {
+		this.parent = parent;
+		parent.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+	}
+
+	@Override
+	protected Boolean doInBackground() throws IOException {
+		return doRequest();
+	}
+
+	protected void done() {
+		parent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
+		try {
+			Boolean success = get();
+			if (success) {
+				onSuccess();
+			} else {
+				onFailure();
+			}
+		} catch (Throwable t) {
+			Utils.showException(parent, t);
+		}
+	}
+
+	protected abstract Boolean doRequest() throws IOException;
+
+	protected abstract void onSuccess();
+
+	protected void onFailure() {
+	}
+}
diff --git a/src/com/gitblit/authority/GitblitAuthority.java b/src/com/gitblit/authority/GitblitAuthority.java
index 7734a15..846e942 100644
--- a/src/com/gitblit/authority/GitblitAuthority.java
+++ b/src/com/gitblit/authority/GitblitAuthority.java
@@ -88,6 +88,7 @@
 import com.gitblit.client.HeaderPanel;
 import com.gitblit.client.Translation;
 import com.gitblit.models.UserModel;
+import com.gitblit.utils.ArrayUtils;
 import com.gitblit.utils.StringUtils;
 import com.gitblit.utils.TimeUtils;
 import com.gitblit.utils.X509Utils;
@@ -364,7 +365,10 @@
 			public void newCertificate(UserCertificateModel ucm, X509Metadata metadata, boolean sendEmail) {
 				prepareX509Infrastructure();
 				Date notAfter = metadata.notAfter;
-				metadata.serverHostname = gitblitSettings.getString(Keys.web.siteName, "localhost");
+				metadata.serverHostname = gitblitSettings.getString(Keys.web.siteName, Constants.NAME);
+				if (StringUtils.isEmpty(metadata.serverHostname)) {
+					metadata.serverHostname = Constants.NAME;
+				}
 				UserModel user = ucm.user;				
 				
 				// set default values from config file
@@ -421,38 +425,7 @@
 				table.getSelectionModel().setSelectionInterval(modelIndex, modelIndex);
 				
 				if (sendEmail) {
-					// send email
-					try {
-						if (mail.isReady()) {
-							Message message = mail.createMessage(user.emailAddress);
-							message.setSubject("Your Gitblit client certificate for " + metadata.serverHostname);
-
-							// body of email
-							String body = X509Utils.processTemplate(new File(caKeystoreFile.getParentFile(), "mail.tmpl"), metadata);
-							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);
-						} else {
-							JOptionPane.showMessageDialog(GitblitAuthority.this, "Sorry, the mail server settings are not configured properly.\nCan not send email.", Translation.get("gb.error"), JOptionPane.ERROR_MESSAGE);
-						}
-					} catch (Exception e) {
-						Utils.showException(GitblitAuthority.this, e);
-					}
+					sendEmail(user, metadata, zip);
 				}
 			}
 			
@@ -527,7 +500,7 @@
 		
 		certificateDefaultsButton = new JButton(new ImageIcon(getClass().getResource("/settings_16x16.png")));
 		certificateDefaultsButton.setFocusable(false);
-		certificateDefaultsButton.setToolTipText(Translation.get("gb.certificateDefaults"));		
+		certificateDefaultsButton.setToolTipText(Translation.get("gb.newCertificateDefaults"));		
 		certificateDefaultsButton.addActionListener(new ActionListener() {
 			@Override
 			public void actionPerformed(ActionEvent e) {
@@ -570,7 +543,7 @@
 				panel.add(oids, BorderLayout.CENTER);
 
 				int result = JOptionPane.showConfirmDialog(GitblitAuthority.this, 
-						panel, Translation.get("gb.certificateDefaults"), JOptionPane.OK_CANCEL_OPTION,
+						panel, Translation.get("gb.newCertificateDefaults"), JOptionPane.OK_CANCEL_OPTION,
 						JOptionPane.QUESTION_MESSAGE, new ImageIcon(getClass().getResource("/settings_32x32.png")));
 				if (result == JOptionPane.OK_OPTION) {
 					try {
@@ -587,33 +560,94 @@
 			}
 		});
 		
-		JButton newWebCertificate = new JButton(new ImageIcon(getClass().getResource("/rosette_16x16.png")));
-		newWebCertificate.setFocusable(false);
-		newWebCertificate.setToolTipText(Translation.get("gb.newWebCertificate"));		
-		newWebCertificate.addActionListener(new ActionListener() {
+		JButton newSSLCertificate = new JButton(new ImageIcon(getClass().getResource("/rosette_16x16.png")));
+		newSSLCertificate.setFocusable(false);
+		newSSLCertificate.setToolTipText(Translation.get("gb.newSSLCertificate"));		
+		newSSLCertificate.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);
+				NewSSLCertificateDialog dialog = new NewSSLCertificateDialog(GitblitAuthority.this, defaultExpiration);
 				dialog.setModal(true);
 				dialog.setVisible(true);
 				if (dialog.isCanceled()) {
 					return;
 				}
-				prepareX509Infrastructure();
-				Date expires = dialog.getExpiration();
-				String hostname = dialog.getHostname();
+				final Date expires = dialog.getExpiration();
+				final String hostname = dialog.getHostname();
+
+				AuthorityWorker worker = new AuthorityWorker(GitblitAuthority.this) {
+
+					@Override
+					protected Boolean doRequest() throws IOException {
+						prepareX509Infrastructure();
+						
+						// 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);
+						X509Certificate cert = X509Utils.newSSLCertificate(metadata, caPrivateKey, caCert, serverKeystoreFile, GitblitAuthority.this);
+						return cert != null;
+					}
+
+					@Override
+					protected void onSuccess() {
+						JOptionPane.showMessageDialog(GitblitAuthority.this, 
+								MessageFormat.format(Translation.get("gb.sslCertificateGenerated"), hostname),
+								Translation.get("gb.newSSLCertificate"), JOptionPane.INFORMATION_MESSAGE);
+					}
+				};
 				
-				// 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);
+				worker.execute();
+			}
+		});
+		
+		JButton emailBundle = new JButton(new ImageIcon(getClass().getResource("/mail_16x16.png")));
+		emailBundle.setFocusable(false);
+		emailBundle.setToolTipText(Translation.get("gb.emailCertificateBundle"));		
+		emailBundle.addActionListener(new ActionListener() {
+			@Override
+			public void actionPerformed(ActionEvent e) {
+				int row = table.getSelectedRow();
+				if (row < 0) {
+					return;
+				}
+				int modelIndex = table.convertRowIndexToModel(row);
+				final UserCertificateModel ucm = tableModel.get(modelIndex);
+				if (ArrayUtils.isEmpty(ucm.certs)) {
+					JOptionPane.showMessageDialog(GitblitAuthority.this, MessageFormat.format(Translation.get("gb.pleaseGenerateClientCertificate"), ucm.user.getDisplayName()));
+				}
+				final File zip = new File(folder, X509Utils.CERTS + File.separator + ucm.user.username + File.separator + ucm.user.username + ".zip");
+				if (!zip.exists()) {
+					return;
+				}
 				
-				// 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);
+				AuthorityWorker worker = new AuthorityWorker(GitblitAuthority.this) {
+					@Override
+					protected Boolean doRequest() throws IOException {
+						X509Metadata metadata = new X509Metadata(ucm.user.username, "whocares");
+						metadata.serverHostname = gitblitSettings.getString(Keys.web.siteName, Constants.NAME);
+						if (StringUtils.isEmpty(metadata.serverHostname)) {
+							metadata.serverHostname = Constants.NAME;
+						}
+						metadata.userDisplayname = ucm.user.getDisplayName();
+						sendEmail(ucm.user, metadata, zip);
+						return true;
+					}
+
+					@Override
+					protected void onSuccess() {
+						JOptionPane.showMessageDialog(GitblitAuthority.this, MessageFormat.format(Translation.get("gb.clientCertificateBundleSent"),
+								ucm.user.getDisplayName()));
+					}
+					
+				};
+				worker.execute();				
 			}
 		});
 		
@@ -631,7 +665,8 @@
 		
 		JPanel buttonControls = new JPanel(new FlowLayout(FlowLayout.LEFT, Utils.MARGIN, Utils.MARGIN));
 		buttonControls.add(certificateDefaultsButton);
-		buttonControls.add(newWebCertificate);
+		buttonControls.add(newSSLCertificate);
+		buttonControls.add(emailBundle);
 
 		JPanel userControls = new JPanel(new FlowLayout(FlowLayout.RIGHT, Utils.MARGIN, Utils.MARGIN));
 		userControls.add(new JLabel(Translation.get("gb.filter")));
@@ -708,4 +743,39 @@
 			}
 		}
 	}
+	
+	private void sendEmail(UserModel user, X509Metadata metadata, File zip) {
+		// send email
+		try {
+			if (mail.isReady()) {
+				Message message = mail.createMessage(user.emailAddress);
+				message.setSubject("Your Gitblit client certificate for " + metadata.serverHostname);
+
+				// body of email
+				String body = X509Utils.processTemplate(new File(folder, X509Utils.CERTS + File.separator + "mail.tmpl"), metadata);
+				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);
+			} else {
+				JOptionPane.showMessageDialog(GitblitAuthority.this, "Sorry, the mail server settings are not configured properly.\nCan not send email.", Translation.get("gb.error"), JOptionPane.ERROR_MESSAGE);
+			}
+		} catch (Exception e) {
+			Utils.showException(GitblitAuthority.this, e);
+		}
+	}
 }
diff --git a/src/com/gitblit/authority/NewClientCertificateDialog.java b/src/com/gitblit/authority/NewClientCertificateDialog.java
index b04ae73..35fac5a 100644
--- a/src/com/gitblit/authority/NewClientCertificateDialog.java
+++ b/src/com/gitblit/authority/NewClientCertificateDialog.java
@@ -16,6 +16,7 @@
 package com.gitblit.authority;
 
 import java.awt.BorderLayout;
+import java.awt.Dimension;
 import java.awt.Frame;
 import java.awt.GridLayout;
 import java.awt.Insets;
@@ -30,6 +31,8 @@
 import javax.swing.JOptionPane;
 import javax.swing.JPanel;
 import javax.swing.JPasswordField;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
 import javax.swing.JTextField;
 
 import org.bouncycastle.util.Arrays;
@@ -64,7 +67,6 @@
 				return Utils.INSETS;
 			}
 		};
-		content.add(new HeaderPanel(Translation.get("gb.newCertificate") + " (" + displayname + ")", "rosette_16x16.png"), BorderLayout.NORTH);
 		
 		expirationDate = new JDateChooser(defaultExpiration);
 		pw1 = new JPasswordField(20);
@@ -91,7 +93,6 @@
 			panel.add(sendEmail);
 		}
 
-		content.add(panel, BorderLayout.CENTER);
 		
 		JButton ok = new JButton(Translation.get("gb.ok"));
 		ok.addActionListener(new ActionListener() {
@@ -114,8 +115,17 @@
 		controls.add(ok);
 		controls.add(cancel);
 		
+		JTextArea message = new JTextArea(Translation.get("gb.newClientCertificateMessage"));
+		message.setLineWrap(true);
+		message.setWrapStyleWord(true);
+		message.setEditable(false);
+		message.setPreferredSize(new Dimension(300, 100));
+		
+		content.add(new JScrollPane(message), BorderLayout.CENTER);
+		content.add(panel, BorderLayout.NORTH);
 		content.add(controls, BorderLayout.SOUTH);
 		
+		getContentPane().add(new HeaderPanel(Translation.get("gb.newCertificate") + " (" + displayname + ")", "rosette_16x16.png"), BorderLayout.NORTH);
 		getContentPane().add(content, BorderLayout.CENTER);
 		pack();
 		
diff --git a/src/com/gitblit/authority/NewWebCertificateDialog.java b/src/com/gitblit/authority/NewSSLCertificateDialog.java
similarity index 91%
rename from src/com/gitblit/authority/NewWebCertificateDialog.java
rename to src/com/gitblit/authority/NewSSLCertificateDialog.java
index a6846c5..1ff542a 100644
--- a/src/com/gitblit/authority/NewWebCertificateDialog.java
+++ b/src/com/gitblit/authority/NewSSLCertificateDialog.java
@@ -35,7 +35,7 @@
 import com.gitblit.utils.StringUtils;
 import com.toedter.calendar.JDateChooser;
 
-public class NewWebCertificateDialog extends JDialog {
+public class NewSSLCertificateDialog extends JDialog {
 
 	private static final long serialVersionUID = 1L;
 	
@@ -43,10 +43,10 @@
 	JTextField hostname;
 	boolean isCanceled = true;
 
-	public NewWebCertificateDialog(Frame owner, Date defaultExpiration) {
+	public NewSSLCertificateDialog(Frame owner, Date defaultExpiration) {
 		super(owner);
 		
-		setTitle(Translation.get("gb.newWebCertificate"));
+		setTitle(Translation.get("gb.newSSLCertificate"));
 		
 		JPanel content = new JPanel(new BorderLayout(Utils.MARGIN, Utils.MARGIN)) {			
 			private static final long serialVersionUID = 1L;
@@ -57,7 +57,6 @@
 				return Utils.INSETS;
 			}
 		};
-		content.add(new HeaderPanel(Translation.get("gb.newWebCertificate"), "rosette_16x16.png"), BorderLayout.NORTH);
 		
 		expirationDate = new JDateChooser(defaultExpiration);
 		hostname = new JTextField(20);
@@ -69,8 +68,6 @@
 
 		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() {
@@ -92,9 +89,11 @@
 		JPanel controls = new JPanel();
 		controls.add(ok);
 		controls.add(cancel);
-		
+
+		content.add(panel, BorderLayout.CENTER);
 		content.add(controls, BorderLayout.SOUTH);
 		
+		getContentPane().add(new HeaderPanel(Translation.get("gb.newSSLCertificate"), "rosette_16x16.png"), BorderLayout.NORTH);
 		getContentPane().add(content, BorderLayout.CENTER);
 		pack();
 		
diff --git a/src/com/gitblit/authority/UserCertificateConfig.java b/src/com/gitblit/authority/UserCertificateConfig.java
index 47132a0..5ec76f7 100644
--- a/src/com/gitblit/authority/UserCertificateConfig.java
+++ b/src/com/gitblit/authority/UserCertificateConfig.java
@@ -51,6 +51,7 @@
 				uc.expires = df.parse(c.getString("user", username, "expires"));
 			} catch (ParseException e) {
 				LoggerFactory.getLogger(UserCertificateConfig.class).error("Failed to parse date!", e);
+			} catch (NullPointerException e) { 
 			}
 			uc.notes = c.getString("user", username, "notes");
 			uc.revoked = new ArrayList<String>(Arrays.asList(c.getStringList("user", username, "revoked")));			
diff --git a/src/com/gitblit/authority/UserCertificateModel.java b/src/com/gitblit/authority/UserCertificateModel.java
index f5d71bb..6c69a93 100644
--- a/src/com/gitblit/authority/UserCertificateModel.java
+++ b/src/com/gitblit/authority/UserCertificateModel.java
@@ -27,6 +27,7 @@
 import com.gitblit.Constants;
 import com.gitblit.models.UserModel;
 import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.StringUtils;
 import com.gitblit.utils.TimeUtils;
 import com.gitblit.utils.X509Utils.RevocationReason;
 
@@ -42,14 +43,20 @@
 		}
 		
 		public void update(Config config) {
-			if (expires != null) {
+			if (expires == null) {
+				config.unset("user",  user.username, "expires");
+			} else {
 				SimpleDateFormat df = new SimpleDateFormat(Constants.ISO8601);
 				config.setString("user", user.username, "expires", df.format(expires));
 			}
-			if (notes != null) {
+			if (StringUtils.isEmpty(notes)) {
+				config.unset("user",  user.username, "notes");
+			} else {
 				config.setString("user", user.username, "notes", notes);
 			}
-			if (!ArrayUtils.isEmpty(revoked)) {
+			if (ArrayUtils.isEmpty(revoked)) {
+				config.unset("user",  user.username, "revoked");
+			} else {
 				config.setStringList("user", user.username, "revoked", revoked);
 			}
 		}
@@ -64,6 +71,16 @@
 				revoked = new ArrayList<String>();
 			}
 			revoked.add(serial.toString() + ":" + reason.ordinal());
+			expires = null;
+			for (X509Certificate cert : certs) {
+				if (!isRevoked(cert.getSerialNumber())) {
+					if (!isExpired(cert.getNotAfter())) {
+						if (expires == null || cert.getNotAfter().after(expires)) {
+							expires = cert.getNotAfter();
+						}
+					}
+				}
+			}
 		}
 		
 		public boolean isRevoked(BigInteger serial) {
diff --git a/src/com/gitblit/authority/UserCertificatePanel.java b/src/com/gitblit/authority/UserCertificatePanel.java
index 79c0d94..8a60f47 100644
--- a/src/com/gitblit/authority/UserCertificatePanel.java
+++ b/src/com/gitblit/authority/UserCertificatePanel.java
@@ -16,14 +16,15 @@
 package com.gitblit.authority;
 
 import java.awt.BorderLayout;
-import java.awt.Cursor;
 import java.awt.FlowLayout;
 import java.awt.Frame;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
+import java.io.IOException;
 import java.security.cert.X509Certificate;
+import java.text.MessageFormat;
 import java.util.Date;
 
 import javax.swing.ImageIcon;
@@ -156,20 +157,32 @@
 					if (dialog.isCanceled()) {
 						return;
 					}
-					
-					setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
-					UserModel user = ucm.user;
-					X509Metadata metadata = new X509Metadata(user.username, dialog.getPassword());
+
+					final boolean sendEmail = dialog.sendEmail();
+					final UserModel user = ucm.user;
+					final X509Metadata metadata = new X509Metadata(user.username, dialog.getPassword());
 					metadata.userDisplayname = user.getDisplayName();
 					metadata.emailAddress = user.emailAddress;				
 					metadata.passwordHint = dialog.getPasswordHint();
 					metadata.notAfter = dialog.getExpiration();
 
-					newCertificate(ucm, metadata, dialog.sendEmail());
+					AuthorityWorker worker = new AuthorityWorker(UserCertificatePanel.this.owner) {
+						@Override
+						protected Boolean doRequest() throws IOException {
+							newCertificate(ucm, metadata, sendEmail);
+							return true;
+						}
+
+						@Override
+						protected void onSuccess() {
+							JOptionPane.showMessageDialog(UserCertificatePanel.this.owner, 
+									MessageFormat.format(Translation.get("gb.clientCertificateGenerated"), user.getDisplayName()),
+									Translation.get("gb.newCertificate"), JOptionPane.INFORMATION_MESSAGE);
+						}
+					};
+					worker.execute();					
 				} catch (Exception x) {
 					Utils.showException(UserCertificatePanel.this, x);
-				} finally {
-					setCursor(Cursor.getDefaultCursor());
 				}
 			}
 		});
@@ -184,7 +197,7 @@
 						return;
 					}
 					int modelIndex = table.convertRowIndexToModel(row);
-					X509Certificate cert = tableModel.get(modelIndex);
+					final X509Certificate cert = tableModel.get(modelIndex);
 					
 					String [] choices = new String[RevocationReason.reasons.length];
 					for (int i = 0; i < choices.length; i++) {
@@ -197,13 +210,14 @@
 					if (choice == null) {
 						return;
 					}
-					RevocationReason reason = RevocationReason.unspecified;
+					RevocationReason selection = RevocationReason.unspecified;
 					for (int i = 0 ; i < choices.length; i++) {
 						if (choices[i].equals(choice)) {
-							reason = RevocationReason.reasons[i];
+							selection = RevocationReason.reasons[i];
 							break;
 						}
 					}
+					final RevocationReason reason = selection;
 					if (!ucm.isRevoked(cert.getSerialNumber())) {
 						if (ucm.certs.size() == 1) {
 							// no other certificates
@@ -222,12 +236,27 @@
 							}
 							ucm.expires = newExpires;
 						}
-						revoke(ucm, cert, reason);
+						
+						AuthorityWorker worker = new AuthorityWorker(UserCertificatePanel.this.owner) {
+
+							@Override
+							protected Boolean doRequest() throws IOException {
+								revoke(ucm, cert, reason);
+								return true;
+							}
+
+							@Override
+							protected void onSuccess() {
+								JOptionPane.showMessageDialog(UserCertificatePanel.this.owner, 
+										MessageFormat.format(Translation.get("gb.certificateRevoked"), cert.getSerialNumber(), cert.getIssuerDN().getName()),
+										Translation.get("gb.revokeCertificate"), JOptionPane.INFORMATION_MESSAGE);
+							}
+							
+						};
+						worker.execute();
 					}
 				} catch (Exception x) {
 					Utils.showException(UserCertificatePanel.this, x);
-				} finally {
-					setCursor(Cursor.getDefaultCursor());
 				}
 			}
 		});
diff --git a/src/com/gitblit/authority/X509CertificateViewer.java b/src/com/gitblit/authority/X509CertificateViewer.java
index 1b09515..797b9a8 100644
--- a/src/com/gitblit/authority/X509CertificateViewer.java
+++ b/src/com/gitblit/authority/X509CertificateViewer.java
@@ -56,7 +56,6 @@
 				return Utils.INSETS;
 			}
 		};
-		content.add(new HeaderPanel("certificiate", "rosette_16x16.png"), BorderLayout.NORTH);
 		
 		DateFormat df = DateFormat.getDateTimeInstance();
 		
@@ -96,6 +95,7 @@
 		
 		content.add(controls, BorderLayout.SOUTH);
 		
+		getContentPane().add(new HeaderPanel(Translation.get("gb.certificate"), "rosette_16x16.png"), BorderLayout.NORTH);
 		getContentPane().add(content, BorderLayout.CENTER);
 		pack();
 		
diff --git a/src/com/gitblit/utils/X509Utils.java b/src/com/gitblit/utils/X509Utils.java
index 1510b2c..24afb8d 100644
--- a/src/com/gitblit/utils/X509Utils.java
+++ b/src/com/gitblit/utils/X509Utils.java
@@ -561,7 +561,7 @@
 					new Certificate[] { cert, caCert });
 			saveKeyStore(targetStoreFile, serverStore, sslMetadata.password);
 			
-	        x509log.log(MessageFormat.format("New web certificate {0,number,0} [{1}]", cert.getSerialNumber(), cert.getSubjectDN().getName()));
+	        x509log.log(MessageFormat.format("New SSL certificate {0,number,0} [{1}]", cert.getSerialNumber(), cert.getSubjectDN().getName()));
 			return cert;
 		} catch (Throwable t) {
 			throw new RuntimeException("Failed to generate SSL certificate!", t);
@@ -935,10 +935,18 @@
 			String message = FileUtils.readContent(template, "\n");
 			if (!StringUtils.isEmpty(message)) {
 				content = message;
-				content = content.replace("$serverHostname", metadata.serverHostname);
-				content = content.replace("$username", metadata.commonName);
-				content = content.replace("$userDisplayname", metadata.userDisplayname);
-				content = content.replace("$storePasswordHint", metadata.passwordHint);
+				if (!StringUtils.isEmpty(metadata.serverHostname)) {
+					content = content.replace("$serverHostname", metadata.serverHostname);
+				}
+				if (!StringUtils.isEmpty(metadata.commonName)) {
+					content = content.replace("$username", metadata.commonName);
+				}
+				if (!StringUtils.isEmpty(metadata.userDisplayname)) {
+					content = content.replace("$userDisplayname", metadata.userDisplayname);
+				}
+				if (!StringUtils.isEmpty(metadata.passwordHint)) {
+					content = content.replace("$storePasswordHint", metadata.passwordHint);
+				}
 			}
 		}
 		return content;
diff --git a/src/com/gitblit/wicket/GitBlitWebApp.properties b/src/com/gitblit/wicket/GitBlitWebApp.properties
index 1a1ee17..2de266e 100644
--- a/src/com/gitblit/wicket/GitBlitWebApp.properties
+++ b/src/com/gitblit/wicket/GitBlitWebApp.properties
@@ -417,6 +417,14 @@
 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
+gb.newSSLCertificate = new server SSL certificate
+gb.newCertificateDefaults = new certificate defaults
+gb.duration = duration
+gb.certificateRevoked = Certificate {0,number,0} has been revoked
+gb.clientCertificateGenerated = Successfully generated new client certificate for {0}
+gb.sslCertificateGenerated = Successfully generated new server SSL certificate for {0}
+gb.newClientCertificateMessage = NOTE:\nThe 'password' is not the user's password, it is the password to protect the user's keystore.  This password is not saved so you must also enter a 'hint' which will be included in the user's README instructions.
+gb.certificate = certificate
+gb.emailCertificateBundle = email client certificate bundle
+gb.pleaseGenerateClientCertificate = Please generate a client certificate for {0}
+gb.clientCertificateBundleSent = Client certificate bundle for {0} sent
\ No newline at end of file

--
Gitblit v1.9.1