/* ==================================================================
* BCCertificateService.java - Dec 5, 2012 10:42:17 AM
*
* Copyright 2007-2012 SolarNetwork.net Dev Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 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
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
* ==================================================================
*/
package net.solarnetwork.pki.bc;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.math.BigInteger;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.CertPath;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import javax.security.auth.x500.X500Principal;
import net.solarnetwork.support.CertificateException;
import net.solarnetwork.support.CertificateService;
import net.solarnetwork.support.CertificationAuthorityService;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.asn1.x509.X509Extension;
import org.bouncycastle.cert.CertIOException;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX500NameUtil;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder;
import org.bouncycastle.operator.DigestCalculatorProvider;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.OperatorException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
import org.bouncycastle.util.io.pem.PemWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Bouncy Castle implementation of {@link CertificateService}.
*
* @author matt
* @version 1.0
*/
public class BCCertificateService implements CertificateService, CertificationAuthorityService {
private final AtomicLong counter = new AtomicLong(System.currentTimeMillis());
private int certificateExpireDays = 730;
private int authorityExpireDays = 7300;
private String signatureAlgorithm = "SHA256WithRSA";
private final Logger log = LoggerFactory.getLogger(getClass());
@Override
public X509Certificate generateCertificate(String dn, PublicKey publicKey, PrivateKey privateKey) {
X500Principal issuer = new X500Principal(dn);
Date now = new Date();
Date expire = new Date(now.getTime() + (1000L * 60L * 60L * 24L * certificateExpireDays));
JcaX509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(issuer, new BigInteger(
String.valueOf(counter.incrementAndGet())), now, expire, issuer, publicKey);
JcaContentSignerBuilder signerBuilder = new JcaContentSignerBuilder(signatureAlgorithm);
ContentSigner signer;
try {
signer = signerBuilder.build(privateKey);
} catch ( OperatorCreationException e ) {
throw new CertificateException("Error signing certificate", e);
}
X509CertificateHolder holder = builder.build(signer);
JcaX509CertificateConverter converter = new JcaX509CertificateConverter();
try {
return converter.getCertificate(holder);
} catch ( java.security.cert.CertificateException e ) {
throw new CertificateException("Error creating certificate", e);
}
}
@Override
public X509Certificate generateCertificationAuthorityCertificate(String dn, PublicKey publicKey,
PrivateKey privateKey) {
X500Principal issuer = new X500Principal(dn);
Date now = new Date();
Date expire = new Date(now.getTime() + (1000L * 60L * 60L * 24L * authorityExpireDays));
JcaX509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(issuer,
new BigInteger("0"), now, expire, issuer, publicKey);
JcaContentSignerBuilder signerBuilder = new JcaContentSignerBuilder(signatureAlgorithm);
DefaultDigestAlgorithmIdentifierFinder digestAlgFinder = new DefaultDigestAlgorithmIdentifierFinder();
ContentSigner signer;
try {
DigestCalculatorProvider digestCalcProvider = new JcaDigestCalculatorProviderBuilder()
.setProvider(new BouncyCastleProvider()).build();
JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils(
digestCalcProvider.get(digestAlgFinder.find("SHA-256")));
builder.addExtension(X509Extension.basicConstraints, true, new BasicConstraints(true));
builder.addExtension(X509Extension.subjectKeyIdentifier, false,
extUtils.createSubjectKeyIdentifier(publicKey));
builder.addExtension(X509Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature
| KeyUsage.nonRepudiation | KeyUsage.keyCertSign | KeyUsage.cRLSign));
builder.addExtension(X509Extension.authorityKeyIdentifier, false,
extUtils.createAuthorityKeyIdentifier(publicKey));
signer = signerBuilder.build(privateKey);
} catch ( OperatorCreationException e ) {
log.error("Error generating CA certificate [{}]", dn, e);
throw new CertificateException("Error signing CA certificate", e);
} catch ( CertIOException e ) {
log.error("Error generating CA certificate [{}]", dn, e);
throw new CertificateException("Error signing CA certificate", e);
}
X509CertificateHolder holder = builder.build(signer);
JcaX509CertificateConverter converter = new JcaX509CertificateConverter();
try {
return converter.getCertificate(holder);
} catch ( java.security.cert.CertificateException e ) {
throw new CertificateException("Error creating certificate", e);
}
}
@Override
public X509Certificate signCertificate(String csrPEM, X509Certificate caCert, PrivateKey privateKey)
throws CertificateException {
if ( !csrPEM.matches("(?is)^\\s*-----BEGIN.*") ) {
// let's throw in the guards
csrPEM = "-----BEGIN CERTIFICATE REQUEST-----\n" + csrPEM
+ "\n-----END CERTIFICATE REQUEST-----\n";
}
PemReader reader = null;
try {
reader = new PemReader(new StringReader(csrPEM));
PemObject pemObj = reader.readPemObject();
log.debug("Parsed PEM type {}", pemObj.getType());
PKCS10CertificationRequest csr = new PKCS10CertificationRequest(pemObj.getContent());
Date now = new Date();
Date expire = new Date(now.getTime() + (1000L * 60L * 60L * 24L * certificateExpireDays));
X509v3CertificateBuilder builder = new X509v3CertificateBuilder(
JcaX500NameUtil.getIssuer(caCert), new BigInteger(String.valueOf(counter
.incrementAndGet())), now, expire, csr.getSubject(),
csr.getSubjectPublicKeyInfo());
JcaContentSignerBuilder signerBuilder = new JcaContentSignerBuilder(signatureAlgorithm);
ContentSigner signer;
DefaultDigestAlgorithmIdentifierFinder digestAlgFinder = new DefaultDigestAlgorithmIdentifierFinder();
try {
DigestCalculatorProvider digestCalcProvider = new JcaDigestCalculatorProviderBuilder()
.setProvider(new BouncyCastleProvider()).build();
JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils(
digestCalcProvider.get(digestAlgFinder.find("SHA-256")));
builder.addExtension(X509Extension.basicConstraints, false, new BasicConstraints(false));
builder.addExtension(X509Extension.subjectKeyIdentifier, false,
extUtils.createSubjectKeyIdentifier(csr.getSubjectPublicKeyInfo()));
builder.addExtension(X509Extension.authorityKeyIdentifier, false,
extUtils.createAuthorityKeyIdentifier(caCert));
signer = signerBuilder.build(privateKey);
} catch ( OperatorException e ) {
log.error("Error signing CSR {}", csr.getSubject(), e);
throw new CertificateException("Error signing CSR" + csr.getSubject() + ": "
+ e.getMessage());
} catch ( CertificateEncodingException e ) {
log.error("Error signing CSR {}", csr.getSubject().toString(), e);
throw new CertificateException("Error signing CSR" + csr.getSubject() + ": "
+ e.getMessage());
}
X509CertificateHolder holder = builder.build(signer);
JcaX509CertificateConverter converter = new JcaX509CertificateConverter();
try {
return converter.getCertificate(holder);
} catch ( java.security.cert.CertificateException e ) {
throw new CertificateException("Error creating certificate", e);
}
} catch ( IOException e ) {
throw new CertificateException("Error signing CSR", e);
} finally {
if ( reader != null ) {
try {
reader.close();
} catch ( IOException e2 ) {
log.warn("IOException closing PemReader", e2);
}
}
}
}
@Override
public String generatePKCS10CertificateRequestString(X509Certificate cert, PrivateKey privateKey)
throws CertificateException {
X509CertificateHolder holder;
try {
holder = new JcaX509CertificateHolder(cert);
} catch ( CertificateEncodingException e ) {
throw new CertificateException("Error creating CSR", e);
}
PKCS10CertificationRequestBuilder builder = new PKCS10CertificationRequestBuilder(
holder.getSubject(), holder.getSubjectPublicKeyInfo());
JcaContentSignerBuilder signerBuilder = new JcaContentSignerBuilder(signatureAlgorithm);
ContentSigner signer;
try {
signer = signerBuilder.build(privateKey);
} catch ( OperatorCreationException e ) {
throw new CertificateException("Error signing certificate request", e);
}
PKCS10CertificationRequest csr = builder.build(signer);
StringWriter writer = new StringWriter();
PemWriter pemWriter = new PemWriter(writer);
try {
pemWriter.writeObject(new PemObject("CERTIFICATE REQUEST", csr.getEncoded()));
} catch ( IOException e ) {
throw new CertificateException("Error signing certificate", e);
} finally {
try {
pemWriter.flush();
pemWriter.close();
writer.close();
} catch ( IOException e ) {
// ignore this
}
}
return writer.toString();
}
private void orderCertificateChain(Map<X500Principal, X509Certificate> map,
List<X509Certificate> results, X509Certificate c) {
X509Certificate parent = map.get(c.getIssuerX500Principal());
if ( parent != null ) {
orderCertificateChain(map, results, parent);
}
// find parent in results, or else add to end
for ( ListIterator<X509Certificate> itr = results.listIterator(); itr.hasNext(); ) {
X509Certificate p = itr.next();
if ( p.getSubjectDN().equals(c.getIssuerDN()) ) {
itr.previous();
itr.add(c);
break;
}
}
map.remove(c.getSubjectX500Principal());
}
private void orderCertificateChain(Map<X500Principal, X509Certificate> map,
List<X509Certificate> results) {
while ( map.size() > 0 ) {
orderCertificateChain(map, results, map.values().iterator().next());
}
}
@Override
public X509Certificate[] parsePKCS7CertificateChainString(String pem) throws CertificateException {
if ( !pem.matches("(?is)^\\s*-----BEGIN.*") ) {
// let's throw in the guards
pem = "-----BEGIN CERTIFICATE CHAIN-----\n" + pem + "\n-----END CERTIFICATE CHAIN-----\n";
}
PemReader reader = new PemReader(new StringReader(pem));
List<X509Certificate> results = new ArrayList<X509Certificate>(3);
try {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
PemObject pemObj = reader.readPemObject();
log.debug("Parsed PEM type {}", pemObj.getType());
Collection<? extends Certificate> certs = cf.generateCertificates(new ByteArrayInputStream(
pemObj.getContent()));
// OK barf, generateCertificates() and even CertPath doesn't return the chain in order
// (see http://bugs.sun.com/view_bug.do?bug_id=6238093; but we can't use the Sun-specific
// workaround listed there). So let's try to order them ourselves
Map<X500Principal, X509Certificate> map = new LinkedHashMap<X500Principal, X509Certificate>();
for ( Certificate c : certs ) {
X509Certificate x509 = (X509Certificate) c;
if ( x509.getIssuerDN().equals(x509.getSubjectDN()) ) {
// root CA
results.add(x509);
} else {
map.put(x509.getSubjectX500Principal(), x509);
}
}
if ( results.size() == 0 ) {
// no root, just add everything to list
results.addAll(map.values());
} else {
orderCertificateChain(map, results);
}
} catch ( IOException e ) {
throw new CertificateException("Error reading certificate", e);
} catch ( java.security.cert.CertificateException e ) {
throw new CertificateException("Error loading CertificateFactory", e);
} finally {
try {
reader.close();
} catch ( IOException e ) {
// ignore me
}
}
return results.toArray(new X509Certificate[results.size()]);
}
@Override
public String generatePKCS7CertificateChainString(X509Certificate[] chain)
throws CertificateException {
try {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
List<X509Certificate> chainList = Arrays.asList(chain);
CertPath path = cf.generateCertPath(chainList);
StringWriter out = new StringWriter();
PemWriter writer = new PemWriter(out);
PemObject pemObj = new PemObject("CERTIFICATE" + (chain.length > 1 ? " CHAIN" : ""),
path.getEncoded("PKCS7"));
writer.writeObject(pemObj);
writer.flush();
writer.close();
out.close();
String result = out.toString();
log.debug("Generated cert chain:\n{}", result);
return result;
} catch ( IOException e ) {
throw new CertificateException("Error generating PKCS#7 chain", e);
} catch ( java.security.cert.CertificateException e ) {
throw new CertificateException("Error generating PKCS#7 chain", e);
}
}
public void setCertificateExpireDays(int certificateExpireDays) {
this.certificateExpireDays = certificateExpireDays;
}
public void setSignatureAlgorithm(String signatureAlgorithm) {
this.signatureAlgorithm = signatureAlgorithm;
}
public void setAuthorityExpireDays(int authorityExpireDays) {
this.authorityExpireDays = authorityExpireDays;
}
}