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 3 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"
}
}
}
5,528 changes: 5,520 additions & 8 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 4 additions & 2 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 @@ -64,6 +65,7 @@
"@mojaloop/central-services-shared": "9.5.5",
"@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 Down
3 changes: 3 additions & 0 deletions secrets/jwsSigningKey.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-----BEGIN PRIVATE KEY-----
some valid signature
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we not put a dev/test key here?

I.e. what happens if JWS signage is enabled and this default non-valid key is used?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. I think we should use a self generated default key pair and also, put the public key in the sdk-scheme-adapter image. Update docs to make it imperative for users/implementers to change the defaults in their own installations. We could include an openssl command to generate a new keypair.

-----END 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: 50 additions & 1 deletion 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
})
jwsSigner.sign(opts)
}

res = await axios.request(opts)
} catch (err) {
// external-error
Expand Down Expand Up @@ -1102,6 +1126,31 @@ class QuotesModel {
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'],
'User-Agent': null, // yuck! node-fetch INSISTS on sending a user-agent header!? infuriating!
shashi165 marked this conversation as resolved.
Show resolved Hide resolved
Accept: null
}

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

return this.removeEmptyKeys(ret)
}

/**
* Writes a formatted message to the console
*
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