From c36fefa30f8e223d8e14177351d45eb2a6b7a4e5 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Mon, 8 Jul 2019 10:14:12 +0200 Subject: [PATCH] feat: add validate method for validating signatures --- src/index.js | 28 ++++++++++++++++++++- src/message/sign.js | 59 ++++++++++++++++++++++++++++++++++++++++++--- src/utils.js | 27 +++++++++++++++------ test/pubsub.spec.js | 12 ++++----- test/sign.spec.js | 43 ++++++++++++++++++++++++++++++--- 5 files changed, 148 insertions(+), 21 deletions(-) diff --git a/src/index.js b/src/index.js index 271cf3e105..d55b96291b 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 } @@ -349,6 +354,27 @@ 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 + */ + 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) + }) + } + } } 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..fbe4c163af 100644 --- a/test/pubsub.spec.js +++ b/test/pubsub.spec.js @@ -96,7 +96,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,12 +105,12 @@ describe('pubsub base protocol', () => { psA._buildMessage(message, (err, signedMessage) => { expect(err).to.not.exist() - const bytesToSign = Buffer.concat([ - SignPrefix, - Message.encode(normalizeOutRpcMessage(message)) - ]) + // const bytesToSign = Buffer.concat([ + // SignPrefix, + // Message.encode(normalizeOutRpcMessage(message)) + // ]) - psA.peerId.pubKey.verify(bytesToSign, signedMessage.signature, (err, verified) => { + psA.validate(signedMessage, (err, verified) => { expect(verified).to.eql(true) done(err) }) 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) })