Parcourir la source

X509 certificate generation utilities for CA, web, and client certificates

tags/v1.2.0
James Moger il y a 11 ans
Parent
révision
b423580c55

+ 19
- 0
src/com/gitblit/utils/FileUtils.java Voir le fichier

@@ -99,6 +99,25 @@ public class FileUtils {
}
return defaultValue;
}
/**
* Returns the byte [] content of the specified file.
*
* @param file
* @return the byte content of the file
*/
public static byte [] readContent(File file) {
byte [] buffer = new byte[(int) file.length()];
try {
BufferedInputStream is = new BufferedInputStream(new FileInputStream(file));
is.read(buffer, 0, buffer.length);
is.close();
} catch (Throwable t) {
System.err.println("Failed to read byte content of " + file.getAbsolutePath());
t.printStackTrace();
}
return buffer;
}
/**
* Returns the string content of the specified file.

+ 991
- 0
src/com/gitblit/utils/X509Utils.java Voir le fichier

@@ -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;
}
}

+ 172
- 0
tests/com/gitblit/tests/X509UtilsTest.java Voir le fichier

@@ -0,0 +1,172 @@
/*
* 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.tests;
import java.io.File;
import java.io.FileInputStream;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.eclipse.jgit.util.FileUtils;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import com.gitblit.models.UserModel;
import com.gitblit.utils.HttpUtils;
import com.gitblit.utils.X509Utils;
import com.gitblit.utils.X509Utils.RevocationReason;
import com.gitblit.utils.X509Utils.X509Metadata;
/**
* Unit tests for X509 certificate generation.
*
* @author James Moger
*
*/
public class X509UtilsTest extends Assert {
// passwords are case-sensitive and may be length-limited
// based on the JCE policy files
String caPassword = "aBcDeFg";
File folder = new File(System.getProperty("user.dir"), "x509test");
@Before
public void prepare() throws Exception {
cleanUp();
X509Metadata goMetadata = new X509Metadata("localhost", caPassword);
X509Utils.prepareX509Infrastructure(goMetadata, folder);
}
@After
public void cleanUp() throws Exception {
if (folder.exists()) {
FileUtils.delete(folder, FileUtils.RECURSIVE);
}
}
@Test
public void testNewCA() throws Exception {
File storeFile = new File(folder, X509Utils.CA_KEY_STORE);
X509Utils.getPrivateKey(X509Utils.CA_FN, storeFile, caPassword);
X509Certificate cert = X509Utils.getCertificate(X509Utils.CA_FN, storeFile, caPassword);
assertEquals("O=Gitblit,OU=Gitblit,CN=Gitblit Certificate Authority", cert.getIssuerDN().getName());
}
@Test
public void testCertificateUserMapping() throws Exception {
File storeFile = new File(folder, X509Utils.CA_KEY_STORE);
PrivateKey caPrivateKey = X509Utils.getPrivateKey(X509Utils.CA_FN, storeFile, caPassword);
X509Certificate caCert = X509Utils.getCertificate(X509Utils.CA_FN, storeFile, caPassword);
X509Metadata userMetadata = new X509Metadata("james", "james");
userMetadata.serverHostname = "www.myserver.com";
userMetadata.userDisplayname = "James Moger";
userMetadata.passwordHint = "your name";
userMetadata.oids.put("C", "US");
X509Certificate cert1 = X509Utils.newClientCertificate(userMetadata, caPrivateKey, caCert, storeFile.getParentFile());
UserModel userModel1 = HttpUtils.getUserModelFromCertificate(cert1);
assertEquals(userMetadata.commonName, userModel1.username);
assertEquals(userMetadata.emailAddress, userModel1.emailAddress);
assertEquals("C=US,O=Gitblit,OU=Gitblit,CN=james", cert1.getSubjectDN().getName());
X509Certificate cert2 = X509Utils.newClientCertificate(userMetadata, caPrivateKey, caCert, storeFile.getParentFile());
UserModel userModel2 = HttpUtils.getUserModelFromCertificate(cert2);
assertEquals(userMetadata.commonName, userModel2.username);
assertEquals(userMetadata.emailAddress, userModel2.emailAddress);
assertEquals("C=US,O=Gitblit,OU=Gitblit,CN=james", cert2.getSubjectDN().getName());
assertNotSame("Serial numbers are the same!", cert1.getSerialNumber().longValue(), cert2.getSerialNumber().longValue());
}
@Test
public void testUserBundle() throws Exception {
File storeFile = new File(folder, X509Utils.CA_KEY_STORE);
X509Metadata userMetadata = new X509Metadata("james", "james");
userMetadata.serverHostname = "www.myserver.com";
userMetadata.userDisplayname = "James Moger";
userMetadata.passwordHint = "your name";
File zip = X509Utils.newClientBundle(userMetadata, storeFile, caPassword);
assertTrue(zip.exists());
List<String> expected = Arrays.asList(userMetadata.commonName + ".pem", userMetadata.commonName + ".p12", "README.TXT");
ZipInputStream zis = new ZipInputStream(new FileInputStream(zip));
ZipEntry entry = null;
while ((entry = zis.getNextEntry()) != null) {
assertTrue("Unexpected file: " + entry.getName(), expected.contains(entry.getName()));
}
zis.close();
}
@Test
public void testCertificateRevocation() throws Exception {
File storeFile = new File(folder, X509Utils.CA_KEY_STORE);
PrivateKey caPrivateKey = X509Utils.getPrivateKey(X509Utils.CA_FN, storeFile, caPassword);
X509Certificate caCert = X509Utils.getCertificate(X509Utils.CA_FN, storeFile, caPassword);
X509Metadata userMetadata = new X509Metadata("james", "james");
userMetadata.serverHostname = "www.myserver.com";
userMetadata.userDisplayname = "James Moger";
userMetadata.passwordHint = "your name";
// generate a new client certificate
X509Certificate cert1 = X509Utils.newClientCertificate(userMetadata, caPrivateKey, caCert, storeFile.getParentFile());
// confirm this certificate IS NOT revoked
File caRevocationList = new File(folder, X509Utils.CA_REVOCATION_LIST);
assertFalse(X509Utils.isRevoked(cert1, caRevocationList));
// revoke certificate and then confirm it IS revoked
X509Utils.revoke(cert1, RevocationReason.ACompromise, caRevocationList, storeFile, caPassword);
assertTrue(X509Utils.isRevoked(cert1, caRevocationList));
// generate a second certificate
X509Certificate cert2 = X509Utils.newClientCertificate(userMetadata, caPrivateKey, caCert, storeFile.getParentFile());
// confirm second certificate IS NOT revoked
assertTrue(X509Utils.isRevoked(cert1, caRevocationList));
assertFalse(X509Utils.isRevoked(cert2, caRevocationList));
// revoke second certificate and then confirm it IS revoked
X509Utils.revoke(cert2, RevocationReason.ACompromise, caRevocationList, caPrivateKey);
assertTrue(X509Utils.isRevoked(cert1, caRevocationList));
assertTrue(X509Utils.isRevoked(cert2, caRevocationList));
// generate a third certificate
X509Certificate cert3 = X509Utils.newClientCertificate(userMetadata, caPrivateKey, caCert, storeFile.getParentFile());
// confirm third certificate IS NOT revoked
assertTrue(X509Utils.isRevoked(cert1, caRevocationList));
assertTrue(X509Utils.isRevoked(cert2, caRevocationList));
assertFalse(X509Utils.isRevoked(cert3, caRevocationList));
// revoke third certificate and then confirm it IS revoked
X509Utils.revoke(cert3, RevocationReason.ACompromise, caRevocationList, caPrivateKey);
assertTrue(X509Utils.isRevoked(cert1, caRevocationList));
assertTrue(X509Utils.isRevoked(cert2, caRevocationList));
assertTrue(X509Utils.isRevoked(cert3, caRevocationList));
}
}

Chargement…
Annuler
Enregistrer