summaryrefslogtreecommitdiffstats
path: root/src/com/gitblit/utils/X509Utils.java
diff options
context:
space:
mode:
authorJames Moger <james.moger@gitblit.com>2012-11-23 10:31:58 -0500
committerJames Moger <james.moger@gitblit.com>2012-11-23 10:31:58 -0500
commitb423580c552ff2c2d47829b0e1632a77958fec68 (patch)
tree6afc595dfc4a47641609cd185441f2dfaa001950 /src/com/gitblit/utils/X509Utils.java
parent1729d4e8878dbdfe2724776bb551655fecdbd8db (diff)
downloadgitblit-b423580c552ff2c2d47829b0e1632a77958fec68.tar.gz
gitblit-b423580c552ff2c2d47829b0e1632a77958fec68.zip
X509 certificate generation utilities for CA, web, and client certificates
Diffstat (limited to 'src/com/gitblit/utils/X509Utils.java')
-rw-r--r--src/com/gitblit/utils/X509Utils.java991
1 files changed, 991 insertions, 0 deletions
diff --git a/src/com/gitblit/utils/X509Utils.java b/src/com/gitblit/utils/X509Utils.java
new file mode 100644
index 00000000..e6ffec1a
--- /dev/null
+++ b/src/com/gitblit/utils/X509Utils.java
@@ -0,0 +1,991 @@
+/*
+ * 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.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+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.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.Arrays;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+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.jcajce.JcaContentSignerBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import sun.security.x509.X509CRLImpl;
+
+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_FN = CA_CN;
+
+ private static final String BC = org.bouncycastle.jce.provider.BouncyCastleProvider.PROVIDER_NAME;
+
+ 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 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;
+
+ 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;
+ }
+ }
+
+ /**
+ * Prepare all the certificates and stores necessary for a Gitblit GO server.
+ *
+ * @param metadata
+ * @param folder
+ * @param logger
+ */
+ public static void prepareX509Infrastructure(X509Metadata metadata, File folder) {
+ // 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);
+ saveCertificate(caCert, new File(caKeyStore.getParentFile(), "ca.cer"));
+ }
+
+ // 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_FN, caKeyStore, metadata.password);
+ X509Certificate caCert = getCertificate(CA_FN, caKeyStore, metadata.password);
+ newSSLCertificate(metadata, caPrivateKey, caCert, serverKeyStore);
+ }
+
+ // server certificate trust store holds trusted public certificates
+ File serverTrustStore = new File(folder, X509Utils.SERVER_TRUST_STORE);
+ if (!serverTrustStore.exists()) {
+ logger.info(MessageFormat.format("Importing {0} into trust store ({1})", CA_FN, serverTrustStore.getAbsolutePath()));
+ X509Certificate caCert = getCertificate(CA_FN, caKeyStore, metadata.password);
+ addTrustedCertificate(CA_FN, caCert, serverTrustStore, metadata.password);
+ }
+ }
+
+ /**
+ * 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("RSA", BC);
+ kpGen.initialize(2048, 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
+ */
+ public static X509Certificate newSSLCertificate(X509Metadata sslMetadata, PrivateKey caPrivateKey, X509Certificate caCert, File targetStoreFile) {
+ 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()));
+
+ ContentSigner caSigner = new JcaContentSignerBuilder("SHA256WithRSAEncryption")
+ .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);
+
+ log(targetStoreFile.getParentFile(), MessageFormat.format("New web certificate {0,number,0} [{1}]", cert.getSerialNumber(), webDN.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
+ * @return
+ */
+ public static X509Certificate newCertificateAuthority(X509Metadata metadata, File storeFile) {
+ try {
+ KeyPair caPair = newKeyPair();
+
+ ContentSigner caSigner = new JcaContentSignerBuilder("SHA1WithRSA").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_FN, caPair.getPrivate(), caMetadata.password.toCharArray(),
+ new Certificate[] { cert });
+ saveKeyStore(storeFile, store, caMetadata.password);
+
+ log(storeFile.getParentFile(), MessageFormat.format("New CA certificate {0,number,0} [{1}]", cert.getSerialNumber(), issuerDN.toString()));
+ return cert;
+ } catch (Throwable t) {
+ throw new RuntimeException("Failed to generate Gitblit CA certificate!", t);
+ }
+ }
+
+ /**
+ * 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
+ * @return a zip file containing the P12, PEM, and personalized README
+ */
+ public static File newClientBundle(X509Metadata clientMetadata, File caKeystoreFile, String caKeystorePassword) {
+ try {
+ // read the Gitblit CA key and certificate
+ KeyStore store = openKeyStore(caKeystoreFile, caKeystorePassword);
+ PrivateKey caPrivateKey = (PrivateKey) store.getKey(CA_FN, caKeystorePassword.toCharArray());
+ X509Certificate caCert = (X509Certificate) store.getCertificate(CA_FN);
+
+ // generate the P12 and PEM files
+ File targetFolder = new File(caKeystoreFile.getParentFile(), clientMetadata.commonName);
+ newClientCertificate(clientMetadata, caPrivateKey, caCert, targetFolder);
+
+ // 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();
+ }
+ 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("SHA1WithRSA").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);
+
+ log(targetFolder.getParentFile(), MessageFormat.format("New client certificate {0,number,0} [{1}]", userCert.getSerialNumber(), userDN.toString()));
+
+ return userCert;
+ } catch (Throwable t) {
+ throw new RuntimeException("Failed to generate client certificate!", t);
+ }
+ }
+
+ /**
+ * 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;
+ content = content.replace("$serverHostname", metadata.serverHostname);
+ content = content.replace("$username", metadata.commonName);
+ content = content.replace("$userDisplayname", metadata.userDisplayname);
+ content = content.replace("$storePasswordHint", metadata.passwordHint);
+ }
+ }
+ return content;
+ }
+
+ private static void log(File folder, String message) {
+ BufferedWriter writer = null;
+ try {
+ writer = new BufferedWriter(new FileWriter(new File(folder, "log.txt"), true));
+ writer.write(MessageFormat.format("{0,date,yyyy-MM-dd HH:mm}: {1}", new Date(), message));
+ writer.newLine();
+ writer.flush();
+ } catch (Exception e) {
+ logger.error("Failed to append log entry!", e);
+ } finally {
+ if (writer != null) {
+ try {
+ writer.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+ }
+
+ /**
+ * Revoke a certificate.
+ *
+ * @param cert
+ * @param reason
+ * @param caRevocationList
+ * @param caKeystoreFile
+ * @param caKeystorePassword
+ * @return true if the certificate has been revoked
+ */
+ public static boolean revoke(X509Certificate cert, RevocationReason reason,
+ 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_FN, caKeystorePassword.toCharArray());
+ return revoke(cert, reason, caRevocationList, caPrivateKey);
+ } 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
+ * @return true if the certificate has been revoked
+ */
+ public static boolean revoke(X509Certificate cert, RevocationReason reason,
+ File caRevocationList, PrivateKey caPrivateKey) {
+ try {
+ X500Name subjectDN = new X500Name(PrincipalUtil.getSubjectX509Principal(cert).getName());
+ X500Name issuerDN = new X500Name(PrincipalUtil.getIssuerX509Principal(cert).getName());
+ X509v2CRLBuilder crlBuilder = new X509v2CRLBuilder(issuerDN, new Date());
+ if (caRevocationList.exists()) {
+ 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);
+
+ log(caRevocationList.getParentFile(), MessageFormat.format("Revoked certificate {0,number,0} reason: {1} [{2}]",
+ cert.getSerialNumber(), reason.toString(), subjectDN.toString()));
+ } finally {
+ if (fos != null) {
+ fos.close();
+ }
+ if (tmpFile.exists()) {
+ tmpFile.delete();
+ }
+ }
+ return true;
+ } 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;
+ }
+
+ /**
+ * 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;
+ }
+ try {
+ byte [] data = FileUtils.readContent(caRevocationList);
+ X509CRL crl = new X509CRLImpl(data);
+ 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));
+ }
+ return false;
+ }
+}