From 8046db027e8172be63ba488d1d42b1b48ade67a5 Mon Sep 17 00:00:00 2001 From: Alon Motro Date: Tue, 3 Nov 2020 09:31:50 -0500 Subject: [PATCH] Allow for use of privateKey instead of privateCert (#488) * Allow for use of privateKey instead of privateCert Co-authored-by: Mark Stosberg --- README.md | 6 +- src/passport-saml/saml-post-signing.ts | 7 +- src/passport-saml/saml.ts | 18 ++-- test/saml-post-signing-tests.js | 38 ++++++- test/tests.js | 137 ++++++++++++++++++++++++- 5 files changed, 190 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 62f28434..ee07fd1d 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ type Profile = { * `issuer`: issuer string to supply to identity provider * `audience`: expected saml response Audience (if not provided, Audience won't be verified) * `cert`: the IDP's public signing certificate used to validate the signatures of the incoming SAML Responses, see [Security and signatures](#security-and-signatures) - * `privateCert`: see [Security and signatures](#security-and-signatures) + * `privateKey`: see [Security and signatures](#security-and-signatures). Old name of `privateCert` is accepted alternative. * `decryptionPvk`: optional private key that will be used to attempt to decrypt any encrypted assertions that are received * `signatureAlgorithm`: optionally set the signature algorithm for signing requests, valid values are 'sha1' (default), 'sha256', or 'sha512' * `digestAlgorithm`: optionally set the digest algorithm used to provide a digest for the signed data object, valid values are 'sha1' (default), 'sha256', or 'sha512' @@ -238,9 +238,9 @@ To select hashing algorithm, use: ... ``` -To sign them you need to provide a private key in the PEM format via the `privateCert` configuration key. +To sign them you need to provide a private key in the PEM format via the `privateKey` configuration key. -Formats supported for `privateCert` field are, +Formats supported for `privateKey` field are, 1. Well formatted PEM: diff --git a/src/passport-saml/saml-post-signing.ts b/src/passport-saml/saml-post-signing.ts index 1c3a5df5..cc1356d6 100644 --- a/src/passport-saml/saml-post-signing.ts +++ b/src/passport-saml/saml-post-signing.ts @@ -6,7 +6,8 @@ const issuerXPath = '/*[local-name(.)="Issuer" and namespace-uri(.)="urn:oasis:n const defaultTransforms = [ 'http://www.w3.org/2000/09/xmldsig#enveloped-signature', 'http://www.w3.org/2001/10/xml-exc-c14n#' ]; interface SignSamlPostOptions { - privateCert: string; + privateCert?: string; + privateKey?: string; signatureAlgorithm?: string; xmlSignatureTransforms?: string[]; digestAlgorithm: string; @@ -15,7 +16,7 @@ interface SignSamlPostOptions { export function signSamlPost(samlMessage: string, xpath: string, options: SignSamlPostOptions) { if (!samlMessage) throw new Error('samlMessage is required'); if (!xpath) throw new Error('xpath is required'); - if (!options || !options.privateCert) throw new Error('options.privateCert is required'); + if (!options || (!options.privateCert && !options.privateKey)) throw new Error('options.privateCert or options.privateKey is required'); const transforms = options.xmlSignatureTransforms || defaultTransforms; const sig = new SignedXml(); @@ -23,7 +24,7 @@ export function signSamlPost(samlMessage: string, xpath: string, options: SignSa sig.signatureAlgorithm = algorithms.getSigningAlgorithm(options.signatureAlgorithm); } sig.addReference(xpath, transforms, algorithms.getDigestAlgorithm(options.digestAlgorithm)); - sig.signingKey = options.privateCert; + sig.signingKey = options.privateCert || options.privateKey; sig.computeSignature(samlMessage, { location: { reference: xpath + issuerXPath, action: 'after' }}); return sig.getSignedXml(); } diff --git a/src/passport-saml/saml.ts b/src/passport-saml/saml.ts index d324ae92..2be73f9f 100644 --- a/src/passport-saml/saml.ts +++ b/src/passport-saml/saml.ts @@ -85,6 +85,7 @@ interface SAMLOptions { signatureAlgorithm: string; path: string; privateCert: string; + privateKey: string; logoutUrl: string; entryPoint: string; skipRequestCompression: boolean; @@ -232,7 +233,7 @@ class SAML { samlMessageToSign.SigAlg = samlMessage.SigAlg; } signer.update(querystring.stringify(samlMessageToSign)); - samlMessage.Signature = signer.sign(this.keyToPEM(this.options.privateCert), 'base64'); + samlMessage.Signature = signer.sign(this.keyToPEM(this.options.privateCert) || this.options.privateKey, 'base64'); } generateAuthorizeRequest = function (req, isPassive, isHttpPostBinding, callback) { @@ -357,7 +358,8 @@ class SAML { } let stringRequest = xmlbuilder.create(request).end(); - if (isHttpPostBinding && this.options.privateCert) { + const privateKey = this.options.privateCert || this.options.privateKey; + if (isHttpPostBinding && privateKey) { stringRequest = signAuthnRequestPost(stringRequest, this.options); } callback(null, stringRequest); @@ -464,8 +466,8 @@ class SAML { Object.keys(additionalParameters).forEach(k => { samlMessage[k] = additionalParameters[k]; }); - - if (this.options.privateCert) { + const privateKey = this.options.privateCert || this.options.privateKey; + if (privateKey) { try { if (!this.options.entryPoint) { throw new Error('"entryPoint" config parameter is required for signed messages'); @@ -1303,17 +1305,17 @@ class SAML { "Missing decryptionCert while generating metadata for decrypting service provider"); } } - - if(this.options.privateCert){ + const privateKey = this.options.privateCert || this.options.privateKey; + if(privateKey){ if(!signingCert){ throw new Error( "Missing signingCert while generating metadata for signing service provider messages"); } } - if(this.options.decryptionPvk || this.options.privateCert){ + if(this.options.decryptionPvk || privateKey){ metadata.EntityDescriptor.SPSSODescriptor.KeyDescriptor=[]; - if (this.options.privateCert) { + if (privateKey) { signingCert = signingCert.replace( /-+BEGIN CERTIFICATE-+\r?\n?/, '' ); signingCert = signingCert.replace( /-+END CERTIFICATE-+\r?\n?/, '' ); diff --git a/test/saml-post-signing-tests.js b/test/saml-post-signing-tests.js index d9fecb9c..a2145980 100644 --- a/test/saml-post-signing-tests.js +++ b/test/saml-post-signing-tests.js @@ -14,6 +14,13 @@ describe('SAML POST Signing', function () { result.should.match(/[A-Za-z0-9\/\+\=]+<\/SignatureValue>/); }); + it('should sign a simple saml request when using a privateKey', function () { + var xml = 'http://example.com'; + var result = signSamlPost(xml, '/SAMLRequest', { privateKey: signingKey }); + result.should.match(/[A-Za-z0-9\/\+\=]+<\/DigestValue>/); + result.should.match(/[A-Za-z0-9\/\+\=]+<\/SignatureValue>/); + }); + it('should place the Signature element after the Issuer element', function () { var xml = 'http://example.com'; var result = signSamlPost(xml, '/SAMLRequest', { privateCert: signingKey }); @@ -21,13 +28,34 @@ describe('SAML POST Signing', function () { result.should.match(/<\/Signature>/); + result.should.match(//); + result.should.match(//); + }); + + it('should sign and digest with SHA256 when specified and using privateKey', function () { + var xml = 'http://example.com'; + var options = { + signatureAlgorithm: 'sha256', + digestAlgorithm: 'sha256', + privateKey: signingKey + }; var result = signSamlPost(xml, '/SAMLRequest', options); result.should.match(//); @@ -41,4 +69,12 @@ describe('SAML POST Signing', function () { result.should.match(/[A-Za-z0-9\/\+\=]+<\/DigestValue>/); result.should.match(/[A-Za-z0-9\/\+\=]+<\/SignatureValue>/); }); + + it('should sign an AuthnRequest when using a privateKey', function () { + var xml = 'http://example.com'; + var result = signAuthnRequestPost(xml, { privateKey: signingKey }); + result.should.match(/[A-Za-z0-9\/\+\=]+<\/DigestValue>/); + result.should.match(/[A-Za-z0-9\/\+\=]+<\/SignatureValue>/); + }); + }); diff --git a/test/tests.js b/test/tests.js index 47f6ebbd..045db3c9 100644 --- a/test/tests.js +++ b/test/tests.js @@ -1445,6 +1445,22 @@ describe( 'passport-saml /', function() { testMetadata( samlConfig, expectedMetadata, signingCert ); }); + it( 'config with protocol, path, host, decryptionPvk and privateKey should pass', function() { + var samlConfig = { + issuer: 'http://example.serviceprovider.com', + protocol: 'http://', + host: 'example.serviceprovider.com', + path: '/saml/callback', + identifierFormat: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', + decryptionPvk: fs.readFileSync(__dirname + '/static/testshib encryption pvk.pem'), + privateKey: fs.readFileSync(__dirname + '/static/acme_tools_com.key') + }; + var expectedMetadata = fs.readFileSync(__dirname + '/static/expectedMetadataWithBothKeys.xml', 'utf-8'); + var signingCert = fs.readFileSync(__dirname + '/static/acme_tools_com.cert').toString(); + + testMetadata( samlConfig, expectedMetadata, signingCert ); + }); + }); it('generateServiceProviderMetadata contains logout callback url', function () { @@ -1895,7 +1911,33 @@ describe( 'passport-saml /', function() { } }); }); - + it( 'acme_tools request signed with sha256 when using privateKey', function( done ) { + var samlConfig = { + entryPoint: 'https://adfs.acme_tools.com/adfs/ls/', + issuer: 'acme_tools_com', + callbackUrl: 'https://relyingparty/adfs/postResponse', + privateKey: fs.readFileSync(__dirname + '/static/acme_tools_com.key', 'utf-8'), + authnContext: 'http://schemas.microsoft.com/ws/2008/06/identity/authenticationmethod/password', + identifierFormat: null, + signatureAlgorithm: 'sha256', + additionalParams: { + customQueryStringParam: 'CustomQueryStringParamValue' + } + }; + var samlObj = new SAML( samlConfig ); + samlObj.generateUniqueID = function () { return '12345678901234567890' }; + samlObj.getAuthorizeUrl({}, {}, function(err, url) { + try { + var qry = require('querystring').parse(require('url').parse(url).query); + qry.SigAlg.should.match('http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'); + qry.Signature.should.match('hel9NaoLU0brY/VhrQsY+lTtuAbTsT/ul6nZ/eVlSMXQRaKn5LTbKadzxmPghX7s4xoHwdah+yZHK/0u4StYSj4b5MKcqbsJapVr2R7H90z8YfGfR2C/G0Gng721YV9Da6VBzKg8Was91zQotgsMpZ9pGX1kPKi6cgFwPwM4NEFugn8AYgXEriNvO5+Q23K/MdBT2bgwRTj2FQCWTuQcgwbyWHXoquHztZ0lbh8UhY5BfQRv7c6D9XPkQEMMQFQeME4PIEg3JnynwFZk5wwhkphMd5nXxau+zt7Nfp4fRm0G8WYnxV1etBnWimwSglZVaSHFYeQBRsC2wvKSiVS8JA=='); + qry.customQueryStringParam.should.match('CustomQueryStringParamValue'); + done(); + } catch (err2) { + done(err2); + } + }); + }); it( 'acme_tools request not signed if missing entry point', function( done ) { var samlConfig = { entryPoint: '', @@ -1922,7 +1964,32 @@ describe( 'passport-saml /', function() { } }); }); + it( 'acme_tools request not signed if missing entry point when using privateKey', function( done ) { + var samlConfig = { + entryPoint: '', + issuer: 'acme_tools_com', + callbackUrl: 'https://relyingparty/adfs/postResponse', + privateKey: fs.readFileSync(__dirname + '/static/acme_tools_com.key', 'utf-8'), + authnContext: 'http://schemas.microsoft.com/ws/2008/06/identity/authenticationmethod/password', + signatureAlgorithm: 'sha256', + additionalParams: { + customQueryStringParam: 'CustomQueryStringParamValue' + } + }; + var samlObj = new SAML( samlConfig ); + samlObj.generateUniqueID = function () { return '12345678901234567890' }; + var request = 'onelogin_samlurn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'; + samlObj.requestToUrl(request, null, 'authorize', {}, function(err) { + try { + should.exist(err); + err.message.should.eql('"entryPoint" config parameter is required for signed messages'); + done(); + } catch (err2) { + done(err2); + } + }); + }); it( 'acme_tools request signed with sha1', function( done ) { var samlConfig = { entryPoint: 'https://adfs.acme_tools.com/adfs/ls/', @@ -1950,6 +2017,33 @@ describe( 'passport-saml /', function() { } }); }); + it( 'acme_tools request signed with sha1 when using privateKey', function( done ) { + var samlConfig = { + entryPoint: 'https://adfs.acme_tools.com/adfs/ls/', + issuer: 'acme_tools_com', + callbackUrl: 'https://relyingparty/adfs/postResponse', + privateKey: fs.readFileSync(__dirname + '/static/acme_tools_com.key', 'utf-8'), + authnContext: 'http://schemas.microsoft.com/ws/2008/06/identity/authenticationmethod/password', + identifierFormat: null, + signatureAlgorithm: 'sha1', + additionalParams: { + customQueryStringParam: 'CustomQueryStringParamValue' + } + }; + var samlObj = new SAML( samlConfig ); + samlObj.generateUniqueID = function () { return '12345678901234567890' }; + samlObj.getAuthorizeUrl({}, {}, function(err, url) { + try { + var qry = require('querystring').parse(require('url').parse(url).query); + qry.SigAlg.should.match('http://www.w3.org/2000/09/xmldsig#rsa-sha1'); + qry.Signature.should.match('MeFo+LjufxP5A+sCRwzR/YH/RV6W14aYSFjUdie62JxkI6hDcVhoSZQUJ3wtWMhL59gJj05tTFnXAZRqUQVsavyy41cmUZVeCsat0gaHBQOILXpp9deB0iSJt1EVQTOJkVx8uu2/WYu/bBiH7w2bpwuCf1gJhlqZb/ca3B6yjHSMjnnVfc2LbNPWHpE5464lrs79VjDXf9GQWfrBr95dh3P51IAb7C+77KDWQUl9WfZfyyuEgS83vyZ0UGOxT4AObJ6NOcLs8+iidDdWJJkBaKQev6U+AghCjLQUYOrflivLIIyqATKu2q9PbOse6Phmnxok50+broXSG23+e+742Q=='); + qry.customQueryStringParam.should.match('CustomQueryStringParamValue'); + done(); + } catch (err2) { + done(err2); + } + }); + }); }); describe( 'getAdditionalParams checks /', function() { @@ -2831,6 +2925,47 @@ describe( 'passport-saml /', function() { } }); }); + it('errors if bad privateKey to requestToURL', function(done){ + var samlObj = new SAML({ + entryPoint: "foo", + privateKey: "-----BEGIN CERTIFICATE-----\n"+ + "8mvhvrcCOiJ3mjgKNN1F31jOBJuZNmq0U7n9v+Z+3NfyU/0E9jkrnFvm5ks+p8kl\n" + + "BjuBk9RAkazsU9l02XMS/VxOOIifxKC7R9bDtx0hjolYxgqxPIO5s4rmjj0rLzvo\n" + + "vQTTTx/tB5e+hbdx922QSeTjP4DO4ms6cIexcH+ZEUOJ3wXiHToJW83SXLRtwPI9\n" + + "JbWKeS9nWPnzcedbDNZkGtohW5vf32BHuvLsWcl6eFXRSkdX/7+rgpXmDRB7caQ+\n" + + "2SXVY7ORily7LTKg1cFmuKHDzKTGFIp5/GU6dwIDAQABAoIBAArgFQ+Uk4UN4diY\n" + + "gJWCAaQlTVmP0UEHZQt/NmJrc9ZVduuhOP0hH6gF53nREHz5UQb4nXB2Ksa3MtYD\n" + + "Z1vhJcu/T7pvmib4q+Ij6oAmlyeL/xwVY3IUURMxX3tCdPItlk4PEFELKeqQOiIS\n" + + "7B0DYxWfJbMle3c95w5ruYEr2A+fHCKVSlDpg7uPd9VQ6t7bGMZZvc9tDSC1qPXQ\n" + + "Gd/WOMXxi+t/TpyVZ6tOcEekQzAMLmWElUUPx3TJ0ur0Zl2LZ7IvQEXXias4lUHV\n" + + "fnH3akDCMmdhlJSVqUfplrh85zAOh6fLloZagphj/Kpgfw1TZ+njSDYqSLYE0NZ1\n" + + "j+83feECgYEA2aNGgbc+t6QLrJJ63l9Mz541lVV3IUAxZ5ACqOnMkQVuLoa5IMwM\n" + + "oENIo38ptfHQqjQ9x8/tEINFqOHnQuOJ/+1xP9f0Me+0clRDCqjGYqNYgmakKyD7\n" + + "vey/q6kwHk679RVGiI1p+HdoA+CbEKWHJiRxE0RhAA3G3wGAq7kpJocCgYEAxp4/\n" + + "tCft+eHVRivspfDN//axc2TR6qWP9E1ueGvbiXPXv0Puag0W9cER/df/s5jW4Rqg\n" + + "CE8649HPUZ0FJT+YaeKgu2Sw9SMcGl4/uyHzg7KnXIeYyQZJPqQkKyXmIix8cw3+\n" + + "HBGRtwX5nOy0DgFdaMiH0F08peNI9QHKKTBoWJECgYEAyymJ1ekzWMaAR1Zt8EvS\n" + + "LjWoG4EuthFwjRZ4BSpLVk1Vb4VAKAeS+cAVfNpmG3xip6Ag0/ebe0CvtFk9QsmZ\n" + + "txj2EP0M7div/9H8y2SF3OpS41fhhIlDtyXcPuivDHu/Jaf4sdwgwlrk9EmlN0Lu\n" + + "CIMYMz4vtpclwGNss+EjMt0CgYEAqepD0Vm/iuCaVhfJsgSaFvnywSdlNfpBdtyv\n" + + "PzH2dFa4IZZ55hwgoklznNgmlnyQh68BbVpqpO+fDtDnz//h4ePRYb84a96Hcj9j\n" + + "AjJ/YxF5f/04xfEsw/wkPQ2FHYM1TDCSTWzyXcMs0gTl3H1qbfPvzF+XPMt+ZKwN\n" + + "SMNy4SECgYB3ig6t+XVfNkw8oBOh0Gx37XKbmImXsA8ucDAX9KUbMIvD03XCEf34\n" + + "jF3SNJh0SmHoT62vc+cJqPxMDP6E7Q1nZxsEyaAkKr2H4dSM4SlRm0VB+bS+jXsz\n" + + "PCiRGSm8eupuxfix05LMMreo4mC7e3Ir4JhdCsXxAMZIvbNyXcvUMA==\n" + + "-----END CERTIFICATE-----\n" + }); + var request = 'onelogin_samlurn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'; + samlObj.requestToUrl(request, null, 'authorize', {}, function(err) { + try { + should.exist(err); + err.message.should.containEql('no start line'); + done(); + } catch (err2) { + done(err2); + } + }); + }); }); describe('validateRedirect()', function() {