From e3ca54479d72ba927486b988384add32e62b04c1 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 26 Sep 2017 23:38:19 -0400 Subject: [PATCH] Add RsaSignature2017 support. - Use jws lib. - Use specific header params. - Use JWS Compact Serialization with Detached Content. --- lib/jsonld-signatures.js | 85 +++++++++++++- package.json | 1 + tests/test.js | 247 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 329 insertions(+), 4 deletions(-) diff --git a/lib/jsonld-signatures.js b/lib/jsonld-signatures.js index 104a9116..e4742981 100644 --- a/lib/jsonld-signatures.js +++ b/lib/jsonld-signatures.js @@ -69,7 +69,8 @@ api.SECURITY_CONTEXT_URL = 'https://w3id.org/security/v1'; api.SUPPORTED_ALGORITHMS = [ 'EcdsaKoblitzSignature2016', 'GraphSignature2012', - 'LinkedDataSignature2015' + 'LinkedDataSignature2015', + 'RsaSignature2017' ]; /* Core API */ @@ -107,8 +108,9 @@ api.use = function(name, injectable) { // api not set yet, load default if(!libs[name]) { var requireAliases = { + 'bitcoreMessage': 'bitcore-message', 'forge': 'node-forge', - 'bitcoreMessage': 'bitcore-message' + 'jws': 'jws' }; var requireName = requireAliases[name] || name; libs[name] = global[name] || (_nodejs && require(requireName)); @@ -797,6 +799,25 @@ var _createSignature = function(input, options) { return Promise.resolve(signature); } + if(options.algorithm === 'RsaSignature2017') { + // works same in any environment + var jws = api.use('jws'); + var fullSignature = jws.sign({ + header: { + alg: 'RS256', + b64: false, + crit: ['b64'] + }, + privateKey: options.privateKeyPem, + payload: _getDataToHash(input, options) + }); + // detached content signature + var parts = fullSignature.split('.'); + parts[1] = ''; + var detachedSignature = parts.join('.'); + return Promise.resolve(detachedSignature); + } + if(_nodejs) { // optimize using node libraries var crypto = api.use('crypto'); @@ -842,6 +863,17 @@ var _verifySignature = function(input, signature, options) { return Promise.resolve(verified); } + if(options.algorithm === 'RsaSignature2017') { + // works same in any environment + var jws = api.use('jws'); + // rebuild detached content signature + var parts = signature.split('.'); + parts[1] = _encodeBase64Url(_getDataToHash(input, options)); + var fullSignature = parts.join('.'); + var verified = jws.verify(fullSignature, 'RS256', options.publicKeyPem); + return Promise.resolve(verified); + } + if(_nodejs) { // optimize using node libraries var crypto = api.use('crypto'); @@ -965,6 +997,55 @@ function _zeroFill(num) { return (num < 10) ? '0' + num : '' + num; } +/** + * Encodes input according to the "Base64url Encoding" format as specified + * in JSON Web Signature (JWS) RFC7517. A URL safe character set is used and + * trailing '=', line breaks, whitespace, and other characters are omitted. + * + * @param input the data to encode. + * + * @return the encoded value. + */ +function _encodeBase64Url(input) { + var forge = api.use('forge'); + var enc = forge.util.encode64(input); + return enc + .replace(/=/g, '') + .replace(/\+/g, '-') + .replace(/\//g, '_'); +} +// expose for testing +api._encodeBase64Url = _encodeBase64Url; + +/** + * Decodes input according to the "Base64url Encoding" format as specified + * in JSON Web Signature (JWS) RFC7517. A URL safe character set is used and + * trailing '=', line breaks, whitespace, and other characters are omitted. + * + * @param input the data to decode. + * + * @return the decoded value. + */ +function _decodeBase64Url(input) { + var forge = api.use('forge'); + var normalInput = input + .replace(/-/g, '+') + .replace(/_/g, '/'); + var mod4 = normalInput.length % 4; + if(mod4 === 0) { + // pass + } else if(mod4 === 2) { + normalInput = normalInput + '=='; + } else if(mod4 === 3) { + normalInput = normalInput + '='; + } else { + throw new Error('Illegal base64 string.'); + } + return forge.util.decode64(normalInput); +} +// expose for testing +api._decodeBase64Url = _decodeBase64Url; + /* Promises API */ /** diff --git a/package.json b/package.json index 90c54c06..ccd50a66 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "commander": "~2.9.0", "es6-promise": "^4.0.5", "jsonld": "^0.5.10", + "jws": "^3.1.4", "node-forge": "~0.7.1" }, "devDependencies": { diff --git a/tests/test.js b/tests/test.js index a9e94565..e9923c25 100644 --- a/tests/test.js +++ b/tests/test.js @@ -14,7 +14,7 @@ var _nodejs = (typeof process !== 'undefined' && process.versions && process.versions.node); -var _jsdir, jsonld, jsigs, assert, program; +var _jsdir, jsonld, jsigs, jws, assert, program; var testLoader = function(url, callback) { if(url === 'https://w3id.org/security/v1') { @@ -63,6 +63,8 @@ if(_nodejs) { jsonld.documentLoader = testLoader; jsigs = require('../' + _jsdir + '/jsonld-signatures')(); jsigs.use('jsonld', jsonld); + jws = require('jws'); + jsigs.use('jws', jws); assert = require('chai').assert; program = require('commander'); program @@ -81,6 +83,9 @@ if(_nodejs) { var bitcoreMessage = require( '../node_modules/bitcore-message/dist/bitcore-message.js'); window.bitcoreMessage = bitcoreMessage; + var jws = require( + '../node_modules/jws'); + window.jws = jws; jsonld = require('../node_modules/jsonld/dist/jsonld.js'); require('../' + _jsdir + '/jsonld-signatures'); jsigs = window.jsigs; @@ -120,6 +125,102 @@ function clone(obj) { // run tests describe('JSON-LD Signatures', function() { + context('common', function() { + var testDocument = { + '@context': { + schema: 'http://schema.org/', + name: 'schema:name', + homepage: 'schema:url', + image: 'schema:image' + }, + name: 'Manu Sporny', + homepage: 'https://manu.sporny.org/', + image: 'https://manu.sporny.org/images/manu.png' + }; + + var testBadDocument = clone(testDocument); + testBadDocument['https://w3id.org/security#signature'] = { + '@type': 'https://w3id.org/security#BogusSignature3000', + 'http://purl.org/dc/terms/created': { + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + '@value': '2017-03-25T22:01:04Z' + }, + 'http://purl.org/dc/terms/creator': { + '@id': 'test:1234' + }, + 'https://w3id.org/security#signatureValue': 'test' + }; + + it('should fail sign with unknown algorithm', function(done) { + jsigs.sign(testDocument, { + algorithm: 'BogusSignature3000', + privateKeyPem: '', + creator: '' + }, function(err, signedDocument) { + assert(err); + done(); + }); + }); + + it('should fail verify with unknown algorithm', function(done) { + jsigs.verify(testBadDocument, {}, function(err, result) { + assert(err); + done(); + }); + }); + + it('should base64url encode', function(done) { + var inputs = [ + '', + '1', + '12', + '123', + '1234', + '12345', + Buffer.from([0xc3,0xbb,0xc3,0xb0,0x00]).toString(), + Buffer.from([0xc3,0xbb,0xc3,0xb0]).toString(), + Buffer.from([0xc3,0xbb]).toString() + ]; + inputs.forEach(function(input) { + var enc = jsigs._encodeBase64Url(input); + var dec = jsigs._decodeBase64Url(enc); + /* + console.log('E', input, '|', Buffer.from(input)); + console.log(' enc', enc, '|', Buffer.from(enc)); + console.log(' dec', dec, '|', Buffer.from(dec)); + */ + assert.equal(enc.indexOf('+'), -1); + assert.equal(enc.indexOf('/'), -1); + assert.equal(enc.indexOf('='), -1); + assert.equal(input, dec); + }); + done() + }); + + it('should base64url decode', function(done) { + var inputs = [ + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_', + '_-E', + '_-E=', + 'AA', + 'eA', + 'eA=', + 'eA==', + ]; + inputs.forEach(function(input) { + var dec = jsigs._decodeBase64Url(input); + var enc = jsigs._encodeBase64Url(dec); + /* + console.log('D', input, '|', Buffer.from(input)); + console.log(' dec', dec, '|', Buffer.from(dec)); + console.log(' enc', enc, '|', Buffer.from(enc)); + */ + assert.equal(input.replace(/=/g, ''), enc); + }); + done() + }); + }); + context('with NO security context', function() { // the test document that will be signed var testDocument = { @@ -604,6 +705,147 @@ describe('JSON-LD Signatures', function() { }).then(done, done); }); }); + + describe.only('signing and verify RsaSignature2017', function() { + + var testDocument; + var testDocumentSigned; + var testDocumentSignedAltered; + var testInvalidPublicKey; + + beforeEach(function() { + testDocument = { + '@context': { + schema: 'http://schema.org/', + name: 'schema:name', + homepage: 'schema:url', + image: 'schema:image' + }, + name: 'Manu Sporny', + homepage: 'https://manu.sporny.org/', + image: 'https://manu.sporny.org/images/manu.png' + }; + + testDocumentSigned = clone(testDocument); + testDocumentSigned["https://w3id.org/security#signature"] = { + "@type": "https://w3id.org/security#RsaSignature2017", + "http://purl.org/dc/terms/created": { + "@type": "http://www.w3.org/2001/XMLSchema#dateTime", + "@value": "2017-09-27T03:12:26Z" + }, + "http://purl.org/dc/terms/creator": { + "@id": testPublicKeyUrl + }, + "https://w3id.org/security#signatureValue": + "eyJhbGciOiJSUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19" + + ".." + + "Vewb2R5LWN2T5_8lFTE6hYqu7MyrUaIBCLE55DDGGtEUFZOfFnW0sxft5TiEdm" + + "BIYhYveNY9LTAqhRLTIzYG2ZOi9WpMI3DsfApEuz9GkZLIkPC0rQZVMW9ssP17" + + "Cxiim9imGA5dyHn2pZ98C_Cd_ptqHMFyjKHluKhG4HpfNaM" + }; + testDocumentSignedAltered = clone(testDocumentSigned); + testDocumentSignedAltered.name = 'Manu Spornoneous'; + + testInvalidPublicKey = clone(testPublicKey); + testInvalidPublicKey.id = testPublicKeyUrl2; + }); + + it('should successfully sign a local document', function(done) { + jsigs.sign(testDocument, { + algorithm: 'RsaSignature2017', + creator: testPublicKeyUrl, + privateKeyPem: testPrivateKeyPem, + }, function(err, signedDocument) { + assert.ifError(err); + assert.notEqual( + signedDocument['https://w3id.org/security#signature'], undefined, + 'signature was not created'); + assert.equal( + signedDocument['https://w3id.org/security#signature'] + ['http://purl.org/dc/terms/creator']['@id'], + testPublicKeyUrl, + 'creator key for signature is wrong'); + done(); + }); + }); + + it('should successfully verify a local signed document', function(done) { + jsigs.verify(testDocumentSigned, { + publicKey: testPublicKey, + publicKeyOwner: testPublicKeyOwner + }, function(err, result) { + assert.ifError(err); + assert.equal(result.verified, true, 'signature verification failed'); + done(); + }); + }); + + it('verify should return false if the document was signed by a ' + + 'different private key', function(done) { + jsigs.verify(testDocumentSigned, { + publicKey: testInvalidPublicKey, + publicKeyOwner: testPublicKeyOwner + }, function(err, result) { + assert.ifError(err); + assert.equal( + result.verified, false, + 'signature verification should have failed'); + done(); + }); + }); + + it('verify returns false if the document was altered after signing', + function(done) { + jsigs.verify(testDocumentSignedAltered, { + publicKey: testPublicKey, + publicKeyOwner: testPublicKeyOwner + }, function(err, result) { + assert.ifError(err); + assert.equal( + result.verified, false, + 'signature verification should have failed'); + done(); + }); + }); + + it('should successfully sign a local document' + + ' w/promises API', function(done) { + jsigs.promises.sign(testDocument, { + algorithm: 'RsaSignature2017', + privateKeyPem: testPrivateKeyPem, + creator: testPublicKeyUrl + }).then(function(signedDocument) { + assert.notEqual( + signedDocument['https://w3id.org/security#signature'], undefined, + 'signature was not created'); + assert.equal( + signedDocument['https://w3id.org/security#signature'] + ['http://purl.org/dc/terms/creator']['@id'], + testPublicKeyUrl, 'creator key for signature is wrong'); + }).then(done, done); + }); + + it('should successfully verify a local signed document' + + ' w/promises API', function(done) { + jsigs.promises.verify(testDocumentSigned, { + publicKey: testPublicKey, + publicKeyOwner: testPublicKeyOwner + }).then(function(result) { + assert.equal(result.verified, true, 'signature verification failed'); + }).then(done, done); + }); + + it('verify should return false if the document was signed by' + + ' a different private key w/promises API', function(done) { + jsigs.promises.verify(testDocumentSigned, { + publicKey: testInvalidPublicKey, + publicKeyOwner: testPublicKeyOwner + }).then(function(result) { + assert.equal(result.verified, false, + 'signature verification should have failed but did not'); + }).then(done, done); + }); + }); }); context('with security context', function() { @@ -816,7 +1058,7 @@ describe('JSON-LD Signatures', function() { }, "https://w3id.org/security#signatureValue": "IOoF0rMmpcdxNZFoirTpRMCyLr8kGHLqXFl7v+m3naetCx+OLNhVY/6SCUwDGZf" + - "Fs4yPXeAl6Tj1WgtLIHOVZmw=" + "Fs4yPXeAl6Tj1WgtLIHOVZmw=" }; testDocumentSignedAltered = clone(testDocumentSigned); testDocumentSignedAltered.name = 'Manu Spornoneous'; @@ -961,6 +1203,7 @@ var securityContext = { "GraphSignature2012": "sec:GraphSignature2012", "LinkedDataSignature2015": "sec:LinkedDataSignature2015", "LinkedDataSignature2016": "sec:LinkedDataSignature2016", + "RsaSignature2017": "sec:RsaSignature2017", "CryptographicKey": "sec:Key", "authenticationTag": "sec:authenticationTag",