Skip to content
This repository was archived by the owner on Jun 26, 2023. It is now read-only.

Commit 946b046

Browse files
authored
feat: pubsub: add global signature policy (#66)
BREAKING CHANGE: `signMessages` and `strictSigning` pubsub configuration options replaced with a `globalSignaturePolicy` option
1 parent d168c7d commit 946b046

File tree

8 files changed

+200
-57
lines changed

8 files changed

+200
-57
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"libp2p-tcp": "^0.15.0",
5757
"multiaddr": "^8.0.0",
5858
"multibase": "^3.0.0",
59+
"multihashes": "^3.0.1",
5960
"p-defer": "^3.0.0",
6061
"p-limit": "^2.3.0",
6162
"p-wait-for": "^3.1.0",

src/pubsub/README.md

+29-10
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ Table of Contents
1111
* [Extend interface](#extend-interface)
1212
* [Example](#example)
1313
* [API](#api)
14+
* [Constructor](#constructor)
15+
* [new Pubsub(options)](#new-pubsuboptions)
16+
* [Parameters](#parameters)
1417
* [Start](#start)
1518
* [pubsub.start()](#pubsubstart)
1619
* [Returns](#returns)
@@ -19,24 +22,24 @@ Table of Contents
1922
* [Returns](#returns-1)
2023
* [Publish](#publish)
2124
* [pubsub.publish(topics, message)](#pubsubpublishtopics-message)
22-
* [Parameters](#parameters)
25+
* [Parameters](#parameters-1)
2326
* [Returns](#returns-2)
2427
* [Subscribe](#subscribe)
2528
* [pubsub.subscribe(topic)](#pubsubsubscribetopic)
26-
* [Parameters](#parameters-1)
29+
* [Parameters](#parameters-2)
2730
* [Unsubscribe](#unsubscribe)
2831
* [pubsub.unsubscribe(topic)](#pubsubunsubscribetopic)
29-
* [Parameters](#parameters-2)
32+
* [Parameters](#parameters-3)
3033
* [Get Topics](#get-topics)
3134
* [pubsub.getTopics()](#pubsubgettopics)
3235
* [Returns](#returns-3)
3336
* [Get Peers Subscribed to a topic](#get-peers-subscribed-to-a-topic)
3437
* [pubsub.getSubscribers(topic)](#pubsubgetsubscriberstopic)
35-
* [Parameters](#parameters-3)
38+
* [Parameters](#parameters-4)
3639
* [Returns](#returns-4)
3740
* [Validate](#validate)
3841
* [pubsub.validate(message)](#pubsubvalidatemessage)
39-
* [Parameters](#parameters-4)
42+
* [Parameters](#parameters-5)
4043
* [Returns](#returns-5)
4144
* [Test suite usage](#test-suite-usage)
4245

@@ -49,7 +52,7 @@ You can check the following implementations as examples for building your own pu
4952

5053
## Interface usage
5154

52-
`interface-pubsub` abstracts the implementation protocol registration within `libp2p` and takes care of all the protocol connections and streams, as well as the subscription management. This way, a pubsub implementation can focus on its message routing algorithm, instead of also needing to create the setup for it.
55+
`interface-pubsub` abstracts the implementation protocol registration within `libp2p` and takes care of all the protocol connections and streams, as well as the subscription management and the features describe in the libp2p [pubsub specs](https://github.com/libp2p/specs/tree/master/pubsub). This way, a pubsub implementation can focus on its message routing algorithm, instead of also needing to create the setup for it.
5356

5457
### Extend interface
5558

@@ -74,16 +77,15 @@ All the remaining functions **MUST NOT** be overwritten.
7477
The following example aims to show how to create your pubsub implementation extending this base protocol. The pubsub implementation will handle the subscriptions logic.
7578

7679
```JavaScript
77-
const Pubsub = require('libp2p-pubsub')
80+
const Pubsub = require('libp2p-interfaces/src/pubsub')
7881

7982
class PubsubImplementation extends Pubsub {
8083
constructor({ libp2p, options })
8184
super({
8285
debugName: 'libp2p:pubsub',
8386
multicodecs: '/pubsub-implementation/1.0.0',
8487
libp2p,
85-
signMessages: options.signMessages,
86-
strictSigning: options.strictSigning
88+
globalSigningPolicy: options.globalSigningPolicy
8789
})
8890
}
8991

@@ -98,6 +100,23 @@ class PubsubImplementation extends Pubsub {
98100

99101
The interface aims to specify a common interface that all pubsub router implementation should follow. A pubsub router implementation should extend the [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter). When peers receive pubsub messages, these messages will be emitted by the event emitter where the `eventName` will be the `topic` associated with the message.
100102

103+
### Constructor
104+
105+
The base class constructor configures the pubsub instance for use with a libp2p instance. It includes settings for logging, signature policies, etc.
106+
107+
#### `new Pubsub({options})`
108+
109+
##### Parameters
110+
111+
| Name | Type | Description | Default |
112+
|------|------|-------------|---------|
113+
| options.libp2p | `Libp2p` | libp2p instance | required, no default |
114+
| options.debugName | `string` | log namespace | required, no default |
115+
| options.multicodecs | `string \| Array<string>` | protocol identifier(s) | required, no default |
116+
| options.globalSignaturePolicy | `'StrictSign' \| 'StrictNoSign'` | signature policy to be globally applied | `'StrictSign'` |
117+
| options.canRelayMessage | `boolean` | if can relay messages if not subscribed | `false` |
118+
| options.emitSelf | `boolean` | if `publish` should emit to self, if subscribed | `false` |
119+
101120
### Start
102121

103122
Starts the pubsub subsystem. The protocol will be registered to `libp2p`, which will result in pubsub being notified when peers who support the protocol connect/disconnect to `libp2p`.
@@ -185,7 +204,7 @@ Get a list of the [PeerId](https://github.com/libp2p/js-peer-id) strings that ar
185204

186205
### Validate
187206

188-
Validates the signature of a message.
207+
Validates a message according to the signature policy and topic-specific validation function.
189208

190209
#### `pubsub.validate(message)`
191210

src/pubsub/errors.js

+41-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,46 @@
11
'use strict'
22

33
exports.codes = {
4+
/**
5+
* Signature policy is invalid
6+
*/
7+
ERR_INVALID_SIGNATURE_POLICY: 'ERR_INVALID_SIGNATURE_POLICY',
8+
/**
9+
* Signature policy is unhandled
10+
*/
11+
ERR_UNHANDLED_SIGNATURE_POLICY: 'ERR_UNHANDLED_SIGNATURE_POLICY',
12+
13+
// Strict signing codes
14+
15+
/**
16+
* Message expected to have a `signature`, but doesn't
17+
*/
418
ERR_MISSING_SIGNATURE: 'ERR_MISSING_SIGNATURE',
5-
ERR_INVALID_SIGNATURE: 'ERR_INVALID_SIGNATURE'
19+
/**
20+
* Message expected to have a `seqno`, but doesn't
21+
*/
22+
ERR_MISSING_SEQNO: 'ERR_MISSING_SEQNO',
23+
/**
24+
* Message `signature` is invalid
25+
*/
26+
ERR_INVALID_SIGNATURE: 'ERR_INVALID_SIGNATURE',
27+
28+
// Strict no-signing codes
29+
30+
/**
31+
* Message expected to not have a `from`, but does
32+
*/
33+
ERR_UNEXPECTED_FROM: 'ERR_UNEXPECTED_FROM',
34+
/**
35+
* Message expected to not have a `signature`, but does
36+
*/
37+
ERR_UNEXPECTED_SIGNATURE: 'ERR_UNEXPECTED_SIGNATURE',
38+
/**
39+
* Message expected to not have a `key`, but does
40+
*/
41+
ERR_UNEXPECTED_KEY: 'ERR_UNEXPECTED_KEY',
42+
/**
43+
* Message expected to not have a `seqno`, but does
44+
*/
45+
ERR_UNEXPECTED_SEQNO: 'ERR_UNEXPECTED_SEQNO'
646
}

src/pubsub/index.js

+61-27
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const { codes } = require('./errors')
1313
*/
1414
const message = require('./message')
1515
const PeerStreams = require('./peer-streams')
16+
const { SignaturePolicy } = require('./signature-policy')
1617
const utils = require('./utils')
1718

1819
const {
@@ -44,8 +45,7 @@ class PubsubBaseProtocol extends EventEmitter {
4445
* @param {String} props.debugName log namespace
4546
* @param {Array<string>|string} props.multicodecs protocol identificers to connect
4647
* @param {Libp2p} props.libp2p
47-
* @param {boolean} [props.signMessages = true] if messages should be signed
48-
* @param {boolean} [props.strictSigning = true] if message signing should be required
48+
* @param {SignaturePolicy} [props.globalSignaturePolicy = SignaturePolicy.StrictSign] defines how signatures should be handled
4949
* @param {boolean} [props.canRelayMessage = false] if can relay messages not subscribed
5050
* @param {boolean} [props.emitSelf = false] if publish should emit to self, if subscribed
5151
* @abstract
@@ -54,8 +54,7 @@ class PubsubBaseProtocol extends EventEmitter {
5454
debugName,
5555
multicodecs,
5656
libp2p,
57-
signMessages = true,
58-
strictSigning = true,
57+
globalSignaturePolicy = SignaturePolicy.StrictSign,
5958
canRelayMessage = false,
6059
emitSelf = false
6160
}) {
@@ -109,14 +108,17 @@ class PubsubBaseProtocol extends EventEmitter {
109108
*/
110109
this.peers = new Map()
111110

112-
// Message signing
113-
this.signMessages = signMessages
111+
// validate signature policy
112+
if (!SignaturePolicy[globalSignaturePolicy]) {
113+
throw errcode(new Error('Invalid global signature policy'), codes.ERR_INVALID_SIGUATURE_POLICY)
114+
}
114115

115116
/**
116-
* If message signing should be required for incoming messages
117-
* @type {boolean}
117+
* The signature policy to follow by default
118+
*
119+
* @type {SignaturePolicy}
118120
*/
119-
this.strictSigning = strictSigning
121+
this.globalSignaturePolicy = globalSignaturePolicy
120122

121123
/**
122124
* If router can relay received messages, even if not subscribed
@@ -440,7 +442,15 @@ class PubsubBaseProtocol extends EventEmitter {
440442
* @returns {Uint8Array} message id as bytes
441443
*/
442444
getMsgId (msg) {
443-
return utils.msgId(msg.from, msg.seqno)
445+
const signaturePolicy = this.globalSignaturePolicy
446+
switch (signaturePolicy) {
447+
case SignaturePolicy.StrictSign:
448+
return utils.msgId(msg.from, msg.seqno)
449+
case SignaturePolicy.StrictNoSign:
450+
return utils.noSignMsgId(msg.data)
451+
default:
452+
throw errcode(new Error('Cannot get message id: unhandled signature policy: ' + signaturePolicy), codes.ERR_UNHANDLED_SIGNATURE_POLICY)
453+
}
444454
}
445455

446456
/**
@@ -511,16 +521,36 @@ class PubsubBaseProtocol extends EventEmitter {
511521
* @returns {Promise<void>}
512522
*/
513523
async validate (message) { // eslint-disable-line require-await
514-
// If strict signing is on and we have no signature, abort
515-
if (this.strictSigning && !message.signature) {
516-
throw errcode(new Error('Signing required and no signature was present'), codes.ERR_MISSING_SIGNATURE)
517-
}
518-
519-
// Check the message signature if present
520-
if (message.signature && !(await verifySignature(message))) {
521-
throw errcode(new Error('Invalid message signature'), codes.ERR_INVALID_SIGNATURE)
524+
const signaturePolicy = this.globalSignaturePolicy
525+
switch (signaturePolicy) {
526+
case SignaturePolicy.StrictNoSign:
527+
if (message.from) {
528+
throw errcode(new Error('StrictNoSigning: from should not be present'), codes.ERR_UNEXPECTED_FROM)
529+
}
530+
if (message.signature) {
531+
throw errcode(new Error('StrictNoSigning: signature should not be present'), codes.ERR_UNEXPECTED_SIGNATURE)
532+
}
533+
if (message.key) {
534+
throw errcode(new Error('StrictNoSigning: key should not be present'), codes.ERR_UNEXPECTED_KEY)
535+
}
536+
if (message.seqno) {
537+
throw errcode(new Error('StrictNoSigning: seqno should not be present'), codes.ERR_UNEXPECTED_SEQNO)
538+
}
539+
break
540+
case SignaturePolicy.StrictSign:
541+
if (!message.signature) {
542+
throw errcode(new Error('StrictSigning: Signing required and no signature was present'), codes.ERR_MISSING_SIGNATURE)
543+
}
544+
if (!message.seqno) {
545+
throw errcode(new Error('StrictSigning: Signing required and no seqno was present'), codes.ERR_MISSING_SEQNO)
546+
}
547+
if (!(await verifySignature(message))) {
548+
throw errcode(new Error('StrictSigning: Invalid message signature'), codes.ERR_INVALID_SIGNATURE)
549+
}
550+
break
551+
default:
552+
throw errcode(new Error('Cannot validate message: unhandled signature policy: ' + signaturePolicy), codes.ERR_UNHANDLED_SIGNATURE_POLICY)
522553
}
523-
524554
for (const topic of message.topicIDs) {
525555
const validatorFn = this.topicValidators.get(topic)
526556
if (!validatorFn) {
@@ -538,11 +568,16 @@ class PubsubBaseProtocol extends EventEmitter {
538568
* @returns {Promise<Message>}
539569
*/
540570
_buildMessage (message) {
541-
const msg = utils.normalizeOutRpcMessage(message)
542-
if (this.signMessages) {
543-
return signMessage(this.peerId, msg)
544-
} else {
545-
return message
571+
const signaturePolicy = this.globalSignaturePolicy
572+
switch (signaturePolicy) {
573+
case SignaturePolicy.StrictSign:
574+
message.from = this.peerId.toB58String()
575+
message.seqno = utils.randomSeqno()
576+
return signMessage(this.peerId, utils.normalizeOutRpcMessage(message))
577+
case SignaturePolicy.StrictNoSign:
578+
return message
579+
default:
580+
throw errcode(new Error('Cannot build message: unhandled signature policy: ' + signaturePolicy), codes.ERR_UNHANDLED_SIGNATURE_POLICY)
546581
}
547582
}
548583

@@ -586,13 +621,11 @@ class PubsubBaseProtocol extends EventEmitter {
586621
const from = this.peerId.toB58String()
587622
let msgObject = {
588623
receivedFrom: from,
589-
from: from,
590624
data: message,
591-
seqno: utils.randomSeqno(),
592625
topicIDs: [topic]
593626
}
594627

595-
// ensure that any operations performed on the message will include the signature
628+
// ensure that the message follows the signature policy
596629
const outMsg = await this._buildMessage(msgObject)
597630
msgObject = utils.normalizeInRpcMessage(outMsg)
598631

@@ -666,3 +699,4 @@ class PubsubBaseProtocol extends EventEmitter {
666699
module.exports = PubsubBaseProtocol
667700
module.exports.message = message
668701
module.exports.utils = utils
702+
module.exports.SignaturePolicy = SignaturePolicy

src/pubsub/signature-policy.js

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use strict'
2+
3+
/**
4+
* Enum for Signature Policy
5+
* Details how message signatures are produced/consumed
6+
*/
7+
exports.SignaturePolicy = {
8+
/**
9+
* On the producing side:
10+
* * Build messages with the signature, key (from may be enough for certain inlineable public key types), from and seqno fields.
11+
*
12+
* On the consuming side:
13+
* * Enforce the fields to be present, reject otherwise.
14+
* * Propagate only if the fields are valid and signature can be verified, reject otherwise.
15+
*/
16+
StrictSign: 'StrictSign',
17+
/**
18+
* On the producing side:
19+
* * Build messages without the signature, key, from and seqno fields.
20+
* * The corresponding protobuf key-value pairs are absent from the marshalled message, not just empty.
21+
*
22+
* On the consuming side:
23+
* * Enforce the fields to be absent, reject otherwise.
24+
* * Propagate only if the fields are absent, reject otherwise.
25+
* * A message_id function will not be able to use the above fields, and should instead rely on the data field. A commonplace strategy is to calculate a hash.
26+
*/
27+
StrictNoSign: 'StrictNoSign'
28+
}

src/pubsub/utils.js

+10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const randomBytes = require('libp2p-crypto/src/random-bytes')
44
const uint8ArrayToString = require('uint8arrays/to-string')
55
const uint8ArrayFromString = require('uint8arrays/from-string')
66
const PeerId = require('peer-id')
7+
const multihash = require('multihashes')
78
exports = module.exports
89

910
/**
@@ -32,6 +33,15 @@ exports.msgId = (from, seqno) => {
3233
return msgId
3334
}
3435

36+
/**
37+
* Generate a message id, based on message `data`.
38+
*
39+
* @param {Uint8Array} data
40+
* @returns {Uint8Array}
41+
* @private
42+
*/
43+
exports.noSignMsgId = (data) => multihash.encode(data, 'sha2')
44+
3545
/**
3646
* Check if any member of the first set is also a member
3747
* of the second set.

0 commit comments

Comments
 (0)