com.itextpdf.signatures.validation.CRLValidator Maven / Gradle / Ivy
The newest version!
/*
This file is part of the iText (R) project.
Copyright (c) 1998-2024 Apryse Group NV
Authors: Apryse Software.
This program is offered under a commercial and under the AGPL license.
For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below.
AGPL licensing:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
*/
package com.itextpdf.signatures.validation;
import com.itextpdf.bouncycastleconnector.BouncyCastleFactoryCreator;
import com.itextpdf.commons.bouncycastle.IBouncyCastleFactory;
import com.itextpdf.commons.bouncycastle.asn1.IASN1Encodable;
import com.itextpdf.commons.bouncycastle.asn1.IASN1Primitive;
import com.itextpdf.commons.bouncycastle.asn1.x509.IDistributionPoint;
import com.itextpdf.commons.bouncycastle.asn1.x509.IIssuingDistributionPoint;
import com.itextpdf.commons.bouncycastle.asn1.x509.IReasonFlags;
import com.itextpdf.commons.utils.DateTimeUtil;
import com.itextpdf.commons.utils.MessageFormatUtil;
import com.itextpdf.signatures.CertificateUtil;
import com.itextpdf.signatures.IssuingCertificateRetriever;
import com.itextpdf.signatures.TimestampConstants;
import com.itextpdf.signatures.logs.SignLogMessageConstant;
import com.itextpdf.signatures.validation.context.CertificateSource;
import com.itextpdf.signatures.validation.context.ValidationContext;
import com.itextpdf.signatures.validation.context.ValidatorContext;
import com.itextpdf.signatures.validation.extensions.DynamicBasicConstraintsExtension;
import com.itextpdf.signatures.validation.report.CertificateReportItem;
import com.itextpdf.signatures.validation.report.ReportItem;
import com.itextpdf.signatures.validation.report.ReportItem.ReportItemStatus;
import com.itextpdf.signatures.validation.report.ValidationReport;
import com.itextpdf.signatures.validation.report.ValidationReport.ValidationResult;
import java.io.IOException;
import java.security.cert.Certificate;
import java.security.cert.CRLReason;
import java.security.cert.X509CRL;
import java.security.cert.X509CRLEntry;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static com.itextpdf.signatures.validation.SafeCalling.onExceptionLog;
/**
* Class that allows you to validate a certificate against a Certificate Revocation List (CRL) Response.
*/
public class CRLValidator {
static final String CRL_CHECK = "CRL response check.";
static final String ATTRIBUTE_CERTS_ASSERTED = "The onlyContainsAttributeCerts is asserted. Conforming CRLs " +
"issuers MUST set the onlyContainsAttributeCerts boolean to FALSE.";
static final String CERTIFICATE_IS_EXPIRED =
"Certificate is expired on {0} and could have been removed from the CRL.";
static final String CERTIFICATE_IS_UNREVOKED = "The certificate was unrevoked.";
static final String CERTIFICATE_IS_NOT_IN_THE_CRL_SCOPE = "Certificate isn't in the current CRL scope.";
static final String CERTIFICATE_REVOKED = "Certificate was revoked by {0} on {1}.";
static final String CRL_ISSUER_NOT_FOUND = "Unable to validate CRL response: no issuer certificate found.";
static final String CRL_ISSUER_REQUEST_FAILED =
"Unable to validate CRL response: Unexpected exception occurred retrieving issuer certificate.";
static final String CRL_ISSUER_CHAIN_FAILED =
"Unable to validate CRL response: Unexpected exception occurred validating issuer certificate.";
static final String CRL_ISSUER_NO_COMMON_ROOT =
"The CRL issuer does not share the root of the inspected certificate.";
static final String CRL_INVALID = "CRL response is invalid.";
static final String FRESHNESS_CHECK = "CRL response is not fresh enough: " +
"this update: {0}, validation date: {1}, freshness: {2}.";
static final String ONLY_SOME_REASONS_CHECKED = "Revocation status cannot be determined since " +
"not all reason codes are covered by the current CRL.";
static final String SAME_REASONS_CHECK = "CRLs that cover the same reason codes were already verified.";
static final String UPDATE_DATE_BEFORE_CHECK_DATE = "nextUpdate: {0} of CRLResponse is before validation date {1}.";
static final String CERTIFICATE_IN_ISSUER_CHAIN = "Unable to validate CRL response: validated certificate is"
+ " part of issuer certificate chain.";
// All reasons without unspecified.
static final int ALL_REASONS = 32895;
private static final IBouncyCastleFactory FACTORY = BouncyCastleFactoryCreator.getFactory();
private final Map checkedReasonsMask = new HashMap<>();
private final IssuingCertificateRetriever certificateRetriever;
private final SignatureValidationProperties properties;
private final ValidatorChainBuilder builder;
/**
* Creates new {@link CRLValidator} instance.
*
* @param builder See {@link ValidatorChainBuilder}
*/
protected CRLValidator(ValidatorChainBuilder builder) {
this.certificateRetriever = builder.getCertificateRetriever();
this.properties = builder.getProperties();
this.builder = builder;
}
/**
* Validates a certificate against Certificate Revocation List (CRL) Responses.
*
* @param report to store all the chain verification results
* @param context the context in which to perform the validation
* @param certificate the certificate to check against CRL response
* @param crl the crl response to be validated
* @param validationDate validation date to check for
* @param responseGenerationDate trusted date at which response is generated
*/
public void validate(ValidationReport report, ValidationContext context, X509Certificate certificate, X509CRL crl,
Date validationDate, Date responseGenerationDate) {
ValidationContext localContext = context.setValidatorContext(ValidatorContext.CRL_VALIDATOR);
if (CertificateUtil.isSelfSigned(certificate)) {
report.addReportItem(new CertificateReportItem(certificate, CRL_CHECK,
RevocationDataValidator.SELF_SIGNED_CERTIFICATE, ReportItemStatus.INFO));
return;
}
Duration freshness = properties.getFreshness(localContext);
// Check that thisUpdate + freshness < validation.
if (DateTimeUtil.addMillisToDate(crl.getThisUpdate(), (long) freshness.toMillis()).before(validationDate)) {
report.addReportItem(new CertificateReportItem(certificate, CRL_CHECK,
MessageFormatUtil.format(FRESHNESS_CHECK, crl.getThisUpdate(), validationDate, freshness),
ReportItemStatus.INDETERMINATE));
return;
}
// Check that the validation date is before the nextUpdate.
if (crl.getNextUpdate() != TimestampConstants.UNDEFINED_TIMESTAMP_DATE &&
validationDate.after(crl.getNextUpdate())) {
report.addReportItem(new CertificateReportItem(certificate, CRL_CHECK, MessageFormatUtil.format(
UPDATE_DATE_BEFORE_CHECK_DATE, crl.getNextUpdate(), validationDate),
ReportItemStatus.INDETERMINATE));
return;
}
// Check expiredCertOnCrl extension in case the certificate is expired.
if (certificate.getNotAfter().before(crl.getThisUpdate())) {
Date startExpirationDate = getExpiredCertsOnCRLExtensionDate(crl);
if (TimestampConstants.UNDEFINED_TIMESTAMP_DATE == startExpirationDate ||
certificate.getNotAfter().before(startExpirationDate)) {
report.addReportItem(new CertificateReportItem(certificate, CRL_CHECK, MessageFormatUtil.format(
CERTIFICATE_IS_EXPIRED, certificate.getNotAfter()), ReportItemStatus.INDETERMINATE));
return;
}
}
IIssuingDistributionPoint issuingDistPoint = getIssuingDistributionPointExtension(crl);
IDistributionPoint distributionPoint = null;
if (!issuingDistPoint.isNull()) {
// Verify that certificate is in the CRL scope using IDP extension.
boolean basicConstraintsCaAsserted = new DynamicBasicConstraintsExtension().withCertificateChainSize(1)
.existsInCertificate(certificate);
if ((issuingDistPoint.onlyContainsUserCerts() && basicConstraintsCaAsserted) ||
(issuingDistPoint.onlyContainsCACerts() && !basicConstraintsCaAsserted)) {
report.addReportItem(new CertificateReportItem(certificate, CRL_CHECK,
CERTIFICATE_IS_NOT_IN_THE_CRL_SCOPE, ReportItemStatus.INDETERMINATE));
return;
}
if (issuingDistPoint.onlyContainsAttributeCerts()) {
report.addReportItem(new CertificateReportItem(certificate, CRL_CHECK,
ATTRIBUTE_CERTS_ASSERTED, ReportItemStatus.INDETERMINATE));
return;
}
// Try to retrieve corresponding DP from the certificate by name specified in the IDP.
if (!issuingDistPoint.getDistributionPoint().isNull()) {
distributionPoint = CertificateUtil.getDistributionPointByName(certificate,
issuingDistPoint.getDistributionPoint());
}
}
int interimReasonsMask = computeInterimReasonsMask(issuingDistPoint, distributionPoint);
Integer reasonsMask = checkedReasonsMask.get(certificate);
if (reasonsMask != null) {
interimReasonsMask |= (int) reasonsMask;
}
// Verify the CRL issuer.
verifyCrlIntegrity(report, localContext, certificate, crl, responseGenerationDate);
// Check the status of the certificate.
verifyRevocation(report, certificate, validationDate, crl);
if (report.getValidationResult() == ValidationReport.ValidationResult.VALID) {
checkedReasonsMask.put(certificate, interimReasonsMask);
}
// If ((reasons_mask is all-reasons) OR (cert_status is not UNREVOKED)),
// then the revocation status has been determined.
if (interimReasonsMask != ALL_REASONS) {
report.addReportItem(new CertificateReportItem(certificate, CRL_CHECK,
ONLY_SOME_REASONS_CHECKED, ReportItemStatus.INDETERMINATE));
}
}
private static void verifyRevocation(ValidationReport report, X509Certificate certificate,
Date verificationDate, X509CRL crl) {
X509CRLEntry revocation = crl.getRevokedCertificate(certificate.getSerialNumber());
if (revocation != null) {
Date revocationDate = revocation.getRevocationDate();
if (verificationDate.before(revocationDate)) {
report.addReportItem(new CertificateReportItem(certificate, CRL_CHECK, MessageFormatUtil.format(
SignLogMessageConstant.VALID_CERTIFICATE_IS_REVOKED, revocationDate),
ReportItemStatus.INFO));
} else if (CRLReason.REMOVE_FROM_CRL == revocation.getRevocationReason()) {
report.addReportItem(new CertificateReportItem(certificate, CRL_CHECK, MessageFormatUtil.format(
CERTIFICATE_IS_UNREVOKED, revocationDate),
ReportItemStatus.INFO));
} else {
report.addReportItem(new CertificateReportItem(certificate, CRL_CHECK, MessageFormatUtil.format(
CERTIFICATE_REVOKED, crl.getIssuerX500Principal(), revocation.getRevocationDate()),
ReportItemStatus.INVALID));
}
}
}
private static IIssuingDistributionPoint getIssuingDistributionPointExtension(X509CRL crl) {
IASN1Primitive issuingDistPointExtension = null;
try {
issuingDistPointExtension = CertificateUtil.getExtensionValue(crl,
FACTORY.createExtension().getIssuingDistributionPoint().getId());
} catch (IOException | RuntimeException e) {
// Ignore exception.
}
return FACTORY.createIssuingDistributionPoint(issuingDistPointExtension);
}
private static Date getExpiredCertsOnCRLExtensionDate(X509CRL crl) {
IASN1Encodable expiredCertsOnCRL = null;
try {
// The scope of a CRL containing this extension is extended to include the revocation status of the
// certificates that expired after the date specified in ExpiredCertsOnCRL or at that date.
expiredCertsOnCRL = CertificateUtil.getExtensionValue(crl,
FACTORY.createExtension().getExpiredCertsOnCRL().getId());
} catch (IOException | RuntimeException e) {
// Ignore exception.
}
if (expiredCertsOnCRL != null) {
try {
return FACTORY.createASN1GeneralizedTime(expiredCertsOnCRL).getDate();
} catch (Exception e) {
// Ignore exception.
}
}
return (Date) TimestampConstants.UNDEFINED_TIMESTAMP_DATE;
}
private static int computeInterimReasonsMask(IIssuingDistributionPoint issuingDistPoint,
IDistributionPoint distributionPoint) {
int interimReasonsMask = ALL_REASONS;
if (!issuingDistPoint.isNull()) {
IReasonFlags onlySomeReasons = issuingDistPoint.getOnlySomeReasons();
if (!onlySomeReasons.isNull()) {
interimReasonsMask &= onlySomeReasons.intValue();
}
}
if (distributionPoint != null) {
IReasonFlags reasons = distributionPoint.getReasons();
if (!reasons.isNull()) {
interimReasonsMask &= reasons.intValue();
}
}
return interimReasonsMask;
}
private void verifyCrlIntegrity(ValidationReport report, ValidationContext context, X509Certificate certificate,
X509CRL crl, Date responseGenerationDate) {
Certificate[][] certificateSets = null;
try {
certificateSets = certificateRetriever.getCrlIssuerCertificatesByName(crl);
} catch (RuntimeException e) {
report.addReportItem(new CertificateReportItem(certificate, CRL_CHECK, CRL_ISSUER_REQUEST_FAILED, e,
ReportItemStatus.INDETERMINATE));
return;
}
if (certificateSets == null || certificateSets.length == 0) {
report.addReportItem(new CertificateReportItem(certificate, CRL_CHECK, CRL_ISSUER_NOT_FOUND,
ReportItemStatus.INDETERMINATE));
return;
}
ValidationReport[] candidateReports = new ValidationReport[certificateSets.length];
for (int i = 0; i < certificateSets.length; i++) {
ValidationReport candidateReport = new ValidationReport();
candidateReports[i] = candidateReport;
Certificate[] certs = certificateSets[i];
if (Arrays.asList(certs).contains(certificate)) {
candidateReport.addReportItem(new CertificateReportItem(certificate,
CRL_CHECK, CERTIFICATE_IN_ISSUER_CHAIN, ReportItemStatus.INDETERMINATE));
continue;
}
Certificate crlIssuer = certs[0];
List crlIssuerRoots = getRoots(crlIssuer);
List subjectRoots = getRoots(certificate);
if (!crlIssuerRoots.stream().anyMatch(cert -> subjectRoots.contains(cert))) {
candidateReport.addReportItem(new CertificateReportItem(certificate,
CRL_CHECK, CRL_ISSUER_NO_COMMON_ROOT, ReportItemStatus.INDETERMINATE));
continue;
}
onExceptionLog(() -> crl.verify(crlIssuer.getPublicKey()), candidateReport,
e -> new CertificateReportItem(certificate, CRL_CHECK, CRL_INVALID, e,
ReportItemStatus.INDETERMINATE));
ValidationReport responderReport = new ValidationReport();
onExceptionLog(() -> builder.getCertificateChainValidator().validate(responderReport,
context.setCertificateSource(CertificateSource.CRL_ISSUER),
(X509Certificate) crlIssuer, responseGenerationDate), candidateReport, e ->
new CertificateReportItem(certificate, CRL_CHECK, CRL_ISSUER_CHAIN_FAILED, e,
ReportItemStatus.INDETERMINATE));
addResponderValidationReport(candidateReport, responderReport);
if (candidateReport.getValidationResult() == ValidationResult.VALID) {
report.merge(candidateReport);
return;
}
}
// if failed, add all logs
for (ValidationReport candidateReport : candidateReports) {
report.merge(candidateReport);
}
}
private List getRoots(Certificate cert) {
List chains = certificateRetriever.buildCertificateChains((X509Certificate) cert);
return chains.stream().map(certArray -> certArray[certArray.length - 1]).collect(Collectors.toList());
}
private static void addResponderValidationReport(ValidationReport report, ValidationReport responderReport) {
for (ReportItem reportItem : responderReport.getLogs()) {
report.addReportItem(ReportItemStatus.INVALID == reportItem.getStatus() ?
reportItem.setStatus(ReportItemStatus.INDETERMINATE) : reportItem);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy