From 364cd92ffb570501b1ccf8142b9a07e029f499d0 Mon Sep 17 00:00:00 2001 From: Kiran Date: Tue, 22 Feb 2022 11:30:14 +0530 Subject: [PATCH] Add response signing - wip --- lib/env.ts | 2 ++ package-lock.json | 28 ++++++++++++++++++++++++---- package.json | 5 +++-- utils/response.ts | 42 +++++++++++++++++++++++++++++++++++------- 4 files changed, 64 insertions(+), 13 deletions(-) diff --git a/lib/env.ts b/lib/env.ts index ee2001f..ace6175 100644 --- a/lib/env.ts +++ b/lib/env.ts @@ -1,3 +1,5 @@ +// import { fetchPrivateKey, fetchPublicKey } from "utils"; + const appUrl = process.env.APP_URL || 'http://localhost:4000'; const entityId = process.env.ENTITY_ID || 'http://saml.example.com'; const ssoUrl = `${appUrl}/api/saml/sso`; diff --git a/package-lock.json b/package-lock.json index 07b94c5..4701a44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,10 +6,10 @@ "": { "name": "fake", "dependencies": { - "@types/xml-crypto": "^1.4.2", "axios": "^0.24.0", "next": "12.1.0", "node-fetch": "^3.2.0", + "node-forge": "^1.2.1", "rambda": "^7.0.2", "react": "17.0.2", "react-dom": "17.0.2", @@ -21,6 +21,7 @@ "devDependencies": { "@types/node": "17.0.8", "@types/react": "17.0.38", + "@types/xml-crypto": "^1.4.2", "@types/xml2js": "0.4.9", "autoprefixer": "10.4.2", "eslint": "8.6.0", @@ -429,7 +430,8 @@ "node_modules/@types/node": { "version": "17.0.8", "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.8.tgz", - "integrity": "sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg==" + "integrity": "sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg==", + "dev": true }, "node_modules/@types/parse-json": { "version": "4.0.0", @@ -464,6 +466,7 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/@types/xml-crypto/-/xml-crypto-1.4.2.tgz", "integrity": "sha512-1kT+3gVkeBDg7Ih8NefxGYfCApwZViMIs5IEs5AXF6Fpsrnf9CLAEIRh0DYb1mIcRcvysVbe27cHsJD6rJi36w==", + "dev": true, "dependencies": { "@types/node": "*", "xpath": "0.0.27" @@ -473,6 +476,7 @@ "version": "0.0.27", "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.27.tgz", "integrity": "sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ==", + "dev": true, "engines": { "node": ">=0.6.0" } @@ -4817,6 +4821,14 @@ "url": "https://opencollective.com/node-fetch" } }, + "node_modules/node-forge": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz", + "integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==", + "engines": { + "node": ">= 6.13.0" + } + }, "node_modules/node-libs-browser": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", @@ -8288,7 +8300,8 @@ "@types/node": { "version": "17.0.8", "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.8.tgz", - "integrity": "sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg==" + "integrity": "sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg==", + "dev": true }, "@types/parse-json": { "version": "4.0.0", @@ -8323,6 +8336,7 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/@types/xml-crypto/-/xml-crypto-1.4.2.tgz", "integrity": "sha512-1kT+3gVkeBDg7Ih8NefxGYfCApwZViMIs5IEs5AXF6Fpsrnf9CLAEIRh0DYb1mIcRcvysVbe27cHsJD6rJi36w==", + "dev": true, "requires": { "@types/node": "*", "xpath": "0.0.27" @@ -8331,7 +8345,8 @@ "xpath": { "version": "0.0.27", "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.27.tgz", - "integrity": "sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ==" + "integrity": "sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ==", + "dev": true } } }, @@ -11787,6 +11802,11 @@ "formdata-polyfill": "^4.0.10" } }, + "node-forge": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz", + "integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==" + }, "node-libs-browser": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", diff --git a/package.json b/package.json index 6ac6f65..0a71b5c 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "axios": "^0.24.0", "next": "12.1.0", "node-fetch": "^3.2.0", + "node-forge": "^1.2.1", "rambda": "^7.0.2", "react": "17.0.2", "react-dom": "17.0.2", @@ -22,13 +23,13 @@ "devDependencies": { "@types/node": "17.0.8", "@types/react": "17.0.38", + "@types/xml-crypto": "^1.4.2", "@types/xml2js": "0.4.9", "autoprefixer": "10.4.2", "eslint": "8.6.0", "eslint-config-next": "12.0.7", "postcss": "8.4.6", "tailwindcss": "3.0.23", - "typescript": "4.5.4", - "@types/xml-crypto": "^1.4.2" + "typescript": "4.5.4" } } diff --git a/utils/response.ts b/utils/response.ts index 5d3b732..ce2b01c 100644 --- a/utils/response.ts +++ b/utils/response.ts @@ -2,6 +2,7 @@ import { User } from '../types'; import xmlbuilder from 'xmlbuilder'; import crypto from 'crypto'; import { SignedXml, FileKeyInfo } from 'xml-crypto'; +import { pki, util, asn1 } from 'node-forge'; const createResponseXML = async (params: { idpIdentityId: string, @@ -66,15 +67,15 @@ const createResponseXML = async (params: { '@Destination': acsUrl, '@InResponseTo': inResponseTo, '@IssueInstant': authTimestamp, - 'saml:Issuer': { - '@xmlns:saml': 'urn:oasis:names:tc:SAML:2.0:assertion', - '#text': idpIdentityId, - }, 'samlp:Status': { 'samlp:StatusCode': { '@Value': 'urn:oasis:names:tc:SAML:2.0:status:Success' } }, + 'saml:Issuer': { + '@xmlns:saml': 'urn:oasis:names:tc:SAML:2.0:assertion', + '#text': idpIdentityId, + }, 'saml:Assertion': { '@xmlns:saml': 'urn:oasis:names:tc:SAML:2.0:assertion', '@Version': '2.0', @@ -141,15 +142,42 @@ const createResponseForm = (relayState: string, encodedSamlResponse: string, acs return formElements.join(''); }; +function getPublicKeyPemFromCertificate(x509Certificate: string) { + const certDerBytes = util.decode64(x509Certificate); + const obj = asn1.fromDer(certDerBytes); + const cert = pki.certificateFromAsn1(obj); + return pki.publicKeyToPem(cert.publicKey); +} + +const stripCertHeaderAndFooter = (cert: string): string => { + cert = cert.replace(/-+BEGIN CERTIFICATE-+\r?\n?/, ''); + cert = cert.replace(/-+END CERTIFICATE-+\r?\n?/, ''); + cert = cert.replace(/\r\n/g, '\n'); + return cert; +}; + +function GetKeyInfo(x509Certificate: string, signatureConfig: any = {}) { + x509Certificate = stripCertHeaderAndFooter(x509Certificate); + + this.getKeyInfo = () => { + const prefix = signatureConfig.prefix ? `${signatureConfig.prefix}:` : ''; + return `<${prefix}X509Data><${prefix}X509Certificate>${x509Certificate}`; + }; + + this.getKey = () => { + return getPublicKeyPemFromCertificate(x509Certificate).toString(); + }; +} + const signResponseXML = async (xml: string, signingKey: any, publicKey: any): Promise => { const sig = new SignedXml(); const responseXPath = '/*[local-name(.)="Response" and namespace-uri(.)="urn:oasis:names:tc:SAML:2.0:protocol"]'; const issuerXPath = '/*[local-name(.)="Issuer" and namespace-uri(.)="urn:oasis:names:tc:SAML:2.0:assertion"]'; - console.log({publicKey, signingKey}) - sig.signatureAlgorithm = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'; - sig.keyInfoProvider = new FileKeyInfo(publicKey); + + // @ts-ignore + sig.keyInfoProvider = new GetKeyInfo(publicKey, {}); sig.signingKey = signingKey; sig.addReference(responseXPath, ['http://www.w3.org/2000/09/xmldsig#enveloped-signature', 'http://www.w3.org/2001/10/xml-exc-c14n#'], 'http://www.w3.org/2001/04/xmlenc#sha256');