Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gossipsub message signing #1583

Merged
merged 43 commits into from
Aug 3, 2020
Merged

Gossipsub message signing #1583

merged 43 commits into from
Aug 3, 2020

Conversation

AgeManning
Copy link
Contributor

Description

This adds optional message signing to the gossipsub protocol as per the libp2p specifications.

This also removes the LruCache received cache and simply uses the memcache in it's place.

@burdges
Copy link

burdges commented May 19, 2020

As an aside, we've gossip messages that benefit from exotic signatures, like VRFs for approval assignments (A&V) or maybe even aggregation tricks in GRANDPA.

It's normally fastest if gossip messages run over authenticated channels that we drop on harmless but incorrect and spammy messages, but then do their own authentication, probably polite, possibly weird, before being sent back out. It's also bad for layering, auditing, etc. to do this. shrug

Copy link
Member

@mxinden mxinden left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a first pass. Thanks for the work!

protocols/gossipsub/src/behaviour.rs Outdated Show resolved Hide resolved
/// When set to `true` all published messages are signed by the libp2p key (default is `true`).
pub sign_messages: bool,

/// Determines whether unsigned messages will be accepted. If set to false, unsigned messages
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Determines whether unsigned messages will be accepted. If set to false, unsigned messages
/// Determines whether incoming unsigned messages will be accepted. If set to false, unsigned messages

This is about incoming messages only, right? Would help me a bit to better comprehend.

Copy link
Contributor Author

@AgeManning AgeManning May 25, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. There is a config in go, around whether to use message signing or not.

I put this in, to mimic the current implementation which completely ignores the signature field. I was concerned with interoperability with other implementations and wanted this maximally configurable such that it could accept messages with other implementations that were not signing messages, even if we were.

However, now that you have pointed this out, perhaps this will cause more trouble than it saves in practice and we should just have a boolean that dictates we either sign/verify or don't sign/verify.

What do you think?

pub fn hash_topics(&mut self) -> &mut Self {
self.config.hash_topics = true;
/// Flag determining if gossipsub topics are hashed or sent as plain strings (default is false).
pub fn hash_topics(&mut self, value: bool) -> &mut Self {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not particularly important: Why change the signatures here? Why would someone call hash_topics(false)? If we keep it as is I would prefer set_hash_topics as one can set the value to both states. I prefer the previous implementation.

Same for the other boolean configuration options below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. I preferred the original version also, but had been playing with other builder implementations and somehow convinced myself that the boolean signature was a more canonical pattern in the rust community (for some reason I thought tokio used this pattern, on inspection some other packages do, like hyper for example: https://docs.rs/hyper/0.13.5/hyper/server/struct.Builder.html).

I also prefer the previous implementation, so its good to know someone else thinks so.
Will change back.

protocols/gossipsub/src/config.rs Show resolved Hide resolved
protocols/gossipsub/src/mcache.rs Outdated Show resolved Hide resolved
protocols/gossipsub/src/protocol.rs Outdated Show resolved Hide resolved
protocols/gossipsub/src/protocol.rs Outdated Show resolved Hide resolved
protocols/gossipsub/src/protocol.rs Outdated Show resolved Hide resolved
use super::*;

#[test]
fn sign_and_verify_message_peer_inline() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could greatly benefit of random test input generation e.g. via quickcheck. I am happy to provide a commit in case you agree.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I agree, I'm about to go off and build out 1.1. If you have the time to add this in, it would be great

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

protocols/gossipsub/src/rpc.proto Show resolved Hide resolved
@AgeManning
Copy link
Contributor Author

Thanks for the quick review @mxinden.

On closer inspection of your comments, I've realised I've made a few pretty significant errors.

Firstly, I was signing all messages and didn't consider forwarded messages, which obviously we shouldn't sign. There are two ways to address this imo:

  1. Perform the signing at the behaviour level. This is nice because we could sign messages in the publish_many() function and prevent the cloning of keypair in the encoding as well as a few other variables. The downside of this is that the signing requires the protobuf encoding of the messages which is mainly dealt with in protocol.rs. For this reason, I went with (2) (also because it required less code changes).
  2. Check outgoing messages. If the source_id is us, then we should be signing it (provided no signature already exists).

This logic, also brings to light a vulnerability in using allow_unsigned_messages. If a peer sent us a message using us as the source id, and we ignored the signature, then signed the message, a malicious peer could get us to sign arbitrary messages. Therefore, I've removed that option, and signing and verification is binary, we either sign and verify, or we don't.

If we are not signing, we also don't bother checking the signatures (even if one exists). I prefer this functionality, somewhat selfishly because in my use, I'm not signing and don't care about signatures and don't want to waste any computation checking signatures that I don't care about, regardless if they are valid or not.

Copy link
Member

@mxinden mxinden left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I can tell a GossipsubMessage instantiated in publish_many is
cloned twice, once for the mesh peers and once for the subscribed peers. Adding
the signature later on implies that we sign the same message at least twice,
correct?

Doing the signing in publish_many as you suggest in (1) would solve the above,
correct?

Small caveat on sigining in NetworkBehaviour is the dealy this introduces to
the main thread with this somewhat heavy computation. But without a benchmark
I doubt we should take this into consideration.

This logic, also brings to light a vulnerability in using
allow_unsigned_messages. If a peer sent us a message using us as the source
id, and we ignored the signature, then signed the message, a malicious peer
could get us to sign arbitrary messages. Therefore, I've removed that option,
and signing and verification is binary, we either sign and verify, or we
don't.

Doing the signing in publish_many would prevent this, right?

Overall I would suggest moving the process of signing close to
GossipsubMessage instantiation for the above reasons. Given that this is
purely protocols/gossipsub internal it can still be changed later on without a
breaking change. What do you think?

If we are not signing, we also don't bother checking the signatures (even if
one exists). I prefer this functionality, somewhat selfishly because in my
use, I'm not signing and don't care about signatures and don't want to waste
any computation checking signatures that I don't care about, regardless if
they are valid or not.

That sounds reasonable to me.

);
return;
}

debug!(
"Publishing message: {:?}",
(self.config.message_id_fn)(&message)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
(self.config.message_id_fn)(&message)
msg_id,

See let msg_id = (self.config.message_id_fn)(&message); above.

@mxinden
Copy link
Member

mxinden commented May 28, 2020

Do I understand correctly that currently if a node is signing messages (disable_message_signing == false) there is no way to allow unsigned messages from other nodes?

Just checking for now, haven't made up my mind whether that is good or not.

@AgeManning
Copy link
Contributor Author

Do I understand correctly that currently if a node is signing messages (disable_message_signing == false) there is no way to allow unsigned messages from other nodes?

I changed it to this, yes. I originally allowed the other behaviour for debugging. After a bit of thought, I decided this current logic was best. Signing is either enabled or not. The negative case can accept and read signed messages and will propagate them but the affirmative case rejects all unsigned/invalid messages.

The reason was to try and avoid networks of mixed cases. If a node wants to enable signing, I think it wouldn't be great that it propagates unsigned messages also.

I haven't completed the scoring in 1.1 in relation to how a peer would score another peer that sent unsigned messages. But if in a network where peers expect signed messages, and one peer was not signing and another peer was forwarding unsigned messages (i.e we enabled reading unsigned messages) I think both peers would be scored negatively (need to confirm this).

We could add flags that allow reading of unsigned messages without propagation and then with propagation, but the complexity grows and I'm not entirely sure we gain much from this added complexity of allowing mixed networks.

For these reasons I decided to leave it binary. I guess the correct logic would be to follow other implementations. If other clients support it, we should do. From memory the go-client has it binary, but perhaps there is a flag that allows it, will need to check.

I don't use the signing part of gossipsub, so if others have some use for mixed networks, I'm happy to allow it add the flags in, but we might end up with more issues.

Copy link
Member

@mxinden mxinden left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AgeManning I am sorry for the delay. Thanks for moving the message signing.

I changed it to this, yes. I originally allowed the other behaviour for debugging. After a bit of thought, I decided this current logic was best. Signing is either enabled or not. The negative case can accept and read signed messages and will propagate them but the affirmative case rejects all unsigned/invalid messages.

This sounds reasonable to me. In case anyone has a use-case for mixed networks we can still adjust the code.

protocols/gossipsub/src/behaviour.rs Outdated Show resolved Hide resolved
protocols/gossipsub/src/behaviour.rs Outdated Show resolved Hide resolved
protocols/gossipsub/src/config.rs Outdated Show resolved Hide resolved
protocols/gossipsub/src/behaviour.rs Outdated Show resolved Hide resolved
protocols/gossipsub/src/behaviour.rs Outdated Show resolved Hide resolved
protocols/gossipsub/src/behaviour.rs Outdated Show resolved Hide resolved
protocols/gossipsub/src/behaviour.rs Outdated Show resolved Hide resolved
protocols/gossipsub/src/behaviour.rs Outdated Show resolved Hide resolved
Comment on lines 984 to 994
let message = rpc_proto::Message {
from: Some(self.message_source_id.clone().into_bytes()),
data: Some(data),
seqno: Some(sequence_number.to_be_bytes().to_vec()),
topic_ids: topics.clone().into_iter().map(|t| t.into()).collect(),
signature: None,
key: None,
};

// If a signature is required, generate it
let signature = if let Some(keypair) = self.keypair.as_ref() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

message is only needed when Some(keypair) == self.keypair.as_ref() right?

Suggested change
let message = rpc_proto::Message {
from: Some(self.message_source_id.clone().into_bytes()),
data: Some(data),
seqno: Some(sequence_number.to_be_bytes().to_vec()),
topic_ids: topics.clone().into_iter().map(|t| t.into()).collect(),
signature: None,
key: None,
};
// If a signature is required, generate it
let signature = if let Some(keypair) = self.keypair.as_ref() {
// If a signature is required, generate it
let signature = if let Some(keypair) = self.keypair.as_ref() {
let message = rpc_proto::Message {
from: Some(self.message_source_id.clone().into_bytes()),
data: Some(data),
seqno: Some(sequence_number.to_be_bytes().to_vec()),
topic_ids: topics.clone().into_iter().map(|t| t.into()).collect(),
signature: None,
key: None,
};

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is true, but if i recall correctly, I was trying to avoid the clone on data.
I'll change it, but it now clones the data.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I guess as this is only scoped to the function itself we can delay the decision. In case it shows up in benchmarks we can easily change it. Rustc or LLVM might as well just optimize it away, not sure.

protocols/gossipsub/src/mcache.rs Outdated Show resolved Hide resolved
@AgeManning
Copy link
Contributor Author

I finally got back around to this.

I think I've addressed all your comments, I've left comments on ones that I think require them.

We've since noticed a bug in gossipsub where subscriptions were not being sent to all peers. I've added that correction in too, whilst I'm here.

Copy link
Member

@mxinden mxinden left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I've addressed all your comments, I've left comments on ones that I think require them.

Thanks. Overall this looks good, just 2 small things that need fixing before merging.

We've since noticed a bug in gossipsub where subscriptions were not being sent to all peers. I've added that correction in too, whilst I'm here.

Thanks!

@@ -29,7 +29,7 @@ pub mod secp256k1;
pub mod error;

use self::error::*;
use crate::{PeerId, keys_proto};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am guessing that the changes in identity.rs and core/src/identity/error.rs were done by rustfmt. Would you mind reverting them in this pull-request as they seem unrelated? If you feel like the suggestions would improve code readability please open a separate pull request.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, editor has it in by default. I usually catch these, seems I missed one.

protocols/gossipsub/src/error.rs Outdated Show resolved Hide resolved
Comment on lines 984 to 994
let message = rpc_proto::Message {
from: Some(self.message_source_id.clone().into_bytes()),
data: Some(data),
seqno: Some(sequence_number.to_be_bytes().to_vec()),
topic_ids: topics.clone().into_iter().map(|t| t.into()).collect(),
signature: None,
key: None,
};

// If a signature is required, generate it
let signature = if let Some(keypair) = self.keypair.as_ref() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I guess as this is only scoped to the function itself we can delay the decision. In case it shows up in benchmarks we can easily change it. Rustc or LLVM might as well just optimize it away, not sure.

@@ -27,16 +27,22 @@ use std::fmt;
#[derive(Debug)]
pub struct DecodingError {
msg: String,
source: Option<Box<dyn Error + Send + Sync>>
source: Option<Box<dyn Error + Send + Sync>>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AgeManning can you revert these formatting changes as well?

AgeManning and others added 4 commits July 24, 2020 18:58
* Make the GossipsubRpc debug instance a bit nicer

Basically just don't show empty components to reduce visual noise

* Introduce send_message helper function to avoid repetition of boilerplate

This also serves to have a central place to log or otherwise intercept all
outgoing messages to other peers of the behaviour.

* Simplify the arc removal in poll
And do the corresponding changes to make this work.
@AgeManning
Copy link
Contributor Author

Sorry, took a bit of a detour on this one.

The extra changes since last review are:

I'll be doing some tests on the current version, but I think this is ready for another review now.

protocols/gossipsub/src/behaviour.rs Outdated Show resolved Hide resolved
protocols/gossipsub/src/behaviour.rs Outdated Show resolved Hide resolved
protocols/gossipsub/src/behaviour.rs Outdated Show resolved Hide resolved
protocols/gossipsub/src/behaviour.rs Outdated Show resolved Hide resolved
protocols/gossipsub/src/behaviour.rs Outdated Show resolved Hide resolved
@AgeManning
Copy link
Contributor Author

Thanks for the review. Have updated based on comments.

Copy link
Member

@mxinden mxinden left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me. In case there are no objections and @AgeManning you merge current master I am happy to merge this.

Thanks for upstreaming all the work!

@mxinden mxinden merged commit d8ad7bd into libp2p:master Aug 3, 2020
@romanb
Copy link
Contributor

romanb commented Aug 3, 2020

Are these changes backward-compatible? Could someone update the changelog?

@rklaehn rklaehn mentioned this pull request Aug 4, 2020
@mxinden
Copy link
Member

mxinden commented Aug 6, 2020

@AgeManning do you have time to open up a pull request with a changelog entry for these changes? Otherwise I will prepare one in the next couple of days.

@AgeManning
Copy link
Contributor Author

Hey yep, can do.

I've also discovered that the lru_time_cache used for duplicates here does an O(N) lookup for each message. For a network with a lot of gossipsub messages this isn't great. I've got corrections for this on my own branch. I can make a PR for them, but I'm tempted to wait for the 1.1 PR which we'll make within a week and should contain this modifications also.

@AgeManning AgeManning deleted the gossipsub-signing branch June 15, 2021 01:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants