Skip to content

Commit

Permalink
feat: verify signatures
Browse files Browse the repository at this point in the history
BREAKING CHANGE: strict signing added and defaults to true
  • Loading branch information
vasco-santos committed Jul 8, 2019
2 parents 4e551b0 + 73df77a commit 41f05bf
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 24 deletions.
13 changes: 13 additions & 0 deletions README.md
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -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": [
Expand Down
38 changes: 37 additions & 1 deletion src/index.js
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 All @@ -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
*
Expand Down Expand Up @@ -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
Expand Down
59 changes: 56 additions & 3 deletions 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`
*
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
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
50 changes: 41 additions & 9 deletions test/pubsub.spec.js
Expand Up @@ -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

Expand All @@ -38,6 +37,10 @@ class PubsubImplementation extends PubsubBaseProtocol {
}

describe('pubsub base protocol', () => {
afterEach(() => {
sinon.restore()
})

describe('fresh nodes', () => {
let nodeA
let nodeB
Expand Down Expand Up @@ -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']
Expand All @@ -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', () => {
Expand Down
43 changes: 39 additions & 4 deletions test/sign.spec.js
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 41f05bf

Please sign in to comment.