xml-crypto
Advanced tools
Comparing version 0.10.1 to 1.0.0
@@ -67,6 +67,8 @@ /* jshint laxcomma: true */ | ||
* @param {String} defaultNs. The current default namespace | ||
* @param {String} defaultNsForPrefix. | ||
* @param {String} ancestorNamespaces - Import ancestor namespaces if it is specified | ||
* @return {String} | ||
* @api private | ||
*/ | ||
C14nCanonicalization.prototype.renderNs = function(node, prefixesInScope, defaultNs, defaultNsForPrefix) { | ||
C14nCanonicalization.prototype.renderNs = function(node, prefixesInScope, defaultNs, defaultNsForPrefix, ancestorNamespaces) { | ||
var a, i, p, attr | ||
@@ -97,4 +99,4 @@ , res = [] | ||
//handle all prefixed attributes that are included in the prefix list and where | ||
//the prefix is not defined already | ||
if (attr.prefix && prefixesInScope.indexOf(attr.localName) === -1) { | ||
//the prefix is not defined already. New prefixes can only be defined by `xmlns:`. | ||
if (attr.prefix === "xmlns" && prefixesInScope.indexOf(attr.localName) === -1) { | ||
nsListToRender.push({"prefix": attr.localName, "namespaceURI": attr.value}); | ||
@@ -112,2 +114,21 @@ prefixesInScope.push(attr.localName); | ||
} | ||
if(Array.isArray(ancestorNamespaces) && ancestorNamespaces.length > 0){ | ||
// Remove namespaces which are already present in nsListToRender | ||
for(var p1 in ancestorNamespaces){ | ||
if(!ancestorNamespaces.hasOwnProperty(p1)) continue; | ||
var alreadyListed = false; | ||
for(var p2 in nsListToRender){ | ||
if(nsListToRender[p2].prefix === ancestorNamespaces[p1].prefix | ||
&& nsListToRender[p2].namespaceURI === ancestorNamespaces[p1].namespaceURI) | ||
{ | ||
alreadyListed = true; | ||
} | ||
} | ||
if(!alreadyListed){ | ||
nsListToRender.push(ancestorNamespaces[p1]); | ||
} | ||
} | ||
} | ||
@@ -127,3 +148,3 @@ nsListToRender.sort(this.nsCompare); | ||
C14nCanonicalization.prototype.processInner = function(node, prefixesInScope, defaultNs, defaultNsForPrefix) { | ||
C14nCanonicalization.prototype.processInner = function(node, prefixesInScope, defaultNs, defaultNsForPrefix, ancestorNamespaces) { | ||
@@ -134,3 +155,3 @@ if (node.nodeType === 8) { return this.renderComment(node); } | ||
var i, pfxCopy | ||
, ns = this.renderNs(node, prefixesInScope, defaultNs, defaultNsForPrefix) | ||
, ns = this.renderNs(node, prefixesInScope, defaultNs, defaultNsForPrefix, ancestorNamespaces) | ||
, res = ["<", node.tagName, ns.rendered, this.renderAttrs(node, ns.newDefaultNs), ">"]; | ||
@@ -140,3 +161,3 @@ | ||
pfxCopy = prefixesInScope.slice(0); | ||
res.push(this.processInner(node.childNodes[i], pfxCopy, ns.newDefaultNs, defaultNsForPrefix)); | ||
res.push(this.processInner(node.childNodes[i], pfxCopy, ns.newDefaultNs, defaultNsForPrefix, [])); | ||
} | ||
@@ -194,4 +215,5 @@ | ||
var defaultNsForPrefix = options.defaultNsForPrefix || {}; | ||
var ancestorNamespaces = options.ancestorNamespaces || []; | ||
var res = this.processInner(node, [], defaultNs, defaultNsForPrefix); | ||
var res = this.processInner(node, [], defaultNs, defaultNsForPrefix, ancestorNamespaces); | ||
return res; | ||
@@ -198,0 +220,0 @@ }; |
@@ -9,3 +9,3 @@ var xpath = require('xpath.js'); | ||
EnvelopedSignature.prototype.process = function (node) { | ||
var signature = xpath(node, ".//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']")[0]; | ||
var signature = xpath(node, "./*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']")[0]; | ||
if (signature) signature.parentNode.removeChild(signature); | ||
@@ -12,0 +12,0 @@ return node; |
@@ -9,2 +9,3 @@ var select = require('xpath.js') | ||
, fs = require('fs') | ||
, xpath = require('xpath.js') | ||
@@ -201,3 +202,85 @@ exports.SignedXml = SignedXml | ||
/** | ||
* Extract ancestor namespaces in order to import it to root of document subset | ||
* which is being canonicalized for non-exclusive c14n. | ||
* | ||
* @param {object} doc - Usually a product from `new DOMParser().parseFromString()` | ||
* @param {string} docSubsetXpath - xpath query to get document subset being canonicalized | ||
* @returns {Array} i.e. [{prefix: "saml", namespaceURI: "urn:oasis:names:tc:SAML:2.0:assertion"}] | ||
*/ | ||
function findAncestorNs(doc, docSubsetXpath){ | ||
var docSubset = xpath(doc, docSubsetXpath); | ||
if(!Array.isArray(docSubset) || docSubset.length < 1){ | ||
return []; | ||
} | ||
// Remove duplicate on ancestor namespace | ||
var ancestorNs = collectAncestorNamespaces(docSubset[0]); | ||
var ancestorNsWithoutDuplicate = []; | ||
for(var i=0;i<ancestorNs.length;i++){ | ||
var notOnTheList = true; | ||
for(var v in ancestorNsWithoutDuplicate){ | ||
if(ancestorNsWithoutDuplicate[v].prefix === ancestorNs[i].prefix){ | ||
notOnTheList = false; | ||
break; | ||
} | ||
} | ||
if(notOnTheList){ | ||
ancestorNsWithoutDuplicate.push(ancestorNs[i]); | ||
} | ||
} | ||
// Remove namespaces which are already declared in the subset with the same prefix | ||
var returningNs = []; | ||
var subsetAttributes = docSubset[0].attributes; | ||
for(var j=0;j<ancestorNsWithoutDuplicate.length;j++){ | ||
var isUnique = true; | ||
for(var k=0;k<subsetAttributes.length;k++){ | ||
var nodeName = subsetAttributes[k].nodeName; | ||
if(nodeName.search(/^xmlns:/) === -1) continue; | ||
var prefix = nodeName.replace(/^xmlns:/, ""); | ||
if(ancestorNsWithoutDuplicate[j].prefix === prefix){ | ||
isUnique = false; | ||
break; | ||
} | ||
} | ||
if(isUnique){ | ||
returningNs.push(ancestorNsWithoutDuplicate[j]); | ||
} | ||
} | ||
return returningNs; | ||
} | ||
function collectAncestorNamespaces(node, nsArray){ | ||
if(!nsArray){ | ||
nsArray = []; | ||
} | ||
var parent = node.parentNode; | ||
if(!parent){ | ||
return nsArray; | ||
} | ||
if(parent.attributes && parent.attributes.length > 0){ | ||
for(var i=0;i<parent.attributes.length;i++){ | ||
var attr = parent.attributes[i]; | ||
if(attr && attr.nodeName && attr.nodeName.search(/^xmlns:/) !== -1){ | ||
nsArray.push({prefix: attr.nodeName.replace(/^xmlns:/, ""), namespaceURI: attr.nodeValue}) | ||
} | ||
} | ||
} | ||
return collectAncestorNamespaces(parent, nsArray); | ||
} | ||
/** | ||
* Xml signature implementation | ||
@@ -226,2 +309,3 @@ * | ||
if (this.options.idAttribute) this.idAttributes.splice(0, 0, this.options.idAttribute); | ||
this.implicitTransforms = this.options.implicitTransforms || []; | ||
} | ||
@@ -254,2 +338,4 @@ | ||
SignedXml.findAncestorNs = findAncestorNs; | ||
SignedXml.prototype.checkSignature = function(xml) { | ||
@@ -271,4 +357,4 @@ this.validationErrors = [] | ||
} | ||
if (!this.validateSignatureValue()) { | ||
if (!this.validateSignatureValue(doc)) { | ||
return false; | ||
@@ -280,6 +366,25 @@ } | ||
SignedXml.prototype.validateSignatureValue = function() { | ||
SignedXml.prototype.validateSignatureValue = function(doc) { | ||
var signedInfo = utils.findChilds(this.signatureNode, "SignedInfo") | ||
if (signedInfo.length==0) throw new Error("could not find SignedInfo element in the message") | ||
var signedInfoCanon = this.getCanonXml([this.canonicalizationAlgorithm], signedInfo[0]) | ||
/** | ||
* When canonicalization algorithm is non-exclusive, search for ancestor namespaces | ||
* before validating signature. | ||
*/ | ||
var ancestorNamespaces = []; | ||
if(this.canonicalizationAlgorithm === "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" | ||
|| this.canonicalizationAlgorithm === "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments") | ||
{ | ||
if(!doc || typeof(doc) !== "object"){ | ||
throw new Error("When canonicalization method is non-exclusive, whole xml dom must be provided as an argument"); | ||
} | ||
ancestorNamespaces = findAncestorNs(doc, "//*[local-name()='SignedInfo']"); | ||
} | ||
var c14nOptions = { | ||
ancestorNamespaces: ancestorNamespaces | ||
}; | ||
var signedInfoCanon = this.getCanonXml([this.canonicalizationAlgorithm], signedInfo[0], c14nOptions) | ||
var signer = this.findSignatureAlgorithm(this.signatureAlgorithm) | ||
@@ -319,2 +424,3 @@ var res = signer.verifySignature(signedInfoCanon, this.signingKey, this.signatureValue) | ||
var elem = []; | ||
var elemXpath; | ||
@@ -332,7 +438,9 @@ if (uri=="") { | ||
if (!this.idAttributes.hasOwnProperty(index)) continue; | ||
var tmp_elem = select(doc, "//*[@*[local-name(.)='" + this.idAttributes[index] + "']='" + uri + "']") | ||
var tmp_elemXpath = "//*[@*[local-name(.)='" + this.idAttributes[index] + "']='" + uri + "']"; | ||
var tmp_elem = select(doc, tmp_elemXpath) | ||
num_elements_for_id += tmp_elem.length; | ||
if (tmp_elem.length > 0) { | ||
elem = tmp_elem; | ||
}; | ||
elemXpath = tmp_elemXpath; | ||
} | ||
} | ||
@@ -351,3 +459,30 @@ if (num_elements_for_id > 1) { | ||
} | ||
var canonXml = this.getCanonXml(ref.transforms, elem[0], { inclusiveNamespacesPrefixList: ref.inclusiveNamespacesPrefixList }); | ||
/** | ||
* When canonicalization algorithm is non-exclusive, search for ancestor namespaces | ||
* before validating references. | ||
*/ | ||
if(Array.isArray(ref.transforms)){ | ||
var hasNonExcC14nTransform = false; | ||
for(var t in ref.transforms){ | ||
if(!ref.transforms.hasOwnProperty(t)) continue; | ||
if(ref.transforms[t] === "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" | ||
|| ref.transforms[t] === "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments") | ||
{ | ||
hasNonExcC14nTransform = true; | ||
break; | ||
} | ||
} | ||
if(hasNonExcC14nTransform){ | ||
ref.ancestorNamespaces = findAncestorNs(doc, elemXpath); | ||
} | ||
} | ||
var c14nOptions = { | ||
inclusiveNamespacesPrefixList: ref.inclusiveNamespacesPrefixList, | ||
ancestorNamespaces: ref.ancestorNamespaces | ||
}; | ||
var canonXml = this.getCanonXml(ref.transforms, elem[0], c14nOptions); | ||
@@ -357,3 +492,3 @@ var hash = this.findHashAlgorithm(ref.digestAlgorithm) | ||
if (digest!=ref.digestValue) { | ||
if (!validateDigestValue(digest, ref.digestValue)) { | ||
this.validationErrors.push("invalid signature: for uri " + ref.uri + | ||
@@ -369,2 +504,32 @@ " calculated digest is " + digest + | ||
function validateDigestValue(digest, expectedDigest) { | ||
var buffer, expectedBuffer; | ||
if (typeof Buffer.from === 'function') { | ||
buffer = Buffer.from(digest, 'base64'); | ||
expectedBuffer = Buffer.from(expectedDigest, 'base64'); | ||
} else { | ||
// Compatibility with Node < 5.10.0 | ||
buffer = new Buffer(digest, 'base64'); | ||
expectedBuffer = new Buffer(expectedDigest, 'base64'); | ||
} | ||
if (typeof buffer.equals === 'function') { | ||
return buffer.equals(expectedBuffer); | ||
} | ||
// Compatibility with Node < 0.11.13 | ||
if (buffer.length !== expectedBuffer.length) { | ||
return false; | ||
} | ||
for (var i = 0; i < buffer.length; i++) { | ||
if (buffer[i] !== expectedBuffer[i]) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
SignedXml.prototype.loadSignature = function(signatureNode) { | ||
@@ -442,5 +607,21 @@ if (typeof signatureNode === 'string') { | ||
//***workaround for validating windows mobile store signatures - it uses c14n but does not state it in the transforms | ||
if (transforms.length==1 && transforms[0]=="http://www.w3.org/2000/09/xmldsig#enveloped-signature") | ||
transforms.push("http://www.w3.org/2001/10/xml-exc-c14n#") | ||
var hasImplicitTransforms = (Array.isArray(this.implicitTransforms) && this.implicitTransforms.length > 0); | ||
if(hasImplicitTransforms){ | ||
this.implicitTransforms.forEach(function(t){ | ||
transforms.push(t); | ||
}); | ||
} | ||
/** | ||
* DigestMethods take an octet stream rather than a node set. If the output of the last transform is a node set, we | ||
* need to canonicalize the node set to an octet stream using non-exclusive canonicalization. If there are no | ||
* transforms, we need to canonicalize because URI dereferencing for a same-document reference will return a node-set. | ||
* See: | ||
* https://www.w3.org/TR/xmldsig-core1/#sec-DigestMethod | ||
* https://www.w3.org/TR/xmldsig-core1/#sec-ReferenceProcessingModel | ||
* https://www.w3.org/TR/xmldsig-core1/#sec-Same-Document | ||
*/ | ||
if (transforms.length === 0 || transforms[transforms.length - 1] === "http://www.w3.org/2000/09/xmldsig#enveloped-signature") { | ||
transforms.push("http://www.w3.org/TR/2001/REC-xml-c14n-20010315") | ||
} | ||
@@ -625,3 +806,3 @@ this.addReference(null, transforms, digestAlgo, utils.findAttr(ref, "URI").value, digestValue, inclusiveNamespacesPrefixList, false) | ||
var canonXml = node | ||
var canonXml = node.cloneNode(true) // Deep clone | ||
@@ -628,0 +809,0 @@ for (var t in transforms) { |
{ | ||
"name": "xml-crypto", | ||
"version": "0.10.1", | ||
"version": "1.0.0", | ||
"description": "Xml digital signature and encryption library for Node.js", | ||
@@ -10,7 +10,7 @@ "engines": { | ||
"dependencies": { | ||
"xmldom": "=0.1.19", | ||
"xmldom": "0.1.27", | ||
"xpath.js": ">=0.0.3" | ||
}, | ||
"devDependencies": { | ||
"nodeunit": ">=0.6.4" | ||
"nodeunit": "^0.11.3" | ||
}, | ||
@@ -33,4 +33,4 @@ "repository": { | ||
"scripts": { | ||
"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": "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" | ||
} | ||
} |
@@ -135,3 +135,3 @@ ## xml-crypto | ||
var res = sig.checkSignature(xml) | ||
if (!res) console.log(sig.validationErrors) | ||
if (!res) console.log(sig.validationErrors) | ||
````` | ||
@@ -141,2 +141,11 @@ | ||
In order to protect from some attacks we must check the content we want to use is the one that has been signed: | ||
`````javascript | ||
var elem = select(doc, "/xpath_to_interesting_element"); | ||
var uri = sig.references[0].uri; // might not be 0 - depending on the document you verify | ||
var id = (uri[0] === '#') ? uri.substring(1) : uri; | ||
if (elem.getAttribute('ID') != id && elem.getAttribute('Id') != id && elem.getAttribute('id') != id) | ||
throw new Error('the interesting element was not the one verified by the signature') | ||
````` | ||
Note: | ||
@@ -147,2 +156,26 @@ | ||
### Caring for Implicit transform | ||
If you fail to verify signed XML, then one possible cause is that there are some hidden implicit transforms(#). | ||
(#) Normalizing XML document to be verified. i.e. remove extra space within a tag, sorting attributes, importing namespace declared in ancestor nodes, etc. | ||
The reason for these implicit transform might come from [complex xml signature specification](https://www.w3.org/TR/2002/REC-xmldsig-core-20020212), | ||
which makes XML developers confused and then leads to incorrect implementation for signing XML document. | ||
If you keep failing verification, it is worth trying to guess such a hidden transform and specify it to the option as below: | ||
```javascript | ||
var option = {implicitTransforms: ["http://www.w3.org/TR/2001/REC-xml-c14n-20010315"]} | ||
var sig = new SignedXml(null, option) | ||
sig.keyInfoProvider = new FileKeyInfo("client_public.pem") | ||
sig.loadSignature(signature) | ||
var res = sig.checkSignature(xml) | ||
``` | ||
You might find it difficult to guess such transforms, but there are typical transforms you can try. | ||
- http://www.w3.org/TR/2001/REC-xml-c14n-20010315 | ||
- http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments | ||
- http://www.w3.org/2001/10/xml-exc-c14n# | ||
- http://www.w3.org/2001/10/xml-exc-c14n#WithComments | ||
## API | ||
@@ -149,0 +182,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
70420
1243
1
454
14
1
+ Addedxmldom@0.1.27(transitive)
Updatedxmldom@0.1.27