1

I have created an method that digitally signs an xml (XAdES with Timestamping) using xades4j library https://github.com/luisgoncalves/xades4j

Unfortunately the xml's are quite big sometimes (1,8 GB) and I was wondering if there is a way to do that by streaming the XML instead of creating a DOM and loading the whole document in memory. Is there a way? Can I do that with xades4j?

Below is the current code that signs the document using a DOM representation of the xml. The initial method that is called first is the signXml().

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyStoreException;

import javax.annotation.PostConstruct;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.commons.io.FilenameUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import com.safe.AbstractManager;
import com.safe.model.DirectPasswordProvider;

import xades4j.algorithms.ExclusiveCanonicalXMLWithoutComments;
import xades4j.production.Enveloped;
import xades4j.production.SignatureAlgorithms;
import xades4j.production.XadesSigner;
import xades4j.production.XadesTSigningProfile;
import xades4j.providers.KeyingDataProvider;
import xades4j.providers.impl.FileSystemKeyStoreKeyingDataProvider;
import xades4j.providers.impl.HttpTsaConfiguration;
import xades4j.providers.impl.KeyStoreKeyingDataProvider;
import xades4j.utils.DOMHelper;

@Component
public class FileOperationsManager extends AbstractManager {

    @Value("${certificates.digital-signature.filepath}")
    private String certPath;

    @Value("${certificates.digital-signature.password}")
    private String certPass;

    @Value("${certificates.digital-signature.type}")
    private String certType;

    private DocumentBuilder db;

    private TransformerFactory tf;

    @PostConstruct
    public void init() throws Exception {
        final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware(true);
        this.db = dbf.newDocumentBuilder();
        this.tf = TransformerFactory.newInstance();
    }

    public Path signXml(final Path xmlFile, final Path targetDir) {

        final String baseName = FilenameUtils.getBaseName(xmlFile.getFileName().toString())
            .concat("_Signed")
            .concat(".")
            .concat(FilenameUtils.getExtension(xmlFile.getFileName().toString()));
        final Path target = Paths.get(targetDir.toString(), baseName);

        try (final FileInputStream fis = new FileInputStream(String.valueOf(xmlFile))) {

            final Document doc = this.parseDocument(fis);
            final Element elementToSign = doc.getDocumentElement();

            final SignatureAlgorithms algorithms = new SignatureAlgorithms()
                .withCanonicalizationAlgorithmForTimeStampProperties(new ExclusiveCanonicalXMLWithoutComments("ds", "xades"))
                .withCanonicalizationAlgorithmForSignature(new ExclusiveCanonicalXMLWithoutComments());

            final KeyingDataProvider kdp = this.createFileSystemKeyingDataProvider(certType, certPath, certPass, true);

            final XadesSigner signer = new XadesTSigningProfile(kdp)
                .withSignatureAlgorithms(algorithms)
                .with(new HttpTsaConfiguration("http://timestamp.digicert.com"))
                .newSigner();

            new Enveloped(signer).sign(elementToSign);

            this.exportDocument(doc, target);

        } catch (final FileNotFoundException e) {
            throw new RuntimeException();
        } catch (final Exception e) {
            throw new RuntimeException();
        }

        return target;
    }

    private FileSystemKeyStoreKeyingDataProvider createFileSystemKeyingDataProvider(
        final String keyStoreType,
        final String keyStorePath,
        final String keyStorePwd,
        final boolean returnFullChain) throws KeyStoreException {

        return FileSystemKeyStoreKeyingDataProvider
            .builder(keyStoreType, keyStorePath, KeyStoreKeyingDataProvider.SigningCertificateSelector.single())
            .storePassword(new DirectPasswordProvider(keyStorePwd))
            .entryPassword(new DirectPasswordProvider(keyStorePwd))
            .fullChain(returnFullChain)
            .build();
    }

    public Document parseDocument(final InputStream is) {
        try {
            final Document doc = this.db.parse(is);
            final Element elem = doc.getDocumentElement();
            DOMHelper.useIdAsXmlId(elem);
            return doc;
        } catch (final Exception e) {
            throw new RuntimeException();
        }
    }

    public void exportDocument(final Document doc, final Path target) {
        try (final FileOutputStream out = new FileOutputStream(target.toFile())) {
            this.tf.newTransformer().transform(
                new DOMSource(doc),
                new StreamResult(out));
        } catch (final Exception e) {
            throw new RuntimeException();
        }
    }
Mark
  • 11
  • 3

1 Answers1

0

Unfortunately xades4j doesn't support streaming on the XML document to which the signature will be appended. I don't know if there are other alternative libraries that do.

A possible workaround using xades4j is to use a detached signature instead of an enveloped signature. The signature can be added to an empty XML document and the large XML file is explicitly added as a Reference to that signature.

xades4j delegates the core XML-DSIG handling to Apache Santuario, so if Santuario uses streaming for Reference resolution, this should avoid your issue. I'm not sure, though, but it may be worth testing.

https://github.com/luisgoncalves/xades4j/wiki/DefiningSignedResources

You may need to use a file URI and/or base URIs.

lgoncalves
  • 2,040
  • 1
  • 14
  • 12
  • Thank you very much for your reply! This solution might help a bit but I'm afraid it still requires to load the initial file in order to produce the signature, even though it won't attach it under it, so again I must load the whole file in memory, but thank you for your proposal! – Mark Jun 06 '22 at 11:47
  • Did you test it? The signature is calculated over an hash of the references, so if the reference hash itself was calculated using streaming, then the overall signature operation shouldn't load the whole file. I'm assuming Santuario would be able to generate the reference hash in a stream-ish way.. – lgoncalves Jun 06 '22 at 15:07