Skip to content

Commit 41f05bf

Browse files
committed
feat: verify signatures
BREAKING CHANGE: strict signing added and defaults to true
2 parents 4e551b0 + 73df77a commit 41f05bf

File tree

7 files changed

+207
-24
lines changed

7 files changed

+207
-24
lines changed

README.md

+13
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,19 @@ class PubsubImplementation extends Pubsub {
6868
}
6969
```
7070

71+
### Validate
72+
73+
Validates the signature of a message.
74+
75+
#### `pubsub.validate(message, callback)`
76+
77+
##### Parameters
78+
79+
| Name | Type | Description |
80+
|------|------|-------------|
81+
| message | `Message` | a pubsub message |
82+
| callback | `function(Error, Boolean)` | calls back with true if the message is valid |
83+
7184
## Implementations using this base protocol
7285

7386
You can use the following implementations as examples for building your own pubsub implementation.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"pull-length-prefixed": "^1.3.1",
7373
"pull-pushable": "^2.2.0",
7474
"pull-stream": "^3.6.9",
75+
"sinon": "^7.3.2",
7576
"time-cache": "~0.3.0"
7677
},
7778
"contributors": [

src/index.js

+37-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ const errcode = require('err-code')
1010

1111
const Peer = require('./peer')
1212
const message = require('./message')
13-
const { signMessage } = require('./message/sign')
13+
const {
14+
signMessage,
15+
verifySignature
16+
} = require('./message/sign')
1417
const utils = require('./utils')
1518

1619
const nextTick = require('async/nextTick')
@@ -25,13 +28,15 @@ class PubsubBaseProtocol extends EventEmitter {
2528
* @param {Object} libp2p libp2p implementation
2629
* @param {Object} options
2730
* @param {boolean} options.signMessages if messages should be signed, defaults to true
31+
* @param {boolean} options.strictSigning if message signing should be required, defaults to true
2832
* @constructor
2933
*/
3034
constructor (debugName, multicodec, libp2p, options) {
3135
super()
3236

3337
options = {
3438
signMessages: true,
39+
strictSigning: true,
3540
...options
3641
}
3742

@@ -45,6 +50,12 @@ class PubsubBaseProtocol extends EventEmitter {
4550
this.peerId = this.libp2p.peerInfo.id
4651
}
4752

53+
/**
54+
* If message signing should be required for incoming messages
55+
* @type {boolean}
56+
*/
57+
this.strictSigning = options.strictSigning
58+
4859
/**
4960
* Map of topics to which peers are subscribed to
5061
*
@@ -349,6 +360,31 @@ class PubsubBaseProtocol extends EventEmitter {
349360
callback()
350361
})
351362
}
363+
364+
/**
365+
* Validates the given message. The signature will be checked for authenticity.
366+
* @param {rpc.RPC.Message} message
367+
* @param {function(Error, Boolean)} callback
368+
* @returns {void}
369+
*/
370+
validate (message, callback) {
371+
// If strict signing is on and we have no signature, abort
372+
if (this.strictSigning && !message.signature) {
373+
this.log('Signing required and no signature was present, dropping message:', message)
374+
return nextTick(callback, null, false)
375+
}
376+
377+
// Check the message signature if present
378+
if (message.signature) {
379+
verifySignature(message, (err, valid) => {
380+
if (err) return callback(err)
381+
callback(null, valid)
382+
})
383+
} else {
384+
// The message is valid
385+
nextTick(callback, null, true)
386+
}
387+
}
352388
}
353389

354390
module.exports = PubsubBaseProtocol

src/message/sign.js

+56-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
'use strict'
22

3+
const PeerId = require('peer-id')
34
const { Message } = require('./index')
45
const SignPrefix = Buffer.from('libp2p-pubsub:')
56

6-
module.exports.SignPrefix = SignPrefix
7-
87
/**
98
* Signs the provided message with the given `peerId`
109
*
@@ -13,7 +12,7 @@ module.exports.SignPrefix = SignPrefix
1312
* @param {function(Error, Message)} callback
1413
* @returns {void}
1514
*/
16-
module.exports.signMessage = function (peerId, message, callback) {
15+
function signMessage (peerId, message, callback) {
1716
// Get the message in bytes, and prepend with the pubsub prefix
1817
const bytes = Buffer.concat([
1918
SignPrefix,
@@ -31,3 +30,57 @@ module.exports.signMessage = function (peerId, message, callback) {
3130
})
3231
})
3332
}
33+
34+
/**
35+
* Verifies the signature of the given message
36+
* @param {rpc.RPC.Message} message
37+
* @param {function(Error, Boolean)} callback
38+
*/
39+
function verifySignature (message, callback) {
40+
// Get message sans the signature
41+
let baseMessage = { ...message }
42+
delete baseMessage.signature
43+
delete baseMessage.key
44+
const bytes = Buffer.concat([
45+
SignPrefix,
46+
Message.encode(baseMessage)
47+
])
48+
49+
// Get the public key
50+
messagePublicKey(message, (err, pubKey) => {
51+
if (err) return callback(err, false)
52+
// Verify the base message
53+
pubKey.verify(bytes, message.signature, callback)
54+
})
55+
}
56+
57+
/**
58+
* Returns the PublicKey associated with the given message.
59+
* If no, valid PublicKey can be retrieved an error will be returned.
60+
*
61+
* @param {Message} message
62+
* @param {function(Error, PublicKey)} callback
63+
* @returns {void}
64+
*/
65+
function messagePublicKey (message, callback) {
66+
if (message.key) {
67+
PeerId.createFromPubKey(message.key, (err, peerId) => {
68+
if (err) return callback(err, null)
69+
// the key belongs to the sender, return the key
70+
if (peerId.isEqual(message.from)) return callback(null, peerId.pubKey)
71+
// We couldn't validate pubkey is from the originator, error
72+
callback(new Error('Public Key does not match the originator'))
73+
})
74+
return
75+
}
76+
// TODO: Once js libp2p supports inlining public keys with the peer id
77+
// attempt to unmarshal the public key here.
78+
callback(new Error('Could not get the public key from the originator id'))
79+
}
80+
81+
module.exports = {
82+
messagePublicKey,
83+
signMessage,
84+
SignPrefix,
85+
verifySignature
86+
}

src/utils.js

+20-7
Original file line numberDiff line numberDiff line change
@@ -68,17 +68,30 @@ exports.ensureArray = (maybeArray) => {
6868
return maybeArray
6969
}
7070

71+
/**
72+
* Ensures `message.from` is base58 encoded
73+
* @param {Object} message
74+
* @param {Buffer|String} message.from
75+
* @return {Object}
76+
*/
77+
exports.normalizeInRpcMessage = (message) => {
78+
const m = Object.assign({}, message)
79+
if (Buffer.isBuffer(message.from)) {
80+
m.from = bs58.encode(message.from)
81+
}
82+
return m
83+
}
84+
85+
/**
86+
* The same as `normalizeInRpcMessage`, but performed on an array of messages
87+
* @param {Object[]} messages
88+
* @return {Object[]}
89+
*/
7190
exports.normalizeInRpcMessages = (messages) => {
7291
if (!messages) {
7392
return messages
7493
}
75-
return messages.map((msg) => {
76-
const m = Object.assign({}, msg)
77-
if (Buffer.isBuffer(msg.from)) {
78-
m.from = bs58.encode(msg.from)
79-
}
80-
return m
81-
})
94+
return messages.map(exports.normalizeInRpcMessage)
8295
}
8396

8497
exports.normalizeOutRpcMessage = (message) => {

test/pubsub.spec.js

+41-9
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@ const chai = require('chai')
55
chai.use(require('dirty-chai'))
66
chai.use(require('chai-spies'))
77
const expect = chai.expect
8+
const sinon = require('sinon')
89
const series = require('async/series')
910
const parallel = require('async/parallel')
1011

11-
const { Message } = require('../src/message')
12-
const { SignPrefix } = require('../src/message/sign')
1312
const PubsubBaseProtocol = require('../src')
14-
const { randomSeqno, normalizeOutRpcMessage } = require('../src/utils')
13+
const { randomSeqno } = require('../src/utils')
1514
const utils = require('./utils')
1615
const createNode = utils.createNode
1716

@@ -38,6 +37,10 @@ class PubsubImplementation extends PubsubBaseProtocol {
3837
}
3938

4039
describe('pubsub base protocol', () => {
40+
afterEach(() => {
41+
sinon.restore()
42+
})
43+
4144
describe('fresh nodes', () => {
4245
let nodeA
4346
let nodeB
@@ -96,7 +99,7 @@ describe('pubsub base protocol', () => {
9699

97100
it('_buildMessage normalizes and signs messages', (done) => {
98101
const message = {
99-
from: 'QmABC',
102+
from: psA.peerId.id,
100103
data: 'hello',
101104
seqno: randomSeqno(),
102105
topicIDs: ['test-topic']
@@ -105,17 +108,46 @@ describe('pubsub base protocol', () => {
105108
psA._buildMessage(message, (err, signedMessage) => {
106109
expect(err).to.not.exist()
107110

108-
const bytesToSign = Buffer.concat([
109-
SignPrefix,
110-
Message.encode(normalizeOutRpcMessage(message))
111-
])
111+
psA.validate(signedMessage, (err, verified) => {
112+
expect(verified).to.eql(true)
113+
done(err)
114+
})
115+
})
116+
})
117+
118+
it('validate with strict signing off will validate a present signature', (done) => {
119+
const message = {
120+
from: psA.peerId.id,
121+
data: 'hello',
122+
seqno: randomSeqno(),
123+
topicIDs: ['test-topic']
124+
}
125+
126+
sinon.stub(psA, 'strictSigning').value(false)
127+
128+
psA._buildMessage(message, (err, signedMessage) => {
129+
expect(err).to.not.exist()
112130

113-
psA.peerId.pubKey.verify(bytesToSign, signedMessage.signature, (err, verified) => {
131+
psA.validate(signedMessage, (err, verified) => {
114132
expect(verified).to.eql(true)
115133
done(err)
116134
})
117135
})
118136
})
137+
138+
it('validate with strict signing requires a signature', (done) => {
139+
const message = {
140+
from: psA.peerId.id,
141+
data: 'hello',
142+
seqno: randomSeqno(),
143+
topicIDs: ['test-topic']
144+
}
145+
146+
psA.validate(message, (err, verified) => {
147+
expect(verified).to.eql(false)
148+
done(err)
149+
})
150+
})
119151
})
120152

121153
describe('dial the pubsub protocol on mount', () => {

test/sign.spec.js

+39-4
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ chai.use(require('dirty-chai'))
77
const expect = chai.expect
88

99
const { Message } = require('../src/message')
10-
const { signMessage, SignPrefix } = require('../src/message/sign')
10+
const {
11+
signMessage,
12+
SignPrefix,
13+
verifySignature
14+
} = require('../src/message/sign')
1115
const PeerId = require('peer-id')
1216
const { randomSeqno } = require('../src/utils')
1317

@@ -22,9 +26,9 @@ describe('message signing', () => {
2226
})
2327
})
2428

25-
it('should be able to sign a message', (done) => {
29+
it('should be able to sign and verify a message', (done) => {
2630
const message = {
27-
from: 'QmABC',
31+
from: peerId.id,
2832
data: 'hello',
2933
seqno: randomSeqno(),
3034
topicIDs: ['test-topic']
@@ -43,7 +47,38 @@ describe('message signing', () => {
4347
expect(signedMessage.key).to.eql(peerId.pubKey.bytes)
4448

4549
// Verify the signature
46-
peerId.pubKey.verify(bytesToSign, signedMessage.signature, (err, verified) => {
50+
verifySignature(signedMessage, (err, verified) => {
51+
expect(err).to.not.exist()
52+
expect(verified).to.eql(true)
53+
done(err)
54+
})
55+
})
56+
})
57+
})
58+
59+
it('should be able to extract the public key from the message', (done) => {
60+
const message = {
61+
from: peerId.id,
62+
data: 'hello',
63+
seqno: randomSeqno(),
64+
topicIDs: ['test-topic']
65+
}
66+
67+
const bytesToSign = Buffer.concat([SignPrefix, Message.encode(message)])
68+
69+
peerId.privKey.sign(bytesToSign, (err, expectedSignature) => {
70+
if (err) return done(err)
71+
72+
signMessage(peerId, message, (err, signedMessage) => {
73+
if (err) return done(err)
74+
75+
// Check the signature and public key
76+
expect(signedMessage.signature).to.eql(expectedSignature)
77+
expect(signedMessage.key).to.eql(peerId.pubKey.bytes)
78+
79+
// Verify the signature
80+
verifySignature(signedMessage, (err, verified) => {
81+
expect(err).to.not.exist()
4782
expect(verified).to.eql(true)
4883
done(err)
4984
})

0 commit comments

Comments
 (0)