diff --git a/package.json b/package.json index 707c88f5..2f881ae6 100644 --- a/package.json +++ b/package.json @@ -46,9 +46,10 @@ "@ava/typescript": "^1.1.1", "@types/node": "^11.11.3", "@types/node-forge": "^0.10.3", + "@types/node-rsa": "^1.1.1", "@types/pako": "^1.0.1", - "@types/xmldom": "^0.1.31", "@types/uuid": "3.0.0", + "@types/xmldom": "^0.1.31", "ava": "^3.15.0", "coveralls": "^3.1.0", "nyc": "^15.0.1", diff --git a/src/binding-redirect.ts b/src/binding-redirect.ts index c4adc01b..62878bd5 100644 --- a/src/binding-redirect.ts +++ b/src/binding-redirect.ts @@ -62,7 +62,18 @@ function buildRedirectURL(opts: BuildRedirectConfig) { if (isSigned) { const sigAlg = pvPair(urlParams.sigAlg, encodeURIComponent(entitySetting.requestSignatureAlgorithm)); const octetString = samlRequest + relayState + sigAlg; - return baseUrl + pvPair(queryParam, octetString, noParams) + pvPair(urlParams.signature, encodeURIComponent(libsaml.constructMessageSignature(queryParam + '=' + octetString, entitySetting.privateKey, entitySetting.privateKeyPass, undefined, entitySetting.requestSignatureAlgorithm))); + return baseUrl + + pvPair(queryParam, octetString, noParams) + + pvPair(urlParams.signature, encodeURIComponent( + libsaml.constructMessageSignature( + queryParam + '=' + octetString, + entitySetting.privateKey, + entitySetting.privateKeyPass, + undefined, + entitySetting.requestSignatureAlgorithm + ).toString() + ) + ); } return baseUrl + pvPair(queryParam, samlRequest + relayState, noParams); } @@ -140,7 +151,7 @@ function loginResponseRedirectURL(requestInfo: any, entity: any, user: any = {}, const selectedNameIDFormat = Array.isArray(nameIDFormat) ? nameIDFormat[0] : nameIDFormat; const nowTime = new Date(); // Five minutes later : nowtime + 5 * 60 * 1000 (in milliseconds) - const fiveMinutesLaterTime = new Date(nowTime.getTime() + 300_000 ); + const fiveMinutesLaterTime = new Date(nowTime.getTime() + 300_000); const tvalue: any = { ID: id, AssertionID: idpSetting.generateID(), diff --git a/src/binding-simplesign.ts b/src/binding-simplesign.ts index a5af5535..532e9136 100644 --- a/src/binding-simplesign.ts +++ b/src/binding-simplesign.ts @@ -60,7 +60,13 @@ function buildSimpleSignature(opts: BuildSimpleSignConfig) : string { const sigAlg = pvPair(urlParams.sigAlg, entitySetting.requestSignatureAlgorithm); const octetString = context + relayState + sigAlg; - return libsaml.constructMessageSignature(queryParam + '=' + octetString, entitySetting.privateKey, entitySetting.privateKeyPass, undefined, entitySetting.requestSignatureAlgorithm); + return libsaml.constructMessageSignature( + queryParam + '=' + octetString, + entitySetting.privateKey, + entitySetting.privateKeyPass, + undefined, + entitySetting.requestSignatureAlgorithm + ).toString(); } /** diff --git a/src/libsaml.ts b/src/libsaml.ts index 9521bcfd..6718c8c2 100644 --- a/src/libsaml.ts +++ b/src/libsaml.ts @@ -7,9 +7,9 @@ import { DOMParser } from '@xmldom/xmldom'; import utility, { flattenDeep, isString } from './utility'; import { algorithms, wording, namespace } from './urn'; -import { select, SelectedValue } from 'xpath'; +import { select } from 'xpath'; import { MetadataInterface } from './metadata'; -import * as nrsa from 'node-rsa'; +import nrsa, { SigningSchemeHash } from 'node-rsa'; import { SignedXml, FileKeyInfo } from 'xml-crypto'; import * as xmlenc from '@authenio/xml-encryption'; import { extract } from './extractor'; @@ -60,7 +60,7 @@ export interface LoginResponseAttribute { export interface LoginResponseAdditionalTemplates { attributeStatementTemplate?: AttributeStatementTemplate; - attributeTemplate?:AttributeTemplate; + attributeTemplate?: AttributeTemplate; } export interface BaseSamlTemplate { @@ -91,7 +91,7 @@ export interface LibSamlInterface { getQueryParamByType: (type: string) => string; createXPath: (local, isExtractAll?: boolean) => string; replaceTagsByValue: (rawXML: string, tagValues: any) => string; - attributeStatementBuilder: (attributes: LoginResponseAttribute[], attributeTemplate : AttributeTemplate, attributeStatementTemplate : AttributeStatementTemplate) => string; + attributeStatementBuilder: (attributes: LoginResponseAttribute[], attributeTemplate: AttributeTemplate, attributeStatementTemplate: AttributeStatementTemplate) => string; constructSAMLSignature: (opts: SignatureConstructor) => string; verifySignature: (xml: string, opts) => [boolean, any]; createKeySection: (use: KeyUse, cert: string | Buffer) => {}; @@ -132,9 +132,9 @@ const libSaml = () => { * */ const nrsaAliasMapping = { - 'http://www.w3.org/2000/09/xmldsig#rsa-sha1': 'sha1', - 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256': 'sha256', - 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512': 'sha512', + 'http://www.w3.org/2000/09/xmldsig#rsa-sha1': 'pkcs1-sha1', + 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256': 'pkcs1-sha256', + 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512': 'pkcs1-sha512', }; /** * @desc Default login request template @@ -164,7 +164,7 @@ const libSaml = () => { * @type {AttributeTemplate} */ const defaultAttributeTemplate = { - context: '{Value}', + context: '{Value}', }; /** @@ -174,7 +174,7 @@ const libSaml = () => { const defaultLoginResponseTemplate = { context: '{Issuer}{Issuer}{NameID}{Audience}{AuthnStatement}{AttributeStatement}', attributes: [], - additionalTemplates:{ + additionalTemplates: { "attributeStatementTemplate": defaultAttributeStatementTemplate, "attributeTemplate": defaultAttributeTemplate } @@ -192,14 +192,14 @@ const libSaml = () => { * @param {string} sigAlg signature algorithm * @return {string/null} signing algorithm short-hand for the module node-rsa */ - function getSigningScheme(sigAlg?: string): string | null { + function getSigningScheme(sigAlg?: string): SigningSchemeHash { if (sigAlg) { const algAlias = nrsaAliasMapping[sigAlg]; if (!(algAlias === undefined)) { return algAlias; } } - return nrsaAliasMapping[signatureAlgorithms.RSA_SHA1]; // default value + return nrsaAliasMapping[signatureAlgorithms.RSA_SHA1]; } /** * @private @@ -270,27 +270,25 @@ const libSaml = () => { * @param {AttributeStatementTemplate} attributeStatementTemplate the attributStatement tag template to be used * @return {string} */ - attributeStatementBuilder(attributes: LoginResponseAttribute[], attributeTemplate : AttributeTemplate, attributeStatementTemplate : AttributeStatementTemplate): string { - if (!attributeStatementTemplate){ - attributeStatementTemplate = defaultAttributeStatementTemplate; - } - if (!attributeTemplate){ - attributeTemplate = defaultAttributeTemplate; - } - const attr = attributes.map(({ name, nameFormat, valueTag, valueXsiType, valueXmlnsXs, valueXmlnsXsi }) => { - const defaultValueXmlnsXs = 'http://www.w3.org/2001/XMLSchema'; - const defaultValueXmlnsXsi = 'http://www.w3.org/2001/XMLSchema-instance'; - let attributeLine = attributeTemplate.context; - attributeLine = attributeLine.replace('{Name}',name); - attributeLine = attributeLine.replace('{NameFormat}',nameFormat); - attributeLine = attributeLine.replace('{ValueXmlnsXs}',valueXmlnsXs ? valueXmlnsXs : defaultValueXmlnsXs); - attributeLine = attributeLine.replace('{ValueXmlnsXsi}',valueXmlnsXsi ? valueXmlnsXsi : defaultValueXmlnsXsi); - attributeLine = attributeLine.replace('{ValueXsiType}',valueXsiType); - attributeLine = attributeLine.replace('{Value}',`{${tagging('attr', valueTag)}}`); - return attributeLine; - }).join(''); - return attributeStatementTemplate.context.replace('{Attributes}',attr); -}, + attributeStatementBuilder( + attributes: LoginResponseAttribute[], + attributeTemplate: AttributeTemplate = defaultAttributeTemplate, + attributeStatementTemplate: AttributeStatementTemplate = defaultAttributeStatementTemplate + ): string { + const attr = attributes.map(({ name, nameFormat, valueTag, valueXsiType, valueXmlnsXs, valueXmlnsXsi }) => { + const defaultValueXmlnsXs = 'http://www.w3.org/2001/XMLSchema'; + const defaultValueXmlnsXsi = 'http://www.w3.org/2001/XMLSchema-instance'; + let attributeLine = attributeTemplate.context; + attributeLine = attributeLine.replace('{Name}', name); + attributeLine = attributeLine.replace('{NameFormat}', nameFormat); + attributeLine = attributeLine.replace('{ValueXmlnsXs}', valueXmlnsXs ? valueXmlnsXs : defaultValueXmlnsXs); + attributeLine = attributeLine.replace('{ValueXmlnsXsi}', valueXmlnsXsi ? valueXmlnsXsi : defaultValueXmlnsXsi); + attributeLine = attributeLine.replace('{ValueXsiType}', valueXsiType); + attributeLine = attributeLine.replace('{Value}', `{${tagging('attr', valueTag)}}`); + return attributeLine; + }).join(''); + return attributeStatementTemplate.context.replace('{Attributes}', attr); + }, /** * @desc Construct the XML signature for POST binding @@ -537,12 +535,22 @@ const libSaml = () => { * @param {string} signingAlgorithm signing algorithm * @return {string} message signature */ - constructMessageSignature(octetString: string, key: string, passphrase?: string, isBase64?: boolean, signingAlgorithm?: string) { + constructMessageSignature( + octetString: string, + key: string, + passphrase?: string, + isBase64?: boolean, + signingAlgorithm?: string + ) { // Default returning base64 encoded signature // Embed with node-rsa module - const decryptedKey = new nrsa(utility.readPrivateKey(key, passphrase), { - signingScheme: getSigningScheme(signingAlgorithm), - }); + const decryptedKey = new nrsa( + utility.readPrivateKey(key, passphrase), + 'private', + { + signingScheme: getSigningScheme(signingAlgorithm), + } + ); const signature = decryptedKey.sign(octetString); // Use private key to sign data return isBase64 !== false ? signature.toString('base64') : signature; @@ -555,11 +563,16 @@ const libSaml = () => { * @param {string} verifyAlgorithm algorithm used to verify * @return {boolean} verification result */ - verifyMessageSignature(metadata, octetString: string, signature: string | Buffer, verifyAlgorithm?: string) { + verifyMessageSignature( + metadata, + octetString: string, + signature: string | Buffer, + verifyAlgorithm?: string + ) { const signCert = metadata.getX509Certificate(certUse.signing); const signingScheme = getSigningScheme(verifyAlgorithm); - const key = new nrsa(utility.getPublicKeyPemFromCertificate(signCert), { signingScheme }); - return key.verify(new Buffer(octetString), signature); + const key = new nrsa(utility.getPublicKeyPemFromCertificate(signCert), 'public', { signingScheme }); + return key.verify(Buffer.from(octetString), Buffer.from(signature)); }, /** * @desc Get the public key in string format diff --git a/src/metadata-idp.ts b/src/metadata-idp.ts index 1918d5a1..89fda7c4 100644 --- a/src/metadata-idp.ts +++ b/src/metadata-idp.ts @@ -8,7 +8,7 @@ import { MetadataIdpOptions, MetadataIdpConstructor } from './types'; import { namespace } from './urn'; import libsaml from './libsaml'; import { isNonEmptyArray, isString } from './utility'; -import * as xml from 'xml'; +import xml from 'xml'; export interface IdpMetadataInterface extends MetadataInterface { diff --git a/src/metadata-sp.ts b/src/metadata-sp.ts index a33ecc76..977624a2 100644 --- a/src/metadata-sp.ts +++ b/src/metadata-sp.ts @@ -8,7 +8,7 @@ import { MetadataSpConstructor, MetadataSpOptions } from './types'; import { namespace, elementsOrder as order } from './urn'; import libsaml from './libsaml'; import { isNonEmptyArray, isString } from './utility'; -import * as xml from 'xml'; +import xml from 'xml'; export interface SpMetadataInterface extends MetadataInterface { diff --git a/test/flow.ts b/test/flow.ts index 16f94670..62ea15d2 100644 --- a/test/flow.ts +++ b/test/flow.ts @@ -7,7 +7,7 @@ import * as url from 'url'; import util from '../src/utility'; import * as tk from 'timekeeper'; -import * as validator from '@authenio/samlify-xsd-schema-validator'; +// import * as validator from '@authenio/samlify-xsd-schema-validator'; // import * as validator from '@authenio/samlify-validate-with-xmllint'; // import * as validator from '@authenio/samlify-node-xmllint'; // import * as validator from '@authenio/samlify-libxml-xsd'; @@ -17,7 +17,7 @@ import * as validator from '@authenio/samlify-xsd-schema-validator'; // const validator = require('@authenio/samlify-node-xmllint'); // const validator = require('@authenio/samlify-libxml-xsd'); -esaml2.setSchemaValidator(validator); +// esaml2.setSchemaValidator(validator); const isString = util.isString; diff --git a/test/index.ts b/test/index.ts index 07cfe83e..0f7bbf04 100644 --- a/test/index.ts +++ b/test/index.ts @@ -164,15 +164,15 @@ test('getAssertionConsumerService with two bindings', t => { }); test('verify stringified SAML message signed with RSA-SHA1', t => { const signature = libsaml.constructMessageSignature(octetString, _spPrivPem, _spPrivKeyPass); - t.is(libsaml.verifyMessageSignature(SPMetadata, octetString, Buffer.from(signature, 'base64')), true); + t.is(libsaml.verifyMessageSignature(SPMetadata, octetString, Buffer.from(signature.toString(), 'base64')), true); }); test('verify stringified SAML message signed with RSA-SHA256', t => { const signature = libsaml.constructMessageSignature(octetStringSHA256, _spPrivPem, _spPrivKeyPass); - t.is(libsaml.verifyMessageSignature(SPMetadata, octetStringSHA256, Buffer.from(signature, 'base64')), true); + t.is(libsaml.verifyMessageSignature(SPMetadata, octetStringSHA256, Buffer.from(signature.toString(), 'base64')), true); }); test('verify stringified SAML message signed with RSA-SHA512', t => { const signature = libsaml.constructMessageSignature(octetStringSHA512, _spPrivPem, _spPrivKeyPass); - t.is(libsaml.verifyMessageSignature(SPMetadata, octetStringSHA512, Buffer.from(signature, 'base64')), true); + t.is(libsaml.verifyMessageSignature(SPMetadata, octetStringSHA512, Buffer.from(signature.toString(), 'base64')), true); }); test('construct signature with RSA-SHA1', t => { t.is(libsaml.constructSAMLSignature({ diff --git a/yarn.lock b/yarn.lock index 92fbaa87..f12b4443 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,14 +2,14 @@ # yarn lockfile v1 -"@authenio/xml-encryption@^1.2.4": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@authenio/xml-encryption/-/xml-encryption-1.2.4.tgz#6e3310a2d629859554404cc1a213a68f4f055c08" - integrity sha512-71ENOSuUbXh5WO5oVQx7JPMdbPoXOYxfZvxWlaQEMzy8/psz1UR2r2QVsFiqbnlaJUGaKUo/HoNPB3Bq97zVxg== +"@authenio/xml-encryption@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@authenio/xml-encryption/-/xml-encryption-1.3.0.tgz#9079f227d5f54daf8bec25245a7a98a46186445d" + integrity sha512-643tVCHSXDXOk8vBt37I0iSgvoO+yJrj1iY/RuQDePoyF2jveUyQhSHYmFdXsG63Zmx/4zmKU9DFjGGKcuIduw== dependencies: + "@xmldom/xmldom" "^0.7.0" escape-html "^1.0.3" node-forge "^0.10.0" - xmldom "~0.6.0" xpath "0.0.32" "@ava/typescript@^1.1.1": @@ -261,6 +261,13 @@ dependencies: "@types/node" "*" +"@types/node-rsa@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/node-rsa/-/node-rsa-1.1.1.tgz#c3649c0ae7fdb93d3e65076a08d3fc9feb017a67" + integrity sha512-itzxtaBgk4OMbrCawVCvas934waMZWjW17v7EYgFVlfYS/cl0/P7KZdojWCq9SDJMI5cnLQLUP8ayhVCTY8TEg== + dependencies: + "@types/node" "*" + "@types/node@*": version "13.13.5" resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.5.tgz#96ec3b0afafd64a4ccea9107b75bf8489f0e5765" @@ -2919,11 +2926,6 @@ xml@^1.0.1: resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" integrity sha1-eLpyAgApxbyHuKgaPPzXS0ovweU= -xmldom@~0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.6.0.tgz#43a96ecb8beece991cef382c08397d82d4d0c46f" - integrity sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg== - xpath@0.0.32, xpath@^0.0.32: version "0.0.32" resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.32.tgz#1b73d3351af736e17ec078d6da4b8175405c48af"