/*
* DSS - Digital Signature Services
*
* Copyright (C) 2013 European Commission, Directorate-General Internal Market and Services (DG MARKT), B-1049 Bruxelles/Brussel
*
* Developed by: 2013 ARHS Developments S.A. (rue Nicolas Bové 2B, L-1253 Luxembourg) http://www.arhs-developments.com
*
* This file is part of the "DSS - Digital Signature Services" project.
*
* "DSS - Digital Signature Services" is free software: you can redistribute it and/or modify it under the terms of
* the GNU Lesser General Public License as published by the Free Software Foundation, either version 2.1 of the
* License, or (at your option) any later version.
*
* DSS 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with
* "DSS - Digital Signature Services". If not, see <http://www.gnu.org/licenses/>.
*/
package eu.europa.ec.markt.dss.validation102853;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import javax.security.auth.x500.X500Principal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import eu.europa.ec.markt.dss.DSSUtils;
import eu.europa.ec.markt.dss.exception.DSSException;
import eu.europa.ec.markt.dss.exception.DSSNullException;
import eu.europa.ec.markt.dss.validation102853.certificate.CertificateSourceType;
import eu.europa.ec.markt.dss.validation102853.condition.ServiceInfo;
import eu.europa.ec.markt.dss.validation102853.crl.CRLSource;
import eu.europa.ec.markt.dss.validation102853.crl.CRLToken;
import eu.europa.ec.markt.dss.validation102853.loader.DataLoader;
import eu.europa.ec.markt.dss.validation102853.ocsp.OCSPSource;
/**
* During the validation of a signature, the software retrieves different X509 artifacts like Certificate, CRL and OCSP Response. The SignatureValidationContext is a "cache" for
* one validation request that contains every object retrieved so far.
* <p/>
* The validate method is multi-threaded, using an CachedThreadPool from ExecutorService, to parallelize fetching of the certificates from AIA and of the revocation information
* from online sources.
*
* @version $Revision: 1839 $ - $Date: 2013-04-04 17:40:51 +0200 (Thu, 04 Apr 2013) $
*/
public class SignatureValidationContext implements ValidationContext {
private static final Logger LOG = LoggerFactory.getLogger(SignatureValidationContext.class);
/**
* Each unit is approximately 5 seconds
*/
public static int MAX_TIMEOUT = 5;
private final Set<CertificateToken> processedCertificates = new HashSet<CertificateToken>();
private final Set<RevocationToken> processedRevocations = new HashSet<RevocationToken>();
private final Set<TimestampToken> processedTimestamps = new HashSet<TimestampToken>();
static int threadCount = 0;
/**
* The data loader used to access AIA certificate source.
*/
private DataLoader dataLoader;
/**
* The certificate pool which encapsulates all certificates used during the validation process and extracted from all used sources
*/
protected CertificatePool validationCertificatePool;
private final Map<Token, Boolean> tokensToProcess = new HashMap<Token, Boolean>();
// External OCSP source.
private OCSPSource ocspSource;
// External CRL source.
private CRLSource crlSource;
// OCSP from the signature.
private OCSPSource signatureOCSPSource;
// CRLs from the signature.
private CRLSource signatureCRLSource;
// The digest value of the certification path references and the revocation status references.
private List<TimestampReference> timestampedReferences;
/**
* This is the time at what the validation is carried out. It is used only for test purpose.
*/
protected Date currentTime = new Date();
/**
* This variable :
*/
protected ExecutorService executorService;
/**
* This constructor is used when a signature need to be validated.
*
* @param certificateVerifier The certificates verifier (eg: using the TSL as list of trusted certificates).
* @param validationCertificatePool The pool of certificates used during the validation process
*/
public SignatureValidationContext(final CertificateVerifier certificateVerifier, final CertificatePool validationCertificatePool) {
if (certificateVerifier == null) {
throw new DSSNullException(CertificateVerifier.class);
}
if (validationCertificatePool == null) {
throw new DSSNullException(CertificatePool.class);
}
this.validationCertificatePool = validationCertificatePool;
this.crlSource = certificateVerifier.getCrlSource();
this.ocspSource = certificateVerifier.getOcspSource();
this.dataLoader = certificateVerifier.getDataLoader();
this.signatureCRLSource = certificateVerifier.getSignatureCRLSource();
this.signatureOCSPSource = certificateVerifier.getSignatureOCSPSource();
}
@Override
public ExecutorService getExecutorService() {
return executorService;
}
@Override
public void setExecutorService(final ExecutorService executorService) {
this.executorService = executorService;
}
private ExecutorService provideExecutorService() {
if (executorService == null) {
executorService = Executors.newCachedThreadPool();
}
return executorService;
}
public Date getCurrentTime() {
return currentTime;
}
public void setCurrentTime(final Date currentTime) throws DSSException {
if (currentTime == null) {
throw new DSSNullException(Date.class, "currentTime");
}
this.currentTime = currentTime;
}
/**
* This method returns a token to verify. If there is no more tokens to verify null is returned.
*
* @return token to verify or null
*/
private Token getNotYetVerifiedToken() {
// LOG.debug("getNotYetVerifiedToken: trying to acquire synchronized block");
synchronized (tokensToProcess) {
// LOG.debug("getNotYetVerifiedToken: acquired synchronized block");
for (final Entry<Token, Boolean> entry : tokensToProcess.entrySet()) {
if (entry.getValue() == null) {
entry.setValue(true);
return entry.getKey();
}
}
// LOG.debug("getNotYetVerifiedToken: almost left synchronized block");
return null;
}
}
/**
* This method returns the issuer certificate (the certificate which was used to sign the token) of the given token.
*
* @param token the token for which the issuer must be obtained.
* @return the issuer certificate token of the given token or null if not found.
* @throws eu.europa.ec.markt.dss.exception.DSSException
*/
private CertificateToken getIssuerCertificate(final Token token) throws DSSException {
if (token.isTrusted()) {
// When the token is trusted the check of the issuer token is not needed so null is returned. Only a certificate token can be trusted.
return null;
}
if (token.getIssuerToken() != null) {
/**
* The signer's certificate have been found already. This can happen in the case of:<br>
* - multiple signatures that use the same certificate,<br>
* - OCSPRespTokens (the issuer certificate is known from the beginning)
*/
return token.getIssuerToken();
}
final X500Principal issuerX500Principal = token.getIssuerX500Principal();
CertificateToken issuerCertificateToken = getIssuerFromPool(token, issuerX500Principal);
if (issuerCertificateToken == null && token instanceof CertificateToken) {
issuerCertificateToken = getIssuerFromAIA((CertificateToken) token);
}
if (issuerCertificateToken == null) {
token.extraInfo().infoTheSigningCertNotFound();
}
if (issuerCertificateToken != null && !issuerCertificateToken.isTrusted() && !issuerCertificateToken.isSelfSigned()) {
// The full chain is retrieved for each certificate
getIssuerCertificate(issuerCertificateToken);
}
return issuerCertificateToken;
}
/**
* Get the issuer's certificate from Authority Information Access through id-ad-caIssuers extension.
*
* @param token {@code CertificateToken} for which the issuer is sought.
* @return {@code CertificateToken} representing the issuer certificate or null.
*/
private CertificateToken getIssuerFromAIA(final CertificateToken token) {
final X509Certificate issuerCert;
try {
LOG.info("Retrieving {} certificate's issuer using AIA.", token.getAbbreviation());
issuerCert = DSSUtils.loadIssuerCertificate(token.getCertificate(), dataLoader);
if (issuerCert != null) {
final CertificateToken issuerCertToken = validationCertificatePool.getInstance(issuerCert, CertificateSourceType.AIA);
if (token.isSignedBy(issuerCertToken)) {
return issuerCertToken;
}
LOG.info("The retrieved certificate using AIA does not sign the certificate {}.", token.getAbbreviation());
} else {
LOG.info("The issuer certificate cannot be loaded using AIA.");
}
} catch (DSSException e) {
LOG.error(e.getMessage());
}
return null;
}
/**
* This function retrieves the issuer certificate from the validation pool (this pool should contain trusted certificates). The check is made if the token is well signed by
* the retrieved certificate.
*
* @param token token for which the issuer have to be found
* @param issuerX500Principal issuer's subject distinguished name
* @return the corresponding {@code CertificateToken} or null if not found
*/
private CertificateToken getIssuerFromPool(final Token token, final X500Principal issuerX500Principal) {
final List<CertificateToken> issuerCertList = validationCertificatePool.get(issuerX500Principal);
for (final CertificateToken issuerCertToken : issuerCertList) {
// We keep the first issuer that signs the certificate
if (token.isSignedBy(issuerCertToken)) {
return issuerCertToken;
}
}
return null;
}
/**
* Adds a new token to the list of tokes to verify only if it was not already verified.
*
* @param token token to verify
* @return true if the token was not yet verified, false otherwise.
*/
private boolean addTokenForVerification(final Token token) {
final boolean traceEnabled = LOG.isTraceEnabled();
synchronized (tokensToProcess) {
if (traceEnabled) {
LOG.trace("addTokenForVerification: trying to acquire synchronized block");
}
try {
if (token == null) {
return false;
}
if (tokensToProcess.containsKey(token)) {
if (traceEnabled) {
LOG.trace("Token was already in the list {}:{}", new Object[]{token.getClass().getSimpleName(), token.getAbbreviation()});
}
return false;
}
tokensToProcess.put(token, null);
if (traceEnabled) {
LOG.trace("+ New {} to check: {}", new Object[]{token.getClass().getSimpleName(), token.getAbbreviation()});
}
return true;
} finally {
if (traceEnabled) {
LOG.trace("addTokenForVerification: almost left synchronized block");
}
}
}
}
@Override
public void addRevocationTokenForVerification(final RevocationToken revocationToken) {
if (addTokenForVerification(revocationToken)) {
final boolean added = processedRevocations.add(revocationToken);
if (LOG.isTraceEnabled()) {
if (added) {
LOG.trace("RevocationToken added to processedRevocations: {} ", revocationToken);
} else {
LOG.trace("RevocationToken already present processedRevocations: {} ", revocationToken);
}
}
}
}
@Override
public void addCertificateTokenForVerification(final CertificateToken certificateToken) {
if (addTokenForVerification(certificateToken)) {
final boolean added = processedCertificates.add(certificateToken);
if (LOG.isTraceEnabled()) {
if (added) {
LOG.trace("CertificateToken added to processedRevocations: {} ", certificateToken);
} else {
LOG.trace("CertificateToken already present processedRevocations: {} ", certificateToken);
}
}
}
}
@Override
public void addTimestampTokenForVerification(final TimestampToken timestampToken) {
if (addTokenForVerification(timestampToken)) {
final boolean added = processedTimestamps.add(timestampToken);
if (LOG.isTraceEnabled()) {
if (added) {
LOG.trace("TimestampToken added to processedRevocations: {} ", processedTimestamps);
} else {
LOG.trace("TimestampToken already present processedRevocations: {} ", processedTimestamps);
}
}
}
}
@Override
public void validate() throws DSSException {
validateLoop();
try {
LOG.debug(">>> MT ***DONE***");
final ExecutorService executorService = provideExecutorService();
executorService.shutdown();
executorService.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
private void validateLoop() {
int threshold = 0;
int max_timeout = 0;
final ExecutorService executorService = provideExecutorService();
final ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executorService;
boolean exit = false;
boolean checkAgain = true;
do {
final Token token = getNotYetVerifiedToken();
if (token != null) {
checkAgain = true;
try {
// System.out.println("----------------------------------------------------");
// System.out.println(" DSS_ID: " + token.getDSSId());
// System.out.println("----------------------------------------------------");
final Task task = new Task(token);
executorService.submit(task);
} catch (RejectedExecutionException e) {
LOG.error(e.getMessage(), e);
throw new DSSException(e);
}
} else {
try {
Thread.sleep(5);
threshold++;
if (threshold > 1000) {
LOG.warn("{} active threads", threadPoolExecutor.getActiveCount());
LOG.warn("{} completed tasks", threadPoolExecutor.getCompletedTaskCount());
LOG.warn("{} waiting tasks", threadPoolExecutor.getQueue());
max_timeout++;
if (max_timeout == MAX_TIMEOUT) {
throw new DSSException("Operation aborted, the retrieval of the validation data takes too long.");
}
threshold = 0;
}
} catch (InterruptedException e) {
throw new DSSException(e);
}
final boolean threadPoolExecutorEmpty = !(threadPoolExecutor.getActiveCount() > 0 || threadPoolExecutor.getQueue().size() > 0);
exit = threadPoolExecutorEmpty && !checkAgain;
if (threadPoolExecutorEmpty) {
checkAgain = false;
}
}
} while (!exit);
}
class Task implements Runnable {
private final Token token;
public Task(final Token token) {
this.token = token;
}
@Override
public void run() {
final int threadCount_ = threadCount++;
LOG.debug(">>> MT IN [" + threadCount_ + "] DSS_ID: " + token.getDSSId());
/**
* Gets the issuer certificate of the Token and checks its signature
*/
final CertificateToken issuerCertToken = getIssuerCertificate(token);
if (issuerCertToken != null) {
addCertificateTokenForVerification(issuerCertToken);
}
if (token instanceof CertificateToken) {
final CertificateToken certificateToken = (CertificateToken) token;
final RevocationToken currentRevocationToken = certificateToken.getRevocationToken();
if (currentRevocationToken != null) {
if (currentRevocationToken instanceof OCSPToken && ocspSource != null) {
if (ocspSource.isFresh(currentRevocationToken)) {
LOG.debug("OCSP revocation data for the certificate {} is considered as fresh", certificateToken.getAbbreviation());
return;
}
} else if (currentRevocationToken instanceof CRLToken && crlSource != null) {
if (crlSource.isFresh(currentRevocationToken)) {
LOG.debug("CRL revocation data for the certificate {} is considered as fresh", certificateToken.getAbbreviation());
return;
}
}
}
final RevocationToken revocationToken = getRevocationData(certificateToken);
addRevocationTokenForVerification(revocationToken);
}
LOG.debug(">>> MT END [" + threadCount_ + "] DSS_ID: " + token.getDSSId());
}
}
/**
* Retrieves the revocation data from signature (if exists) or from the online sources. The issuer certificate must be provided, the underlining library (bouncy castle) needs
* it to build the request. This feature has an impact on the multi-threaded data retrieval.
*
* @param certToken
* @return
*/
private RevocationToken getRevocationData(final CertificateToken certToken) {
if (LOG.isTraceEnabled()) {
LOG.trace("Checking revocation data for: " + certToken.getDSSIdAsString());
}
if (certToken.isSelfSigned() || certToken.isTrusted() || certToken.getIssuerToken() == null) {
// It is not possible to check the revocation data without its signing certificate;
// This check is not needed for the trust anchor.
return null;
}
if (certToken.isOCSPSigning() && certToken.hasIdPkixOcspNoCheckExtension()) {
certToken.extraInfo().add("OCSP check not needed: id-pkix-ocsp-nocheck extension present.");
return null;
}
boolean checkOnLine = shouldCheckOnLine(certToken);
if (checkOnLine) {
final OCSPAndCRLCertificateVerifier onlineVerifier = new OCSPAndCRLCertificateVerifier(crlSource, ocspSource, validationCertificatePool);
final RevocationToken revocationToken = onlineVerifier.check(certToken);
if (revocationToken != null) {
return revocationToken;
}
}
final OCSPAndCRLCertificateVerifier offlineVerifier = new OCSPAndCRLCertificateVerifier(signatureCRLSource, signatureOCSPSource, validationCertificatePool);
final RevocationToken revocationToken = offlineVerifier.check(certToken);
return revocationToken;
}
private boolean shouldCheckOnLine(final CertificateToken certificateToken) {
final boolean expired = certificateToken.isExpiredOn(currentTime);
if (!expired) {
return true;
}
final CertificateToken issuerCertToken = certificateToken.getIssuerToken();
// issuerCertToken cannot be null
final boolean expiredCertOnCRLExtension = issuerCertToken.hasExpiredCertOnCRLExtension();
if (expiredCertOnCRLExtension) {
certificateToken.extraInfo().add("Certificate is expired but the issuer certificate has ExpiredCertOnCRL extension.");
return true;
}
final Date expiredCertsRevocationFromDate = getExpiredCertsRevocationFromDate(certificateToken);
if (expiredCertsRevocationFromDate != null) {
certificateToken.extraInfo().add("Certificate is expired but the TSL extension 'expiredCertsRevocationInfo' is present: " + expiredCertsRevocationFromDate);
return true;
}
return false;
}
private Date getExpiredCertsRevocationFromDate(final CertificateToken certificateToken) {
final CertificateToken trustAnchor = certificateToken.getTrustAnchor();
if (trustAnchor != null) {
final List<ServiceInfo> serviceInfoList = trustAnchor.getAssociatedTSPS();
if (serviceInfoList != null) {
final Date notAfter = certificateToken.getNotAfter();
for (final ServiceInfo serviceInfo : serviceInfoList) {
final Date date = serviceInfo.getExpiredCertsRevocationInfo();
if (date != null && date.before(notAfter)) {
if (serviceInfo.getStatusEndDate() == null) {
/**
* Service is still active (operational)
*/
// if(serviceInfo.getStatus().equals())
return date;
}
}
}
}
}
return null;
}
@Override
public Set<CertificateToken> getProcessedCertificates() {
return Collections.unmodifiableSet(processedCertificates);
}
@Override
public Set<RevocationToken> getProcessedRevocations() {
return Collections.unmodifiableSet(processedRevocations);
}
@Override
public Set<TimestampToken> getProcessedTimestamps() {
return Collections.unmodifiableSet(processedTimestamps);
}
/**
* Returns certificate and revocation references.
*
* @return
*/
public List<TimestampReference> getTimestampedReferences() {
return timestampedReferences;
}
/**
* This method returns the human readable representation of the ValidationContext.
*
* @param indentStr
* @return
*/
public String toString(String indentStr) {
try {
final StringBuilder builder = new StringBuilder();
builder.append(indentStr).append("ValidationContext[").append('\n');
indentStr += "\t";
// builder.append(indentStr).append("Validation time:").append(validationDate).append('\n');
builder.append(indentStr).append("Certificates[").append('\n');
indentStr += "\t";
for (CertificateToken certToken : processedCertificates) {
builder.append(certToken.toString(indentStr));
}
indentStr = indentStr.substring(1);
builder.append(indentStr).append("],\n");
indentStr = indentStr.substring(1);
builder.append(indentStr).append("],\n");
return builder.toString();
} catch (Exception e) {
return super.toString();
}
}
@Override
public String toString() {
return toString("");
}
}