five-bells-shared
Advanced tools
Comparing version 12.2.0 to 12.2.1
{ | ||
"name": "five-bells-shared", | ||
"version": "12.2.0", | ||
"version": "12.2.1", | ||
"description": "Shared components for Five Bells projects.", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
@@ -47,1 +47,31 @@ # Five Bells Shared [![Circle CI](https://circleci.com/gh/interledger/five-bells-shared/tree/master.svg?style=svg&circle-token=df06f3b2d8bce028f8b2410d8b993285c3da5c9b)](https://circleci.com/gh/interledger/five-bells-shared/tree/master) | ||
``` | ||
## JSON signing | ||
JSONSigning module provides API for adding signature block, and verifying it, in a JSON object. The format follows JSON Cleartext Signature (JCS) specification. | ||
### Usage | ||
``` | ||
JSONSigning.sign (json, cryptoType, privateKey, publicKey) | ||
json : JSON object. Must not have "signature" block at the top level. | ||
cryptoType : "PS256" for RSA PSS, or "ES256" for ECDSA | ||
privateKey : Private key. | ||
For RSA, it can be a PEM format string, or jsrsasign RSA key object. | ||
For ECDSA, it has to be jsrsasign ECDSA key object. | ||
publicKey : Public key. | ||
This is not needed for | ||
For ECDSA, it has to be jsrsasign ECDSA key object. | ||
Returns copy of JSON object with "signature" block added. It does not modify the original JSON object. | ||
JSONSigning.verify (json, cryptoType, publicKey) | ||
json : JSON object. Must have "signature" block at the top level. | ||
cryptoType : "PS256" for RSA PSS, or "ES256" for ECDSA | ||
publicKey : Public key. | ||
For RSA, it can be a PEM format string, or jsrsasign RSA key object. | ||
For ECDSA, it has to be jsrsasign ECDSA key object. | ||
Returns true (signature is valid) or false (otherwise) | ||
``` |
@@ -25,3 +25,18 @@ { | ||
} | ||
}, | ||
"expectedRSASignedNotification": { | ||
"id": "6821484d-6ca7-4157-b15b-388c49637111", | ||
"subscription_id": "c6345f6e-d3c9-440b-8aa0-f0077c0c40f4", | ||
"transfer_id": "ba32d964-f143-4b67-b470-d258025288d4", | ||
"retry_count": 1, | ||
"retry_at": 1457645427712, | ||
"signature": { | ||
"algorithm": "PS256", | ||
"publicKey": { | ||
"type": "RSA", | ||
"e": "NjU1Mzc=", | ||
"n": "Njc1NTkwOTcxMTE2NTEwMTIyODY4MzE0NjkwMzkxODI3NTAyMjQ4MzA1NzQ3NzA0NTkxNDg3MzA4MzI4MzQ3ODEzODgyNTgwMTIzMDU1NzE4OTM1MjAyMTY2Njk0OTIwODcxMDkzMzcwNjA2MDc5NTU3Mzg2ODg1MjI3MTY0MTE2NTkwMDYxNTkzMDU0NTQyMDgyMzU0Nzc5NjczMzExODExMzMwNzkwNjI0NTMxNjIxMjg2OTg0MTE3NDgwNzM3MzUwNzUwNjM4Mzg0MjYzNDMwMjczNDQ0OTIwNDgyODY5MDc2MTgzNDEwOTc1NTU2NDM4MzYxNTg4MTIyNzIxNzU0NzU2ODcwNDAyMTI3OTcxNzIxMTc2MjkxMTE2MzEwNzIxMzEyOTExMTgwNTMyNDE5ODE4NzM0NjYwNTE3MDc0MDIxNDE4Nzc3Mjc5NjcwNDkyNjc1NDA5NzU1NTk2MzUxOTAwOTAwMTA5NDMyMzAzNzg2NjExMTA3NTExNjk1NDU2MzUwNzI5NDQ5NTE4NzkxNTQ1NTAxMjkzNDcwNzExNzI3MzExNDgwMDY3Njk2MDQ3MDgwNDAwMzE5Njk2MzYxNjk3NTY0MTg2NzIxNDI3NDMwNDIyMDk3MzExNjgxNjQxNDkyNjM2Nzk1ODQxNDE5MTY5MjM1NzM1MTUzNDA2MDc1MDk1OTk3NTc2MDA4NDE3NTEyMjgzMjY5MTI3NDU1OTU4OTM3MTk5MDI1MDMxNTM4NTIyMzE5MTg5MTMyOTM3NDgyNzg2NzE3MzAxMDM0MjkyMDM2NTEzMzQ2NDU2OTE4MzcwOTk1NzQyMDM5NzAwOTkyNDM3NzY5OTM1NDQ2OTc1OTIxNDE1NjQyMTU2NzIxMzkzMTAwMjQyMDkxMTk1NTIyMjQ5ODc3NTk3NzY2MjE3NzE2MDc4MzgxNzY1MjYxMDIyNjY4NjEwNzE0NTY1MTk5ODkzODcxMTU0NDQ5NTQzMzk4NjQzNTA0Njc3NjIwNjEwOTY5ODkwNzE2MDk0MTM5NjcxOTQ1MjY2ODEzNzY1MTkyOTc4Mjc0NjcwMTk2Njc5NDM5MTM4MjgxMTk5MTc5Nzg5NjIzMzU5ODk0MTExOTEwOTAxNTYyMTg1NjE1ODcxMzQ5NjQzOTA5MjcwMDg3ODM0MzUxNTg5NTA3MjgxOTc5NzE4MzQxMzkxNzc0NjE0NzI1NzI3MjQ0ODQ0MDM0NTUyNzg2ODQxNzM3MDQ5NDc0ODU4NTY3OTAxOTY4NTcyMjcxMTY2NDk5OTgzNjI0MjkyODcwMjM5ODY0Njk4ODU0ODY3ODAyMzk5NTUxNTE3MDcyOTI0MDk1OTUzMjY4MzEzNzk4Nzg5MDEyODUzNjc4OTU5NjE1MTg1NTUxNzQwMTU0MzYxODc3OTM3NjkxMzg4MzU0MDc1Nzk0ODA4OTQxOTEwNzkxMDA3Njc2MzQzNTcyODUwNjY4NTM3MjU2NjU5MDU1Mzk2ODE5ODc0OTk0NDA2NzMzNzc0NTEwMjE0MzYyMDYxNjc0MDc4OTI2NTEzODYwOTczMjEzNTY2OTQ1MDYwNjk5MjEyNTg5Njk2Njg2NjA4NjMwODYxOTA3OTQ2NTUyNzQzNzM5OTUyMDkzNTQxMzUxNjcx" | ||
} | ||
} | ||
} | ||
} |
@@ -19,6 +19,12 @@ 'use strict' | ||
// Keys were generated by: | ||
// openssl genrsa -out signKeyRSAPrv.pem 4096 | ||
// openssl rsa -in signKeyRSAPrv.pem -outform PEM -pubout -out signKeyRSAPub.pem | ||
const prvRSAPEM = fs.readFileSync('test/data/signKeyRSAPrv.pem', 'utf8') | ||
const pubRSAPEM = fs.readFileSync('test/data/signKeyRSAPub.pem', 'utf8') | ||
describe('jsonSigningTests', function () { | ||
describe('jsonSigning', function () { | ||
it('should sign JSON object successfully', function () { | ||
const signedJSON = jsonSigning.sign(signData.sampleNotification, prvKey, pubKey) | ||
const signedJSON = jsonSigning.sign(signData.sampleNotification, jsonSigning.types.ES256, prvKey, pubKey) | ||
// Do not check signature.value, because this value changes every time it signs | ||
@@ -32,4 +38,4 @@ delete signedJSON.signature.value | ||
it('should sign and verify JSON object successfully', function () { | ||
const signedJSON = jsonSigning.sign(signData.sampleNotification, prvKey, pubKey) | ||
const result = jsonSigning.verify(signedJSON, pubKey) | ||
const signedJSON = jsonSigning.sign(signData.sampleNotification, jsonSigning.types.ES256, prvKey, pubKey) | ||
const result = jsonSigning.verify(signedJSON, jsonSigning.types.ES256, pubKey) | ||
expect(result).to.be.true | ||
@@ -41,6 +47,6 @@ }) | ||
it('should catch invalid JSON signature', function () { | ||
const signedJSON = jsonSigning.sign(signData.sampleNotification, prvKey, pubKey) | ||
const signedJSON = jsonSigning.sign(signData.sampleNotification, jsonSigning.types.ES256, prvKey, pubKey) | ||
// Modify signature value to make verification fail | ||
signedJSON.signature.value += 'X' | ||
const result = jsonSigning.verify(signedJSON, pubKey) | ||
const result = jsonSigning.verify(signedJSON, jsonSigning.types.ES256, pubKey) | ||
expect(result).to.be.false | ||
@@ -52,10 +58,47 @@ }) | ||
it('should catch invalid public key in signed JSON', function () { | ||
const signedJSON = jsonSigning.sign(signData.sampleNotification, prvKey, pubKey) | ||
const signedJSON = jsonSigning.sign(signData.sampleNotification, jsonSigning.types.ES256, prvKey, pubKey) | ||
// Modify public key in signature to make verification throw error | ||
signedJSON.signature.publicKey.y += 'X' | ||
expect(function () { | ||
jsonSigning.verify(signedJSON, pubKey) | ||
jsonSigning.verify(signedJSON, jsonSigning.types.ES256, pubKey) | ||
}).to.throw(ServerError) | ||
}) | ||
}) | ||
describe('jsonRSASigning', function () { | ||
it('should sign JSON object with RSA successfully', function () { | ||
const signedJSON = jsonSigning.sign(signData.sampleNotification, jsonSigning.types.PS256, prvRSAPEM) | ||
// Do not check signature.value, because this value changes every time it signs | ||
delete signedJSON.signature.value | ||
expect(signedJSON).to.deep.equal(signData.expectedRSASignedNotification) | ||
}) | ||
}) | ||
describe('jsonRSASignAndVerify', function () { | ||
it('should sign and verify JSON object with RSA successfully', function () { | ||
const signedJSON = jsonSigning.sign(signData.sampleNotification, jsonSigning.types.PS256, prvRSAPEM) | ||
const result = jsonSigning.verify(signedJSON, jsonSigning.types.PS256, pubRSAPEM) | ||
expect(result).to.be.true | ||
}) | ||
}) | ||
describe('jsonRSAInvalidSignature', function () { | ||
it('should catch invalid JSON signature with RSA', function () { | ||
const signedJSON = jsonSigning.sign(signData.sampleNotification, jsonSigning.types.PS256, prvRSAPEM) | ||
signedJSON.signature.value += 'X' | ||
const result = jsonSigning.verify(signedJSON, jsonSigning.types.PS256, pubRSAPEM) | ||
expect(result).to.be.false | ||
}) | ||
}) | ||
describe('jsonRSAInvalidPublicKey', function () { | ||
it('should catch invalid RSA public key in signed JSON', function () { | ||
const signedJSON = jsonSigning.sign(signData.sampleNotification, jsonSigning.types.PS256, prvRSAPEM) | ||
// Modify public key in signature to make verification throw error | ||
signedJSON.signature.publicKey.n += 'X' | ||
expect(function () { | ||
jsonSigning.verify(signedJSON, jsonSigning.types.PS256, pubRSAPEM) | ||
}).to.throw(ServerError) | ||
}) | ||
}) | ||
}) |
@@ -8,6 +8,30 @@ // JSON Cleartext Signing | ||
// Constants for crypto algorithm types (ECDSA and RSA) | ||
const ES256 = 'ES256' | ||
const PS256 = 'PS256' | ||
module.exports.types = { | ||
ES256, | ||
PS256 | ||
} | ||
// If given key is PEM formatted string, convert it to key object | ||
// If it's already key object, just use it | ||
function parseKey (key) { | ||
if (typeof key === 'string') { | ||
return rsasign.KEYUTIL.getKey(key) | ||
} else { | ||
return key | ||
} | ||
} | ||
// Sign JSON object, using ECDSA P-256 private key and public key. | ||
// Insert 'signature' block in the JSON object. | ||
// Warning: there must not be an existing 'signature' block. | ||
module.exports.sign = function (json, prvKey, pubKey) { | ||
module.exports.sign = function (json, cryptoType, prvKey, pubKey) { | ||
if (cryptoType === ES256) return signECDSA(json, prvKey, pubKey) | ||
else if (cryptoType === PS256) return signRSA(json, prvKey) | ||
else throw new ServerError('Unsupported crypto algorithm: ' + cryptoType) | ||
} | ||
function signECDSA (json, prvKey, pubKey) { | ||
if (!prvKey) throw new ServerError('Problem reading private key for JSON signing') | ||
@@ -45,4 +69,37 @@ if (!pubKey) throw new ServerError('Problem reading public key for JSON signing') | ||
// Sign JSON object using RSA | ||
function signRSA (json, prvKey) { | ||
if (!prvKey) throw new ServerError('Problem reading private key for JSON signing') | ||
const prvKeyObj = parseKey(prvKey) | ||
const strJWS = KJUR.jws.JWS.sign(PS256, // eslint-disable-line no-undef | ||
JSON.stringify({alg: PS256}), | ||
JSON.stringify(json), | ||
prvKeyObj) | ||
const jwsArray = strJWS.split('.') | ||
// TODO: To make e and n exactly compliant with JCS spec, E and N must be represented as | ||
// "Base64URL-encoded positive integer with arbitrary precision." | ||
// jsrsasign stores public key components "e" as Number, and "n" as BigInteger | ||
const pubkeyE = new Buffer(prvKeyObj.e.toString()).toString('base64') | ||
const pubkeyN = new Buffer(prvKeyObj.n.toString()).toString('base64') | ||
const signedJSON = _.cloneDeep(json) | ||
signedJSON.signature = { | ||
'algorithm': PS256, | ||
'publicKey': { | ||
'type': 'RSA', | ||
'e': pubkeyE, | ||
'n': pubkeyN | ||
}, | ||
'value': jwsArray[2] | ||
} | ||
return signedJSON | ||
} | ||
// Verify 'signature' block on the input JSON object has the correct signature value. | ||
module.exports.verify = function (json, pubKey) { | ||
module.exports.verify = function (json, cryptoType, pubKey) { | ||
if (cryptoType === ES256) return verifyECDSA(json, pubKey) | ||
else if (cryptoType === PS256) return verifyRSA(json, pubKey) | ||
else throw new ServerError('Unsupported crypto algorithm: ' + cryptoType) | ||
} | ||
function verifyECDSA (json, pubKey) { | ||
if (!json || !json.signature || !json.signature.value) { | ||
@@ -54,3 +111,3 @@ throw new ServerError('Invalid input for JSON verification') | ||
let strPayload = new Buffer(JSON.stringify(jsonWithoutSignature)).toString('base64') | ||
// chop training '==' | ||
// chop trailing '==' | ||
if (_.endsWith(strPayload, '==')) { | ||
@@ -78,1 +135,25 @@ strPayload = strPayload.slice(0, strPayload.length - 2) | ||
} | ||
function verifyRSA (json, pubKey) { | ||
if (!json || !json.signature || !json.signature.value) { | ||
throw new ServerError('Invalid input for JSON verification') | ||
} | ||
if (!pubKey) throw new ServerError('Problem reading public key for JSON signing') | ||
const pubKeyObj = parseKey(pubKey) | ||
const jsonWithoutSignature = _.omit(json, 'signature') | ||
let strPayload = new Buffer(JSON.stringify(jsonWithoutSignature)).toString('base64') | ||
// chop trailing '==' | ||
if (_.endsWith(strPayload, '==')) { | ||
strPayload = strPayload.slice(0, strPayload.length - 2) | ||
} | ||
// Check pub key parameters | ||
const pubkeyE = new Buffer(pubKeyObj.e.toString()).toString('base64') | ||
const pubkeyN = new Buffer(pubKeyObj.n.toString()).toString('base64') | ||
if (json.signature.publicKey.e !== pubkeyE || json.signature.publicKey.n !== pubkeyN) { | ||
throw new ServerError('Public key mismatch in JSON signature verification') | ||
} | ||
const jws = 'eyJhbGciOiJQUzI1NiJ9\.' + // Base64 encoded {"alg":"PS256"} | ||
strPayload + '\.' + json.signature.value | ||
return KJUR.jws.JWS.verify(jws, pubKeyObj, [PS256]) // eslint-disable-line no-undef | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
1770068
136
4062
77