/* See LICENSE for licensing and NOTICE for copyright. */
package org.cryptacular.util;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
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.List;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.GeneralNamesBuilder;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.asn1.x509.PolicyInformation;
import org.cryptacular.EncodingException;
import org.cryptacular.StreamException;
import org.cryptacular.x509.ExtensionReader;
import org.cryptacular.x509.GeneralNameType;
import org.cryptacular.x509.KeyUsageBits;
import org.cryptacular.x509.dn.NameReader;
import org.cryptacular.x509.dn.StandardAttributeType;
/**
* Utility class providing convenience methods for common operations on X.509 certificates.
*
* @author Middleware Services
*/
public final class CertUtil
{
/** Private constructor of utility class. */
private CertUtil() {}
/**
* Gets the common name attribute (CN) of the certificate subject distinguished name.
*
* @param cert Certificate to examine.
*
* @return Subject CN or null if no CN attribute is defined in the subject DN.
*
* @throws EncodingException on cert field extraction.
*/
public static String subjectCN(final X509Certificate cert) throws EncodingException
{
return new NameReader(cert).readSubject().getValue(StandardAttributeType.CommonName);
}
/**
* Gets all subject alternative names defined on the given certificate.
*
* @param cert X.509 certificate to examine.
*
* @return List of subject alternative names or null if no subject alt names are defined.
*
* @throws EncodingException on cert field extraction.
*/
public static GeneralNames subjectAltNames(final X509Certificate cert) throws EncodingException
{
return new ExtensionReader(cert).readSubjectAlternativeName();
}
/**
* Gets all subject alternative names of the given type(s) on the given cert.
*
* @param cert X.509 certificate to examine.
* @param types One or more subject alternative name types to fetch.
*
* @return List of subject alternative names of the matching type(s) or null if none found.
*
* @throws EncodingException on cert field extraction.
*/
public static GeneralNames subjectAltNames(final X509Certificate cert, final GeneralNameType... types)
throws EncodingException
{
final GeneralNamesBuilder builder = new GeneralNamesBuilder();
final GeneralNames altNames = subjectAltNames(cert);
if (altNames != null) {
for (GeneralName name : altNames.getNames()) {
for (GeneralNameType type : types) {
if (type.ordinal() == name.getTagNo()) {
builder.addName(name);
}
}
}
}
final GeneralNames names = builder.build();
if (names.getNames().length == 0) {
return null;
}
return names;
}
/**
* Gets a list of all subject names defined for the given certificate. The list includes the first common name (CN)
* specified in the subject distinguished name (if defined) and all subject alternative names.
*
* @param cert X.509 certificate to examine.
*
* @return List of subject names.
*
* @throws EncodingException on cert field extraction.
*/
public static List<String> subjectNames(final X509Certificate cert) throws EncodingException
{
final List<String> names = new ArrayList<>();
final String cn = subjectCN(cert);
if (cn != null) {
names.add(cn);
}
final GeneralNames altNames = subjectAltNames(cert);
if (altNames == null) {
return names;
}
for (GeneralName name : altNames.getNames()) {
names.add(name.getName().toString());
}
return names;
}
/**
* Gets a list of subject names defined for the given certificate. The list includes the first common name (CN)
* specified in the subject distinguished name (if defined) and all subject alternative names of the given type.
*
* @param cert X.509 certificate to examine.
* @param types One or more subject alternative name types to fetch.
*
* @return List of subject names.
*
* @throws EncodingException on cert field extraction.
*/
public static List<String> subjectNames(final X509Certificate cert, final GeneralNameType... types)
throws EncodingException
{
final List<String> names = new ArrayList<>();
final String cn = subjectCN(cert);
if (cn != null) {
names.add(cn);
}
final GeneralNames altNames = subjectAltNames(cert, types);
if (altNames == null) {
return names;
}
for (GeneralName name : altNames.getNames()) {
names.add(name.getName().toString());
}
return names;
}
/**
* Finds a certificate whose public key is paired with the given private key.
*
* @param key Private key used to find matching public key.
* @param candidates Array of candidate certificates.
*
* @return Certificate whose public key forms a keypair with the private key or null if no match is found.
*
* @throws EncodingException on cert field extraction.
*/
public static X509Certificate findEntityCertificate(final PrivateKey key, final X509Certificate... candidates)
throws EncodingException
{
return findEntityCertificate(key, Arrays.asList(candidates));
}
/**
* Finds a certificate whose public key is paired with the given private key.
*
* @param key Private key used to find matching public key.
* @param candidates Collection of candidate certificates.
*
* @return Certificate whose public key forms a keypair with the private key or null if no match is found.
*
* @throws EncodingException on cert field extraction.
*/
public static X509Certificate findEntityCertificate(
final PrivateKey key,
final Collection<X509Certificate> candidates)
throws EncodingException
{
for (X509Certificate candidate : candidates) {
if (KeyPairUtil.isKeyPair(candidate.getPublicKey(), key)) {
return candidate;
}
}
return null;
}
/**
* Reads an X.509 certificate from ASN.1 encoded format in the file at the given location.
*
* @param path Path to file containing an DER or PEM encoded X.509 certificate.
*
* @return Certificate.
*
* @throws EncodingException on cert parsing errors.
* @throws StreamException on IO errors.
*/
public static X509Certificate readCertificate(final String path) throws EncodingException, StreamException
{
return readCertificate(StreamUtil.makeStream(new File(path)));
}
/**
* Reads an X.509 certificate from ASN.1 encoded format from the given file.
*
* @param file File containing an DER or PEM encoded X.509 certificate.
*
* @return Certificate.
*
* @throws EncodingException on cert parsing errors.
* @throws StreamException on IO errors.
*/
public static X509Certificate readCertificate(final File file) throws EncodingException, StreamException
{
return readCertificate(StreamUtil.makeStream(file));
}
/**
* Reads an X.509 certificate from ASN.1 encoded data in the given stream.
*
* @param in Input stream containing PEM or DER encoded X.509 certificate.
*
* @return Certificate.
*
* @throws EncodingException on cert parsing errors.
* @throws StreamException on IO errors.
*/
public static X509Certificate readCertificate(final InputStream in) throws EncodingException, StreamException
{
try {
final CertificateFactory factory = CertificateFactory.getInstance("X.509");
return (X509Certificate) factory.generateCertificate(in);
} catch (CertificateException e) {
if (e.getCause() instanceof IOException) {
throw new StreamException((IOException) e.getCause());
}
throw new EncodingException("Cannot decode certificate", e);
}
}
/**
* Creates an X.509 certificate from its ASN.1 encoded form.
*
* @param encoded PEM or DER encoded ASN.1 data.
*
* @return Certificate.
*
* @throws EncodingException on cert parsing errors.
*/
public static X509Certificate decodeCertificate(final byte[] encoded) throws EncodingException
{
return readCertificate(new ByteArrayInputStream(encoded));
}
/**
* Reads an X.509 certificate chain from ASN.1 encoded format in the file at the given location.
*
* @param path Path to file containing a sequence of PEM or DER encoded certificates or PKCS#7 certificate chain.
*
* @return Certificate.
*
* @throws EncodingException on cert parsing errors.
* @throws StreamException on IO errors.
*/
public static X509Certificate[] readCertificateChain(final String path) throws EncodingException, StreamException
{
return readCertificateChain(StreamUtil.makeStream(new File(path)));
}
/**
* Reads an X.509 certificate chain from ASN.1 encoded format from the given file.
*
* @param file File containing a sequence of PEM or DER encoded certificates or PKCS#7 certificate chain.
*
* @return Certificate.
*
* @throws EncodingException on cert parsing errors.
* @throws StreamException on IO errors.
*/
public static X509Certificate[] readCertificateChain(final File file) throws EncodingException, StreamException
{
return readCertificateChain(StreamUtil.makeStream(file));
}
/**
* Reads an X.509 certificate chain from ASN.1 encoded data in the given stream.
*
* @param in Input stream containing a sequence of PEM or DER encoded certificates or PKCS#7 certificate chain.
*
* @return Certificate.
*
* @throws EncodingException on cert parsing errors.
* @throws StreamException on IO errors.
*/
public static X509Certificate[] readCertificateChain(final InputStream in) throws EncodingException, StreamException
{
try {
final CertificateFactory factory = CertificateFactory.getInstance("X.509");
final Collection<? extends Certificate> certs = factory.generateCertificates(in);
return certs.toArray(new X509Certificate[certs.size()]);
} catch (CertificateException e) {
if (e.getCause() instanceof IOException) {
throw new StreamException((IOException) e.getCause());
}
throw new EncodingException("Cannot decode certificate", e);
}
}
/**
* Creates an X.509 certificate chain from its ASN.1 encoded form.
*
* @param encoded Sequence of PEM or DER encoded certificates or PKCS#7 certificate chain.
*
* @return Certificate.
*
* @throws EncodingException on cert parsing errors.
*/
public static X509Certificate[] decodeCertificateChain(final byte[] encoded) throws EncodingException
{
return readCertificateChain(new ByteArrayInputStream(encoded));
}
/**
* Determines whether the certificate allows the given basic key usages.
*
* @param cert Certificate to check.
* @param bits One or more basic key usage types to check.
*
* @return True if certificate allows all given usage types, false otherwise.
*
* @throws EncodingException on cert field extraction.
*/
public static boolean allowsUsage(final X509Certificate cert, final KeyUsageBits... bits) throws EncodingException
{
final KeyUsage usage = new ExtensionReader(cert).readKeyUsage();
for (KeyUsageBits bit : bits) {
if (!bit.isSet(usage)) {
return false;
}
}
return true;
}
/**
* Determines whether the certificate allows the given extended key usages.
*
* @param cert Certificate to check.
* @param purposes One ore more extended key usage purposes to check.
*
* @return True if certificate allows all given purposes, false otherwise.
*
* @throws EncodingException on cert field extraction.
*/
public static boolean allowsUsage(final X509Certificate cert, final KeyPurposeId... purposes) throws EncodingException
{
final List<KeyPurposeId> allowedUses = new ExtensionReader(cert).readExtendedKeyUsage();
for (KeyPurposeId purpose : purposes) {
if (allowedUses == null || !allowedUses.contains(purpose)) {
return false;
}
}
return true;
}
/**
* Determines whether the certificate defines all of the given certificate policies.
*
* @param cert Certificate to check.
* @param policyOidsToCheck One or more certificate policy OIDs to check.
*
* @return True if certificate defines all given policy OIDs, false otherwise.
*
* @throws EncodingException on cert field extraction.
*/
public static boolean hasPolicies(final X509Certificate cert, final String... policyOidsToCheck)
throws EncodingException
{
final List<PolicyInformation> policies = new ExtensionReader(cert).readCertificatePolicies();
boolean hasPolicy;
for (String policyOid : policyOidsToCheck) {
hasPolicy = false;
if (policies != null) {
for (PolicyInformation policy : policies) {
if (policy.getPolicyIdentifier().getId().equals(policyOid)) {
hasPolicy = true;
break;
}
}
}
if (!hasPolicy) {
return false;
}
}
return true;
}
/**
* Gets the subject key identifier of the given certificate in delimited hexadecimal format, e.g. <code>
* 25:48:2f:28:ec:5d:19:bb:1d:25:ae:94:93:b1:7b:b5:35:96:24:66</code>.
*
* @param cert Certificate to process.
*
* @return Subject key identifier in colon-delimited hex format.
*
* @throws EncodingException on cert field extraction.
*/
public static String subjectKeyId(final X509Certificate cert) throws EncodingException
{
return CodecUtil.hex(new ExtensionReader(cert).readSubjectKeyIdentifier().getKeyIdentifier(), true);
}
/**
* Gets the authority key identifier of the given certificate in delimited hexadecimal format, e.g. <code>
* 25:48:2f:28:ec:5d:19:bb:1d:25:ae:94:93:b1:7b:b5:35:96:24:66</code>.
*
* @param cert Certificate to process.
*
* @return Authority key identifier in colon-delimited hex format.
*
* @throws EncodingException on cert field extraction.
*/
public static String authorityKeyId(final X509Certificate cert) throws EncodingException
{
return CodecUtil.hex(new ExtensionReader(cert).readAuthorityKeyIdentifier().getKeyIdentifier(), true);
}
}