/*
* oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text.
*
* Copyright (c) 2014, Gluu
*/
package org.xdi.oxauth.cert.validation;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.NoSuchProviderException;
import java.security.Principal;
import java.security.cert.CRLException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509CRL;
import java.security.cert.X509CRLEntry;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.BoundedInputStream;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.ASN1TaggedObject;
import org.bouncycastle.asn1.DERIA5String;
import org.bouncycastle.asn1.DERInteger;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.x509.CRLDistPoint;
import org.bouncycastle.asn1.x509.DistributionPoint;
import org.bouncycastle.asn1.x509.DistributionPointName;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.X509Extensions;
import org.bouncycastle.x509.NoSuchParserException;
import org.bouncycastle.x509.util.StreamParsingException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xdi.oxauth.cert.validation.model.ValidationStatus;
import org.xdi.oxauth.cert.validation.model.ValidationStatus.CertificateValidity;
import org.xdi.oxauth.cert.validation.model.ValidationStatus.ValidatorSourceType;
import org.xdi.oxauth.model.util.SecurityProviderUtility;
import com.google.common.base.Preconditions;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
/**
* Certificate verifier based on CRL
*
* @author Yuriy Movchan
* @version March 10, 2016
*/
public class CRLCertificateVerifier implements CertificateVerifier {
private static final Logger log = LoggerFactory.getLogger(CRLCertificateVerifier.class);
private int maxCrlSize;
private LoadingCache<String, X509CRL> crlCache;
public CRLCertificateVerifier(final int maxCrlSize) {
SecurityProviderUtility.installBCProvider(true);
this.maxCrlSize = maxCrlSize;
CacheLoader<String, X509CRL> checkedLoader = new CacheLoader<String, X509CRL>() {
public X509CRL load(String crlURL) throws CertificateException, CRLException, NoSuchProviderException, NoSuchParserException, StreamParsingException, MalformedURLException, IOException, ExecutionException {
X509CRL result = requestCRL(crlURL);
Preconditions.checkNotNull(result);
return result;
}
};
this.crlCache = CacheBuilder.newBuilder().maximumSize(10).expireAfterWrite(60, TimeUnit.MINUTES).build(checkedLoader);
}
@Override
public ValidationStatus validate(X509Certificate certificate, List<X509Certificate> issuers, Date validationDate) {
X509Certificate issuer = issuers.get(0);
ValidationStatus status = new ValidationStatus(certificate, issuer, validationDate, ValidatorSourceType.CRL, CertificateValidity.UNKNOWN);
try {
Principal subjectX500Principal = certificate.getSubjectX500Principal();
String crlURL = getCrlUri(certificate);
if (crlURL == null) {
log.error("CRL's URL for '" + subjectX500Principal + "' is empty");
return status;
}
log.debug("CRL's URL for '" + subjectX500Principal + "' is '" + crlURL + "'");
X509CRL x509crl = getCrl(crlURL);
if (!validateCRL(x509crl, certificate, issuer, validationDate)) {
log.error("The CRL is not valid!");
status.setValidity(CertificateValidity.INVALID);
return status;
}
X509CRLEntry crlEntry = x509crl.getRevokedCertificate(certificate.getSerialNumber());
if (crlEntry == null) {
log.debug("CRL status is valid for '" + subjectX500Principal + "'");
status.setValidity(CertificateValidity.VALID);
} else if (crlEntry.getRevocationDate().after(validationDate)) {
log.warn("CRL revocation time after the validation date, the certificate '" + subjectX500Principal + "' was valid at " + validationDate);
status.setRevocationObjectIssuingTime(x509crl.getThisUpdate());
status.setValidity(CertificateValidity.VALID);
} else {
log.info("CRL for certificate '" + subjectX500Principal + "' is revoked since " + crlEntry.getRevocationDate());
status.setRevocationObjectIssuingTime(x509crl.getThisUpdate());
status.setRevocationDate(crlEntry.getRevocationDate());
status.setValidity(CertificateValidity.REVOKED);
}
} catch (Exception ex) {
log.error("CRL exception: ", ex);
}
return status;
}
private boolean validateCRL(X509CRL x509crl, X509Certificate certificate, X509Certificate issuerCertificate, Date validationDate) {
Principal subjectX500Principal = certificate.getSubjectX500Principal();
if (x509crl == null) {
log.error("No CRL found for certificate '" + subjectX500Principal + "'");
return false;
}
if (log.isTraceEnabled()) {
try {
log.trace("CRL number: " + getCrlNumber(x509crl));
} catch (IOException ex) {
log.error("Failed to get CRL number", ex);
}
}
if (!x509crl.getIssuerX500Principal().equals(issuerCertificate.getSubjectX500Principal())) {
log.error("The CRL must be signed by the issuer '" + subjectX500Principal + "' but instead is signed by '"
+ x509crl.getIssuerX500Principal() + "'");
return false;
}
try {
x509crl.verify(issuerCertificate.getPublicKey());
} catch (Exception ex) {
log.error("The signature verification for CRL cannot be performed", ex);
return false;
}
log.debug("CRL validationDate: " + validationDate);
log.debug("CRL nextUpdate: " + x509crl.getThisUpdate());
log.debug("CRL thisUpdate: " + x509crl.getNextUpdate());
if (x509crl.getNextUpdate() != null && validationDate.after(x509crl.getNextUpdate())) {
log.error("CRL is too old");
return false;
}
if (issuerCertificate.getKeyUsage() == null) {
log.error("There is no KeyUsage extension for certificate '" + subjectX500Principal + "'");
return false;
}
if (!issuerCertificate.getKeyUsage()[6]) {
log.error("cRLSign bit is not set for CRL certificate'" + subjectX500Principal + "'");
return false;
}
return true;
}
private X509CRL getCrl(String url) throws CertificateException, CRLException, NoSuchProviderException, NoSuchParserException, StreamParsingException,
MalformedURLException, IOException, ExecutionException {
if (!(url.startsWith("http://") || url.startsWith("https://"))) {
log.error("It's possbiel to downloid CRL via HTTP and HTTPS only");
return null;
}
String cacheKey = url.toLowerCase();
X509CRL crl = crlCache.get(cacheKey);
return crl;
}
public X509CRL requestCRL(String url) throws IOException, MalformedURLException, CertificateException, CRLException {
HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection();
try {
con.setUseCaches(false);
InputStream in = new BoundedInputStream(con.getInputStream(), maxCrlSize);
try {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
X509CRL crl = (X509CRL) certificateFactory.generateCRL(in);
log.debug("CRL size: " + crl.getEncoded().length + " bytes");
return crl;
} finally {
IOUtils.closeQuietly(in);
}
} catch (IOException ex) {
log.error("Failed to download CRL from '" + url + "'", ex);
} finally {
if (con != null) {
con.disconnect();
}
}
return null;
}
@SuppressWarnings({ "deprecation", "resource" })
private BigInteger getCrlNumber(X509CRL crl) throws IOException {
byte[] crlNumberExtensionValue = crl.getExtensionValue(X509Extensions.CRLNumber.getId());
if (crlNumberExtensionValue == null) {
return null;
}
DEROctetString octetString = (DEROctetString) (new ASN1InputStream(new ByteArrayInputStream(crlNumberExtensionValue)).readObject());
byte[] octets = octetString.getOctets();
DERInteger integer = (DERInteger) new ASN1InputStream(octets).readObject();
BigInteger crlNumber = integer.getPositiveValue();
return crlNumber;
}
public String getCrlUri(X509Certificate certificate) throws IOException {
ASN1Primitive obj;
try {
obj = getExtensionValue(certificate, Extension.cRLDistributionPoints.getId());
} catch (IOException ex) {
log.error("Failed to get CRL URL", ex);
return null;
}
if (obj == null) {
return null;
}
CRLDistPoint distPoint = CRLDistPoint.getInstance(obj);
DistributionPoint[] distributionPoints = distPoint.getDistributionPoints();
for (DistributionPoint distributionPoint : distributionPoints) {
DistributionPointName distributionPointName = distributionPoint.getDistributionPoint();
if (DistributionPointName.FULL_NAME != distributionPointName.getType()) {
continue;
}
GeneralNames generalNames = (GeneralNames) distributionPointName.getName();
GeneralName[] names = generalNames.getNames();
for (GeneralName name : names) {
if (name.getTagNo() != GeneralName.uniformResourceIdentifier) {
continue;
}
DERIA5String derStr = DERIA5String.getInstance((ASN1TaggedObject) name.toASN1Primitive(), false);
return derStr.getString();
}
}
return null;
}
/**
* @param certificate
* the certificate from which we need the ExtensionValue
* @param oid
* the Object Identifier value for the extension.
* @return the extension value as an ASN1Primitive object
* @throws IOException
*/
private static ASN1Primitive getExtensionValue(X509Certificate certificate, String oid) throws IOException {
byte[] bytes = certificate.getExtensionValue(oid);
if (bytes == null) {
return null;
}
ASN1InputStream aIn = new ASN1InputStream(new ByteArrayInputStream(bytes));
ASN1OctetString octs = (ASN1OctetString) aIn.readObject();
aIn = new ASN1InputStream(new ByteArrayInputStream(octs.getOctets()));
return aIn.readObject();
}
@Override
public void destroy() {
crlCache.cleanUp();
}
}