Skip to content

Commit c36fefa

Browse files
committed
feat: add validate method for validating signatures
1 parent 4e551b0 commit c36fefa

File tree

5 files changed

+148
-21
lines changed

5 files changed

+148
-21
lines changed

src/index.js

Lines changed: 27 additions & 1 deletion
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

@@ -349,6 +354,27 @@ class PubsubBaseProtocol extends EventEmitter {
349354
callback()
350355
})
351356
}
357+
358+
/**
359+
* Validates the given message. The signature will be checked for authenticity.
360+
* @param {rpc.RPC.Message} message
361+
* @param {function(Error, Boolean)} callback
362+
*/
363+
validate (message, callback) {
364+
// If strict signing is on and we have no signature, abort
365+
if (this.strictSigning && !message.signature) {
366+
this.log('Signing required and no signature was present, dropping message:', message)
367+
return nextTick(callback, null, false)
368+
}
369+
370+
// Check the message signature if present
371+
if (message.signature) {
372+
verifySignature(message, (err, valid) => {
373+
if (err) return callback(err)
374+
callback(null, valid)
375+
})
376+
}
377+
}
352378
}
353379

354380
module.exports = PubsubBaseProtocol

src/message/sign.js

Lines changed: 56 additions & 3 deletions
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

Lines changed: 20 additions & 7 deletions
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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ describe('pubsub base protocol', () => {
9696

9797
it('_buildMessage normalizes and signs messages', (done) => {
9898
const message = {
99-
from: 'QmABC',
99+
from: psA.peerId.id,
100100
data: 'hello',
101101
seqno: randomSeqno(),
102102
topicIDs: ['test-topic']
@@ -105,12 +105,12 @@ describe('pubsub base protocol', () => {
105105
psA._buildMessage(message, (err, signedMessage) => {
106106
expect(err).to.not.exist()
107107

108-
const bytesToSign = Buffer.concat([
109-
SignPrefix,
110-
Message.encode(normalizeOutRpcMessage(message))
111-
])
108+
// const bytesToSign = Buffer.concat([
109+
// SignPrefix,
110+
// Message.encode(normalizeOutRpcMessage(message))
111+
// ])
112112

113-
psA.peerId.pubKey.verify(bytesToSign, signedMessage.signature, (err, verified) => {
113+
psA.validate(signedMessage, (err, verified) => {
114114
expect(verified).to.eql(true)
115115
done(err)
116116
})

test/sign.spec.js

Lines changed: 39 additions & 4 deletions
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)