-
Notifications
You must be signed in to change notification settings - Fork 998
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
Conversation
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 |
There was a problem hiding this 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/config.rs
Outdated
/// 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// 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.
There was a problem hiding this comment.
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?
protocols/gossipsub/src/config.rs
Outdated
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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/protocol.rs
Outdated
use super::*; | ||
|
||
#[test] | ||
fn sign_and_verify_message_peer_inline() { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@AgeManning see sigp#31.
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:
This logic, also brings to light a vulnerability in using 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. |
fb51926
to
ac3c977
Compare
There was a problem hiding this 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.
protocols/gossipsub/src/behaviour.rs
Outdated
); | ||
return; | ||
} | ||
|
||
debug!( | ||
"Publishing message: {:?}", | ||
(self.config.message_id_fn)(&message) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(self.config.message_id_fn)(&message) | |
msg_id, |
See let msg_id = (self.config.message_id_fn)(&message);
above.
Do I understand correctly that currently if a node is signing messages ( Just checking for now, haven't made up my mind whether that is good or not. |
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. |
There was a problem hiding this 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
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() { |
There was a problem hiding this comment.
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?
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, | |
}; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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. |
There was a problem hiding this 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!
core/src/identity.rs
Outdated
@@ -29,7 +29,7 @@ pub mod secp256k1; | |||
pub mod error; | |||
|
|||
use self::error::*; | |||
use crate::{PeerId, keys_proto}; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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/behaviour.rs
Outdated
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() { |
There was a problem hiding this comment.
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.
core/src/identity/error.rs
Outdated
@@ -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>>, |
There was a problem hiding this comment.
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?
* 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.
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. |
Thanks for the review. Have updated based on comments. |
There was a problem hiding this 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!
Are these changes backward-compatible? Could someone update the changelog? |
@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. |
Hey yep, can do. I've also discovered that the |
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 thememcache
in it's place.