Skip to content

Commit

Permalink
Merge pull request #303 from luisgoncalves/iss259-custom-element-ids
Browse files Browse the repository at this point in the history
Support custom XML element IDs
  • Loading branch information
luisgoncalves authored Sep 12, 2024
2 parents 5168f6d + 98c56f4 commit 5278408
Show file tree
Hide file tree
Showing 14 changed files with 810 additions and 621 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* XAdES4j - A Java library for generation and verification of XAdES signatures.
* Copyright (C) 2024 Luis Goncalves.
*
* XAdES4j 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 3 of the License, or any later version.
*
* XAdES4j 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.
*
* You should have received a copy of the GNU Lesser General Public License along
* with XAdES4j. If not, see <http://www.gnu.org/licenses/>.
*/

package xades4j.production;

import java.util.UUID;

final class DefaultElementIdGeneratorFactory implements ElementIdGeneratorFactory
{
@Override
public ElementIdGenerator create()
{
return (namespace, name) -> {
return name.toLowerCase() + "-" + UUID.randomUUID();
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ protected void configure()
bind(HttpTsaConfiguration.class).toProvider(() -> {
throw new IllegalStateException("HttpTsaConfiguration must be configured in the profile in order to use an HTTP-based time-stamp token provider.");
});
bind(ElementIdGeneratorFactory.class).to(DefaultElementIdGeneratorFactory.class);

// PropertiesDataObjectsGenerator is not configurable but the individual
// generators may have dependencies.
Expand Down
57 changes: 57 additions & 0 deletions src/main/java/xades4j/production/ElementIdGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* XAdES4j - A Java library for generation and verification of XAdES signatures.
* Copyright (C) 2024 Luis Goncalves.
*
* XAdES4j 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 3 of the License, or any later version.
*
* XAdES4j 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.
*
* You should have received a copy of the GNU Lesser General Public License along
* with XAdES4j. If not, see <http://www.gnu.org/licenses/>.
*/

package xades4j.production;

import java.util.UUID;

/**
* Generates IDs for XML elements in a given signing operation.
*/
public interface ElementIdGenerator
{
/**
* Generate an ID for an XML element.
*
* @param namespace the element namespace
* @param name the element name
* @return the ID
*/
String generateId(String namespace, String name);

/**
* Gets a {@link ElementIdGenerator} that uses a UUID for each requested ID.
*/
static ElementIdGenerator uuid()
{
return uuid(null, null);
}

/**
* Gets a {@link ElementIdGenerator} that uses a UUID for each requested ID, optionally using a constant prefix
* and/or suffix.
*
* @param prefix the ID prefix (may be null)
* @param suffix the ID suffix (may be null)
*/
static ElementIdGenerator uuid(String prefix, String suffix)
{
final String p = prefix == null ? "" : prefix;
final String s = suffix == null ? "" : suffix;
return (ns, n) -> p + UUID.randomUUID() + s;
}
}
55 changes: 55 additions & 0 deletions src/main/java/xades4j/production/ElementIdGeneratorFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* XAdES4j - A Java library for generation and verification of XAdES signatures.
* Copyright (C) 2024 Luis Goncalves.
*
* XAdES4j 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 3 of the License, or any later version.
*
* XAdES4j 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.
*
* You should have received a copy of the GNU Lesser General Public License along
* with XAdES4j. If not, see <http://www.gnu.org/licenses/>.
*/

package xades4j.production;

import java.util.UUID;

/**
* A factory of {@link ElementIdGenerator}.
*/
public interface ElementIdGeneratorFactory
{
/**
* Create a new {@link ElementIdGenerator}. This method is invoked once for each signing operation and the returned
* instance is used to obtain element IDs during that operation. This allows for scenarios where all the element IDs
* share a common base.
*
* @return the ID generator
*/
ElementIdGenerator create();

/**
* Gets a {@link ElementIdGeneratorFactory} that uses a UUID for each requested ID.
*/
static ElementIdGeneratorFactory uuid()
{
return ElementIdGenerator::uuid;
}

/**
* Gets a {@link ElementIdGeneratorFactory} that uses a UUID for each requested ID, optionally using a constant
* prefix and/or suffix.
*
* @param prefix the ID prefix (may be null)
* @param suffix the ID suffix (may be null)
*/
static ElementIdGeneratorFactory uuid(String prefix, String suffix)
{
return () -> ElementIdGenerator.uuid(prefix, suffix);
}
}
11 changes: 7 additions & 4 deletions src/main/java/xades4j/production/KeyInfoBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
import xades4j.utils.TransformUtils;
import xades4j.xml.marshalling.algorithms.AlgorithmsParametersMarshallingProvider;

import static xades4j.production.SignerBES.idFor;

/**
* Helper class that creates the {@code ds:KeyInfo} element accordingly to some
* signature options. The signing certificate validity and key usages are
Expand Down Expand Up @@ -60,15 +62,16 @@ class KeyInfoBuilder

void buildKeyInfo(
List<X509Certificate> signingCertificateChain,
XMLSignature xmlSig) throws KeyingDataException, UnsupportedAlgorithmException
XMLSignature xmlSig,
ElementIdGenerator idGenerator) throws KeyingDataException, UnsupportedAlgorithmException
{
X509Certificate signingCertificate = getSigningCertificate(signingCertificateChain);

addSigningCertificateElements(signingCertificateChain, signingCertificate, xmlSig);

addPublicKey(signingCertificate, xmlSig);

addKeyInfoReference(xmlSig);
addKeyInfoReference(xmlSig, idGenerator);
}

private void addSigningCertificateElements(List<X509Certificate> signingCertificateChain, X509Certificate signingCertificate, XMLSignature xmlSig) throws KeyingDataException
Expand Down Expand Up @@ -119,13 +122,13 @@ private void addPublicKey(X509Certificate signingCertificate, XMLSignature xmlSi
}
}

private void addKeyInfoReference(XMLSignature xmlSig) throws UnsupportedAlgorithmException
private void addKeyInfoReference(XMLSignature xmlSig, ElementIdGenerator idGenerator) throws UnsupportedAlgorithmException
{
if (this.basicSignatureOptions.signKeyInfo())
{
try
{
String keyInfoId = xmlSig.getId() + "-keyinfo";
String keyInfoId = idFor(xmlSig.getKeyInfo(), idGenerator);
xmlSig.getKeyInfo().setId(keyInfoId);

// Use same canonicalization URI as specified in the ds:CanonicalizationMethod for Signature.
Expand Down
25 changes: 15 additions & 10 deletions src/main/java/xades4j/production/SignedDataObjectsProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
import java.util.Map;
import java.util.Set;

import static xades4j.production.SignerBES.idFor;

/**
* Helper class that processes a set of data object descriptions.
*
Expand Down Expand Up @@ -80,7 +82,8 @@ public Result(Map<DataObjectDesc, Reference> referenceMappings, Set<Manifest> ma
*/
SignedDataObjectsProcessor.Result process(
SignedDataObjects signedDataObjects,
XMLSignature xmlSignature) throws UnsupportedAlgorithmException {
XMLSignature xmlSignature,
ElementIdGenerator idGenerator) throws UnsupportedAlgorithmException {
if (xmlSignature.getSignedInfo().getLength() != 0)
{
throw new IllegalStateException("XMLSignature already contains references");
Expand All @@ -89,19 +92,19 @@ SignedDataObjectsProcessor.Result process(
return process(
signedDataObjects.getDataObjectsDescs(),
xmlSignature.getSignedInfo(),
xmlSignature.getId(),
signedDataObjects.getResourceResolvers(),
xmlSignature,
false);
false,
idGenerator);
}

private SignedDataObjectsProcessor.Result process(
Collection<? extends DataObjectDesc> dataObjects,
Manifest container,
String idPrefix,
List<ResourceResolverSpi> resourceResolvers,
XMLSignature xmlSignature,
boolean hasNullURIReference) throws UnsupportedAlgorithmException {
boolean hasNullURIReference,
ElementIdGenerator idGenerator) throws UnsupportedAlgorithmException {
Map<DataObjectDesc, Reference> referenceMappings = new IdentityHashMap<>(dataObjects.size());
Set<Manifest> manifests = new HashSet<>();

Expand Down Expand Up @@ -134,9 +137,9 @@ else if (dataObjDesc instanceof EnvelopedXmlObject)
// If the data object info is a EnvelopedXmlObject we need to create a ds:Object to embed it.
// The Reference uri will refer the new ds:Object's id.
EnvelopedXmlObject envXmlObj = (EnvelopedXmlObject) dataObjDesc;
String xmlObjId = String.format("%s-object%d", idPrefix, index);

ObjectContainer xmlObj = new ObjectContainer(container.getDocument());
String xmlObjId = idFor(xmlObj, idGenerator);
xmlObj.setId(xmlObjId);
xmlObj.appendChild(envXmlObj.getContent());
xmlObj.setMimeType(envXmlObj.getMimeType());
Expand Down Expand Up @@ -164,17 +167,18 @@ else if (dataObjDesc instanceof EnvelopedManifest)
// If the data object info is a EnvelopedManifest we need to create a ds:Manifest and a ds:Object
// to embed it. The Reference uri will refer the manifest's id.
EnvelopedManifest envManifest = (EnvelopedManifest) dataObjDesc;
String xmlManifestId = String.format("%s-manifest%d", idPrefix, index);

Manifest xmlManifest = new Manifest(container.getDocument());
String xmlManifestId = idFor(xmlManifest, idGenerator);
xmlManifest.setId(xmlManifestId);

SignedDataObjectsProcessor.Result manifestResult = process(
envManifest.getDataObjects(),
xmlManifest,
xmlManifestId,
resourceResolvers,
xmlSignature,
hasNullURIReference);
hasNullURIReference,
idGenerator);

ObjectContainer xmlObj = new ObjectContainer(container.getDocument());
xmlObj.appendChild(xmlManifest.getElement());
Expand All @@ -199,12 +203,13 @@ else if (dataObjDesc instanceof EnvelopedManifest)
refUri,
transforms,
digestMethodUri,
String.format("%s-ref%d", idPrefix, index), // id
null,
refType);

// SignedDataObjects and EnvelopedManifest don't allow repeated instances, so there's no
// need to check for duplicate entries on the map.
Reference ref = container.item(index);
ref.setId(idFor(ref, idGenerator));
referenceMappings.put(dataObjDesc, ref);
}

Expand Down
30 changes: 22 additions & 8 deletions src/main/java/xades4j/production/SignerBES.java
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ class SignerBES implements XadesSigner
private final SignedPropertiesMarshaller signedPropsMarshaller;
private final UnsignedPropertiesMarshaller unsignedPropsMarshaller;
private final AlgorithmsParametersMarshallingProvider algorithmsParametersMarshaller;
private final ElementIdGeneratorFactory idGeneratorFactory;
/**/
private final KeyInfoBuilder keyInfoBuilder;
private final QualifyingPropertiesProcessor qualifPropsProcessor;
Expand All @@ -101,13 +102,14 @@ protected SignerBES(
SignedPropertiesMarshaller signedPropsMarshaller,
UnsignedPropertiesMarshaller unsignedPropsMarshaller,
AlgorithmsParametersMarshallingProvider algorithmsParametersMarshaller,
X500NameStyleProvider x500NameStyleProvider)
X500NameStyleProvider x500NameStyleProvider,
ElementIdGeneratorFactory idGeneratorFactory)
{
if (ObjectUtils.anyNull(
keyingProvider, signatureAlgorithms, basicSignatureOptions,
signaturePropsProvider, dataObjPropsProvider, propsDataObjectsGenerator,
signedPropsMarshaller, unsignedPropsMarshaller, algorithmsParametersMarshaller,
x500NameStyleProvider))
x500NameStyleProvider, idGeneratorFactory))
{
throw new NullPointerException("One or more arguments are null");
}
Expand All @@ -120,6 +122,7 @@ protected SignerBES(
this.unsignedPropsMarshaller = unsignedPropsMarshaller;
this.algorithmsParametersMarshaller = algorithmsParametersMarshaller;
this.dataObjectDescsProcessor = dataObjectDescsProcessor;
this.idGeneratorFactory = idGeneratorFactory;
this.keyInfoBuilder = new KeyInfoBuilder(basicSignatureOptions, signatureAlgorithms, algorithmsParametersMarshaller, x500NameStyleProvider);
this.qualifPropsProcessor = new QualifyingPropertiesProcessor(signaturePropsProvider, dataObjPropsProvider);
}
Expand Down Expand Up @@ -154,10 +157,8 @@ public final XadesSignatureResult sign(
this.basicSignatureOptions.ensureValid();

Document signatureDocument = DOMHelper.getOwnerDocument(referenceNode);
ElementIdGenerator idGenerator = this.idGeneratorFactory.create();

// Generate unique identifiers for the Signature and the SignedProperties.
String signatureId = String.format("xmldsig-%s", UUID.randomUUID());
String signedPropsId = String.format("%s-signedprops", signatureId);

// Signing certificate chain (may contain only the signing certificate).
List<X509Certificate> signingCertificateChain = this.keyingProvider.getSigningCertificateChain();
Expand All @@ -173,6 +174,7 @@ public final XadesSignatureResult sign(
signedDataObjects.getBaseUri(),
signingCertificate.getPublicKey().getAlgorithm());

String signatureId = idFor(signature, idGenerator);
signature.setId(signatureId);

/* References */
Expand All @@ -181,10 +183,11 @@ public final XadesSignatureResult sign(
// are added to the signature.
SignedDataObjectsProcessor.Result signedDataObjectsResult = this.dataObjectDescsProcessor.process(
signedDataObjects,
signature);
signature,
idGenerator);

/* ds:KeyInfo */
this.keyInfoBuilder.buildKeyInfo(signingCertificateChain, signature);
this.keyInfoBuilder.buildKeyInfo(signingCertificateChain, signature, idGenerator);

/* QualifyingProperties element */
// Create the QualifyingProperties element
Expand Down Expand Up @@ -239,6 +242,7 @@ public final XadesSignatureResult sign(
// Marshal the signed properties data to the QualifyingProperties node.
this.signedPropsMarshaller.marshal(signedPropsData, qualifyingPropsElem);
Element signedPropsElem = DOMHelper.getFirstChildElement(qualifyingPropsElem);
String signedPropsId = idFor(signedPropsElem, idGenerator);
DOMHelper.setIdAsXmlId(signedPropsElem, signedPropsId);

// SignedProperties reference
Expand Down Expand Up @@ -285,7 +289,7 @@ public final XadesSignatureResult sign(
Element sigValueElem = DOMHelper.getFirstDescendant(
signature.getElement(),
Constants.SignatureSpecNS, Constants._TAG_SIGNATUREVALUE);
DOMHelper.setIdAsXmlId(sigValueElem, String.format("%s-sigvalue", signatureId));
DOMHelper.setIdAsXmlId(sigValueElem, idFor(sigValueElem, idGenerator));

/* Marshal unsigned properties */
// Generate the unsigned properties data objects. The data objects structure
Expand Down Expand Up @@ -385,4 +389,14 @@ protected void getFormatSpecificSignatureProperties(
formatSpecificSignedSigProps.add(scp);
}
}

public static String idFor(ElementProxy elementProxy, ElementIdGenerator idGenerator)
{
return idGenerator.generateId(elementProxy.getBaseNamespace(), elementProxy.getBaseLocalName());
}

public static String idFor(Element element, ElementIdGenerator idGenerator)
{
return idGenerator.generateId(element.getNamespaceURI(), element.getLocalName());
}
}
3 changes: 2 additions & 1 deletion src/main/java/xades4j/production/SignerC.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,10 @@ protected SignerC(
UnsignedPropertiesMarshaller unsignedPropsMarshaller,
AlgorithmsParametersMarshallingProvider algorithmsParametersMarshaller,
X500NameStyleProvider x500NameStyleProvider,
ElementIdGeneratorFactory idGeneratorFactory,
Optional<SignaturePolicyInfoProvider> policyInfoProvider)
{
super(keyingProvider, signatureAlgorithms, basicSignatureOptions, dataObjectDescsProcessor, signaturePropsProvider, dataObjPropsProvider, propsDataObjectsGenerator, signedPropsMarshaller, unsignedPropsMarshaller, algorithmsParametersMarshaller, x500NameStyleProvider, policyInfoProvider);
super(keyingProvider, signatureAlgorithms, basicSignatureOptions, dataObjectDescsProcessor, signaturePropsProvider, dataObjPropsProvider, propsDataObjectsGenerator, signedPropsMarshaller, unsignedPropsMarshaller, algorithmsParametersMarshaller, x500NameStyleProvider, idGeneratorFactory, policyInfoProvider);
if (null == validationDataProvider)
throw new NullPointerException("ValidationDataProvider is null");

Expand Down
Loading

0 comments on commit 5278408

Please sign in to comment.