From e8c417f4e63f84ac6e14f6d5540dcb1f0f9862fc Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Fri, 23 Nov 2012 10:46:35 -0500
Subject: [PATCH] Command-line tool to generate client certificate bundles for existing users

---
 src/com/gitblit/authority/NewCertificateConfig.java  |   72 ++++++
 distrib/makeclientcertificate.sh                     |    2 
 distrib/makeclientcertificate.cmd                    |    1 
 src/com/gitblit/authority/UserCertificateModel.java  |  134 +++++++++++
 src/com/gitblit/ConfigUserService.java               |   30 ++
 distrib/mail.tmpl                                    |    7 
 src/com/gitblit/MailExecutor.java                    |    4 
 src/com/gitblit/authority/UserCertificateConfig.java |   69 +++++
 build.xml                                            |    2 
 src/com/gitblit/authority/MakeClientCertificate.java |  230 +++++++++++++++++++
 distrib/instructions.tmpl                            |  123 ++++++++++
 src/com/gitblit/models/UserModel.java                |    5 
 12 files changed, 679 insertions(+), 0 deletions(-)

diff --git a/build.xml b/build.xml
index 4e73519..7c5e8cc 100644
--- a/build.xml
+++ b/build.xml
@@ -183,6 +183,7 @@
 				<exclude name="federation.properties" />
 				<exclude name="openshift.mkd" />
 				<exclude name="authority.conf" />
+				<exclude name="*.tmpl" />
 			</fileset>
 			<fileset dir="${basedir}">
 				<include name="LICENSE" />
@@ -195,6 +196,7 @@
 		<mkdir dir="${project.deploy.dir}/certs"/>
 		<copy todir="${project.deploy.dir}/certs">
 			<fileset dir="${basedir}/distrib">
+				<include name="*.tmpl" />
 				<include name="authority.conf" />
 			</fileset>
 		</copy>
diff --git a/distrib/instructions.tmpl b/distrib/instructions.tmpl
new file mode 100644
index 0000000..11ea78f
--- /dev/null
+++ b/distrib/instructions.tmpl
@@ -0,0 +1,123 @@
+********************************************************************************
+ Gitblit SSL Client Certificate for $serverHostname
+********************************************************************************
+
+ Hello $userDisplayname,
+
+ Your private key, public certificate, and the Gitblit Certificate Authority 
+ certificate for $serverHostname are stored in $username.p12, a PKCS#12 certificate
+ store[1], and also in $username.pem, a PEM certificate store.
+
+ Both of these certificate stores are password-protected. 
+ Password Hint: $storePasswordHint
+
+
+Git (All) Installation Instructions
+=============================================
+
+ The provided PEM file can be directly used by your git client.
+ 
+    git config [--global] http.sslCert path/to/$username.pem
+    
+ The supplied PEM file is password-protected and you may be prompted for your
+ password multiple times during an exchange with Gitblit.  If you desire a
+ password-less git client workflow then you will need to decrypt and export your
+ private key with OpenSSL[2] and then update your git config to use that key.
+ 
+    openssl rsa -in path/to/$username.pem -out path/to/$username.key    
+    git config [--global] http.sslKey path/to/$username.key
+
+ Obviously, you should protect access to any decrypted private key.
+ 
+ NOTE:
+ Some older git clients may have trouble using the PEM file without explicitly
+ extracting the private key.  This has been observed, for example, on Ubuntu 12.04
+ with git 1.7.9.5.
+
+
+Firefox (All) Installation Instructions
+=============================================
+
+ Firefox maintains it's own certificate store which is separate from the operating
+ system.
+
+ 1. Navigate to Options->Advanced->Encryption
+ 2. Click "View Certificates"
+ 3. Switch to the "Your Certificates" tab
+ 4. Click "Import..."
+ 5. Navigate your filesystem and select $username.p12
+ 6. At the password prompt enter the certificate store password
+    You have now imported your private key, public certificate, and the CA certificate
+    but now we must manually set the trust settings of the CA certificate.
+ 7. Switch to the "Authorities" tab
+ 8. Scroll down and find "Gitblit-> Gitblit Certificate Authority"
+ 9. Select it and click "Edit Trust..."
+ 10. Check "This certificate can identify websites" and click OK.
+
+
+Chrome/IE (Windows) Installation Instructions
+=============================================
+
+ On Windows, Chrome and IE share their certificate store so configuring one will
+ automatically apply for both.
+
+ IE
+ ------------------------------------
+ 1. Navigate to Internet Options->Content
+ 2. Click the "Certificates" button
+
+ Chrome
+ ------------------------------------
+ 1. Navigate to Settings->Show Advanced Settings->HTTP/SSL
+ 2. Click the "Manage Certificates..." button
+
+ Both (Windows)
+ ------------------------------------
+ 3. Switch to the "Personal" tab
+ 4. Click the "Import..." button
+ 5. Follow the Import Wizard instructions.
+    You will need to change the selected file filter when navigating to $username.p12
+ 6. At the password prompt enter the certificate store password
+ 7. Because both your personal certificate and the CA certifcate are stored in 
+    $username.p12, you must choose "Automatically select the certificate store based on the type of certificate".
+    If you choose the default you will not install the CA certificate.
+
+
+Chrome (Linux) Installation Instructions
+=============================================
+ 
+ On Linux, Chrome maintains it's own certificate store.
+ 
+ 1. Navigate to Settings->Show Advanced Settings->HTTP/SSL
+ 2. Click the "Manage Certificates..." button
+ 3. Navigate your filesystem and select $username.p12
+ 4. At the password prompt enter the certificate store password
+    You have now imported your private key, public certificate, and the CA certificate
+    but now we must manually set the trust settings of the CA certificate.
+ 5. Switch to the "Authorities" tab
+ 6. Scroll down and find "Gitblit-> Gitblit Certificate Authority"
+ 7. Select it and click "Edit Trust..."
+ 8. Check "This certificate can identify websites" and click OK.
+
+
+Chrome/Safari (Mac OS X) Installation Instructions
+=============================================
+
+On Mac OS X, Chrome and Safari both use Keychain Access to store certificates
+so configuring one will automatically apply for both.
+
+ 1. Double-click $username.pem
+ 2. At the password prompt enter the certificate store password
+    You have now imported your private key, public certificate, and the CA certificate
+    but now we must manually set the trust settings of the CA certificate.
+ 3. Find the Gitblit Certificate Authority certificate, it should have a red
+    indicator meaning untrusted, and double-click it.
+ 4. Open the "Trust" disclosure triangle and change "When using this certificate"
+    to "Always Trust".
+ 5. Close the certificate view and enter your system password to save the changes
+    to your keychain. 
+  
+    
+[1] PKCS#12 is one of the standard container formats for sharing private keys and
+    public certificates.
+[2] http://www.openssl.org
diff --git a/distrib/mail.tmpl b/distrib/mail.tmpl
new file mode 100644
index 0000000..463e124
--- /dev/null
+++ b/distrib/mail.tmpl
@@ -0,0 +1,7 @@
+ Hello $userDisplayname,
+
+ Your private key, public certificate, and the Gitblit Certificate Authority 
+ certificate for $serverHostname are bundled together in the attached zip file.
+
+ There are also setup/installation instructions included in the zip for Git and
+ several major browsers to get you started.
\ No newline at end of file
diff --git a/distrib/makeclientcertificate.cmd b/distrib/makeclientcertificate.cmd
new file mode 100644
index 0000000..7294142
--- /dev/null
+++ b/distrib/makeclientcertificate.cmd
@@ -0,0 +1 @@
+@java -cp gitblit.jar;"%CD%\ext\*" com.gitblit.authority.MakeClientCertificate
diff --git a/distrib/makeclientcertificate.sh b/distrib/makeclientcertificate.sh
new file mode 100644
index 0000000..76a195e
--- /dev/null
+++ b/distrib/makeclientcertificate.sh
@@ -0,0 +1,2 @@
+#!/bin/sh
+java -cp gitblit.jar:$PWD/ext/* com.gitblit.authority.MakeClientCertificate
diff --git a/src/com/gitblit/ConfigUserService.java b/src/com/gitblit/ConfigUserService.java
index 9ad805b..068bbe3 100644
--- a/src/com/gitblit/ConfigUserService.java
+++ b/src/com/gitblit/ConfigUserService.java
@@ -66,6 +66,16 @@
 	
 	private static final String EMAILADDRESS = "emailAddress";
 	
+	private static final String ORGANIZATIONALUNIT = "organizationalUnit";
+	
+	private static final String ORGANIZATION = "organization";
+	
+	private static final String LOCALITY = "locality";
+	
+	private static final String STATEPROVINCE = "stateProvince";
+	
+	private static final String COUNTRYCODE = "countryCode";
+	
 	private static final String COOKIE = "cookie";
 
 	private static final String REPOSITORY = "repository";
@@ -817,6 +827,21 @@
 			if (!StringUtils.isEmpty(model.emailAddress)) {
 				config.setString(USER, model.username, EMAILADDRESS, model.emailAddress);
 			}
+			if (!StringUtils.isEmpty(model.organizationalUnit)) {
+				config.setString(USER, model.username, ORGANIZATIONALUNIT, model.organizationalUnit);
+			}
+			if (!StringUtils.isEmpty(model.organization)) {
+				config.setString(USER, model.username, ORGANIZATION, model.organization);
+			}
+			if (!StringUtils.isEmpty(model.locality)) {
+				config.setString(USER, model.username, LOCALITY, model.locality);
+			}
+			if (!StringUtils.isEmpty(model.stateProvince)) {
+				config.setString(USER, model.username, STATEPROVINCE, model.stateProvince);
+			}
+			if (!StringUtils.isEmpty(model.countryCode)) {
+				config.setString(USER, model.username, COUNTRYCODE, model.countryCode);
+			}
 
 			// user roles
 			List<String> roles = new ArrayList<String>();
@@ -964,6 +989,11 @@
 					user.password = config.getString(USER, username, PASSWORD);					
 					user.displayName = config.getString(USER, username, DISPLAYNAME);
 					user.emailAddress = config.getString(USER, username, EMAILADDRESS);
+					user.organizationalUnit = config.getString(USER, username, ORGANIZATIONALUNIT);
+					user.organization = config.getString(USER, username, ORGANIZATION);
+					user.locality = config.getString(USER, username, LOCALITY);
+					user.stateProvince = config.getString(USER, username, STATEPROVINCE);
+					user.countryCode = config.getString(USER, username, COUNTRYCODE);
 					user.cookie = config.getString(USER, username, COOKIE);
 					if (StringUtils.isEmpty(user.cookie) && !StringUtils.isEmpty(user.password)) {
 						user.cookie = StringUtils.getSHA1(user.username + user.password);
diff --git a/src/com/gitblit/MailExecutor.java b/src/com/gitblit/MailExecutor.java
index ea19edb..9001e83 100644
--- a/src/com/gitblit/MailExecutor.java
+++ b/src/com/gitblit/MailExecutor.java
@@ -231,4 +231,8 @@
 			}
 		}
 	}
+	
+	public void sendNow(Message message) throws Exception {
+		Transport.send(message);
+	}
 }
diff --git a/src/com/gitblit/authority/MakeClientCertificate.java b/src/com/gitblit/authority/MakeClientCertificate.java
new file mode 100644
index 0000000..5829fc1
--- /dev/null
+++ b/src/com/gitblit/authority/MakeClientCertificate.java
@@ -0,0 +1,230 @@
+/*
+ * 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
new file mode 100644
index 0000000..e4db130
--- /dev/null
+++ b/src/com/gitblit/authority/NewCertificateConfig.java
@@ -0,0 +1,72 @@
+/*
+ * 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.util.Date;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Config.SectionParser;
+
+import com.gitblit.utils.StringUtils;
+import com.gitblit.utils.TimeUtils;
+import com.gitblit.utils.X509Utils.X509Metadata;
+
+/**
+ * Certificate config file parser.
+ *  
+ * @author James Moger
+ */
+public class NewCertificateConfig {
+		public static final SectionParser<NewCertificateConfig> KEY = new SectionParser<NewCertificateConfig>() {
+			public NewCertificateConfig parse(final Config cfg) {
+				return new NewCertificateConfig(cfg);
+			}
+		};
+
+		public final String OU;
+		public final String O;
+		public final String L;
+		public final String ST;
+		public final String C;
+		
+		public final int duration;
+		
+		private NewCertificateConfig(final Config c) {
+			duration = c.getInt("new",  null, "duration", 0);
+			OU = c.getString("new", null, "organizationalUnit");
+			O = c.getString("new", null, "organization");
+			L = c.getString("new", null, "locality");
+			ST = c.getString("new", null, "stateProvince");
+			C = c.getString("new", null, "countryCode");			
+		}
+		
+		public void update(X509Metadata metadata) {
+			update(metadata, "OU", OU);
+			update(metadata, "O", O);
+			update(metadata, "L", L);
+			update(metadata, "ST", ST);
+			update(metadata, "C", C);
+			if (duration > 0) {
+				metadata.notAfter = new Date(System.currentTimeMillis() + duration*TimeUtils.ONEDAY);
+			}
+		}
+		
+		private void update(X509Metadata metadata, String oid, String value) {
+			if (!StringUtils.isEmpty(value)) {
+				metadata.oids.put(oid, value);
+			}
+		}
+	}
\ No newline at end of file
diff --git a/src/com/gitblit/authority/UserCertificateConfig.java b/src/com/gitblit/authority/UserCertificateConfig.java
new file mode 100644
index 0000000..47132a0
--- /dev/null
+++ b/src/com/gitblit/authority/UserCertificateConfig.java
@@ -0,0 +1,69 @@
+/*
+ * 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.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Config.SectionParser;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.Constants;
+import com.gitblit.models.UserModel;
+
+/**
+ * User certificate config section parser.
+ * 
+ * @author James Moger
+ */
+public class UserCertificateConfig {
+	public static final SectionParser<UserCertificateConfig> KEY = new SectionParser<UserCertificateConfig>() {
+		public UserCertificateConfig parse(final Config cfg) {			
+			return new UserCertificateConfig(cfg);
+		}
+	};
+	
+	public final List<UserCertificateModel> list;
+
+	private UserCertificateConfig(final Config c) {
+		SimpleDateFormat df = new SimpleDateFormat(Constants.ISO8601);
+		list = new ArrayList<UserCertificateModel>(); 
+		for (String username : c.getSubsections("user")) {
+			UserCertificateModel uc = new UserCertificateModel(new UserModel(username));
+			try {
+				uc.expires = df.parse(c.getString("user", username, "expires"));
+			} catch (ParseException e) {
+				LoggerFactory.getLogger(UserCertificateConfig.class).error("Failed to parse date!", e);
+			}
+			uc.notes = c.getString("user", username, "notes");
+			uc.revoked = new ArrayList<String>(Arrays.asList(c.getStringList("user", username, "revoked")));			
+			list.add(uc);
+		}
+	}
+	
+	public UserCertificateModel getUserCertificateModel(String username) {
+		for (UserCertificateModel ucm : list) {
+			if (ucm.user.username.equalsIgnoreCase(username)) {
+				return ucm;
+			}
+		}
+		return null;
+	}
+}
\ No newline at end of file
diff --git a/src/com/gitblit/authority/UserCertificateModel.java b/src/com/gitblit/authority/UserCertificateModel.java
new file mode 100644
index 0000000..f5d71bb
--- /dev/null
+++ b/src/com/gitblit/authority/UserCertificateModel.java
@@ -0,0 +1,134 @@
+/*
+ * 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.math.BigInteger;
+import java.security.cert.X509Certificate;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import org.eclipse.jgit.lib.Config;
+
+import com.gitblit.Constants;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.TimeUtils;
+import com.gitblit.utils.X509Utils.RevocationReason;
+
+public class UserCertificateModel implements Comparable<UserCertificateModel> {
+		public UserModel user;
+		public Date expires;
+		public List<X509Certificate> certs;
+		public List<String> revoked;
+		public String notes;
+
+		public UserCertificateModel(UserModel user) {
+			this.user = user;
+		}
+		
+		public void update(Config config) {
+			if (expires != null) {
+				SimpleDateFormat df = new SimpleDateFormat(Constants.ISO8601);
+				config.setString("user", user.username, "expires", df.format(expires));
+			}
+			if (notes != null) {
+				config.setString("user", user.username, "notes", notes);
+			}
+			if (!ArrayUtils.isEmpty(revoked)) {
+				config.setStringList("user", user.username, "revoked", revoked);
+			}
+		}
+
+		@Override
+		public int compareTo(UserCertificateModel o) {
+			return user.compareTo(o.user);
+		}
+		
+		public void revoke(BigInteger serial, RevocationReason reason) {
+			if (revoked == null) {
+				revoked = new ArrayList<String>();
+			}
+			revoked.add(serial.toString() + ":" + reason.ordinal());
+		}
+		
+		public boolean isRevoked(BigInteger serial) {
+			return isRevoked(serial.toString());
+		}
+
+		public boolean isRevoked(String serial) {
+			if (ArrayUtils.isEmpty(revoked)) {
+				return false;
+			}
+			String sn = serial + ":";
+			for (String s : revoked) {
+				if (s.startsWith(sn)) {
+					return true;
+				}
+			}
+			return false;
+		}
+		
+		public RevocationReason getRevocationReason(BigInteger serial) {
+			try {
+				String sn = serial + ":";
+				for (String s : revoked) {
+					if (s.startsWith(sn)) {
+						String r = s.substring(sn.length());
+						int i = Integer.parseInt(r);
+						return RevocationReason.values()[i];
+					}
+				}
+			} catch (Exception e) {
+			}
+			return RevocationReason.unspecified;
+		}
+		
+		public CertificateStatus getStatus() {
+			if (expires == null) {
+				return CertificateStatus.unknown;
+			} else if (isExpired(expires)) {
+				return CertificateStatus.expired;
+			} else if (isExpiring(expires)) {
+				return CertificateStatus.expiring;
+			}
+			return CertificateStatus.ok;
+		}
+
+		public boolean hasExpired() {
+			return expires != null && isExpiring(expires);
+		}
+
+		public CertificateStatus getStatus(X509Certificate cert) {
+			if (isRevoked(cert.getSerialNumber())) {
+				return CertificateStatus.revoked;
+			} else if (isExpired(cert.getNotAfter())) {
+				return CertificateStatus.expired;
+			} else if (isExpiring(cert.getNotAfter())) {
+				return CertificateStatus.expiring;
+			}
+			return CertificateStatus.ok;
+		}
+		
+		private boolean isExpiring(Date date) {
+			return (date.getTime() - System.currentTimeMillis()) <= TimeUtils.ONEDAY * 30;
+		}
+		
+		private boolean isExpired(Date date) {
+			return date.getTime() < System.currentTimeMillis();
+		}
+	}
\ No newline at end of file
diff --git a/src/com/gitblit/models/UserModel.java b/src/com/gitblit/models/UserModel.java
index 1159905..bd40985 100644
--- a/src/com/gitblit/models/UserModel.java
+++ b/src/com/gitblit/models/UserModel.java
@@ -56,6 +56,11 @@
 	public String cookie;
 	public String displayName;
 	public String emailAddress;
+	public String organizationalUnit;
+	public String organization;
+	public String locality;
+	public String stateProvince;
+	public String countryCode;
 	public boolean canAdmin;
 	public boolean canFork;
 	public boolean canCreate;

--
Gitblit v1.9.1