CRLVerifier.java
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.pdfbox.examples.signature.cert;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
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.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Set;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.apache.pdfbox.examples.signature.SigUtils;
import org.apache.pdfbox.pdmodel.encryption.SecurityProvider;
import org.bouncycastle.asn1.ASN1IA5String;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1Primitive;
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;
/**
* Copied from Apache CXF 2.4.9, initial version:
* https://svn.apache.org/repos/asf/cxf/tags/cxf-2.4.9/distribution/src/main/release/samples/sts_issue_operation/src/main/java/demo/sts/provider/cert/
*
*/
public final class CRLVerifier
{
private static final Logger LOG = LogManager.getLogger(CRLVerifier.class);
private CRLVerifier()
{
}
/**
* Extracts the CRL distribution points from the certificate (if available)
* and checks the certificate revocation status against the CRLs coming from
* the distribution points. Supports HTTP, HTTPS, FTP and LDAP based URLs.
*
* @param cert the certificate to be checked for revocation
* @param signDate the date when the signing took place
* @param additionalCerts set of trusted root CA certificates that will be
* used as "trust anchors" and intermediate CA certificates that will be
* used as part of the certification chain.
* @throws CertificateVerificationException if the certificate could not be verified
* @throws RevokedCertificateException if the certificate is revoked
*/
@SuppressWarnings({"squid:S1141"}) // nested exception needed to try several distribution points
public static void verifyCertificateCRLs(X509Certificate cert, Date signDate,
Set<X509Certificate> additionalCerts)
throws CertificateVerificationException, RevokedCertificateException
{
try
{
Date now = Calendar.getInstance().getTime();
Exception firstException = null;
List<String> crlDistributionPointsURLs = getCrlDistributionPoints(cert);
for (String crlDistributionPointsURL : crlDistributionPointsURLs)
{
LOG.info("Checking distribution point URL: {}", crlDistributionPointsURL);
X509CRL crl;
try
{
crl = downloadCRL(crlDistributionPointsURL);
}
catch (IOException | GeneralSecurityException | CertificateVerificationException | NamingException ex)
{
// e.g. LDAP behind corporate proxy
// but couldn't get LDAP to work at all, see e.g. file from PDFBOX-1452
LOG.warn(
"Caught {} downloading CRL, will try next distribution point if available",
ex.getClass().getSimpleName());
if (firstException == null)
{
firstException = ex;
}
continue;
}
Set<X509Certificate> mergedCertSet = CertificateVerifier.downloadExtraCertificates(crl);
mergedCertSet.addAll(additionalCerts);
// Verify CRL, see wikipedia:
// "To validate a specific CRL prior to relying on it,
// the certificate of its corresponding CA is needed"
X509Certificate crlIssuerCert = null;
for (X509Certificate possibleCert : mergedCertSet)
{
try
{
cert.verify(possibleCert.getPublicKey(), SecurityProvider.getProvider());
crlIssuerCert = possibleCert;
break;
}
catch (GeneralSecurityException ex)
{
// not the issuer
}
}
if (crlIssuerCert == null)
{
throw new CertificateVerificationException(
"Certificate for " + crl.getIssuerX500Principal() +
"not found in certificate chain, so the CRL at " +
crlDistributionPointsURL + " could not be verified");
}
crl.verify(crlIssuerCert.getPublicKey(), SecurityProvider.getProvider());
//TODO these should be exceptions, but for that we need a test case where
// a PDF has a broken OCSP and a working CRL
if (crl.getThisUpdate().after(now))
{
LOG.error("CRL not yet valid, thisUpdate is {}", crl.getThisUpdate());
}
if (crl.getNextUpdate().before(now))
{
LOG.error("CRL no longer valid, nextUpdate is {}", crl.getNextUpdate());
}
if (!crl.getIssuerX500Principal().equals(cert.getIssuerX500Principal()))
{
LOG.info("CRL issuer certificate is not identical to cert issuer, check needed");
CertificateVerifier.verifyCertificate(crlIssuerCert, mergedCertSet, true, now);
LOG.info("CRL issuer certificate checked successfully");
}
else
{
LOG.info("CRL issuer certificate is identical to cert issuer, no extra check needed");
}
checkRevocation(crl, cert, signDate, crlDistributionPointsURL);
// https://tools.ietf.org/html/rfc5280#section-4.2.1.13
// If the DistributionPointName contains multiple values,
// each name describes a different mechanism to obtain the same
// CRL. For example, the same CRL could be available for
// retrieval through both LDAP and HTTP.
//
// => thus no need to check several protocols
return;
}
if (firstException != null)
{
throw firstException;
}
}
catch (RevokedCertificateException | CertificateVerificationException ex)
{
throw ex;
}
catch (Exception ex)
{
throw new CertificateVerificationException(
"Cannot verify CRL for certificate: "
+ cert.getSubjectX500Principal(), ex);
}
}
/**
* Check whether the certificate was revoked at signing time.
*
* @param crl certificate revocation list
* @param cert certificate to be checked
* @param signDate date the certificate was used for signing
* @param crlDistributionPointsURL URL for log message or exception text
* @throws RevokedCertificateException if the certificate was revoked at signing time
*/
public static void checkRevocation(
X509CRL crl, X509Certificate cert, Date signDate, String crlDistributionPointsURL)
throws RevokedCertificateException
{
X509CRLEntry revokedCRLEntry = crl.getRevokedCertificate(cert);
if (revokedCRLEntry != null &&
revokedCRLEntry.getRevocationDate().compareTo(signDate) <= 0)
{
throw new RevokedCertificateException(
"The certificate was revoked by CRL " +
crlDistributionPointsURL + " on " + revokedCRLEntry.getRevocationDate(),
revokedCRLEntry.getRevocationDate());
}
else if (revokedCRLEntry != null)
{
LOG.info("The certificate was revoked after signing by CRL {} on {}",
crlDistributionPointsURL, revokedCRLEntry.getRevocationDate());
}
else
{
LOG.info("The certificate was not revoked by CRL {}", crlDistributionPointsURL);
}
}
/**
* Downloads CRL from given URL. Supports http, https, ftp and ldap based URLs.
*/
private static X509CRL downloadCRL(String crlURL) throws IOException,
CertificateException, CRLException,
CertificateVerificationException, NamingException, URISyntaxException
{
if (crlURL.startsWith("http://") || crlURL.startsWith("https://")
|| crlURL.startsWith("ftp://"))
{
return downloadCRLFromWeb(crlURL);
}
else if (crlURL.startsWith("ldap://"))
{
return downloadCRLFromLDAP(crlURL);
}
else
{
throw new CertificateVerificationException(
"Can not download CRL from certificate "
+ "distribution point: " + crlURL);
}
}
/**
* Downloads a CRL from given LDAP url, e.g.
* ldap://ldap.infonotary.com/dc=identity-ca,dc=infonotary,dc=com
*/
private static X509CRL downloadCRLFromLDAP(String ldapURL) throws CertificateException,
NamingException, CRLException,
CertificateVerificationException
{
@SuppressWarnings({"squid:S1149"})
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, ldapURL);
// https://docs.oracle.com/javase/jndi/tutorial/ldap/connect/create.html
// don't wait forever behind corporate proxy
env.put("com.sun.jndi.ldap.connect.timeout", "1000");
DirContext ctx = new InitialDirContext(env);
Attributes avals = ctx.getAttributes("");
Attribute aval = avals.get("certificateRevocationList;binary");
byte[] val = (byte[]) aval.get();
if (val == null || val.length == 0)
{
throw new CertificateVerificationException("Can not download CRL from: " + ldapURL);
}
else
{
InputStream inStream = new ByteArrayInputStream(val);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
return (X509CRL) cf.generateCRL(inStream);
}
}
/**
* Downloads a CRL from given HTTP/HTTPS/FTP URL, e.g.
* http://crl.infonotary.com/crl/identity-ca.crl
*/
public static X509CRL downloadCRLFromWeb(String crlURL)
throws IOException, CertificateException, CRLException, URISyntaxException
{
try (InputStream crlStream = SigUtils.openURL(crlURL))
{
return (X509CRL) CertificateFactory.getInstance("X.509").generateCRL(crlStream);
}
}
/**
* Extracts all CRL distribution point URLs from the "CRL Distribution
* Point" extension in a X.509 certificate. If CRL distribution point
* extension is unavailable, returns an empty list.
* @param cert
* @return List of CRL distribution point URLs.
* @throws java.io.IOException
*/
public static List<String> getCrlDistributionPoints(X509Certificate cert)
throws IOException
{
byte[] crldpExt = cert.getExtensionValue(Extension.cRLDistributionPoints.getId());
if (crldpExt == null)
{
return new ArrayList<>();
}
ASN1Primitive derObjCrlDP;
try (ASN1InputStream oAsnInStream = new ASN1InputStream(crldpExt))
{
derObjCrlDP = oAsnInStream.readObject();
}
if (!(derObjCrlDP instanceof ASN1OctetString))
{
LOG.warn(
"CRL distribution points for certificate subject {} should be an octet string, but is {}",
cert.getSubjectX500Principal().getName(), derObjCrlDP);
return new ArrayList<>();
}
ASN1OctetString dosCrlDP = (ASN1OctetString) derObjCrlDP;
byte[] crldpExtOctets = dosCrlDP.getOctets();
ASN1Primitive derObj2;
try (ASN1InputStream oAsnInStream2 = new ASN1InputStream(crldpExtOctets))
{
derObj2 = oAsnInStream2.readObject();
}
CRLDistPoint distPoint = CRLDistPoint.getInstance(derObj2);
List<String> crlUrls = new ArrayList<>();
for (DistributionPoint dp : distPoint.getDistributionPoints())
{
DistributionPointName dpn = dp.getDistributionPoint();
// Look for URIs in fullName
if (dpn != null && dpn.getType() == DistributionPointName.FULL_NAME)
{
// Look for an URI
for (GeneralName genName : GeneralNames.getInstance(dpn.getName()).getNames())
{
if (genName.getTagNo() == GeneralName.uniformResourceIdentifier)
{
String url = ASN1IA5String.getInstance(genName.getName()).getString();
crlUrls.add(url);
}
}
}
}
return crlUrls;
}
}