XAdESSignatureService.java

package io.mersel.dss.signer.api.services.signature.xades;

import java.io.InputStream;
import java.util.Base64;
import java.util.concurrent.Semaphore;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import eu.europa.esig.dss.enumerations.MimeType;
import eu.europa.esig.dss.model.DSSDocument;
import eu.europa.esig.dss.model.InMemoryDocument;
import eu.europa.esig.dss.model.SignatureValue;
import eu.europa.esig.dss.model.ToBeSigned;
import eu.europa.esig.dss.model.x509.CertificateToken;
import eu.europa.esig.dss.spi.validation.CertificateVerifier;
import eu.europa.esig.dss.spi.validation.CommonCertificateVerifier;
import eu.europa.esig.dss.spi.x509.CertificateSource;
import eu.europa.esig.dss.spi.x509.CommonCertificateSource;
import eu.europa.esig.dss.validation.SignedDocumentValidator;
import eu.europa.esig.dss.xades.XAdESSignatureParameters;
import eu.europa.esig.dss.xades.reference.DSSReference;
import eu.europa.esig.dss.xades.signature.XAdESLevelC;
import eu.europa.esig.dss.xades.signature.XAdESService;
import io.mersel.dss.signer.api.exceptions.SignatureException;
import io.mersel.dss.signer.api.models.SignResponse;
import io.mersel.dss.signer.api.models.SigningMaterial;
import io.mersel.dss.signer.api.models.enums.DocumentType;
import io.mersel.dss.signer.api.services.crypto.CryptoSignerService;

/**
 * XAdES imzaları oluşturan servis.
 * UBL, e-Arşiv Raporu, HrXml gibi çeşitli belge tiplerini destekler.
 *
 * <p>
 * Desteklenen belge türleri:
 * <ul>
 * <li>e-Fatura (UBL Invoice)</li>
 * <li>e-Arşiv Raporu (XAdES-A seviyesine otomatik yükseltilir)</li>
 * <li>e-İrsaliye (Waybill)</li>
 * <li>İrsaliye Yanıtı (Waybill Response)</li>
 * <li>Uygulama Yanıtı (Application Response)</li>
 * <li>HrXml (Kullanıcı Açma/Kapama)</li>
 * <li>Diğer XML belgeleri</li>
 * </ul>
 */
@Service
public class XAdESSignatureService {

    private static final Logger LOGGER = LoggerFactory.getLogger(XAdESSignatureService.class);
    private static final String DEFAULT_XML_NAME = "document.xml";
    private static final String ZIP_ENTRY_NAME = "signedcontent.xml";
    private static final String SIGNED_PROPERTIES_TYPE = "http://uri.etsi.org/01903#SignedProperties";

    private final XAdESService xadesService;
    private final XAdESParametersBuilderService parametersBuilder;
    private final XmlProcessingService xmlProcessor;
    private final XAdESDocumentPlacementService documentPlacement;
    private final XAdESLevelUpgradeService levelUpgradeService;
    private final CryptoSignerService cryptoSigner;
    private final CertificateVerifier certificateVerifier;
    private final io.mersel.dss.signer.api.services.util.CompressionService compressionService;
    private final Semaphore semaphore;

    public XAdESSignatureService(XAdESService xadesService,
            XAdESParametersBuilderService parametersBuilder,
            XmlProcessingService xmlProcessor,
            XAdESDocumentPlacementService documentPlacement,
            XAdESLevelUpgradeService levelUpgradeService,
            CryptoSignerService cryptoSigner,
            CertificateVerifier certificateVerifier,
            io.mersel.dss.signer.api.services.util.CompressionService compressionService,
            Semaphore signatureSemaphore) {
        this.xadesService = xadesService;
        this.parametersBuilder = parametersBuilder;
        this.xmlProcessor = xmlProcessor;
        this.documentPlacement = documentPlacement;
        this.levelUpgradeService = levelUpgradeService;
        this.cryptoSigner = cryptoSigner;
        this.certificateVerifier = certificateVerifier;
        this.compressionService = compressionService;
        this.semaphore = signatureSemaphore;
    }

    /**
     * XML belgesini XAdES imzası ile imzalar.
     *
     * @param xmlInputStream XML belgesi içeren input stream
     * @param documentType   Belge tipi (e-Fatura, e-Arşiv vb.)
     * @param signatureId    İsteğe bağlı imza tanımlayıcısı
     * @param zipped         Belgenin ZIP formatında olup olmadığı
     * @param material       İmzalama sertifikası ve private key içeren materyal
     * @return İmzalanmış belge ve imza değeri içeren yanıt
     */
    public SignResponse signXml(InputStream xmlInputStream,
            DocumentType documentType,
            String signatureId,
            boolean zipped,
            SigningMaterial material) {
        try {
            // 1. XML byte'larını çıkar
            byte[] xmlBytes = extractXmlBytes(xmlInputStream, zipped);

            // 2. Belge tipini normalize et
            if (documentType == null || documentType == DocumentType.None) {
                documentType = DocumentType.OtherXmlDocument;
            }

            // 3. Belgeyi parse et
            Document document = xmlProcessor.parseDocument(xmlBytes);

            // 4. UBL belgeleri için UBLExtensions'ı imzadan ÖNCE ekle.
            // İmza, belgenin canonical formu üzerinden hesaplanır. UBLExtensions
            // imza yerleştirme sırasında eklenirse, imzalanan içerik ile nihai
            // belge uyuşmaz ve doğrulama başarısız olur.
            if (documentType == DocumentType.UblDocument) {
                documentPlacement.ensureUblExtensionContentExists(document);
                xmlBytes = xmlProcessor.documentToBytes(document);
            }

            // 5. Parametreleri oluştur
            DSSDocument dssDocument = new InMemoryDocument(xmlBytes, DEFAULT_XML_NAME,
                    MimeType.fromFileExtension("xml"));
            XAdESSignatureParameters parameters = parametersBuilder.buildParameters(
                    document, documentType, signatureId, material);

            // 6. İmzalama sertifika zincirini doğrulayıcıya ekle
            addSigningCertificateChainToVerifier(material);

            // 7. İmzayı oluştur
            SignResponse response = createSignature(document, dssDocument, parameters,
                    documentType, material);

            // 8. Gerekirse ZIP'le
            if (zipped) {
                byte[] zippedBytes = compressionService.zipBytes(ZIP_ENTRY_NAME, response.getSignedDocument());
                return new SignResponse(zippedBytes, response.getSignatureValue());
            }

            LOGGER.info("XAdES imzası başarıyla oluşturuldu. Belge tipi: {}", documentType);

            return response;

        } catch (SignatureException e) {
            throw e;
        } catch (Exception e) {
            LOGGER.error("XAdES imzası oluşturulurken hata", e);
            throw new SignatureException("XAdES imzası oluşturulamadı", e);
        }
    }

    /**
     * İmzalama sürecini orkestre ederek imzayı oluşturur.
     * Semaphore ile eşzamanlı imza sayısını kontrol eder.
     */
    private SignResponse createSignature(Document mainDocument,
            DSSDocument dssDocument,
            XAdESSignatureParameters parameters,
            DocumentType documentType,
            SigningMaterial material) throws Exception {

        // OCSP cache cleanup için signature ID'yi takip et
        String actualSignatureId = null;
        SignatureValue capturedSignatureValue = null;

        semaphore.acquire();
        try {
            // Referanslar için içerik ayarla
            if (parameters.getReferences() != null) {
                for (DSSReference reference : parameters.getReferences()) {
                    if (reference.getContents() == null &&
                            (reference.getType() == null ||
                                    !SIGNED_PROPERTIES_TYPE.equals(reference.getType()))) {
                        reference.setContents(dssDocument);
                    }
                }
            }

            ToBeSigned dataToSign = xadesService.getDataToSign(dssDocument, parameters);

            // Veriyi imzala
            SignatureValue signatureValue = cryptoSigner.sign(
                    dataToSign,
                    material,
                    parameters.getDigestAlgorithm());

            // XML-DSig (XAdES) spec'i ECDSA SignatureValue'nun r||s (plain) formatında
            // olmasını şart koşar; DSS XAdESSignatureBuilder.signDocument() içinde
            // ensurePlainSignatureValue() çağrısı bulunmasına rağmen 6.3 sürümünde
            // bazı senaryolarda DER bytes XML'e olduğu gibi yazılıyor ve sonuçta
            // verifier SIG_CRYPTO_FAILURE üretiyor. Burada DSS'e vermeden önce
            // explicit convert ederek tüm akışlar için garanti altına alıyoruz.
            // RSA için ensurePlainSignatureValue no-op, bu yüzden zararsız.
            signatureValue = ensureXadesSignatureValueFormat(parameters, signatureValue);

            // ÖNEMLİ: capturedSignatureValue'yu DÖNÜŞÜM SONRASI değerle set et.
            // Aksi halde response'taki "x-signature-value" header'ı (ve
            // SignResponse.signatureValue) XML içindeki gerçek <ds:SignatureValue>
            // ile uyuşmaz — ECDSA için header DER, XML plain r||s döner. Tüketici
            // tarafın header'ı XML ile diff'lediği akışlar sessizce kırılır.
            capturedSignatureValue = signatureValue;

            // İmzalı belgeyi oluştur
            DSSDocument signedDocument = xadesService.signDocument(
                    dssDocument, parameters, signatureValue);

            // e-Arşiv Raporu ise XAdES-A seviyesine yükselt
            signedDocument = levelUpgradeService.upgradeIfNeeded(
                    signedDocument, documentType, parameters);

            // Signature ID'yi yakala (cache cleanup için)
            SignedDocumentValidator tempValidator = SignedDocumentValidator.fromDocument(signedDocument);
            if (tempValidator.getSignatures() != null && !tempValidator.getSignatures().isEmpty()) {
                actualSignatureId = tempValidator.getSignatures().get(0).getId();
            }

            // Son işleme: İmzayı doğru konuma yerleştir
            byte[] signedBytes = xmlProcessor.dssDocumentToBytes(signedDocument);
            Document signedDom = xmlProcessor.parseDocument(signedBytes);
            Element signatureElement = xmlProcessor.findSignatureElement(signedDom);

            byte[] finalSignedBytes;
            if (signatureElement != null) {
                documentPlacement.placeSignatureElement(
                        mainDocument, signatureElement, documentType);
                finalSignedBytes = xmlProcessor.documentToBytes(mainDocument);
            } else {
                finalSignedBytes = signedBytes;
            }

            // SignatureValue'yu Base64 string'e çevir
            String encodedSignature = capturedSignatureValue != null
                    ? Base64.getEncoder().encodeToString(capturedSignatureValue.getValue())
                    : null;

            return new SignResponse(finalSignedBytes, encodedSignature);

        } finally {
            semaphore.release();

            // OCSP cache cleanup (memory leak önleme)
            if (actualSignatureId != null) {
                // 1. Bu imzaya özel cache'i temizle (her imza için)
                XAdESLevelC.cleanupOcspCache(actualSignatureId);

                // 2. Eski genel cache'leri temizle (sadece e-Arşiv Raporu/XAdES-A upgrade
                // yapıldıysa)
                // Çünkü XAdES-A upgrade sırasında çok fazla OCSP/CRL cache'i oluşur
                if (documentType == DocumentType.EArchiveReport || documentType == DocumentType.EBiletReport) {
                    XAdESLevelC.cleanupOldCaches(5 * 60 * 1000L);
                }
            }
        }
    }

    /**
     * İmzalama sertifika zincirini doğrulayıcının yardımcı kaynağına ekler.
     * Bu, DSS doğrulayıcısının zinciri çevrimiçi bulabilmesini sağlar.
     */
    private void addSigningCertificateChainToVerifier(SigningMaterial material) {
        if (!(certificateVerifier instanceof CommonCertificateVerifier)) {
            return;
        }

        CommonCertificateVerifier commonVerifier = (CommonCertificateVerifier) certificateVerifier;
        CertificateSource adjunctSource = commonVerifier.getAdjunctCertSources();

        CommonCertificateSource adjunct;
        if (adjunctSource instanceof CommonCertificateSource) {
            adjunct = (CommonCertificateSource) adjunctSource;
        } else {
            adjunct = new CommonCertificateSource();
            commonVerifier.setAdjunctCertSources(adjunct);
        }

        // İmzalama zincirindeki tüm sertifikaları ekle
        for (CertificateToken cert : material.getCertificateTokens()) {
            adjunct.addCertificate(cert);
        }

        LOGGER.debug("Doğrulayıcıya {} adet sertifika eklendi",
                material.getCertificateTokens().size());
    }

    /**
     * XAdES SignatureValue formatını (r||s plain) garantiler. ECDSA için DSS'in
     * {@link eu.europa.esig.dss.spi.DSSASN1Utils#ensurePlainSignatureValue} yardımcısını
     * çağırır; RSA için no-op.
     *
     * <h3>Neden burada?</h3>
     * DSS XAdES Builder iç akışında zaten aynı convert çağrısı vardır; ancak
     * 6.3'te bazı entegrasyon senaryolarında (özellikle 2-aşamalı dış imza akışı
     * ve sonradan DOM manipülasyonu birleştirildiğinde) bu çağrı no-op haline
     * gelebiliyor ve DER bytes XML'e yansıyıp imza verify'ı SIG_CRYPTO_FAILURE
     * dönüyor. Burada explicit garantilemek hem ürünü DSS sürümü davranış
     * varyasyonlarından izole eder hem de XML-DSig spec uyumunu net bir
     * sözleşmeye bağlar.
     */
    private static SignatureValue ensureXadesSignatureValueFormat(
            XAdESSignatureParameters parameters, SignatureValue signatureValue) {
        eu.europa.esig.dss.enumerations.EncryptionAlgorithm enc =
                parameters.getEncryptionAlgorithm();
        if (enc != eu.europa.esig.dss.enumerations.EncryptionAlgorithm.ECDSA
                && enc != eu.europa.esig.dss.enumerations.EncryptionAlgorithm.PLAIN_ECDSA) {
            return signatureValue;
        }
        byte[] raw = signatureValue.getValue();
        if (!eu.europa.esig.dss.spi.DSSASN1Utils.isAsn1EncodedSignatureValue(raw)) {
            return signatureValue;
        }
        byte[] plain = eu.europa.esig.dss.spi.DSSASN1Utils.ensurePlainSignatureValue(enc, raw);
        if (plain == raw || java.util.Arrays.equals(plain, raw)) {
            return signatureValue;
        }
        LOGGER.debug("XAdES ECDSA SignatureValue {} byte DER → {} byte plain (r||s) çevrildi",
                raw.length, plain.length);
        return new SignatureValue(signatureValue.getAlgorithm(), plain);
    }

    /**
     * Input stream'den XML byte'larını çıkarır (ZIP içeriğini de işler).
     */
    private byte[] extractXmlBytes(InputStream inputStream, boolean zipped) {
        if (!zipped) {
            try {
                return org.apache.commons.io.IOUtils.toByteArray(inputStream);
            } catch (Exception e) {
                throw new SignatureException("XML byte'ları okunamadı", e);
            }
        }

        // ZIP dosyası ise CompressionService kullan
        return compressionService.unzipFirstEntry(inputStream);
    }
}