Skip to content

Commit ee5036d

Browse files
authored
feat: enable elliptic curve es256 for JWS (#205)
1 parent b7ec9f3 commit ee5036d

5 files changed

Lines changed: 120 additions & 25 deletions

File tree

‎package-lock.json‎

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@mojaloop/sdk-standard-components",
3-
"version": "18.2.1",
3+
"version": "18.3.0-snapshot.0",
44
"description": "A set of standard components for connecting to Mojaloop API enabled Switches",
55
"main": "src/index.js",
66
"types": "src/index.d.ts",
@@ -50,7 +50,7 @@
5050
"@mojaloop/api-snippets": "^17.5.1",
5151
"@types/jest": "^29.5.12",
5252
"@types/node": "^20.14.9",
53-
"audit-ci": "^7.0.1",
53+
"audit-ci": "^7.1.0",
5454
"eslint": "8.57.0",
5555
"eslint-config-airbnb-base": "15.0.0",
5656
"eslint-plugin-import": "2.29.1",
@@ -61,7 +61,7 @@
6161
"pre-commit": "^1.2.2",
6262
"replace": "^1.2.2",
6363
"standard-version": "^9.5.0",
64-
"typescript": "^5.5.2"
64+
"typescript": "^5.5.3"
6565
},
6666
"standard-version": {
6767
"scripts": {

‎src/lib/jws/jwsSigner.js‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,6 @@
1313
const jws = require('jws');
1414
const safeStringify = require('fast-safe-stringify');
1515

16-
// the JWS signature algorithm to use. Note that Mojaloop spec requires RS256 at present
17-
const SIGNATURE_ALGORITHM = 'RS256';
18-
19-
// a regular expression to extract the Mojaloop API spec compliant HTTP-URI header value
2016
const uriRegex = /(?:^.*)(\/(participants|parties|quotes|bulkQuotes|transfers|bulkTransfers|transactionRequests|thirdpartyRequests|authorizations|consents|consentRequests|fxQuotes|fxTransfers|)(\/.*)*)$/;
2117

2218

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

30+
// the JWS signature algorithm to use. Note that Mojaloop spec requires RS256 at present
31+
this.alg = config.signingKey.includes('BEGIN EC ') ? 'ES256' : 'RS256';
32+
3433
this.signingKey = config.signingKey;
3534
}
3635

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

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

6665
if(requestOptions.body && typeof(requestOptions.body) !== 'string') {
6766
requestOptions.body = JSON.stringify(requestOptions.body);
@@ -99,7 +98,7 @@ class JwsSigner {
9998
// Note: Property names are case sensitive in the protected header object even though they are
10099
// not case sensitive in the actual HTTP headers
101100
const protectedHeaderObject = {
102-
alg: SIGNATURE_ALGORITHM,
101+
alg: this.alg,
103102
'FSPIOP-URI': requestOptions.headers['fspiop-uri'],
104103
'FSPIOP-HTTP-Method': requestOptions.method.toUpperCase(),
105104
'FSPIOP-Source': requestOptions.headers['fspiop-source']

‎src/lib/jws/jwsValidator.js‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ const jwt = require('jsonwebtoken');
1515
const safeStringify = require('fast-safe-stringify');
1616

1717
// the JWS signature algorithm to use. Note that Mojaloop spec requires RS256 at present
18-
const SIGNATURE_ALGORITHM = 'RS256';
19-
18+
const SIGNATURE_ALGORITHMS = ['RS256', 'ES256'];
2019

2120
/**
2221
* Provides methods for Mojaloop compliant JWS signing and signature verification
@@ -73,7 +72,7 @@ class JwsValidator {
7372
// validate signature
7473
const result = jwt.verify(token, pubKey, {
7574
complete: true,
76-
algorithms: [ SIGNATURE_ALGORITHM ] //only allow our SIGNATURE_ALGORITHM
75+
algorithms: SIGNATURE_ALGORITHMS //only allow our SIGNATURE_ALGORITHM
7776
});
7877

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

108107
// check FSPIOP-URI is present and matches

‎test/unit/jws.perf.test.js‎

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
'use strict';
2+
3+
const fs = require('fs');
4+
const JwsTest = require('../../src/lib/jws');
5+
const Signer = JwsTest.signer;
6+
const Validator = JwsTest.validator;
7+
const mockLogger = require('../__mocks__/mockLogger');
8+
const crypto = require('crypto');
9+
10+
const signingKey = fs.readFileSync(__dirname + '/data/jwsSigningKey.pem');
11+
const validationKey = fs.readFileSync(__dirname + '/data/jwsValidationKey.pem');
12+
const key = {
13+
'kty': 'EC',
14+
'd': 'iYjERsNErBjCQljkeU8EJVAwU-dMxi_07vdYgTPRsx4',
15+
'use': 'sig',
16+
'crv': 'P-256',
17+
'x': 'ok3_fYYnzhXij__aLGXKr0AKGjjUo1tAqt9z4jp3iog',
18+
'y': '_AlRjdUsqPTbpRExkd5vNcsqCSKSDx31mBuMewZTcds',
19+
'alg': 'ES256'
20+
};
21+
22+
const signingKeyEC = crypto.createPrivateKey({format: 'jwk', key}).export({format: 'pem', type: 'sec1'});
23+
const validationKeyEC = crypto.createPublicKey({format: 'jwk', key}).export({format: 'pem', type: 'spki'});
24+
25+
describe('JWS', () => {
26+
let signer;
27+
let signerEC;
28+
let testOpts;
29+
// let testOptsData;
30+
let body;
31+
32+
beforeEach(() => {
33+
signer = new Signer({
34+
signingKey: signingKey,
35+
logger: mockLogger({ app: 'jws-test' }, undefined)
36+
});
37+
signerEC = new Signer({
38+
signingKey: signingKeyEC,
39+
logger: mockLogger({ app: 'jws-test' }, undefined)
40+
});
41+
body = { test: 123 };
42+
// An request-promise-native style request uses the `.uri` and `.body` properties instead of the `.url` and `.data` properties.
43+
testOpts = {
44+
headers: {
45+
'fspiop-source': 'mojaloop-sdk',
46+
'fspiop-destination': 'some-other-fsp',
47+
'date': new Date().toISOString(),
48+
},
49+
method: 'PUT',
50+
uri: 'https://someswitch.com:443/prefix/parties/MSISDN/12345678',
51+
body,
52+
};
53+
});
54+
55+
function testValidateSignedRequest(shouldFail, key) {
56+
const request = {
57+
headers: testOpts.headers,
58+
body: body,
59+
};
60+
61+
const validate = () => {
62+
const validator = new Validator({
63+
validationKeys: {
64+
'mojaloop-sdk': key
65+
},
66+
logger: mockLogger({ app: 'validate-test' }, undefined)
67+
});
68+
validator.validate(request);
69+
};
70+
71+
if (shouldFail) {
72+
expect(validate).toThrow();
73+
} else {
74+
validate();
75+
}
76+
}
77+
78+
test('Should generate valid JWS headers and signature for request with body', () => {
79+
for (let i = 1; i < 1000; i++) signer.sign(testOpts);
80+
81+
expect(testOpts.headers['fspiop-signature']).toBeTruthy();
82+
expect(testOpts.headers['fspiop-uri']).toBe('/parties/MSISDN/12345678');
83+
expect(testOpts.headers['fspiop-http-method']).toBe('PUT');
84+
85+
testValidateSignedRequest(false, validationKey);
86+
});
87+
88+
test('Should generate valid JWS headers and signature for request with body ES256', () => {
89+
for (let i = 1; i < 1000; i++) signerEC.sign(testOpts, 'ES256');
90+
91+
expect(testOpts.headers['fspiop-signature']).toBeTruthy();
92+
expect(testOpts.headers['fspiop-uri']).toBe('/parties/MSISDN/12345678');
93+
expect(testOpts.headers['fspiop-http-method']).toBe('PUT');
94+
95+
testValidateSignedRequest(false, validationKeyEC);
96+
});
97+
});

0 commit comments

Comments
 (0)