SignatureValidationService.java
package io.mersel.dss.signer.api.services.validation;
import eu.europa.esig.dss.detailedreport.DetailedReport;
import eu.europa.esig.dss.diagnostic.CertificateWrapper;
import eu.europa.esig.dss.diagnostic.DiagnosticData;
import eu.europa.esig.dss.enumerations.Indication;
import eu.europa.esig.dss.model.DSSDocument;
import eu.europa.esig.dss.simplereport.SimpleReport;
import eu.europa.esig.dss.spi.validation.CertificateVerifier;
import eu.europa.esig.dss.validation.SignedDocumentValidator;
import eu.europa.esig.dss.validation.reports.Reports;
import io.mersel.dss.signer.api.exceptions.SignatureException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* Service for validating electronic signatures.
* Provides detailed validation reports and trust chain analysis.
*/
@Service
public class SignatureValidationService {
private static final Logger LOGGER = LoggerFactory.getLogger(SignatureValidationService.class);
private final CertificateVerifier certificateVerifier;
public SignatureValidationService(CertificateVerifier certificateVerifier) {
this.certificateVerifier = certificateVerifier;
}
/**
* Validates a signed document and returns validation reports.
*
* @param signedDocument The document to validate
* @return Validation reports
*/
public Reports validateDocument(DSSDocument signedDocument) {
SignedDocumentValidator validator = SignedDocumentValidator.fromDocument(signedDocument);
validator.setCertificateVerifier(certificateVerifier);
return validator.validateDocument();
}
/**
* Validates document and throws exception if validation fails.
*
* @param signedDocument The document to validate
* @throws SignatureException if validation fails
*/
public void validateOrFail(DSSDocument signedDocument) {
Reports reports = validateDocument(signedDocument);
SimpleReport simpleReport = reports.getSimpleReport();
DetailedReport detailedReport = reports.getDetailedReport();
DiagnosticData diagnosticData = reports.getDiagnosticData();
// Get signature ID (use first signature if multiple exist)
List<String> signatureIds = simpleReport.getSignatureIdList();
String signatureId = signatureIds.isEmpty() ? null : signatureIds.get(0);
validateSignatureOrFail(simpleReport, detailedReport, diagnosticData, signatureId);
}
/**
* Validates signature and throws exception if validation fails.
* Only signing certificate chain issues are treated as critical.
* Timestamp TSA chain issues are warnings only.
*/
private void validateSignatureOrFail(SimpleReport simpleReport,
DetailedReport detailedReport,
DiagnosticData diagnosticData,
String signatureId) {
List<String> signatureIds = simpleReport.getSignatureIdList();
if (signatureIds == null || signatureIds.isEmpty()) {
String error = "Validation failed: No signatures found in document";
LOGGER.error(error);
throw new SignatureException("VALIDATION_FAILED", error);
}
LOGGER.info("Validating {} signature(s)", signatureIds.size());
boolean allValid = true;
StringBuilder errorDetails = new StringBuilder();
for (String sigId : signatureIds) {
Indication indication = simpleReport.getIndication(sigId);
LOGGER.debug("Signature {}: Indication = {}", sigId, indication);
if (indication != Indication.TOTAL_PASSED) {
allValid = false;
logCertificateChainDetails(diagnosticData, sigId);
errorDetails.append("\n[Signature: ").append(sigId).append("]");
errorDetails.append("\n Indication: ").append(indication);
if (simpleReport.getSubIndication(sigId) != null) {
errorDetails.append("\n SubIndication: ")
.append(simpleReport.getSubIndication(sigId));
}
} else {
LOGGER.info("Signature {} validation PASSED", sigId);
}
}
if (!allValid) {
// Check if failure is only due to timestamp chain issues
boolean hasSigningCertChainIssue = checkIfSigningCertificateChainHasIssues(
diagnosticData, signatureIds);
if (hasSigningCertChainIssue) {
String fullError = "Signing certificate chain validation failed!" +
errorDetails.toString();
LOGGER.error(fullError);
throw new SignatureException("CERTIFICATE_CHAIN_ERROR", fullError);
} else {
LOGGER.warn("Validation warnings detected (timestamp TSA chain only)");
LOGGER.info("Signing certificate chain is valid - signature is acceptable");
}
} else {
LOGGER.info("All signature validations PASSED");
}
}
/**
* Checks if signing certificate (not timestamp) has chain validation issues.
*/
private boolean checkIfSigningCertificateChainHasIssues(DiagnosticData diagnosticData,
List<String> signatureIds) {
try {
List<CertificateWrapper> allCerts = diagnosticData.getUsedCertificates();
// Find KamuSM or TUBİTAK root
boolean foundKamuSMRoot = false;
CertificateWrapper kamusmRoot = null;
for (CertificateWrapper cert : allCerts) {
String sources = cert.getSources() != null ? cert.getSources().toString() : "";
String subject = cert.getCertificateDN();
if (cert.isSelfSigned() &&
sources.contains("SIGNATURE") &&
!sources.contains("TIMESTAMP") &&
(subject.contains("KamuSM") || subject.contains("TÜBİTAK") ||
subject.contains("TUBITAK"))) {
foundKamuSMRoot = true;
kamusmRoot = cert;
LOGGER.debug("Found KamuSM/TUBİTAK root: {}", subject);
break;
}
}
if (!foundKamuSMRoot) {
LOGGER.error("No KamuSM/TUBİTAK root found in signing certificate chain");
return true; // Critical error
}
// Check if signing certificate chains to root
for (CertificateWrapper cert : allCerts) {
String sources = cert.getSources() != null ? cert.getSources().toString() : "";
if (!cert.isSelfSigned() &&
sources.contains("SIGNATURE") &&
!sources.contains("TIMESTAMP") &&
!sources.contains("OCSP")) {
if (canReachRoot(diagnosticData, cert, kamusmRoot)) {
LOGGER.info("Signing certificate chains to trusted root successfully");
return false; // All good
}
}
}
return false; // Conservative: allow if inconclusive
} catch (Exception e) {
LOGGER.warn("Error checking signing certificate chain: {}", e.getMessage());
return false; // Don't block on analysis errors
}
}
private boolean canReachRoot(DiagnosticData diagnosticData,
CertificateWrapper cert,
CertificateWrapper targetRoot) {
try {
if (cert.getId().equals(targetRoot.getId())) {
return true;
}
if (cert.isSelfSigned()) {
return false;
}
String issuerDN = cert.getCertificateIssuerDN();
List<CertificateWrapper> allCerts = diagnosticData.getUsedCertificates();
for (CertificateWrapper candidate : allCerts) {
if (candidate.getCertificateDN().equals(issuerDN)) {
return canReachRoot(diagnosticData, candidate, targetRoot);
}
}
return false;
} catch (Exception e) {
return false;
}
}
private void logCertificateChainDetails(DiagnosticData diagnosticData, String signatureId) {
LOGGER.info("Certificate chain analysis:");
try {
List<CertificateWrapper> allCerts = diagnosticData.getUsedCertificates();
LOGGER.info("Found {} certificate(s) in signature", allCerts.size());
for (CertificateWrapper cert : allCerts) {
LOGGER.debug("Certificate: Subject={}, Issuer={}, Sources={}, Trusted={}, SelfSigned={}",
cert.getCertificateDN(),
cert.getCertificateIssuerDN(),
cert.getSources(),
cert.isTrusted(),
cert.isSelfSigned());
}
} catch (Exception e) {
LOGGER.warn("Failed to extract certificate chain details: {}", e.getMessage());
}
}
}