XAdESLevelXL.java
// @formatter:off
/**
* DSS - Digital Signature Services
* Copyright (C) 2015 European Commission, provided under the CEF programme
* <p>
* This file is part of the "DSS - Digital Signature Services" project.
* <p>
* This library 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.
* <p>
* This library 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.
* <p>
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package eu.europa.esig.dss.xades.signature;
import eu.europa.esig.dss.model.x509.CertificateToken;
import eu.europa.esig.dss.model.x509.Token;
import eu.europa.esig.dss.signature.SignatureRequirementsChecker;
import eu.europa.esig.dss.spi.x509.revocation.crl.CRLToken;
import eu.europa.esig.dss.spi.x509.revocation.ocsp.OCSPToken;
import eu.europa.esig.dss.spi.x509.tsp.TimestampToken;
import eu.europa.esig.dss.utils.Utils;
import eu.europa.esig.dss.spi.signature.AdvancedSignature;
import eu.europa.esig.dss.spi.validation.CertificateVerifier;
import eu.europa.esig.dss.spi.validation.ValidationData;
import eu.europa.esig.dss.spi.validation.ValidationDataContainer;
import eu.europa.esig.dss.xades.DSSXMLUtils;
import eu.europa.esig.dss.xades.definition.xades141.XAdES141Element;
import eu.europa.esig.dss.xades.validation.XAdESSignature;
import eu.europa.esig.dss.xml.common.definition.xmldsig.XMLDSigAttribute;
import eu.europa.esig.dss.xml.utils.DomUtils;
import eu.europa.esig.dss.enumerations.DigestAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static eu.europa.esig.dss.enumerations.SignatureLevel.XAdES_XL;
/*
* ########################OVERRIDE_DSS#########################
* #### DİKKAT: BU DÖKÜMAN, STANDART DSS KODUNDAN ZİYADE ####
* #### SIFIRDAN YAZILMIŞTIR. ORİJİNAL OVERRIDE YAKLAŞIMI ####
* #### DEĞİL, CACHE YÖNETİMİ VE XAdES 1.4.1 UYUMLULUĞU ####
* #### ODAKLI ÖZEL TÜRKİYE İÇİN UYGULANMIŞTIR. ####
* #############################################################
*
* 1) Revocation (ve timestamp) validasyon verilerinin saklanması ve yeniden kullanılabilmesi önceliklidir.
* Burada cache ve validation-data yönetimine özel geliştirmeler ana motivasyondur. Yani, sadece
* imza zinciri ve doğrulama için dışarıdan gelen kaynaklar tekrar tekrar sorgulanmaz, gereken
* şekilde cache'den çekilir/kullanılır.
*
* 2) Tübitak'ın XAdES 1.4.1 profilinde tanımladığı gibi, timestamp validasyon verileri
* XAdES-X düzeyindeki <xades:RevocationValues> veya <xades:CertificateValues> bloklarından
* genelden ayrılarak yalnızca <xades141:TimestampValidationData> alanında kapsüllenmiştir.
* Yani, zamana bağlı doğrulama için kullanılan ek bilgiler (ör: dış OCSP/CRL, ek sertifikalar)
* artık shared/generic alanlar yerine, direk olarak sadece zamana-mühür validasyonunun altına
* XAdES141 namespace'iyle geçirilir ve ayrıştırılır. Bu hem şematik hem fonksiyonel ayrımı
* sağlar.
*
* 3) Söz konusu kod, Tübitak'ın yayımladığı e-İmza Kılavuzlarına/formatlarına ve Türkiye
* dokümantasyonuna uygun olacak şekilde kapsülleme ve doğrulama veri yönetimini gerçekleştirir.
*
* 4) Temel farklar:
* - XAdES 1.4.1 uyumlu <xades141:TimestampValidationData> node kullanımı.
* - Revocation (CRL/OCSP) değerlerinin ve ilgili sertifikaların, timestamp'in dışındaki
* doğrulama gereksinimlerinden ayrık şekilde işlenmesi.
* - Imza seviyesi güncellemelerinde (XL+ seviyelerine yükseltmede), bu ayrık veri modellerine
* uygun olarak validation-data segmentlerinin ilgili alt alanlarda oluşturulması ve yönetilmesi.
* - DSS'nin generic cache/validation-data mekanizmaları, Türkiye uygulama ihtiyacına uygun şekilde,
* hem signature-chain hem de zaman damgası altındaki tokenlar için revize edilmiştir.
*
* 5) Kendi içinde, TÜBİTAK'ın kamu uygulamalarında zorunlu tuttuğu şema ve geçerlilik gereklilikleri
* sağlanır.
*
* 6) Kodun kalanında, "XAdES 1.4.1 REVOCATION/TIMESTAMP VALIDATION DATA HANDLING" etiketli bölümlerde
* ayrımın ve özel modellemenin detaylarına yer verilmiştir, gerekirse ilgili fonksiyonlarda
* açıklama ve ek loglama ile takip edilebilir.
*/
/**
* XL profile of XAdES signature
*
*/
public class XAdESLevelXL extends XAdESLevelX {
private static final Logger LOGGER = LoggerFactory.getLogger(XAdESLevelXL.class);
/**
* The default constructor for XAdESLevelXL.
*
* @param certificateVerifier {@link CertificateVerifier}
*/
public XAdESLevelXL(final CertificateVerifier certificateVerifier) {
super(certificateVerifier);
}
/**
* Adds CertificateValues and RevocationValues segments to UnsignedSignatureProperties.<br>
*
* An XML electronic signature MAY contain at most one:<br>
* - CertificateValues element and<br>
* - RevocationValues element.
*
* @see XAdESLevelX#extendSignatures(List)
*/
@Override
protected void extendSignatures(List<AdvancedSignature> signatures) {
super.extendSignatures(signatures);
final List<AdvancedSignature> signaturesToExtend = getExtendToXLLevelSignatures(signatures);
if (Utils.isCollectionEmpty(signaturesToExtend)) {
return;
}
for (AdvancedSignature signature : signatures) {
initializeSignatureBuilder((XAdESSignature) signature);
// NOTE: do not force sources reload for certificate and revocation sources
// in order to ensure the same validation data as on -C level
xadesSignature.resetTimestampSource();
}
final SignatureRequirementsChecker signatureRequirementsChecker = getSignatureRequirementsChecker();
if (XAdES_XL.equals(params.getSignatureLevel())) {
signatureRequirementsChecker.assertExtendToXLLevelPossible(signatures);
}
signatureRequirementsChecker.assertSignaturesValid(signaturesToExtend);
signatureRequirementsChecker.assertCertificateChainValidForXLLevel(signatures);
// Perform signature validation
// CRITICAL: Fetch NEW validation data for timestamp validation
// OCSP tokens will be replaced with cached versions in
// replaceWithCachedOcspTokens()
ValidationDataContainer validationDataContainer = documentAnalyzer.getValidationData(signatures);
for (AdvancedSignature signature : signatures) {
initializeSignatureBuilder((XAdESSignature) signature);
if (signatureRequirementsChecker.hasALevelOrHigher(signature)) {
// Unable to extend due to higher levels covering the current XL-level
continue;
}
String indent = removeOldCertificateValues();
removeOldRevocationValues();
String timestampIndent = removeLastTimestampAndAnyValidationData();
if (indent == null) {
indent = timestampIndent;
}
Element levelXUnsignedProperties = (Element) unsignedSignaturePropertiesDom.cloneNode(true);
ValidationData aggregatedValidationData = validationDataContainer
.getAllValidationDataForSignatureForInclusion(signature);
ValidationData remainingValidationData = new ValidationData();
if (aggregatedValidationData != null) {
remainingValidationData.addValidationData(aggregatedValidationData);
}
List<TimestampToken> timestamps = xadesSignature.getAllTimestamps();
if (Utils.isCollectionNotEmpty(timestamps)) {
for (TimestampToken timestampToken : timestamps) {
ValidationData timestampValidationData = validationDataContainer.getValidationData(timestampToken);
if (timestampValidationData == null) {
continue;
}
ValidationData timestampDataCopy = new ValidationData();
timestampDataCopy.addValidationData(timestampValidationData);
if (timestampDataCopy.isEmpty()) {
continue;
}
remainingValidationData.excludeCertificateTokens(timestampDataCopy.getCertificateTokens());
remainingValidationData.excludeCRLTokensCollection(timestampDataCopy.getCrlTokens());
remainingValidationData.excludeOCSPTokens(
timestampDataCopy.getOcspTokens()
.stream()
.map(Token::getDSSId)
.collect(Collectors.toSet()));
appendTimestampValidationDataAfterTimestamp(timestampToken, timestampDataCopy, indent);
}
}
Set<CertificateToken> certificateValuesToAdd = remainingValidationData.getCertificateTokens();
Set<CRLToken> crlsToAdd = remainingValidationData.getCrlTokens();
Set<OCSPToken> ocspsToAdd = remainingValidationData.getOcspTokens();
// CRITICAL: Exclude timestamp signing certificates from the certificate values
// TSA certificates are already in their respective timestamp tokens
Set<CertificateToken> filteredCerts = excludeAllTimestampSigningCertificates(
certificateValuesToAdd, timestamps);
LOGGER.info("XL-LEVEL: Total certs: {}, After excluding TSA certs: {}",
certificateValuesToAdd.size(), filteredCerts.size());
// CRITICAL: Replace OCSP tokens with cached ones from C-level to ensure digest
// matches
Set<OCSPToken> ocspTokensToEmbed = replaceWithCachedOcspTokens(ocspsToAdd);
incorporateCertificateValues(unsignedSignaturePropertiesDom, filteredCerts, indent);
incorporateRevocationValues(unsignedSignaturePropertiesDom, crlsToAdd, ocspTokensToEmbed, indent);
unsignedSignaturePropertiesDom = indentIfPrettyPrint(unsignedSignaturePropertiesDom,
levelXUnsignedProperties);
}
}
private void appendTimestampValidationDataAfterTimestamp(TimestampToken timestampToken,
ValidationData validationData,
String indent) {
if (validationData == null || validationData.isEmpty()) {
return;
}
Element timestampElement = locateTimestampElement(timestampToken);
Element parentElement = unsignedSignaturePropertiesDom;
String fallbackTimestampId = TIMESTAMP_PREFIX + toXmlIdentifier(timestampToken.getDSSId());
String effectiveTimestampId = fallbackTimestampId;
if (timestampElement != null) {
Node parentNode = timestampElement.getParentNode();
if (parentNode instanceof Element) {
parentElement = (Element) parentNode;
}
String existingId = timestampElement.getAttribute(XMLDSigAttribute.ID.getAttributeName());
if (existingId != null && !existingId.isEmpty()) {
effectiveTimestampId = existingId;
}
}
Element validationDataDom = DomUtils.addElement(documentDom, parentElement,
getXades141Namespace(), XAdES141Element.TIMESTAMP_VALIDATION_DATA);
Set<CertificateToken> certificateTokens = validationData.getCertificateTokens();
Set<CRLToken> crlTokens = validationData.getCrlTokens();
Set<OCSPToken> ocspTokens = validationData.getOcspTokens();
// CRITICAL: Exclude timestamp's signing certificate (TSA certificate)
// It's already included in the timestamp token itself, no need to duplicate
Set<CertificateToken> certificatesToEmbed = excludeTimestampSigningCertificate(
certificateTokens, timestampToken);
LOGGER.info("TIMESTAMP-VAL-DATA: Total certs: {}, After excluding TSA cert: {}",
certificateTokens.size(), certificatesToEmbed.size());
incorporateCertificateValues(validationDataDom, certificatesToEmbed, indent);
incorporateRevocationValues(validationDataDom, crlTokens, ocspTokens, indent);
String idSuffix = toXmlIdentifier(timestampToken.getDSSId());
validationDataDom.setAttribute(XMLDSigAttribute.ID.getAttributeName(), TST_VD_PREFIX + idSuffix);
validationDataDom.setAttribute("URI", "#" + effectiveTimestampId);
if (timestampElement != null && timestampElement.getParentNode() == parentElement) {
Node nextSibling = timestampElement.getNextSibling();
if (nextSibling != null) {
parentElement.insertBefore(validationDataDom, nextSibling);
}
}
if (params.isPrettyPrint()) {
DSSXMLUtils.indentAndReplace(documentDom, validationDataDom);
}
}
private Element locateTimestampElement(TimestampToken timestampToken) {
byte[] targetEncoded = timestampToken.getEncoded();
if (targetEncoded == null) {
return null;
}
Element directMatch = locateTimestampElement(unsignedSignaturePropertiesDom, targetEncoded);
if (directMatch != null) {
return directMatch;
}
Element signatureElement = xadesSignature.getSignatureElement();
NodeList encapsulatedNodes = signatureElement.getElementsByTagNameNS("*",
getCurrentXAdESElements().getElementEncapsulatedTimeStamp().getTagName());
for (int i = 0; i < encapsulatedNodes.getLength(); i++) {
Node node = encapsulatedNodes.item(i);
if (!(node instanceof Element)) {
continue;
}
String xmlValue = ((Element) node).getTextContent();
if (!Utils.isStringNotEmpty(xmlValue)) {
continue;
}
byte[] xmlBytes = Utils.fromBase64(xmlValue);
if (xmlBytes != null && Arrays.equals(xmlBytes, targetEncoded)) {
Node parent = node.getParentNode();
if (parent instanceof Element) {
return (Element) parent;
}
}
}
return null;
}
private Element locateTimestampElement(Element parent, byte[] targetEncoded) {
NodeList children = parent.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
if (!(node instanceof Element)) {
continue;
}
Element element = (Element) node;
if (!isTimestampLocalName(element.getLocalName())) {
continue;
}
Element encapsulated = findEncapsulatedTimestamp(element);
if (encapsulated == null) {
continue;
}
String xmlValue = encapsulated.getTextContent();
if (!Utils.isStringNotEmpty(xmlValue)) {
continue;
}
byte[] xmlBytes = Utils.fromBase64(xmlValue);
if (xmlBytes != null && Arrays.equals(xmlBytes, targetEncoded)) {
return element;
}
}
return null;
}
private boolean isTimestampLocalName(String localName) {
if (localName == null) {
return false;
}
return "SignatureTimeStamp".equals(localName)
|| "SigAndRefsTimeStamp".equals(localName)
|| "SigAndRefsTimeStampV2".equals(localName)
|| "RefsOnlyTimeStamp".equals(localName)
|| "RefsOnlyTimeStampV2".equals(localName)
|| "ArchiveTimeStamp".equals(localName);
}
private Element findEncapsulatedTimestamp(Element timestampElement) {
String encapsulatedTag = getCurrentXAdESElements().getElementEncapsulatedTimeStamp().getTagName();
NodeList encapsulatedList = timestampElement.getElementsByTagNameNS("*", encapsulatedTag);
for (int i = 0; i < encapsulatedList.getLength(); i++) {
Node candidate = encapsulatedList.item(i);
if (candidate instanceof Element) {
return (Element) candidate;
}
}
return null;
}
private List<AdvancedSignature> getExtendToXLLevelSignatures(List<AdvancedSignature> signatures) {
final List<AdvancedSignature> signaturesToExtend = new ArrayList<>();
for (AdvancedSignature signature : signatures) {
if (xlLevelExtensionRequired(signature)) {
signaturesToExtend.add(signature);
}
}
return signaturesToExtend;
}
private boolean xlLevelExtensionRequired(AdvancedSignature signature) {
return XAdES_XL.equals(params.getSignatureLevel()) || !signature.hasAProfile();
}
/**
* Excludes all timestamp signing certificates from the certificate set.
* This processes all timestamps in the signature and removes their TSA certificates.
*
* @param certificates The set of certificates to filter
* @param timestamps List of all timestamps in the signature
* @return A new set with all timestamp signing certificates excluded
*/
private Set<CertificateToken> excludeAllTimestampSigningCertificates(
Set<CertificateToken> certificates, List<TimestampToken> timestamps) {
if (certificates == null || certificates.isEmpty()) {
return certificates;
}
if (timestamps == null || timestamps.isEmpty()) {
return certificates;
}
Set<CertificateToken> filteredCerts = new java.util.LinkedHashSet<>(certificates);
int excludedCount = 0;
try {
// Collect all timestamp signing certificates
Set<CertificateToken> tsaCerts = new java.util.HashSet<>();
for (TimestampToken timestamp : timestamps) {
// Get TSA certificate from timestamp's certificate source
// The first certificate in the chain is the signing certificate
if (timestamp.getCertificates() != null && !timestamp.getCertificates().isEmpty()) {
CertificateToken tsaCert = timestamp.getCertificates().get(0);
tsaCerts.add(tsaCert);
}
}
// Remove all TSA certificates from the set
for (CertificateToken tsaCert : tsaCerts) {
if (filteredCerts.remove(tsaCert)) {
excludedCount++;
LOGGER.info("XL-LEVEL: ✂️ Excluded TSA signing certificate: {}",
tsaCert.getSubject().getPrettyPrintRFC2253());
}
}
if (excludedCount > 0) {
LOGGER.info("XL-LEVEL: Excluded {} TSA certificate(s) from CertificateValues", excludedCount);
}
} catch (Exception e) {
// If anything fails, return original set (safe fallback)
LOGGER.error("XL-LEVEL: Failed to exclude TSA certs: {}", e.getMessage());
e.printStackTrace();
return certificates;
}
return filteredCerts;
}
/**
* Excludes the timestamp's signing certificate from the certificate set.
* The TSA's signing certificate is already embedded in the timestamp token,
* so it doesn't need to be duplicated in CertificateValues.
*
* @param certificates The set of certificates to filter
* @param timestampToken The timestamp token containing the signing certificate
* @return A new set with the timestamp signing certificate excluded
*/
private Set<CertificateToken> excludeTimestampSigningCertificate(
Set<CertificateToken> certificates, TimestampToken timestampToken) {
if (certificates == null || certificates.isEmpty()) {
return certificates;
}
if (timestampToken == null) {
return certificates;
}
try {
// Get timestamp's signing certificate (TSA certificate)
// The first certificate in the chain is the signing certificate
CertificateToken timestampSigningCert = null;
if (timestampToken.getCertificates() != null && !timestampToken.getCertificates().isEmpty()) {
timestampSigningCert = timestampToken.getCertificates().get(0);
}
if (timestampSigningCert == null) {
LOGGER.info("TIMESTAMP-VAL-DATA: No signing cert found in timestamp, keeping all certs");
return certificates;
}
// Create new set without the timestamp signing certificate
Set<CertificateToken> filteredCerts = new java.util.LinkedHashSet<>();
for (CertificateToken cert : certificates) {
if (!cert.equals(timestampSigningCert)) {
filteredCerts.add(cert);
} else {
LOGGER.info("TIMESTAMP-VAL-DATA: ✂️ Excluded TSA signing certificate: {}",
cert.getSubject().getPrettyPrintRFC2253());
}
}
return filteredCerts;
} catch (Exception e) {
// If anything fails, return original set (safe fallback)
LOGGER.error("TIMESTAMP-VAL-DATA: Failed to exclude TSA cert: {}", e.getMessage());
e.printStackTrace();
return certificates;
}
}
/**
* Replaces OCSP tokens with cached versions from C-level to ensure digest
* consistency.
* Matches by CERTIFICATE (not digest) since same cert gets different OCSP
* responses over time.
*
* @param newOcspTokens The newly fetched OCSP tokens
* @return A set of OCSP tokens with cached versions where available
*/
private Set<OCSPToken> replaceWithCachedOcspTokens(Set<OCSPToken> newOcspTokens) {
LOGGER.info("=== XL-LEVEL: replaceWithCachedOcspTokens ===");
if (newOcspTokens == null || newOcspTokens.isEmpty()) {
LOGGER.info("XL-LEVEL: No new OCSP tokens to process");
return newOcspTokens;
}
// Get signature-specific cache
java.util.concurrent.ConcurrentHashMap<String, OCSPToken> signatureCache = null;
if (currentSignatureId != null) {
signatureCache = ocspCacheBySignature.get(currentSignatureId);
}
if (signatureCache == null || signatureCache.isEmpty()) {
LOGGER.error("XL-LEVEL: ERROR - No cached OCSP tokens available for signature: {}", currentSignatureId);
LOGGER.error("XL-LEVEL: This will cause 'OCSP not found in references' error!");
return newOcspTokens;
}
LOGGER.info("XL-LEVEL [{}]: Cache has {} OCSP tokens", currentSignatureId, signatureCache.size());
LOGGER.info("XL-LEVEL: Processing {} new OCSP tokens", newOcspTokens.size());
Set<OCSPToken> replacedTokens = new java.util.LinkedHashSet<>();
DigestAlgorithm digestAlgorithm = params.getTokenReferencesDigestAlgorithm();
int replacedCount = 0;
int notFoundCount = 0;
for (OCSPToken newToken : newOcspTokens) {
try {
// Get the certificate that this OCSP is validating
CertificateToken relatedCert = newToken.getRelatedCertificate();
if (relatedCert == null) {
LOGGER.warn("XL-LEVEL: WARNING - OCSP token has no related certificate!");
replacedTokens.add(newToken);
notFoundCount++;
continue;
}
String certKey = Utils.toBase64(relatedCert.getEncoded());
byte[] newOcspDigest = newToken.getDigest(digestAlgorithm);
LOGGER.info("XL-LEVEL [{}]: Processing OCSP for cert {}",
currentSignatureId, certKey.substring(0, Math.min(20, certKey.length())));
LOGGER.info("XL-LEVEL: New OCSP digest: {}",
Utils.toBase64(newOcspDigest).substring(0, 20));
// Check if we have a cached token for this certificate in signature-specific
// cache
OCSPToken cachedToken = signatureCache.get(certKey);
if (cachedToken != null) {
// Use cached token - ensures it has a matching OCSPRef from C-level
byte[] cachedOcspDigest = cachedToken.getDigest(digestAlgorithm);
LOGGER.info("XL-LEVEL: ✅ Found cached OCSP!");
LOGGER.info("XL-LEVEL: Cached OCSP digest: {}",
Utils.toBase64(cachedOcspDigest).substring(0, 20));
LOGGER.info("XL-LEVEL: Using CACHED token (has OCSPRef)");
replacedTokens.add(cachedToken);
replacedCount++;
} else {
// No cached version for this certificate - this will cause validation error!
LOGGER.error("XL-LEVEL: ❌ ERROR - No cached OCSP for this certificate!");
LOGGER.error("XL-LEVEL: This OCSP will have NO OCSPRef!");
LOGGER.error("XL-LEVEL: Validation will fail!");
replacedTokens.add(newToken);
notFoundCount++;
}
} catch (Exception e) {
LOGGER.error("XL-LEVEL: Exception while processing OCSP: {}", e.getMessage());
e.printStackTrace();
replacedTokens.add(newToken);
notFoundCount++;
}
}
LOGGER.info("=== XL-LEVEL SUMMARY ===");
LOGGER.info("Total OCSP tokens: {}", newOcspTokens.size());
LOGGER.info("Replaced with cached: {}", replacedCount);
LOGGER.info("Not found in cache: {}", notFoundCount);
LOGGER.info("========================");
return replacedTokens;
}
}