-
Notifications
You must be signed in to change notification settings - Fork 189
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
Implement message signing #97
Conversation
added some tests. |
@@ -500,22 +547,34 @@ loop: | |||
} | |||
|
|||
if throttle { | |||
log.Warningf("message validation throttled; dropping message from %s", src) |
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.
why drop the warning?
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.
it gets logged on the return path, so this would be logging it twice.
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.
Basically the function doesn't do the dropping any more, just validation; so the logging has been moved to the caller.
} | ||
|
||
for i := 0; i < rcount; i++ { | ||
valid := <-rch | ||
if !valid { | ||
log.Warningf("message validation failed; dropping message from %s", src) |
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.
again, dropping the warning feels weird
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.
ditto.
sign.go
Outdated
// no attached key, it must be extractable from the source ID | ||
pubk, err = pid.ExtractPublicKey() | ||
if err != nil { | ||
return err |
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.
maybe log a more specific error here. "could not validate because public key was not available or extractable: $ERR"
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.
sure, that's probably a good idea.
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.
done
return err | ||
} | ||
|
||
if m.Key == nil { |
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'd pull this whole logic out into a 'get key' method either on the message, or that takes a 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.
ok
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.
done
sign.go
Outdated
} | ||
} | ||
|
||
xm := pb.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.
I wonder if we can do something where we enforce that the protobuf puts the signature at the end, and simply slice the raw bytes to the right point instead of remarshaling
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.
that would be very nice indeed -- i don't like the remarshal either.
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.
we could also have a pre-allocated buffer where we WriteMsg
into instead of marshaling.
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.
- Let's not rely on slicing protobufs, if at all possible.
- Please read: How-To Crypto ipfs/notes#249. We need to include something that says that the signature is for pubsub.
Regardless, we need to make sure we can add new fields and have them covered by the signature by default.
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.
mmm, good point. We could prefix the data we're signing with something like "libp2p-pubsub"
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.
Hrm, not convinced it's absolutely necessary as this is transient data, but sure we can add a prefix.
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.
Added a libp2p-pubsub:
prefix.
@@ -19,6 +19,8 @@ message Message { | |||
optional bytes data = 2; | |||
optional bytes seqno = 3; | |||
repeated string topicIDs = 4; | |||
optional bytes signature = 5; |
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.
in light of my other comment, how about we make key come before signature, and space them out a bit (instead of 5 and 6, use 50 and 51) so future additions to the protocol don't have to come after it.
cc @Stebalien @warpfork to tell me "no, you cant just slice protobufs like that"
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.
the key is not included in the signature, so it can stay where it is.
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'm not sure I can say "you can't" but I probably "wouldn't" if I could help it. I'd be a lot more comfortable if an envelope type {content, sig}
was added.
I feel like holistically, even across wildly different formats, people tend to rue the day they made in-band signatures. For (distant, but again, holistically comparable) example, android putting sigs on apks inside the filesystem has been no end of comedic problems over the years.
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 guess one way to do this would be to have in the top level protobuf both the existing regular unsigned messages, and a new field for signed 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.
that also kind of disrupts the chronology of the development of the protobuf. i think we'd probably be best off keeping it as is
Approach looks good to me, nice and simple. My main concern now is making sure we don't shoot ourselves in the foot on perf. This code as written looks pretty allocation heavy. |
pubsub.go
Outdated
@@ -186,6 +190,13 @@ func WithValidateThrottle(n int) Option { | |||
} | |||
} | |||
|
|||
func WithMessageSigning() Option { |
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.
Maybe we should consider using different keys than our nodes peer key?
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.
Sure, we can do that too -- the from
in published messages will have to change to match the signing key though.
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 we can have a WithMessageSigningKey
option that explicitly specifies the signing key.
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, from
can be any valid key. However, I'd prefer it if it were a peer ID (by default at least).
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.
Also, this can't be optional.
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.
Implemented strict mode with a flag in the WithMessageSigning
option.
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 don't know about this, it has perfomance implications.
I'd rather use CIDs or hashes to identify messages but we need to do something.
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.
Well, ideally we would use message signing with strict mode.
I would argue that it's sane to keep the default behaviour unchanged (for a while at least), and provide the easy upgrade path of WithMessageSigning(true)
for applications that need to secure their pubsub.
Eventually we can make it the default.
I don't think we need to take reflexive action and dump our simple message id construction just yet -- note that using hashes also has some perf implications.
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 don't think we need to take reflexive action
Either is fine by me. My preference is to layer things (handle authorship at a higher layer) but it is useful to have that baked into the bottom layer.
I would argue that it's sane to keep the default behaviour unchanged (for a while at least), and provide the easy upgrade path of
WithMessageSigning(true)
for applications that need to secure their pubsub.
I'm not worried about "security", I'm worried about griefing by bored children (e.g., what happens on IRC). However, as long as we turn it on by default in go-ipfs (with strict off), I can live with it.
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.
Makes sense to enable it in go-ipfs, I'll add the relevant options when we integrate it.
@@ -452,7 +463,7 @@ func (p *PubSub) pushMsg(vals []*topicVal, src peer.ID, msg *Message) { | |||
} | |||
p.markSeen(id) | |||
|
|||
if len(vals) > 0 { | |||
if len(vals) > 0 || msg.Signature != nil { |
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.
A dumb question: Is it correct that PubSub.processLoop
only focuses on dispatching events to workers, and leave computations(like this signature validation) done by goroutines, to avoid increasing response time?
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, this is correct. Also, concurrency -- not doing this validation in parallel wastes idle cores.
@@ -89,6 +90,9 @@ type PubSub struct { | |||
peers map[peer.ID]chan *RPC | |||
seenMessages *timecache.TimeCache | |||
|
|||
// key for signing messages; nil when signing is disabled (default for now) |
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.
Does it make sense to have a flag as a field of the struct PubSub
indicating whether to verify the signatures or not and is set when the PubSub
is initialized? This way the existing code can just check this flag, instead of checking msg.Signature != nil
. Because in my imagination msg.Signature == nil
should be an invalid behavior when authentication is required, while it is valid when authentication is not enabled.
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.
The signatures are always verified when present.
The question is what happens when the signature is not present, in which case a validator can reject the message as unsigned when authentication is required.
We could have a flag that only accepts signed messages however.
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.
Ahh you're right. If the authentication is required, it is sufficient to just add the check for whether the signature is available in that case. Thank you!
rebased on master. |
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 really like this change and think it's cleanly implemented. could we maybe make this configurable behavior for now so we can do some testing in the IPFS cluster? i can't imagine it'd be a huge drain on perf relative to the usefulness it adds to the end user
sign.go
Outdated
return err | ||
} | ||
|
||
xm := pb.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.
This is fine if we're never planning on adding additional fields. Otherwise, we should probably copy the entire message, remove just the signature (and key), and remarshal that. That should keep the unknown fields.
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.
It's probably more futureproof to do it that way, although ... message copying again.
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.
It should be as efficient or more efficient than the current code. I'm suggesting:
xm := *m
xm.Signature = nil
xm.Key = nil
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.
Sure, we can do that.
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.
done.
sign.go
Outdated
|
||
m.Signature = sig | ||
switch key.Type() { | ||
case crypto.RSA: |
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.
Ah. Actually, the "correct" way to do this is to:
- Generate a peer ID.
- Try to extract the key from the peer ID (
ExtractPublicKey
).
We may (will) have other key types that don't "embed".
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.
It's also a lot more slower. Are we planning on adding other non-embeddable keys in the future?
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.
maybe that overhead is negligible in the grand scale of things though.
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.
We could add some "Embeds(key) bool` helper somewhere.
Are we planning on adding other non-embeddable keys in the future?
I don't think sekp256 keys embed and there's a PR in progress to add more general ECDSA keys that definitely won't embed.
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.
Ok, but let's define a helper function in go-libp2p-peer.
edit: or go-libp2p-crypto, but somewhere more general than 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.
for now I added an extraction test.
Agreed! I am concerned that we start entering the validation pipeline with it.
I think that spawning an (admittedly very short-lived) goroutine for each message to validate the signatures should be a bigger concern. |
m := &pb.Message{ | ||
Data: data, | ||
TopicIDs: []string{topic}, | ||
From: []byte(p.host.ID()), |
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 needs to be the ID of the key we're using to sign.
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.
indeed, good catch.
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.
it's also not a bug in the current implementation, as we don't allow arbitrary keys yet.
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 will fix it anyway, so that it's futureproof (in case we want to allow arbitrary keys in the future)
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.
done.
Per discussion with @whyrusleeping on zoom, we decided that the goroutine issue should not block merging this, but it's definitely something we want to fix. |
Follow up in #103. |
so that we don't do the goroutine dance for just a single validator, which ought to be the most common case.
rebased on master |
Implements message signing, optionally enabled with the
WithMessageSigning
option:TBD: