From 1b175bb230fadfe821812c1010c8fd973096c870 Mon Sep 17 00:00:00 2001 From: Troy <5209556+troyfactor4@users.noreply.github.com> Date: Wed, 3 Mar 2021 04:08:15 -0400 Subject: [PATCH 1/2] feat: add external async signing and encryption strategies --- lib/saml11.js | 44 ++++++++++++++++++++++++---- lib/saml20.js | 69 +++++++++++++++++++++++++++++++++++--------- test/saml11.tests.js | 36 +++++++++++++++++++++++ test/saml20.tests.js | 36 +++++++++++++++++++++++ 4 files changed, 166 insertions(+), 19 deletions(-) diff --git a/lib/saml11.js b/lib/saml11.js index bc3512a..bb73197 100644 --- a/lib/saml11.js +++ b/lib/saml11.js @@ -57,17 +57,34 @@ function extractSaml11Options(opts) { * @param [options.encryptionAlgorithm] {string} * @param [options.keyEncryptionAlgorithm] {string} * + * @param [strategies] + * + * // Signing strategy + * @param {Function} [strategies.signXml] + * + * // Encryption strategy + * @param {Function} [strategies.encryptXml] + * * @param {Function} [callback] required if encrypting * @return {String|*} */ -exports.create = function(options, callback) { - return createAssertion(extractSaml11Options(options), { +exports.create = function(options, strategies, callback) { + var defaultStrategy = { signXml: SignXml.fromSignXmlOptions(Object.assign({ xpathToNodeBeforeSignature: "//*[local-name(.)='AuthenticationStatement']", signatureIdAttribute: 'AssertionID' }, options)), encryptXml: EncryptXml.fromEncryptXmlOptions(options) - }, callback); + } + + if (typeof strategies === 'function' && callback == null) { + callback = strategies + strategies = defaultStrategy + } else { + strategies = strategies || defaultStrategy + } + + return createAssertion(extractSaml11Options(options), strategies, callback); } /** @@ -177,9 +194,19 @@ function createAssertion(options, strategies, callback) { nameIDs[1].setAttribute('Format', options.nameIdentifierFormat); } + if (callback) { + signAndEncryptAsync(doc, strategies, options, callback) + } else { + return signAndEncryptSync(doc, strategies) + } +} + +function signAndEncryptAsync(doc, strategies, options, callback) { if (strategies.encryptXml === EncryptXml.unencrypted) { - var signed = strategies.signXml(doc); - return strategies.encryptXml(signed, callback); + strategies.signXml(doc, function(err, signed){ + strategies.encryptXml(signed, callback); + }); + return } // encryption is turned on, @@ -206,6 +233,13 @@ function createAssertion(options, strategies, callback) { }); } +function signAndEncryptSync(doc, strategies) { + if (strategies.encryptXml === EncryptXml.unencrypted) { + var signed = strategies.signXml(doc); + return strategies.encryptXml(signed); + } +} + function addSubjectConfirmation(encryptOptions, doc, randomBytes, callback) { xmlenc.encryptKeyInfo(randomBytes, encryptOptions, function(err, keyinfo) { if (err) return callback(err); diff --git a/lib/saml20.js b/lib/saml20.js index 9db8141..7a9d1b0 100644 --- a/lib/saml20.js +++ b/lib/saml20.js @@ -96,17 +96,34 @@ function extractSaml20Options(opts) { * @param [options.encryptionAlgorithm] {string} * @param [options.keyEncryptionAlgorithm] {string} * + * @param [strategies] + * + * // Signing strategy + * @param {Function} [strategies.signXml] + * + * // Encryption strategy + * @param {Function} [strategies.encryptXml] + * * @param {Function} [callback] required if encrypting * @return {*} */ -exports.create = function createSignedAssertion(options, callback) { - return createAssertion(extractSaml20Options(options), { +exports.create = function createSignedAssertion(options, strategies, callback) { + var defaultStrategy = { signXml: SignXml.fromSignXmlOptions(Object.assign({ xpathToNodeBeforeSignature: "//*[local-name(.)='Issuer']", signatureIdAttribute: 'ID' }, options)), encryptXml: EncryptXml.fromEncryptXmlOptions(options) - }, callback); + } + + if (typeof strategies === 'function' && callback == null) { + callback = strategies + strategies = defaultStrategy + } else { + strategies = strategies || defaultStrategy + } + + return createAssertion(extractSaml20Options(options), strategies, callback); }; /** @@ -251,6 +268,40 @@ function createAssertion(options, strategies, callback) { authnCtxClassRef.textContent = options.authnContextClassRef; } + if (callback) { + signAndEncryptAsync(doc, strategies, callback) + } else { + return signAndEncryptSync(doc, strategies) + } +} + +function signAndEncryptAsync(doc, strategies, callback) { + try { + strategies.signXml(doc, function(err, signed){ + if (err || !signed) { + return utils.reportError(err, callback); + } + + if (strategies.encryptXml === EncryptXml.unencrypted) { + return strategies.encryptXml(signed, callback); + } + + async.waterfall([ + function (cb) { + strategies.encryptXml(signed, cb) + }, + function (encrypted, cb) { + var assertion = '' + encrypted + ''; + cb(null, utils.removeWhitespace(assertion)); + }, + ], callback); + }); + } catch(err) { + utils.reportError(err, callback); + } +} + +function signAndEncryptSync(doc, strategies) { var signed; try { signed = strategies.signXml(doc); @@ -259,16 +310,6 @@ function createAssertion(options, strategies, callback) { } if (strategies.encryptXml === EncryptXml.unencrypted) { - return strategies.encryptXml(signed, callback); + return strategies.encryptXml(signed); } - - async.waterfall([ - function (cb) { - strategies.encryptXml(signed, cb) - }, - function (encrypted, cb) { - var assertion = '' + encrypted + ''; - cb(null, utils.removeWhitespace(assertion)); - }, - ], callback); } diff --git a/test/saml11.tests.js b/test/saml11.tests.js index 0e7a3c8..d37dada 100644 --- a/test/saml11.tests.js +++ b/test/saml11.tests.js @@ -8,6 +8,9 @@ var xmlenc = require('xml-encryption'); var utils = require('./utils'); var saml11 = require('../lib/saml11'); +var EncryptXml = require('../lib/xml/encrypt'); +var SignXml = require('../lib/xml/sign'); + describe('saml 1.1', function () { saml11TestSuite({ @@ -347,6 +350,39 @@ describe('saml 1.1', function () { }); }); + it('should create a saml 1.1 encrypted assertion using async strategy', function (done) { + var options = { + cert: fs.readFileSync(__dirname + '/test-auth0.pem'), + key: fs.readFileSync(__dirname + '/test-auth0.key'), + encryptionPublicKey: fs.readFileSync(__dirname + '/test-auth0_rsa.pub'), + encryptionCert: fs.readFileSync(__dirname + '/test-auth0.pem') + }; + + var strategies = { + signXml: SignXml.fromSignXmlOptions(Object.assign({ + xpathToNodeBeforeSignature: "//*[local-name(.)='AuthenticationStatement']", + signatureIdAttribute: 'AssertionID' + }, options)), + encryptXml: EncryptXml.fromEncryptXmlOptions(options) + } + + var callback = function(err, encrypted) { + if (err) return done(err); + + xmlenc.decrypt(encrypted, { key: fs.readFileSync(__dirname + '/test-auth0.key')}, function(err, decrypted) { + if (err) return done(err); + assertSignature(decrypted, options); + done(); + }); + } + + if (createAssertion === 'create'){ + saml11[createAssertion](options, strategies, callback); + } else { + saml11[createAssertion](options, callback); + } + }); + it('should support holder-of-key suject confirmationmethod', function (done) { var options = { cert: fs.readFileSync(__dirname + '/test-auth0.pem'), diff --git a/test/saml20.tests.js b/test/saml20.tests.js index b55a7b6..dd51e23 100644 --- a/test/saml20.tests.js +++ b/test/saml20.tests.js @@ -7,6 +7,8 @@ var xmldom = require('xmldom'); var xmlenc = require('xml-encryption'); var saml = require('../lib/saml20'); +var EncryptXml = require('../lib/xml/encrypt'); +var SignXml = require('../lib/xml/sign'); describe('saml 2.0', function () { saml20TestSuite({ @@ -513,6 +515,40 @@ describe('saml 2.0', function () { }); }); + it('should create a saml 2.0 signed and encrypted assertion with async strategy', function (done) { + var options = { + cert: fs.readFileSync(__dirname + '/test-auth0.pem'), + key: fs.readFileSync(__dirname + '/test-auth0.key'), + encryptionPublicKey: fs.readFileSync(__dirname + '/test-auth0_rsa.pub'), + encryptionCert: fs.readFileSync(__dirname + '/test-auth0.pem') + }; + + var strategies = { + signXml: SignXml.fromSignXmlOptions(Object.assign({ + xpathToNodeBeforeSignature: "//*[local-name(.)='Issuer']", + signatureIdAttribute: 'ID' + }, options)), + encryptXml: EncryptXml.fromEncryptXmlOptions(options) + } + + var callback = function(err, encrypted){ + if (err) return done(err); + var encryptedData = utils.getEncryptedData(encrypted); + + xmlenc.decrypt(encryptedData.toString(), { key: fs.readFileSync(__dirname + '/test-auth0.key') }, function (err, decrypted) { + if (err) return done(err); + assertSignature(decrypted, options); + done(); + }); + } + + if (createAssertion === 'create'){ + saml[createAssertion](options, strategies, callback) + } else { + saml[createAssertion](options, callback) + } + }); + it('should set attributes', function (done) { var options = { cert: fs.readFileSync(__dirname + '/test-auth0.pem'), From da5e66c55b3d95f24e1067713097619e23e6695e Mon Sep 17 00:00:00 2001 From: D1plo1d Date: Fri, 29 Apr 2022 14:21:49 -0400 Subject: [PATCH 2/2] fix(sign): passing callback to compute signature --- lib/xml/sign.js | 69 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/lib/xml/sign.js b/lib/xml/sign.js index 7c01fd0..d3de3a3 100644 --- a/lib/xml/sign.js +++ b/lib/xml/sign.js @@ -39,7 +39,7 @@ exports.fromSignXmlOptions = function (options) { * @return {string} */ return function signXmlDocument(doc, callback) { - function sign(key) { + function sign(key, signCallback) { const unsigned = exports.unsigned(doc); const cert = utils.pemToCert(pem); @@ -60,32 +60,59 @@ exports.fromSignXmlOptions = function (options) { } }; - sig.computeSignature(unsigned, { - location: {reference: xpathToNodeBeforeSignature, action: 'after'}, - prefix: signatureNamespacePrefix - }); + if (signCallback == null) { + sig.computeSignature(unsigned, { + location: {reference: xpathToNodeBeforeSignature, action: 'after'}, + prefix: signatureNamespacePrefix + }); - return sig.getSignedXml(); + return sig.getSignedXml(); + } else { + sig.computeSignature(unsigned, { + location: {reference: xpathToNodeBeforeSignature, action: 'after'}, + prefix: signatureNamespacePrefix + }, function (err) { + if (err != null) { + signCallback(err, null) + } else { + signCallback(null, sig.getSignedXml()) + } + }); + } } - let signed - try { - try { - signed = sign(key) - } catch (err) { - signed = sign(utils.fixPemFormatting(key)) - } + if (callback != null) { + sign(key, function(err, signed) { + if (err != null) { + sign(utils.fixPemFormatting(key), function(err, signed) { + setImmediate(callback, err, signed); + }) + return; + } - if (callback) { setImmediate(callback, null, signed); - } else { - return signed; - } - } catch (e) { - if (callback) { - setImmediate(callback, e) + }) + } else { + let signed + + try { + try { + signed = sign(key) + } catch (err) { + signed = sign(utils.fixPemFormatting(key)) + } + + if (callback) { + setImmediate(callback, null, signed); + } else { + return signed; + } + } catch (e) { + if (callback) { + setImmediate(callback, e) + } + throw e } - throw e } }; };