/**
* Copyright (c) 2009 - 2012 Red Hat, Inc.
*
* This software is licensed to you under the GNU General Public License,
* version 2 (GPLv2). There is NO WARRANTY for this software, express or
* implied, including the implied warranties of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2
* along with this software; if not, see
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
*
* Red Hat trademarks are not licensed under GPLv2. No permission is
* granted to use or replicate Red Hat trademarks that are incorporated
* in this software or its documentation.
*/
package org.candlepin.pki.impl;
import org.candlepin.pki.PKIReader;
import org.candlepin.pki.PKIUtility;
import org.candlepin.pki.SubjectKeyIdentifierWriter;
import org.candlepin.pki.X509ByteExtensionWrapper;
import org.candlepin.pki.X509CRLEntryWrapper;
import org.candlepin.pki.X509ExtensionWrapper;
import org.candlepin.util.Util;
import com.google.inject.Inject;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.DERUTF8String;
import org.bouncycastle.asn1.misc.MiscObjectIdentifiers;
import org.bouncycastle.asn1.misc.NetscapeCertType;
import org.bouncycastle.asn1.x509.CRLNumber;
import org.bouncycastle.asn1.x509.CRLReason;
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.asn1.x509.X509Extensions;
import org.bouncycastle.openssl.PEMWriter;
import org.bouncycastle.x509.X509V2CRLGenerator;
import org.bouncycastle.x509.X509V3CertificateGenerator;
import org.bouncycastle.x509.extension.AuthorityKeyIdentifierStructure;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyPair;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.List;
import java.util.Set;
import javax.security.auth.x500.X500Principal;
/**
* The default {@link PKIUtility} for Candlepin.
* This class implements methods to create X509 Certificates, X509 CRLs, encode
* objects in PEM format (for saving to the db or sending to the client), and
* decode raw ASN.1 DER values (as read from a Certificate/CRL).
*
* All code that imports bouncycastle should live either in this module,
* or in {@link BouncyCastlePKIReader}
*
* (March 24, 2011) Notes on implementing a PKIUtility with NSS/JSS:
*
* JSS provides classes and functions to generate X509Certificates (see CertificateInfo,
* for example).
*
* PEM encoding requires us to determine the object type (which we know), add the correct
* header and footer to the output, base64 encode the DER for the object, and line wrap
* the base64 encoding.
*
* decodeDERValue should be simple, as JSS provides code to parse ASN.1, but I wasn't
* able to get it to work.
*
* The big one is CRL generation. JSS has no code to generate CRLs in any format. We'll
* have to use the raw ASN.1 libraries to build up our own properly formatted CRL DER
* representation, then PEM encode it.
*
* See also {@link BouncyCastlePKIReader} for more notes on using NSS/JSS, and a note
* about not using bouncycastle as the JSSE provider.
*/
@SuppressWarnings("deprecation")
public class BouncyCastlePKIUtility extends PKIUtility {
private static Logger log = LoggerFactory.getLogger(BouncyCastlePKIUtility.class);
@Inject
public BouncyCastlePKIUtility(PKIReader reader,
SubjectKeyIdentifierWriter subjectKeyWriter) {
super(reader, subjectKeyWriter);
}
@Override
public X509Certificate createX509Certificate(String dn,
Set<X509ExtensionWrapper> extensions, Set<X509ByteExtensionWrapper> byteExtensions,
Date startDate, Date endDate,
KeyPair clientKeyPair, BigInteger serialNumber, String alternateName)
throws GeneralSecurityException, IOException {
X509V3CertificateGenerator certGen = new X509V3CertificateGenerator();
X509Certificate caCert = reader.getCACert();
// set cert fields
certGen.setSerialNumber(serialNumber);
certGen.setIssuerDN(caCert.getSubjectX500Principal());
certGen.setNotBefore(startDate);
certGen.setNotAfter(endDate);
X500Principal subjectPrincipal = new X500Principal(dn);
certGen.setSubjectDN(subjectPrincipal);
certGen.setPublicKey(clientKeyPair.getPublic());
certGen.setSignatureAlgorithm(SIGNATURE_ALGO);
// set key usage - required for proper x509 function
KeyUsage keyUsage = new KeyUsage(KeyUsage.digitalSignature |
KeyUsage.keyEncipherment | KeyUsage.dataEncipherment);
// add SSL extensions - required for proper x509 function
NetscapeCertType certType = new NetscapeCertType(
NetscapeCertType.sslClient | NetscapeCertType.smime);
certGen.addExtension(MiscObjectIdentifiers.netscapeCertType.toString(),
false, certType);
certGen.addExtension(X509Extensions.KeyUsage.toString(), false,
keyUsage);
certGen.addExtension(X509Extensions.AuthorityKeyIdentifier, false,
new AuthorityKeyIdentifierStructure(caCert));
certGen.addExtension(X509Extensions.SubjectKeyIdentifier, false,
subjectKeyWriter.getSubjectKeyIdentifier(clientKeyPair, extensions));
certGen.addExtension(X509Extensions.ExtendedKeyUsage, false,
new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth));
// Add an alternate name if provided
if (alternateName != null) {
GeneralName name = new GeneralName(GeneralName.uniformResourceIdentifier,
"CN=" + alternateName);
certGen.addExtension(X509Extensions.SubjectAlternativeName, false,
new GeneralNames(name));
}
if (extensions != null) {
for (X509ExtensionWrapper wrapper : extensions) {
// Bouncycastle hates null values. So, set them to blank
// if they are null
String value = wrapper.getValue() == null ? "" : wrapper.getValue();
certGen.addExtension(wrapper.getOid(), wrapper.isCritical(),
new DERUTF8String(value));
}
}
if (byteExtensions != null) {
for (X509ByteExtensionWrapper wrapper : byteExtensions) {
// Bouncycastle hates null values. So, set them to blank
// if they are null
byte[] value = wrapper.getValue() == null ? new byte[0] :
wrapper.getValue();
certGen.addExtension(wrapper.getOid(), wrapper.isCritical(),
new DEROctetString(value));
}
}
// Generate the certificate
return certGen.generate(reader.getCaKey());
}
@Override
public X509CRL createX509CRL(List<X509CRLEntryWrapper> entries, BigInteger crlNumber) {
try {
X509Certificate caCert = reader.getCACert();
X509V2CRLGenerator generator = new X509V2CRLGenerator();
generator.setIssuerDN(caCert.getIssuerX500Principal());
generator.setThisUpdate(new Date());
generator.setNextUpdate(Util.tomorrow());
generator.setSignatureAlgorithm(SIGNATURE_ALGO);
// add all the CRL entries.
for (X509CRLEntryWrapper entry : entries) {
generator.addCRLEntry(entry.getSerialNumber(), entry.getRevocationDate(),
CRLReason.privilegeWithdrawn);
}
log.info("Completed adding CRL numbers to the certificate.");
generator.addExtension(X509Extensions.AuthorityKeyIdentifier,
false, new AuthorityKeyIdentifierStructure(caCert));
generator.addExtension(X509Extensions.CRLNumber, false,
new CRLNumber(crlNumber));
return generator.generate(reader.getCaKey());
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
private byte[] getPemEncoded(Object obj) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
OutputStreamWriter oswriter = new OutputStreamWriter(byteArrayOutputStream);
PEMWriter writer = new PEMWriter(oswriter);
writer.writeObject(obj);
writer.close();
return byteArrayOutputStream.toByteArray();
}
@Override
public byte[] getPemEncoded(X509Certificate cert) throws IOException {
return getPemEncoded((Object) cert);
}
@Override
public byte[] getPemEncoded(Key key) throws IOException {
return getPemEncoded((Object) key);
}
@Override
public byte[] getPemEncoded(X509CRL crl) throws IOException {
return getPemEncoded((Object) crl);
}
@Override
public String decodeDERValue(byte[] value) {
ASN1InputStream vis = null;
ASN1InputStream decoded = null;
try {
vis = new ASN1InputStream(value);
decoded = new ASN1InputStream(
((DEROctetString) vis.readObject()).getOctets());
return decoded.readObject().toString();
}
catch (IOException e) {
throw new RuntimeException(e);
}
finally {
if (vis != null) {
try {
vis.close();
}
catch (IOException e) {
log.warn("failed to close ASN1 stream", e);
}
}
if (decoded != null) {
try {
decoded.close();
}
catch (IOException e) {
log.warn("failed to close ASN1 stream", e);
}
}
}
}
}