Skip to content

Commit 5aa48bb

Browse files
authored
Merge pull request #161 from datagovsg/master
Merge changes from datagovsg fork
2 parents 6e22854 + 342af8d commit 5aa48bb

File tree

7 files changed

+431
-57
lines changed

7 files changed

+431
-57
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: 29 additions & 7 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
@@ -93,8 +95,8 @@ C14nCanonicalization.prototype.renderNs = function(node, prefixesInScope, defaul
9395
attr = node.attributes[i];
9496

9597
//handle all prefixed attributes that are included in the prefix list and where
96-
//the prefix is not defined already
97-
if (attr.prefix && prefixesInScope.indexOf(attr.localName) === -1) {
98+
//the prefix is not defined already. New prefixes can only be defined by `xmlns:`.
99+
if (attr.prefix === "xmlns" && prefixesInScope.indexOf(attr.localName) === -1) {
98100
nsListToRender.push({"prefix": attr.localName, "namespaceURI": attr.value});
99101
prefixesInScope.push(attr.localName);
100102
}
@@ -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/enveloped-signature.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ function EnvelopedSignature() {
66
}
77

88
EnvelopedSignature.prototype.process = function (node) {
9-
var signature = xpath(node, ".//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']")[0];
9+
var signature = xpath(node, "./*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']")[0];
1010
if (signature) signature.parentNode.removeChild(signature);
1111
return node;
1212
};

lib/signed-xml.js

Lines changed: 162 additions & 11 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,9 +563,25 @@ SignedXml.prototype.loadReference = function(ref) {
428563
}
429564
}
430565

431-
//***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")
433-
transforms.push("http://www.w3.org/2001/10/xml-exc-c14n#")
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+
573+
/**
574+
* DigestMethods take an octet stream rather than a node set. If the output of the last transform is a node set, we
575+
* need to canonicalize the node set to an octet stream using non-exclusive canonicalization. If there are no
576+
* transforms, we need to canonicalize because URI dereferencing for a same-document reference will return a node-set.
577+
* See:
578+
* https://www.w3.org/TR/xmldsig-core1/#sec-DigestMethod
579+
* https://www.w3.org/TR/xmldsig-core1/#sec-ReferenceProcessingModel
580+
* https://www.w3.org/TR/xmldsig-core1/#sec-Same-Document
581+
*/
582+
if (transforms.length === 0 || transforms[transforms.length - 1] === "http://www.w3.org/2000/09/xmldsig#enveloped-signature") {
583+
transforms.push("http://www.w3.org/TR/2001/REC-xml-c14n-20010315")
584+
}
434585

435586
this.addReference(null, transforms, digestAlgo, utils.findAttr(ref, "URI").value, digestValue, inclusiveNamespacesPrefixList, false)
436587
}
@@ -610,7 +761,7 @@ SignedXml.prototype.getCanonXml = function(transforms, node, options) {
610761
options = options || {};
611762
options.defaultNsForPrefix = options.defaultNsForPrefix || SignedXml.defaultNsForPrefix;
612763

613-
var canonXml = node
764+
var canonXml = node.cloneNode(true) // Deep clone
614765

615766
for (var t in transforms) {
616767
if (!transforms.hasOwnProperty(t)) continue;

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "xml-crypto",
3-
"version": "0.10.1",
3+
"version": "0.10.2",
44
"description": "Xml digital signature and encryption library for Node.js",
55
"engines": {
66
"node": ">=0.4.0"
@@ -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)