diff --git a/README.md b/README.md index c333d797bc..f6074de8ff 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,19 @@ class PubsubImplementation extends Pubsub { } ``` +### Validate + +Validates the signature of a message. + +#### `pubsub.validate(message, callback)` + +##### Parameters + +| Name | Type | Description | +|------|------|-------------| +| message | `Message` | a pubsub message | +| callback | `function(Error, Boolean)` | calls back with true if the message is valid | + ## Implementations using this base protocol You can use the following implementations as examples for building your own pubsub implementation. diff --git a/package.json b/package.json index 2a69cba707..4cdc1c95ac 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "pull-length-prefixed": "^1.3.1", "pull-pushable": "^2.2.0", "pull-stream": "^3.6.9", + "sinon": "^7.3.2", "time-cache": "~0.3.0" }, "contributors": [ diff --git a/src/index.js b/src/index.js index 271cf3e105..f5b13002fc 100644 --- a/src/index.js +++ b/src/index.js @@ -10,7 +10,10 @@ const errcode = require('err-code') const Peer = require('./peer') const message = require('./message') -const { signMessage } = require('./message/sign') +const { + signMessage, + verifySignature +} = require('./message/sign') const utils = require('./utils') const nextTick = require('async/nextTick') @@ -25,6 +28,7 @@ class PubsubBaseProtocol extends EventEmitter { * @param {Object} libp2p libp2p implementation * @param {Object} options * @param {boolean} options.signMessages if messages should be signed, defaults to true + * @param {boolean} options.strictSigning if message signing should be required, defaults to true * @constructor */ constructor (debugName, multicodec, libp2p, options) { @@ -32,6 +36,7 @@ class PubsubBaseProtocol extends EventEmitter { options = { signMessages: true, + strictSigning: true, ...options } @@ -45,6 +50,12 @@ class PubsubBaseProtocol extends EventEmitter { this.peerId = this.libp2p.peerInfo.id } + /** + * If message signing should be required for incoming messages + * @type {boolean} + */ + this.strictSigning = options.strictSigning + /** * Map of topics to which peers are subscribed to * @@ -349,6 +360,31 @@ class PubsubBaseProtocol extends EventEmitter { callback() }) } + + /** + * Validates the given message. The signature will be checked for authenticity. + * @param {rpc.RPC.Message} message + * @param {function(Error, Boolean)} callback + * @returns {void} + */ + validate (message, callback) { + // If strict signing is on and we have no signature, abort + if (this.strictSigning && !message.signature) { + this.log('Signing required and no signature was present, dropping message:', message) + return nextTick(callback, null, false) + } + + // Check the message signature if present + if (message.signature) { + verifySignature(message, (err, valid) => { + if (err) return callback(err) + callback(null, valid) + }) + } else { + // The message is valid + nextTick(callback, null, true) + } + } } module.exports = PubsubBaseProtocol diff --git a/src/message/sign.js b/src/message/sign.js index a275214358..ae836cc18b 100644 --- a/src/message/sign.js +++ b/src/message/sign.js @@ -1,10 +1,9 @@ 'use strict' +const PeerId = require('peer-id') const { Message } = require('./index') const SignPrefix = Buffer.from('libp2p-pubsub:') -module.exports.SignPrefix = SignPrefix - /** * Signs the provided message with the given `peerId` * @@ -13,7 +12,7 @@ module.exports.SignPrefix = SignPrefix * @param {function(Error, Message)} callback * @returns {void} */ -module.exports.signMessage = function (peerId, message, callback) { +function signMessage (peerId, message, callback) { // Get the message in bytes, and prepend with the pubsub prefix const bytes = Buffer.concat([ SignPrefix, @@ -31,3 +30,57 @@ module.exports.signMessage = function (peerId, message, callback) { }) }) } + +/** + * Verifies the signature of the given message + * @param {rpc.RPC.Message} message + * @param {function(Error, Boolean)} callback + */ +function verifySignature (message, callback) { + // Get message sans the signature + let baseMessage = { ...message } + delete baseMessage.signature + delete baseMessage.key + const bytes = Buffer.concat([ + SignPrefix, + Message.encode(baseMessage) + ]) + + // Get the public key + messagePublicKey(message, (err, pubKey) => { + if (err) return callback(err, false) + // Verify the base message + pubKey.verify(bytes, message.signature, callback) + }) +} + +/** + * Returns the PublicKey associated with the given message. + * If no, valid PublicKey can be retrieved an error will be returned. + * + * @param {Message} message + * @param {function(Error, PublicKey)} callback + * @returns {void} + */ +function messagePublicKey (message, callback) { + if (message.key) { + PeerId.createFromPubKey(message.key, (err, peerId) => { + if (err) return callback(err, null) + // the key belongs to the sender, return the key + if (peerId.isEqual(message.from)) return callback(null, peerId.pubKey) + // We couldn't validate pubkey is from the originator, error + callback(new Error('Public Key does not match the originator')) + }) + return + } + // TODO: Once js libp2p supports inlining public keys with the peer id + // attempt to unmarshal the public key here. + callback(new Error('Could not get the public key from the originator id')) +} + +module.exports = { + messagePublicKey, + signMessage, + SignPrefix, + verifySignature +} diff --git a/src/utils.js b/src/utils.js index 547aaf550d..a2d67678ac 100644 --- a/src/utils.js +++ b/src/utils.js @@ -68,17 +68,30 @@ exports.ensureArray = (maybeArray) => { return maybeArray } +/** + * Ensures `message.from` is base58 encoded + * @param {Object} message + * @param {Buffer|String} message.from + * @return {Object} + */ +exports.normalizeInRpcMessage = (message) => { + const m = Object.assign({}, message) + if (Buffer.isBuffer(message.from)) { + m.from = bs58.encode(message.from) + } + return m +} + +/** + * The same as `normalizeInRpcMessage`, but performed on an array of messages + * @param {Object[]} messages + * @return {Object[]} + */ exports.normalizeInRpcMessages = (messages) => { if (!messages) { return messages } - return messages.map((msg) => { - const m = Object.assign({}, msg) - if (Buffer.isBuffer(msg.from)) { - m.from = bs58.encode(msg.from) - } - return m - }) + return messages.map(exports.normalizeInRpcMessage) } exports.normalizeOutRpcMessage = (message) => { diff --git a/test/pubsub.spec.js b/test/pubsub.spec.js index b78b2bd964..11f10bc106 100644 --- a/test/pubsub.spec.js +++ b/test/pubsub.spec.js @@ -5,13 +5,12 @@ const chai = require('chai') chai.use(require('dirty-chai')) chai.use(require('chai-spies')) const expect = chai.expect +const sinon = require('sinon') const series = require('async/series') const parallel = require('async/parallel') -const { Message } = require('../src/message') -const { SignPrefix } = require('../src/message/sign') const PubsubBaseProtocol = require('../src') -const { randomSeqno, normalizeOutRpcMessage } = require('../src/utils') +const { randomSeqno } = require('../src/utils') const utils = require('./utils') const createNode = utils.createNode @@ -38,6 +37,10 @@ class PubsubImplementation extends PubsubBaseProtocol { } describe('pubsub base protocol', () => { + afterEach(() => { + sinon.restore() + }) + describe('fresh nodes', () => { let nodeA let nodeB @@ -96,7 +99,7 @@ describe('pubsub base protocol', () => { it('_buildMessage normalizes and signs messages', (done) => { const message = { - from: 'QmABC', + from: psA.peerId.id, data: 'hello', seqno: randomSeqno(), topicIDs: ['test-topic'] @@ -105,17 +108,46 @@ describe('pubsub base protocol', () => { psA._buildMessage(message, (err, signedMessage) => { expect(err).to.not.exist() - const bytesToSign = Buffer.concat([ - SignPrefix, - Message.encode(normalizeOutRpcMessage(message)) - ]) + psA.validate(signedMessage, (err, verified) => { + expect(verified).to.eql(true) + done(err) + }) + }) + }) + + it('validate with strict signing off will validate a present signature', (done) => { + const message = { + from: psA.peerId.id, + data: 'hello', + seqno: randomSeqno(), + topicIDs: ['test-topic'] + } + + sinon.stub(psA, 'strictSigning').value(false) + + psA._buildMessage(message, (err, signedMessage) => { + expect(err).to.not.exist() - psA.peerId.pubKey.verify(bytesToSign, signedMessage.signature, (err, verified) => { + psA.validate(signedMessage, (err, verified) => { expect(verified).to.eql(true) done(err) }) }) }) + + it('validate with strict signing requires a signature', (done) => { + const message = { + from: psA.peerId.id, + data: 'hello', + seqno: randomSeqno(), + topicIDs: ['test-topic'] + } + + psA.validate(message, (err, verified) => { + expect(verified).to.eql(false) + done(err) + }) + }) }) describe('dial the pubsub protocol on mount', () => { diff --git a/test/sign.spec.js b/test/sign.spec.js index e7bbd34e51..4258d255c3 100644 --- a/test/sign.spec.js +++ b/test/sign.spec.js @@ -7,7 +7,11 @@ chai.use(require('dirty-chai')) const expect = chai.expect const { Message } = require('../src/message') -const { signMessage, SignPrefix } = require('../src/message/sign') +const { + signMessage, + SignPrefix, + verifySignature +} = require('../src/message/sign') const PeerId = require('peer-id') const { randomSeqno } = require('../src/utils') @@ -22,9 +26,9 @@ describe('message signing', () => { }) }) - it('should be able to sign a message', (done) => { + it('should be able to sign and verify a message', (done) => { const message = { - from: 'QmABC', + from: peerId.id, data: 'hello', seqno: randomSeqno(), topicIDs: ['test-topic'] @@ -43,7 +47,38 @@ describe('message signing', () => { expect(signedMessage.key).to.eql(peerId.pubKey.bytes) // Verify the signature - peerId.pubKey.verify(bytesToSign, signedMessage.signature, (err, verified) => { + verifySignature(signedMessage, (err, verified) => { + expect(err).to.not.exist() + expect(verified).to.eql(true) + done(err) + }) + }) + }) + }) + + it('should be able to extract the public key from the message', (done) => { + const message = { + from: peerId.id, + data: 'hello', + seqno: randomSeqno(), + topicIDs: ['test-topic'] + } + + const bytesToSign = Buffer.concat([SignPrefix, Message.encode(message)]) + + peerId.privKey.sign(bytesToSign, (err, expectedSignature) => { + if (err) return done(err) + + signMessage(peerId, message, (err, signedMessage) => { + if (err) return done(err) + + // Check the signature and public key + expect(signedMessage.signature).to.eql(expectedSignature) + expect(signedMessage.key).to.eql(peerId.pubKey.bytes) + + // Verify the signature + verifySignature(signedMessage, (err, verified) => { + expect(err).to.not.exist() expect(verified).to.eql(true) done(err) })