/*******************************************************************************
* Copyright (C) 2010 Marco Sandrini
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this program.
* If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
package org.casbah.provider.openssl;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.security.PrivateKey;
import java.security.cert.CRLException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import javax.security.auth.x500.X500Principal;
import org.apache.commons.io.FileUtils;
import org.casbah.provider.CAProvider;
import org.casbah.provider.CAProviderException;
import org.casbah.provider.CertificateMetainfo;
import org.casbah.provider.KeyCertificateBundle;
import org.casbah.provider.SSLeayEncoder;
import org.casbah.provider.Principal.MatchingRule;
import org.casbah.provider.Principal.PrincipalField;
public class OpenSslCAProvider implements CAProvider{
private static final String KEY_SUFFIX = ".key";
private static final String REQ_SUFFIX = ".csr";
private static final String REQ_PATH = "requests";
private static final Logger logger = Logger.getLogger(OpenSslCAProvider.class.getCanonicalName());
private static final String KEY_FILE = "keys" + File.separatorChar + "ca.key";
private static final String CACERT_FILE = "certs" + File.separatorChar + "ca.cer";
private static final String SERIAL_FILE = "serial.txt";
private static final String CERT_SUFFIX = ".pem";
private static final String EXPORTED_CERT_SUFFIX = ".crt";
private static final String DATABASE_FILE = "database.txt";
private static final String CERT_PATH = "certs";
private static final String KEY_PATH = "keys";
private static final String CA_PUBLIC_CERT = "ca.cer";
private static final String CONFIG_FILE = "openssl.cnf";
private static final String CRL_FILE = "crl.der";
private static final String CRL_NUMBER = "crlnumber.txt";
private static final int CRL_VALIDITY_DAYS = 7;
private final File caRootDir;
private String keypass;
private final String openSslExecutable;
public OpenSslCAProvider(final String openSslExecutable, final File caRootDir, String keypass) {
this.openSslExecutable = openSslExecutable;
this.keypass = keypass;
this.caRootDir = caRootDir;
}
@Override
public X509Certificate getCACertificate() throws CAProviderException {
if (!isCASetup()) {
throw new CAProviderException("CA Not initialized", null);
}
try {
return getCertificate(new File(new File(caRootDir,CERT_PATH), CA_PUBLIC_CERT));
} catch (FileNotFoundException fnfe) {
throw new CAProviderException("Could not find public cert file", fnfe);
} catch (CertificateException ce) {
throw new CAProviderException("Could not parse public certificate", ce);
}
}
private X509Certificate getCertificate(File certFile) throws CertificateException, FileNotFoundException {
FileInputStream fis = new FileInputStream(certFile);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
return (X509Certificate) cf.generateCertificate(fis);
}
@Override
public List<CertificateMetainfo> getIssuedCertificates() throws CAProviderException {
OpenSslDatabaseAdapter dbAdapter = new OpenSslDatabaseAdapter(new File(caRootDir.getAbsolutePath(), DATABASE_FILE));
dbAdapter.parse();
return dbAdapter.getIssuedCertificates();
}
@Override
public X509Certificate sign(String csr) throws CAProviderException {
File tempFile = null;
try {
tempFile = File.createTempFile("ssl", "csr");
FileWriter writer = new FileWriter(tempFile);
BufferedWriter bw = new BufferedWriter(writer);
bw.write(csr);
bw.close();
} catch (IOException ioe) {
if (tempFile.exists()) {
tempFile.delete();
}
throw new CAProviderException("Failed while writing csr to temp file", ioe);
}
try {
String nextSerial = new OpenSslSerialAdapter(new File(caRootDir, SERIAL_FILE)).getNextSerialNumber();
File certFile = new File(caRootDir, CERT_PATH + File.separator + nextSerial + CERT_SUFFIX);
return signCsr(tempFile, certFile);
} finally {
if (tempFile.exists()) {
tempFile.delete();
}
}
}
private X509Certificate signCsr(File csrFile, File certFile) throws CAProviderException {
try {
OpenSslWrapper wrapper = new OpenSslWrapper(openSslExecutable, caRootDir);
StringBuffer output = new StringBuffer();
StringBuffer error = new StringBuffer();
String input = keypass + "\n";
OpenSslWrapperArgumentList args = new OpenSslWrapperArgumentList();
args.setCA().setNoText().setBatch().addConfig(new File(caRootDir, CONFIG_FILE))
.addInFile(csrFile).addOutdir(new File(caRootDir, CERT_PATH)).addStdinPassin().setVerbose();
System.out.println(args.toString());
if (wrapper.executeCommand(input, output, error, args.toList()) == 0) {
return getCertificate(certFile);
} else {
throw new CAProviderException("Error while signing the certificate", new OpenSslNativeException(error.toString()));
}
} catch (InterruptedException ie) {
throw new CAProviderException("Could not sign CSR", ie);
} catch (CertificateException ce) {
throw new CAProviderException("Cannot read created certificate", ce);
} catch (FileNotFoundException e) {
throw new CAProviderException("Error while reading created certificate", e);
} catch (IOException e) {
throw new CAProviderException("Error while reading created certificate", e);
}
}
@Override
public boolean isCASetup() {
if (!caRootDir.exists() || !caRootDir.isDirectory() || !caRootDir.canWrite()) {
return false;
}
File caKey = new File(caRootDir, KEY_FILE);
if (!caKey.exists() || !caKey.isFile() || !caKey.canRead()) {
logger.warning("private key is not present");
return false;
}
File caCert = new File(new File(caRootDir, CERT_PATH) , CA_PUBLIC_CERT);
if (!caCert.exists() || !caCert.isFile() || !caCert.canRead()) {
logger.warning("public certificate not present");
return false;
}
File caReqs = new File(caRootDir, REQ_PATH);
if (!caReqs.exists() || !caReqs.isDirectory() || !caReqs.canWrite()) {
logger.warning("requests directory not present");
return false;
}
File database = new File(caRootDir, DATABASE_FILE);
if (!database.exists() || !database.isFile() || !database.canWrite()) {
logger.warning("database file not present");
return false;
}
File serial = new File(caRootDir, SERIAL_FILE);
if (!serial.exists() || !serial.isFile() || !serial.canWrite()) {
logger.warning("serial number file not present");
return false;
}
File crlNumber = new File(caRootDir, CRL_NUMBER);
if (!crlNumber.exists() || (!crlNumber.isFile()) || (!crlNumber.canWrite())) {
logger.warning("CRL number file not present");
return false;
}
return true;
}
@Override
public X509Certificate getIssuedCertificateBySerialNumber(
String serialNumber) throws CAProviderException {
try {
return getCertificate(new File(new File(caRootDir,CERT_PATH), serialNumber + CERT_SUFFIX));
} catch (FileNotFoundException fnfe) {
throw new CAProviderException("Could not find public cert file", fnfe);
} catch (CertificateException ce) {
throw new CAProviderException("Could not parse public certificate", ce);
}
}
@Override
public String getProviderVersion() throws CAProviderException {
try {
OpenSslWrapper wrapper = new OpenSslWrapper(openSslExecutable, caRootDir);
OpenSslWrapperArgumentList args = new OpenSslWrapperArgumentList().setVersion();
StringBuffer output = new StringBuffer();
StringBuffer error = new StringBuffer();
int result = wrapper.executeCommand(output, error, args.toList());
if (result != 0) {
throw new CAProviderException("Could not execute " + openSslExecutable, null);
}
System.out.println(output.toString());
return output.toString();
} catch (InterruptedException ie) {
throw new CAProviderException("An error occurred while executing openssl", ie);
} catch (IOException ioe) {
throw new CAProviderException("AN error occurred while executing openssl", ioe);
}
}
@Override
public boolean setUpCA(X500Principal principal, String keypass) throws CAProviderException {
generateDirectoryStructure();
copyDefaultCnfFile();
generatePrivateKey(keypass, new File(caRootDir, KEY_FILE));
this.keypass = keypass;
generateSelfSignedCert(principal, keypass);
OpenSslDatabaseAdapter dbAdapter = new OpenSslDatabaseAdapter(new File(caRootDir, DATABASE_FILE));
dbAdapter.createEmptyDatabase();
OpenSslSerialAdapter serialAdapter = new OpenSslSerialAdapter(new File(caRootDir, SERIAL_FILE));
serialAdapter.initializeSerialNumberFile();
initializeCrlNumberFile();
return true;
}
private void generateDirectoryStructure() {
File certDir = new File(caRootDir, CERT_PATH);
if (!certDir.exists()) {
certDir.mkdirs();
}
File requestDir = new File(caRootDir, REQ_PATH);
if (!requestDir.exists()) {
requestDir.mkdirs();
}
File keyDir = new File(caRootDir, KEY_PATH);
if (!keyDir.exists()) {
keyDir.mkdirs();
}
}
private void copyDefaultCnfFile() throws CAProviderException {
try {
InputStream in = this.getClass().getResourceAsStream("/org/casbah/provider/openssl/" + CONFIG_FILE);
FileOutputStream out = new FileOutputStream(new File(caRootDir,CONFIG_FILE));
int i = 0;
while ((i = in.read()) != -1) {
out.write(i);
}
in.close();
out.close();
} catch (Exception e) {
throw new CAProviderException("Could not create default openssl.cnf", e);
}
}
private void initializeCrlNumberFile() throws CAProviderException {
try {
FileWriter writer = new FileWriter(new File(caRootDir, CRL_NUMBER));
writer.write("01\n");
writer.close();
} catch (IOException e) {
throw new CAProviderException("Could not initialize CRL number file", e);
}
}
private PrivateKey generatePrivateKey(String keypass, File outFile) throws CAProviderException {
try {
logger.info("Key generation started");
OpenSslWrapper wrapper = new OpenSslWrapper(openSslExecutable, caRootDir);
OpenSslWrapperArgumentList args = new OpenSslWrapperArgumentList().addGenrsa().
addStdinPassout().setDes3().addOutFile(outFile).
addKeyLength(2048);
String input = keypass + '\n';
StringBuffer output = new StringBuffer();
StringBuffer error = new StringBuffer();
if (wrapper.executeCommand(input, output, error, args.toList()) != 0) {
throw new CAProviderException("Could not generate the private key", null);
}
String pemData = FileUtils.readFileToString(outFile);
return SSLeayEncoder.decodeKey(pemData, keypass);
} catch (InterruptedException ie) {
throw new CAProviderException("Could not generate the private key", ie);
} catch (IOException ioe) {
throw new CAProviderException("An IO error prevented key generation", ioe);
}
}
private void generateSelfSignedCert(X500Principal principal, String keypass) throws CAProviderException {
try {
String subject = OpenSslDnConverter.convertToOpenSsl(principal.getName());
OpenSslWrapper wrapper = new OpenSslWrapper(openSslExecutable, caRootDir);
OpenSslWrapperArgumentList args = new OpenSslWrapperArgumentList().setReq().
setNew().setX509().setBatch().addDays(365).addSubject(subject).addStdinPassin().
addKey(new File(caRootDir,KEY_FILE)).addOutFile(new File(caRootDir, CACERT_FILE));
String input = keypass + "\n";
StringBuffer output = new StringBuffer();
StringBuffer error = new StringBuffer();
if (wrapper.executeCommand(input, output, error, args.toList()) != 0) {
throw new CAProviderException("Could not generated self-signed cert", null);
}
} catch (InterruptedException e) {
throw new CAProviderException("Generation of self-signed certificate failed", e);
} catch (IOException e) {
throw new CAProviderException("Generation of self-signed certificate failed", e);
}
}
@Override
public X509CRL getLatestCrl(boolean generateCrl) throws CAProviderException {
X509CRL result = null;
if (generateCrl) {
result = generateNewCrl();
} else {
try {
result = loadCrlFromFile();
} catch (FileNotFoundException fnfe) {
result = generateNewCrl();
} catch (CertificateException e) {
throw new CAProviderException("Could not parse CRL file", e);
} catch (CRLException e) {
throw new CAProviderException("Could not parse CRL file", e);
} catch (IOException e) {
throw new CAProviderException("Could not parse CRL file", e);
}
if (result.getNextUpdate().before(new Date())) {
result = generateNewCrl();
}
}
return result;
}
private X509CRL loadCrlFromFile() throws FileNotFoundException, IOException, CRLException, CertificateException {
FileInputStream fis = new FileInputStream(new File(caRootDir, CRL_FILE));
CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509CRL result = (X509CRL) cf.generateCRL(fis);
fis.close();
return result;
}
private X509CRL generateNewCrl() throws CAProviderException {
try {
OpenSslWrapper wrapper = new OpenSslWrapper(openSslExecutable, caRootDir);
OpenSslWrapperArgumentList args = new OpenSslWrapperArgumentList().
setCA().addConfig(new File(caRootDir, CONFIG_FILE)).setGencrl().
addStdinPassin().
addCrlDays(CRL_VALIDITY_DAYS).addOutFile(new File(caRootDir,CRL_FILE));
String input = keypass + "\n";
StringBuffer output = new StringBuffer();
StringBuffer error = new StringBuffer();
if (wrapper.executeCommand(input, output, error, args.toList()) != 0) {
throw new CAProviderException("Could not generated CRL file", new OpenSslNativeException(error.toString()));
}
} catch (InterruptedException ie) {
throw new CAProviderException("An error prevented generation of new CRL file", ie);
} catch (IOException e) {
throw new CAProviderException("An error prevented generatio of new CRL file", e);
}
try {
return loadCrlFromFile();
} catch (Exception e) {
throw new CAProviderException("New crl has been generated but could not be read", e);
}
}
private void generateCsr(X500Principal principal, File keyFile, String keypass, File csrFile) throws CAProviderException {
try {
String subject = OpenSslDnConverter.convertToOpenSsl(principal.getName());
OpenSslWrapper wrapper = new OpenSslWrapper(openSslExecutable, caRootDir);
OpenSslWrapperArgumentList args = new OpenSslWrapperArgumentList().setReq().
setNew().setBatch().addDays(365).addSubject(subject).addStdinPassin().
addKey(keyFile).addOutFile(csrFile);
String input = keypass + "\n";
StringBuffer output = new StringBuffer();
StringBuffer error = new StringBuffer();
if (wrapper.executeCommand(input, output, error, args.toList()) != 0) {
throw new CAProviderException("Could not generated self-signed cert", null);
}
} catch (InterruptedException ie) {
throw new CAProviderException("Could not generate CSR file", ie);
} catch (IOException e) {
throw new CAProviderException("Could not generate CSR file", e);
}
}
@Override
public KeyCertificateBundle getKeyCertificateBundle(X500Principal principal,
String keypass) throws CAProviderException {
OpenSslSerialAdapter serialAdapter = new OpenSslSerialAdapter(new File(caRootDir, SERIAL_FILE));
String nextSerial = serialAdapter.getNextSerialNumber();
File keyFile = new File(caRootDir,KEY_PATH + File.separator + nextSerial + KEY_SUFFIX);
PrivateKey privateKey = generatePrivateKey(keypass, keyFile);
File csrFile = new File(caRootDir, REQ_PATH + File.separator + nextSerial + REQ_SUFFIX);
generateCsr(principal, keyFile, keypass, csrFile);
File certFile = new File(caRootDir, CERT_PATH + File.separator + nextSerial + CERT_SUFFIX);
X509Certificate cert = signCsr(csrFile, certFile);
return new KeyCertificateBundle(privateKey, cert);
}
@Override
public Map<PrincipalField, MatchingRule> getRuleMap() {
// TODO read this from configuration
Map<PrincipalField, MatchingRule> result = new HashMap<PrincipalField, MatchingRule>();
result.put(PrincipalField.C, MatchingRule.MATCH);
result.put(PrincipalField.ST, MatchingRule.MATCH);
result.put(PrincipalField.L, MatchingRule.MATCH);
result.put(PrincipalField.O, MatchingRule.MATCH);
result.put(PrincipalField.OU, MatchingRule.OPTIONAL);
result.put(PrincipalField.CN, MatchingRule.PROVIDED);
return result;
}
}