CAMEL-8129: XAdES BES/EPES for XML Signature Signer. Thanks to Franz Forsthofer for the patch.
Project: http://git-wip-us.apache.org/repos/asf/camel/repo Commit: http://git-wip-us.apache.org/repos/asf/camel/commit/23688d3d Tree: http://git-wip-us.apache.org/repos/asf/camel/tree/23688d3d Diff: http://git-wip-us.apache.org/repos/asf/camel/diff/23688d3d Branch: refs/heads/master Commit: 23688d3d84886e9f6fab8d73bc72991484f5c151 Parents: 0866d13 Author: Claus Ibsen <[email protected]> Authored: Wed Dec 17 08:18:59 2014 +0100 Committer: Claus Ibsen <[email protected]> Committed: Wed Dec 17 08:18:59 2014 +0100 ---------------------------------------------------------------------- components/camel-xmlsecurity/pom.xml | 6 + .../api/DefaultXAdESSignatureProperties.java | 58 + .../api/XAdESEncapsulatedPKIData.java | 83 ++ .../api/XAdESSignatureProperties.java | 1207 ++++++++++++++++++ .../xmlsecurity/api/XmlSignatureConstants.java | 49 + .../xmlsecurity/api/XmlSignatureProperties.java | 43 +- .../processor/XmlSignerProcessor.java | 42 +- .../xmlsecurity/SpringXmlSignatureTest.java | 28 + .../XAdESSignaturePropertiesTest.java | 922 +++++++++++++ .../component/xmlsecurity/XmlSignatureTest.java | 8 +- .../xmlsecurity/util/TestKeystore.java | 2 +- .../xmlsecurity/SpringXmlSignatureTests.xml | 33 + .../camel/component/xmlsecurity/xades/XAdES.xsd | 466 +++++++ .../xmlsecurity/xades/xmldsig-core-schema.xsd | 291 +++++ .../features/src/main/resources/features.xml | 1 + 15 files changed, 3221 insertions(+), 18 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/camel/blob/23688d3d/components/camel-xmlsecurity/pom.xml ---------------------------------------------------------------------- diff --git a/components/camel-xmlsecurity/pom.xml b/components/camel-xmlsecurity/pom.xml index 142a71e..74d02ec 100755 --- a/components/camel-xmlsecurity/pom.xml +++ b/components/camel-xmlsecurity/pom.xml @@ -46,6 +46,12 @@ <groupId>org.apache.camel</groupId> <artifactId>camel-core</artifactId> </dependency> + <!-- for base 64 in XAdES --> + <dependency> + <groupId>commons-codec</groupId> + <artifactId>commons-codec</artifactId> + <version>${commons-codec-version}</version> + </dependency> <dependency> <groupId>org.apache.santuario</groupId> <artifactId>xmlsec</artifactId> http://git-wip-us.apache.org/repos/asf/camel/blob/23688d3d/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/api/DefaultXAdESSignatureProperties.java ---------------------------------------------------------------------- diff --git a/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/api/DefaultXAdESSignatureProperties.java b/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/api/DefaultXAdESSignatureProperties.java new file mode 100644 index 0000000..edfd757 --- /dev/null +++ b/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/api/DefaultXAdESSignatureProperties.java @@ -0,0 +1,58 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.xmlsecurity.api; + +import java.security.KeyStore; +import java.security.cert.X509Certificate; + +/** + * Default implementation for the XAdES signature properties which determines + * the Signing Certificate from a keystore and an alias. + * + */ +public class DefaultXAdESSignatureProperties extends XAdESSignatureProperties { + + private KeyStore keystore; + + private String alias; + + public DefaultXAdESSignatureProperties() { + } + + public void setKeystore(KeyStore keystore) { + this.keystore = keystore; + } + + public void setAlias(String alias) { + this.alias = alias; + } + + @Override + protected X509Certificate getSigningCertificate() throws Exception { //NOPMD + X509Certificate cert = (X509Certificate) keystore.getCertificate(alias); + if (cert == null) { + throw new XmlSignatureException("No certificate found in keystore for alias '%s'"); + } + return cert; + } + + @Override + protected X509Certificate[] getSigningCertificateChain() throws Exception { //NOPMD + return null; + } + +} http://git-wip-us.apache.org/repos/asf/camel/blob/23688d3d/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/api/XAdESEncapsulatedPKIData.java ---------------------------------------------------------------------- diff --git a/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/api/XAdESEncapsulatedPKIData.java b/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/api/XAdESEncapsulatedPKIData.java new file mode 100644 index 0000000..55b7c68 --- /dev/null +++ b/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/api/XAdESEncapsulatedPKIData.java @@ -0,0 +1,83 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.xmlsecurity.api; + +/** + * Class representing the Encapsulated PKI Data of the XAdES specification. + * + */ +public class XAdESEncapsulatedPKIData { + + private final String base64Conent; + + private final String encoding; + + private final String id; + + /** + * Constructor + * + * @param base64Conent + * base64 encoded content + * @param encoding + * , can be <code>null</code> or empty; encoding + * http://uri.etsi.org/01903/v1.2.2#DER for denoting that the + * original PKI data were ASN.1 data encoded in DER. + * http://uri.etsi.org/01903/v1.2.2#BER for denoting that the + * original PKI data were ASN.1 data encoded in BER. + * http://uri.etsi.org/01903/v1.2.2#CER for denoting that the + * original PKI data were ASN.1 data encoded in CER. + * http://uri.etsi.org/01903/v1.2.2#PER for denoting that the + * original PKI data were ASN.1 data encoded in PER. + * http://uri.etsi.org/01903/v1.2.2#XER for denoting that the + * original PKI data were ASN.1 data encoded in XER. + * + * @param id + * ID for the Id attribute, can be <code>null</code> + * @throws IllegalArgumentException + * if <tt>base64Conent</tt> is <code>null</code> or empty + */ + public XAdESEncapsulatedPKIData(String base64Conent, String encoding, String id) { + if (base64Conent == null || base64Conent.isEmpty()) { + throw new IllegalArgumentException("Value for parameter 'base64Conent' is null or empty"); + } + this.base64Conent = base64Conent; + this.encoding = encoding; + this.id = id; + } + + /** + * Returns the base 64 encoded content. Cannot be <code>null</code> or + * empty. + */ + public String getBase64Conent() { + return base64Conent; + } + + /** + * Returns the character encoding of the content. Cannot be + * <code>null</code> or empty. + */ + public String getEncoding() { + return encoding; + } + + public String getId() { + return id; + } + +} http://git-wip-us.apache.org/repos/asf/camel/blob/23688d3d/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/api/XAdESSignatureProperties.java ---------------------------------------------------------------------- diff --git a/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/api/XAdESSignatureProperties.java b/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/api/XAdESSignatureProperties.java new file mode 100644 index 0000000..3daddf9 --- /dev/null +++ b/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/api/XAdESSignatureProperties.java @@ -0,0 +1,1207 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.xmlsecurity.api; + +import java.io.IOException; +import java.io.StringReader; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.UUID; + +import javax.security.auth.x500.X500Principal; +import javax.xml.crypto.dom.DOMStructure; +import javax.xml.crypto.dsig.DigestMethod; +import javax.xml.crypto.dsig.Reference; +import javax.xml.crypto.dsig.XMLObject; +import javax.xml.crypto.dsig.XMLSignature; +import javax.xml.parsers.ParserConfigurationException; + +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import org.apache.camel.Message; +import org.apache.commons.codec.binary.Base64; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.apache.camel.util.ObjectHelper.isNotEmpty; + +/** + * Implementation of the XAdES-BES and XAdES-EPES properties defined in + * http://www.etsi.org/deliver/etsi_ts%5C101900_101999%5C101903%5C01.04 + * .02_60%5Cts_101903v010402p.pdf. XAdES-T and XAdES-C is not implemented. + * <p> + * You have to overwrite the method {@link #getSigningCertificate()} or + * {@link #getSigningCertificateChain()} if you want to have a + * 'SigningCertificate' element in your XML Signature. + * <p> + * Further limitations: + * <ul> + * <li>No support for the 'QualifyingPropertiesReference' element (see section + * 6.3.2 of spec).</li> + * <li>No support for the 'Transforms' element contained in the + * 'SignaturePolicyId' element contained in 'SignaturePolicyIdentifier' element</li> + * <li>No support of the 'CounterSignature' element --> no support for the + * 'UnsignedProperties' element</li> + * <li>A 'CommitmentTypeIndication' element contains always the + * 'AllSignedDataObjects' element. The 'ObjectReference' element within the + * 'CommitmentTypeIndication' element is not supported.</li> + * <li>The 'AllDataObjectsTimeStamp' element is not supported (it requires a + * time authority)</li> + * <li>The 'IndividualDataObjectsTimeStamp' element is not supported (it + * requires a time authority)</li> + * </ul> + */ +public class XAdESSignatureProperties implements XmlSignatureProperties { + + public static final String HTTP_URI_ETSI_ORG_01903_V1_3_2 = "http://uri.etsi.org/01903/v1.3.2#"; + + public static final String HTTP_URI_ETSI_ORG_01903_V1_1_1 = "http://uri.etsi.org/01903/v1.1.1#"; + + public static final String HTTP_URI_ETSI_ORG_01903_V1_2_2 = "http://uri.etsi.org/01903/v1.2.2#"; + + public static final String SIG_POLICY_NONE = "None"; + + public static final String SIG_POLICY_IMPLIED = "Implied"; + + public static final String SIG_POLICY_EXPLICIT_ID = "ExplicitId"; + + private static final Logger LOG = LoggerFactory.getLogger(XAdESSignatureProperties.class); + + private static final Set<String> SIG_POLICY_VALUES = new TreeSet<String>(); + + private boolean addSigningTime = true; + + private String namespace = HTTP_URI_ETSI_ORG_01903_V1_3_2; + + private String prefix = "etsi"; + + private List<String> signingCertificateURIs = Collections.emptyList(); + + private String digestAlgorithmForSigningCertificate = DigestMethod.SHA256; //"http://www.w3.org/2000/09/xmldsig#sha1"; + + private String signaturePolicy = SIG_POLICY_NONE; + + private String sigPolicyId; + + private String sigPolicyIdQualifier; + + private String sigPolicyIdDescription; + + private List<String> sigPolicyIdDocumentationReferences = Collections.emptyList(); + + private String signaturePolicyDigestAlgorithm = DigestMethod.SHA256; //"http://www.w3.org/2000/09/xmldsig#sha1"; + + private String signaturePolicyDigestValue; + + private List<String> sigPolicyQualifiers = Collections.emptyList(); + + private String dataObjectFormatDescription; + + private String dataObjectFormatMimeType; + + private String dataObjectFormatIdentifier; + + private String dataObjectFormatIdentifierQualifier; + + private String dataObjectFormatIdentifierDescription; + + private List<String> dataObjectFormatIdentifierDocumentationReferences = Collections.emptyList(); + + private List<String> signerClaimedRoles = Collections.emptyList(); + + private List<XAdESEncapsulatedPKIData> signerCertifiedRoles = Collections.emptyList(); + + private String signatureProductionPlaceCity; + + private String signatureProductionPlaceStateOrProvince; + + private String signatureProductionPlacePostalCode; + + private String signatureProductionPlaceCountryName; + + private String commitmentTypeId; + + private String commitmentTypeIdQualifier; + + private String commitmentTypeIdDescription; + + private List<String> commitmentTypeIdDocumentationReferences = Collections.emptyList(); + + private List<String> commitmentTypeQualifiers = Collections.emptyList(); + + static { + SIG_POLICY_VALUES.add(SIG_POLICY_NONE); + SIG_POLICY_VALUES.add(SIG_POLICY_IMPLIED); + SIG_POLICY_VALUES.add(SIG_POLICY_EXPLICIT_ID); + } + + public XAdESSignatureProperties() { + } + + public boolean isAddSigningTime() { + return addSigningTime; + } + + public void setAddSigningTime(boolean addSigningTime) { + this.addSigningTime = addSigningTime; + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + if (namespace == null) { + throw new IllegalArgumentException("Parameter 'namespace' is null"); + } + this.namespace = namespace; + } + + protected String findNamespace(Message message) { + return message.getHeader(XmlSignatureConstants.HEADER_XADES_NAMESPACE, getNamespace(), String.class); + } + + public String getPrefix() { + return prefix; + } + + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + protected String findPrefix(Message message) { + return message.getHeader(XmlSignatureConstants.HEADER_XADES_PREFIX, getPrefix(), String.class); + } + + /** + * URIs of the signing certificate or signing certificate chain. For the + * sining certificate the first URI is taken. If there is a signing + * certificate chain specified, then the URIs are assigned to the + * certificates in the chain in the order given in the provided list. You + * have to specify an empty entry (null or empty srting), if no URI should + * be assigned to a specific certificate in the list. If you specify an + * empty list, then no URIs are assigned. + * + * @throws IllegalArgumentException + * if the parameter is <code>null</code> or one of the URIs is + * <code>null</code> + */ + public void setSigningCertificateURIs(List<String> signingCertificateURIs) { + if (signingCertificateURIs == null) { + throw new IllegalArgumentException("Parameter 'signingCertificateURIs' is null"); + } + this.signingCertificateURIs = new ArrayList<String>(signingCertificateURIs); + } + + public List<String> getSigningCertificateURIs() { + return signingCertificateURIs; + } + + public String getDigestAlgorithmForSigningCertificate() { + return digestAlgorithmForSigningCertificate; + } + + /** + * Digest Algorithm for creating the digest of the signing certificate. + * Possible values: "http://www.w3.org/2000/09/xmldsig#sha1", + * "http://www.w3.org/2001/04/xmlenc#sha256", + * "http://www.w3.org/2001/04/xmldsig-more#sha384", + * "http://www.w3.org/2001/04/xmlenc#sha512". Default value is + * "http://www.w3.org/2001/04/xmlenc#sha256". + * + */ + public void setDigestAlgorithmForSigningCertificate(String digestAlgorithm) { + this.digestAlgorithmForSigningCertificate = digestAlgorithm; + } + + public String getSignaturePolicy() { + return signaturePolicy; + } + + /** + * Signature Policy. Possible values: {@link #SIG_POLICY_NONE}, + * {@link #SIG_POLICY_IMPLIED}, {@link #SIG_POLICY_EXPLICIT_ID}. Default + * value is {@link #SIG_POLICY_NONE}. + * + */ + public void setSignaturePolicy(String signaturePolicy) { + if (!SIG_POLICY_VALUES.contains(signaturePolicy)) { + throw new IllegalArgumentException(String.format( + "Signature policy '%s' is invalid. Possible values are 'None', 'Implied', and 'ExplicitId'.", signaturePolicy)); + } + this.signaturePolicy = signaturePolicy; + } + + public String getSigPolicyId() { + return sigPolicyId; + } + + /** + * Identifier must be specified if {@link #getSignaturePolicy()} equals + * "ExplicitId". Must be an URI + */ + public void setSigPolicyId(String sigPolicyId) { + this.sigPolicyId = sigPolicyId; + } + + public String getSigPolicyIdQualifier() { + return sigPolicyIdQualifier; + } + + /** + * Qualifier for the Signature Policy Identifier. Possible values are + * <code>null</code> (which means no Qualifier element is created), + * "OIDAsURI", or "OIDAsURN". Default value is <code>null</code>. If the + * identifier is an OID then a qualifier must be set. + */ + public void setSigPolicyIdQualifier(String sigPolicyIdQualifier) { + this.sigPolicyIdQualifier = sigPolicyIdQualifier; + } + + public String getSigPolicyIdDescription() { + return sigPolicyIdDescription; + } + + public void setSigPolicyIdDescription(String sigPolicyIdDescription) { + this.sigPolicyIdDescription = sigPolicyIdDescription; + } + + public List<String> getSigPolicyIdDocumentationReferences() { + return sigPolicyIdDocumentationReferences; + } + + /** + * + * Sets the documentation references of the signature policy. + * + * @throws IllegalArgumentException + * if the parameter is <code>null</code> or one of the + * documentation references is <code>null</code> or empty + */ + public void setSigPolicyIdDocumentationReferences(List<String> sigPolicyIdDocumentationReferences) { + if (sigPolicyIdDocumentationReferences == null) { + throw new IllegalArgumentException("Parameter 'sigPolicyIdDocumentationReferences' is null"); + } + for (String ref : sigPolicyIdDocumentationReferences) { + if (ref == null || ref.isEmpty()) { + throw new IllegalArgumentException("At least one documentation reference of the signature policy is null or empty"); + } + } + this.sigPolicyIdDocumentationReferences = sigPolicyIdDocumentationReferences; + } + + public String getSignaturePolicyDigestAlgorithm() { + return signaturePolicyDigestAlgorithm; + } + + /** + * Digest Algorithm for creating the digest of the signature policy + * document. Possible values: "http://www.w3.org/2000/09/xmldsig#sha1", + * "http://www.w3.org/2001/04/xmlenc#sha256", + * "http://www.w3.org/2001/04/xmldsig-more#sha384", + * "http://www.w3.org/2001/04/xmlenc#sha512". Default value is + * "http://www.w3.org/2001/04/xmlenc#sha256". + * + */ + public void setSignaturePolicyDigestAlgorithm(String signaturePolicyDigestAlgorithm) { + this.signaturePolicyDigestAlgorithm = signaturePolicyDigestAlgorithm; + } + + public String getSignaturePolicyDigestValue() { + return signaturePolicyDigestValue; + } + + /** Digest value for the signature policy base 64 encoded. */ + public void setSignaturePolicyDigestValue(String signaturePolicyDigestValue) { + this.signaturePolicyDigestValue = signaturePolicyDigestValue; + } + + public List<String> getSigPolicyQualifiers() { + return sigPolicyQualifiers; + } + + /** + * Sets the signature policy qualifiers. Each qualifier can be a text or a + * XML fragment with the root element 'SigPolicyQualifier' with the XAdES + * namespace. + * + * @throws IllegalArgumentException + * if the input parameter is <code>null</code>, or one of the + * qualifiers is <code>null</code> or empty + * + */ + public void setSigPolicyQualifiers(List<String> sigPolicyQualifiers) { + if (sigPolicyQualifiers == null) { + throw new IllegalArgumentException("Parameter 'sigPolicyQualifiers' is null"); + } + for (String qualifier : sigPolicyQualifiers) { + if (qualifier == null || qualifier.isEmpty()) { + throw new IllegalArgumentException("At least one of the policy qualifiers is null or empty"); + } + } + this.sigPolicyQualifiers = new ArrayList<String>(sigPolicyQualifiers); + } + + public String getDataObjectFormatDescription() { + return dataObjectFormatDescription; + } + + public void setDataObjectFormatDescription(String dataObjectFormatDescription) { + this.dataObjectFormatDescription = dataObjectFormatDescription; + } + + public String getDataObjectFormatMimeType() { + return dataObjectFormatMimeType; + } + + public void setDataObjectFormatMimeType(String dataObjectFormatMimeType) { + this.dataObjectFormatMimeType = dataObjectFormatMimeType; + } + + public String getDataObjectFormatIdentifier() { + return dataObjectFormatIdentifier; + } + + public void setDataObjectFormatIdentifier(String dataObjectFormatIdentifier) { + this.dataObjectFormatIdentifier = dataObjectFormatIdentifier; + } + + public String getDataObjectFormatIdentifierQualifier() { + return dataObjectFormatIdentifierQualifier; + } + + /** + * Qualifier for the Format Identifier. Possible values are + * <code>null</code> (which means no Qualifier element is created), + * "OIDAsURI", or "OIDAsURN". Default value is <code>null</code>. If the + * identifier is an OID then a qualifier must be set. + */ + public void setDataObjectFormatIdentifierQualifier(String dataObjectFormatIdentifierQualifier) { + this.dataObjectFormatIdentifierQualifier = dataObjectFormatIdentifierQualifier; + } + + public String getDataObjectFormatIdentifierDescription() { + return dataObjectFormatIdentifierDescription; + } + + public void setDataObjectFormatIdentifierDescription(String dataObjectFormatIdentifierDescription) { + this.dataObjectFormatIdentifierDescription = dataObjectFormatIdentifierDescription; + } + + public List<String> getDataObjectFormatIdentifierDocumentationReferences() { + return dataObjectFormatIdentifierDocumentationReferences; + } + + /** + * + * Sets the documentation references of the data object format identifier. + * + * @throws IllegalArgumentException + * if the parameter is <code>null</code> or one of the + * documentation references is <code>null</code> or empty + */ + public void setDataObjectFormatIdentifierDocumentationReferences(List<String> dataObjectFormatIdentifierDocumentationReferences) { + if (dataObjectFormatIdentifierDocumentationReferences == null) { + throw new IllegalArgumentException("Parameter 'dataObjectFormatIdentifierDocumentationReferences' is null"); + } + for (String ref : dataObjectFormatIdentifierDocumentationReferences) { + if (ref == null || ref.isEmpty()) { + throw new IllegalArgumentException("At least one reference of the identifier of the data object format is null or empty"); + } + } + this.dataObjectFormatIdentifierDocumentationReferences = new ArrayList<String>(dataObjectFormatIdentifierDocumentationReferences); + } + + public List<String> getSignerClaimedRoles() { + return signerClaimedRoles; + } + + /** + * Sets the claimed roles list. A role can be either a text or a XML + * fragment with the root element 'ClaimedRole' with the XAdES namespace. + * + * @throws IllegalArgumentException + * if <tt>signerClaimedRoles</tt> is <code>null</code>, or if + * one of the roles is <code>null</code> or empty + */ + public void setSignerClaimedRoles(List<String> signerClaimedRoles) { + if (signerClaimedRoles == null) { + throw new IllegalArgumentException("Parameter 'signerClaimedRoles' is null"); + } + for (String role : signerClaimedRoles) { + if (role == null || role.isEmpty()) { + throw new IllegalArgumentException("At least one of the signer claimed roles is null or empty"); + } + } + this.signerClaimedRoles = new ArrayList<String>(signerClaimedRoles); + } + + public List<XAdESEncapsulatedPKIData> getSignerCertifiedRoles() { + return signerCertifiedRoles; + } + + /** + * Sets the certified roles. + * + * @throws IllegalArgumentException + * if <tt>signerCertifiedRoles</tt> is <code>null</code> + */ + public void setSignerCertifiedRoles(List<XAdESEncapsulatedPKIData> signerCertifiedRoles) { + if (signerCertifiedRoles == null) { + throw new IllegalArgumentException("Parameter 'signerCertifiedRoles' is null"); + } + for (XAdESEncapsulatedPKIData role : signerCertifiedRoles) { + if (role == null) { + throw new IllegalArgumentException("At least one of the signer certified roles is null"); + } + } + this.signerCertifiedRoles = new ArrayList<XAdESEncapsulatedPKIData>(signerCertifiedRoles); + } + + public String getSignatureProductionPlaceCity() { + return signatureProductionPlaceCity; + } + + public void setSignatureProductionPlaceCity(String signatureProductionPlaceCity) { + this.signatureProductionPlaceCity = signatureProductionPlaceCity; + } + + public String getSignatureProductionPlaceStateOrProvince() { + return signatureProductionPlaceStateOrProvince; + } + + public void setSignatureProductionPlaceStateOrProvince(String signatureProductionPlaceStateOrProvince) { + this.signatureProductionPlaceStateOrProvince = signatureProductionPlaceStateOrProvince; + } + + public String getSignatureProductionPlacePostalCode() { + return signatureProductionPlacePostalCode; + } + + public void setSignatureProductionPlacePostalCode(String signatureProductionPlacePostalCode) { + this.signatureProductionPlacePostalCode = signatureProductionPlacePostalCode; + } + + public String getSignatureProductionPlaceCountryName() { + return signatureProductionPlaceCountryName; + } + + public void setSignatureProductionPlaceCountryName(String signatureProductionPlaceCountryName) { + this.signatureProductionPlaceCountryName = signatureProductionPlaceCountryName; + } + + public String getCommitmentTypeId() { + return commitmentTypeId; + } + + public void setCommitmentTypeId(String commitmentTypeId) { + this.commitmentTypeId = commitmentTypeId; + } + + public String getCommitmentTypeIdQualifier() { + return commitmentTypeIdQualifier; + } + + /** + * Qualifier for the Commitment Type ID. Possible values are + * <code>null</code> (which means no Qualifier element is created), + * "OIDAsURI", or "OIDAsURN". Default value is <code>null</code>. If the + * identifier is an OID then a qualifier must be set. + */ + public void setCommitmentTypeIdQualifier(String commitmentTypeIdQualifier) { + this.commitmentTypeIdQualifier = commitmentTypeIdQualifier; + } + + public String getCommitmentTypeIdDescription() { + return commitmentTypeIdDescription; + } + + public void setCommitmentTypeIdDescription(String commitmentTypeIdDescription) { + this.commitmentTypeIdDescription = commitmentTypeIdDescription; + } + + public List<String> getCommitmentTypeIdDocumentationReferences() { + return commitmentTypeIdDocumentationReferences; + } + + /** + * Sets the documentation references for the Commitment Type ID: + * + * @throws IllegalArgumentException + * if the parameter is <code>null</code> or a documentation + * reference is <code>null</code> or empty + * + */ + public void setCommitmentTypeIdDocumentationReferences(List<String> commitmentTypeIdDocumentationReferences) { + if (commitmentTypeIdDocumentationReferences == null) { + throw new IllegalArgumentException("Parameter 'commitmentTypeIdDocumentationReferences' is null"); + } + for (String ref : commitmentTypeIdDocumentationReferences) { + if (ref == null || ref.isEmpty()) { + throw new IllegalArgumentException("At least one documentation reference of the commitment type is null or empty"); + } + } + this.commitmentTypeIdDocumentationReferences = new ArrayList<String>(commitmentTypeIdDocumentationReferences); + } + + public List<String> getCommitmentTypeQualifiers() { + return commitmentTypeQualifiers; + } + + /** + * List of additional qualifying information on the commitment. Each list + * element can be a text or an XML fragment with the root element + * 'CommitmentTypeQualifier' with the XAdES namespace. + * + * @throws IllegalArgumentException + * if the input parameter is <code>null</code>, or one qualifier + * is <code>null</code> or empty + */ + public void setCommitmentTypeQualifiers(List<String> commitmentTypeQualifiers) { + if (commitmentTypeQualifiers == null) { + throw new IllegalArgumentException("Parameter 'commitmentTypeQualifiers' is null"); + } + for (String qualifier : commitmentTypeQualifiers) { + if (qualifier == null || qualifier.isEmpty()) { + throw new IllegalArgumentException("At least one qualifier of the commitment type is null or empty"); + } + } + this.commitmentTypeQualifiers = new ArrayList<String>(commitmentTypeQualifiers); + } + + @Override + public Output get(Input input) throws Exception { //NOPMD + + XmlSignatureProperties.Output result = new Output(); + + if (!isAddSignedSignatureProperties() && !isAddSignedDataObjectPropeties()) { + LOG.debug("XAdES signature properties are empty. Therefore no XAdES element will be added to the signature."); + return result; + } + String signedPropertiesId = "_" + UUID.randomUUID().toString(); + Reference ref = input.getSignatureFactory().newReference("#" + signedPropertiesId, + input.getSignatureFactory().newDigestMethod(input.getContentDigestAlgorithm(), null), Collections.emptyList(), + "http://uri.etsi.org/01903#SignedProperties", null); + + Node parent = input.getParent(); + Document doc; + if (Node.DOCUMENT_NODE == parent.getNodeType()) { + doc = (Document) parent; // enveloping + } else { + doc = parent.getOwnerDocument(); // enveloped + } + + Element qualifyingProperties = createElement("QualifyingProperties", doc, input); + setIdAttributeFromHeader(XmlSignatureConstants.HEADER_XADES_QUALIFYING_PROPERTIES_ID, qualifyingProperties, input); + String signatureId = input.getSignatureId(); + if (signatureId == null || signatureId.isEmpty()) { + LOG.debug("No signature Id configured. Therefore a value is generated."); + // generate one + signatureId = "_" + UUID.randomUUID().toString(); + // and set to output + result.setSignatureId(signatureId); + } + setAttribute(qualifyingProperties, "Target", "#" + signatureId); + Element signedProperties = createElement("SignedProperties", doc, input); + qualifyingProperties.appendChild(signedProperties); + setAttribute(signedProperties, "Id", signedPropertiesId); + signedProperties.setIdAttribute("Id", true); + addSignedSignatureProperties(doc, signedProperties, input); + String contentReferenceId = addSignedDataObjectProperties(doc, signedProperties, input); + result.setContentReferenceId(contentReferenceId); + DOMStructure structure = new DOMStructure(qualifyingProperties); + + XMLObject propertiesObject = input.getSignatureFactory().newXMLObject(Collections.singletonList(structure), null, null, null); + + result.setReferences(Collections.singletonList(ref)); + result.setObjects(Collections.singletonList(propertiesObject)); + + return result; + } + + protected void setAttribute(Element element, String attrName, String value) { + // element.setAttribute(name, value); did cause NullPointerException in santuario 2.02 + element.setAttributeNS("", attrName, value); + } + + protected void setIdAttributeFromHeader(String header, Element element, Input input) { + String value = input.getMessage().getHeader(header, String.class); + if (value != null && !value.isEmpty()) { + setAttribute(element, "Id", value); + element.setIdAttribute("Id", true); + } + } + + protected String addSignedDataObjectProperties(Document doc, Element signedProperties, Input input) throws XmlSignatureException, + SAXException, IOException, ParserConfigurationException { + if (isAddSignedDataObjectPropeties()) { + Element signedDataObjectProperties = createElement("SignedDataObjectProperties", doc, input); + setIdAttributeFromHeader(XmlSignatureConstants.HEADER_XADES_SIGNED_DATA_OBJECT_PROPERTIES_ID, signedDataObjectProperties, input); + signedProperties.appendChild(signedDataObjectProperties); + String contentReferenceId = addDataObjectFormat(signedDataObjectProperties, doc, input); + addCommitmentTypeIndication(signedDataObjectProperties, doc, input); + return contentReferenceId; + } else { + return null; + } + } + + protected boolean isAddSignedDataObjectPropeties() { + return isAddDataObjectFormat() || isAddCommitmentType(); + } + + protected void addCommitmentTypeIndication(Element signedDataObjectProperties, Document doc, Input input) throws SAXException, + IOException, ParserConfigurationException, XmlSignatureException { + if (!isAddCommitmentType()) { + return; + } + Element commitmentTypeIndication = createElement("CommitmentTypeIndication", doc, input); + signedDataObjectProperties.appendChild(commitmentTypeIndication); + Element commitmentTypeIdEl = createElement("CommitmentTypeId", doc, input); + commitmentTypeIndication.appendChild(commitmentTypeIdEl); + Element identifier = createElement("Identifier", doc, input); + commitmentTypeIdEl.appendChild(identifier); + identifier.setTextContent(getCommitmentTypeId()); + if (getDataObjectFormatIdentifierQualifier() != null && !getDataObjectFormatIdentifierQualifier().isEmpty()) { + setAttribute(identifier, "Qualifier", getDataObjectFormatIdentifierQualifier()); + } + if (getCommitmentTypeIdDescription() != null && !getCommitmentTypeIdDescription().isEmpty()) { + Element description = createElement("Description", doc, input); + commitmentTypeIdEl.appendChild(description); + description.setTextContent(getCommitmentTypeIdDescription()); + } + if (!getCommitmentTypeIdDocumentationReferences().isEmpty()) { + Element documentationReferences = createElement("DocumentationReferences", doc, input); + commitmentTypeIdEl.appendChild(documentationReferences); + List<String> docReferences = getCommitmentTypeIdDocumentationReferences(); + for (String documentationReferenceValue : docReferences) { + Element documentationReference = createElement("DocumentationReference", doc, input); + documentationReferences.appendChild(documentationReference); + documentationReference.setTextContent(documentationReferenceValue); + } + } + Element allSignedDataObjects = createElement("AllSignedDataObjects", doc, input); + commitmentTypeIndication.appendChild(allSignedDataObjects); + + List<String> qualifiers = getCommitmentTypeQualifiers(); + if (!qualifiers.isEmpty()) { + Element qualifiersEl = createElement("CommitmentTypeQualifiers", doc, input); + commitmentTypeIndication.appendChild(qualifiersEl); + String errorMessage = "The XAdES confguration is invalid. The list of the commitment type qualifiers contains the invalid entry '%s'. An entry must either be a text or an XML fragment " + + "with the root element '%s' with the namespace '%s'."; + for (String qualifier : getCommitmentTypeQualifiers()) { + Element qualifierEl = createChildFromXmlFragmentOrText(doc, input, "CommitmentTypeQualifier", errorMessage, qualifier); + qualifiersEl.appendChild(qualifierEl); + } + } + } + + protected boolean isAddCommitmentType() { + return getCommitmentTypeId() != null && !getCommitmentTypeId().isEmpty(); + } + + protected String addDataObjectFormat(Element signedDataObjectProperties, Document doc, Input input) throws XmlSignatureException { + if (!isAddDataObjectFormat()) { + return null; + } + Element dataObjectFormat = createElement("DataObjectFormat", doc, input); + signedDataObjectProperties.appendChild(dataObjectFormat); + String contentReferenceId = "_" + UUID.randomUUID().toString(); + setAttribute(dataObjectFormat, "ObjectReference", contentReferenceId); + + if (getDataObjectFormatDescription() != null && !getDataObjectFormatDescription().isEmpty()) { + Element description = createElement("Description", doc, input); + dataObjectFormat.appendChild(description); + description.setTextContent(getDataObjectFormatDescription()); + } + if (getDataObjectFormatIdentifier() != null && !getDataObjectFormatIdentifier().isEmpty()) { + Element objectIdentifier = createElement("ObjectIdentifier", doc, input); + dataObjectFormat.appendChild(objectIdentifier); + Element identifier = createElement("Identifier", doc, input); + objectIdentifier.appendChild(identifier); + + identifier.setTextContent(getDataObjectFormatIdentifier()); + if (getDataObjectFormatIdentifierQualifier() != null && !getDataObjectFormatIdentifierQualifier().isEmpty()) { + setAttribute(identifier, "Qualifier", getDataObjectFormatIdentifierQualifier()); + } + if (getDataObjectFormatIdentifierDescription() != null && !getDataObjectFormatIdentifierDescription().isEmpty()) { + Element description = createElement("Description", doc, input); + objectIdentifier.appendChild(description); + description.setTextContent(getDataObjectFormatIdentifierDescription()); + } + if (!getDataObjectFormatIdentifierDocumentationReferences().isEmpty()) { + Element documentationReferences = createElement("DocumentationReferences", doc, input); + objectIdentifier.appendChild(documentationReferences); + List<String> docReferences = getDataObjectFormatIdentifierDocumentationReferences(); + for (String documentationReferenceValue : docReferences) { + Element documentationReference = createElement("DocumentationReference", doc, input); + documentationReferences.appendChild(documentationReference); + documentationReference.setTextContent(documentationReferenceValue); + } + } + + } + if (getDataObjectFormatMimeType() != null && !getDataObjectFormatMimeType().isEmpty()) { + Element mimeType = createElement("MimeType", doc, input); + dataObjectFormat.appendChild(mimeType); + mimeType.setTextContent(getDataObjectFormatMimeType()); + } + String encoding = input.getMessage().getHeader(XmlSignatureConstants.HEADER_XADES_DATA_OBJECT_FORMAT_ENCODING, String.class); + if (encoding != null && !encoding.isEmpty()) { + Element encodingEl = createElement("Encoding", doc, input); + dataObjectFormat.appendChild(encodingEl); + encodingEl.setTextContent(encoding); + } + return contentReferenceId; + } + + protected boolean isAddDataObjectFormat() { + return (getDataObjectFormatIdentifier() != null && !getDataObjectFormatIdentifier().isEmpty()) + || (getDataObjectFormatDescription() != null && !getDataObjectFormatDescription().isEmpty()) + || (getDataObjectFormatMimeType() != null && !getDataObjectFormatMimeType().isEmpty()); + } + + protected void addSignedSignatureProperties(Document doc, Element signedProperties, Input input) throws Exception { //NOPMD + if (isAddSignedSignatureProperties()) { + LOG.debug("Adding signed signature properties"); + Element signedSignatureProperties = createElement("SignedSignatureProperties", doc, input); + setIdAttributeFromHeader(XmlSignatureConstants.HEADER_XADES_SIGNED_SIGNATURE_PROPERTIES_ID, signedSignatureProperties, input); + signedProperties.appendChild(signedSignatureProperties); + addSigningTime(doc, signedSignatureProperties, input); + addSigningCertificate(doc, signedSignatureProperties, input); + addSignaturePolicyIdentifier(doc, signedSignatureProperties, input); + addSignatureProductionPlace(doc, signedSignatureProperties, input); + addSignerRole(doc, signedSignatureProperties, input); + } + } + + protected boolean isAddSignedSignatureProperties() throws Exception { //NOPMD + return isAddSigningTime() || getSigningCertificate() != null + || (getSigningCertificateChain() != null && getSigningCertificateChain().length > 0) || isAddSignaturePolicy() + || isAddSignatureProductionPlace() || isAddSignerRole(); + } + + protected boolean isAddSignerRole() { + return getSignerClaimedRoles().size() > 0 || getSignerCertifiedRoles().size() > 0; + } + + protected void addSignatureProductionPlace(Document doc, Element signedSignatureProperties, Input input) { + if (!isAddSignatureProductionPlace()) { + return; + } + Element signatureProductionPlace = createElement("SignatureProductionPlace", doc, input); + signedSignatureProperties.appendChild(signatureProductionPlace); + if (getSignatureProductionPlaceCity() != null && !getSignatureProductionPlaceCity().isEmpty()) { + LOG.debug("Adding production city"); + Element city = createElement("City", doc, input); + signatureProductionPlace.appendChild(city); + city.setTextContent(getSignatureProductionPlaceCity()); + } + if (getSignatureProductionPlaceStateOrProvince() != null && !getSignatureProductionPlaceStateOrProvince().isEmpty()) { + LOG.debug("Adding production state or province"); + Element stateOrProvince = createElement("StateOrProvince", doc, input); + signatureProductionPlace.appendChild(stateOrProvince); + stateOrProvince.setTextContent(getSignatureProductionPlaceStateOrProvince()); + } + if (getSignatureProductionPlacePostalCode() != null && !getSignatureProductionPlacePostalCode().isEmpty()) { + LOG.debug("Adding production postal code"); + Element postalCode = createElement("PostalCode", doc, input); + signatureProductionPlace.appendChild(postalCode); + postalCode.setTextContent(getSignatureProductionPlacePostalCode()); + } + if (getSignatureProductionPlaceCountryName() != null && !getSignatureProductionPlaceCountryName().isEmpty()) { + LOG.debug("Adding production country name"); + Element countryName = createElement("CountryName", doc, input); + signatureProductionPlace.appendChild(countryName); + countryName.setTextContent(getSignatureProductionPlaceCountryName()); + } + } + + protected boolean isAddSignatureProductionPlace() { + return isNotEmpty(getSignatureProductionPlaceCity()) || isNotEmpty(getSignatureProductionPlaceCountryName()) + || isNotEmpty(getSignatureProductionPlacePostalCode()) || isNotEmpty(getSignatureProductionPlaceStateOrProvince()); + } + + protected void addSignerRole(Document doc, Element signedSignatureProperties, Input input) throws XmlSignatureException, SAXException, + IOException, ParserConfigurationException { + if (!isAddSignerRole()) { + return; + } + Element signerRole = createElement("SignerRole", doc, input); + signedSignatureProperties.appendChild(signerRole); + List<String> claimedRoles = getSignerClaimedRoles(); + if (!claimedRoles.isEmpty()) { + LOG.debug("Adding claimed roles"); + Element claimedRolesEl = createElement("ClaimedRoles", doc, input); + signerRole.appendChild(claimedRolesEl); + String errorMessage = "The XAdES confguration is invalid. The list of the claimed roles contains the invalid entry '%s'." + + " An entry must either be a text or an XML fragment with the root element '%s' with the namespace '%s'."; + for (String claimedRole : claimedRoles) { + Element claimedRoleEl = createChildFromXmlFragmentOrText(doc, input, "ClaimedRole", errorMessage, claimedRole); + claimedRolesEl.appendChild(claimedRoleEl); + } + } + List<XAdESEncapsulatedPKIData> certifiedRoles = getSignerCertifiedRoles(); + if (!certifiedRoles.isEmpty()) { + LOG.debug("Adding certified roles"); + Element certifiedRolesEl = createElement("CertifiedRoles", doc, input); + signerRole.appendChild(certifiedRolesEl); + for (XAdESEncapsulatedPKIData certifiedRole : certifiedRoles) { + Element certifiedRoleEl = createElement("CertifiedRole", doc, input); + certifiedRolesEl.appendChild(certifiedRoleEl); + certifiedRoleEl.setTextContent(certifiedRole.getBase64Conent()); + if (certifiedRole.getEncoding() != null && !certifiedRole.getEncoding().isEmpty()) { + setAttribute(certifiedRoleEl, "Encoding", certifiedRole.getEncoding()); + } + if (certifiedRole.getId() != null && !certifiedRole.getId().isEmpty()) { + setAttribute(certifiedRoleEl, "Id", certifiedRole.getId()); + certifiedRoleEl.setIdAttribute("Id", true); + } + } + } + + } + + protected void addSignaturePolicyIdentifier(Document doc, Element signedProperties, Input input) throws XmlSignatureException, + SAXException, IOException, ParserConfigurationException { + if (!isAddSignaturePolicy()) { + return; + } + Element signaturePolicyIdentifier = createElement("SignaturePolicyIdentifier", doc, input); + signedProperties.appendChild(signaturePolicyIdentifier); + if (SIG_POLICY_IMPLIED.equals(getSignaturePolicy())) { + LOG.debug("Adding implied signature policy"); + Element implied = createElement("SignaturePolicyImplied", doc, input); + signaturePolicyIdentifier.appendChild(implied); + } else if (SIG_POLICY_EXPLICIT_ID.equals(getSignaturePolicy())) { + LOG.debug("Adding signatue policy ID"); + Element id = createElement("SignaturePolicyId", doc, input); + signaturePolicyIdentifier.appendChild(id); + Element sigPolicyId = createElement("SigPolicyId", doc, input); + id.appendChild(sigPolicyId); + Element identifier = createElement("Identifier", doc, input); + sigPolicyId.appendChild(identifier); + if (getSigPolicyId() == null || getSigPolicyId().isEmpty()) { + throw new XmlSignatureException("The XAdES-EPES confguration is invalid. The signature policy identifier is missing."); + } + identifier.setTextContent(getSigPolicyId()); + if (getSigPolicyIdQualifier() != null && !getSigPolicyIdQualifier().isEmpty()) { + setAttribute(identifier, "Qualifier", getSigPolicyIdQualifier()); + } + if (getSigPolicyIdDescription() != null && !getSigPolicyIdDescription().isEmpty()) { + Element description = createElement("Description", doc, input); + sigPolicyId.appendChild(description); + description.setTextContent(getSigPolicyIdDescription()); + } + if (!getSigPolicyIdDocumentationReferences().isEmpty()) { + Element documentationReferences = createElement("DocumentationReferences", doc, input); + sigPolicyId.appendChild(documentationReferences); + List<String> docReferences = getSigPolicyIdDocumentationReferences(); + for (String documentationReferenceValue : docReferences) { + Element documentationReference = createElement("DocumentationReference", doc, input); + documentationReferences.appendChild(documentationReference); + documentationReference.setTextContent(documentationReferenceValue); + } + } + //here we could introduce the transformations for the signature policy, which we do not yet support + Element sigPolicyHash = createElement("SigPolicyHash", doc, input); + id.appendChild(sigPolicyHash); + if (getSignaturePolicyDigestAlgorithm() == null || getSignaturePolicyDigestAlgorithm().isEmpty()) { + throw new XmlSignatureException( + "The XAdES-EPES confguration is invalid. The digest algorithm for the signature policy is missing."); + } + Element digestMethod = createDigSigElement("DigestMethod", doc, input.getPrefixForXmlSignatureNamespace()); + sigPolicyHash.appendChild(digestMethod); + setAttribute(digestMethod, "Algorithm", getSignaturePolicyDigestAlgorithm()); + if (getSignaturePolicyDigestValue() == null || getSignaturePolicyDigestValue().isEmpty()) { + throw new XmlSignatureException( + "The XAdES-EPES confguration is invalid. The digest value for the signature policy is missing."); + } + Element digestValue = createDigSigElement("DigestValue", doc, input.getPrefixForXmlSignatureNamespace()); + sigPolicyHash.appendChild(digestValue); + digestValue.setTextContent(getSignaturePolicyDigestValue()); + + List<String> qualifiers = getSigPolicyQualifiers(); + if (!qualifiers.isEmpty()) { + Element qualifiersEl = createElement("SigPolicyQualifiers", doc, input); + id.appendChild(qualifiersEl); + String errorMessage = "The XAdES confguration is invalid. The list of the signatue policy qualifiers contains the invalid entry '%s'." + + " An entry must either be a text or an XML fragment with the root element '%s' with the namespace '%s'."; + for (String elementOrText : getSigPolicyQualifiers()) { + Element child = createChildFromXmlFragmentOrText(doc, input, "SigPolicyQualifier", errorMessage, elementOrText); + qualifiersEl.appendChild(child); + } + } + } else { + // cannot happen + throw new IllegalStateException(String.format( + "Invalid value '%s' for parameter 'SignaturePolicy'. Possible values are: 'None', 'Implied', and 'ExplictId'.", + getSignaturePolicy())); + } + + } + + protected Element createChildFromXmlFragmentOrText(Document doc, Input input, String localElementName, String errorMessage, + String elementOrText) throws IOException, ParserConfigurationException, XmlSignatureException { + String ending = localElementName + ">"; + Element child; + if (elementOrText.startsWith("<") && elementOrText.endsWith(ending)) { + try { + // assume xml + InputSource source = new InputSource(new StringReader(elementOrText)); + source.setEncoding("UTF-8"); + Document parsedDoc = XmlSignatureHelper.newDocumentBuilder(Boolean.TRUE).parse(source); + replacePrefixes(parsedDoc, input); + child = (Element) doc.adoptNode(parsedDoc.getDocumentElement()); + // check for correct namespace + String ns = findNamespace(input.getMessage()); + if (!ns.equals(child.getNamespaceURI())) { + throw new XmlSignatureException( + String.format( + "The XAdES confguration is invalid. The root element '%s' of the provided XML fragment '%s' has the invalid namespace '%s'. The correct namespace is '%s'.", + child.getLocalName(), elementOrText, child.getNamespaceURI(), ns)); + } + } catch (SAXException e) { + throw new XmlSignatureException(String.format(errorMessage, elementOrText, localElementName, namespace), e); + } + } else { + child = createElement(localElementName, doc, input); + child.setTextContent(elementOrText); + } + return child; + } + + protected void replacePrefixes(Document qualifierDoc, Input input) { + + Element el = qualifierDoc.getDocumentElement(); + replacePrefix(el, input); + + List<Element> childElements = getChildElements(el); + + List<Element> collectedNewChildElements = new ArrayList<Element>(); + for (; !childElements.isEmpty();) { + collectedNewChildElements.clear(); + for (Element child : childElements) { + replacePrefix(child, input); + List<Element> newChildElements = getChildElements(child); + collectedNewChildElements.addAll(newChildElements); + } + childElements = new ArrayList<Element>(collectedNewChildElements); + } + } + + protected List<Element> getChildElements(Element el) { + List<Element> childElements = new ArrayList<Element>(5); + NodeList children = el.getChildNodes(); + int length = children.getLength(); + for (int i = 0; i < length; i++) { + Node child = children.item(i); + if (Node.ELEMENT_NODE == child.getNodeType()) { + childElements.add((Element) child); + } + } + return childElements; + } + + protected void replacePrefix(Element el, Input input) { + replacePrefixForNode(el, input); + NamedNodeMap nnm = el.getAttributes(); + List<Attr> xmlnsToBeRemoved = new ArrayList<Attr>(2); + int length = nnm.getLength(); + for (int i = 0; i < length; i++) { + Node attr = nnm.item(i); + replacePrefixForNode(attr, input); + if (attr.getNodeType() == Node.ATTRIBUTE_NODE) { + if ("xmlns".equals(attr.getLocalName()) || "xmlns".equals(attr.getPrefix())) { + if (XMLSignature.XMLNS.equals(attr.getTextContent()) || findNamespace(input.getMessage()).equals(attr.getTextContent())) { + xmlnsToBeRemoved.add((Attr) attr); + } + } + } + } + // remove xml namespace declaration for XML signature and XAdES namespace + for (Attr toBeRemoved : xmlnsToBeRemoved) { + el.removeAttributeNode(toBeRemoved); + } + + } + + protected void replacePrefixForNode(Node node, Input input) { + if (XMLSignature.XMLNS.equals(node.getNamespaceURI())) { + node.setPrefix(input.getPrefixForXmlSignatureNamespace()); + } else if (findNamespace(input.getMessage()).equals(node.getNamespaceURI())) { + node.setPrefix(findPrefix(input.getMessage())); + } + } + + protected boolean isAddSignaturePolicy() { + return !SIG_POLICY_NONE.equals(getSignaturePolicy()); + } + + protected void addSigningCertificate(Document doc, Element signedProperties, Input input) throws Exception { //NOPMD + if (getSigningCertificate() == null && (getSigningCertificateChain() == null || getSigningCertificateChain().length == 0)) { + return; + } + // signed certificate + Element signedCertificate = createElement("SigningCertificate", doc, input); + signedProperties.appendChild(signedCertificate); + if (getSigningCertificate() != null) { + LOG.debug("Adding signing certificate"); + X509Certificate cert = getSigningCertificate(); + addCertificate(cert, signedCertificate, doc, 0, input); + } else if (getSigningCertificateChain() != null && getSigningCertificateChain().length > 0) { + Certificate[] certs = getSigningCertificateChain(); + int index = 0; + for (Certificate cert : certs) { + LOG.debug("Adding chain certtificate {}", index); + X509Certificate x509Cert = (X509Certificate) cert; + addCertificate(x509Cert, signedCertificate, doc, index, input); + index++; + } + } else { + // cannot happen + throw new IllegalStateException("Unexpected exception"); + } + } + + /** + * Returns the signing certificate. If you want to have a + * "SigningCertificate" element then either this method or the method + * {@link #getSigningCertificateChain()} must return a value which is + * different from <code>null</code> or an empty array. + * <p> + * This implementation returns <code>null</code> + */ + protected X509Certificate getSigningCertificate() throws Exception { //NOPMD + return null; + } + + /** + * Returns the signing certificate. If you want to have a + * "SigningCertificate" element then either this method or the method + * {@link #getSigningCertificate()} must return a value. + * <p> + * This implementation returns <code>null</code> + */ + protected X509Certificate[] getSigningCertificateChain() throws Exception { //NOPMD + return null; + } + + protected void addSigningTime(Document doc, Element signedProperties, Input input) { + if (isAddSigningTime()) { + LOG.debug("Adding signing time"); + //signing time + Element signingTime = createElement("SigningTime", doc, input); + signedProperties.appendChild(signingTime); + Date current = new Date(); + signingTime.setTextContent(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX").format(current)); + } + } + + protected void addCertificate(X509Certificate cert, Element signedCertificate, Document doc, int index, Input input) + throws CertificateEncodingException, NoSuchAlgorithmException, XmlSignatureException { + Element elCert = createElement("Cert", doc, input); + signedCertificate.appendChild(elCert); + + String algorithm = getMessageDigestAlgorithm(getDigestAlgorithmForSigningCertificate(), + "The digest algorithm '%s' for the signing certificate is invalid"); + String digest = calculateDigest(algorithm, cert.getEncoded()); + Element certDigest = createElement("CertDigest", doc, input); + elCert.appendChild(certDigest); + Element digestMethod = createDigSigElement("DigestMethod", doc, input.getPrefixForXmlSignatureNamespace()); + certDigest.appendChild(digestMethod); + setAttribute(digestMethod, "Algorithm", getDigestAlgorithmForSigningCertificate()); + Element digestValue = createDigSigElement("DigestValue", doc, input.getPrefixForXmlSignatureNamespace()); + certDigest.appendChild(digestValue); + digestValue.setTextContent(digest); + + Element issuerSerial = createElement("IssuerSerial", doc, input); + elCert.appendChild(issuerSerial); + Element x509IssuerName = createDigSigElement("X509IssuerName", doc, input.getPrefixForXmlSignatureNamespace()); + issuerSerial.appendChild(x509IssuerName); + x509IssuerName.setTextContent(cert.getIssuerX500Principal().getName(X500Principal.RFC2253)); + Element x509SerialNumber = createDigSigElement("X509SerialNumber", doc, input.getPrefixForXmlSignatureNamespace()); + issuerSerial.appendChild(x509SerialNumber); + x509SerialNumber.setTextContent(cert.getSerialNumber().toString()); + + List<String> uris = getSigningCertificateURIs(); + if (!uris.isEmpty() && uris.size() > index) { + String uri = uris.get(index); + if (uri != null && !uri.isEmpty()) { + setAttribute(elCert, "URI", uri); + } + } + } + + protected String getMessageDigestAlgorithm(String xmlSigDigestMethod, String errorMessage) throws XmlSignatureException { + String algorithm; + if (DigestMethod.SHA1.equals(xmlSigDigestMethod)) { + algorithm = "SHA-1"; + } else if (DigestMethod.SHA256.equals(xmlSigDigestMethod)) { + algorithm = "SHA-256"; + } else if ("http://www.w3.org/2001/04/xmldsig-more#sha384".equals(xmlSigDigestMethod)) { + algorithm = "SHA-384"; + } else if (DigestMethod.SHA512.equals(getDigestAlgorithmForSigningCertificate())) { + algorithm = "SHA-512"; + } else { + throw new XmlSignatureException(String.format(errorMessage, xmlSigDigestMethod)); + } + return algorithm; + } + + protected String calculateDigest(String algorithm, byte[] bytes) throws NoSuchAlgorithmException, CertificateEncodingException { + MessageDigest digest = MessageDigest.getInstance(algorithm); + byte[] digestBytes = digest.digest(bytes); + return new Base64().encodeAsString(digestBytes); + } + + protected Element createDigSigElement(String localName, Document doc, String prefixForXmlSignatureNamespace) { + Element el = doc.createElementNS("http://www.w3.org/2000/09/xmldsig#", localName); + if (prefixForXmlSignatureNamespace != null && !prefixForXmlSignatureNamespace.isEmpty()) { + el.setPrefix(prefixForXmlSignatureNamespace); + } + return el; + } + + protected Element createElement(String localName, Document doc, Input input) { + + Element el = doc.createElementNS(findNamespace(input.getMessage()), localName); + String p = findPrefix(input.getMessage()); + if (p != null && !p.isEmpty()) { + el.setPrefix(p); + } + return el; + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/23688d3d/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/api/XmlSignatureConstants.java ---------------------------------------------------------------------- diff --git a/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/api/XmlSignatureConstants.java b/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/api/XmlSignatureConstants.java index 4de4358..5fecbcf 100644 --- a/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/api/XmlSignatureConstants.java +++ b/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/api/XmlSignatureConstants.java @@ -62,6 +62,55 @@ public final class XmlSignatureConstants { public static final String HEADER_XPATHS_TO_ID_ATTRIBUTES = "CamelXmlSignatureXpathsToIdAttributes"; + /*------------------------- headers for XAdES signer ----------------------------------------------------------*/ + /** + * Header for the 'Id' attribute value of the XAdES element + * 'QualifyingProperties' + * + */ + public static final String HEADER_XADES_QUALIFYING_PROPERTIES_ID = "CamelXmlSignatureXAdESQualifyingPropertiesId"; + + /** + * Header for the 'Id' attribute value of the XAdES element + * 'SignedDataObjectProperties' + * + */ + public static final String HEADER_XADES_SIGNED_DATA_OBJECT_PROPERTIES_ID = "CamelXmlSignatureXAdESSignedDataObjectPropertiesId"; + + /** + * Header for the 'Id' attribute value of the XAdES element + * 'SignedSignatureProperties' + * + */ + public static final String HEADER_XADES_SIGNED_SIGNATURE_PROPERTIES_ID = "CamelXmlSignatureXAdESSignedSignaturePropertiesId"; + + /** + * Header for the "Encoding" element contained in the "DataObjectFormat" + * XAdES element. + */ + public static final String HEADER_XADES_DATA_OBJECT_FORMAT_ENCODING = "CamelXmlSignatureXAdESDataObjectFormatEncoding"; + + /** + * Header for the XAdES namespace. Different namespaces represent different + * XAdES specification versions. Currently supported namespaces are: + * + * http://uri.etsi.org/01903/v1.1.1#, + * + * http://uri.etsi.org/01903/v1.2.2#, + * + * http://uri.etsi.org/01903/v1.3.2#. + * + */ + public static final String HEADER_XADES_NAMESPACE = "CamelXmlSignatureXAdESNamespace"; + + /** + * Header for the XAdES namespace prefix. An empty string means that no + * prefix shall be used. A <code>null</code> header value will have no + * effect. + * + */ + public static final String HEADER_XADES_PREFIX = "CamelXmlSignatureXAdESPrefix"; + private XmlSignatureConstants() { // no instance } http://git-wip-us.apache.org/repos/asf/camel/blob/23688d3d/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/api/XmlSignatureProperties.java ---------------------------------------------------------------------- diff --git a/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/api/XmlSignatureProperties.java b/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/api/XmlSignatureProperties.java index 9fe72c8..3dea473 100644 --- a/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/api/XmlSignatureProperties.java +++ b/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/api/XmlSignatureProperties.java @@ -58,10 +58,10 @@ public interface XmlSignatureProperties { /** * Returns the parent node of the signature element in the case of - * enveloped or detached XML signature. <code>null</code> is returned in + * enveloped or detached XML signature, or the empty result document in * the case of enveloping XML signature. * - * @return parent node or <code>null</code> + * @return parent node, cannot be <code>null</code> */ Node getParent(); @@ -109,10 +109,20 @@ public interface XmlSignatureProperties { */ SignatureType getSignatureType(); + /** + * Returns the prefix for the XML Signature namespace + * ("http://www.w3.org/2000/09/xmldsig#"). Can be null or empty. + */ + String getPrefixForXmlSignatureNamespace(); + } public static class Output { + private String contentReferenceId; + + private String signatureId; + private List<? extends XMLObject> objects; private List<? extends Reference> references; @@ -133,6 +143,35 @@ public interface XmlSignatureProperties { this.references = references; } + public String getContentReferenceId() { + return contentReferenceId; + } + + /** + * Id value for the reference of the signed content. Currently used by + * the XAdES parameter DataObjectFormat. See XAdESSignatureProperties. + * */ + public void setContentReferenceId(String contentReferenceId) { + this.contentReferenceId = contentReferenceId; + } + + public String getSignatureId() { + return signatureId; + } + + /** + * You can overwrite the value of the Id attribute of the Signature + * element that you get from {@link Input#getSignatureId()}. Only if the + * provided value is not <code>null</code> and not empty, then the + * signature Id will be overwritten. + * + * @param signatureId + * Id attribute value of the Signature element + */ + public void setSignatureId(String signatureId) { + this.signatureId = signatureId; + } + } } http://git-wip-us.apache.org/repos/asf/camel/blob/23688d3d/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/processor/XmlSignerProcessor.java ---------------------------------------------------------------------- diff --git a/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/processor/XmlSignerProcessor.java b/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/processor/XmlSignerProcessor.java index eb1e4a7..0122b45 100644 --- a/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/processor/XmlSignerProcessor.java +++ b/components/camel-xmlsecurity/src/main/java/org/apache/camel/component/xmlsecurity/processor/XmlSignerProcessor.java @@ -248,7 +248,7 @@ public class XmlSignerProcessor extends XmlSignatureProcessor { if (getConfiguration().getKeyAccessor() == null) { throw new XmlSignatureNoKeyException( - "Key selector is missing for XML signature generation. Specify a key selector in the configuration."); + "Key accessor is missing for XML signature generation. Specify a key accessor in the configuration."); } final KeySelector keySelector = getConfiguration().getKeyAccessor().getKeySelector(out); @@ -281,24 +281,31 @@ public class XmlSignerProcessor extends XmlSignatureProcessor { // parent only relevant for enveloped or detached signature Node parent = getParentOfSignature(out, node, contentReferenceUri, signatureType); + if (parent == null) { + // for enveloping signature, create new document + parent = XmlSignatureHelper.newDocumentBuilder(Boolean.TRUE).newDocument(); + } + lastParent = parent; + XmlSignatureProperties.Input input = new InputBuilder().contentDigestAlgorithm(getDigestAlgorithmUri()).keyInfo(keyInfo) .message(out).messageBodyNode(node).parent(parent).signatureAlgorithm(getConfiguration().getSignatureAlgorithm()) .signatureFactory(fac).signatureId(signatureId).contentReferenceUri(contentReferenceUri) - .signatureType(signatureType).build(); + .signatureType(signatureType) + .prefixForXmlSignatureNamespace(getConfiguration().getPrefixForXmlSignatureNamespace()).build(); XmlSignatureProperties.Output properties = getSignatureProperties(input); + + // the signature properties can overwrite the signature Id + if (properties != null && properties.getSignatureId() != null && !properties.getSignatureId().isEmpty()) { + signatureId = properties.getSignatureId(); + } + List<? extends XMLObject> objects = getObjects(input, properties); List<? extends Reference> refs = getReferences(input, properties, getKeyInfoId(keyInfo)); SignedInfo si = createSignedInfo(fac, refs); - if (parent == null) { - // for enveloping signature, create new document - parent = XmlSignatureHelper.newDocumentBuilder(Boolean.TRUE).newDocument(); - } - lastParent = parent; - DOMSignContext dsc = createAndConfigureSignContext(parent, keySelector); XMLSignature signature = fac.newXMLSignature(si, keyInfo, objects, signatureId, null); @@ -559,9 +566,10 @@ public class XmlSignerProcessor extends XmlSignatureProcessor { protected List<? extends Reference> getReferences(XmlSignatureProperties.Input input, XmlSignatureProperties.Output properties, String keyInfoId) throws Exception { //NOPMD + String referenceId = properties == null ? null : properties.getContentReferenceId(); // Create Reference with URI="#<objectId>" for enveloping signature, URI="" for enveloped signature, and URI = <value from configuration> for detached signature and the transforms Reference ref = createReference(input.getSignatureFactory(), input.getContentReferenceUri(), - getContentReferenceType(input.getMessage()), input.getSignatureType()); + getContentReferenceType(input.getMessage()), input.getSignatureType(), referenceId); Reference keyInfoRef = createKeyInfoReference(input.getSignatureFactory(), keyInfoId, input.getContentDigestAlgorithm()); int propsRefsSize = properties == null || properties.getReferences() == null || properties.getReferences().isEmpty() ? 0 @@ -638,11 +646,11 @@ public class XmlSignerProcessor extends XmlSignatureProcessor { } } - protected Reference createReference(XMLSignatureFactory fac, String uri, String type, SignatureType sigType) + protected Reference createReference(XMLSignatureFactory fac, String uri, String type, SignatureType sigType, String id) throws InvalidAlgorithmParameterException, XmlSignatureException { try { List<Transform> transforms = getTransforms(fac, sigType); - Reference ref = fac.newReference(uri, fac.newDigestMethod(getDigestAlgorithmUri(), null), transforms, type, null); + Reference ref = fac.newReference(uri, fac.newDigestMethod(getDigestAlgorithmUri(), null), transforms, type, id); return ref; } catch (NoSuchAlgorithmException e) { throw new XmlSignatureException("Wrong algorithm specified in the configuration.", e); @@ -880,6 +888,8 @@ public class XmlSignerProcessor extends XmlSignatureProcessor { private SignatureType signatureType; + private String prefixForXmlSignatureNamespace; + public InputBuilder signatureFactory(XMLSignatureFactory signatureFactory) { this.signatureFactory = signatureFactory; return this; @@ -930,6 +940,11 @@ public class XmlSignerProcessor extends XmlSignatureProcessor { return this; } + public InputBuilder prefixForXmlSignatureNamespace(String prefixForXmlSignatureNamespace) { + this.prefixForXmlSignatureNamespace = prefixForXmlSignatureNamespace; + return this; + } + public XmlSignatureProperties.Input build() { return new XmlSignatureProperties.Input() { @@ -983,6 +998,11 @@ public class XmlSignerProcessor extends XmlSignatureProcessor { return signatureType; } + @Override + public String getPrefixForXmlSignatureNamespace() { + return prefixForXmlSignatureNamespace; + } + }; } http://git-wip-us.apache.org/repos/asf/camel/blob/23688d3d/components/camel-xmlsecurity/src/test/java/org/apache/camel/component/xmlsecurity/SpringXmlSignatureTest.java ---------------------------------------------------------------------- diff --git a/components/camel-xmlsecurity/src/test/java/org/apache/camel/component/xmlsecurity/SpringXmlSignatureTest.java b/components/camel-xmlsecurity/src/test/java/org/apache/camel/component/xmlsecurity/SpringXmlSignatureTest.java index f3f0957..a0969ce 100644 --- a/components/camel-xmlsecurity/src/test/java/org/apache/camel/component/xmlsecurity/SpringXmlSignatureTest.java +++ b/components/camel-xmlsecurity/src/test/java/org/apache/camel/component/xmlsecurity/SpringXmlSignatureTest.java @@ -16,15 +16,24 @@ */ package org.apache.camel.component.xmlsecurity; +import java.io.ByteArrayInputStream; import java.security.KeyPair; +import java.util.Map; import javax.xml.crypto.KeySelector; +import org.w3c.dom.Document; + import org.apache.camel.CamelContext; +import org.apache.camel.Message; import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; import org.apache.camel.component.xmlsecurity.api.KeyAccessor; +import org.apache.camel.component.xmlsecurity.api.XmlSignatureHelper; import org.apache.camel.impl.JndiRegistry; import org.apache.camel.spring.SpringCamelContext; +import org.junit.Test; + public class SpringXmlSignatureTest extends XmlSignatureTest { @@ -96,4 +105,23 @@ public class SpringXmlSignatureTest extends XmlSignatureTest { String getSignerEndpointURIEnveloping() { return "xmlsecurity:sign://enveloping?keyAccessor=#accessorRsa"; } + + @Test + public void xades() throws Exception { + MockEndpoint mock = getMockEndpoint("mock:result"); + mock.expectedMessageCount(1); + sendBody("direct:xades", payload); + assertMockEndpointsSatisfied(); + + Message message = getMessage(mock); + byte[] body = message.getBody(byte[].class); + Document doc = XmlSignatureHelper.newDocumentBuilder(true).parse(new ByteArrayInputStream(body)); + Map<String, String> prefix2Ns = XAdESSignaturePropertiesTest.getPrefix2NamespaceMap(); + prefix2Ns.put("t", "http://test.com/"); + XAdESSignaturePropertiesTest + .checkXpath( + doc, + "/ds:Signature/ds:Object/etsi:QualifyingProperties/etsi:SignedProperties/etsi:SignedSignatureProperties/etsi:SignerRole/etsi:ClaimedRoles/etsi:ClaimedRole/t:test", + prefix2Ns, "test"); + } } \ No newline at end of file
