Skip to content

Commit 8d9571f

Browse files
authored
Merge pull request #2 from Hinaser/patch-for-non-exclusive-c14n
Patch for non exclusive c14n
2 parents e8baf4a + 2450505 commit 8d9571f

File tree

5 files changed

+385
-15
lines changed

5 files changed

+385
-15
lines changed

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,30 @@ Note:
151151
The xml-crypto api requires you to supply it separately the xml signature ("<Signature>...</Signature>", in loadSignature) and the signed xml (in checkSignature). The signed xml may or may not contain the signature in it, but you are still required to supply the signature separately.
152152

153153

154+
### Caring for Implicit transform
155+
If you fail to verify signed XML, then one possible cause is that there are some hidden implicit transforms(#).
156+
(#) Normalizing XML document to be verified. i.e. remove extra space within a tag, sorting attributes, importing namespace declared in ancestor nodes, etc.
157+
158+
The reason for these implicit transform might come from [complex xml signature specification](https://www.w3.org/TR/2002/REC-xmldsig-core-20020212),
159+
which makes XML developers confused and then leads to incorrect implementation for signing XML document.
160+
161+
If you keep failing verification, it is worth trying to guess such a hidden transform and specify it to the option as below:
162+
163+
```javascript
164+
var option = {implicitTransforms: ["http://www.w3.org/TR/2001/REC-xml-c14n-20010315"]}
165+
var sig = new SignedXml(null, option)
166+
sig.keyInfoProvider = new FileKeyInfo("client_public.pem")
167+
sig.loadSignature(signature)
168+
var res = sig.checkSignature(xml)
169+
```
170+
171+
You might find it difficult to guess such transforms, but there are typical transforms you can try.
172+
173+
- http://www.w3.org/TR/2001/REC-xml-c14n-20010315
174+
- http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments
175+
- http://www.w3.org/2001/10/xml-exc-c14n#
176+
- http://www.w3.org/2001/10/xml-exc-c14n#WithComments
177+
154178
## API
155179

156180
### xpath

lib/c14n-canonicalization.js

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,12 @@ C14nCanonicalization.prototype.renderAttrs = function(node, defaultNS) {
6464
* @param {Array} prefixesInScope. The prefixes defined on this node
6565
* parents which are a part of the output set
6666
* @param {String} defaultNs. The current default namespace
67+
* @param {String} defaultNsForPrefix.
68+
* @param {String} ancestorNamespaces - Import ancestor namespaces if it is specified
6769
* @return {String}
6870
* @api private
6971
*/
70-
C14nCanonicalization.prototype.renderNs = function(node, prefixesInScope, defaultNs, defaultNsForPrefix) {
72+
C14nCanonicalization.prototype.renderNs = function(node, prefixesInScope, defaultNs, defaultNsForPrefix, ancestorNamespaces) {
7173
var a, i, p, attr
7274
, res = []
7375
, newDefaultNs = defaultNs
@@ -107,6 +109,25 @@ C14nCanonicalization.prototype.renderNs = function(node, prefixesInScope, defaul
107109
}
108110
}
109111
}
112+
113+
if(Array.isArray(ancestorNamespaces) && ancestorNamespaces.length > 0){
114+
// Remove namespaces which are already present in nsListToRender
115+
for(var p1 in ancestorNamespaces){
116+
if(!ancestorNamespaces.hasOwnProperty(p1)) continue;
117+
var alreadyListed = false;
118+
for(var p2 in nsListToRender){
119+
if(nsListToRender[p2].prefix === ancestorNamespaces[p1].prefix
120+
&& nsListToRender[p2].namespaceURI === ancestorNamespaces[p1].namespaceURI)
121+
{
122+
alreadyListed = true;
123+
}
124+
}
125+
126+
if(!alreadyListed){
127+
nsListToRender.push(ancestorNamespaces[p1]);
128+
}
129+
}
130+
}
110131

111132
nsListToRender.sort(this.nsCompare);
112133

@@ -121,18 +142,18 @@ C14nCanonicalization.prototype.renderNs = function(node, prefixesInScope, defaul
121142
return {"rendered": res.join(""), "newDefaultNs": newDefaultNs};
122143
};
123144

124-
C14nCanonicalization.prototype.processInner = function(node, prefixesInScope, defaultNs, defaultNsForPrefix) {
145+
C14nCanonicalization.prototype.processInner = function(node, prefixesInScope, defaultNs, defaultNsForPrefix, ancestorNamespaces) {
125146

126147
if (node.nodeType === 8) { return this.renderComment(node); }
127148
if (node.data) { return utils.encodeSpecialCharactersInText(node.data); }
128149

129150
var i, pfxCopy
130-
, ns = this.renderNs(node, prefixesInScope, defaultNs, defaultNsForPrefix)
151+
, ns = this.renderNs(node, prefixesInScope, defaultNs, defaultNsForPrefix, ancestorNamespaces)
131152
, res = ["<", node.tagName, ns.rendered, this.renderAttrs(node, ns.newDefaultNs), ">"];
132153

133154
for (i = 0; i < node.childNodes.length; ++i) {
134155
pfxCopy = prefixesInScope.slice(0);
135-
res.push(this.processInner(node.childNodes[i], pfxCopy, ns.newDefaultNs, defaultNsForPrefix));
156+
res.push(this.processInner(node.childNodes[i], pfxCopy, ns.newDefaultNs, defaultNsForPrefix, []));
136157
}
137158

138159
res.push("</", node.tagName, ">");
@@ -185,8 +206,9 @@ C14nCanonicalization.prototype.process = function(node, options) {
185206
options = options || {};
186207
var defaultNs = options.defaultNs || "";
187208
var defaultNsForPrefix = options.defaultNsForPrefix || {};
209+
var ancestorNamespaces = options.ancestorNamespaces || [];
188210

189-
var res = this.processInner(node, [], defaultNs, defaultNsForPrefix);
211+
var res = this.processInner(node, [], defaultNs, defaultNsForPrefix, ancestorNamespaces);
190212
return res;
191213
};
192214

lib/signed-xml.js

Lines changed: 151 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ var select = require('xpath.js')
66
, EnvelopedSignature = require('./enveloped-signature').EnvelopedSignature
77
, crypto = require('crypto')
88
, fs = require('fs')
9+
, xpath = require('xpath.js')
910

1011
exports.SignedXml = SignedXml
1112
exports.FileKeyInfo = FileKeyInfo
@@ -197,6 +198,88 @@ function HMACSHA1() {
197198
};
198199
}
199200

201+
202+
203+
/**
204+
* Extract ancestor namespaces in order to import it to root of document subset
205+
* which is being canonicalized for non-exclusive c14n.
206+
*
207+
* @param {object} doc - Usually a product from `new DOMParser().parseFromString()`
208+
* @param {string} docSubsetXpath - xpath query to get document subset being canonicalized
209+
* @returns {Array} i.e. [{prefix: "saml", namespaceURI: "urn:oasis:names:tc:SAML:2.0:assertion"}]
210+
*/
211+
function findAncestorNs(doc, docSubsetXpath){
212+
var docSubset = xpath(doc, docSubsetXpath);
213+
214+
if(!Array.isArray(docSubset) || docSubset.length < 1){
215+
return [];
216+
}
217+
218+
// Remove duplicate on ancestor namespace
219+
var ancestorNs = collectAncestorNamespaces(docSubset[0]);
220+
var ancestorNsWithoutDuplicate = [];
221+
for(var i=0;i<ancestorNs.length;i++){
222+
var notOnTheList = true;
223+
for(var v in ancestorNsWithoutDuplicate){
224+
if(ancestorNsWithoutDuplicate[v].prefix === ancestorNs[i].prefix){
225+
notOnTheList = false;
226+
break;
227+
}
228+
}
229+
230+
if(notOnTheList){
231+
ancestorNsWithoutDuplicate.push(ancestorNs[i]);
232+
}
233+
}
234+
235+
// Remove namespaces which are already declared in the subset with the same prefix
236+
var returningNs = [];
237+
var subsetAttributes = docSubset[0].attributes;
238+
for(var j=0;j<ancestorNsWithoutDuplicate.length;j++){
239+
var isUnique = true;
240+
for(var k=0;k<subsetAttributes.length;k++){
241+
var nodeName = subsetAttributes[k].nodeName;
242+
if(nodeName.search(/^xmlns:/) === -1) continue;
243+
var prefix = nodeName.replace(/^xmlns:/, "");
244+
if(ancestorNsWithoutDuplicate[j].prefix === prefix){
245+
isUnique = false;
246+
break;
247+
}
248+
}
249+
250+
if(isUnique){
251+
returningNs.push(ancestorNsWithoutDuplicate[j]);
252+
}
253+
}
254+
255+
return returningNs;
256+
}
257+
258+
259+
260+
function collectAncestorNamespaces(node, nsArray){
261+
if(!nsArray){
262+
nsArray = [];
263+
}
264+
265+
var parent = node.parentNode;
266+
267+
if(!parent){
268+
return nsArray;
269+
}
270+
271+
if(parent.attributes && parent.attributes.length > 0){
272+
for(var i=0;i<parent.attributes.length;i++){
273+
var attr = parent.attributes[i];
274+
if(attr && attr.nodeName && attr.nodeName.search(/^xmlns:/) !== -1){
275+
nsArray.push({prefix: attr.nodeName.replace(/^xmlns:/, ""), namespaceURI: attr.nodeValue})
276+
}
277+
}
278+
}
279+
280+
return collectAncestorNamespaces(parent, nsArray);
281+
}
282+
200283
/**
201284
* Xml signature implementation
202285
*
@@ -221,6 +304,7 @@ function SignedXml(idMode, options) {
221304
this.keyInfo = null
222305
this.idAttributes = [ 'Id', 'ID', 'id' ];
223306
if (this.options.idAttribute) this.idAttributes.splice(0, 0, this.options.idAttribute);
307+
this.implicitTransforms = this.options.implicitTransforms || [];
224308
}
225309

226310
SignedXml.CanonicalizationAlgorithms = {
@@ -248,6 +332,8 @@ SignedXml.defaultNsForPrefix = {
248332
ds: 'http://www.w3.org/2000/09/xmldsig#'
249333
};
250334

335+
SignedXml.findAncestorNs = findAncestorNs;
336+
251337
SignedXml.prototype.checkSignature = function(xml) {
252338
this.validationErrors = []
253339
this.signedXml = xml
@@ -264,18 +350,37 @@ SignedXml.prototype.checkSignature = function(xml) {
264350
if (!this.validateReferences(doc)) {
265351
return false;
266352
}
267-
268-
if (!this.validateSignatureValue()) {
353+
354+
if (!this.validateSignatureValue(doc)) {
269355
return false;
270356
}
271357

272358
return true
273359
}
274360

275-
SignedXml.prototype.validateSignatureValue = function() {
361+
SignedXml.prototype.validateSignatureValue = function(doc) {
276362
var signedInfo = utils.findChilds(this.signatureNode, "SignedInfo")
277363
if (signedInfo.length==0) throw new Error("could not find SignedInfo element in the message")
278-
var signedInfoCanon = this.getCanonXml([this.canonicalizationAlgorithm], signedInfo[0])
364+
365+
/**
366+
* When canonicalization algorithm is non-exclusive, search for ancestor namespaces
367+
* before validating signature.
368+
*/
369+
var ancestorNamespaces = [];
370+
if(this.canonicalizationAlgorithm === "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"
371+
|| this.canonicalizationAlgorithm === "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments")
372+
{
373+
if(!doc || typeof(doc) !== "object"){
374+
throw new Error("When canonicalization method is non-exclusive, whole xml dom must be provided as an argument");
375+
}
376+
377+
ancestorNamespaces = findAncestorNs(doc, "//*[local-name()='SignedInfo']");
378+
}
379+
380+
var c14nOptions = {
381+
ancestorNamespaces: ancestorNamespaces
382+
};
383+
var signedInfoCanon = this.getCanonXml([this.canonicalizationAlgorithm], signedInfo[0], c14nOptions)
279384
var signer = this.findSignatureAlgorithm(this.signatureAlgorithm)
280385
var res = signer.verifySignature(signedInfoCanon, this.signingKey, this.signatureValue)
281386
if (!res) this.validationErrors.push("invalid signature: the signature value " +
@@ -310,6 +415,7 @@ SignedXml.prototype.validateReferences = function(doc) {
310415

311416
var uri = ref.uri[0]=="#" ? ref.uri.substring(1) : ref.uri
312417
var elem = [];
418+
var elemXpath;
313419

314420
if (uri=="") {
315421
elem = select(doc, "//*")
@@ -322,11 +428,13 @@ SignedXml.prototype.validateReferences = function(doc) {
322428
var num_elements_for_id = 0;
323429
for (var index in this.idAttributes) {
324430
if (!this.idAttributes.hasOwnProperty(index)) continue;
325-
var tmp_elem = select(doc, "//*[@*[local-name(.)='" + this.idAttributes[index] + "']='" + uri + "']")
431+
var tmp_elemXpath = "//*[@*[local-name(.)='" + this.idAttributes[index] + "']='" + uri + "']";
432+
var tmp_elem = select(doc, tmp_elemXpath)
326433
num_elements_for_id += tmp_elem.length;
327434
if (tmp_elem.length > 0) {
328435
elem = tmp_elem;
329-
};
436+
elemXpath = tmp_elemXpath;
437+
}
330438
}
331439
if (num_elements_for_id > 1) {
332440
throw new Error('Cannot validate a document which contains multiple elements with the ' +
@@ -340,7 +448,34 @@ SignedXml.prototype.validateReferences = function(doc) {
340448
ref.uri + " but could not find such element in the xml")
341449
return false
342450
}
343-
var canonXml = this.getCanonXml(ref.transforms, elem[0], { inclusiveNamespacesPrefixList: ref.inclusiveNamespacesPrefixList });
451+
452+
/**
453+
* When canonicalization algorithm is non-exclusive, search for ancestor namespaces
454+
* before validating references.
455+
*/
456+
if(Array.isArray(ref.transforms)){
457+
var hasNonExcC14nTransform = false;
458+
for(var t in ref.transforms){
459+
if(!ref.transforms.hasOwnProperty(t)) continue;
460+
461+
if(ref.transforms[t] === "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"
462+
|| ref.transforms[t] === "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments")
463+
{
464+
hasNonExcC14nTransform = true;
465+
break;
466+
}
467+
}
468+
469+
if(hasNonExcC14nTransform){
470+
ref.ancestorNamespaces = findAncestorNs(doc, elemXpath);
471+
}
472+
}
473+
474+
var c14nOptions = {
475+
inclusiveNamespacesPrefixList: ref.inclusiveNamespacesPrefixList,
476+
ancestorNamespaces: ref.ancestorNamespaces
477+
};
478+
var canonXml = this.getCanonXml(ref.transforms, elem[0], c14nOptions);
344479

345480
var hash = this.findHashAlgorithm(ref.digestAlgorithm)
346481
var digest = hash.getHash(canonXml)
@@ -428,8 +563,15 @@ SignedXml.prototype.loadReference = function(ref) {
428563
}
429564
}
430565

566+
var hasImplicitTransforms = (Array.isArray(this.implicitTransforms) && this.implicitTransforms.length > 0);
567+
if(hasImplicitTransforms){
568+
this.implicitTransforms.forEach(function(t){
569+
transforms.push(t);
570+
});
571+
}
572+
431573
//***workaround for validating windows mobile store signatures - it uses c14n but does not state it in the transforms
432-
if (transforms.length==1 && transforms[0]=="http://www.w3.org/2000/09/xmldsig#enveloped-signature")
574+
if (!hasImplicitTransforms && transforms.length==1 && transforms[0]=="http://www.w3.org/2000/09/xmldsig#enveloped-signature")
433575
transforms.push("http://www.w3.org/2001/10/xml-exc-c14n#")
434576

435577
this.addReference(null, transforms, digestAlgo, utils.findAttr(ref, "URI").value, digestValue, inclusiveNamespacesPrefixList, false)
@@ -610,7 +752,7 @@ SignedXml.prototype.getCanonXml = function(transforms, node, options) {
610752
options = options || {};
611753
options.defaultNsForPrefix = options.defaultNsForPrefix || SignedXml.defaultNsForPrefix;
612754

613-
var canonXml = node
755+
var canonXml = node.cloneNode(true) // Deep clone
614756

615757
for (var t in transforms) {
616758
if (!transforms.hasOwnProperty(t)) continue;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,6 @@
2929
],
3030
"license": "MIT",
3131
"scripts": {
32-
"test": "nodeunit ./test/canonicalization-unit-tests.js ./test/c14nWithComments-unit-tests.js ./test/signature-unit-tests.js ./test/saml-response-test.js ./test/signature-integration-tests.js ./test/document-test.js ./test/wsfed-metadata-test.js ./test/hmac-tests.js"
32+
"test": "nodeunit ./test/canonicalization-unit-tests.js ./test/c14nWithComments-unit-tests.js ./test/signature-unit-tests.js ./test/saml-response-test.js ./test/signature-integration-tests.js ./test/document-test.js ./test/wsfed-metadata-test.js ./test/hmac-tests.js ./test/c14n-non-exclusive-unit-test.js"
3333
}
3434
}

0 commit comments

Comments
 (0)