/*
* Copyright (c) 2011 ICM Uniwersytet Warszawski All rights reserved.
* See LICENCE file for licensing information.
*/
package eu.emi.security.authn.x509.helpers;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertPath;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.security.auth.x500.X500Principal;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
/**
* Utility methods for certificates handling and reading/writing PEM files.
*
* @author K. Benedyczak
*/
public class CertificateHelpers
{
public enum PEMContentsType {PRIVATE_KEY, LEGACY_OPENSSL_PRIVATE_KEY,
CERTIFICATE, CSR, CRL, UNKNOWN};
private static final byte[] TEST = new byte[] {1, 2, 3, 4, 100};
/**
* Assumes that the input is the contents of the PEM identification line,
* after '-----BEGIN ' prefix.
*
* @param name PEM first line to be checked.
* @return the type
*/
public static PEMContentsType getPEMType(String name)
{
if (name.contains("CERTIFICATE") && !name.contains("REQUEST"))
return PEMContentsType.CERTIFICATE;
if (name.equals("PRIVATE KEY"))
return PEMContentsType.PRIVATE_KEY;
if (name.equals("ENCRYPTED PRIVATE KEY"))
return PEMContentsType.PRIVATE_KEY;
if (name.contains("PRIVATE KEY"))
return PEMContentsType.LEGACY_OPENSSL_PRIVATE_KEY;
if (name.contains("REQUEST") && name.contains("CERTIFICATE"))
return PEMContentsType.CSR;
if (name.contains("CRL"))
return PEMContentsType.CRL;
return PEMContentsType.UNKNOWN;
}
public static Collection<? extends Certificate> readDERCertificates(InputStream input) throws IOException
{
CertificateFactory factory = getFactory();
try
{
return factory.generateCertificates(input);
} catch (CertificateException e)
{
throw new IOException("Can not parse the input data as a certificate", e);
} catch (ClassCastException e)
{
throw new IOException("Can not parse the input as it contains a certificate " +
"but it is not an X.509 certificate.", e);
} finally
{
input.close();
}
}
public static Certificate readDERCertificate(InputStream input) throws IOException
{
CertificateFactory factory = getFactory();
try
{
return factory.generateCertificate(input);
} catch (CertificateException e)
{
throw new IOException("Can not parse the input data as a certificate", e);
} catch (ClassCastException e)
{
throw new IOException("Can not parse the input as it contains a certificate " +
"but it is not an X.509 certificate.", e);
} finally
{
input.close();
}
}
private static CertificateFactory getFactory()
{
try
{
return CertificateFactory.getInstance("X.509", BouncyCastleProvider.PROVIDER_NAME);
} catch (CertificateException e)
{
throw new RuntimeException("Can not initialize CertificateFactory, " +
"your JDK installation is misconfigured!", e);
} catch (NoSuchProviderException e)
{
throw new RuntimeException("Can not initialize CertificateFactory, " +
"no BouncyCastle provider, it is a BUG!", e);
}
}
/**
* Creates a chain of certificates, where the top-most certificate (the one without
* issuing certificate) is the last in the returned array.
* @param certificates unsorted certificates of one chain
* @return sorted certificate chain
* @throws IOException if the passed chain is inconsistent
*/
public static X509Certificate[] sortChain(List<X509Certificate> certificates) throws IOException
{
if (certificates.size() == 0)
return new X509Certificate[0];
Map<X500Principal, X509Certificate> certsMapBySubject = new HashMap<X500Principal, X509Certificate>();
//in this map root CA cert is not stored (as it has the same Issuer as its direct child)
Map<X500Principal, X509Certificate> certsMapByIssuer = new HashMap<X500Principal, X509Certificate>();
for (X509Certificate c: certificates)
{
certsMapBySubject.put(c.getSubjectX500Principal(), c);
if (!c.getIssuerX500Principal().equals(c.getSubjectX500Principal()))
certsMapByIssuer.put(c.getIssuerX500Principal(), c);
}
//let's start from the random one (the 1st on the received list)
List<X509Certificate> certsList = new LinkedList<X509Certificate>();
X509Certificate current = certsMapBySubject.remove(certificates.get(0).getSubjectX500Principal());
if (!current.getIssuerX500Principal().equals(current.getSubjectX500Principal()))
certsMapByIssuer.remove(current.getIssuerX500Principal());
certsList.add(current);
//build path from current to root
while (true)
{
X509Certificate parent = certsMapBySubject.remove(current.getIssuerX500Principal());
if (parent != null)
{
certsMapByIssuer.remove(parent.getIssuerX500Principal());
certsList.add(parent);
current = parent;
} else
break;
}
//build path from the first on the list down to the user's certificate
current = certsList.get(0);
while (true)
{
X509Certificate child = certsMapByIssuer.remove(current.getSubjectX500Principal());
if (child != null)
{
certsList.add(0, child);
current = child;
} else
break;
}
if (certsMapByIssuer.size() > 0)
throw new IOException("The keystore is inconsistent as it contains certificates from different chains");
return certsList.toArray(new X509Certificate[certsList.size()]);
}
/**
* Converts certificates array to {@link CertPath}
* @param in array
* @return converted object
* @throws CertificateException certificate exception
*/
public static CertPath toCertPath(X509Certificate[] in) throws CertificateException
{
CertificateFactory certFactory;
try
{
certFactory = CertificateFactory.getInstance("X.509");
} catch (CertificateException e)
{
throw new RuntimeException("No provider supporting X.509 " +
"CertificateFactory. JDK is misconfigured?", e);
}
return certFactory.generateCertPath(Arrays.asList(in));
}
/**
* Converts {@link X500Principal} to {@link X500Name} with the {@link JavaAndBCStyle}
* style.
* @param srcDn source object
* @return converted object
*/
public static X500Name toX500Name(X500Principal srcDn)
{
X500Name withDefaultStyle = X500Name.getInstance(srcDn.getEncoded());
JavaAndBCStyle style = new JavaAndBCStyle();
return X500Name.getInstance(style, withDefaultStyle);
}
/**
* Gets the certificate extension identified by the oid and returns the
* value bytes unwrapped by the ASN1OctetString.
*
* @param cert
* The certificate to inspect.
* @param oid
* The extension OID to fetch.
* @return The value bytes of the extension, returns null in case the
* extension was not present or was empty.
* @throws IOException
* thrown in case the certificate parsing fails.
*/
public static byte[] getExtensionBytes(X509Certificate cert, String oid)
throws IOException
{
byte[] bytes = cert.getExtensionValue(oid);
if (bytes == null)
return null;
DEROctetString valueOctets = (DEROctetString) ASN1Primitive
.fromByteArray(bytes);
return valueOctets.getOctets();
}
/**
* Throws an exception if the private key is not matching the public key.
* The check is done only for known types of keys - RSA and DSA currently.
* @param privKey first key to match
* @param pubKey 2nd key to match
* @throws InvalidKeyException invalid key exception
*/
public static void checkKeysMatching(PrivateKey privKey, PublicKey pubKey) throws InvalidKeyException
{
String algorithm = pubKey.getAlgorithm();
if (!privKey.getAlgorithm().equals(algorithm))
throw new InvalidKeyException("Private and public keys are not matching: different algorithms");
if (algorithm.equals("DSA"))
{
if (!checkKeysViaSignature("SHA1withDSA", privKey, pubKey))
throw new InvalidKeyException("Private and public keys are not matching: DSA");
} else if (algorithm.equals("RSA"))
{
RSAPublicKey rpub = (RSAPublicKey)pubKey;
RSAPrivateKey rpriv = (RSAPrivateKey)privKey;
if (!rpub.getModulus().equals(rpriv.getModulus()))
throw new InvalidKeyException("Private and public keys are not matching: RSA parameters");
} else if (algorithm.equals("GOST3410"))
{
if (!checkKeysViaSignature("GOST3411withGOST3410", privKey, pubKey))
throw new InvalidKeyException("Private and public keys are not matching: GOST 34.10");
} else if (algorithm.equals("ECGOST3410"))
{
if (!checkKeysViaSignature("GOST3411withECGOST3410", privKey, pubKey))
throw new InvalidKeyException("Private and public keys are not matching: EC GOST 34.10");
} else if (algorithm.equals("ECDSA"))
{
if (!checkKeysViaSignature("SHA1withECDSA", privKey, pubKey))
throw new InvalidKeyException("Private and public keys are not matching: EC DSA");
}
}
private static boolean checkKeysViaSignature(String alg, PrivateKey privKey, PublicKey pubKey) throws InvalidKeyException
{
try
{
Signature s = Signature.getInstance(alg);
s.initSign(privKey);
s.update(TEST);
byte[] signature = s.sign();
Signature s2 = Signature.getInstance(alg);
s2.initVerify(pubKey);
s2.update(TEST);
return s2.verify(signature);
} catch (NoSuchAlgorithmException e)
{
throw new RuntimeException("Bug: BC provider not available in checkKeysMatching()", e);
} catch (SignatureException e)
{
throw new RuntimeException("Bug: can't sign/verify test data", e);
}
}
}