Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added JWS support for switch generated msg #203

Merged
merged 11 commits into from
May 19, 2020
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,12 @@
"includeCauseExtension": false,
"truncateExtensions": true
},
"SIMPLE_ROUTING_MODE": true
"SIMPLE_ROUTING_MODE": true,
"ENDPOINT_SECURITY":{
"JWS": {
"JWS_SIGN": false,
"FSPIOP_SOURCE_TO_SIGN": "switch",
"JWS_SIGNING_KEY_PATH": "secrets/jwsSigningKey.key"
}
}
}
1,189 changes: 773 additions & 416 deletions package-lock.json

Large diffs are not rendered by default.

17 changes: 10 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "quoting-service",
"description": "Quoting Service hosted by a scheme",
"license": "Apache-2.0",
"version": "10.1.1",
"version": "10.1.2",
"author": "ModusBox",
"contributors": [
"James Bush <james.bush@modusbox.com>",
Expand All @@ -11,7 +11,8 @@
"Miguel de Barros <miguel.debarros@modusbox.com>",
"Rajiv Mothilal <rajiv.mothilal@modusbox.com>",
"Steven Oderayi <steven.oderayi@modusbox.com>",
"Vassilis Barzokas <vassilis.barzokas@modusbox.com>"
"Vassilis Barzokas <vassilis.barzokas@modusbox.com>",
"Shashikant Hirugade <shashikant.hirugade@modusbox.com>"
],
"repository": {
"type": "git",
Expand Down Expand Up @@ -59,11 +60,12 @@
"dependencies": {
"@hapi/good": "9.0.0",
"@hapi/hapi": "18.4.1",
"@mojaloop/central-services-error-handling": "9.1.0",
"@mojaloop/central-services-error-handling": "10.2.0",
"@mojaloop/central-services-logger": "9.5.1",
"@mojaloop/central-services-shared": "9.5.5",
"@mojaloop/central-services-shared": "10.1.1",
"@mojaloop/event-sdk": "9.5.2",
"@mojaloop/ml-number": "8.2.0",
"@mojaloop/sdk-standard-components": "10.1.0",
"axios": "0.19.2",
"blipp": "4.0.1",
"eslint-config-standard": "14.1.1",
Expand All @@ -73,23 +75,24 @@
"json-rules-engine": "5.0.2",
"knex": "0.21.1",
"memory-cache": "0.2.0",
"minimist": "^1.2.5",
"mysql": "2.18.1",
"node-fetch": "2.6.0",
"parse-strings-in-object": "2.0.0",
"rc": "1.2.8"
},
"devDependencies": {
"@types/jest": "25.2.1",
"eslint": "6.8.0",
"eslint": "7.0.0",
"jest": "26.0.1",
"jest-junit": "10.0.0",
"npm-audit-resolver": "2.2.0",
"npm-check-updates": "4.1.2",
"npm-check-updates": "5.0.0",
"nyc": "15.0.1",
"pre-commit": "1.2.2",
"proxyquire": "2.1.3",
"sinon": "9.0.2",
"standard": "14.3.3",
"standard": "14.3.4",
"swagmock": "1.0.0"
},
"generator-swaggerize": {
Expand Down
27 changes: 27 additions & 0 deletions secrets/jwsSigningKey.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAvs6EjaQYxTPI2dS+L6zJQhszHCP79huvCDPhy/BmTWI4350r
jWiclBC/joy7BUGJszIVv6rrwbeN9OORHXwW0XSjcCEeksdkAee9kGvx/A2UcUOs
QQ3A3IvoxJAv+HJSmCGeKsc2pMrelBtma54i1JBF6H+SwrcM0a+UVsADQXOzKuyi
RYnkCEhZYHXAk7hM3cARvhYydqUnPehFccjS8v6sNTRZnqJ7z/XT3EKYkfizcCkH
iV9elB/PGGzkFWLjHZVTC/+hK6WhFYGEktNbQfDuMA4mnZT37QZEL1PlhnFfjfnr
npjFjcFzKJ+hHRrHqA9FZlo6J9efnUn/VHVCsQIDAQABAoIBAC5eFUwuUm6ptOdq
kYUYYhgUcAkonXQjKhe+M+LRWqr3cYa1tP26LEK3BzXm8RPH52SKiUp4iK4covyd
yIf3vbvMsmLvSXS1y5HC9QgBk16DFk2n+lYct39g6IHL4Dxp27Rohv0ySA8n1Yr8
VZlVZnVtUe+8ALgcr5rNJikmpCPlQ85QFExS8JpxYbbczbm/Q7qcQGdRkop5oFR6
0Zaze7L2F2M1C6IVzUUJKr+TVY58uWqCEqhRDadStkQgw9QPq+PVza5fnTtlIakK
sSk1orJqONWpMY90wNOk/DasMIrQK4RFlkkDgTPjvWSPDRG/p/iRxO2jde5/oyCU
b24TraECgYEA90yIBp3/0BgKE6KPBZzlN8SuwXXH6A3HBXvcVNaNucO5zgXSJcYy
UT8iIUUd1bndfFOv3W8c6bkkeX5fkLm35bqM442m7r56PeugxIpRFPaj6IoTqe3y
9TCaatnOztDjCBEaNcIrJZUzHj6/yffr5pakTcsyBPhpywHGfBNeu9UCgYEAxYUm
bDJOK4NrkUFRGdN6KuWQ1MYrFxFChO9bmmxntt3ncC79qPojmTVCrajjarIBbv97
v/D8PoSf4MKK9qDDdKqZ0dViYUyQm0rbWIxWfVT2fWdy5Xr34BLGkyiyuGam1XBy
XR76uNTjT+z8VIMkXv2mi/tW9XecGTWTx6BTpW0CgYEA6z8jOuA8RDFKDKmb0iQR
LL7t2tz3ALlydLYBv7al9WvKKcEByqtCnGUpS2rbWb/IgQd/CW80muVjXCuqLxts
Y53VfnX2sbACyzqFodFOFloVTBVlZUYU08ebbT+q6rRs8/SI8VC3CLZofUIF4dP0
1VLCE7rS50WEJfiXXtpySM0CgYAuJf3kKZy57eqj9W7NaeuROjGshOBsc0vnY0Uz
v0A8KXx5aYE6ibbnhNofWOZJd21+UZLINbW5paL1EH8HQGhqSbHGpORnd0wR/Ftj
ROawQbmVcyYVF7XyVHCw0UFwdAtmsu8FcseBh7vUYLpTxJAq4XiYHBElKNz/xtTQ
P1VLwQKBgBZuwI0/s/VORrmvok27Wbkup9dDA3XlMccyK2osf31IcH7JY4uMw+9i
RYVjMjIsPtO1Gf71mRcFsWkQYYtZ6V5yrxiLRVq8iSRkOPm2iJtolSg1edOrlVS6
xLPUjxxopeTkZbi4heszkkOn8MvzRznONCXjDt98FubgcAmEr+XA
-----END RSA PRIVATE KEY-----
15 changes: 15 additions & 0 deletions src/lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,20 @@
******/

const RC = require('parse-strings-in-object')(require('rc')('QUOTE', require('../../config/default.json')))
const fs = require('fs')

/**
* Loads config from environment
*/
class Config {
getFileContent (path) {
if (!fs.existsSync(path)) {
console.log(`File ${path} doesn't exist, can't enable JWS signing`)
throw new Error('File doesn\'t exist')
}
return fs.readFileSync(path)
}

constructor () {
// load config from environment (or use sensible defaults)
this.listenAddress = RC.LISTEN_ADDRESS
Expand Down Expand Up @@ -80,6 +89,12 @@ class Config {
debug: RC.DATABASE.DEBUG ? RC.DATABASE.DEBUG : false
}
this.errorHandling = RC.ERROR_HANDLING
this.jws = {
jwsSign: RC.ENDPOINT_SECURITY.JWS.JWS_SIGN,
fspiopSourceToSign: RC.ENDPOINT_SECURITY.JWS.FSPIOP_SOURCE_TO_SIGN,
jwsSigningKeyPath: RC.ENDPOINT_SECURITY.JWS.JWS_SIGNING_KEY_PATH,
jwsSigningKey: RC.ENDPOINT_SECURITY.JWS.JWS_SIGN ? this.getFileContent(RC.ENDPOINT_SECURITY.JWS.JWS_SIGNING_KEY_PATH) : undefined
}
}
}

Expand Down
51 changes: 49 additions & 2 deletions src/model/quotes.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const EventSdk = require('@mojaloop/event-sdk')
const LibUtil = require('@mojaloop/central-services-shared').Util
const Logger = require('@mojaloop/central-services-logger')
const MLNumber = require('@mojaloop/ml-number')
const JwsSigner = require('@mojaloop/sdk-standard-components').Jws.signer

const Config = require('../lib/config')
const { httpRequest } = require('../lib/http')
Expand Down Expand Up @@ -882,12 +883,15 @@ class QuotesModel {

// make an error callback
let fromSwitchHeaders
let formattedHeaders

// modify/set the headers only in case it is explicitly requested to do so
// as this part needs to cover two different cases:
// 1. (do not modify them) when the Switch needs to relay an error, e.g. from a DFSP to another
// 2. (modify/set them) when the Switch needs send errors that are originating in the Switch, e.g. to send an error back to the caller
if (modifyHeaders === true) {
// Should not forward 'fspiop-signature' header for switch generated messages
delete headers['fspiop-signature']
fromSwitchHeaders = Object.assign({}, headers, {
'fspiop-destination': fspiopSource,
'fspiop-source': ENUM.Http.Headers.FSPIOP.SWITCH.value,
Expand All @@ -898,13 +902,20 @@ class QuotesModel {
fromSwitchHeaders = Object.assign({}, headers)
}

// JWS Signer expects headers in lowercase
if (envConfig.jws && envConfig.jws.jwsSign && fromSwitchHeaders['fspiop-source'] === envConfig.jws.fspiopSourceToSign) {
formattedHeaders = this.generateRequestHeadersForJWS(fromSwitchHeaders, true)
} else {
formattedHeaders = this.generateRequestHeaders(fromSwitchHeaders, true)
}

let opts = {
method: ENUM.Http.RestMethods.PUT,
url: fullCallbackUrl,
data: JSON.stringify(fspiopError.toApiErrorObject(envConfig.errorHandling), LibUtil.getCircularReplacer()),
// use headers of the error object if they are there...
// otherwise use sensible defaults
headers: this.generateRequestHeaders(fromSwitchHeaders, true)
headers: formattedHeaders
}

if (span) {
Expand All @@ -914,6 +925,19 @@ class QuotesModel {

let res
try {
// If JWS is enabled and the 'fspiop-source' matches the configured jws header value('switch')
// that means it's a switch generated message and we need to sign it
if (envConfig.jws && envConfig.jws.jwsSign && opts.headers['fspiop-source'] === envConfig.jws.fspiopSourceToSign) {
const logger = Logger
logger.log = logger.info
this.writeLog('Getting the JWS Signer to sign the switch generated message')
const jwsSigner = new JwsSigner({
logger,
signingKey: envConfig.jws.jwsSigningKey
})
opts.headers['fspiop-signature'] = jwsSigner.getSignature(opts)
}

res = await axios.request(opts)
} catch (err) {
// external-error
Expand Down Expand Up @@ -1091,7 +1115,30 @@ class QuotesModel {
'FSPIOP-HTTP-Method': headers['fspiop-http-method'],
'FSPIOP-Signature': headers['fspiop-signature'],
'FSPIOP-URI': headers['fspiop-uri'],
'User-Agent': null, // yuck! node-fetch INSISTS on sending a user-agent header!? infuriating!
Accept: null
}

if (!noAccept) {
ret.Accept = 'application/vnd.interoperability.quotes+json;version=1'
}

return this.removeEmptyKeys(ret)
}

/**
* Generates and returns an object containing API spec compliant lowercase HTTP request headers for JWS Signing
*
* @returns {object}
*/
generateRequestHeadersForJWS (headers, noAccept) {
const ret = {
'Content-Type': 'application/vnd.interoperability.quotes+json;version=1.0',
date: headers.date,
'fspiop-source': headers['fspiop-source'],
'fspiop-destination': headers['fspiop-destination'],
'fspiop-http-method': headers['fspiop-http-method'],
'fspiop-signature': headers['fspiop-signature'],
'fspiop-uri': headers['fspiop-uri'],
Accept: null
}

Expand Down
34 changes: 33 additions & 1 deletion test/unit/lib/config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,14 @@ const mockDefaultFile = {
includeCauseExtension: false,
truncateExtensions: true
},
SIMPLE_ROUTING_MODE: true
SIMPLE_ROUTING_MODE: true,
ENDPOINT_SECURITY: {
JWS: {
JWS_SIGN: true,
FSPIOP_SOURCE_TO_SIGN: 'switch',
JWS_SIGNING_KEY_PATH: 'secrets/jwsSigningKey.key'
}
}
}

describe('Config', () => {
Expand All @@ -80,4 +87,29 @@ describe('Config', () => {
expect(result.amount.scale).toBe(4)
expect(result.database.debug).toBe(true)
})

it('throws when JWS Signing key file is not provided', () => {
// Arrange
jest.mock('../../../config/default.json', () => ({
...mockDefaultFile,
ENDPOINT_SECURITY: {
JWS: {
JWS_SIGN: true,
FSPIOP_SOURCE_TO_SIGN: 'switch',
JWS_SIGNING_KEY_PATH: '/fake/path'
}
}
}), { virtual: true })

const Config = require('../../../src/lib/config')

// Act
try {
const result = new Config()
expect(result).toBeUndefined()
} catch (error) {
expect(error).toBeInstanceOf(Error)
expect(error).toHaveProperty('message', 'File doesn\'t exist')
}
})
})
Loading