Package com.gitblit.utils

Source Code of com.gitblit.utils.X509Utils

/*
* 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;
  }
}
TOP

Related Classes of com.gitblit.utils.X509Utils

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.