Skip to content

Commit

Permalink
feat: add validate method for validating signatures
Browse files Browse the repository at this point in the history
  • Loading branch information
jacobheun committed Jul 8, 2019
1 parent 4e551b0 commit c36fefa
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 21 deletions.
28 changes: 27 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -25,13 +28,15 @@ 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) {
super()

options = {
signMessages: true,
strictSigning: true,
...options
}

Expand Down Expand Up @@ -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
Expand Down
59 changes: 56 additions & 3 deletions src/message/sign.js
Original file line number Diff line number Diff line change
@@ -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`
*
Expand All @@ -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,
Expand All @@ -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
}
27 changes: 20 additions & 7 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
12 changes: 6 additions & 6 deletions test/pubsub.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand All @@ -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)
})
Expand Down
43 changes: 39 additions & 4 deletions test/sign.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand All @@ -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']
Expand All @@ -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)
})
Expand Down

0 comments on commit c36fefa

Please sign in to comment.