@@ -6,6 +6,7 @@ var select = require('xpath.js')
6
6
, EnvelopedSignature = require ( './enveloped-signature' ) . EnvelopedSignature
7
7
, crypto = require ( 'crypto' )
8
8
, fs = require ( 'fs' )
9
+ , xpath = require ( 'xpath.js' )
9
10
10
11
exports . SignedXml = SignedXml
11
12
exports . FileKeyInfo = FileKeyInfo
@@ -197,6 +198,88 @@ function HMACSHA1() {
197
198
} ;
198
199
}
199
200
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 ( / ^ x m l n s : / ) === - 1 ) continue ;
243
+ var prefix = nodeName . replace ( / ^ x m l n s : / , "" ) ;
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 ( / ^ x m l n s : / ) !== - 1 ) {
275
+ nsArray . push ( { prefix : attr . nodeName . replace ( / ^ x m l n s : / , "" ) , namespaceURI : attr . nodeValue } )
276
+ }
277
+ }
278
+ }
279
+
280
+ return collectAncestorNamespaces ( parent , nsArray ) ;
281
+ }
282
+
200
283
/**
201
284
* Xml signature implementation
202
285
*
@@ -221,6 +304,7 @@ function SignedXml(idMode, options) {
221
304
this . keyInfo = null
222
305
this . idAttributes = [ 'Id' , 'ID' , 'id' ] ;
223
306
if ( this . options . idAttribute ) this . idAttributes . splice ( 0 , 0 , this . options . idAttribute ) ;
307
+ this . implicitTransforms = this . options . implicitTransforms || [ ] ;
224
308
}
225
309
226
310
SignedXml . CanonicalizationAlgorithms = {
@@ -248,6 +332,8 @@ SignedXml.defaultNsForPrefix = {
248
332
ds : 'http://www.w3.org/2000/09/xmldsig#'
249
333
} ;
250
334
335
+ SignedXml . findAncestorNs = findAncestorNs ;
336
+
251
337
SignedXml . prototype . checkSignature = function ( xml ) {
252
338
this . validationErrors = [ ]
253
339
this . signedXml = xml
@@ -264,18 +350,37 @@ SignedXml.prototype.checkSignature = function(xml) {
264
350
if ( ! this . validateReferences ( doc ) ) {
265
351
return false ;
266
352
}
267
-
268
- if ( ! this . validateSignatureValue ( ) ) {
353
+
354
+ if ( ! this . validateSignatureValue ( doc ) ) {
269
355
return false ;
270
356
}
271
357
272
358
return true
273
359
}
274
360
275
- SignedXml . prototype . validateSignatureValue = function ( ) {
361
+ SignedXml . prototype . validateSignatureValue = function ( doc ) {
276
362
var signedInfo = utils . findChilds ( this . signatureNode , "SignedInfo" )
277
363
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 )
279
384
var signer = this . findSignatureAlgorithm ( this . signatureAlgorithm )
280
385
var res = signer . verifySignature ( signedInfoCanon , this . signingKey , this . signatureValue )
281
386
if ( ! res ) this . validationErrors . push ( "invalid signature: the signature value " +
@@ -310,6 +415,7 @@ SignedXml.prototype.validateReferences = function(doc) {
310
415
311
416
var uri = ref . uri [ 0 ] == "#" ? ref . uri . substring ( 1 ) : ref . uri
312
417
var elem = [ ] ;
418
+ var elemXpath ;
313
419
314
420
if ( uri == "" ) {
315
421
elem = select ( doc , "//*" )
@@ -322,11 +428,13 @@ SignedXml.prototype.validateReferences = function(doc) {
322
428
var num_elements_for_id = 0 ;
323
429
for ( var index in this . idAttributes ) {
324
430
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 )
326
433
num_elements_for_id += tmp_elem . length ;
327
434
if ( tmp_elem . length > 0 ) {
328
435
elem = tmp_elem ;
329
- } ;
436
+ elemXpath = tmp_elemXpath ;
437
+ }
330
438
}
331
439
if ( num_elements_for_id > 1 ) {
332
440
throw new Error ( 'Cannot validate a document which contains multiple elements with the ' +
@@ -340,7 +448,34 @@ SignedXml.prototype.validateReferences = function(doc) {
340
448
ref . uri + " but could not find such element in the xml" )
341
449
return false
342
450
}
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 ) ;
344
479
345
480
var hash = this . findHashAlgorithm ( ref . digestAlgorithm )
346
481
var digest = hash . getHash ( canonXml )
@@ -428,9 +563,25 @@ SignedXml.prototype.loadReference = function(ref) {
428
563
}
429
564
}
430
565
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
+ }
434
585
435
586
this . addReference ( null , transforms , digestAlgo , utils . findAttr ( ref , "URI" ) . value , digestValue , inclusiveNamespacesPrefixList , false )
436
587
}
@@ -610,7 +761,7 @@ SignedXml.prototype.getCanonXml = function(transforms, node, options) {
610
761
options = options || { } ;
611
762
options . defaultNsForPrefix = options . defaultNsForPrefix || SignedXml . defaultNsForPrefix ;
612
763
613
- var canonXml = node
764
+ var canonXml = node . cloneNode ( true ) // Deep clone
614
765
615
766
for ( var t in transforms ) {
616
767
if ( ! transforms . hasOwnProperty ( t ) ) continue ;
0 commit comments