-
Notifications
You must be signed in to change notification settings - Fork 4.9k
/
DefaultXmlSignature2Message.java
512 lines (468 loc) · 21.8 KB
/
DefaultXmlSignature2Message.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
/*
* 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.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.List;
import javax.xml.crypto.XMLStructure;
import javax.xml.crypto.dom.DOMStructure;
import javax.xml.crypto.dsig.Manifest;
import javax.xml.crypto.dsig.Reference;
import javax.xml.crypto.dsig.XMLObject;
import javax.xml.crypto.dsig.XMLSignature;
import javax.xml.crypto.dsig.XMLSignatureException;
import javax.xml.crypto.dsig.spec.XPathFilterParameterSpec;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.apache.camel.Message;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Maps the XML signature to a camel message. A output node is determined from the XML signature document via a node
* search and then serialized and set to the output message body.
* <p>
* There are three output node search types supported: "Default", "ElementName", and "XPath". All these search types
* support enveloped XML signature or enveloping XML signature.
* <p>
* <ul>
* <li>The "ElementName" search uses the local name and namespace specified in the search value to determine the output
* element from the XML signature document. With the input parameter 'RemoveSignatureElements", you can specify whether
* the signature elements should be removed from the resulting output document. This flag shall be used for enveloped
* XML signatures.
* <li>The "XPath" search uses an XPath expression to evaluate the output node. In this case the output node can be of
* type Element, TextNode, or Document. With the input parameter 'RemoveSignatureElements", you can specify whether the
* signature elements should be removed from the resulting output document. This flag shall be used for enveloped XML
* signatures.
* <li>The "Default" search is explained in more detail below.
* </ul>
* <p>
* Default Output Node Search:
* <ul>
* In the enveloped XML signature case, the XML document without the signature part is returned in the message body.
* <p>
* In the enveloping XML signature case, the message body is determined from a referenced Object element in the
* following way:
* <ul>
* <li>Only same document references are taken into account (URI must start with '#').
* <li>Also indirect same document references to an object via manifest are taken into account.
* <li>The resulting number of object references must be 1.
* <li>The referenced object must contain exactly 1 {@link DOMStructure}.
* <li>The node of the DOMStructure is serialized to a byte array and added as body to the message.
* </ul>
* This does mean that the enveloping XML signature must have either the structure
*
* <pre>
* {@code
* <Signature>
* <SignedInfo>
* <Reference URI="#object"/>
* <!-- further references possible but they must not point to an Object or Manifest containing an object reference -->
* ...
* </SignedInfo>
*
* <Object Id="object">
* <!-- contains the DOM node which should be extracted to the message body -->
* <Object>
* <!-- further object elements possible which are not referenced-->
* ...
* (<KeyInfo>)?
* </Signature>
* }
* </pre>
*
* or the structure
*
* <pre>
* {@code
* <Signature>
* <SignedInfo>
* <Reference URI="#manifest"/>
* <!-- further references are possible but they must not point to an Object or other manifest containing an object reference -->
* ...
* </SignedInfo>
*
* <Object >
* <Manifest Id="manifest">
* <Reference URI=#object/>
* </Manifest>
* </Objet>
* <Object Id="object">
* <!-- contains the DOM node which should be extracted to the message body -->
* </Object>
* <!-- further object elements possible which are not referenced -->
* ...
* (<KeyInfo>)?
* </Signature>
* }
* </pre>
*
* </ul>
*/
public class DefaultXmlSignature2Message implements XmlSignature2Message {
/**
* Search type 'Default' for determining the output node.
*
*/
public static final String OUTPUT_NODE_SEARCH_TYPE_DEFAULT = "Default";
/**
* Search type 'ElementName' for determining the output element.
*
*/
public static final String OUTPUT_NODE_SEARCH_TYPE_ELEMENT_NAME = "ElementName";
/**
* Search type 'XPath' for determining the output node. Search value must be of type
* {@link XPathFilterParameterSpec}.
*
*/
public static final String OUTPUT_NODE_SEARCH_TYPE_XPATH = "XPath";
private static final Logger LOG = LoggerFactory.getLogger(DefaultXmlSignature2Message.class);
@Override
public void mapToMessage(Input input, Message output) throws Exception {
Node node;
boolean removeSignatureElements = false;
if (OUTPUT_NODE_SEARCH_TYPE_DEFAULT.equals(input.getOutputNodeSearchType())) {
LOG.debug("Searching for output node via default search");
if (isEnveloping(input)) {
node = getNodeForMessageBodyInEnvelopingCase(input);
} else {
// enveloped or detached XML signature --> remove signature element
node = input.getMessageBodyDocument().getDocumentElement();
removeSignatureElements = true;
}
} else if (OUTPUT_NODE_SEARCH_TYPE_ELEMENT_NAME.equals(input.getOutputNodeSearchType())) {
node = getOutputElementViaLocalNameAndNamespace(input);
} else if (OUTPUT_NODE_SEARCH_TYPE_XPATH.equals(input.getOutputNodeSearchType())) {
node = getOutputNodeViaXPath(input);
} else {
throw new XmlSignatureException(
String.format("Wrong configuration: The output node search type %s is not supported.",
input.getOutputNodeSearchType()));
}
LOG.debug("Output node with local name {} and namespace {} found", node.getLocalName(), node.getNamespaceURI());
if (!removeSignatureElements) {
removeSignatureElements = input.getRemoveSignatureElements() != null && input.getRemoveSignatureElements();
}
if (removeSignatureElements) {
removeSignatureElements(node);
}
transformNodeToByteArrayAndSetToOutputMessage(input, output, node);
}
protected void transformNodeToByteArrayAndSetToOutputMessage(Input input, Message output, Node node)
throws Exception {
ByteArrayOutputStream os = new ByteArrayOutputStream();
XmlSignatureHelper.transformToOutputStream(node, os, omitXmlDeclaration(output, input), input.getOutputXmlEncoding());
output.setBody(os.toByteArray());
if (input.getOutputXmlEncoding() != null) {
output.setHeader(XmlSignatureConstants.CHARSET_NAME, input.getOutputXmlEncoding());
}
}
protected Node getOutputNodeViaXPath(Input input) throws Exception {
checkSearchValueNotNull(input);
checkSearchValueOfType(XPathFilterParameterSpec.class, input);
XPathFilterParameterSpec xpathFilter = (XPathFilterParameterSpec) input.getOutputNodeSearch();
XPathExpression expr = XmlSignatureHelper.getXPathExpression(xpathFilter);
NodeList nodes = (NodeList) expr.evaluate(input.getMessageBodyDocument(), XPathConstants.NODESET);
if (nodes == null || nodes.getLength() == 0) {
throw new XmlSignatureException(
String.format(
"Cannot extract root node for the output document from the XML signature document. No node found for XPATH %s as specified in the output node search.",
xpathFilter.getXPath()));
}
if (nodes.getLength() > 1) {
throw new XmlSignatureException(
String.format(
"Cannot extract root node for the output document from the XML signature document. XPATH %s as specified in the output node search results into more than one child.",
xpathFilter.getXPath()));
}
Node result = nodes.item(0);
if (Node.ELEMENT_NODE == result.getNodeType() || Node.TEXT_NODE == result.getNodeType()
|| Node.DOCUMENT_NODE == result.getNodeType()) {
return result;
}
throw new XmlSignatureException(
String.format("Cannot extract root node for the output document from the XML signature document. "
+ "XPATH %s as specified in the output node search results into a node which has the wrong type.",
xpathFilter.getXPath()));
}
protected Node getOutputElementViaLocalNameAndNamespace(Input input) throws Exception {
String search = getNonEmptyStringSearchValue(input);
String namespace;
String localName;
if ('{' == search.charAt(0)) {
// namespace
int index = search.indexOf('}');
if (index < 1) {
throw new XmlSignatureException(
String.format(
"Wrong configuration: Value %s for the output node search %s has wrong format. "
+ "Value must have the form '{<namespace>}<element local name>' or '<element local name>' if no the element has no namespace.",
search, input.getOutputNodeSearchType()));
}
namespace = search.substring(1, index);
if (search.length() < index + 1) {
throw new XmlSignatureException(
String.format(
"Wrong configuration: Value %s for the output node search %s has wrong format. "
+ "Value must have the form '{<namespace>}<element local name>' or '<element local name>' if no the element has no namespace.",
search, input.getOutputNodeSearchType()));
}
localName = search.substring(index + 1);
} else {
namespace = null;
localName = search;
}
NodeList nodeList = input.getMessageBodyDocument().getElementsByTagNameNS(namespace, localName);
if (nodeList.getLength() == 0) {
throw new XmlSignatureException(
String.format(
"Cannot extract root element for the output document from the XML signature document. Element with local name %s and namespace %s does not exist.",
namespace, localName));
}
if (nodeList.getLength() > 1) {
throw new XmlSignatureException(
String.format(
"Cannot extract root element for the output document from the XML signature document. More than one element found with local name %s and namespace %s.",
namespace, localName));
}
return nodeList.item(0);
}
protected String getNonEmptyStringSearchValue(Input input) throws Exception {
checkSearchValueNotNull(input);
checkSearchValueOfType(String.class, input);
String search = (String) input.getOutputNodeSearch();
checkStringSarchValueNotEmpty(search, input.getOutputNodeSearchType());
return search;
}
protected void checkSearchValueOfType(Class<?> cl, Input input) throws Exception {
if (!cl.isAssignableFrom(input.getOutputNodeSearch().getClass())) {
throw new XMLSignatureException(
String.format(
"Wrong configuration: Search value is of class %s, the output node search %s requires class %s.",
input
.getOutputNodeSearch().getClass().getName(),
input.getOutputNodeSearchType(), cl.getName()));
}
}
protected void checkStringSarchValueNotEmpty(String searchValue, String outputNodeSearchType) throws Exception {
if (searchValue.isEmpty()) {
throw new XMLSignatureException(
String.format("Wrong configuration: Value for output node search %s is empty.",
outputNodeSearchType));
}
}
protected void checkSearchValueNotNull(Input input) throws Exception {
LOG.debug("Searching for output element with search value '{}' and sarch type {}", input.getOutputNodeSearch(),
input.getOutputNodeSearchType());
if (input.getOutputNodeSearch() == null) {
throw new XMLSignatureException(
String.format("Wrong configuration: Value is missing for output node search %s.",
input.getOutputNodeSearchType()));
}
}
protected Node getNodeForMessageBodyInEnvelopingCase(Input input) throws Exception {
Node node;
List<Reference> relevantReferences = getReferencesForMessageMapping(input);
List<XMLObject> relevantObjects = getObjectsForMessageMapping(input);
DOMStructure domStruc = getDomStructureForMessageBody(relevantReferences, relevantObjects);
node = domStruc.getNode();
return node;
}
/**
* Removes the Signature elements from the document.
*/
protected void removeSignatureElements(Node node) {
Document doc = XmlSignatureHelper.getDocument(node);
NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
List<Node> nodesToBeRemoved = new ArrayList<>(nl.getLength());
for (int i = 0; i < nl.getLength(); i++) {
// you cannot remove the nodes within this loop, because nl list would change
nodesToBeRemoved.add(nl.item(i));
}
for (Node n : nodesToBeRemoved) {
Node parent = n.getParentNode();
if (parent != null) {
parent.removeChild(n);
}
}
}
/**
* Checks whether the XML document has as root element the signature element.
*
* @param input XML signature input
* @return <code>true</code> if the root element of the xml signature document is the signature element;
* otherwise <code>false</code>
* @throws Exception
*/
protected boolean isEnveloping(Input input) {
Element el = input.getMessageBodyDocument().getDocumentElement();
if ("Signature".equals(el.getLocalName()) && XMLSignature.XMLNS.equals(el.getNamespaceURI())) {
return true;
}
return false;
}
protected Boolean omitXmlDeclaration(Message message, Input input) {
Boolean omitXmlDeclaration = message.getHeader(XmlSignatureConstants.HEADER_OMIT_XML_DECLARATION, Boolean.class);
if (omitXmlDeclaration == null) {
omitXmlDeclaration = input.omitXmlDeclaration();
}
if (omitXmlDeclaration == null) {
omitXmlDeclaration = Boolean.FALSE;
}
return omitXmlDeclaration;
}
/**
* Returns the references whose referenced objects are taken into account for the message body. This message you can
* use to filter the relevant references from the references provided by the input parameter.
*
*
* @param input references and objects
* @return relevant references for the mapping to the camel message
* @throws Exception if an error occurs
*/
protected List<Reference> getReferencesForMessageMapping(Input input) throws Exception {
return input.getReferences();
}
/**
* Returns the objects which must be taken into account for the mapping to the camel message.
*
* @param input references and objects
* @return relevant objects for the mapping to camel message
* @throws Exception if an error occurs
*/
protected List<XMLObject> getObjectsForMessageMapping(Input input) throws Exception {
return input.getObjects();
}
/**
* Returns the DOM structure which is transformed to a byte array and set to the camel message body.
*
* @param relevantReferences input from method {@link #getReferencesForMessageMapping(ReferencesAndObjects)}
* @param relevantObjects input from method {@link #getObjectsForMessageMapping(ReferencesAndObjects)}
* @return dom structure
* @throws Exception if an error occurs
*/
protected DOMStructure getDomStructureForMessageBody(List<Reference> relevantReferences, List<XMLObject> relevantObjects)
throws Exception {
List<XMLObject> referencedObjects = getReferencedSameDocumentObjects(relevantReferences, relevantObjects);
if (referencedObjects.isEmpty()) {
throw new XmlSignatureException(
"Unsupported XML signature document: Content object not found in the enveloping XML signature.");
}
if (referencedObjects.size() > 1) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < referencedObjects.size(); i++) {
XMLObject xmlOb = referencedObjects.get(i);
sb.append(xmlOb.getId());
if (i < referencedObjects.size() - 1) {
sb.append(", ");
}
}
throw new XmlSignatureException(
String.format(
"Unsupported XML signature document: More than one content objects found. Object IDs: %s",
sb.toString()));
}
@SuppressWarnings("unchecked")
List<XMLStructure> structures = referencedObjects.get(0).getContent();
if (structures.isEmpty()) {
throw new XmlSignatureException(
"Unsupported XML signature: XML signature is not enveloping; content not found in XML signature: structure list is empty.");
}
if (structures.size() > 1) {
throw new XmlSignatureException(
"Unsupported XML signature: more than one structure elements in referenced content object.");
}
XMLStructure structure = structures.get(0);
// only dom currently supported
DOMStructure domStruc = (DOMStructure) structure;
return domStruc;
}
protected List<XMLObject> getReferencedSameDocumentObjects(
List<Reference> relevantReferences, List<XMLObject> relevantObjects) {
List<XMLObject> referencedObjects = new ArrayList<>(1);
for (Reference ref : relevantReferences) {
String refUri = getSameDocumentReferenceUri(ref);
if (refUri == null) {
continue;
}
XMLObject referencedOb = getReferencedObject(relevantObjects, refUri);
if (referencedOb != null) {
referencedObjects.add(referencedOb);
continue;
}
// content could also be indirectly referenced via manifest
addManifestReferencedObjects(relevantObjects, referencedObjects, refUri);
}
return referencedObjects;
}
@SuppressWarnings("unchecked")
protected void addManifestReferencedObjects(
List<XMLObject> allObjects, List<XMLObject> referencedObjects, String manifestId) {
Manifest manifest = getReferencedManifest(allObjects, manifestId);
if (manifest == null) {
return;
}
for (Reference manifestRef : manifest.getReferences()) {
String manifestRefUri = getSameDocumentReferenceUri(manifestRef);
if (manifestRefUri == null) {
continue;
}
XMLObject manifestReferencedOb = getReferencedObject(allObjects, manifestRefUri);
if (manifestReferencedOb != null) {
referencedObjects.add(manifestReferencedOb);
}
}
}
protected String getSameDocumentReferenceUri(Reference ref) {
String refUri = ref.getURI();
if (refUri == null) {
LOG.warn("Ignoring reference {} which has no URI", ref);
return null;
}
if (!refUri.startsWith("#")) {
LOG.warn("Ignoring non-same document reference {}", refUri);
return null;
}
return refUri.substring(1);
}
protected Manifest getReferencedManifest(List<XMLObject> objects, String id) {
for (XMLObject xo : objects) {
@SuppressWarnings("unchecked")
List<XMLStructure> content = xo.getContent();
for (XMLStructure xs : content) {
if (xs instanceof Manifest) {
Manifest man = (Manifest) xs;
if (id.equals(man.getId())) {
return man;
}
}
}
}
return null;
}
protected XMLObject getReferencedObject(List<XMLObject> objects, String id) {
for (XMLObject ob : objects) {
if (id.equals(ob.getId())) {
return ob;
}
}
return null;
}
}