/* * 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 javax.naming.ldap.LdapName; 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.PEMEncryptor; import org.bouncycastle.openssl.PEMWriter; import org.bouncycastle.openssl.jcajce.JcaPEMWriter; import org.bouncycastle.openssl.jcajce.JcePEMEncryptorBuilder; 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 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(); } 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 altNames = new ArrayList(); 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(); } JcePEMEncryptorBuilder builder = new JcePEMEncryptorBuilder("DES-EDE3-CBC"); builder.setSecureRandom(new SecureRandom()); PEMEncryptor pemEncryptor = builder.build(clientMetadata.password.toCharArray()); JcaPEMWriter pemWriter = new JcaPEMWriter(new FileWriter(pemFile)); pemWriter.writeObject(pair.getPrivate(), pemEncryptor); 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 certs = new HashSet(); 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 trustAnchors = new HashSet(); 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) { Map oids = new HashMap(); try { String dn = cert.getSubjectDN().getName(); LdapName ldapName = new LdapName(dn); for (int i = 0; i < ldapName.size(); i++) { String [] val = ldapName.get(i).trim().split("=", 2); String oid = val[0].toUpperCase().trim(); String data = val[1].trim(); oids.put(oid, data); } } catch (Exception e) { throw new RuntimeException(e); } 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; } }