Skip to content

Commit

Permalink
feat: enable elliptic curve es256 for JWS (#205)
Browse files Browse the repository at this point in the history
  • Loading branch information
kalinkrustev authored Jul 4, 2024
1 parent b7ec9f3 commit ee5036d
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 25 deletions.
20 changes: 10 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mojaloop/sdk-standard-components",
"version": "18.2.1",
"version": "18.3.0-snapshot.0",
"description": "A set of standard components for connecting to Mojaloop API enabled Switches",
"main": "src/index.js",
"types": "src/index.d.ts",
Expand Down Expand Up @@ -50,7 +50,7 @@
"@mojaloop/api-snippets": "^17.5.1",
"@types/jest": "^29.5.12",
"@types/node": "^20.14.9",
"audit-ci": "^7.0.1",
"audit-ci": "^7.1.0",
"eslint": "8.57.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-plugin-import": "2.29.1",
Expand All @@ -61,7 +61,7 @@
"pre-commit": "^1.2.2",
"replace": "^1.2.2",
"standard-version": "^9.5.0",
"typescript": "^5.5.2"
"typescript": "^5.5.3"
},
"standard-version": {
"scripts": {
Expand Down
13 changes: 6 additions & 7 deletions src/lib/jws/jwsSigner.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@
const jws = require('jws');
const safeStringify = require('fast-safe-stringify');

// the JWS signature algorithm to use. Note that Mojaloop spec requires RS256 at present
const SIGNATURE_ALGORITHM = 'RS256';

// a regular expression to extract the Mojaloop API spec compliant HTTP-URI header value
const uriRegex = /(?:^.*)(\/(participants|parties|quotes|bulkQuotes|transfers|bulkTransfers|transactionRequests|thirdpartyRequests|authorizations|consents|consentRequests|fxQuotes|fxTransfers|)(\/.*)*)$/;


Expand All @@ -31,6 +27,9 @@ class JwsSigner {
throw new Error('Signing key must be supplied as config argument');
}

// the JWS signature algorithm to use. Note that Mojaloop spec requires RS256 at present
this.alg = config.signingKey.includes('BEGIN EC ') ? 'ES256' : 'RS256';

this.signingKey = config.signingKey;
}

Expand All @@ -42,7 +41,7 @@ class JwsSigner {
* (see https://github.com/request/request-promise-native)
* (see https://github.com/axios/axios)
*/
sign(requestOptions) {
sign(requestOptions, alg) {
this.logger.isDebugEnabled && this.logger.debug(`JWS Signing request: ${safeStringify(requestOptions)}`);
const payload = requestOptions.body || requestOptions.data;
const uri = requestOptions.uri || requestOptions.url;
Expand All @@ -61,7 +60,7 @@ class JwsSigner {
requestOptions.headers['fspiop-uri'] = uriMatches[1];

// get the signature and add it to the header
requestOptions.headers['fspiop-signature'] = this.getSignature(requestOptions);
requestOptions.headers['fspiop-signature'] = this.getSignature(requestOptions, alg);

if(requestOptions.body && typeof(requestOptions.body) !== 'string') {
requestOptions.body = JSON.stringify(requestOptions.body);
Expand Down Expand Up @@ -99,7 +98,7 @@ class JwsSigner {
// Note: Property names are case sensitive in the protected header object even though they are
// not case sensitive in the actual HTTP headers
const protectedHeaderObject = {
alg: SIGNATURE_ALGORITHM,
alg: this.alg,
'FSPIOP-URI': requestOptions.headers['fspiop-uri'],
'FSPIOP-HTTP-Method': requestOptions.method.toUpperCase(),
'FSPIOP-Source': requestOptions.headers['fspiop-source']
Expand Down
9 changes: 4 additions & 5 deletions src/lib/jws/jwsValidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ const jwt = require('jsonwebtoken');
const safeStringify = require('fast-safe-stringify');

// the JWS signature algorithm to use. Note that Mojaloop spec requires RS256 at present
const SIGNATURE_ALGORITHM = 'RS256';

const SIGNATURE_ALGORITHMS = ['RS256', 'ES256'];

/**
* Provides methods for Mojaloop compliant JWS signing and signature verification
Expand Down Expand Up @@ -73,7 +72,7 @@ class JwsValidator {
// validate signature
const result = jwt.verify(token, pubKey, {
complete: true,
algorithms: [ SIGNATURE_ALGORITHM ] //only allow our SIGNATURE_ALGORITHM
algorithms: SIGNATURE_ALGORITHMS //only allow our SIGNATURE_ALGORITHM
});

// check protected header has all required fields and matches actual incoming headers
Expand Down Expand Up @@ -101,8 +100,8 @@ class JwsValidator {
if(!decodedProtectedHeader['alg']) {
throw new Error(`Decoded protected header does not contain required alg element: ${safeStringify(decodedProtectedHeader)}`);
}
if(decodedProtectedHeader.alg !== SIGNATURE_ALGORITHM) {
throw new Error(`Invalid protected header alg '${decodedProtectedHeader.alg}' should be '${SIGNATURE_ALGORITHM}'`);
if(!SIGNATURE_ALGORITHMS.includes(decodedProtectedHeader.alg)) {
throw new Error(`Invalid protected header alg '${decodedProtectedHeader.alg}' should be '${SIGNATURE_ALGORITHMS.join(' or ')}'`);
}

// check FSPIOP-URI is present and matches
Expand Down
97 changes: 97 additions & 0 deletions test/unit/jws.perf.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
'use strict';

const fs = require('fs');
const JwsTest = require('../../src/lib/jws');
const Signer = JwsTest.signer;
const Validator = JwsTest.validator;
const mockLogger = require('../__mocks__/mockLogger');
const crypto = require('crypto');

const signingKey = fs.readFileSync(__dirname + '/data/jwsSigningKey.pem');
const validationKey = fs.readFileSync(__dirname + '/data/jwsValidationKey.pem');
const key = {
'kty': 'EC',
'd': 'iYjERsNErBjCQljkeU8EJVAwU-dMxi_07vdYgTPRsx4',
'use': 'sig',
'crv': 'P-256',
'x': 'ok3_fYYnzhXij__aLGXKr0AKGjjUo1tAqt9z4jp3iog',
'y': '_AlRjdUsqPTbpRExkd5vNcsqCSKSDx31mBuMewZTcds',
'alg': 'ES256'
};

const signingKeyEC = crypto.createPrivateKey({format: 'jwk', key}).export({format: 'pem', type: 'sec1'});
const validationKeyEC = crypto.createPublicKey({format: 'jwk', key}).export({format: 'pem', type: 'spki'});

describe('JWS', () => {
let signer;
let signerEC;
let testOpts;
// let testOptsData;
let body;

beforeEach(() => {
signer = new Signer({
signingKey: signingKey,
logger: mockLogger({ app: 'jws-test' }, undefined)
});
signerEC = new Signer({
signingKey: signingKeyEC,
logger: mockLogger({ app: 'jws-test' }, undefined)
});
body = { test: 123 };
// An request-promise-native style request uses the `.uri` and `.body` properties instead of the `.url` and `.data` properties.
testOpts = {
headers: {
'fspiop-source': 'mojaloop-sdk',
'fspiop-destination': 'some-other-fsp',
'date': new Date().toISOString(),
},
method: 'PUT',
uri: 'https://someswitch.com:443/prefix/parties/MSISDN/12345678',
body,
};
});

function testValidateSignedRequest(shouldFail, key) {
const request = {
headers: testOpts.headers,
body: body,
};

const validate = () => {
const validator = new Validator({
validationKeys: {
'mojaloop-sdk': key
},
logger: mockLogger({ app: 'validate-test' }, undefined)
});
validator.validate(request);
};

if (shouldFail) {
expect(validate).toThrow();
} else {
validate();
}
}

test('Should generate valid JWS headers and signature for request with body', () => {
for (let i = 1; i < 1000; i++) signer.sign(testOpts);

expect(testOpts.headers['fspiop-signature']).toBeTruthy();
expect(testOpts.headers['fspiop-uri']).toBe('/parties/MSISDN/12345678');
expect(testOpts.headers['fspiop-http-method']).toBe('PUT');

testValidateSignedRequest(false, validationKey);
});

test('Should generate valid JWS headers and signature for request with body ES256', () => {
for (let i = 1; i < 1000; i++) signerEC.sign(testOpts, 'ES256');

expect(testOpts.headers['fspiop-signature']).toBeTruthy();
expect(testOpts.headers['fspiop-uri']).toBe('/parties/MSISDN/12345678');
expect(testOpts.headers['fspiop-http-method']).toBe('PUT');

testValidateSignedRequest(false, validationKeyEC);
});
});

0 comments on commit ee5036d

Please sign in to comment.