WsSecuritySignatureService.java

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

import eu.europa.esig.dss.enumerations.DigestAlgorithm;
import eu.europa.esig.dss.enumerations.EncryptionAlgorithm;
import eu.europa.esig.dss.enumerations.SignatureAlgorithm;
import io.mersel.dss.signer.api.constants.XmlConstants;
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.services.crypto.DigestAlgorithmResolverService;
import io.mersel.dss.signer.api.services.keystore.iaik.Pkcs11EcdsaSignatureEncoder;
import io.mersel.dss.signer.api.services.keystore.iaik.Pkcs11Signer;
import io.mersel.dss.signer.api.util.xml.SecureXmlFactories;
import org.apache.xml.security.Init;
import org.apache.xml.security.c14n.Canonicalizer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayOutputStream;
import java.security.MessageDigest;
import java.security.cert.X509Certificate;
import java.security.interfaces.DSAPublicKey;
import java.security.interfaces.ECPublicKey;
import java.util.Base64;
import java.util.concurrent.Semaphore;

/**
 * SOAP mesajları için WS-Security XML imzaları oluşturan servis.
 * SOAP 1.1 ve SOAP 1.2'yi destekler; <b>hem PFX hem HSM (PKCS#11)</b>
 * imzalama yolunu aynı kod akışıyla çözer.
 *
 * <h2>Tasarım</h2>
 * <p>Bu servis JCA {@link javax.xml.crypto.dsig.XMLSignatureFactory}
 * kullanmaz çünkü o API yalnızca {@link java.security.PrivateKey} ile imza
 * atabilir — HSM yolunda elimizde sadece bir <em>key handle</em> var,
 * JCA-uyumlu PrivateKey değil. Bunun yerine:</p>
 *
 * <ol>
 *   <li><b>SignedInfo</b> + <b>Reference</b> elementlerini doğrudan DOM ile
 *       inşa ederiz.</li>
 *   <li>Her referans için hedef elementi <b>Apache Santuario</b>'nun
 *       {@link Canonicalizer}'ı ile EXC-C14N'leyip {@link MessageDigest}
 *       ile hash'leriz.</li>
 *   <li>SignedInfo'yu yine EXC-C14N'leyip {@link CryptoSigner} aracılığıyla
 *       imzalarız — PFX yolunda JCA, HSM yolunda
 *       {@link Pkcs11Signer#sign(byte[], SignatureAlgorithm)}.</li>
 *   <li>ECDSA imzaları XMLDsig spec'inde (RFC 4051 §3.4.1)
 *       <b>raw {@code r||s}</b> formatında olmalıdır. CMS/CAdES'in
 *       beklediği DER SEQUENCE'tan farklı; bu yüzden imzayı
 *       {@link Pkcs11EcdsaSignatureEncoder#derToRaw} ile dönüştürürüz.</li>
 * </ol>
 *
 * <h2>Thread-safety</h2>
 * <p>{@code signatureSemaphore} aynı anda imza atan thread sayısını sınırlar
 * (HSM session pool boyutuyla eşleşmeli). Servis state'siz; her çağrı izole.</p>
 */
@Service
public class WsSecuritySignatureService {

    private static final Logger LOGGER = LoggerFactory.getLogger(WsSecuritySignatureService.class);

    private static final String BODY_ID = "SignedSoapBodyContent";
    private static final String TS_ID = "SignedSoapTimestampContent";
    private static final String NS_WSSE = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd";
    private static final String NS_DSIG = "http://www.w3.org/2000/09/xmldsig#";
    private static final String NS_DSIG_MORE = "http://www.w3.org/2001/04/xmldsig-more#";
    private static final String NS_XMLENC = "http://www.w3.org/2001/04/xmlenc#";
    private static final String C14N_EXCL = Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS;

    static {
        // Apache Santuario'nun bir kez initialize edilmesi gerekiyor (Canonicalizer
        // ve diğer algoritmaların registry'sini kurar). Idempotent.
        if (!Init.isInitialized()) {
            Init.init();
        }
    }

    private final Semaphore semaphore;
    private final DigestAlgorithmResolverService digestAlgorithmResolver;

    public WsSecuritySignatureService(Semaphore signatureSemaphore,
                                      DigestAlgorithmResolverService digestAlgorithmResolver) {
        this.semaphore = signatureSemaphore;
        this.digestAlgorithmResolver = digestAlgorithmResolver;
    }

    /**
     * SOAP zarfını WS-Security imzası ile imzalar. PFX ve HSM yapılandırmaları
     * her ikisi de desteklenir.
     */
    public SignResponse signSoapEnvelope(Document soapDocument,
                                        boolean useSoap12,
                                        SigningMaterial material,
                                        String alias,
                                        char[] pin) {
        try {
            String soapNamespace = useSoap12
                ? XmlConstants.NS_SOAP_1_DOT_2_ENVELOPE
                : XmlConstants.NS_SOAP_ENVELOPE;

            Element soapHeaderElement = ensureSoapHeader(soapDocument, soapNamespace);
            Element securityElement = createSecurityHeader(soapDocument, soapHeaderElement);

            addTimestamp(soapDocument, securityElement);

            Element bodyElement = (Element) soapDocument
                .getElementsByTagNameNS(soapNamespace, "Body").item(0);
            if (bodyElement != null) {
                bodyElement.setAttribute("Id", BODY_ID);
                bodyElement.setIdAttribute("Id", true);
                bodyElement.removeAttribute("wsu:Id");
                bodyElement.removeAttribute("xmlns:xsi");
                bodyElement.removeAttribute("xmlns:xsd");
            }

            String bstReference = addBinarySecurityToken(
                soapDocument, soapNamespace, material);

            Element testTimestamp = findElementById(soapDocument, TS_ID);
            Element testBody = findElementById(soapDocument, BODY_ID);
            LOGGER.debug("Pre-signature validation - Timestamp found: {}, Body found: {}",
                testTimestamp != null, testBody != null);
            if (testTimestamp == null) {
                LOGGER.error("{} elementi bulunamadı!", TS_ID);
            }
            if (testBody == null) {
                LOGGER.error("{} elementi bulunamadı!", BODY_ID);
            }

            signDocument(soapDocument, securityElement, material, bstReference);

            byte[] signedBytes = documentToBytes(soapDocument);

            LOGGER.info("WS-Security imzası başarıyla oluşturuldu (SOAP {}, backend={})",
                useSoap12 ? "1.2" : "1.1",
                material.isPkcs11() ? "HSM/PKCS#11" : "PFX/JCA");
            return new SignResponse(signedBytes, null);

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

    /**
     * SOAP Header'ın doğru namespace ile var olduğundan emin olur. Yoksa oluşturur.
     */
    private Element ensureSoapHeader(Document document, String soapNamespace) {
        Element envelopeElement = document.getDocumentElement();
        Element headerElement = (Element) document
            .getElementsByTagNameNS(soapNamespace, "Header").item(0);

        if (headerElement == null) {
            LOGGER.debug("SOAP Header bulunamadı, oluşturuluyor (namespace: {})", soapNamespace);
            String prefix = soapNamespace.equals(XmlConstants.NS_SOAP_1_DOT_2_ENVELOPE) ? "env" : "soapenv";
            headerElement = document.createElementNS(soapNamespace, prefix + ":Header");

            Element bodyElement = (Element) document
                .getElementsByTagNameNS(soapNamespace, "Body").item(0);
            if (bodyElement != null) {
                envelopeElement.insertBefore(headerElement, bodyElement);
            } else {
                envelopeElement.appendChild(headerElement);
            }
        }

        return headerElement;
    }

    private Element createSecurityHeader(Document document, Element soapHeaderElement) {
        Element securityElement = (Element) soapHeaderElement
            .getElementsByTagNameNS(XmlConstants.NS_WSSE, "Security").item(0);

        if (securityElement == null) {
            securityElement = document.createElementNS(XmlConstants.NS_WSSE, "wsse:Security");
            securityElement.setAttributeNS(
                "http://www.w3.org/2000/xmlns/",
                "xmlns:wsse",
                XmlConstants.NS_WSSE);
            securityElement.setAttributeNS(
                "http://www.w3.org/2000/xmlns/",
                "xmlns:wsu",
                XmlConstants.NS_WSU);

            if (soapHeaderElement.hasChildNodes()) {
                soapHeaderElement.insertBefore(securityElement, soapHeaderElement.getFirstChild());
            } else {
                soapHeaderElement.appendChild(securityElement);
            }

            LOGGER.debug("Security header oluşturuldu");
        }

        return securityElement;
    }

    private void addTimestamp(Document document, Element securityElement) {
        Element timestampElement = document.createElementNS(XmlConstants.NS_WSU, "wsu:Timestamp");
        timestampElement.setAttribute("Id", TS_ID);
        timestampElement.setIdAttribute("Id", true);

        Element createdElement = document.createElementNS(XmlConstants.NS_WSU, "wsu:Created");
        java.time.Instant now = java.time.Instant.now();
        createdElement.setTextContent(now.toString());
        timestampElement.appendChild(createdElement);

        Element expiresElement = document.createElementNS(XmlConstants.NS_WSU, "wsu:Expires");
        java.time.Instant expires = now.plusSeconds(30);
        expiresElement.setTextContent(expires.toString());
        timestampElement.appendChild(expiresElement);

        if (securityElement.hasChildNodes()) {
            securityElement.insertBefore(timestampElement, securityElement.getFirstChild());
        } else {
            securityElement.appendChild(timestampElement);
        }
    }

    private String addBinarySecurityToken(Document document,
                                         String soapNamespace,
                                         SigningMaterial material) throws Exception {
        String bstReference = "X509-" + material.getSigningCertificate().getSerialNumber();

        Element headerElement = (Element) document
            .getElementsByTagNameNS(soapNamespace, "Header").item(0);
        if (headerElement == null) {
            throw new SignatureException("SOAP Header bulunamadı");
        }

        Element securityElement = (Element) headerElement
            .getElementsByTagNameNS(XmlConstants.NS_WSSE, "Security").item(0);
        if (securityElement == null) {
            securityElement = document.createElementNS(XmlConstants.NS_WSSE, "wsse:Security");
            headerElement.appendChild(securityElement);
        }

        Element binarySecurityToken = document.createElementNS(
            XmlConstants.NS_WSSE, "wsse:BinarySecurityToken");
        binarySecurityToken.setAttribute("EncodingType", XmlConstants.ATTR_EncodingType);
        binarySecurityToken.setAttribute("ValueType", XmlConstants.ATTR_ValueType);
        binarySecurityToken.setAttributeNS(XmlConstants.NS_WSU, "wsu:Id", bstReference);
        binarySecurityToken.setIdAttributeNS(XmlConstants.NS_WSU, "Id", true);
        binarySecurityToken.setTextContent(
            Base64.getEncoder().encodeToString(
                material.getSigningCertificate().getEncoded()));

        securityElement.insertBefore(binarySecurityToken, securityElement.getFirstChild());

        return bstReference;
    }

    /**
     * Manuel XMLDsig akışı: SignedInfo + Reference oluştur, digest'leri hesapla,
     * SignedInfo'yu canonicalize edip imzala, sonucu DOM'a inject et.
     *
     * <p>Bu yol PFX (JCA) ve HSM (PKCS#11) için tamamen ortaktır; tek fark
     * {@link #signRaw} içindeki backend seçimi.</p>
     */
    private void signDocument(Document document,
                             Element securityElement,
                             SigningMaterial material,
                             String bstReference) throws Exception {
        semaphore.acquire();
        try {
            X509Certificate cert = material.getSigningCertificate();
            DigestAlgorithm digestAlg = digestAlgorithmResolver.resolveDigestAlgorithm(cert);
            EncryptionAlgorithm encAlg = EncryptionAlgorithm.forKey(cert.getPublicKey());
            SignatureAlgorithm sigAlg = SignatureAlgorithm.getAlgorithm(encAlg, digestAlg);
            if (sigAlg == null) {
                throw new SignatureException(
                    "Desteklenmeyen kombinasyon: enc=" + encAlg + ", digest=" + digestAlg);
            }
            String signatureMethodUri = signatureMethodUri(sigAlg);
            String digestMethodUri = digestMethodUri(digestAlg);

            // 1) <ds:Signature> skeleton'u
            Element signatureElem = document.createElementNS(NS_DSIG, "ds:Signature");
            signatureElem.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:ds", NS_DSIG);

            Element signedInfo = document.createElementNS(NS_DSIG, "ds:SignedInfo");
            Element cm = document.createElementNS(NS_DSIG, "ds:CanonicalizationMethod");
            cm.setAttribute("Algorithm", C14N_EXCL);
            signedInfo.appendChild(cm);

            Element sm = document.createElementNS(NS_DSIG, "ds:SignatureMethod");
            sm.setAttribute("Algorithm", signatureMethodUri);
            signedInfo.appendChild(sm);

            // Referanslar: Timestamp + Body
            signedInfo.appendChild(buildReference(document, TS_ID, digestMethodUri));
            signedInfo.appendChild(buildReference(document, BODY_ID, digestMethodUri));

            signatureElem.appendChild(signedInfo);

            Element sigValueElem = document.createElementNS(NS_DSIG, "ds:SignatureValue");
            signatureElem.appendChild(sigValueElem);

            // KeyInfo → SecurityTokenReference → BST reference
            signatureElem.appendChild(buildKeyInfo(document, bstReference));

            // Security header'ın altına Signature ekle.
            securityElement.appendChild(signatureElem);

            // 2) Referansların DigestValue'larını hesapla.
            NodeList refs = signedInfo.getElementsByTagNameNS(NS_DSIG, "Reference");
            for (int i = 0; i < refs.getLength(); i++) {
                Element refElem = (Element) refs.item(i);
                String uri = refElem.getAttribute("URI");
                String id = uri.startsWith("#") ? uri.substring(1) : uri;
                Element target = findElementById(document, id);
                if (target == null) {
                    throw new SignatureException("Reference URI=" + uri + " hedefi DOM'da bulunamadı");
                }
                byte[] c14nBytes = canonicalize(target);
                byte[] digest = MessageDigest.getInstance(digestAlg.getJavaName()).digest(c14nBytes);
                Element digestValueElem = (Element) refElem
                    .getElementsByTagNameNS(NS_DSIG, "DigestValue").item(0);
                digestValueElem.setTextContent(Base64.getEncoder().encodeToString(digest));
            }

            // 3) SignedInfo'yu canonicalize edip imzala.
            byte[] signedInfoBytes = canonicalize(signedInfo);
            byte[] signatureBytes = signRaw(signedInfoBytes, material, sigAlg);

            // 4) ECDSA/DSA imzaları XMLDsig spec'ine göre raw r||s'a indirgenir
            //    (RFC 4051 §3.4.1, DSA için xmldsig dsa-sha1/256).
            //    PFX yolunda JCA Signature.sign() DER üretir;
            //    HSM yolunda IaikPkcs11Module DER'e normalize ediyor — her iki
            //    durumda da DER'den raw'a çevirmek gerek. DSA için aynı encoder
            //    çalışır çünkü hem ECDSA hem DSA imzası ASN.1
            //    {@code SEQUENCE { INTEGER r, INTEGER s }} formatındadır;
            //    aradaki tek fark "field size" — ECDSA için curve P-XXX bit,
            //    DSA için subprime Q'nun bit uzunluğu.
            if (isEcdsa(encAlg)) {
                int fieldSize = ecFieldSizeBytes(cert);
                signatureBytes = Pkcs11EcdsaSignatureEncoder.derToRaw(signatureBytes, fieldSize);
            } else if (encAlg == EncryptionAlgorithm.DSA) {
                int fieldSize = dsaFieldSizeBytes(cert);
                signatureBytes = Pkcs11EcdsaSignatureEncoder.derToRaw(signatureBytes, fieldSize);
            }

            sigValueElem.setTextContent(Base64.getEncoder().encodeToString(signatureBytes));
        } finally {
            semaphore.release();
        }
    }

    private Element buildReference(Document document, String elementId, String digestMethodUri) {
        Element ref = document.createElementNS(NS_DSIG, "ds:Reference");
        ref.setAttribute("URI", "#" + elementId);

        Element transforms = document.createElementNS(NS_DSIG, "ds:Transforms");
        Element t = document.createElementNS(NS_DSIG, "ds:Transform");
        t.setAttribute("Algorithm", C14N_EXCL);
        transforms.appendChild(t);
        ref.appendChild(transforms);

        Element dm = document.createElementNS(NS_DSIG, "ds:DigestMethod");
        dm.setAttribute("Algorithm", digestMethodUri);
        ref.appendChild(dm);

        // DigestValue içeriği signDocument içinde set edilir.
        Element dv = document.createElementNS(NS_DSIG, "ds:DigestValue");
        ref.appendChild(dv);

        return ref;
    }

    private Element buildKeyInfo(Document document, String bstReference) {
        Element keyInfo = document.createElementNS(NS_DSIG, "ds:KeyInfo");
        Element str = document.createElementNS(NS_WSSE, "wsse:SecurityTokenReference");
        Element ref = document.createElementNS(NS_WSSE, "wsse:Reference");
        ref.setAttribute("URI", "#" + bstReference);
        ref.setAttribute(
            "ValueType",
            "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3");
        str.appendChild(ref);
        keyInfo.appendChild(str);
        return keyInfo;
    }

    /**
     * Backend-agnostic imzalama: PFX yolunda JCA, HSM yolunda PKCS#11.
     * Çıktı ECDSA için <b>DER SEQUENCE</b>, RSA için RSASSA-PKCS1-v1_5 encoded
     * (her ikisi de {@link SigningMaterial} kontratının standart çıktısı).
     */
    private byte[] signRaw(byte[] data, SigningMaterial material, SignatureAlgorithm sigAlg) throws Exception {
        return material.sign(data, sigAlg);
    }

    private byte[] canonicalize(Element element) throws Exception {
        Canonicalizer canonicalizer = Canonicalizer.getInstance(C14N_EXCL);
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        canonicalizer.canonicalizeSubtree(element, out);
        return out.toByteArray();
    }

    private static boolean isEcdsa(EncryptionAlgorithm enc) {
        return enc == EncryptionAlgorithm.ECDSA || enc == EncryptionAlgorithm.PLAIN_ECDSA;
    }

    /**
     * EC eğri parametre boyutunu (P-256/384/521) byte cinsinden döndürür.
     * XMLDsig {@code SignatureValue} = {@code padZ(r, n) || padZ(s, n)} olur.
     */
    private static int ecFieldSizeBytes(X509Certificate cert) {
        if (!(cert.getPublicKey() instanceof ECPublicKey)) {
            throw new SignatureException(
                "ECDSA imzalama isteniyor ama sertifika public key EC değil: "
                + cert.getPublicKey().getAlgorithm());
        }
        ECPublicKey ec = (ECPublicKey) cert.getPublicKey();
        int bits = ec.getParams().getCurve().getField().getFieldSize();
        return (bits + 7) / 8;
    }

    /**
     * DSA subprime Q'nun bit uzunluğunu byte cinsinden döndürür. XMLDsig
     * {@code SignatureValue} = {@code padZ(r, |q|) || padZ(s, |q|)}.
     *
     * <p><b>Pratik notu:</b> Türkiye Mali Mühür sertifikaları DSA değildir
     * (RSA veya ECDSA). DSA desteği tamlık ve XMLDsig spec uyumu için
     * eklenmiştir; üretim ortamında DSA imzalama doğrulanmamıştır. Eğer
     * gerçekten kullanılacaksa entegrasyon testi yazılması gerekir.</p>
     */
    private static int dsaFieldSizeBytes(X509Certificate cert) {
        if (!(cert.getPublicKey() instanceof DSAPublicKey)) {
            throw new SignatureException(
                "DSA imzalama isteniyor ama sertifika public key DSA değil: "
                + cert.getPublicKey().getAlgorithm());
        }
        DSAPublicKey dsa = (DSAPublicKey) cert.getPublicKey();
        // DSA L (P bit length) ile N (Q bit length) farklı parametrelerdir;
        // imza komponentleri (r, s) ∈ [1, q-1] olduğu için N'i kullanırız.
        // Tipik: DSA-1024 → N=160 (20 byte), DSA-2048/3072 → N=256 (32 byte).
        int qBits = dsa.getParams().getQ().bitLength();
        return (qBits + 7) / 8;
    }

    private static String digestMethodUri(DigestAlgorithm digest) {
        switch (digest) {
            case SHA1:   return NS_DSIG + "sha1";
            case SHA224: return NS_DSIG_MORE + "sha224";
            case SHA256: return NS_XMLENC + "sha256";
            case SHA384: return NS_DSIG_MORE + "sha384";
            case SHA512: return NS_XMLENC + "sha512";
            default:
                throw new SignatureException("XMLDsig için desteklenmeyen digest: " + digest);
        }
    }

    /**
     * XMLDsig / xmldsig-more URI'lerine map eder. RSA-PSS şu an WS-Security
     * tarafında kapsam dışı (mvcd RFC 6931 var ama kullanım çok dar).
     */
    private static String signatureMethodUri(SignatureAlgorithm sigAlg) {
        EncryptionAlgorithm enc = sigAlg.getEncryptionAlgorithm();
        DigestAlgorithm digest = sigAlg.getDigestAlgorithm();
        if (enc == EncryptionAlgorithm.ECDSA || enc == EncryptionAlgorithm.PLAIN_ECDSA) {
            switch (digest) {
                case SHA1:   return NS_DSIG_MORE + "ecdsa-sha1";
                case SHA224: return NS_DSIG_MORE + "ecdsa-sha224";
                case SHA256: return NS_DSIG_MORE + "ecdsa-sha256";
                case SHA384: return NS_DSIG_MORE + "ecdsa-sha384";
                case SHA512: return NS_DSIG_MORE + "ecdsa-sha512";
                default:
                    throw new SignatureException("ECDSA için desteklenmeyen digest: " + digest);
            }
        }
        if (enc == EncryptionAlgorithm.DSA) {
            // DSA Türkiye e-imzasında pratik olarak ölü, ama legacy desteği:
            return digest == DigestAlgorithm.SHA1
                ? NS_DSIG + "dsa-sha1"
                : NS_DSIG_MORE + "dsa-sha256";
        }
        // RSA (PKCS#1 v1.5)
        switch (digest) {
            case SHA1:   return NS_DSIG + "rsa-sha1";
            case SHA224: return NS_DSIG_MORE + "rsa-sha224";
            case SHA256: return NS_DSIG_MORE + "rsa-sha256";
            case SHA384: return NS_DSIG_MORE + "rsa-sha384";
            case SHA512: return NS_DSIG_MORE + "rsa-sha512";
            default:
                throw new SignatureException("RSA için desteklenmeyen digest: " + digest);
        }
    }

    /**
     * Tüm dokümanda recursive olarak verilen ID değerini taşıyan elementi arar.
     * "Id" (WS-Security tarzı) ve "wsu:Id" attribute'larını kontrol eder.
     */
    private Element findElementById(Document document, String id) {
        return findElementByIdRecursive(document.getDocumentElement(), id);
    }

    private Element findElementByIdRecursive(Element element, String id) {
        if (id == null || id.isEmpty()) {
            return null;
        }
        String elementId = element.getAttribute("Id");
        if (elementId != null && !elementId.isEmpty() && id.equals(elementId)) {
            return element;
        }
        String wsuId = element.getAttributeNS(XmlConstants.NS_WSU, "Id");
        if (wsuId != null && !wsuId.isEmpty() && id.equals(wsuId)) {
            return element;
        }
        org.w3c.dom.NodeList children = element.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            org.w3c.dom.Node node = children.item(i);
            if (node instanceof Element) {
                Element found = findElementByIdRecursive((Element) node, id);
                if (found != null) {
                    return found;
                }
            }
        }
        return null;
    }

    private byte[] documentToBytes(Document document) throws Exception {
        TransformerFactory transformerFactory = SecureXmlFactories.newTransformerFactory();
        Transformer transformer = transformerFactory.newTransformer();
        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");

        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        transformer.transform(new DOMSource(document), new StreamResult(outputStream));
        return outputStream.toByteArray();
    }
}