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

Implement message signing #97

Merged
merged 16 commits into from Oct 13, 2018
Merged

Implement message signing #97

merged 16 commits into from Oct 13, 2018

Conversation

vyzo
Copy link
Collaborator

@vyzo vyzo commented Aug 26, 2018

Implements message signing, optionally enabled with the WithMessageSigning option:

  • Adds two fields to the Message protobuf: signature and key.
  • When the option has been specified, all published messages are signed with the host key.
  • When a signature is present, it is always validated. If strict mode has been specified, then all unsigned messages are rejected without validation. Otherwise, it is up to the topic validator to reject unsigned messages.
  • RSA keys are attached on signed messages, otherwise the key is extracted from the peer ID.

TBD:

  • tests

@ghost ghost assigned vyzo Aug 26, 2018
@ghost ghost added the in progress label Aug 26, 2018
@vyzo
Copy link
Collaborator Author

vyzo commented Aug 27, 2018

added some tests.

@@ -500,22 +547,34 @@ loop:
}

if throttle {
log.Warningf("message validation throttled; dropping message from %s", src)
Copy link
Contributor

Choose a reason for hiding this comment

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

why drop the warning?

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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)
Copy link
Contributor

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

Copy link
Collaborator Author

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
Copy link
Contributor

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"

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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 {
Copy link
Contributor

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

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ok

Copy link
Collaborator Author

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{
Copy link
Contributor

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

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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.

Copy link
Member

Choose a reason for hiding this comment

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

  1. Let's not rely on slicing protobufs, if at all possible.
  2. 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.

Copy link
Contributor

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"

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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;
Copy link
Contributor

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"

Copy link
Collaborator Author

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.

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.

Copy link
Contributor

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.

Copy link

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

@whyrusleeping
Copy link
Contributor

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 {
Copy link
Contributor

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?

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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.

Copy link
Member

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).

Copy link
Member

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.

Copy link
Collaborator Author

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.

Copy link
Member

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.

Copy link
Collaborator Author

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.

Copy link
Member

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.

Copy link
Collaborator Author

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 {
Copy link

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?

Copy link
Collaborator Author

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)
Copy link

@mhchia mhchia Aug 28, 2018

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.

Copy link
Collaborator Author

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.

Copy link

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!

@vyzo
Copy link
Collaborator Author

vyzo commented Aug 28, 2018

rebased on master.

Copy link

@bigs bigs left a 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{
Copy link
Member

@Stebalien Stebalien Sep 4, 2018

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.

Copy link
Collaborator Author

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.

Copy link
Member

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

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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:
Copy link
Member

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:

  1. Generate a peer ID.
  2. Try to extract the key from the peer ID (ExtractPublicKey).

We may (will) have other key types that don't "embed".

Copy link
Collaborator Author

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?

Copy link
Collaborator Author

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.

Copy link
Member

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.

Copy link
Collaborator Author

@vyzo vyzo Sep 5, 2018

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.

Copy link
Collaborator Author

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.

@vyzo
Copy link
Collaborator Author

vyzo commented Sep 5, 2018

@whyrusleeping

My main concern now is making sure we don't shoot ourselves in the foot on perf.

Agreed! I am concerned that we start entering the validation pipeline with it.

This code as written looks pretty allocation heavy.

I think that spawning an (admittedly very short-lived) goroutine for each message to validate the signatures should be a bigger concern.
Maybe we should pre-spawn NUMCPU goroutines to handle the validation instead and feed them through a buffered channel; if the buffer fills, we should drop the message.

m := &pb.Message{
Data: data,
TopicIDs: []string{topic},
From: []byte(p.host.ID()),
Copy link
Member

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

indeed, good catch.

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done.

@vyzo
Copy link
Collaborator Author

vyzo commented Sep 6, 2018

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.
I'll make a follow up issue and pr.

@vyzo vyzo mentioned this pull request Sep 6, 2018
@vyzo
Copy link
Collaborator Author

vyzo commented Sep 6, 2018

Follow up in #103.

@vyzo
Copy link
Collaborator Author

vyzo commented Oct 13, 2018

rebased on master

@vyzo vyzo merged commit 4eb6b7c into master Oct 13, 2018
@ghost ghost removed the in progress label Oct 13, 2018
@vyzo vyzo deleted the feat/signing branch October 13, 2018 23:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants