]> source.dussan.org Git - gitblit.git/commitdiff
X509 certificate generation utilities for CA, web, and client certificates
authorJames Moger <james.moger@gitblit.com>
Fri, 23 Nov 2012 15:31:58 +0000 (10:31 -0500)
committerJames Moger <james.moger@gitblit.com>
Fri, 23 Nov 2012 15:31:58 +0000 (10:31 -0500)
src/com/gitblit/utils/FileUtils.java
src/com/gitblit/utils/X509Utils.java [new file with mode: 0644]
tests/com/gitblit/tests/X509UtilsTest.java [new file with mode: 0644]

index cba88d0ca74ce23c63165b379a9b3c1e04c017d8..0b8aeb4ade1caf5c4e5aa1e975e059851f8911b2 100644 (file)
@@ -99,6 +99,25 @@ public class FileUtils {
                }\r
                return defaultValue;\r
        }\r
+       \r
+       /**\r
+        * Returns the byte [] content of the specified file.\r
+        * \r
+        * @param file\r
+        * @return the byte content of the file\r
+        */\r
+       public static byte [] readContent(File file) {\r
+               byte [] buffer = new byte[(int) file.length()];\r
+               try {\r
+                       BufferedInputStream is = new BufferedInputStream(new FileInputStream(file));\r
+                       is.read(buffer,  0,  buffer.length);\r
+                       is.close();\r
+               } catch (Throwable t) {\r
+                       System.err.println("Failed to read byte content of " + file.getAbsolutePath());\r
+                       t.printStackTrace();\r
+               }\r
+               return buffer;\r
+       }\r
 \r
        /**\r
         * Returns the string content of the specified file.\r
diff --git a/src/com/gitblit/utils/X509Utils.java b/src/com/gitblit/utils/X509Utils.java
new file mode 100644 (file)
index 0000000..e6ffec1
--- /dev/null
@@ -0,0 +1,991 @@
+/*\r
+ * Copyright 2012 gitblit.com.\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ *     http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+package com.gitblit.utils;\r
+\r
+import java.io.BufferedWriter;\r
+import java.io.File;\r
+import java.io.FileInputStream;\r
+import java.io.FileOutputStream;\r
+import java.io.FileWriter;\r
+import java.io.IOException;\r
+import java.lang.reflect.Field;\r
+import java.math.BigInteger;\r
+import java.security.InvalidKeyException;\r
+import java.security.KeyPair;\r
+import java.security.KeyPairGenerator;\r
+import java.security.KeyStore;\r
+import java.security.NoSuchAlgorithmException;\r
+import java.security.PrivateKey;\r
+import java.security.SecureRandom;\r
+import java.security.Security;\r
+import java.security.SignatureException;\r
+import java.security.cert.CertPathBuilder;\r
+import java.security.cert.CertPathBuilderException;\r
+import java.security.cert.CertStore;\r
+import java.security.cert.Certificate;\r
+import java.security.cert.CollectionCertStoreParameters;\r
+import java.security.cert.PKIXBuilderParameters;\r
+import java.security.cert.PKIXCertPathBuilderResult;\r
+import java.security.cert.TrustAnchor;\r
+import java.security.cert.X509CRL;\r
+import java.security.cert.X509CertSelector;\r
+import java.security.cert.X509Certificate;\r
+import java.text.MessageFormat;\r
+import java.text.SimpleDateFormat;\r
+import java.util.Arrays;\r
+import java.util.Calendar;\r
+import java.util.Date;\r
+import java.util.HashMap;\r
+import java.util.HashSet;\r
+import java.util.Map;\r
+import java.util.Set;\r
+import java.util.TimeZone;\r
+import java.util.zip.ZipEntry;\r
+import java.util.zip.ZipOutputStream;\r
+\r
+import javax.crypto.Cipher;\r
+\r
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;\r
+import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;\r
+import org.bouncycastle.asn1.x500.X500Name;\r
+import org.bouncycastle.asn1.x500.X500NameBuilder;\r
+import org.bouncycastle.asn1.x500.style.BCStyle;\r
+import org.bouncycastle.asn1.x509.BasicConstraints;\r
+import org.bouncycastle.asn1.x509.GeneralName;\r
+import org.bouncycastle.asn1.x509.GeneralNames;\r
+import org.bouncycastle.asn1.x509.KeyUsage;\r
+import org.bouncycastle.asn1.x509.X509Extension;\r
+import org.bouncycastle.cert.X509CRLHolder;\r
+import org.bouncycastle.cert.X509v2CRLBuilder;\r
+import org.bouncycastle.cert.X509v3CertificateBuilder;\r
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;\r
+import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;\r
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;\r
+import org.bouncycastle.jce.PrincipalUtil;\r
+import org.bouncycastle.jce.interfaces.PKCS12BagAttributeCarrier;\r
+import org.bouncycastle.openssl.PEMWriter;\r
+import org.bouncycastle.operator.ContentSigner;\r
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;\r
+import org.slf4j.Logger;\r
+import org.slf4j.LoggerFactory;\r
+\r
+import sun.security.x509.X509CRLImpl;\r
+\r
+import com.gitblit.Constants;\r
+\r
+/**\r
+ * Utility class to generate X509 certificates, keystores, and truststores.\r
+ * \r
+ * @author James Moger\r
+ * \r
+ */\r
+public class X509Utils {\r
+       \r
+       public static final String SERVER_KEY_STORE = "serverKeyStore.jks";\r
+       \r
+       public static final String SERVER_TRUST_STORE = "serverTrustStore.jks";\r
+\r
+       public static final String CERTS = "certs";\r
+       \r
+       public static final String CA_KEY_STORE = "certs/caKeyStore.p12";\r
+\r
+       public static final String CA_REVOCATION_LIST = "certs/caRevocationList.crl";\r
+       \r
+       public static final String CA_CONFIG = "certs/authority.conf";\r
+\r
+       public static final String CA_CN = "Gitblit Certificate Authority";\r
+       \r
+       public static final String CA_FN = CA_CN;\r
+\r
+       private static final String BC = org.bouncycastle.jce.provider.BouncyCastleProvider.PROVIDER_NAME;\r
+       \r
+       public static final boolean unlimitedStrength;\r
+       \r
+       private static final Logger logger = LoggerFactory.getLogger(X509Utils.class);\r
+       \r
+       static {\r
+               Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());\r
+               \r
+               // check for JCE Unlimited Strength\r
+               int maxKeyLen = 0;\r
+               try {\r
+                       maxKeyLen = Cipher.getMaxAllowedKeyLength("AES");\r
+               } catch (NoSuchAlgorithmException e) {\r
+               }\r
+\r
+               unlimitedStrength = maxKeyLen > 128;\r
+               if (unlimitedStrength) {\r
+                       logger.info("Using JCE Unlimited Strength Jurisdiction Policy files");\r
+               } else {\r
+                       logger.info("Using JCE Standard Encryption Policy files, encryption key lengths will be limited");\r
+               }\r
+       }\r
+       \r
+       public static enum RevocationReason {\r
+               // https://en.wikipedia.org/wiki/Revocation_list\r
+                unspecified, keyCompromise, caCompromise, affiliationChanged, superseded,\r
+                cessationOfOperation, certificateHold, unused, removeFromCRL, privilegeWithdrawn,\r
+                ACompromise;\r
+                \r
+                public static RevocationReason [] reasons = {\r
+                               unspecified, keyCompromise, caCompromise, \r
+                                affiliationChanged, superseded, cessationOfOperation, \r
+                                privilegeWithdrawn };\r
+                \r
+                @Override\r
+                public String toString() {\r
+                        return name() +  " (" + ordinal() + ")";\r
+                }\r
+       }\r
+       \r
+       public static class X509Metadata {\r
+\r
+               // map for distinguished name OIDs\r
+               public final Map<String, String> oids;\r
+\r
+               // CN in distingiushed name\r
+               public final String commonName;\r
+               \r
+               // password for store\r
+               public final String password;\r
+               \r
+               // password hint for README in bundle\r
+               public String passwordHint;\r
+\r
+               // E or EMAILADDRESS in distinguished name\r
+               public String emailAddress;\r
+\r
+               // start date of generated certificate\r
+               public Date notBefore;\r
+               \r
+               // expiraiton date of generated certificate\r
+               public Date notAfter;\r
+               \r
+               // hostname of server for which certificate is generated\r
+               public String serverHostname;\r
+               \r
+               // displayname of user for README in bundle\r
+               public String userDisplayname;\r
+\r
+               public X509Metadata(String cn, String pwd) {\r
+                       if (StringUtils.isEmpty(cn)) {\r
+                               throw new RuntimeException("Common name required!");\r
+                       }\r
+                       if (StringUtils.isEmpty(pwd)) {\r
+                               throw new RuntimeException("Password required!");\r
+                       }\r
+\r
+                       commonName = cn;\r
+                       password = pwd;\r
+                       Calendar c = Calendar.getInstance(TimeZone.getDefault());\r
+                       c.set(Calendar.SECOND, 0);\r
+                       c.set(Calendar.MILLISECOND, 0);\r
+                       notBefore = c.getTime();\r
+                       c.add(Calendar.YEAR, 1);\r
+                       c.add(Calendar.DATE, 1);\r
+                       notAfter = c.getTime();\r
+                       oids = new HashMap<String, String>();\r
+               }\r
+               \r
+               public X509Metadata clone(String commonName, String password) {\r
+                       X509Metadata clone = new X509Metadata(commonName, password);\r
+                       clone.emailAddress = emailAddress;\r
+                       clone.notBefore = notBefore;\r
+                       clone.notAfter = notAfter;\r
+                       clone.oids.putAll(oids);\r
+                       clone.passwordHint = passwordHint;\r
+                       clone.serverHostname = serverHostname;\r
+                       clone.userDisplayname = userDisplayname;\r
+                       return clone;\r
+               }\r
+       }\r
+       \r
+       /**\r
+        * Prepare all the certificates and stores necessary for a Gitblit GO server.\r
+        * \r
+        * @param metadata\r
+        * @param folder\r
+        * @param logger\r
+        */\r
+       public static void prepareX509Infrastructure(X509Metadata metadata, File folder) {\r
+               // make the specified folder, if necessary\r
+               folder.mkdirs();\r
+               \r
+               // Gitblit CA certificate\r
+               File caKeyStore = new File(folder, CA_KEY_STORE);                       \r
+               if (!caKeyStore.exists()) {\r
+                       logger.info(MessageFormat.format("Generating {0} ({1})", CA_CN, caKeyStore.getAbsolutePath()));\r
+                       X509Certificate caCert = newCertificateAuthority(metadata, caKeyStore);\r
+                       saveCertificate(caCert, new File(caKeyStore.getParentFile(), "ca.cer"));\r
+               }\r
+\r
+               // rename the old keystore to the new name\r
+               File oldKeyStore = new File(folder, "keystore");\r
+               if (oldKeyStore.exists()) {\r
+                       oldKeyStore.renameTo(new File(folder, SERVER_KEY_STORE));\r
+                       logger.info(MessageFormat.format("Renaming {0} to {1}", oldKeyStore.getName(), SERVER_KEY_STORE));                              \r
+               }\r
+\r
+               // create web SSL certificate signed by CA\r
+               File serverKeyStore = new File(folder, SERVER_KEY_STORE);\r
+               if (!serverKeyStore.exists()) {\r
+                       logger.info(MessageFormat.format("Generating SSL certificate for {0} signed by {1} ({2})", metadata.commonName, CA_CN, serverKeyStore.getAbsolutePath()));\r
+                       PrivateKey caPrivateKey = getPrivateKey(CA_FN, caKeyStore, metadata.password);\r
+                       X509Certificate caCert = getCertificate(CA_FN, caKeyStore, metadata.password);\r
+                       newSSLCertificate(metadata, caPrivateKey, caCert, serverKeyStore);\r
+               }\r
+\r
+               // server certificate trust store holds trusted public certificates\r
+               File serverTrustStore = new File(folder, X509Utils.SERVER_TRUST_STORE);\r
+               if (!serverTrustStore.exists()) {\r
+                       logger.info(MessageFormat.format("Importing {0} into trust store ({1})", CA_FN, serverTrustStore.getAbsolutePath()));\r
+                       X509Certificate caCert = getCertificate(CA_FN, caKeyStore, metadata.password);\r
+                       addTrustedCertificate(CA_FN, caCert, serverTrustStore, metadata.password);\r
+               }\r
+       }\r
+       \r
+       /**\r
+        * Open a keystore.  Store type is determined by file extension of name. If\r
+        * undetermined, JKS is assumed.  The keystore does not need to exist.\r
+        *  \r
+        * @param storeFile\r
+        * @param storePassword\r
+        * @return a KeyStore\r
+        */\r
+       public static KeyStore openKeyStore(File storeFile, String storePassword) {\r
+               String lc = storeFile.getName().toLowerCase();\r
+               String type = "JKS";\r
+               String provider = null;\r
+               if (lc.endsWith(".p12") || lc.endsWith(".pfx")) {\r
+                       type = "PKCS12";\r
+                       provider = BC;\r
+               }\r
+\r
+               try {\r
+                       KeyStore store;\r
+                       if (provider == null) {\r
+                               store = KeyStore.getInstance(type);\r
+                       } else {\r
+                               store = KeyStore.getInstance(type, provider);\r
+                       }\r
+                       if (storeFile.exists()) {\r
+                               FileInputStream fis = null;\r
+                               try {\r
+                                       fis = new FileInputStream(storeFile);\r
+                                       store.load(fis, storePassword.toCharArray());\r
+                               } finally {\r
+                                       if (fis != null) {\r
+                                               fis.close();\r
+                                       }\r
+                               }\r
+                       } else {\r
+                               store.load(null);\r
+                       }\r
+                       return store;\r
+               } catch (Exception e) {\r
+                       throw new RuntimeException("Could not open keystore " + storeFile, e);\r
+               }\r
+       }\r
+       \r
+       /**\r
+        * Saves the keystore to the specified file.\r
+        * \r
+        * @param targetStoreFile\r
+        * @param store\r
+        * @param password\r
+        */\r
+       public static void saveKeyStore(File targetStoreFile, KeyStore store, String password) {\r
+               File folder = targetStoreFile.getAbsoluteFile().getParentFile();\r
+               if (!folder.exists()) {\r
+                       folder.mkdirs();\r
+               }\r
+               File tmpFile = new File(folder, Long.toHexString(System.currentTimeMillis()) + ".tmp");\r
+               FileOutputStream fos = null;\r
+               try {\r
+                       fos = new FileOutputStream(tmpFile);\r
+                       store.store(fos, password.toCharArray());\r
+                       fos.flush();\r
+                       fos.close();\r
+                       if (targetStoreFile.exists()) {\r
+                               targetStoreFile.delete();\r
+                       }\r
+                       tmpFile.renameTo(targetStoreFile);\r
+               } catch (IOException e) {\r
+                       String message = e.getMessage().toLowerCase();\r
+                       if (message.contains("illegal key size")) {\r
+                               throw new RuntimeException("Illegal Key Size! You might consider installing the JCE Unlimited Strength Jurisdiction Policy files for your JVM.");\r
+                       } else {\r
+                               throw new RuntimeException("Could not save keystore " + targetStoreFile, e);\r
+                       }\r
+               } catch (Exception e) {\r
+                       throw new RuntimeException("Could not save keystore " + targetStoreFile, e);\r
+               } finally {\r
+                       if (fos != null) {\r
+                               try {\r
+                                       fos.close();\r
+                               } catch (IOException e) {\r
+                               }\r
+                       }\r
+                       \r
+                       if (tmpFile.exists()) {\r
+                               tmpFile.delete();\r
+                       }\r
+               }\r
+       }       \r
+\r
+       /**\r
+        * Retrieves the X509 certificate with the specified alias from the certificate\r
+        * store.\r
+        * \r
+        * @param alias\r
+        * @param storeFile\r
+        * @param storePassword\r
+        * @return the certificate\r
+        */\r
+       public static X509Certificate getCertificate(String alias, File storeFile, String storePassword) {\r
+               try {\r
+                       KeyStore store = openKeyStore(storeFile, storePassword);\r
+                       X509Certificate caCert = (X509Certificate) store.getCertificate(alias);\r
+                       return caCert;\r
+               } catch (Exception e) {\r
+                       throw new RuntimeException(e);\r
+               }\r
+       }\r
+       \r
+       /**\r
+        * Retrieves the private key for the specified alias from the certificate\r
+        * store.\r
+        * \r
+        * @param alias\r
+        * @param storeFile\r
+        * @param storePassword\r
+        * @return the private key\r
+        */\r
+       public static PrivateKey getPrivateKey(String alias, File storeFile, String storePassword) {\r
+               try {\r
+                       KeyStore store = openKeyStore(storeFile, storePassword);\r
+                       PrivateKey key = (PrivateKey) store.getKey(alias, storePassword.toCharArray());\r
+                       return key;\r
+               } catch (Exception e) {\r
+                       throw new RuntimeException(e);\r
+               }\r
+       }\r
+\r
+       /**\r
+        * Saves the certificate to the file system.  If the destination filename\r
+        * ends with the pem extension, the certificate is written in the PEM format,\r
+        * otherwise the certificate is written in the DER format.\r
+        * \r
+        * @param cert\r
+        * @param targetFile\r
+        */\r
+       public static void saveCertificate(X509Certificate cert, File targetFile) {\r
+               File folder = targetFile.getAbsoluteFile().getParentFile();\r
+               if (!folder.exists()) {\r
+                       folder.mkdirs();\r
+               }\r
+               File tmpFile = new File(folder, Long.toHexString(System.currentTimeMillis()) + ".tmp");\r
+               try {\r
+                       boolean asPem = targetFile.getName().toLowerCase().endsWith(".pem");\r
+                       if (asPem) {\r
+                               // PEM encoded X509\r
+                               PEMWriter pemWriter = null;\r
+                               try {\r
+                                       pemWriter = new PEMWriter(new FileWriter(tmpFile));\r
+                                       pemWriter.writeObject(cert);\r
+                                       pemWriter.flush();                                      \r
+                               } finally {\r
+                                       if (pemWriter != null) {\r
+                                               pemWriter.close();\r
+                                       }\r
+                               }\r
+                       } else {\r
+                               // DER encoded X509\r
+                               FileOutputStream fos = null;\r
+                               try {\r
+                                       fos = new FileOutputStream(tmpFile);\r
+                                       fos.write(cert.getEncoded());\r
+                                       fos.flush();\r
+                               } finally {\r
+                                       if (fos != null) {\r
+                                               fos.close();\r
+                                       }\r
+                               }\r
+                       }\r
+                       \r
+                       // rename tmp file to target\r
+                       if (targetFile.exists()) { \r
+                               targetFile.delete();\r
+                       }\r
+                       tmpFile.renameTo(targetFile);\r
+               } catch (Exception e) {\r
+                       if (tmpFile.exists()) {\r
+                               tmpFile.delete();\r
+                       }\r
+                       throw new RuntimeException("Failed to save certificate " + cert.getSubjectX500Principal().getName(), e);\r
+               }\r
+       }\r
+       \r
+       /**\r
+        * Generate a new keypair.\r
+        * \r
+        * @return a keypair\r
+        * @throws Exception\r
+        */\r
+       private static KeyPair newKeyPair() throws Exception {\r
+               KeyPairGenerator kpGen = KeyPairGenerator.getInstance("RSA", BC);\r
+               kpGen.initialize(2048, new SecureRandom());\r
+               return kpGen.generateKeyPair();\r
+       }\r
+       \r
+       /**\r
+        * Builds a distinguished name from the X509Metadata.\r
+        * \r
+        * @return a DN\r
+        */\r
+       private static X500Name buildDistinguishedName(X509Metadata metadata) {\r
+               X500NameBuilder dnBuilder = new X500NameBuilder(BCStyle.INSTANCE);\r
+               setOID(dnBuilder, metadata, "C", null);\r
+               setOID(dnBuilder, metadata, "ST", null);\r
+               setOID(dnBuilder, metadata, "L", null);\r
+               setOID(dnBuilder, metadata, "O", Constants.NAME);\r
+               setOID(dnBuilder, metadata, "OU", Constants.NAME);\r
+               setOID(dnBuilder, metadata, "E", metadata.emailAddress);\r
+               setOID(dnBuilder, metadata, "CN", metadata.commonName);         \r
+               X500Name dn = dnBuilder.build();\r
+               return dn;\r
+       }\r
+       \r
+       private static void setOID(X500NameBuilder dnBuilder, X509Metadata metadata,\r
+                       String oid, String defaultValue) {\r
+               \r
+               String value = null;\r
+               if (metadata.oids != null && metadata.oids.containsKey(oid)) {\r
+                       value = metadata.oids.get(oid);\r
+               }\r
+               if (StringUtils.isEmpty(value)) {\r
+                       value = defaultValue;\r
+               }\r
+               \r
+               if (!StringUtils.isEmpty(value)) {\r
+                       try {\r
+                               Field field = BCStyle.class.getField(oid);\r
+                               ASN1ObjectIdentifier objectId = (ASN1ObjectIdentifier) field.get(null);\r
+                               dnBuilder.addRDN(objectId, value);\r
+                       } catch (Exception e) {\r
+                               logger.error(MessageFormat.format("Failed to set OID \"{0}\"!", oid) ,e);\r
+                       }\r
+               }\r
+       }\r
+\r
+       /**\r
+        * Creates a new SSL certificate signed by the CA private key and stored in\r
+        * keyStore.\r
+        * \r
+        * @param sslMetadata\r
+        * @param caPrivateKey\r
+        * @param caCert\r
+        * @param targetStoreFile\r
+        */\r
+       public static X509Certificate newSSLCertificate(X509Metadata sslMetadata, PrivateKey caPrivateKey, X509Certificate caCert, File targetStoreFile) {\r
+               try {\r
+                       KeyPair pair = newKeyPair();\r
+\r
+                       X500Name webDN = buildDistinguishedName(sslMetadata);\r
+                       X500Name issuerDN = new X500Name(PrincipalUtil.getIssuerX509Principal(caCert).getName());\r
+                       \r
+                       X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(\r
+                                       issuerDN,\r
+                                       BigInteger.valueOf(System.currentTimeMillis()), \r
+                                       sslMetadata.notBefore,\r
+                                       sslMetadata.notAfter,\r
+                                       webDN,\r
+                                       pair.getPublic());\r
+                       \r
+                       JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();\r
+                       certBuilder.addExtension(X509Extension.subjectKeyIdentifier, false, extUtils.createSubjectKeyIdentifier(pair.getPublic()));\r
+                       certBuilder.addExtension(X509Extension.basicConstraints, false, new BasicConstraints(false));\r
+                       certBuilder.addExtension(X509Extension.authorityKeyIdentifier, false, extUtils.createAuthorityKeyIdentifier(caCert.getPublicKey()));\r
+\r
+                       ContentSigner caSigner = new JcaContentSignerBuilder("SHA256WithRSAEncryption")\r
+                                       .setProvider(BC).build(caPrivateKey);\r
+                       X509Certificate cert = new JcaX509CertificateConverter().setProvider(BC)\r
+                                       .getCertificate(certBuilder.build(caSigner));\r
+                       \r
+                       cert.checkValidity(new Date());\r
+                       cert.verify(caCert.getPublicKey());\r
+\r
+                       // Save to keystore\r
+                       KeyStore serverStore = openKeyStore(targetStoreFile, sslMetadata.password);\r
+                       serverStore.setKeyEntry(sslMetadata.commonName, pair.getPrivate(), sslMetadata.password.toCharArray(),\r
+                                       new Certificate[] { cert, caCert });\r
+                       saveKeyStore(targetStoreFile, serverStore, sslMetadata.password);\r
+                       \r
+               log(targetStoreFile.getParentFile(), MessageFormat.format("New web certificate {0,number,0} [{1}]", cert.getSerialNumber(), webDN.toString()));\r
+                       \r
+                       return cert;\r
+               } catch (Throwable t) {\r
+                       throw new RuntimeException("Failed to generate SSL certificate!", t);\r
+               }\r
+       }\r
+\r
+       /**\r
+        * Creates a new certificate authority PKCS#12 store.  This function will\r
+        * destroy any existing CA store.\r
+        * \r
+        * @param metadata\r
+        * @param storeFile\r
+        * @param keystorePassword\r
+        * @return\r
+        */\r
+       public static X509Certificate newCertificateAuthority(X509Metadata metadata, File storeFile) {\r
+               try {\r
+                       KeyPair caPair = newKeyPair();\r
+                       \r
+                       ContentSigner caSigner = new JcaContentSignerBuilder("SHA1WithRSA").setProvider(BC).build(caPair.getPrivate());\r
+                       \r
+                       // clone metadata\r
+                       X509Metadata caMetadata = metadata.clone(CA_CN, metadata.password);\r
+                       X500Name issuerDN = buildDistinguishedName(caMetadata);\r
+                       \r
+                       // Generate self-signed certificate\r
+                       X509v3CertificateBuilder caBuilder = new JcaX509v3CertificateBuilder(\r
+                                       issuerDN,\r
+                                       BigInteger.valueOf(System.currentTimeMillis()),\r
+                                       caMetadata.notBefore,\r
+                                       caMetadata.notAfter,\r
+                                       issuerDN,\r
+                                       caPair.getPublic());\r
+                       \r
+                       JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();\r
+                       caBuilder.addExtension(X509Extension.subjectKeyIdentifier, false, extUtils.createSubjectKeyIdentifier(caPair.getPublic()));\r
+                       caBuilder.addExtension(X509Extension.authorityKeyIdentifier, false, extUtils.createAuthorityKeyIdentifier(caPair.getPublic()));\r
+                       caBuilder.addExtension(X509Extension.basicConstraints, false, new BasicConstraints(true));\r
+                       caBuilder.addExtension(X509Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyCertSign | KeyUsage.cRLSign));\r
+                                               \r
+                       JcaX509CertificateConverter converter = new JcaX509CertificateConverter().setProvider(BC);\r
+                       X509Certificate cert = converter.getCertificate(caBuilder.build(caSigner));\r
+                       \r
+                       // confirm the validity of the CA certificate\r
+                       cert.checkValidity(new Date());\r
+                       cert.verify(cert.getPublicKey());\r
+\r
+                       // Delete existing keystore\r
+                       if (storeFile.exists()) {\r
+                               storeFile.delete();\r
+                       }\r
+                       \r
+                       // Save private key and certificate to new keystore\r
+                       KeyStore store = openKeyStore(storeFile, caMetadata.password);\r
+                       store.setKeyEntry(CA_FN, caPair.getPrivate(), caMetadata.password.toCharArray(),\r
+                                       new Certificate[] { cert });\r
+                       saveKeyStore(storeFile, store, caMetadata.password);\r
+                       \r
+               log(storeFile.getParentFile(), MessageFormat.format("New CA certificate {0,number,0} [{1}]", cert.getSerialNumber(), issuerDN.toString()));\r
+                       return cert;\r
+               } catch (Throwable t) {\r
+                       throw new RuntimeException("Failed to generate Gitblit CA certificate!", t);\r
+               }\r
+       }\r
+       \r
+       /**\r
+        * Imports a certificate into the trust store.\r
+        * \r
+        * @param alias\r
+        * @param cert\r
+        * @param storeFile\r
+        * @param storePassword\r
+        */\r
+       public static void addTrustedCertificate(String alias, X509Certificate cert, File storeFile, String storePassword) {\r
+               try {\r
+                       KeyStore store = openKeyStore(storeFile, storePassword);\r
+                       store.setCertificateEntry(alias, cert);\r
+                       saveKeyStore(storeFile, store, storePassword);                  \r
+               } catch (Exception e) {\r
+                       throw new RuntimeException("Failed to import certificate into trust store " + storeFile, e);\r
+               }\r
+       }\r
+       \r
+       /**\r
+        * Creates a new client certificate PKCS#12 and PEM store.  Any existing\r
+        * stores are destroyed.  After generation, the certificates are bundled\r
+        * into a zip file with a personalized README file.\r
+        * \r
+        * The zip file reference is returned. \r
+        * \r
+        * @param clientMetadata a container for dynamic parameters needed for generation\r
+        * @param caKeystoreFile\r
+        * @param caKeystorePassword\r
+        * @return a zip file containing the P12, PEM, and personalized README\r
+        */\r
+       public static File newClientBundle(X509Metadata clientMetadata, File caKeystoreFile, String caKeystorePassword) {\r
+               try {\r
+                       // read the Gitblit CA key and certificate\r
+                       KeyStore store = openKeyStore(caKeystoreFile, caKeystorePassword);\r
+                       PrivateKey caPrivateKey = (PrivateKey) store.getKey(CA_FN, caKeystorePassword.toCharArray());\r
+                       X509Certificate caCert = (X509Certificate) store.getCertificate(CA_FN);\r
+                       \r
+                       // generate the P12 and PEM files\r
+                       File targetFolder = new File(caKeystoreFile.getParentFile(), clientMetadata.commonName);\r
+                       newClientCertificate(clientMetadata, caPrivateKey, caCert, targetFolder);\r
+                       \r
+               // process template message\r
+               String readme = processTemplate(new File(caKeystoreFile.getParentFile(), "instructions.tmpl"), clientMetadata);\r
+               \r
+               // Create a zip bundle with the p12, pem, and a personalized readme\r
+               File zipFile = new File(targetFolder, clientMetadata.commonName + ".zip");\r
+               if (zipFile.exists()) {\r
+                       zipFile.delete();\r
+               }\r
+               ZipOutputStream zos = null;\r
+               try {\r
+                       zos = new ZipOutputStream(new FileOutputStream(zipFile));\r
+                       File p12File = new File(targetFolder, clientMetadata.commonName + ".p12");\r
+                       if (p12File.exists()) {\r
+                               zos.putNextEntry(new ZipEntry(p12File.getName()));\r
+                               zos.write(FileUtils.readContent(p12File));\r
+                               zos.closeEntry();\r
+                       }                       \r
+                       File pemFile = new File(targetFolder, clientMetadata.commonName + ".pem");\r
+                       if (pemFile.exists()) {\r
+                               zos.putNextEntry(new ZipEntry(pemFile.getName()));\r
+                               zos.write(FileUtils.readContent(pemFile));\r
+                               zos.closeEntry();\r
+                       }\r
+                       if (readme != null) {\r
+                               zos.putNextEntry(new ZipEntry("README.TXT"));\r
+                               zos.write(readme.getBytes("UTF-8"));\r
+                               zos.closeEntry();\r
+                       }\r
+                       zos.flush();\r
+               } finally {\r
+                       if (zos != null) {\r
+                               zos.close();\r
+                       }\r
+               }\r
+               \r
+                       return zipFile;\r
+               } catch (Throwable t) {\r
+                       throw new RuntimeException("Failed to generate client bundle!", t);\r
+               }\r
+       }\r
+       \r
+       /**\r
+        * Creates a new client certificate PKCS#12 and PEM store.  Any existing\r
+        * stores are destroyed.\r
+        * \r
+        * @param clientMetadata a container for dynamic parameters needed for generation\r
+        * @param caKeystoreFile\r
+        * @param caKeystorePassword\r
+        * @param targetFolder\r
+        * @return\r
+        */\r
+       public static X509Certificate newClientCertificate(X509Metadata clientMetadata,\r
+                       PrivateKey caPrivateKey, X509Certificate caCert, File targetFolder) {\r
+               try {\r
+                       KeyPair pair = newKeyPair();\r
+                       \r
+                       X500Name userDN = buildDistinguishedName(clientMetadata);                       \r
+                       X500Name issuerDN = new X500Name(PrincipalUtil.getIssuerX509Principal(caCert).getName());\r
+                       \r
+                       // create a new certificate signed by the Gitblit CA certificate\r
+                       X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(\r
+                                       issuerDN,\r
+                                       BigInteger.valueOf(System.currentTimeMillis()),\r
+                                       clientMetadata.notBefore,\r
+                                       clientMetadata.notAfter,\r
+                                       userDN,\r
+                                       pair.getPublic());\r
+                       \r
+                       JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();\r
+                       certBuilder.addExtension(X509Extension.subjectKeyIdentifier, false, extUtils.createSubjectKeyIdentifier(pair.getPublic()));\r
+                       certBuilder.addExtension(X509Extension.basicConstraints, false, new BasicConstraints(false));\r
+                       certBuilder.addExtension(X509Extension.authorityKeyIdentifier, false, extUtils.createAuthorityKeyIdentifier(caCert.getPublicKey()));\r
+                       certBuilder.addExtension(X509Extension.keyUsage, true, new KeyUsage(KeyUsage.keyEncipherment | KeyUsage.digitalSignature));\r
+                       if (!StringUtils.isEmpty(clientMetadata.emailAddress)) {\r
+                               GeneralNames subjectAltName = new GeneralNames(\r
+                    new GeneralName(GeneralName.rfc822Name, clientMetadata.emailAddress));\r
+                               certBuilder.addExtension(X509Extension.subjectAlternativeName, false, subjectAltName);\r
+                       }\r
+\r
+                       ContentSigner signer = new JcaContentSignerBuilder("SHA1WithRSA").setProvider(BC).build(caPrivateKey);\r
+\r
+                       X509Certificate userCert = new JcaX509CertificateConverter().setProvider(BC).getCertificate(certBuilder.build(signer));\r
+                       PKCS12BagAttributeCarrier bagAttr = (PKCS12BagAttributeCarrier)pair.getPrivate();\r
+                       bagAttr.setBagAttribute(PKCSObjectIdentifiers.pkcs_9_at_localKeyId,\r
+                                       extUtils.createSubjectKeyIdentifier(pair.getPublic()));\r
+                        \r
+                       // confirm the validity of the user certificate\r
+                       userCert.checkValidity();\r
+                       userCert.verify(caCert.getPublicKey());\r
+                       userCert.getIssuerDN().equals(caCert.getSubjectDN());\r
+\r
+               // verify user certificate chain\r
+               verifyChain(userCert, caCert);\r
+\r
+               targetFolder.mkdirs();\r
+               \r
+               // save certificate, stamped with unique name\r
+               String date = new SimpleDateFormat("yyyyMMdd").format(new Date());\r
+               String id = date;\r
+               File certFile = new File(targetFolder, id + ".cer");\r
+               int count = 0;\r
+               while (certFile.exists()) {\r
+                       id = date + "_" + Character.toString((char) (0x61 + count));\r
+                       certFile = new File(targetFolder, id + ".cer");\r
+                       count++;\r
+               }\r
+               \r
+               // save user private key, user certificate and CA certificate to a PKCS#12 store\r
+               File p12File = new File(targetFolder, clientMetadata.commonName + ".p12");\r
+               if (p12File.exists()) {\r
+                       p12File.delete();\r
+               }\r
+               KeyStore userStore = openKeyStore(p12File, clientMetadata.password);\r
+               userStore.setKeyEntry(MessageFormat.format("Gitblit ({0}) {1} {2}", clientMetadata.serverHostname, clientMetadata.userDisplayname, id), pair.getPrivate(), null, new Certificate [] { userCert });\r
+               userStore.setCertificateEntry(MessageFormat.format("Gitblit ({0}) Certificate Authority", clientMetadata.serverHostname), caCert);              \r
+               saveKeyStore(p12File, userStore, clientMetadata.password);\r
+               \r
+               // save user private key, user certificate, and CA certificate to a PEM store\r
+               File pemFile = new File(targetFolder, clientMetadata.commonName + ".pem");\r
+               if (pemFile.exists()) {\r
+                       pemFile.delete();\r
+               }\r
+               PEMWriter pemWriter = new PEMWriter(new FileWriter(pemFile));\r
+               pemWriter.writeObject(pair.getPrivate(), "DES-EDE3-CBC", clientMetadata.password.toCharArray(), new SecureRandom());\r
+               pemWriter.writeObject(userCert);\r
+               pemWriter.writeObject(caCert);\r
+               pemWriter.flush();\r
+               pemWriter.close();\r
+               \r
+               // save certificate after successfully creating the key stores\r
+               saveCertificate(userCert, certFile);\r
+               \r
+               log(targetFolder.getParentFile(), MessageFormat.format("New client certificate {0,number,0} [{1}]", userCert.getSerialNumber(), userDN.toString()));\r
+               \r
+               return userCert;\r
+               } catch (Throwable t) {\r
+                       throw new RuntimeException("Failed to generate client certificate!", t);\r
+               }\r
+       }\r
+       \r
+       /**\r
+        * Verifies a certificate's chain to ensure that it will function properly.\r
+        * \r
+        * @param testCert\r
+        * @param additionalCerts\r
+        * @return\r
+        */\r
+       public static PKIXCertPathBuilderResult verifyChain(X509Certificate testCert, X509Certificate... additionalCerts) {\r
+               try {\r
+                       // Check for self-signed certificate\r
+                       if (isSelfSigned(testCert)) {\r
+                               throw new RuntimeException("The certificate is self-signed.  Nothing to verify.");\r
+                       }\r
+                       \r
+                       // Prepare a set of all certificates\r
+                       // chain builder must have all certs, including cert to validate\r
+                       // http://stackoverflow.com/a/10788392\r
+                       Set<X509Certificate> certs = new HashSet<X509Certificate>();\r
+                       certs.add(testCert);\r
+                       certs.addAll(Arrays.asList(additionalCerts));\r
+                       \r
+                       // Attempt to build the certification chain and verify it\r
+                       // Create the selector that specifies the starting certificate\r
+                       X509CertSelector selector = new X509CertSelector(); \r
+                   selector.setCertificate(testCert);\r
+                   \r
+                   // Create the trust anchors (set of root CA certificates)\r
+                   Set<TrustAnchor> trustAnchors = new HashSet<TrustAnchor>();\r
+                   for (X509Certificate cert : additionalCerts) {\r
+                       if (isSelfSigned(cert)) {\r
+                               trustAnchors.add(new TrustAnchor(cert, null));\r
+                       }\r
+                   }\r
+                   \r
+                   // Configure the PKIX certificate builder\r
+                   PKIXBuilderParameters pkixParams = new PKIXBuilderParameters(trustAnchors, selector);\r
+                       pkixParams.setRevocationEnabled(false);\r
+                       pkixParams.addCertStore(CertStore.getInstance("Collection", new CollectionCertStoreParameters(certs), BC));\r
+               \r
+                       // Build and verify the certification chain\r
+                       CertPathBuilder builder = CertPathBuilder.getInstance("PKIX", BC);\r
+                       PKIXCertPathBuilderResult verifiedCertChain = (PKIXCertPathBuilderResult) builder.build(pkixParams);\r
+                       \r
+                       // The chain is built and verified\r
+                       return verifiedCertChain;\r
+               } catch (CertPathBuilderException e) {\r
+                       throw new RuntimeException("Error building certification path: " + testCert.getSubjectX500Principal(), e);\r
+               } catch (Exception e) {\r
+                       throw new RuntimeException("Error verifying the certificate: " + testCert.getSubjectX500Principal(), e);\r
+               }\r
+       }\r
+       \r
+       /**\r
+        * Checks whether given X.509 certificate is self-signed.\r
+        * \r
+        * @param cert\r
+        * @return true if the certificate is self-signed\r
+        */\r
+       public static boolean isSelfSigned(X509Certificate cert) {\r
+               try {\r
+                       cert.verify(cert.getPublicKey());\r
+                       return true;\r
+               } catch (SignatureException e) {\r
+                       return false;\r
+               } catch (InvalidKeyException e) {\r
+                       return false;\r
+               } catch (Exception e) {\r
+                       throw new RuntimeException(e);\r
+               }\r
+       }\r
+       \r
+       public static String processTemplate(File template, X509Metadata metadata) {\r
+               String content = null;\r
+               if (template.exists()) {\r
+                       String message = FileUtils.readContent(template, "\n");\r
+                       if (!StringUtils.isEmpty(message)) {\r
+                               content = message;\r
+                               content = content.replace("$serverHostname", metadata.serverHostname);\r
+                               content = content.replace("$username", metadata.commonName);\r
+                               content = content.replace("$userDisplayname", metadata.userDisplayname);\r
+                               content = content.replace("$storePasswordHint", metadata.passwordHint);\r
+                       }\r
+               }\r
+               return content;\r
+       }\r
+       \r
+       private static void log(File folder, String message) {\r
+               BufferedWriter writer = null;\r
+               try {\r
+                       writer = new BufferedWriter(new FileWriter(new File(folder, "log.txt"), true));\r
+                       writer.write(MessageFormat.format("{0,date,yyyy-MM-dd HH:mm}: {1}", new Date(), message));\r
+                       writer.newLine();\r
+                       writer.flush();\r
+               } catch (Exception e) {\r
+                       logger.error("Failed to append log entry!", e);\r
+               } finally {\r
+                       if (writer != null) {\r
+                               try {\r
+                                       writer.close();\r
+                               } catch (IOException e) {\r
+                               }\r
+                       }\r
+               }\r
+       }\r
+       \r
+       /**\r
+        * Revoke a certificate.\r
+        * \r
+        * @param cert\r
+        * @param reason\r
+        * @param caRevocationList\r
+        * @param caKeystoreFile\r
+        * @param caKeystorePassword\r
+        * @return true if the certificate has been revoked\r
+        */\r
+       public static boolean revoke(X509Certificate cert, RevocationReason reason,\r
+                       File caRevocationList, File caKeystoreFile, String caKeystorePassword) {\r
+               try {\r
+                       // read the Gitblit CA key and certificate\r
+                       KeyStore store = openKeyStore(caKeystoreFile, caKeystorePassword);\r
+                       PrivateKey caPrivateKey = (PrivateKey) store.getKey(CA_FN, caKeystorePassword.toCharArray());\r
+                       return revoke(cert, reason, caRevocationList, caPrivateKey);\r
+               } catch (Exception e) {\r
+                       logger.error(MessageFormat.format("Failed to revoke certificate {0,number,0} [{1}] in {2}",\r
+                                       cert.getSerialNumber(), cert.getSubjectDN().getName(), caRevocationList));\r
+               }\r
+               return false;\r
+       }\r
+       \r
+       /**\r
+        * Revoke a certificate.\r
+        * \r
+        * @param cert\r
+        * @param reason\r
+        * @param caRevocationList\r
+        * @param caPrivateKey\r
+        * @return true if the certificate has been revoked\r
+        */\r
+       public static boolean revoke(X509Certificate cert, RevocationReason reason,\r
+                        File caRevocationList, PrivateKey caPrivateKey) {\r
+               try {\r
+                       X500Name subjectDN = new X500Name(PrincipalUtil.getSubjectX509Principal(cert).getName());\r
+                       X500Name issuerDN = new X500Name(PrincipalUtil.getIssuerX509Principal(cert).getName());\r
+                       X509v2CRLBuilder crlBuilder = new X509v2CRLBuilder(issuerDN, new Date());\r
+                       if (caRevocationList.exists()) {\r
+                               byte [] data = FileUtils.readContent(caRevocationList);                         \r
+                               X509CRLHolder crl = new X509CRLHolder(data);\r
+                               crlBuilder.addCRL(crl);\r
+                       }\r
+                       crlBuilder.addCRLEntry(cert.getSerialNumber(), new Date(), reason.ordinal());\r
+                       \r
+                       // build and sign CRL with CA private key\r
+                       ContentSigner signer = new JcaContentSignerBuilder("SHA1WithRSA").setProvider(BC).build(caPrivateKey);\r
+                       X509CRLHolder crl = crlBuilder.build(signer);\r
+                       \r
+                       File tmpFile = new File(caRevocationList.getParentFile(), Long.toHexString(System.currentTimeMillis()) + ".tmp");\r
+                       FileOutputStream fos = null;\r
+                       try {\r
+                               fos = new FileOutputStream(tmpFile);\r
+                               fos.write(crl.getEncoded());\r
+                               fos.flush();\r
+                               fos.close();\r
+                               if (caRevocationList.exists()) {\r
+                                       caRevocationList.delete();\r
+                               }\r
+                               tmpFile.renameTo(caRevocationList);\r
+                               \r
+                               log(caRevocationList.getParentFile(), MessageFormat.format("Revoked certificate {0,number,0} reason: {1} [{2}]",\r
+                                               cert.getSerialNumber(), reason.toString(), subjectDN.toString()));\r
+                       } finally {\r
+                               if (fos != null) {\r
+                                       fos.close();\r
+                               }\r
+                               if (tmpFile.exists()) {\r
+                                       tmpFile.delete();\r
+                               }\r
+                       }\r
+                       return true;\r
+               } catch (Exception e) {\r
+                       logger.error(MessageFormat.format("Failed to revoke certificate {0,number,0} [{1}] in {2}",\r
+                                       cert.getSerialNumber(), cert.getSubjectDN().getName(), caRevocationList));\r
+               }\r
+               return false;\r
+       }\r
+       \r
+       /**\r
+        * Returns true if the certificate has been revoked.\r
+        * \r
+        * @param cert\r
+        * @param caRevocationList\r
+        * @return true if the certificate is revoked\r
+        */\r
+       public static boolean isRevoked(X509Certificate cert, File caRevocationList) {\r
+               if (!caRevocationList.exists()) {\r
+                       return false;\r
+               }\r
+               try {\r
+                       byte [] data = FileUtils.readContent(caRevocationList);\r
+                       X509CRL crl = new X509CRLImpl(data);\r
+                       return crl.isRevoked(cert);\r
+               } catch (Exception e) {\r
+                       logger.error(MessageFormat.format("Failed to check revocation status for certificate {0,number,0} [{1}] in {2}",\r
+                                       cert.getSerialNumber(), cert.getSubjectDN().getName(), caRevocationList));\r
+               }\r
+               return false;\r
+       }\r
+}\r
diff --git a/tests/com/gitblit/tests/X509UtilsTest.java b/tests/com/gitblit/tests/X509UtilsTest.java
new file mode 100644 (file)
index 0000000..85afce0
--- /dev/null
@@ -0,0 +1,172 @@
+/*\r
+ * Copyright 2012 gitblit.com.\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ *     http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+package com.gitblit.tests;\r
+\r
+import java.io.File;\r
+import java.io.FileInputStream;\r
+import java.security.PrivateKey;\r
+import java.security.cert.X509Certificate;\r
+import java.util.Arrays;\r
+import java.util.List;\r
+import java.util.zip.ZipEntry;\r
+import java.util.zip.ZipInputStream;\r
+\r
+import org.eclipse.jgit.util.FileUtils;\r
+import org.junit.After;\r
+import org.junit.Assert;\r
+import org.junit.Before;\r
+import org.junit.Test;\r
+\r
+import com.gitblit.models.UserModel;\r
+import com.gitblit.utils.HttpUtils;\r
+import com.gitblit.utils.X509Utils;\r
+import com.gitblit.utils.X509Utils.RevocationReason;\r
+import com.gitblit.utils.X509Utils.X509Metadata;\r
+\r
+/**\r
+ * Unit tests for X509 certificate generation.\r
+ * \r
+ * @author James Moger\r
+ * \r
+ */\r
+public class X509UtilsTest extends Assert {\r
+       \r
+       // passwords are case-sensitive and may be length-limited\r
+       // based on the JCE policy files\r
+       String caPassword = "aBcDeFg";\r
+       File folder = new File(System.getProperty("user.dir"), "x509test");\r
+\r
+       @Before\r
+       public void prepare() throws Exception {\r
+               cleanUp();\r
+               X509Metadata goMetadata = new X509Metadata("localhost", caPassword);\r
+               X509Utils.prepareX509Infrastructure(goMetadata, folder);\r
+       }\r
+       \r
+       @After\r
+       public void cleanUp() throws Exception {\r
+               if (folder.exists()) {\r
+                       FileUtils.delete(folder, FileUtils.RECURSIVE);\r
+               }\r
+       }\r
+       \r
+       @Test\r
+       public void testNewCA() throws Exception {              \r
+               File storeFile = new File(folder, X509Utils.CA_KEY_STORE);\r
+               X509Utils.getPrivateKey(X509Utils.CA_FN, storeFile, caPassword);\r
+               X509Certificate cert = X509Utils.getCertificate(X509Utils.CA_FN, storeFile, caPassword);\r
+               assertEquals("O=Gitblit,OU=Gitblit,CN=Gitblit Certificate Authority", cert.getIssuerDN().getName());\r
+       }       \r
+\r
+       @Test\r
+       public void testCertificateUserMapping() throws Exception {             \r
+               File storeFile = new File(folder, X509Utils.CA_KEY_STORE);\r
+               PrivateKey caPrivateKey = X509Utils.getPrivateKey(X509Utils.CA_FN, storeFile, caPassword);\r
+               X509Certificate caCert = X509Utils.getCertificate(X509Utils.CA_FN, storeFile, caPassword);\r
+               \r
+               X509Metadata userMetadata = new X509Metadata("james", "james");\r
+               userMetadata.serverHostname = "www.myserver.com";\r
+               userMetadata.userDisplayname = "James Moger";\r
+               userMetadata.passwordHint = "your name";\r
+               userMetadata.oids.put("C",  "US");\r
+               \r
+               X509Certificate cert1 = X509Utils.newClientCertificate(userMetadata, caPrivateKey, caCert, storeFile.getParentFile());\r
+               UserModel userModel1 = HttpUtils.getUserModelFromCertificate(cert1);\r
+               assertEquals(userMetadata.commonName, userModel1.username);\r
+               assertEquals(userMetadata.emailAddress, userModel1.emailAddress);\r
+               assertEquals("C=US,O=Gitblit,OU=Gitblit,CN=james", cert1.getSubjectDN().getName());\r
+               \r
+               \r
+               X509Certificate cert2 = X509Utils.newClientCertificate(userMetadata, caPrivateKey, caCert, storeFile.getParentFile());\r
+               UserModel userModel2 = HttpUtils.getUserModelFromCertificate(cert2);\r
+               assertEquals(userMetadata.commonName, userModel2.username);\r
+               assertEquals(userMetadata.emailAddress, userModel2.emailAddress);\r
+               assertEquals("C=US,O=Gitblit,OU=Gitblit,CN=james", cert2.getSubjectDN().getName());\r
+               \r
+               assertNotSame("Serial numbers are the same!", cert1.getSerialNumber().longValue(), cert2.getSerialNumber().longValue());\r
+       }\r
+       \r
+       @Test\r
+       public void testUserBundle() throws Exception {\r
+               File storeFile = new File(folder, X509Utils.CA_KEY_STORE);\r
+               \r
+               X509Metadata userMetadata = new X509Metadata("james", "james");\r
+               userMetadata.serverHostname = "www.myserver.com";               \r
+               userMetadata.userDisplayname = "James Moger";\r
+               userMetadata.passwordHint = "your name";\r
+\r
+               File zip = X509Utils.newClientBundle(userMetadata, storeFile, caPassword);\r
+               assertTrue(zip.exists());\r
+               \r
+               List<String> expected = Arrays.asList(userMetadata.commonName + ".pem", userMetadata.commonName + ".p12", "README.TXT");\r
+               \r
+               ZipInputStream zis = new ZipInputStream(new FileInputStream(zip));\r
+               ZipEntry entry = null;\r
+               while ((entry = zis.getNextEntry()) != null) {\r
+                       assertTrue("Unexpected file: " + entry.getName(), expected.contains(entry.getName()));\r
+               }\r
+               zis.close();\r
+       }\r
+       \r
+       @Test\r
+       public void testCertificateRevocation() throws Exception {              \r
+               File storeFile = new File(folder, X509Utils.CA_KEY_STORE);\r
+               PrivateKey caPrivateKey = X509Utils.getPrivateKey(X509Utils.CA_FN, storeFile, caPassword);\r
+               X509Certificate caCert = X509Utils.getCertificate(X509Utils.CA_FN, storeFile, caPassword);\r
+               \r
+               X509Metadata userMetadata = new X509Metadata("james", "james");\r
+               userMetadata.serverHostname = "www.myserver.com";\r
+               userMetadata.userDisplayname = "James Moger";\r
+               userMetadata.passwordHint = "your name";\r
+               \r
+               // generate a new client certificate\r
+               X509Certificate cert1 = X509Utils.newClientCertificate(userMetadata, caPrivateKey, caCert, storeFile.getParentFile());\r
+               \r
+               // confirm this certificate IS NOT revoked\r
+               File caRevocationList = new File(folder, X509Utils.CA_REVOCATION_LIST);\r
+               assertFalse(X509Utils.isRevoked(cert1, caRevocationList));\r
+               \r
+               // revoke certificate and then confirm it IS revoked\r
+               X509Utils.revoke(cert1, RevocationReason.ACompromise, caRevocationList, storeFile, caPassword);\r
+               assertTrue(X509Utils.isRevoked(cert1, caRevocationList));\r
+               \r
+               // generate a second certificate\r
+               X509Certificate cert2 = X509Utils.newClientCertificate(userMetadata, caPrivateKey, caCert, storeFile.getParentFile());\r
+               \r
+               // confirm second certificate IS NOT revoked\r
+               assertTrue(X509Utils.isRevoked(cert1, caRevocationList));\r
+               assertFalse(X509Utils.isRevoked(cert2, caRevocationList));\r
+               \r
+               // revoke second certificate and then confirm it IS revoked\r
+               X509Utils.revoke(cert2, RevocationReason.ACompromise, caRevocationList, caPrivateKey);\r
+               assertTrue(X509Utils.isRevoked(cert1, caRevocationList));\r
+               assertTrue(X509Utils.isRevoked(cert2, caRevocationList));\r
+               \r
+               // generate a third certificate\r
+               X509Certificate cert3 = X509Utils.newClientCertificate(userMetadata, caPrivateKey, caCert, storeFile.getParentFile());\r
+               \r
+               // confirm third certificate IS NOT revoked\r
+               assertTrue(X509Utils.isRevoked(cert1, caRevocationList));\r
+               assertTrue(X509Utils.isRevoked(cert2, caRevocationList));\r
+               assertFalse(X509Utils.isRevoked(cert3, caRevocationList));\r
+               \r
+               // revoke third certificate and then confirm it IS revoked\r
+               X509Utils.revoke(cert3, RevocationReason.ACompromise, caRevocationList, caPrivateKey);\r
+               assertTrue(X509Utils.isRevoked(cert1, caRevocationList));\r
+               assertTrue(X509Utils.isRevoked(cert2, caRevocationList));\r
+               assertTrue(X509Utils.isRevoked(cert3, caRevocationList));\r
+       }\r
+}\r