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();
}
}
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.