summaryrefslogtreecommitdiffstats
path: root/src/main/java/com/gitblit/utils/X509Utils.java
blob: f0c1b9dfd69a03685515536a87d7e8ef752f547f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */
.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */
.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */
.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */
.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */
.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
.highlight .vc { color: #336699 } /* Name.Variable.Class */
.highlight .vg { color: #dd7700 } /* Name.Variable.Global */
.highlight .vi { color: #3333bb } /* Name.Variable.Instance */
.highlight .vm { color: #336699 } /* Name.Variable.Magic */
.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
define( [
	"jquery",
	"./helper",
	"ui/draggable"
], function( $, testHelper ) {

var element;

module( "draggable: methods", {
	setup: function() {
		element = $("<div style='background: green; width: 200px; height: 100px; position: absolute; top: 10px; left: 10px;'><span>Absolute</span></div>").appendTo("#qunit-fixture");
	},
	teardown: function() {
		element.remove();
	}
});

test( "init", function() {
	expect( 5 );

	element.draggable();
	ok( true, ".draggable() called on element" );

	$([]).draggable();
	ok( true, ".draggable() called on empty collection" );

	$("<div></div>").draggable();
	ok( true, ".draggable() called on disconnected DOMElement" );

	element.draggable( "option", "foo" );
	ok( true, "arbitrary option getter after init" );

	element.draggable( "option", "foo", "bar" );
	ok( true, "arbitrary option setter after init" );
});

test( "destroy", function() {
	expect( 4 );

	element.draggable().draggable("destroy");
	ok( true, ".draggable('destroy') called on element" );

	$([]).draggable().draggable("destroy");
	ok( true, ".draggable('destroy') called on empty collection" );

	element.draggable().draggable("destroy");
	ok( true, ".draggable('destroy') called on disconnected DOMElement" );

	var expected = element.draggable(),
		actual = expected.draggable("destroy");
	equal( actual, expected, "destroy is chainable" );
});

test( "enable", function() {
	expect( 11 );

	element.draggable({ disabled: true });
	testHelper.shouldNotDrag( element, ".draggable({ disabled: true })" );

	element.draggable("enable");
	testHelper.shouldMove( element, ".draggable('enable')" );
	equal( element.draggable( "option", "disabled" ), false, "disabled option getter" );

	element.draggable("destroy");
	element.draggable({ disabled: true });
	testHelper.shouldNotDrag( element, ".draggable({ disabled: true })" );

	element.draggable( "option", "disabled", false );
	equal(element.draggable( "option", "disabled" ), false, "disabled option setter" );
	testHelper.shouldMove( element, ".draggable('option', 'disabled', false)" );

	var expected = element.draggable(),
		actual = expected.draggable("enable");
	equal( actual, expected, "enable is chainable" );
});

test( "disable", function( assert ) {
	expect( 14 );

	element = $( "#draggable2" ).draggable({ disabled: false });
	testHelper.shouldMove( element, ".draggable({ disabled: false })" );

	element.draggable( "disable" );
	testHelper.shouldNotDrag( element, ".draggable('disable')" );
	equal( element.draggable( "option", "disabled" ), true, "disabled option getter" );

	element.draggable( "destroy" );
	element.draggable({ disabled: false });
	testHelper.shouldMove( element, ".draggable({ disabled: false })" );

	element.draggable( "option", "disabled", true );
	equal( element.draggable( "option", "disabled" ), true, "disabled option setter" );
	testHelper.shouldNotDrag( element, ".draggable('option', 'disabled', true)" );

	assert.lacksClasses( element.draggable( "widget" ), "ui-state-disabled" );
	ok( !element.draggable( "widget" ).attr( "aria-disabled" ), "element does not get aria-disabled" );
	assert.hasClasses( element.draggable( "widget" ), "ui-draggable-disabled" );

	var expected = element.draggable(),
		actual = expected.draggable( "disable" );
	equal( actual, expected, "disable is chainable" );
});

} );
0' href='#n980'>980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138
/*
 * 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.utils;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.SignatureException;
import java.security.cert.CertPathBuilder;
import java.security.cert.CertPathBuilderException;
import java.security.cert.CertStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateFactory;
import java.security.cert.CollectionCertStoreParameters;
import java.security.cert.PKIXBuilderParameters;
import java.security.cert.PKIXCertPathBuilderResult;
import java.security.cert.TrustAnchor;
import java.security.cert.X509CRL;
import java.security.cert.X509CertSelector;
import java.security.cert.X509Certificate;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import javax.crypto.Cipher;

import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.X500NameBuilder;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.asn1.x509.X509Extension;
import org.bouncycastle.cert.X509CRLHolder;
import org.bouncycastle.cert.X509v2CRLBuilder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.PrincipalUtil;
import org.bouncycastle.jce.interfaces.PKCS12BagAttributeCarrier;
import org.bouncycastle.openssl.PEMWriter;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.gitblit.Constants;

/**
 * Utility class to generate X509 certificates, keystores, and truststores.
 *
 * @author James Moger
 *
 */
public class X509Utils {

	public static final String SERVER_KEY_STORE = "serverKeyStore.jks";

	public static final String SERVER_TRUST_STORE = "serverTrustStore.jks";

	public static final String CERTS = "certs";

	public static final String CA_KEY_STORE = "certs/caKeyStore.p12";

	public static final String CA_REVOCATION_LIST = "certs/caRevocationList.crl";

	public static final String CA_CONFIG = "certs/authority.conf";

	public static final String CA_CN = "Gitblit Certificate Authority";

	public static final String CA_ALIAS = CA_CN;

	private static final String BC = org.bouncycastle.jce.provider.BouncyCastleProvider.PROVIDER_NAME;

	private static final int KEY_LENGTH = 2048;

	private static final String KEY_ALGORITHM = "RSA";

	private static final String SIGNING_ALGORITHM = "SHA512withRSA";

	public static final boolean unlimitedStrength;

	private static final Logger logger = LoggerFactory.getLogger(X509Utils.class);

	static {
		Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());

		// check for JCE Unlimited Strength
		int maxKeyLen = 0;
		try {
			maxKeyLen = Cipher.getMaxAllowedKeyLength("AES");
		} catch (NoSuchAlgorithmException e) {
		}

		unlimitedStrength = maxKeyLen > 128;
		if (unlimitedStrength) {
			logger.info("Using JCE Unlimited Strength Jurisdiction Policy files");
		} else {
			logger.info("Using JCE Standard Encryption Policy files, encryption key lengths will be limited");
		}
	}

	public static enum RevocationReason {
		// https://en.wikipedia.org/wiki/Revocation_list
		 unspecified, keyCompromise, caCompromise, affiliationChanged, superseded,
		 cessationOfOperation, certificateHold, unused, removeFromCRL, privilegeWithdrawn,
		 ACompromise;

		 public static RevocationReason [] reasons = {
			 	unspecified, keyCompromise, caCompromise,
				 affiliationChanged, superseded, cessationOfOperation,
				 privilegeWithdrawn };

		 @Override
		 public String toString() {
			 return name() +  " (" + ordinal() + ")";
		 }
	}

	public interface X509Log {
		void log(String message);
	}

	public static class X509Metadata {

		// map for distinguished name OIDs
		public final Map<String, String> oids;

		// CN in distingiushed name
		public final String commonName;

		// password for store
		public final String password;

		// password hint for README in bundle
		public String passwordHint;

		// E or EMAILADDRESS in distinguished name
		public String emailAddress;

		// start date of generated certificate
		public Date notBefore;

		// expiraiton date of generated certificate
		public Date notAfter;

		// hostname of server for which certificate is generated
		public String serverHostname;

		// displayname of user for README in bundle
		public String userDisplayname;

		// serialnumber of generated or read certificate
		public String serialNumber;

		public X509Metadata(String cn, String pwd) {
			if (StringUtils.isEmpty(cn)) {
				throw new RuntimeException("Common name required!");
			}
			if (StringUtils.isEmpty(pwd)) {
				throw new RuntimeException("Password required!");
			}

			commonName = cn;
			password = pwd;
			Calendar c = Calendar.getInstance(TimeZone.getDefault());
			c.set(Calendar.SECOND, 0);
			c.set(Calendar.MILLISECOND, 0);
			notBefore = c.getTime();
			c.add(Calendar.YEAR, 1);
			c.add(Calendar.DATE, 1);
			notAfter = c.getTime();
			oids = new HashMap<String, String>();
		}

		public X509Metadata clone(String commonName, String password) {
			X509Metadata clone = new X509Metadata(commonName, password);
			clone.emailAddress = emailAddress;
			clone.notBefore = notBefore;
			clone.notAfter = notAfter;
			clone.oids.putAll(oids);
			clone.passwordHint = passwordHint;
			clone.serverHostname = serverHostname;
			clone.userDisplayname = userDisplayname;
			return clone;
		}

		public String getOID(String oid, String defaultValue) {
			if (oids.containsKey(oid)) {
				return oids.get(oid);
			}
			return defaultValue;
		}

		public void setOID(String oid, String value) {
			if (StringUtils.isEmpty(value)) {
				oids.remove(oid);
			} else {
				oids.put(oid, value);
			}
		}
	}

	/**
	 * Prepare all the certificates and stores necessary for a Gitblit GO server.
	 *
	 * @param metadata
	 * @param folder
	 * @param x509log
	 */
	public static void prepareX509Infrastructure(X509Metadata metadata, File folder, X509Log x509log) {
		// make the specified folder, if necessary
		folder.mkdirs();

		// Gitblit CA certificate
		File caKeyStore = new File(folder, CA_KEY_STORE);
		if (!caKeyStore.exists()) {
			logger.info(MessageFormat.format("Generating {0} ({1})", CA_CN, caKeyStore.getAbsolutePath()));
			X509Certificate caCert = newCertificateAuthority(metadata, caKeyStore, x509log);
			saveCertificate(caCert, new File(caKeyStore.getParentFile(), "ca.cer"));
		}

		// Gitblit CRL
		File caRevocationList = new File(folder, CA_REVOCATION_LIST);
		if (!caRevocationList.exists()) {
			logger.info(MessageFormat.format("Generating {0} CRL ({1})", CA_CN, caRevocationList.getAbsolutePath()));
			newCertificateRevocationList(caRevocationList, caKeyStore, metadata.password);
			x509log.log("new certificate revocation list created");
		}

		// rename the old keystore to the new name
		File oldKeyStore = new File(folder, "keystore");
		if (oldKeyStore.exists()) {
			oldKeyStore.renameTo(new File(folder, SERVER_KEY_STORE));
			logger.info(MessageFormat.format("Renaming {0} to {1}", oldKeyStore.getName(), SERVER_KEY_STORE));
		}

		// create web SSL certificate signed by CA
		File serverKeyStore = new File(folder, SERVER_KEY_STORE);
		if (!serverKeyStore.exists()) {
			logger.info(MessageFormat.format("Generating SSL certificate for {0} signed by {1} ({2})", metadata.commonName, CA_CN, serverKeyStore.getAbsolutePath()));
			PrivateKey caPrivateKey = getPrivateKey(CA_ALIAS, caKeyStore, metadata.password);
			X509Certificate caCert = getCertificate(CA_ALIAS, caKeyStore, metadata.password);
			newSSLCertificate(metadata, caPrivateKey, caCert, serverKeyStore, x509log);
		}

		// server certificate trust store holds trusted public certificates
		File serverTrustStore = new File(folder, X509Utils.SERVER_TRUST_STORE);
		if (!serverTrustStore.exists()) {
			logger.info(MessageFormat.format("Importing {0} into trust store ({1})", CA_ALIAS, serverTrustStore.getAbsolutePath()));
			X509Certificate caCert = getCertificate(CA_ALIAS, caKeyStore, metadata.password);
			addTrustedCertificate(CA_ALIAS, caCert, serverTrustStore, metadata.password);
		}
	}

	/**
	 * Open a keystore.  Store type is determined by file extension of name. If
	 * undetermined, JKS is assumed.  The keystore does not need to exist.
	 *
	 * @param storeFile
	 * @param storePassword
	 * @return a KeyStore
	 */
	public static KeyStore openKeyStore(File storeFile, String storePassword) {
		String lc = storeFile.getName().toLowerCase();
		String type = "JKS";
		String provider = null;
		if (lc.endsWith(".p12") || lc.endsWith(".pfx")) {
			type = "PKCS12";
			provider = BC;
		}

		try {
			KeyStore store;
			if (provider == null) {
				store = KeyStore.getInstance(type);
			} else {
				store = KeyStore.getInstance(type, provider);
			}
			if (storeFile.exists()) {
				FileInputStream fis = null;
				try {
					fis = new FileInputStream(storeFile);
					store.load(fis, storePassword.toCharArray());
				} finally {
					if (fis != null) {
						fis.close();
					}
				}
			} else {
				store.load(null);
			}
			return store;
		} catch (Exception e) {
			throw new RuntimeException("Could not open keystore " + storeFile, e);
		}
	}

	/**
	 * Saves the keystore to the specified file.
	 *
	 * @param targetStoreFile
	 * @param store
	 * @param password
	 */
	public static void saveKeyStore(File targetStoreFile, KeyStore store, String password) {
		File folder = targetStoreFile.getAbsoluteFile().getParentFile();
		if (!folder.exists()) {
			folder.mkdirs();
		}
		File tmpFile = new File(folder, Long.toHexString(System.currentTimeMillis()) + ".tmp");
		FileOutputStream fos = null;
		try {
			fos = new FileOutputStream(tmpFile);
			store.store(fos, password.toCharArray());
			fos.flush();
			fos.close();
			if (targetStoreFile.exists()) {
				targetStoreFile.delete();
			}
			tmpFile.renameTo(targetStoreFile);
		} catch (IOException e) {
			String message = e.getMessage().toLowerCase();
			if (message.contains("illegal key size")) {
				throw new RuntimeException("Illegal Key Size! You might consider installing the JCE Unlimited Strength Jurisdiction Policy files for your JVM.");
			} else {
				throw new RuntimeException("Could not save keystore " + targetStoreFile, e);
			}
		} catch (Exception e) {
			throw new RuntimeException("Could not save keystore " + targetStoreFile, e);
		} finally {
			if (fos != null) {
				try {
					fos.close();
				} catch (IOException e) {
				}
			}

			if (tmpFile.exists()) {
				tmpFile.delete();
			}
		}
	}

	/**
	 * Retrieves the X509 certificate with the specified alias from the certificate
	 * store.
	 *
	 * @param alias
	 * @param storeFile
	 * @param storePassword
	 * @return the certificate
	 */
	public static X509Certificate getCertificate(String alias, File storeFile, String storePassword) {
		try {
			KeyStore store = openKeyStore(storeFile, storePassword);
			X509Certificate caCert = (X509Certificate) store.getCertificate(alias);
			return caCert;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * Retrieves the private key for the specified alias from the certificate
	 * store.
	 *
	 * @param alias
	 * @param storeFile
	 * @param storePassword
	 * @return the private key
	 */
	public static PrivateKey getPrivateKey(String alias, File storeFile, String storePassword) {
		try {
			KeyStore store = openKeyStore(storeFile, storePassword);
			PrivateKey key = (PrivateKey) store.getKey(alias, storePassword.toCharArray());
			return key;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * Saves the certificate to the file system.  If the destination filename
	 * ends with the pem extension, the certificate is written in the PEM format,
	 * otherwise the certificate is written in the DER format.
	 *
	 * @param cert
	 * @param targetFile
	 */
	public static void saveCertificate(X509Certificate cert, File targetFile) {
		File folder = targetFile.getAbsoluteFile().getParentFile();
		if (!folder.exists()) {
			folder.mkdirs();
		}
		File tmpFile = new File(folder, Long.toHexString(System.currentTimeMillis()) + ".tmp");
		try {
			boolean asPem = targetFile.getName().toLowerCase().endsWith(".pem");
			if (asPem) {
				// PEM encoded X509
				PEMWriter pemWriter = null;
				try {
					pemWriter = new PEMWriter(new FileWriter(tmpFile));
					pemWriter.writeObject(cert);
					pemWriter.flush();
				} finally {
					if (pemWriter != null) {
						pemWriter.close();
					}
				}
			} else {
				// DER encoded X509
				FileOutputStream fos = null;
				try {
					fos = new FileOutputStream(tmpFile);
					fos.write(cert.getEncoded());
					fos.flush();
				} finally {
					if (fos != null) {
						fos.close();
					}
				}
			}

			// rename tmp file to target
			if (targetFile.exists()) {
				targetFile.delete();
			}
			tmpFile.renameTo(targetFile);
		} catch (Exception e) {
			if (tmpFile.exists()) {
				tmpFile.delete();
			}
			throw new RuntimeException("Failed to save certificate " + cert.getSubjectX500Principal().getName(), e);
		}
	}

	/**
	 * Generate a new keypair.
	 *
	 * @return a keypair
	 * @throws Exception
	 */
	private static KeyPair newKeyPair() throws Exception {
		KeyPairGenerator kpGen = KeyPairGenerator.getInstance(KEY_ALGORITHM, BC);
		kpGen.initialize(KEY_LENGTH, new SecureRandom());
		return kpGen.generateKeyPair();
	}

	/**
	 * Builds a distinguished name from the X509Metadata.
	 *
	 * @return a DN
	 */
	private static X500Name buildDistinguishedName(X509Metadata metadata) {
		X500NameBuilder dnBuilder = new X500NameBuilder(BCStyle.INSTANCE);
		setOID(dnBuilder, metadata, "C", null);
		setOID(dnBuilder, metadata, "ST", null);
		setOID(dnBuilder, metadata, "L", null);
		setOID(dnBuilder, metadata, "O", Constants.NAME);
		setOID(dnBuilder, metadata, "OU", Constants.NAME);
		setOID(dnBuilder, metadata, "E", metadata.emailAddress);
		setOID(dnBuilder, metadata, "CN", metadata.commonName);
		X500Name dn = dnBuilder.build();
		return dn;
	}

	private static void setOID(X500NameBuilder dnBuilder, X509Metadata metadata,
			String oid, String defaultValue) {

		String value = null;
		if (metadata.oids != null && metadata.oids.containsKey(oid)) {
			value = metadata.oids.get(oid);
		}
		if (StringUtils.isEmpty(value)) {
			value = defaultValue;
		}

		if (!StringUtils.isEmpty(value)) {
			try {
				Field field = BCStyle.class.getField(oid);
				ASN1ObjectIdentifier objectId = (ASN1ObjectIdentifier) field.get(null);
				dnBuilder.addRDN(objectId, value);
			} catch (Exception e) {
				logger.error(MessageFormat.format("Failed to set OID \"{0}\"!", oid) ,e);
			}
		}
	}

	/**
	 * Creates a new SSL certificate signed by the CA private key and stored in
	 * keyStore.
	 *
	 * @param sslMetadata
	 * @param caPrivateKey
	 * @param caCert
	 * @param targetStoreFile
	 * @param x509log
	 */
	public static X509Certificate newSSLCertificate(X509Metadata sslMetadata, PrivateKey caPrivateKey, X509Certificate caCert, File targetStoreFile, X509Log x509log) {
		try {
			KeyPair pair = newKeyPair();

			X500Name webDN = buildDistinguishedName(sslMetadata);
			X500Name issuerDN = new X500Name(PrincipalUtil.getIssuerX509Principal(caCert).getName());

			X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
					issuerDN,
					BigInteger.valueOf(System.currentTimeMillis()),
					sslMetadata.notBefore,
					sslMetadata.notAfter,
					webDN,
					pair.getPublic());

			JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
			certBuilder.addExtension(X509Extension.subjectKeyIdentifier, false, extUtils.createSubjectKeyIdentifier(pair.getPublic()));
			certBuilder.addExtension(X509Extension.basicConstraints, false, new BasicConstraints(false));
			certBuilder.addExtension(X509Extension.authorityKeyIdentifier, false, extUtils.createAuthorityKeyIdentifier(caCert.getPublicKey()));

			// support alternateSubjectNames for SSL certificates
			List<GeneralName> altNames = new ArrayList<GeneralName>();
			if (HttpUtils.isIpAddress(sslMetadata.commonName)) {
				altNames.add(new GeneralName(GeneralName.iPAddress, sslMetadata.commonName));
			}
			if (altNames.size() > 0) {
				GeneralNames subjectAltName = new GeneralNames(altNames.toArray(new GeneralName [altNames.size()]));
				certBuilder.addExtension(X509Extension.subjectAlternativeName, false, subjectAltName);
			}

			ContentSigner caSigner = new JcaContentSignerBuilder(SIGNING_ALGORITHM)
					.setProvider(BC).build(caPrivateKey);
			X509Certificate cert = new JcaX509CertificateConverter().setProvider(BC)
					.getCertificate(certBuilder.build(caSigner));

			cert.checkValidity(new Date());
			cert.verify(caCert.getPublicKey());

			// Save to keystore
			KeyStore serverStore = openKeyStore(targetStoreFile, sslMetadata.password);
			serverStore.setKeyEntry(sslMetadata.commonName, pair.getPrivate(), sslMetadata.password.toCharArray(),
					new Certificate[] { cert, caCert });
			saveKeyStore(targetStoreFile, serverStore, sslMetadata.password);

	        x509log.log(MessageFormat.format("New SSL certificate {0,number,0} [{1}]", cert.getSerialNumber(), cert.getSubjectDN().getName()));

	        // update serial number in metadata object
	        sslMetadata.serialNumber = cert.getSerialNumber().toString();

			return cert;
		} catch (Throwable t) {
			throw new RuntimeException("Failed to generate SSL certificate!", t);
		}
	}

	/**
	 * Creates a new certificate authority PKCS#12 store.  This function will
	 * destroy any existing CA store.
	 *
	 * @param metadata
	 * @param storeFile
	 * @param keystorePassword
	 * @param x509log
	 * @return
	 */
	public static X509Certificate newCertificateAuthority(X509Metadata metadata, File storeFile, X509Log x509log) {
		try {
			KeyPair caPair = newKeyPair();

			ContentSigner caSigner = new JcaContentSignerBuilder(SIGNING_ALGORITHM).setProvider(BC).build(caPair.getPrivate());

			// clone metadata
			X509Metadata caMetadata = metadata.clone(CA_CN, metadata.password);
			X500Name issuerDN = buildDistinguishedName(caMetadata);

			// Generate self-signed certificate
			X509v3CertificateBuilder caBuilder = new JcaX509v3CertificateBuilder(
					issuerDN,
					BigInteger.valueOf(System.currentTimeMillis()),
					caMetadata.notBefore,
					caMetadata.notAfter,
					issuerDN,
					caPair.getPublic());

			JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
			caBuilder.addExtension(X509Extension.subjectKeyIdentifier, false, extUtils.createSubjectKeyIdentifier(caPair.getPublic()));
			caBuilder.addExtension(X509Extension.authorityKeyIdentifier, false, extUtils.createAuthorityKeyIdentifier(caPair.getPublic()));
			caBuilder.addExtension(X509Extension.basicConstraints, false, new BasicConstraints(true));
			caBuilder.addExtension(X509Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyCertSign | KeyUsage.cRLSign));

			JcaX509CertificateConverter converter = new JcaX509CertificateConverter().setProvider(BC);
			X509Certificate cert = converter.getCertificate(caBuilder.build(caSigner));

			// confirm the validity of the CA certificate
			cert.checkValidity(new Date());
			cert.verify(cert.getPublicKey());

			// Delete existing keystore
			if (storeFile.exists()) {
				storeFile.delete();
			}

			// Save private key and certificate to new keystore
			KeyStore store = openKeyStore(storeFile, caMetadata.password);
			store.setKeyEntry(CA_ALIAS, caPair.getPrivate(), caMetadata.password.toCharArray(),
					new Certificate[] { cert });
			saveKeyStore(storeFile, store, caMetadata.password);

			x509log.log(MessageFormat.format("New CA certificate {0,number,0} [{1}]", cert.getSerialNumber(), cert.getIssuerDN().getName()));

	        // update serial number in metadata object
	        caMetadata.serialNumber = cert.getSerialNumber().toString();

			return cert;
		} catch (Throwable t) {
			throw new RuntimeException("Failed to generate Gitblit CA certificate!", t);
		}
	}

	/**
	 * Creates a new certificate revocation list (CRL).  This function will
	 * destroy any existing CRL file.
	 *
	 * @param caRevocationList
	 * @param storeFile
	 * @param keystorePassword
	 * @return
	 */
	public static void newCertificateRevocationList(File caRevocationList, File caKeystoreFile, String caKeystorePassword) {
		try {
			// read the Gitblit CA key and certificate
			KeyStore store = openKeyStore(caKeystoreFile, caKeystorePassword);
			PrivateKey caPrivateKey = (PrivateKey) store.getKey(CA_ALIAS, caKeystorePassword.toCharArray());
			X509Certificate caCert = (X509Certificate) store.getCertificate(CA_ALIAS);

			X500Name issuerDN = new X500Name(PrincipalUtil.getIssuerX509Principal(caCert).getName());
			X509v2CRLBuilder crlBuilder = new X509v2CRLBuilder(issuerDN, new Date());

			// build and sign CRL with CA private key
			ContentSigner signer = new JcaContentSignerBuilder(SIGNING_ALGORITHM).setProvider(BC).build(caPrivateKey);
			X509CRLHolder crl = crlBuilder.build(signer);

			File tmpFile = new File(caRevocationList.getParentFile(), Long.toHexString(System.currentTimeMillis()) + ".tmp");
			FileOutputStream fos = null;
			try {
				fos = new FileOutputStream(tmpFile);
				fos.write(crl.getEncoded());
				fos.flush();
				fos.close();
				if (caRevocationList.exists()) {
					caRevocationList.delete();
				}
				tmpFile.renameTo(caRevocationList);
			} finally {
				if (fos != null) {
					fos.close();
				}
				if (tmpFile.exists()) {
					tmpFile.delete();
				}
			}
		} catch (Exception e) {
			throw new RuntimeException("Failed to create new certificate revocation list " + caRevocationList, e);
		}
	}

	/**
	 * Imports a certificate into the trust store.
	 *
	 * @param alias
	 * @param cert
	 * @param storeFile
	 * @param storePassword
	 */
	public static void addTrustedCertificate(String alias, X509Certificate cert, File storeFile, String storePassword) {
		try {
			KeyStore store = openKeyStore(storeFile, storePassword);
			store.setCertificateEntry(alias, cert);
			saveKeyStore(storeFile, store, storePassword);
		} catch (Exception e) {
			throw new RuntimeException("Failed to import certificate into trust store " + storeFile, e);
		}
	}

	/**
	 * Creates a new client certificate PKCS#12 and PEM store.  Any existing
	 * stores are destroyed.  After generation, the certificates are bundled
	 * into a zip file with a personalized README file.
	 *
	 * The zip file reference is returned.
	 *
	 * @param clientMetadata a container for dynamic parameters needed for generation
	 * @param caKeystoreFile
	 * @param caKeystorePassword
	 * @param x509log
	 * @return a zip file containing the P12, PEM, and personalized README
	 */
	public static File newClientBundle(X509Metadata clientMetadata, File caKeystoreFile,
			String caKeystorePassword, X509Log x509log) {
		try {
			// read the Gitblit CA key and certificate
			KeyStore store = openKeyStore(caKeystoreFile, caKeystorePassword);
			PrivateKey caPrivateKey = (PrivateKey) store.getKey(CA_ALIAS, caKeystorePassword.toCharArray());
			X509Certificate caCert = (X509Certificate) store.getCertificate(CA_ALIAS);

			// generate the P12 and PEM files
			File targetFolder = new File(caKeystoreFile.getParentFile(), clientMetadata.commonName);
			X509Certificate cert = newClientCertificate(clientMetadata, caPrivateKey, caCert, targetFolder);
	        x509log.log(MessageFormat.format("New client certificate {0,number,0} [{1}]", cert.getSerialNumber(), cert.getSubjectDN().getName()));

	        // process template message
	        String readme = processTemplate(new File(caKeystoreFile.getParentFile(), "instructions.tmpl"), clientMetadata);

	        // Create a zip bundle with the p12, pem, and a personalized readme
	        File zipFile = new File(targetFolder, clientMetadata.commonName + ".zip");
	        if (zipFile.exists()) {
	        	zipFile.delete();
	        }
	        ZipOutputStream zos = null;
	        try {
	        	zos = new ZipOutputStream(new FileOutputStream(zipFile));
	        	File p12File = new File(targetFolder, clientMetadata.commonName + ".p12");
	        	if (p12File.exists()) {
	        		zos.putNextEntry(new ZipEntry(p12File.getName()));
	        		zos.write(FileUtils.readContent(p12File));
	        		zos.closeEntry();
	        	}
	        	File pemFile = new File(targetFolder, clientMetadata.commonName + ".pem");
	        	if (pemFile.exists()) {
	        		zos.putNextEntry(new ZipEntry(pemFile.getName()));
	        		zos.write(FileUtils.readContent(pemFile));
	        		zos.closeEntry();
	        	}

	        	// include user's public certificate
	        	zos.putNextEntry(new ZipEntry(clientMetadata.commonName + ".cer"));
        		zos.write(cert.getEncoded());
        		zos.closeEntry();

	        	// include CA public certificate
	        	zos.putNextEntry(new ZipEntry("ca.cer"));
        		zos.write(caCert.getEncoded());
        		zos.closeEntry();

	        	if (readme != null) {
	        		zos.putNextEntry(new ZipEntry("README.TXT"));
	        		zos.write(readme.getBytes("UTF-8"));
	        		zos.closeEntry();
	        	}
	        	zos.flush();
	        } finally {
	        	if (zos != null) {
	        		zos.close();
	        	}
	        }

			return zipFile;
		} catch (Throwable t) {
			throw new RuntimeException("Failed to generate client bundle!", t);
		}
	}

	/**
	 * Creates a new client certificate PKCS#12 and PEM store.  Any existing
	 * stores are destroyed.
	 *
	 * @param clientMetadata a container for dynamic parameters needed for generation
	 * @param caKeystoreFile
	 * @param caKeystorePassword
	 * @param targetFolder
	 * @return
	 */
	public static X509Certificate newClientCertificate(X509Metadata clientMetadata,
			PrivateKey caPrivateKey, X509Certificate caCert, File targetFolder) {
		try {
			KeyPair pair = newKeyPair();

			X500Name userDN = buildDistinguishedName(clientMetadata);
			X500Name issuerDN = new X500Name(PrincipalUtil.getIssuerX509Principal(caCert).getName());

			// create a new certificate signed by the Gitblit CA certificate
			X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
					issuerDN,
					BigInteger.valueOf(System.currentTimeMillis()),
					clientMetadata.notBefore,
					clientMetadata.notAfter,
					userDN,
					pair.getPublic());

			JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
			certBuilder.addExtension(X509Extension.subjectKeyIdentifier, false, extUtils.createSubjectKeyIdentifier(pair.getPublic()));
			certBuilder.addExtension(X509Extension.basicConstraints, false, new BasicConstraints(false));
			certBuilder.addExtension(X509Extension.authorityKeyIdentifier, false, extUtils.createAuthorityKeyIdentifier(caCert.getPublicKey()));
			certBuilder.addExtension(X509Extension.keyUsage, true, new KeyUsage(KeyUsage.keyEncipherment | KeyUsage.digitalSignature));
			if (!StringUtils.isEmpty(clientMetadata.emailAddress)) {
				GeneralNames subjectAltName = new GeneralNames(
                    new GeneralName(GeneralName.rfc822Name, clientMetadata.emailAddress));
				certBuilder.addExtension(X509Extension.subjectAlternativeName, false, subjectAltName);
			}

			ContentSigner signer = new JcaContentSignerBuilder(SIGNING_ALGORITHM).setProvider(BC).build(caPrivateKey);

			X509Certificate userCert = new JcaX509CertificateConverter().setProvider(BC).getCertificate(certBuilder.build(signer));
			PKCS12BagAttributeCarrier bagAttr = (PKCS12BagAttributeCarrier)pair.getPrivate();
			bagAttr.setBagAttribute(PKCSObjectIdentifiers.pkcs_9_at_localKeyId,
					extUtils.createSubjectKeyIdentifier(pair.getPublic()));

			// confirm the validity of the user certificate
			userCert.checkValidity();
			userCert.verify(caCert.getPublicKey());
			userCert.getIssuerDN().equals(caCert.getSubjectDN());

	        // verify user certificate chain
	        verifyChain(userCert, caCert);

	        targetFolder.mkdirs();

	        // save certificate, stamped with unique name
	        String date = new SimpleDateFormat("yyyyMMdd").format(new Date());
	        String id = date;
	        File certFile = new File(targetFolder, id + ".cer");
	        int count = 0;
	        while (certFile.exists()) {
	        	id = date + "_" + Character.toString((char) (0x61 + count));
	        	certFile = new File(targetFolder, id + ".cer");
	        	count++;
	        }

	        // save user private key, user certificate and CA certificate to a PKCS#12 store
	        File p12File = new File(targetFolder, clientMetadata.commonName + ".p12");
	        if (p12File.exists()) {
	        	p12File.delete();
	        }
	        KeyStore userStore = openKeyStore(p12File, clientMetadata.password);
	        userStore.setKeyEntry(MessageFormat.format("Gitblit ({0}) {1} {2}", clientMetadata.serverHostname, clientMetadata.userDisplayname, id), pair.getPrivate(), null, new Certificate [] { userCert });
	        userStore.setCertificateEntry(MessageFormat.format("Gitblit ({0}) Certificate Authority", clientMetadata.serverHostname), caCert);
	        saveKeyStore(p12File, userStore, clientMetadata.password);

	        // save user private key, user certificate, and CA certificate to a PEM store
	        File pemFile = new File(targetFolder, clientMetadata.commonName + ".pem");
	        if (pemFile.exists()) {
	        	pemFile.delete();
	        }
	        PEMWriter pemWriter = new PEMWriter(new FileWriter(pemFile));
	        pemWriter.writeObject(pair.getPrivate(), "DES-EDE3-CBC", clientMetadata.password.toCharArray(), new SecureRandom());
	        pemWriter.writeObject(userCert);
	        pemWriter.writeObject(caCert);
	        pemWriter.flush();
	        pemWriter.close();

	        // save certificate after successfully creating the key stores
	        saveCertificate(userCert, certFile);

	        // update serial number in metadata object
	        clientMetadata.serialNumber = userCert.getSerialNumber().toString();

	        return userCert;
		} catch (Throwable t) {
			throw new RuntimeException("Failed to generate client certificate!", t);
		}
	}

	/**
	 * Verifies a certificate's chain to ensure that it will function properly.
	 *
	 * @param testCert
	 * @param additionalCerts
	 * @return
	 */
	public static PKIXCertPathBuilderResult verifyChain(X509Certificate testCert, X509Certificate... additionalCerts) {
		try {
			// Check for self-signed certificate
			if (isSelfSigned(testCert)) {
				throw new RuntimeException("The certificate is self-signed.  Nothing to verify.");
			}

			// Prepare a set of all certificates
			// chain builder must have all certs, including cert to validate
			// http://stackoverflow.com/a/10788392
			Set<X509Certificate> certs = new HashSet<X509Certificate>();
			certs.add(testCert);
			certs.addAll(Arrays.asList(additionalCerts));

			// Attempt to build the certification chain and verify it
			// Create the selector that specifies the starting certificate
			X509CertSelector selector = new X509CertSelector();
		    selector.setCertificate(testCert);

		    // Create the trust anchors (set of root CA certificates)
		    Set<TrustAnchor> trustAnchors = new HashSet<TrustAnchor>();
		    for (X509Certificate cert : additionalCerts) {
		    	if (isSelfSigned(cert)) {
		    		trustAnchors.add(new TrustAnchor(cert, null));
		    	}
		    }

		    // Configure the PKIX certificate builder
		    PKIXBuilderParameters pkixParams = new PKIXBuilderParameters(trustAnchors, selector);
			pkixParams.setRevocationEnabled(false);
			pkixParams.addCertStore(CertStore.getInstance("Collection", new CollectionCertStoreParameters(certs), BC));

			// Build and verify the certification chain
			CertPathBuilder builder = CertPathBuilder.getInstance("PKIX", BC);
			PKIXCertPathBuilderResult verifiedCertChain = (PKIXCertPathBuilderResult) builder.build(pkixParams);

			// The chain is built and verified
			return verifiedCertChain;
		} catch (CertPathBuilderException e) {
			throw new RuntimeException("Error building certification path: " + testCert.getSubjectX500Principal(), e);
		} catch (Exception e) {
			throw new RuntimeException("Error verifying the certificate: " + testCert.getSubjectX500Principal(), e);
		}
	}

	/**
	 * Checks whether given X.509 certificate is self-signed.
	 *
	 * @param cert
	 * @return true if the certificate is self-signed
	 */
	public static boolean isSelfSigned(X509Certificate cert) {
		try {
			cert.verify(cert.getPublicKey());
			return true;
		} catch (SignatureException e) {
			return false;
		} catch (InvalidKeyException e) {
			return false;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public static String processTemplate(File template, X509Metadata metadata) {
		String content = null;
		if (template.exists()) {
			String message = FileUtils.readContent(template, "\n");
			if (!StringUtils.isEmpty(message)) {
				content = message;
				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;
	}

	/**
	 * Revoke a certificate.
	 *
	 * @param cert
	 * @param reason
	 * @param caRevocationList
	 * @param caKeystoreFile
	 * @param caKeystorePassword
	 * @param x509log
	 * @return true if the certificate has been revoked
	 */
	public static boolean revoke(X509Certificate cert, RevocationReason reason,
			File caRevocationList, File caKeystoreFile, String caKeystorePassword,
			X509Log x509log) {
		try {
			// read the Gitblit CA key and certificate
			KeyStore store = openKeyStore(caKeystoreFile, caKeystorePassword);
			PrivateKey caPrivateKey = (PrivateKey) store.getKey(CA_ALIAS, caKeystorePassword.toCharArray());
			return revoke(cert, reason, caRevocationList, caPrivateKey, x509log);
		} catch (Exception e) {
			logger.error(MessageFormat.format("Failed to revoke certificate {0,number,0} [{1}] in {2}",
					cert.getSerialNumber(), cert.getSubjectDN().getName(), caRevocationList));
		}
		return false;
	}

	/**
	 * Revoke a certificate.
	 *
	 * @param cert
	 * @param reason
	 * @param caRevocationList
	 * @param caPrivateKey
	 * @param x509log
	 * @return true if the certificate has been revoked
	 */
	public static boolean revoke(X509Certificate cert, RevocationReason reason,
			 File caRevocationList, PrivateKey caPrivateKey, X509Log x509log) {
		try {
			X500Name issuerDN = new X500Name(PrincipalUtil.getIssuerX509Principal(cert).getName());
			X509v2CRLBuilder crlBuilder = new X509v2CRLBuilder(issuerDN, new Date());
			if (caRevocationList.exists()) {
				byte [] data = FileUtils.readContent(caRevocationList);
				X509CRLHolder crl = new X509CRLHolder(data);
				crlBuilder.addCRL(crl);
			}
			crlBuilder.addCRLEntry(cert.getSerialNumber(), new Date(), reason.ordinal());

			// build and sign CRL with CA private key
			ContentSigner signer = new JcaContentSignerBuilder("SHA1WithRSA").setProvider(BC).build(caPrivateKey);
			X509CRLHolder crl = crlBuilder.build(signer);

			File tmpFile = new File(caRevocationList.getParentFile(), Long.toHexString(System.currentTimeMillis()) + ".tmp");
			FileOutputStream fos = null;
			try {
				fos = new FileOutputStream(tmpFile);
				fos.write(crl.getEncoded());
				fos.flush();
				fos.close();
				if (caRevocationList.exists()) {
					caRevocationList.delete();
				}
				tmpFile.renameTo(caRevocationList);

			} finally {
				if (fos != null) {
					fos.close();
				}
				if (tmpFile.exists()) {
					tmpFile.delete();
				}
			}

			x509log.log(MessageFormat.format("Revoked certificate {0,number,0} reason: {1} [{2}]",
					cert.getSerialNumber(), reason.toString(), cert.getSubjectDN().getName()));
			return true;
		} catch (IOException | OperatorCreationException | CertificateEncodingException e) {
			logger.error(MessageFormat.format("Failed to revoke certificate {0,number,0} [{1}] in {2}",
					cert.getSerialNumber(), cert.getSubjectDN().getName(), caRevocationList));
		}
		return false;
	}

	/**
	 * Returns true if the certificate has been revoked.
	 *
	 * @param cert
	 * @param caRevocationList
	 * @return true if the certificate is revoked
	 */
	public static boolean isRevoked(X509Certificate cert, File caRevocationList) {
		if (!caRevocationList.exists()) {
			return false;
		}
		InputStream inStream = null;
		try {
			inStream = new FileInputStream(caRevocationList);
			CertificateFactory cf = CertificateFactory.getInstance("X.509");
			X509CRL crl = (X509CRL)cf.generateCRL(inStream);
			return crl.isRevoked(cert);
		} catch (Exception e) {
			logger.error(MessageFormat.format("Failed to check revocation status for certificate {0,number,0} [{1}] in {2}",
					cert.getSerialNumber(), cert.getSubjectDN().getName(), caRevocationList));
		} finally {
			if (inStream != null) {
				try {
					inStream.close();
				} catch (Exception e) {
				}
			}
		}
		return false;
	}

	public static X509Metadata getMetadata(X509Certificate cert) {
		// manually split DN into OID components
		// this is instead of parsing with LdapName which:
		// (1) I don't trust the order of values
		// (2) it filters out values like EMAILADDRESS
		String dn = cert.getSubjectDN().getName();
		Map<String, String> oids = new HashMap<String, String>();
		for (String kvp : dn.split(",")) {
			String [] val = kvp.trim().split("=");
			String oid = val[0].toUpperCase().trim();
			String data = val[1].trim();
			oids.put(oid, data);
		}

		X509Metadata metadata = new X509Metadata(oids.get("CN"), "whocares");
		metadata.oids.putAll(oids);
		metadata.serialNumber = cert.getSerialNumber().toString();
		metadata.notAfter = cert.getNotAfter();
		metadata.notBefore = cert.getNotBefore();
		metadata.emailAddress = metadata.getOID("E", null);
		if (metadata.emailAddress == null) {
			metadata.emailAddress = metadata.getOID("EMAILADDRESS", null);
		}
		return metadata;
	}
}