package org.apereo.cas.adaptors.x509.authentication.revocation.checker; import com.google.common.base.Throwables; import net.sf.ehcache.Cache; import net.sf.ehcache.Element; import org.apereo.cas.adaptors.x509.authentication.CRLFetcher; import org.apereo.cas.adaptors.x509.authentication.ResourceCRLFetcher; import org.apereo.cas.adaptors.x509.authentication.revocation.policy.RevocationPolicy; import org.apereo.cas.adaptors.x509.util.CertUtils; import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.asn1.DERIA5String; import org.bouncycastle.asn1.x509.DistributionPoint; import org.bouncycastle.asn1.x509.GeneralName; import org.cryptacular.x509.ExtensionReader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.io.ByteArrayResource; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.security.cert.X509CRL; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.stream.IntStream; /** * Performs CRL-based revocation checking by consulting resources defined in * the CRLDistributionPoints extension field on the certificate. Although RFC * 2459 allows the distribution point name to have arbitrary meaning, this class * expects the name to define an absolute URL, which is the most common * implementation. This implementation caches CRL resources fetched from remote * URLs to improve performance by avoiding CRL fetching on every revocation * check. * * @author Marvin S. Addison * @since 3.4.6 */ public class CRLDistributionPointRevocationChecker extends AbstractCRLRevocationChecker { private static final Logger LOGGER = LoggerFactory.getLogger(CRLDistributionPointRevocationChecker.class); private final Cache crlCache; private final CRLFetcher fetcher; private final boolean throwOnFetchFailure; /** * Creates a new instance that uses the given cache instance for CRL caching. * * @param crlCache Cache for CRL data. */ public CRLDistributionPointRevocationChecker(final Cache crlCache) { this(crlCache, new ResourceCRLFetcher(), false); } /** * Creates a new instance that uses the given cache instance for CRL caching. * * @param crlCache Cache for CRL data. * @param throwOnFetchFailure the throw on fetch failure */ public CRLDistributionPointRevocationChecker(final Cache crlCache, final boolean throwOnFetchFailure) { this(crlCache, new ResourceCRLFetcher(), throwOnFetchFailure); } /** * Instantiates a new CRL distribution point revocation checker. * * @param crlCache the crl cache * @param fetcher the fetcher * @param throwOnFetchFailure the throw on fetch failure */ public CRLDistributionPointRevocationChecker( final Cache crlCache, final CRLFetcher fetcher, final boolean throwOnFetchFailure) { this(false, null, null, crlCache, fetcher, throwOnFetchFailure); } public CRLDistributionPointRevocationChecker(final Cache crlCache, final RevocationPolicy<Void> unavailableCRLPolicy) { this(crlCache, null, unavailableCRLPolicy); } public CRLDistributionPointRevocationChecker(final Cache crlCache, final RevocationPolicy<X509CRL> expiredCRLPolicy, final RevocationPolicy<Void> unavailableCRLPolicy) { this(crlCache, expiredCRLPolicy, unavailableCRLPolicy, false); } public CRLDistributionPointRevocationChecker(final Cache crlCache, final RevocationPolicy<X509CRL> expiredCRLPolicy, final RevocationPolicy<Void> unavailableCRLPolicy, final boolean throwOnFetchFailure) { this(false, unavailableCRLPolicy, expiredCRLPolicy, crlCache, new ResourceCRLFetcher(), throwOnFetchFailure); } public CRLDistributionPointRevocationChecker(final boolean checkAll, final RevocationPolicy<Void> unavailableCRLPolicy, final RevocationPolicy<X509CRL> expiredCRLPolicy, final Cache crlCache, final CRLFetcher fetcher, final boolean throwOnFetchFailure) { super(checkAll, unavailableCRLPolicy, expiredCRLPolicy); this.crlCache = crlCache; this.fetcher = fetcher; this.throwOnFetchFailure = throwOnFetchFailure; } @Override protected List<X509CRL> getCRLs(final X509Certificate cert) { if (this.crlCache == null) { throw new IllegalArgumentException("CRL cache is not defined"); } if (this.fetcher == null) { throw new IllegalArgumentException("CRL fetcher is not defined"); } if (getExpiredCRLPolicy() == null) { throw new IllegalArgumentException("Expiration CRL policy is not defined"); } if (getUnavailableCRLPolicy() == null) { throw new IllegalArgumentException("Unavailable CRL policy is not defined"); } final URI[] urls = getDistributionPoints(cert); LOGGER.debug("Distribution points for [{}]: [{}].", CertUtils.toString(cert), Arrays.asList(urls)); final List<X509CRL> listOfLocations = new ArrayList<>(urls.length); boolean stopFetching = false; try { for (int index = 0; !stopFetching && index < urls.length; index++) { final URI url = urls[index]; final Element item = this.crlCache.get(url); if (item != null) { LOGGER.debug("Found CRL in cache for [{}]", CertUtils.toString(cert)); final byte[] encodedCrl = (byte[]) item.getObjectValue(); final X509CRL crlFetched = this.fetcher.fetch(new ByteArrayResource(encodedCrl)); if (crlFetched != null) { listOfLocations.add(crlFetched); } else { LOGGER.warn("Could fetch X509 CRL for [{}]. Returned value is null", url); } } else { LOGGER.debug("CRL for [{}] is not cached. Fetching and caching...", CertUtils.toString(cert)); try { final X509CRL crl = this.fetcher.fetch(url); if (crl != null) { LOGGER.info("Success. Caching fetched CRL at [{}].", url); addCRL(url, crl); listOfLocations.add(crl); } } catch (final Exception e) { LOGGER.error("Error fetching CRL at [{}]", url, e); if (this.throwOnFetchFailure) { throw Throwables.propagate(e); } } } if (!this.checkAll && !listOfLocations.isEmpty()) { LOGGER.debug("CRL fetching is configured to not check all locations."); stopFetching = true; } } } catch (final Exception e) { throw Throwables.propagate(e); } LOGGER.debug("Found [{}] CRLs", listOfLocations.size()); return listOfLocations; } @Override protected boolean addCRL(final Object id, final X509CRL crl) { try { if (crl == null) { LOGGER.debug("No CRL was passed. Removing [{}] from cache...", id); return this.crlCache.remove(id); } this.crlCache.put(new Element(id, crl.getEncoded())); return this.crlCache.get(id) != null; } catch (final Exception e) { LOGGER.warn("Failed to add the crl entry [{}] to the cache", crl); throw Throwables.propagate(e); } } /** * Gets the distribution points. * * @param cert the cert * @return the url distribution points */ private static URI[] getDistributionPoints(final X509Certificate cert) { final List<DistributionPoint> points; try { points = new ExtensionReader(cert).readCRLDistributionPoints(); } catch (final RuntimeException e) { LOGGER.error("Error reading CRLDistributionPoints extension field on [{}]", CertUtils.toString(cert), e); return new URI[0]; } final List<URI> urls = new ArrayList<>(); if (points != null) { points.stream().map(DistributionPoint::getDistributionPoint).filter(Objects::nonNull).forEach(pointName -> { final ASN1Sequence nameSequence = ASN1Sequence.getInstance(pointName.getName()); IntStream.range(0, nameSequence.size()).mapToObj(i -> GeneralName.getInstance(nameSequence.getObjectAt(i))).forEach(name -> { LOGGER.debug("Found CRL distribution point [{}].", name); try { addURL(urls, DERIA5String.getInstance(name.getName()).getString()); } catch (final RuntimeException e) { LOGGER.warn("[{}] not supported. String or GeneralNameList expected.", pointName); } }); }); } return urls.toArray(new URI[urls.size()]); } /** * Adds the url to the list. * Build URI by components to facilitate proper encoding of querystring. * e.g. http://example.com:8085/ca?action=crl&issuer=CN=CAS Test User CA * <p> * <p>If {@code uriString} is encoded, it will be decoded with {@code UTF-8} * first before it's added to the list.</p> * * @param list the list * @param uriString the uri string */ private static void addURL(final List<URI> list, final String uriString) { try { URI uri; try { final URL url = new URL(URLDecoder.decode(uriString, StandardCharsets.UTF_8.name())); uri = new URI(url.getProtocol(), url.getAuthority(), url.getPath(), url.getQuery(), null); } catch (final MalformedURLException e) { uri = new URI(uriString); } list.add(uri); } catch (final Exception e) { LOGGER.warn("[{}] is not a valid distribution point URI.", uriString); } } }